mirror of
https://github.com/nonebot/nonebot2.git
synced 2026-05-30 23:52:37 +00:00
Compare commits
198 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6883b4192c | |||
| de8e185b53 | |||
| 585401bab8 | |||
| 1d66fc1fbd | |||
| 9ed1879ac1 | |||
| d5218b9640 | |||
| f67cb24867 | |||
| 13284d0fa1 | |||
| 57cb80bded | |||
| 01de00fe8d | |||
| 947dad7860 | |||
| a491a1ab85 | |||
| 2b77b122af | |||
| 6b1c616860 | |||
| 5d2760259c | |||
| e9edad0df0 | |||
| be68b2d27a | |||
| 1cad2070ac | |||
| cad763a998 | |||
| a1033d6467 | |||
| 8a284e5174 | |||
| 952d11607f | |||
| f552a71bae | |||
| b96f6bbf37 | |||
| f08c3c53cc | |||
| 738a193c49 | |||
| 5fee05f223 | |||
| f2306baa9b | |||
| ac0447ff86 | |||
| f120f1a305 | |||
| c011f70fd1 | |||
| 53c4a4b299 | |||
| 9b69f5d763 | |||
| c0f9a494bc | |||
| 6fef9c1d2c | |||
| 68a73ca67f | |||
| cbe6eee868 | |||
| cf8127ee4d | |||
| bf6e290ed3 | |||
| b0d2d790d6 | |||
| 1240e1e551 | |||
| 8639b1d33b | |||
| 260016e297 | |||
| e2620f0352 | |||
| 7996be1746 | |||
| 113c6be704 | |||
| dd8a9015a1 | |||
| 6aa1586ef7 | |||
| 0574813cf4 | |||
| eea5257394 | |||
| 4f1c590ee9 | |||
| 10117ce0fe | |||
| ab49151f98 | |||
| 9ebbf3ec19 | |||
| a293953c4a | |||
| 968a9d3fc7 | |||
| 141fa7947c | |||
| 50c8357bc1 | |||
| 4d402c715d | |||
| 18fad00b65 | |||
| 73f5e5d2b8 | |||
| b47280689b | |||
| 080cbca069 | |||
| 0f73f659ff | |||
| 8258a92e0a | |||
| 2ad9e40fe3 | |||
| 85d4aa5738 | |||
| 8f7aaf2b21 | |||
| 6090870244 | |||
| 441e8c5f39 | |||
| 6ebbb5a8bb | |||
| 9417d16d10 | |||
| 2c1a43b400 | |||
| 28b1ec66cd | |||
| d1f7cbaa17 | |||
| e33cee2ff0 | |||
| 26e66d72f1 | |||
| bd163dc4aa | |||
| 0121498c1a | |||
| 536f5b1404 | |||
| 911cd8b43a | |||
| f1e9971f85 | |||
| 61146d94ba | |||
| 5388aea144 | |||
| 1f3dbcb657 | |||
| b4062c574c | |||
| f428e856d3 | |||
| b882a79ea5 | |||
| 17109ca50f | |||
| 0041babc24 | |||
| 8b80f6b491 | |||
| 41af94fde4 | |||
| 8cc49a0e95 | |||
| f27b116f1c | |||
| 174e5b55ef | |||
| a331fff198 | |||
| a3ef278ff2 | |||
| d79aa5e0a2 | |||
| 63cde5da77 | |||
| f719a6b41b | |||
| 7a83fd0e27 | |||
| 346eddda06 | |||
| 7fbab3de79 | |||
| 74a2e28ed7 | |||
| 6e18b6bc08 | |||
| 2c54eef80d | |||
| 01a53ec6be | |||
| f65d2a6249 | |||
| 52bf62a632 | |||
| 9f809e7772 | |||
| 1440b1f203 | |||
| 825b6aff3c | |||
| 9200fd3bc3 | |||
| 1a7339cc34 | |||
| 7621b097e0 | |||
| 4ee0a39d3d | |||
| 5fc660a57d | |||
| 0554e4a1d5 | |||
| 40600a1b2c | |||
| 43956539c4 | |||
| 9fcdc3a374 | |||
| ff4b8a3a92 | |||
| 31bdbe97fe | |||
| 58c50dba3c | |||
| a84eb5b5b7 | |||
| e4f30b6901 | |||
| 974279e7ff | |||
| 70e321851e | |||
| e22652ffe5 | |||
| b3ce2a7463 | |||
| e02264d0b4 | |||
| 30b6b30b41 | |||
| ecedb0c7c5 | |||
| 909d30c9e9 | |||
| 1e2a44070b | |||
| 934742a373 | |||
| 38a9ec8ae0 | |||
| 72b7f6e457 | |||
| 57c362ad3c | |||
| b28770cebe | |||
| 46a99feeef | |||
| c2acb40c26 | |||
| ccec962a21 | |||
| 6e5ae611a7 | |||
| eb1bf0e214 | |||
| 9eda6dde5c | |||
| b4c6186a52 | |||
| 995fc03428 | |||
| b423b2bdf1 | |||
| 52aacc488a | |||
| e7867cb814 | |||
| 13d99ea37e | |||
| c31c70980c | |||
| e4490f15e8 | |||
| b8518e462a | |||
| d087a3289d | |||
| dbc1965024 | |||
| 44ff1128dc | |||
| 768dd2db9a | |||
| 921ea28a34 | |||
| 701e65b9b7 | |||
| 7c3330b47f | |||
| 76fab0e5e3 | |||
| e9c1898225 | |||
| ed83c285ca | |||
| ba1ef67dc8 | |||
| 362e803add | |||
| ea61bf512b | |||
| 125095f971 | |||
| 358e574d3e | |||
| 30e605a8fa | |||
| b29f2eace9 | |||
| 24a3d62358 | |||
| 70219965f2 | |||
| bad6d237db | |||
| 07ef0b7ab4 | |||
| 9a73132941 | |||
| 1079cf366f | |||
| 644c785b03 | |||
| 94f88014aa | |||
| 2cf1b04c73 | |||
| 0bedb4663e | |||
| a9058bf685 | |||
| 2b601ec0e5 | |||
| 7da74b98fd | |||
| 7450315b97 | |||
| ccb04ea9a0 | |||
| a295bb7c0d | |||
| 3454c716a9 | |||
| f7cd888df2 | |||
| 8363f8c061 | |||
| 8ebda5758a | |||
| 0f21ff4a90 | |||
| 2d543f45b5 | |||
| 37863cc665 | |||
| d04558a4cb | |||
| 4e3cf55955 | |||
| 08f7af05b6 |
@@ -3,10 +3,10 @@
|
|||||||
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
|
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
|
||||||
"features": {
|
"features": {
|
||||||
"ghcr.io/jsburckhardt/devcontainer-features/uv:1": {},
|
"ghcr.io/jsburckhardt/devcontainer-features/uv:1": {},
|
||||||
"ghcr.io/devcontainers/features/node:1": {},
|
"ghcr.io/devcontainers/features/node:2": {},
|
||||||
"ghcr.io/meaningful-ooo/devcontainer-features/fish:2": {}
|
"ghcr.io/meaningful-ooo/devcontainer-features/fish:2": {}
|
||||||
},
|
},
|
||||||
"postCreateCommand": "./scripts/setup-envs.sh",
|
"postCreateCommand": "corepack enable && COREPACK_ENABLE_DOWNLOAD_PROMPT=0 ./scripts/setup-envs.sh",
|
||||||
"customizations": {
|
"customizations": {
|
||||||
"vscode": {
|
"vscode": {
|
||||||
"settings": {
|
"settings": {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
||||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||||
env: [pydantic-v1, pydantic-v2]
|
env: [pydantic-v1, pydantic-v2]
|
||||||
env:
|
env:
|
||||||
@@ -34,7 +34,7 @@ jobs:
|
|||||||
PYDANTIC_VERSION: ${{ matrix.env }}
|
PYDANTIC_VERSION: ${{ matrix.env }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Setup Python environment
|
- name: Setup Python environment
|
||||||
uses: ./.github/actions/setup-python
|
uses: ./.github/actions/setup-python
|
||||||
@@ -56,7 +56,7 @@ jobs:
|
|||||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
|
||||||
- name: Upload coverage report
|
- name: Upload coverage report
|
||||||
uses: codecov/codecov-action@v5
|
uses: codecov/codecov-action@v6
|
||||||
with:
|
with:
|
||||||
env_vars: OS,PYTHON_VERSION,PYDANTIC_VERSION
|
env_vars: OS,PYTHON_VERSION,PYDANTIC_VERSION
|
||||||
files: ./tests/coverage.xml
|
files: ./tests/coverage.xml
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ jobs:
|
|||||||
private_key: ${{ secrets.APP_KEY }}
|
private_key: ${{ secrets.APP_KEY }}
|
||||||
|
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.generate-token.outputs.token }}
|
token: ${{ steps.generate-token.outputs.token }}
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ jobs:
|
|||||||
PRIVATE_KEY: ${{ secrets.APP_KEY }}
|
PRIVATE_KEY: ${{ secrets.APP_KEY }}
|
||||||
|
|
||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: noneflow
|
name: noneflow
|
||||||
path: artifact/*
|
path: artifact/*
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Setup Python environment
|
- name: Setup Python environment
|
||||||
uses: ./.github/actions/setup-python
|
uses: ./.github/actions/setup-python
|
||||||
@@ -43,6 +43,6 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Run Pyright
|
- name: Run Pyright
|
||||||
uses: jakebailey/pyright-action@v2
|
uses: jakebailey/pyright-action@v3
|
||||||
with:
|
with:
|
||||||
pylance-version: latest-release
|
pylance-version: latest-release
|
||||||
|
|||||||
@@ -2,17 +2,14 @@ name: Release Drafter
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
|
||||||
- v*
|
|
||||||
pull_request_target:
|
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
types:
|
tags:
|
||||||
- closed
|
- v*
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
update-release-draft:
|
update-release-draft:
|
||||||
if: github.event_name == 'pull_request_target'
|
if: github.ref == 'refs/heads/master'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
concurrency:
|
concurrency:
|
||||||
group: pull-request-changelog
|
group: pull-request-changelog
|
||||||
@@ -25,7 +22,7 @@ jobs:
|
|||||||
app_id: ${{ secrets.APP_ID }}
|
app_id: ${{ secrets.APP_ID }}
|
||||||
private_key: ${{ secrets.APP_KEY }}
|
private_key: ${{ secrets.APP_KEY }}
|
||||||
|
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.generate-token.outputs.token }}
|
token: ${{ steps.generate-token.outputs.token }}
|
||||||
|
|
||||||
@@ -70,7 +67,7 @@ jobs:
|
|||||||
app_id: ${{ secrets.APP_ID }}
|
app_id: ${{ secrets.APP_ID }}
|
||||||
private_key: ${{ secrets.APP_KEY }}
|
private_key: ${{ secrets.APP_KEY }}
|
||||||
|
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Setup Python Environment
|
- name: Setup Python Environment
|
||||||
uses: ./.github/actions/setup-python
|
uses: ./.github/actions/setup-python
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
app_id: ${{ secrets.APP_ID }}
|
app_id: ${{ secrets.APP_ID }}
|
||||||
private_key: ${{ secrets.APP_KEY }}
|
private_key: ${{ secrets.APP_KEY }}
|
||||||
|
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.generate-token.outputs.token }}
|
token: ${{ steps.generate-token.outputs.token }}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Run Ruff Lint
|
- name: Run Ruff Lint
|
||||||
uses: astral-sh/ruff-action@v3
|
uses: astral-sh/ruff-action@v3
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ jobs:
|
|||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Set Commit Status
|
- name: Set Commit Status
|
||||||
uses: actions/github-script@v8
|
uses: actions/github-script@v9
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
github.rest.repos.createCommitStatus({
|
github.rest.repos.createCommitStatus({
|
||||||
@@ -37,7 +37,7 @@ jobs:
|
|||||||
})
|
})
|
||||||
|
|
||||||
- name: Download Artifact
|
- name: Download Artifact
|
||||||
uses: actions/download-artifact@v6
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: website-preview
|
name: website-preview
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -70,7 +70,7 @@ jobs:
|
|||||||
|
|
||||||
# action netlify has no pull request context, so we need to comment by ourselves
|
# action netlify has no pull request context, so we need to comment by ourselves
|
||||||
- name: Comment on Pull Request
|
- name: Comment on Pull Request
|
||||||
uses: marocchino/sticky-pull-request-comment@v2
|
uses: marocchino/sticky-pull-request-comment@v3
|
||||||
with:
|
with:
|
||||||
header: website
|
header: website
|
||||||
number: ${{ env.PR_NUMBER }}
|
number: ${{ env.PR_NUMBER }}
|
||||||
@@ -78,7 +78,7 @@ jobs:
|
|||||||
:rocket: Deployed to ${{ steps.deploy.outputs.deploy-url }}
|
:rocket: Deployed to ${{ steps.deploy.outputs.deploy-url }}
|
||||||
|
|
||||||
- name: Set Commit Status
|
- name: Set Commit Status
|
||||||
uses: actions/github-script@v8
|
uses: actions/github-script@v9
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ jobs:
|
|||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
echo "${{ github.event.pull_request.number }}" > ./pr-number
|
echo "${{ github.event.pull_request.number }}" > ./pr-number
|
||||||
|
|
||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: website-preview
|
name: website-preview
|
||||||
path: |
|
path: |
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ ci:
|
|||||||
autoupdate_commit_msg: ":arrow_up: auto update by pre-commit hooks"
|
autoupdate_commit_msg: ":arrow_up: auto update by pre-commit hooks"
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.14.0
|
rev: v0.15.9
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff-check
|
- id: ruff-check
|
||||||
args: [--fix]
|
args: [--fix]
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
.github/**/*.md
|
.github/**/*.md
|
||||||
website/docs/tutorial/application.mdx
|
website/docs/tutorial/application.mdx
|
||||||
|
website/versioned_docs/*/tutorial/application.mdx
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ _✨ 跨平台 Python 异步机器人框架 ✨_
|
|||||||
<a href="https://pypi.python.org/pypi/nonebot2">
|
<a href="https://pypi.python.org/pypi/nonebot2">
|
||||||
<img src="https://img.shields.io/pypi/v/nonebot2?logo=python&logoColor=edb641" alt="pypi">
|
<img src="https://img.shields.io/pypi/v/nonebot2?logo=python&logoColor=edb641" alt="pypi">
|
||||||
</a>
|
</a>
|
||||||
<img src="https://img.shields.io/badge/python-3.9+-blue?logo=python&logoColor=edb641" alt="python">
|
<img src="https://img.shields.io/badge/python-3.10+-blue?logo=python&logoColor=edb641" alt="python">
|
||||||
<a href="https://github.com/psf/black">
|
<a href="https://github.com/psf/black">
|
||||||
<img src="https://img.shields.io/badge/code%20style-black-000000.svg?logo=python&logoColor=edb641" alt="black">
|
<img src="https://img.shields.io/badge/code%20style-black-000000.svg?logo=python&logoColor=edb641" alt="black">
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -338,4 +338,49 @@
|
|||||||
],
|
],
|
||||||
"is_official": false
|
"is_official": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot.adapters.yunhu",
|
||||||
|
"project_link": "nonebot-adapter-yunhu",
|
||||||
|
"name": "云湖适配器",
|
||||||
|
"desc": "云湖的NoneBot适配器",
|
||||||
|
"author_id": 104612722,
|
||||||
|
"homepage": "https://github.com/molanp/nonebot-adapter-yunhu",
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "云湖",
|
||||||
|
"color": "#8a74eb"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot.adapters.wxclaw",
|
||||||
|
"project_link": "nonebot-adapter-wxclaw",
|
||||||
|
"name": "WxClaw",
|
||||||
|
"desc": "基于 openclaw-weixin 协议的 NoneBot2 微信智能体适配器",
|
||||||
|
"author_id": 51957264,
|
||||||
|
"homepage": "https://github.com/shoucandanghehe/nonebot-adapter-wxclaw",
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "微信",
|
||||||
|
"color": "#1aad19"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot.adapters.wechatclaw",
|
||||||
|
"project_link": "nonebot-adapter-wechatclaw",
|
||||||
|
"name": "WeChatClaw",
|
||||||
|
"desc": "通过 微信ClawBot 连接微信",
|
||||||
|
"author_id": 99632363,
|
||||||
|
"homepage": "https://github.com/Decrabbityyy/adapter-wechatclaw",
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "微信",
|
||||||
|
"color": "#ea5252"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -764,4 +764,17 @@
|
|||||||
],
|
],
|
||||||
"is_official": false
|
"is_official": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Rosmontis.io",
|
||||||
|
"desc": "简单的机器人",
|
||||||
|
"author_id": 225668725,
|
||||||
|
"homepage": "https://github.com/com-wuqi/Rosmontis.io",
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "可爱",
|
||||||
|
"color": "#ea5252"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
+915
-14
@@ -8250,13 +8250,6 @@
|
|||||||
],
|
],
|
||||||
"is_official": false
|
"is_official": false
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"module_name": "nonebot_plugin_ACMD",
|
|
||||||
"project_link": "nonebot_plugin_ACMD",
|
|
||||||
"author_id": 78413699,
|
|
||||||
"tags": [],
|
|
||||||
"is_official": false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"module_name": "nonebot_plugin_suggarchat",
|
"module_name": "nonebot_plugin_suggarchat",
|
||||||
"project_link": "nonebot-plugin-suggarchat",
|
"project_link": "nonebot-plugin-suggarchat",
|
||||||
@@ -8304,13 +8297,6 @@
|
|||||||
],
|
],
|
||||||
"is_official": false
|
"is_official": false
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"module_name": "nonebot_plugin_group_config",
|
|
||||||
"project_link": "nonebot-plugin-group-config",
|
|
||||||
"author_id": 157892771,
|
|
||||||
"tags": [],
|
|
||||||
"is_official": false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"module_name": "nonebot_plugin_luoguluck",
|
"module_name": "nonebot_plugin_luoguluck",
|
||||||
"project_link": "nonebot-plugin-luoguluck",
|
"project_link": "nonebot-plugin-luoguluck",
|
||||||
@@ -8751,6 +8737,10 @@
|
|||||||
{
|
{
|
||||||
"label": "明日方舟",
|
"label": "明日方舟",
|
||||||
"color": "#67fdfe"
|
"color": "#67fdfe"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "终末地",
|
||||||
|
"color": "#e3d600"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"is_official": false
|
"is_official": false
|
||||||
@@ -9887,4 +9877,915 @@
|
|||||||
],
|
],
|
||||||
"is_official": false
|
"is_official": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_bili_helper",
|
||||||
|
"project_link": "nonebot-plugin-bili-helper",
|
||||||
|
"author_id": 4216470,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "bilibili",
|
||||||
|
"color": "#ea5252"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "B站",
|
||||||
|
"color": "#ea5252"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_markdown2img",
|
||||||
|
"project_link": "nonebot-plugin-markdown2img",
|
||||||
|
"author_id": 96008766,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "markdown",
|
||||||
|
"color": "#ea5252"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "func",
|
||||||
|
"color": "#0fe16e"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_jrrp3",
|
||||||
|
"project_link": "nonebot-plugin-jrrp3",
|
||||||
|
"author_id": 79314033,
|
||||||
|
"tags": [],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_image_symmetry",
|
||||||
|
"project_link": "nonebot-plugin-image-symmetry",
|
||||||
|
"author_id": 79314033,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "image",
|
||||||
|
"color": "#ea5252"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_manosaba_memes",
|
||||||
|
"project_link": "nonebot-plugin-manosaba-memes",
|
||||||
|
"author_id": 55650833,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "魔法少女的魔法审判",
|
||||||
|
"color": "#de7d92"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_course_schedule",
|
||||||
|
"project_link": "nonebot-plugin-course-schedule",
|
||||||
|
"author_id": 49135577,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "课表",
|
||||||
|
"color": "#ea5252"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_ipinfo",
|
||||||
|
"project_link": "nonebot-plugin-ipinfo",
|
||||||
|
"author_id": 122811297,
|
||||||
|
"tags": [],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_templates_draw",
|
||||||
|
"project_link": "nonebot-plugin-templates-draw",
|
||||||
|
"author_id": 99017826,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "AI画图",
|
||||||
|
"color": "#ea5252"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_manosoba_reply_generator",
|
||||||
|
"project_link": "nonebot-plugin-manosoba-reply-generator",
|
||||||
|
"author_id": 111682952,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "func",
|
||||||
|
"color": "#ea5252"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_anans_sketchbook",
|
||||||
|
"project_link": "nonebot-plugin-anans-sketchbook",
|
||||||
|
"author_id": 64982342,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "魔法少女的魔女审判",
|
||||||
|
"color": "#d07e82"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "夏目安安",
|
||||||
|
"color": "#6a5acd"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "表情包",
|
||||||
|
"color": "#ca64dc"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_anan_say",
|
||||||
|
"project_link": "nonebot-plugin-anan-say",
|
||||||
|
"author_id": 122149478,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "魔女审判",
|
||||||
|
"color": "#b10ba0"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_terralink",
|
||||||
|
"project_link": "nonebot-plugin-terralink",
|
||||||
|
"author_id": 96228495,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "泰拉瑞亚",
|
||||||
|
"color": "#b6e161"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "群服互通",
|
||||||
|
"color": "#669ed7"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Terraria",
|
||||||
|
"color": "#df6262"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_railwaytools",
|
||||||
|
"project_link": "nonebot-plugin-railwaytools",
|
||||||
|
"author_id": 51502183,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "中国铁路",
|
||||||
|
"color": "#5287ea"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_omikuji",
|
||||||
|
"project_link": "nonebot-plugin-omikuji",
|
||||||
|
"author_id": 67693593,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "御神签",
|
||||||
|
"color": "#ea5252"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "运势",
|
||||||
|
"color": "#0bc2af"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "占卜",
|
||||||
|
"color": "#811eee"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_rollpig",
|
||||||
|
"project_link": "nonebot-plugin-rollpig",
|
||||||
|
"author_id": 30120610,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "pig",
|
||||||
|
"color": "#fdd7e4"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_instagram",
|
||||||
|
"project_link": "nonebot-plugin-instagram",
|
||||||
|
"author_id": 49683326,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "instagram",
|
||||||
|
"color": "#ea5252"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_mcserver_status_check",
|
||||||
|
"project_link": "nonebot-plugin-mcserver-status-check",
|
||||||
|
"author_id": 153894603,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "Minecraft",
|
||||||
|
"color": "#ea5252"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "MC",
|
||||||
|
"color": "#ea5252"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_perithacus",
|
||||||
|
"project_link": "nonebot-plugin-pErithacus",
|
||||||
|
"author_id": 21017431,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "chat",
|
||||||
|
"color": "#ea5252"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "自动回复",
|
||||||
|
"color": "#ea5252"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_quickreply",
|
||||||
|
"project_link": "nonebot-plugin-quickreply",
|
||||||
|
"author_id": 104259619,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "工具",
|
||||||
|
"color": "#4de024"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_jimeng",
|
||||||
|
"project_link": "nonebot-plugin-jimeng",
|
||||||
|
"author_id": 104259619,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "OpenAi",
|
||||||
|
"color": "#1d7374"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "绘画",
|
||||||
|
"color": "#8b1bb4"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_boardgamehelper",
|
||||||
|
"project_link": "nonebot-plugin-boardgamehelper",
|
||||||
|
"author_id": 60382099,
|
||||||
|
"tags": [],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_ai_groupmate",
|
||||||
|
"project_link": "nonebot-plugin-ai-groupmate",
|
||||||
|
"author_id": 30517062,
|
||||||
|
"tags": [],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_trans_progress",
|
||||||
|
"project_link": "nonebot-plugin-trans-progress",
|
||||||
|
"author_id": 99017826,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "汉化",
|
||||||
|
"color": "#ea5252"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_word_censor",
|
||||||
|
"project_link": "nonebot-plugin-word-censor",
|
||||||
|
"author_id": 99163726,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "rule",
|
||||||
|
"color": "#ea5252"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "黑名单",
|
||||||
|
"color": "#000000"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_internet_outage",
|
||||||
|
"project_link": "nonebot-plugin-internet-outage",
|
||||||
|
"author_id": 110646806,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "Cloudflare",
|
||||||
|
"color": "#f97316"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "网络中断监测",
|
||||||
|
"color": "#dc2626"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_group_relay",
|
||||||
|
"project_link": "nonebot-plugin-group-relay",
|
||||||
|
"author_id": 86188856,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "func",
|
||||||
|
"color": "#ea5252"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_tavily",
|
||||||
|
"project_link": "nonebot-plugin-tavily",
|
||||||
|
"author_id": 1080807,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "search",
|
||||||
|
"color": "#39c5bb"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_bf6_stats",
|
||||||
|
"project_link": "nonebot-plugin-bf6-stats",
|
||||||
|
"author_id": 202926395,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "bf6",
|
||||||
|
"color": "#ea5252"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_bili2mp4",
|
||||||
|
"project_link": "nonebot-plugin-bili2mp4",
|
||||||
|
"author_id": 181480818,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "bilibili",
|
||||||
|
"color": "#ff6699"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "mp4",
|
||||||
|
"color": "#ff6699"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "小程序",
|
||||||
|
"color": "#ff6699"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_qqmusic_reco",
|
||||||
|
"project_link": "nonebot-plugin-qqmusic-reco",
|
||||||
|
"author_id": 99163726,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "music",
|
||||||
|
"color": "#ea5252"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "recommend",
|
||||||
|
"color": "#beeb0c"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_osugreek",
|
||||||
|
"project_link": "nonebot-plugin-osugreek",
|
||||||
|
"author_id": 64720173,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "osu",
|
||||||
|
"color": "#ff66aa"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_uniconf",
|
||||||
|
"project_link": "nonebot-plugin-uniconf",
|
||||||
|
"author_id": 67693593,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "config",
|
||||||
|
"color": "#0cccff"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "配置文件",
|
||||||
|
"color": "#c3ff0c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "API",
|
||||||
|
"color": "#ffff00"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_dice_helper",
|
||||||
|
"project_link": "nonebot-plugin-dice-helper",
|
||||||
|
"author_id": 30589360,
|
||||||
|
"tags": [],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_mcpclient",
|
||||||
|
"project_link": "nonebot-plugin-mcpclient",
|
||||||
|
"author_id": 1080807,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "mcp",
|
||||||
|
"color": "#243576"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_BitTorrents",
|
||||||
|
"project_link": "nonebot-plugin-bittorrents",
|
||||||
|
"author_id": 98020024,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "func",
|
||||||
|
"color": "#ea5252"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_personification",
|
||||||
|
"project_link": "nonebot-plugin-shiro-personification",
|
||||||
|
"author_id": 114895516,
|
||||||
|
"tags": [],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_shiro_web_console",
|
||||||
|
"project_link": "nonebot-plugin-shiro-web-console",
|
||||||
|
"author_id": 114895516,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "WebUi",
|
||||||
|
"color": "#ea5252"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_nodejsphira",
|
||||||
|
"project_link": "nonebot-plugin-nodejsphira",
|
||||||
|
"author_id": 75435667,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "server",
|
||||||
|
"color": "#ea5252"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_doroending",
|
||||||
|
"project_link": "nonebot-plugin-doroending",
|
||||||
|
"author_id": 127853582,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "doro",
|
||||||
|
"color": "#ea5252"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_auto_emojimix",
|
||||||
|
"project_link": "nonebot-plugin-auto-emojimix",
|
||||||
|
"author_id": 74812967,
|
||||||
|
"tags": [],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_peek",
|
||||||
|
"project_link": "nonebot-plugin-peek",
|
||||||
|
"author_id": 74812967,
|
||||||
|
"tags": [],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_rikka",
|
||||||
|
"project_link": "nonebot-plugin-rikka",
|
||||||
|
"author_id": 72406624,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "maimai",
|
||||||
|
"color": "#60bacf"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_cardimg",
|
||||||
|
"project_link": "nonebot-plugin-cardimg",
|
||||||
|
"author_id": 99666950,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "模板渲染",
|
||||||
|
"color": "#dec7ef"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "图片生成",
|
||||||
|
"color": "#f8d8cf"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "HTML渲染",
|
||||||
|
"color": "#c2dff2"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_mute_cat",
|
||||||
|
"project_link": "nonebot-plugin-mute-cat",
|
||||||
|
"author_id": 199351962,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "工具",
|
||||||
|
"color": "#ea5252"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "群管理",
|
||||||
|
"color": "#52eacf"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "禁言",
|
||||||
|
"color": "#830daa"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot-plugin-trumpwatcher",
|
||||||
|
"project_link": "nonebot-plugin-trumpwatcher",
|
||||||
|
"author_id": 42519315,
|
||||||
|
"tags": [],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_mc_whitelist_controller",
|
||||||
|
"project_link": "nonebot-plugin-mc-whitelist-controller",
|
||||||
|
"author_id": 51502183,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "Minecraft",
|
||||||
|
"color": "#ea5252"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "MC",
|
||||||
|
"color": "#ea5252"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "server",
|
||||||
|
"color": "#ea5252"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_nbnhhsh",
|
||||||
|
"project_link": "nonebot-plugin-nbnhhsh",
|
||||||
|
"author_id": 24908800,
|
||||||
|
"tags": [],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_codex",
|
||||||
|
"project_link": "nonebot-plugin-codex",
|
||||||
|
"author_id": 98325911,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "OpenAI",
|
||||||
|
"color": "#ea5252"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Codex",
|
||||||
|
"color": "#6fd8fc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "VibeCoding",
|
||||||
|
"color": "#74fc6f"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_sentry_transaction",
|
||||||
|
"project_link": "nonebot-plugin-sentry-transaction",
|
||||||
|
"author_id": 51957264,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "sentry",
|
||||||
|
"color": "#ea5252"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "hook",
|
||||||
|
"color": "#3fb568"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_calcgame_new",
|
||||||
|
"project_link": "nonebot-plugin-calcgame-new",
|
||||||
|
"author_id": 143202058,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "game",
|
||||||
|
"color": "#ea5252"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_qwqa",
|
||||||
|
"project_link": "nonebot-plugin-qwqa",
|
||||||
|
"author_id": 114509415,
|
||||||
|
"tags": [],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_appstore_gameranking",
|
||||||
|
"project_link": "nonebot-plugin-appstore-gameranking",
|
||||||
|
"author_id": 47425055,
|
||||||
|
"tags": [],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "endfield",
|
||||||
|
"project_link": "nonebot-plugin-endfield",
|
||||||
|
"author_id": 96970949,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "终末地",
|
||||||
|
"color": "#ffd700"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_maimaimonitor",
|
||||||
|
"project_link": "nonebot-plugin-maimaimonitor",
|
||||||
|
"author_id": 141206905,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "舞萌",
|
||||||
|
"color": "#ea5252"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "maimai",
|
||||||
|
"color": "#ea5252"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "服务器状态",
|
||||||
|
"color": "#ea5252"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_cs2radar",
|
||||||
|
"project_link": "nonebot-plugin-cs2radar",
|
||||||
|
"author_id": 114895516,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "反恐精英",
|
||||||
|
"color": "#ea5252"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "onebot_plugin_ts3_tracker",
|
||||||
|
"project_link": "nonebot-plugin-ts3-tracker",
|
||||||
|
"author_id": 219686092,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "TS3",
|
||||||
|
"color": "#ea5252"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "teamspeak",
|
||||||
|
"color": "#ea5252"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "语音",
|
||||||
|
"color": "#ea5252"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_session_config",
|
||||||
|
"project_link": "nonebot-plugin-session-config",
|
||||||
|
"author_id": 157892771,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "config",
|
||||||
|
"color": "#ea5252"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_12306_ticket",
|
||||||
|
"project_link": "nonebot-plugin-12306-ticket",
|
||||||
|
"author_id": 51502183,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "中国铁路",
|
||||||
|
"color": "#5257ea"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "12306",
|
||||||
|
"color": "#5257ea"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_tg_stickers_downloads",
|
||||||
|
"project_link": "nonebot-plugin-tg-stickers-downloads",
|
||||||
|
"author_id": 122811297,
|
||||||
|
"tags": [],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_bot_monitor",
|
||||||
|
"project_link": "nonebot-plugin-bot-monitor",
|
||||||
|
"author_id": 62124595,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "monitor",
|
||||||
|
"color": "#ea5252"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_dingtalk_logger",
|
||||||
|
"project_link": "nonebot-plugin-dingtalk-logger",
|
||||||
|
"author_id": 41439182,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "DingTalk",
|
||||||
|
"color": "#007fff"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_arkguesser",
|
||||||
|
"project_link": "nonebot-plugin-arkguesser",
|
||||||
|
"author_id": 225500959,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "明日方舟 ",
|
||||||
|
"color": "#ea5252"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "猜谜",
|
||||||
|
"color": "#ea5252"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_quotation",
|
||||||
|
"project_link": "nonebot-plugin-quotation",
|
||||||
|
"author_id": 41883458,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "语录",
|
||||||
|
"color": "#2aa7dd"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_dancecube",
|
||||||
|
"project_link": "nonebot-plugin-dancecube",
|
||||||
|
"author_id": 25610914,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "音游",
|
||||||
|
"color": "#ea5252"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "舞立方",
|
||||||
|
"color": "#377aba"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_suda_electricity",
|
||||||
|
"project_link": "nonebot_plugin_suda_electricity",
|
||||||
|
"author_id": 78413699,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "苏州大学",
|
||||||
|
"color": "#e13667"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "电费",
|
||||||
|
"color": "#243b83"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_amrita",
|
||||||
|
"project_link": "nonebot-plugin-amrita",
|
||||||
|
"author_id": 67693593,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "agent",
|
||||||
|
"color": "#ea5252"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "ai",
|
||||||
|
"color": "#ea5252"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "LLM",
|
||||||
|
"color": "#1313e6"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_osumania_toolkit",
|
||||||
|
"project_link": "nonebot-plugin-osumania-toolkit",
|
||||||
|
"author_id": 95923342,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "osu!mania",
|
||||||
|
"color": "#ea5252"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "func",
|
||||||
|
"color": "#ea5252"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "tool",
|
||||||
|
"color": "#ea5252"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_name": "nonebot_plugin_kuwo",
|
||||||
|
"project_link": "nonebot-plugin-kuwo",
|
||||||
|
"author_id": 144674902,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"label": "music",
|
||||||
|
"color": "#52e7ea"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "kuwo",
|
||||||
|
"color": "#ddf106"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_official": false
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
+5
-5
@@ -47,7 +47,7 @@ FrontMatter:
|
|||||||
|
|
||||||
from importlib.metadata import version
|
from importlib.metadata import version
|
||||||
import os
|
import os
|
||||||
from typing import Any, Optional, TypeVar, Union, overload
|
from typing import Any, TypeVar, overload
|
||||||
|
|
||||||
import loguru
|
import loguru
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ except Exception: # pragma: no cover
|
|||||||
|
|
||||||
A = TypeVar("A", bound=Adapter)
|
A = TypeVar("A", bound=Adapter)
|
||||||
|
|
||||||
_driver: Optional[Driver] = None
|
_driver: Driver | None = None
|
||||||
|
|
||||||
|
|
||||||
def get_driver() -> Driver:
|
def get_driver() -> Driver:
|
||||||
@@ -112,7 +112,7 @@ def get_adapter(name: type[A]) -> A:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def get_adapter(name: Union[str, type[Adapter]]) -> Adapter:
|
def get_adapter(name: str | type[Adapter]) -> Adapter:
|
||||||
"""获取已注册的 {ref}`nonebot.adapters.Adapter` 实例。
|
"""获取已注册的 {ref}`nonebot.adapters.Adapter` 实例。
|
||||||
|
|
||||||
异常:
|
异常:
|
||||||
@@ -196,7 +196,7 @@ def get_asgi() -> Any:
|
|||||||
return driver.asgi
|
return driver.asgi
|
||||||
|
|
||||||
|
|
||||||
def get_bot(self_id: Optional[str] = None) -> Bot:
|
def get_bot(self_id: str | None = None) -> Bot:
|
||||||
"""获取一个连接到 NoneBot 的 {ref}`nonebot.adapters.Bot` 对象。
|
"""获取一个连接到 NoneBot 的 {ref}`nonebot.adapters.Bot` 对象。
|
||||||
|
|
||||||
当提供 `self_id` 时,此函数是 `get_bots()[self_id]` 的简写;
|
当提供 `self_id` 时,此函数是 `get_bots()[self_id]` 的简写;
|
||||||
@@ -277,7 +277,7 @@ def _log_patcher(record: "loguru.Record"):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def init(*, _env_file: Optional[DOTENV_TYPE] = None, **kwargs: Any) -> None:
|
def init(*, _env_file: DOTENV_TYPE | None = None, **kwargs: Any) -> None:
|
||||||
"""初始化 NoneBot 以及 全局 {ref}`nonebot.drivers.Driver` 对象。
|
"""初始化 NoneBot 以及 全局 {ref}`nonebot.drivers.Driver` 对象。
|
||||||
|
|
||||||
NoneBot 将会从 .env 文件中读取环境信息,并使用相应的 env 文件配置。
|
NoneBot 将会从 .env 文件中读取环境信息,并使用相应的 env 文件配置。
|
||||||
|
|||||||
+38
-26
@@ -9,23 +9,24 @@ FrontMatter:
|
|||||||
description: nonebot.compat 模块
|
description: nonebot.compat 模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from collections.abc import Generator
|
from collections.abc import Callable, Generator
|
||||||
from dataclasses import dataclass, is_dataclass
|
from dataclasses import dataclass, is_dataclass
|
||||||
from functools import cached_property, wraps
|
from functools import cached_property, wraps
|
||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
Annotated,
|
Annotated,
|
||||||
Any,
|
Any,
|
||||||
Callable,
|
|
||||||
Generic,
|
Generic,
|
||||||
Literal,
|
Literal,
|
||||||
Optional,
|
|
||||||
Protocol,
|
Protocol,
|
||||||
|
TypeAlias,
|
||||||
TypeVar,
|
TypeVar,
|
||||||
Union,
|
cast,
|
||||||
|
get_args,
|
||||||
|
get_origin,
|
||||||
overload,
|
overload,
|
||||||
)
|
)
|
||||||
from typing_extensions import ParamSpec, Self, get_args, get_origin, is_typeddict
|
from typing_extensions import ParamSpec, Self, is_typeddict
|
||||||
|
|
||||||
from pydantic import VERSION, BaseModel
|
from pydantic import VERSION, BaseModel
|
||||||
|
|
||||||
@@ -45,6 +46,16 @@ if TYPE_CHECKING:
|
|||||||
CVC = TypeVar("CVC", bound=_CustomValidationClass)
|
CVC = TypeVar("CVC", bound=_CustomValidationClass)
|
||||||
|
|
||||||
|
|
||||||
|
ModelDumpIncEx: TypeAlias = (
|
||||||
|
set[int]
|
||||||
|
| set[str]
|
||||||
|
| dict[int, "ModelDumpIncEx"]
|
||||||
|
| dict[str, "ModelDumpIncEx"]
|
||||||
|
| None
|
||||||
|
)
|
||||||
|
"""Common include/exclude shape accepted by all supported pydantic versions."""
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
"DEFAULT_CONFIG",
|
"DEFAULT_CONFIG",
|
||||||
"PYDANTIC_V2",
|
"PYDANTIC_V2",
|
||||||
@@ -129,7 +140,7 @@ if PYDANTIC_V2: # pragma: pydantic-v2
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _inherit_construct(
|
def _inherit_construct(
|
||||||
cls, field_info: Optional[BaseFieldInfo] = None, **kwargs: Any
|
cls, field_info: BaseFieldInfo | None = None, **kwargs: Any
|
||||||
) -> Self:
|
) -> Self:
|
||||||
init_kwargs = {}
|
init_kwargs = {}
|
||||||
if field_info:
|
if field_info:
|
||||||
@@ -158,7 +169,7 @@ if PYDANTIC_V2: # pragma: pydantic-v2
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def construct(
|
def construct(
|
||||||
cls, name: str, annotation: Any, field_info: Optional[FieldInfo] = None
|
cls, name: str, annotation: Any, field_info: FieldInfo | None = None
|
||||||
) -> Self:
|
) -> Self:
|
||||||
"""Construct a ModelField from given infos."""
|
"""Construct a ModelField from given infos."""
|
||||||
return cls._construct(name, annotation, field_info or FieldInfo())
|
return cls._construct(name, annotation, field_info or FieldInfo())
|
||||||
@@ -231,16 +242,17 @@ if PYDANTIC_V2: # pragma: pydantic-v2
|
|||||||
|
|
||||||
def model_dump(
|
def model_dump(
|
||||||
model: BaseModel,
|
model: BaseModel,
|
||||||
include: Optional[set[str]] = None,
|
include: ModelDumpIncEx = None,
|
||||||
exclude: Optional[set[str]] = None,
|
exclude: ModelDumpIncEx = None,
|
||||||
by_alias: bool = False,
|
by_alias: bool = False,
|
||||||
exclude_unset: bool = False,
|
exclude_unset: bool = False,
|
||||||
exclude_defaults: bool = False,
|
exclude_defaults: bool = False,
|
||||||
exclude_none: bool = False,
|
exclude_none: bool = False,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
return model.model_dump(
|
return model.model_dump(
|
||||||
include=include,
|
# Nested types cannot be inferred correctly
|
||||||
exclude=exclude,
|
include=cast(Any, include),
|
||||||
|
exclude=cast(Any, exclude),
|
||||||
by_alias=by_alias,
|
by_alias=by_alias,
|
||||||
exclude_unset=exclude_unset,
|
exclude_unset=exclude_unset,
|
||||||
exclude_defaults=exclude_defaults,
|
exclude_defaults=exclude_defaults,
|
||||||
@@ -251,7 +263,7 @@ if PYDANTIC_V2: # pragma: pydantic-v2
|
|||||||
"""Validate data with given type."""
|
"""Validate data with given type."""
|
||||||
return TypeAdapter(type_).validate_python(data)
|
return TypeAdapter(type_).validate_python(data)
|
||||||
|
|
||||||
def type_validate_json(type_: type[T], data: Union[str, bytes]) -> T:
|
def type_validate_json(type_: type[T], data: str | bytes) -> T:
|
||||||
"""Validate JSON with given type."""
|
"""Validate JSON with given type."""
|
||||||
return TypeAdapter(type_).validate_json(data)
|
return TypeAdapter(type_).validate_json(data)
|
||||||
|
|
||||||
@@ -317,7 +329,7 @@ else: # pragma: pydantic-v1
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _inherit_construct(
|
def _inherit_construct(
|
||||||
cls, field_info: Optional[BaseFieldInfo] = None, **kwargs: Any
|
cls, field_info: BaseFieldInfo | None = None, **kwargs: Any
|
||||||
):
|
):
|
||||||
if field_info:
|
if field_info:
|
||||||
init_kwargs = {
|
init_kwargs = {
|
||||||
@@ -350,7 +362,7 @@ else: # pragma: pydantic-v1
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def construct(
|
def construct(
|
||||||
cls, name: str, annotation: Any, field_info: Optional[FieldInfo] = None
|
cls, name: str, annotation: Any, field_info: FieldInfo | None = None
|
||||||
) -> Self:
|
) -> Self:
|
||||||
"""Construct a ModelField from given infos.
|
"""Construct a ModelField from given infos.
|
||||||
|
|
||||||
@@ -375,7 +387,7 @@ else: # pragma: pydantic-v1
|
|||||||
self,
|
self,
|
||||||
type: type[T],
|
type: type[T],
|
||||||
*,
|
*,
|
||||||
config: Optional[ConfigDict] = ...,
|
config: ConfigDict | None = ...,
|
||||||
) -> None: ...
|
) -> None: ...
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
@@ -383,14 +395,14 @@ else: # pragma: pydantic-v1
|
|||||||
self,
|
self,
|
||||||
type: Any,
|
type: Any,
|
||||||
*,
|
*,
|
||||||
config: Optional[ConfigDict] = ...,
|
config: ConfigDict | None = ...,
|
||||||
) -> None: ...
|
) -> None: ...
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
type: Any,
|
type: Any,
|
||||||
*,
|
*,
|
||||||
config: Optional[ConfigDict] = None,
|
config: ConfigDict | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.type = type
|
self.type = type
|
||||||
self.config = config
|
self.config = config
|
||||||
@@ -398,7 +410,7 @@ else: # pragma: pydantic-v1
|
|||||||
def validate_python(self, value: Any) -> T:
|
def validate_python(self, value: Any) -> T:
|
||||||
return type_validate_python(self.type, value)
|
return type_validate_python(self.type, value)
|
||||||
|
|
||||||
def validate_json(self, value: Union[str, bytes]) -> T:
|
def validate_json(self, value: str | bytes) -> T:
|
||||||
return type_validate_json(self.type, value)
|
return type_validate_json(self.type, value)
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
@@ -407,7 +419,7 @@ else: # pragma: pydantic-v1
|
|||||||
/,
|
/,
|
||||||
*fields: str,
|
*fields: str,
|
||||||
mode: Literal["before"],
|
mode: Literal["before"],
|
||||||
check_fields: Optional[bool] = None,
|
check_fields: bool | None = None,
|
||||||
): ...
|
): ...
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
@@ -416,7 +428,7 @@ else: # pragma: pydantic-v1
|
|||||||
/,
|
/,
|
||||||
*fields: str,
|
*fields: str,
|
||||||
mode: Literal["after"] = ...,
|
mode: Literal["after"] = ...,
|
||||||
check_fields: Optional[bool] = None,
|
check_fields: bool | None = None,
|
||||||
): ...
|
): ...
|
||||||
|
|
||||||
def field_validator(
|
def field_validator(
|
||||||
@@ -424,7 +436,7 @@ else: # pragma: pydantic-v1
|
|||||||
/,
|
/,
|
||||||
*fields: str,
|
*fields: str,
|
||||||
mode: Literal["before", "after"] = "after",
|
mode: Literal["before", "after"] = "after",
|
||||||
check_fields: Optional[bool] = None,
|
check_fields: bool | None = None,
|
||||||
):
|
):
|
||||||
if mode == "before":
|
if mode == "before":
|
||||||
return validator(
|
return validator(
|
||||||
@@ -458,16 +470,16 @@ else: # pragma: pydantic-v1
|
|||||||
|
|
||||||
def model_dump(
|
def model_dump(
|
||||||
model: BaseModel,
|
model: BaseModel,
|
||||||
include: Optional[set[str]] = None,
|
include: ModelDumpIncEx = None,
|
||||||
exclude: Optional[set[str]] = None,
|
exclude: ModelDumpIncEx = None,
|
||||||
by_alias: bool = False,
|
by_alias: bool = False,
|
||||||
exclude_unset: bool = False,
|
exclude_unset: bool = False,
|
||||||
exclude_defaults: bool = False,
|
exclude_defaults: bool = False,
|
||||||
exclude_none: bool = False,
|
exclude_none: bool = False,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
return model.dict(
|
return model.dict(
|
||||||
include=include,
|
include=cast(Any, include),
|
||||||
exclude=exclude,
|
exclude=cast(Any, exclude),
|
||||||
by_alias=by_alias,
|
by_alias=by_alias,
|
||||||
exclude_unset=exclude_unset,
|
exclude_unset=exclude_unset,
|
||||||
exclude_defaults=exclude_defaults,
|
exclude_defaults=exclude_defaults,
|
||||||
@@ -490,7 +502,7 @@ else: # pragma: pydantic-v1
|
|||||||
"""Validate data with given type."""
|
"""Validate data with given type."""
|
||||||
return parse_obj_as(type_, data)
|
return parse_obj_as(type_, data)
|
||||||
|
|
||||||
def type_validate_json(type_: type[T], data: Union[str, bytes]) -> T:
|
def type_validate_json(type_: type[T], data: str | bytes) -> T:
|
||||||
"""Validate JSON with given type."""
|
"""Validate JSON with given type."""
|
||||||
return parse_raw_as(type_, data)
|
return parse_raw_as(type_, data)
|
||||||
|
|
||||||
|
|||||||
+26
-31
@@ -20,8 +20,7 @@ from ipaddress import IPv4Address
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, Any, Optional, Union
|
from typing import TYPE_CHECKING, Any, TypeAlias, get_args, get_origin
|
||||||
from typing_extensions import TypeAlias, get_args, get_origin
|
|
||||||
|
|
||||||
from dotenv import dotenv_values
|
from dotenv import dotenv_values
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
@@ -41,9 +40,7 @@ from nonebot.log import logger
|
|||||||
from nonebot.typing import origin_is_union
|
from nonebot.typing import origin_is_union
|
||||||
from nonebot.utils import deep_update, lenient_issubclass, type_is_complex
|
from nonebot.utils import deep_update, lenient_issubclass, type_is_complex
|
||||||
|
|
||||||
DOTENV_TYPE: TypeAlias = Union[
|
DOTENV_TYPE: TypeAlias = Path | str | list[Path | str] | tuple[Path | str, ...]
|
||||||
Path, str, list[Union[Path, str]], tuple[Union[Path, str], ...]
|
|
||||||
]
|
|
||||||
|
|
||||||
ENV_FILE_SENTINEL = Path("")
|
ENV_FILE_SENTINEL = Path("")
|
||||||
|
|
||||||
@@ -84,10 +81,10 @@ class DotEnvSettingsSource(BaseSettingsSource):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
settings_cls: type[BaseModel],
|
settings_cls: type[BaseModel],
|
||||||
env_file: Optional[DOTENV_TYPE],
|
env_file: DOTENV_TYPE | None,
|
||||||
env_file_encoding: str,
|
env_file_encoding: str,
|
||||||
case_sensitive: Optional[bool] = False,
|
case_sensitive: bool | None = False,
|
||||||
env_nested_delimiter: Optional[str] = None,
|
env_nested_delimiter: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(settings_cls)
|
super().__init__(settings_cls)
|
||||||
self.env_file = env_file
|
self.env_file = env_file
|
||||||
@@ -108,17 +105,17 @@ class DotEnvSettingsSource(BaseSettingsSource):
|
|||||||
return False, False
|
return False, False
|
||||||
|
|
||||||
def _parse_env_vars(
|
def _parse_env_vars(
|
||||||
self, env_vars: Mapping[str, Optional[str]]
|
self, env_vars: Mapping[str, str | None]
|
||||||
) -> dict[str, Optional[str]]:
|
) -> dict[str, str | None]:
|
||||||
return {
|
return {
|
||||||
self._apply_case_sensitive(key): value for key, value in env_vars.items()
|
self._apply_case_sensitive(key): value for key, value in env_vars.items()
|
||||||
}
|
}
|
||||||
|
|
||||||
def _read_env_file(self, file_path: Path) -> dict[str, Optional[str]]:
|
def _read_env_file(self, file_path: Path) -> dict[str, str | None]:
|
||||||
file_vars = dotenv_values(file_path, encoding=self.env_file_encoding)
|
file_vars = dotenv_values(file_path, encoding=self.env_file_encoding)
|
||||||
return self._parse_env_vars(file_vars)
|
return self._parse_env_vars(file_vars)
|
||||||
|
|
||||||
def _read_env_files(self) -> dict[str, Optional[str]]:
|
def _read_env_files(self) -> dict[str, str | None]:
|
||||||
env_files = self.env_file
|
env_files = self.env_file
|
||||||
if env_files is None:
|
if env_files is None:
|
||||||
return {}
|
return {}
|
||||||
@@ -126,16 +123,14 @@ class DotEnvSettingsSource(BaseSettingsSource):
|
|||||||
if isinstance(env_files, (str, os.PathLike)):
|
if isinstance(env_files, (str, os.PathLike)):
|
||||||
env_files = [env_files]
|
env_files = [env_files]
|
||||||
|
|
||||||
dotenv_vars: dict[str, Optional[str]] = {}
|
dotenv_vars: dict[str, str | None] = {}
|
||||||
for env_file in env_files:
|
for env_file in env_files:
|
||||||
env_path = Path(env_file).expanduser()
|
env_path = Path(env_file).expanduser()
|
||||||
if env_path.is_file():
|
if env_path.is_file():
|
||||||
dotenv_vars.update(self._read_env_file(env_path))
|
dotenv_vars.update(self._read_env_file(env_path))
|
||||||
return dotenv_vars
|
return dotenv_vars
|
||||||
|
|
||||||
def _next_field(
|
def _next_field(self, field: ModelField | None, key: str) -> ModelField | None:
|
||||||
self, field: Optional[ModelField], key: str
|
|
||||||
) -> Optional[ModelField]:
|
|
||||||
if not field or origin_is_union(get_origin(field.annotation)):
|
if not field or origin_is_union(get_origin(field.annotation)):
|
||||||
return None
|
return None
|
||||||
elif field.annotation and lenient_issubclass(field.annotation, BaseModel):
|
elif field.annotation and lenient_issubclass(field.annotation, BaseModel):
|
||||||
@@ -147,8 +142,8 @@ class DotEnvSettingsSource(BaseSettingsSource):
|
|||||||
def _explode_env_vars(
|
def _explode_env_vars(
|
||||||
self,
|
self,
|
||||||
field: ModelField,
|
field: ModelField,
|
||||||
env_vars: dict[str, Optional[str]],
|
env_vars: dict[str, str | None],
|
||||||
env_file_vars: dict[str, Optional[str]],
|
env_file_vars: dict[str, str | None],
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
if self.env_nested_delimiter is None:
|
if self.env_nested_delimiter is None:
|
||||||
return {}
|
return {}
|
||||||
@@ -164,7 +159,7 @@ class DotEnvSettingsSource(BaseSettingsSource):
|
|||||||
|
|
||||||
_, *keys, last_key = env_name.split(self.env_nested_delimiter)
|
_, *keys, last_key = env_name.split(self.env_nested_delimiter)
|
||||||
env_var = result
|
env_var = result
|
||||||
target_field: Optional[ModelField] = field
|
target_field: ModelField | None = field
|
||||||
for key in keys:
|
for key in keys:
|
||||||
target_field = self._next_field(target_field, key)
|
target_field = self._next_field(target_field, key)
|
||||||
env_var = env_var.setdefault(key, {})
|
env_var = env_var.setdefault(key, {})
|
||||||
@@ -293,18 +288,18 @@ class DotEnvSettingsSource(BaseSettingsSource):
|
|||||||
if PYDANTIC_V2: # pragma: pydantic-v2
|
if PYDANTIC_V2: # pragma: pydantic-v2
|
||||||
|
|
||||||
class SettingsConfig(ConfigDict, total=False):
|
class SettingsConfig(ConfigDict, total=False):
|
||||||
env_file: Optional[DOTENV_TYPE]
|
env_file: DOTENV_TYPE | None
|
||||||
env_file_encoding: str
|
env_file_encoding: str
|
||||||
case_sensitive: bool
|
case_sensitive: bool
|
||||||
env_nested_delimiter: Optional[str]
|
env_nested_delimiter: str | None
|
||||||
|
|
||||||
else: # pragma: pydantic-v1
|
else: # pragma: pydantic-v1
|
||||||
|
|
||||||
class SettingsConfig(ConfigDict):
|
class SettingsConfig(ConfigDict):
|
||||||
env_file: Optional[DOTENV_TYPE]
|
env_file: DOTENV_TYPE | None
|
||||||
env_file_encoding: str
|
env_file_encoding: str
|
||||||
case_sensitive: bool
|
case_sensitive: bool
|
||||||
env_nested_delimiter: Optional[str]
|
env_nested_delimiter: str | None
|
||||||
|
|
||||||
|
|
||||||
class BaseSettings(BaseModel):
|
class BaseSettings(BaseModel):
|
||||||
@@ -332,9 +327,9 @@ class BaseSettings(BaseModel):
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
__settings_self__, # pyright: ignore[reportSelfClsParameterName]
|
__settings_self__, # pyright: ignore[reportSelfClsParameterName]
|
||||||
_env_file: Optional[DOTENV_TYPE] = ENV_FILE_SENTINEL,
|
_env_file: DOTENV_TYPE | None = ENV_FILE_SENTINEL,
|
||||||
_env_file_encoding: Optional[str] = None,
|
_env_file_encoding: str | None = None,
|
||||||
_env_nested_delimiter: Optional[str] = None,
|
_env_nested_delimiter: str | None = None,
|
||||||
**values: Any,
|
**values: Any,
|
||||||
) -> None:
|
) -> None:
|
||||||
settings_config = model_config(__settings_self__.__class__)
|
settings_config = model_config(__settings_self__.__class__)
|
||||||
@@ -372,9 +367,9 @@ class BaseSettings(BaseModel):
|
|||||||
def _settings_build_values(
|
def _settings_build_values(
|
||||||
settings_cls: type[BaseModel],
|
settings_cls: type[BaseModel],
|
||||||
init_kwargs: dict[str, Any],
|
init_kwargs: dict[str, Any],
|
||||||
env_file: Optional[DOTENV_TYPE],
|
env_file: DOTENV_TYPE | None,
|
||||||
env_file_encoding: str,
|
env_file_encoding: str,
|
||||||
env_nested_delimiter: Optional[str],
|
env_nested_delimiter: str | None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
init_settings = InitSettingsSource(settings_cls, init_kwargs=init_kwargs)
|
init_settings = InitSettingsSource(settings_cls, init_kwargs=init_kwargs)
|
||||||
env_settings = DotEnvSettingsSource(
|
env_settings = DotEnvSettingsSource(
|
||||||
@@ -409,7 +404,7 @@ class Config(BaseSettings):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
_env_file: Optional[DOTENV_TYPE] = ".env", ".env.prod"
|
_env_file: DOTENV_TYPE | None = ".env", ".env.prod"
|
||||||
|
|
||||||
# nonebot configs
|
# nonebot configs
|
||||||
driver: str = "~fastapi"
|
driver: str = "~fastapi"
|
||||||
@@ -425,7 +420,7 @@ class Config(BaseSettings):
|
|||||||
"""NoneBot {ref}`nonebot.drivers.ReverseDriver` 服务端监听的 IP/主机名。"""
|
"""NoneBot {ref}`nonebot.drivers.ReverseDriver` 服务端监听的 IP/主机名。"""
|
||||||
port: int = Field(default=8080, ge=1, le=65535)
|
port: int = Field(default=8080, ge=1, le=65535)
|
||||||
"""NoneBot {ref}`nonebot.drivers.ReverseDriver` 服务端监听的端口。"""
|
"""NoneBot {ref}`nonebot.drivers.ReverseDriver` 服务端监听的端口。"""
|
||||||
log_level: Union[int, str] = LegacyUnionField(default="INFO")
|
log_level: int | str = LegacyUnionField(default="INFO")
|
||||||
"""NoneBot 日志输出等级,可以为 `int` 类型等级或等级名称。
|
"""NoneBot 日志输出等级,可以为 `int` 类型等级或等级名称。
|
||||||
|
|
||||||
参考 [记录日志](https://nonebot.dev/docs/appendices/log),[loguru 日志等级](https://loguru.readthedocs.io/en/stable/api/logger.html#levels)。
|
参考 [记录日志](https://nonebot.dev/docs/appendices/log),[loguru 日志等级](https://loguru.readthedocs.io/en/stable/api/logger.html#levels)。
|
||||||
@@ -442,7 +437,7 @@ class Config(BaseSettings):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# bot connection configs
|
# bot connection configs
|
||||||
api_timeout: Optional[float] = 30.0
|
api_timeout: float | None = 30.0
|
||||||
"""API 请求超时时间,单位: 秒。"""
|
"""API 请求超时时间,单位: 秒。"""
|
||||||
|
|
||||||
# bot runtime configs
|
# bot runtime configs
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ FrontMatter:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import abc
|
import abc
|
||||||
from collections.abc import Awaitable, Iterable
|
from collections.abc import Awaitable, Callable, Iterable
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from functools import partial
|
from functools import partial
|
||||||
import inspect
|
import inspect
|
||||||
from typing import Any, Callable, Generic, Optional, TypeVar, cast
|
from typing import Any, Generic, TypeVar, cast
|
||||||
|
|
||||||
import anyio
|
import anyio
|
||||||
from exceptiongroup import BaseExceptionGroup, catch
|
from exceptiongroup import BaseExceptionGroup, catch
|
||||||
@@ -47,13 +47,13 @@ class Param(abc.ABC, FieldInfo):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def _check_param(
|
def _check_param(
|
||||||
cls, param: inspect.Parameter, allow_types: tuple[type["Param"], ...]
|
cls, param: inspect.Parameter, allow_types: tuple[type["Param"], ...]
|
||||||
) -> Optional["Param"]:
|
) -> "Param | None":
|
||||||
return
|
return
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _check_parameterless(
|
def _check_parameterless(
|
||||||
cls, value: Any, allow_types: tuple[type["Param"], ...]
|
cls, value: Any, allow_types: tuple[type["Param"], ...]
|
||||||
) -> Optional["Param"]:
|
) -> "Param | None":
|
||||||
return
|
return
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
@@ -92,7 +92,7 @@ class Dependent(Generic[R]):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def __call__(self, **kwargs: Any) -> R:
|
async def __call__(self, **kwargs: Any) -> R:
|
||||||
exception: Optional[BaseExceptionGroup[SkippedException]] = None
|
exception: BaseExceptionGroup[SkippedException] | None = None
|
||||||
|
|
||||||
def _handle_skipped(exc_group: BaseExceptionGroup[SkippedException]):
|
def _handle_skipped(exc_group: BaseExceptionGroup[SkippedException]):
|
||||||
nonlocal exception
|
nonlocal exception
|
||||||
@@ -167,7 +167,7 @@ class Dependent(Generic[R]):
|
|||||||
cls,
|
cls,
|
||||||
*,
|
*,
|
||||||
call: _DependentCallable[R],
|
call: _DependentCallable[R],
|
||||||
parameterless: Optional[Iterable[Any]] = None,
|
parameterless: Iterable[Any] | None = None,
|
||||||
allow_types: Iterable[type[Param]],
|
allow_types: Iterable[type[Param]],
|
||||||
) -> "Dependent[R]":
|
) -> "Dependent[R]":
|
||||||
allow_types = tuple(allow_types)
|
allow_types = tuple(allow_types)
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ FrontMatter:
|
|||||||
description: nonebot.dependencies.utils 模块
|
description: nonebot.dependencies.utils 模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
import inspect
|
import inspect
|
||||||
from typing import Any, Callable, ForwardRef, cast
|
from typing import Any, ForwardRef, cast
|
||||||
from typing_extensions import TypeAliasType
|
from typing_extensions import TypeAliasType
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ FrontMatter:
|
|||||||
description: nonebot.drivers 模块
|
description: nonebot.drivers 模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from nonebot.internal.driver import DEFAULT_TIMEOUT as DEFAULT_TIMEOUT
|
||||||
from nonebot.internal.driver import URL as URL
|
from nonebot.internal.driver import URL as URL
|
||||||
from nonebot.internal.driver import ASGIMixin as ASGIMixin
|
from nonebot.internal.driver import ASGIMixin as ASGIMixin
|
||||||
from nonebot.internal.driver import Cookies as Cookies
|
from nonebot.internal.driver import Cookies as Cookies
|
||||||
@@ -31,6 +32,7 @@ from nonebot.internal.driver import WebSocketServerSetup as WebSocketServerSetup
|
|||||||
from nonebot.internal.driver import combine_driver as combine_driver
|
from nonebot.internal.driver import combine_driver as combine_driver
|
||||||
|
|
||||||
__autodoc__ = {
|
__autodoc__ = {
|
||||||
|
"DEFAULT_TIMEOUT": True,
|
||||||
"URL": True,
|
"URL": True,
|
||||||
"Cookies": True,
|
"Cookies": True,
|
||||||
"Request": True,
|
"Request": True,
|
||||||
|
|||||||
+119
-41
@@ -19,7 +19,7 @@ FrontMatter:
|
|||||||
|
|
||||||
from collections.abc import AsyncGenerator
|
from collections.abc import AsyncGenerator
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from typing import TYPE_CHECKING, Optional, Union
|
from typing import TYPE_CHECKING
|
||||||
from typing_extensions import override
|
from typing_extensions import override
|
||||||
|
|
||||||
from multidict import CIMultiDict
|
from multidict import CIMultiDict
|
||||||
@@ -38,6 +38,7 @@ from nonebot.drivers import WebSocket as BaseWebSocket
|
|||||||
from nonebot.drivers.none import Driver as NoneDriver
|
from nonebot.drivers.none import Driver as NoneDriver
|
||||||
from nonebot.exception import WebSocketClosed
|
from nonebot.exception import WebSocketClosed
|
||||||
from nonebot.internal.driver import (
|
from nonebot.internal.driver import (
|
||||||
|
DEFAULT_TIMEOUT,
|
||||||
Cookies,
|
Cookies,
|
||||||
CookieTypes,
|
CookieTypes,
|
||||||
HeaderTypes,
|
HeaderTypes,
|
||||||
@@ -45,6 +46,8 @@ from nonebot.internal.driver import (
|
|||||||
Timeout,
|
Timeout,
|
||||||
TimeoutTypes,
|
TimeoutTypes,
|
||||||
)
|
)
|
||||||
|
from nonebot.log import logger
|
||||||
|
from nonebot.utils import UNSET, UnsetType, exclude_unset
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import aiohttp
|
import aiohttp
|
||||||
@@ -62,11 +65,11 @@ class Session(HTTPClientSession):
|
|||||||
params: QueryTypes = None,
|
params: QueryTypes = None,
|
||||||
headers: HeaderTypes = None,
|
headers: HeaderTypes = None,
|
||||||
cookies: CookieTypes = None,
|
cookies: CookieTypes = None,
|
||||||
version: Union[str, HTTPVersion] = HTTPVersion.H11,
|
version: str | HTTPVersion = HTTPVersion.H11,
|
||||||
timeout: TimeoutTypes = None,
|
timeout: TimeoutTypes | UnsetType = UNSET,
|
||||||
proxy: Optional[str] = None,
|
proxy: str | None = None,
|
||||||
):
|
):
|
||||||
self._client: Optional[aiohttp.ClientSession] = None
|
self._client: aiohttp.ClientSession | None = None
|
||||||
|
|
||||||
self._params = URL.build(query=params).query if params is not None else None
|
self._params = URL.build(query=params).query if params is not None else None
|
||||||
|
|
||||||
@@ -85,15 +88,32 @@ class Session(HTTPClientSession):
|
|||||||
else:
|
else:
|
||||||
raise RuntimeError(f"Unsupported HTTP version: {version}")
|
raise RuntimeError(f"Unsupported HTTP version: {version}")
|
||||||
|
|
||||||
|
_timeout = None
|
||||||
if isinstance(timeout, Timeout):
|
if isinstance(timeout, Timeout):
|
||||||
self._timeout = aiohttp.ClientTimeout(
|
timeout_kwargs: dict[str, float | None] = exclude_unset(
|
||||||
total=timeout.total,
|
{
|
||||||
connect=timeout.connect,
|
"total": timeout.total,
|
||||||
sock_read=timeout.read,
|
"connect": timeout.connect,
|
||||||
|
"sock_read": timeout.read,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
else:
|
if timeout_kwargs:
|
||||||
self._timeout = aiohttp.ClientTimeout(timeout)
|
_timeout = aiohttp.ClientTimeout(**timeout_kwargs) # type: ignore
|
||||||
|
elif timeout is not UNSET:
|
||||||
|
_timeout = aiohttp.ClientTimeout(connect=timeout, sock_read=timeout)
|
||||||
|
|
||||||
|
if _timeout is None:
|
||||||
|
_timeout = aiohttp.ClientTimeout(
|
||||||
|
**exclude_unset(
|
||||||
|
{
|
||||||
|
"total": DEFAULT_TIMEOUT.total,
|
||||||
|
"connect": DEFAULT_TIMEOUT.connect,
|
||||||
|
"sock_read": DEFAULT_TIMEOUT.read,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self._timeout = _timeout
|
||||||
self._proxy = proxy
|
self._proxy = proxy
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -102,6 +122,25 @@ class Session(HTTPClientSession):
|
|||||||
raise RuntimeError("Session is not initialized")
|
raise RuntimeError("Session is not initialized")
|
||||||
return self._client
|
return self._client
|
||||||
|
|
||||||
|
def _get_timeout(self, timeout: TimeoutTypes | UnsetType) -> aiohttp.ClientTimeout:
|
||||||
|
_timeout = None
|
||||||
|
if isinstance(timeout, Timeout):
|
||||||
|
timeout_kwargs: dict[str, float | None] = exclude_unset(
|
||||||
|
{
|
||||||
|
"total": timeout.total,
|
||||||
|
"connect": timeout.connect,
|
||||||
|
"sock_read": timeout.read,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if timeout_kwargs:
|
||||||
|
_timeout = aiohttp.ClientTimeout(**timeout_kwargs) # type: ignore
|
||||||
|
elif timeout is not UNSET:
|
||||||
|
_timeout = aiohttp.ClientTimeout(connect=timeout, sock_read=timeout)
|
||||||
|
|
||||||
|
if _timeout is None:
|
||||||
|
return self._timeout
|
||||||
|
return _timeout
|
||||||
|
|
||||||
@override
|
@override
|
||||||
async def request(self, setup: Request) -> Response:
|
async def request(self, setup: Request) -> Response:
|
||||||
if self._params:
|
if self._params:
|
||||||
@@ -121,15 +160,6 @@ class Session(HTTPClientSession):
|
|||||||
if cookie.value is not None
|
if cookie.value is not None
|
||||||
)
|
)
|
||||||
|
|
||||||
if isinstance(setup.timeout, Timeout):
|
|
||||||
timeout = aiohttp.ClientTimeout(
|
|
||||||
total=setup.timeout.total,
|
|
||||||
connect=setup.timeout.connect,
|
|
||||||
sock_read=setup.timeout.read,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
timeout = aiohttp.ClientTimeout(setup.timeout)
|
|
||||||
|
|
||||||
async with await self.client.request(
|
async with await self.client.request(
|
||||||
setup.method,
|
setup.method,
|
||||||
url,
|
url,
|
||||||
@@ -138,7 +168,7 @@ class Session(HTTPClientSession):
|
|||||||
cookies=cookies,
|
cookies=cookies,
|
||||||
headers=setup.headers,
|
headers=setup.headers,
|
||||||
proxy=setup.proxy or self._proxy,
|
proxy=setup.proxy or self._proxy,
|
||||||
timeout=timeout,
|
timeout=self._get_timeout(setup.timeout),
|
||||||
) as response:
|
) as response:
|
||||||
return Response(
|
return Response(
|
||||||
response.status,
|
response.status,
|
||||||
@@ -171,15 +201,6 @@ class Session(HTTPClientSession):
|
|||||||
if cookie.value is not None
|
if cookie.value is not None
|
||||||
)
|
)
|
||||||
|
|
||||||
if isinstance(setup.timeout, Timeout):
|
|
||||||
timeout = aiohttp.ClientTimeout(
|
|
||||||
total=setup.timeout.total,
|
|
||||||
connect=setup.timeout.connect,
|
|
||||||
sock_read=setup.timeout.read,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
timeout = aiohttp.ClientTimeout(setup.timeout)
|
|
||||||
|
|
||||||
async with self.client.request(
|
async with self.client.request(
|
||||||
setup.method,
|
setup.method,
|
||||||
url,
|
url,
|
||||||
@@ -188,14 +209,29 @@ class Session(HTTPClientSession):
|
|||||||
cookies=cookies,
|
cookies=cookies,
|
||||||
headers=setup.headers,
|
headers=setup.headers,
|
||||||
proxy=setup.proxy or self._proxy,
|
proxy=setup.proxy or self._proxy,
|
||||||
timeout=timeout,
|
timeout=self._get_timeout(setup.timeout),
|
||||||
) as response:
|
) as response:
|
||||||
response_headers = response.headers.copy()
|
response_headers = response.headers.copy()
|
||||||
|
# aiohttp does not guarantee fixed-size chunks; re-chunk to exact size
|
||||||
|
buffer = bytearray()
|
||||||
async for chunk in response.content.iter_chunked(chunk_size):
|
async for chunk in response.content.iter_chunked(chunk_size):
|
||||||
|
if not chunk:
|
||||||
|
continue
|
||||||
|
buffer.extend(chunk)
|
||||||
|
while len(buffer) >= chunk_size:
|
||||||
|
out = bytes(buffer[:chunk_size])
|
||||||
|
del buffer[:chunk_size]
|
||||||
|
yield Response(
|
||||||
|
response.status,
|
||||||
|
headers=response_headers,
|
||||||
|
content=out,
|
||||||
|
request=setup,
|
||||||
|
)
|
||||||
|
if buffer:
|
||||||
yield Response(
|
yield Response(
|
||||||
response.status,
|
response.status,
|
||||||
headers=response_headers,
|
headers=response_headers,
|
||||||
content=chunk,
|
content=bytes(buffer),
|
||||||
request=setup,
|
request=setup,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -255,13 +291,49 @@ class Mixin(HTTPClientMixin, WebSocketClientMixin):
|
|||||||
else:
|
else:
|
||||||
raise RuntimeError(f"Unsupported HTTP version: {setup.version}")
|
raise RuntimeError(f"Unsupported HTTP version: {setup.version}")
|
||||||
|
|
||||||
|
timeout = None
|
||||||
if isinstance(setup.timeout, Timeout):
|
if isinstance(setup.timeout, Timeout):
|
||||||
timeout = aiohttp.ClientWSTimeout(
|
timeout_kwargs: dict[str, float | None] = exclude_unset(
|
||||||
ws_receive=setup.timeout.read, # type: ignore
|
{
|
||||||
ws_close=setup.timeout.total, # type: ignore
|
"ws_receive": setup.timeout.read,
|
||||||
|
"ws_close": (
|
||||||
|
setup.timeout.total
|
||||||
|
if setup.timeout.close is UNSET
|
||||||
|
else setup.timeout.close
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if timeout_kwargs:
|
||||||
|
timeout = aiohttp.ClientWSTimeout(**timeout_kwargs)
|
||||||
|
elif setup.timeout is not UNSET:
|
||||||
|
timeout = aiohttp.ClientWSTimeout(
|
||||||
|
ws_receive=setup.timeout, # type: ignore
|
||||||
|
ws_close=setup.timeout, # type: ignore
|
||||||
|
)
|
||||||
|
|
||||||
|
if timeout is None:
|
||||||
|
timeout = aiohttp.ClientWSTimeout(
|
||||||
|
**exclude_unset(
|
||||||
|
{
|
||||||
|
"ws_receive": DEFAULT_TIMEOUT.read,
|
||||||
|
"ws_close": (
|
||||||
|
DEFAULT_TIMEOUT.total
|
||||||
|
if DEFAULT_TIMEOUT.close is UNSET
|
||||||
|
else DEFAULT_TIMEOUT.close
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
heartbeat = None
|
||||||
|
if setup.ping_interval is not UNSET:
|
||||||
|
heartbeat = setup.ping_interval
|
||||||
|
|
||||||
|
if isinstance(setup.timeout, Timeout) and setup.timeout.ping is not UNSET:
|
||||||
|
logger.warning(
|
||||||
|
"aiohttp driver does not expose a separate ping timeout; "
|
||||||
|
"the configured ping timeout will be ignored."
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
timeout = aiohttp.ClientWSTimeout(ws_close=setup.timeout or 10.0) # type: ignore
|
|
||||||
|
|
||||||
async with aiohttp.ClientSession(version=version, trust_env=True) as session:
|
async with aiohttp.ClientSession(version=version, trust_env=True) as session:
|
||||||
async with session.ws_connect(
|
async with session.ws_connect(
|
||||||
@@ -270,6 +342,8 @@ class Mixin(HTTPClientMixin, WebSocketClientMixin):
|
|||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
headers=setup.headers,
|
headers=setup.headers,
|
||||||
proxy=setup.proxy,
|
proxy=setup.proxy,
|
||||||
|
autoping=heartbeat is not None,
|
||||||
|
heartbeat=heartbeat,
|
||||||
) as ws:
|
) as ws:
|
||||||
yield WebSocket(request=setup, session=session, websocket=ws)
|
yield WebSocket(request=setup, session=session, websocket=ws)
|
||||||
|
|
||||||
@@ -279,9 +353,9 @@ class Mixin(HTTPClientMixin, WebSocketClientMixin):
|
|||||||
params: QueryTypes = None,
|
params: QueryTypes = None,
|
||||||
headers: HeaderTypes = None,
|
headers: HeaderTypes = None,
|
||||||
cookies: CookieTypes = None,
|
cookies: CookieTypes = None,
|
||||||
version: Union[str, HTTPVersion] = HTTPVersion.H11,
|
version: str | HTTPVersion = HTTPVersion.H11,
|
||||||
timeout: TimeoutTypes = None,
|
timeout: TimeoutTypes | UnsetType = UNSET,
|
||||||
proxy: Optional[str] = None,
|
proxy: str | None = None,
|
||||||
) -> Session:
|
) -> Session:
|
||||||
return Session(
|
return Session(
|
||||||
params=params,
|
params=params,
|
||||||
@@ -323,7 +397,11 @@ class WebSocket(BaseWebSocket):
|
|||||||
|
|
||||||
async def _receive(self) -> aiohttp.WSMessage:
|
async def _receive(self) -> aiohttp.WSMessage:
|
||||||
msg = await self.websocket.receive()
|
msg = await self.websocket.receive()
|
||||||
if msg.type in (aiohttp.WSMsgType.CLOSE, aiohttp.WSMsgType.CLOSING):
|
if msg.type in (
|
||||||
|
aiohttp.WSMsgType.CLOSE,
|
||||||
|
aiohttp.WSMsgType.CLOSING,
|
||||||
|
aiohttp.WSMsgType.CLOSED,
|
||||||
|
):
|
||||||
raise WebSocketClosed(self.websocket.close_code or 1006)
|
raise WebSocketClosed(self.websocket.close_code or 1006)
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
|
|||||||
+13
-13
@@ -20,7 +20,7 @@ FrontMatter:
|
|||||||
import contextlib
|
import contextlib
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Optional, Union
|
from typing import Any
|
||||||
from typing_extensions import override
|
from typing_extensions import override
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
@@ -63,23 +63,23 @@ def catch_closed(func):
|
|||||||
class Config(BaseModel):
|
class Config(BaseModel):
|
||||||
"""FastAPI 驱动框架设置,详情参考 FastAPI 文档"""
|
"""FastAPI 驱动框架设置,详情参考 FastAPI 文档"""
|
||||||
|
|
||||||
fastapi_openapi_url: Optional[str] = None
|
fastapi_openapi_url: str | None = None
|
||||||
"""`openapi.json` 地址,默认为 `None` 即关闭"""
|
"""`openapi.json` 地址,默认为 `None` 即关闭"""
|
||||||
fastapi_docs_url: Optional[str] = None
|
fastapi_docs_url: str | None = None
|
||||||
"""`swagger` 地址,默认为 `None` 即关闭"""
|
"""`swagger` 地址,默认为 `None` 即关闭"""
|
||||||
fastapi_redoc_url: Optional[str] = None
|
fastapi_redoc_url: str | None = None
|
||||||
"""`redoc` 地址,默认为 `None` 即关闭"""
|
"""`redoc` 地址,默认为 `None` 即关闭"""
|
||||||
fastapi_include_adapter_schema: bool = True
|
fastapi_include_adapter_schema: bool = True
|
||||||
"""是否包含适配器路由的 schema,默认为 `True`"""
|
"""是否包含适配器路由的 schema,默认为 `True`"""
|
||||||
fastapi_reload: bool = False
|
fastapi_reload: bool = False
|
||||||
"""开启/关闭冷重载"""
|
"""开启/关闭冷重载"""
|
||||||
fastapi_reload_dirs: Optional[list[str]] = None
|
fastapi_reload_dirs: list[str] | None = None
|
||||||
"""重载监控文件夹列表,默认为 uvicorn 默认值"""
|
"""重载监控文件夹列表,默认为 uvicorn 默认值"""
|
||||||
fastapi_reload_delay: float = 0.25
|
fastapi_reload_delay: float = 0.25
|
||||||
"""重载延迟,默认为 uvicorn 默认值"""
|
"""重载延迟,默认为 uvicorn 默认值"""
|
||||||
fastapi_reload_includes: Optional[list[str]] = None
|
fastapi_reload_includes: list[str] | None = None
|
||||||
"""要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值"""
|
"""要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值"""
|
||||||
fastapi_reload_excludes: Optional[list[str]] = None
|
fastapi_reload_excludes: list[str] | None = None
|
||||||
"""不要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值"""
|
"""不要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值"""
|
||||||
fastapi_extra: dict[str, Any] = {}
|
fastapi_extra: dict[str, Any] = {}
|
||||||
"""传递给 `FastAPI` 的其他参数。"""
|
"""传递给 `FastAPI` 的其他参数。"""
|
||||||
@@ -160,10 +160,10 @@ class Driver(BaseDriver, ASGIMixin):
|
|||||||
@override
|
@override
|
||||||
def run(
|
def run(
|
||||||
self,
|
self,
|
||||||
host: Optional[str] = None,
|
host: str | None = None,
|
||||||
port: Optional[int] = None,
|
port: int | None = None,
|
||||||
*args,
|
*args,
|
||||||
app: Optional[str] = None,
|
app: str | None = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
"""使用 `uvicorn` 启动 FastAPI"""
|
"""使用 `uvicorn` 启动 FastAPI"""
|
||||||
@@ -206,8 +206,8 @@ class Driver(BaseDriver, ASGIMixin):
|
|||||||
with contextlib.suppress(Exception):
|
with contextlib.suppress(Exception):
|
||||||
json = await request.json()
|
json = await request.json()
|
||||||
|
|
||||||
data: Optional[dict] = None
|
data: dict | None = None
|
||||||
files: Optional[list[tuple[str, FileTypes]]] = None
|
files: list[tuple[str, FileTypes]] | None = None
|
||||||
with contextlib.suppress(Exception):
|
with contextlib.suppress(Exception):
|
||||||
form = await request.form()
|
form = await request.form()
|
||||||
data = {}
|
data = {}
|
||||||
@@ -280,7 +280,7 @@ class FastAPIWebSocket(BaseWebSocket):
|
|||||||
await self.websocket.close(code, reason)
|
await self.websocket.close(code, reason)
|
||||||
|
|
||||||
@override
|
@override
|
||||||
async def receive(self) -> Union[str, bytes]:
|
async def receive(self) -> str | bytes:
|
||||||
# assert self.websocket.application_state == WebSocketState.CONNECTED
|
# assert self.websocket.application_state == WebSocketState.CONNECTED
|
||||||
msg = await self.websocket.receive()
|
msg = await self.websocket.receive()
|
||||||
if msg["type"] == "websocket.disconnect":
|
if msg["type"] == "websocket.disconnect":
|
||||||
|
|||||||
+56
-33
@@ -18,7 +18,7 @@ FrontMatter:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from collections.abc import AsyncGenerator
|
from collections.abc import AsyncGenerator
|
||||||
from typing import TYPE_CHECKING, Optional, Union
|
from typing import TYPE_CHECKING
|
||||||
from typing_extensions import override
|
from typing_extensions import override
|
||||||
|
|
||||||
from multidict import CIMultiDict
|
from multidict import CIMultiDict
|
||||||
@@ -34,6 +34,7 @@ from nonebot.drivers import (
|
|||||||
)
|
)
|
||||||
from nonebot.drivers.none import Driver as NoneDriver
|
from nonebot.drivers.none import Driver as NoneDriver
|
||||||
from nonebot.internal.driver import (
|
from nonebot.internal.driver import (
|
||||||
|
DEFAULT_TIMEOUT,
|
||||||
Cookies,
|
Cookies,
|
||||||
CookieTypes,
|
CookieTypes,
|
||||||
HeaderTypes,
|
HeaderTypes,
|
||||||
@@ -41,6 +42,7 @@ from nonebot.internal.driver import (
|
|||||||
Timeout,
|
Timeout,
|
||||||
TimeoutTypes,
|
TimeoutTypes,
|
||||||
)
|
)
|
||||||
|
from nonebot.utils import UNSET, UnsetType, exclude_unset
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import httpx
|
import httpx
|
||||||
@@ -58,11 +60,11 @@ class Session(HTTPClientSession):
|
|||||||
params: QueryTypes = None,
|
params: QueryTypes = None,
|
||||||
headers: HeaderTypes = None,
|
headers: HeaderTypes = None,
|
||||||
cookies: CookieTypes = None,
|
cookies: CookieTypes = None,
|
||||||
version: Union[str, HTTPVersion] = HTTPVersion.H11,
|
version: str | HTTPVersion = HTTPVersion.H11,
|
||||||
timeout: TimeoutTypes = None,
|
timeout: TimeoutTypes | UnsetType = UNSET,
|
||||||
proxy: Optional[str] = None,
|
proxy: str | None = None,
|
||||||
):
|
):
|
||||||
self._client: Optional[httpx.AsyncClient] = None
|
self._client: httpx.AsyncClient | None = None
|
||||||
|
|
||||||
self._params = (
|
self._params = (
|
||||||
tuple(URL.build(query=params).query.items()) if params is not None else None
|
tuple(URL.build(query=params).query.items()) if params is not None else None
|
||||||
@@ -73,15 +75,34 @@ class Session(HTTPClientSession):
|
|||||||
self._cookies = Cookies(cookies)
|
self._cookies = Cookies(cookies)
|
||||||
self._version = HTTPVersion(version)
|
self._version = HTTPVersion(version)
|
||||||
|
|
||||||
|
_timeout = None
|
||||||
if isinstance(timeout, Timeout):
|
if isinstance(timeout, Timeout):
|
||||||
self._timeout = httpx.Timeout(
|
avg_timeout = timeout.total and timeout.total / 4
|
||||||
timeout=timeout.total,
|
timeout_kwargs: dict[str, float | None] = exclude_unset(
|
||||||
connect=timeout.connect,
|
{
|
||||||
read=timeout.read,
|
"timeout": avg_timeout,
|
||||||
|
"connect": timeout.connect,
|
||||||
|
"read": timeout.read,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
else:
|
if timeout_kwargs:
|
||||||
self._timeout = httpx.Timeout(timeout)
|
_timeout = httpx.Timeout(**timeout_kwargs)
|
||||||
|
elif timeout is not UNSET:
|
||||||
|
_timeout = httpx.Timeout(timeout)
|
||||||
|
|
||||||
|
if _timeout is None:
|
||||||
|
avg_timeout = DEFAULT_TIMEOUT.total and DEFAULT_TIMEOUT.total / 4
|
||||||
|
_timeout = httpx.Timeout(
|
||||||
|
**exclude_unset(
|
||||||
|
{
|
||||||
|
"timeout": avg_timeout,
|
||||||
|
"connect": DEFAULT_TIMEOUT.connect,
|
||||||
|
"read": DEFAULT_TIMEOUT.read,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self._timeout = _timeout
|
||||||
self._proxy = proxy
|
self._proxy = proxy
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -90,17 +111,28 @@ class Session(HTTPClientSession):
|
|||||||
raise RuntimeError("Session is not initialized")
|
raise RuntimeError("Session is not initialized")
|
||||||
return self._client
|
return self._client
|
||||||
|
|
||||||
|
def _get_timeout(self, timeout: TimeoutTypes | UnsetType) -> httpx.Timeout:
|
||||||
|
_timeout = None
|
||||||
|
if isinstance(timeout, Timeout):
|
||||||
|
avg_timeout = timeout.total and timeout.total / 4
|
||||||
|
timeout_kwargs: dict[str, float | None] = exclude_unset(
|
||||||
|
{
|
||||||
|
"timeout": avg_timeout,
|
||||||
|
"connect": timeout.connect,
|
||||||
|
"read": timeout.read,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if timeout_kwargs:
|
||||||
|
_timeout = httpx.Timeout(**timeout_kwargs)
|
||||||
|
elif timeout is not UNSET:
|
||||||
|
_timeout = httpx.Timeout(timeout)
|
||||||
|
|
||||||
|
if _timeout is None:
|
||||||
|
return self._timeout
|
||||||
|
return _timeout
|
||||||
|
|
||||||
@override
|
@override
|
||||||
async def request(self, setup: Request) -> Response:
|
async def request(self, setup: Request) -> Response:
|
||||||
if isinstance(setup.timeout, Timeout):
|
|
||||||
timeout = httpx.Timeout(
|
|
||||||
timeout=setup.timeout.total,
|
|
||||||
connect=setup.timeout.connect,
|
|
||||||
read=setup.timeout.read,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
timeout = httpx.Timeout(setup.timeout)
|
|
||||||
|
|
||||||
response = await self.client.request(
|
response = await self.client.request(
|
||||||
setup.method,
|
setup.method,
|
||||||
str(setup.url),
|
str(setup.url),
|
||||||
@@ -112,7 +144,7 @@ class Session(HTTPClientSession):
|
|||||||
params=setup.url.raw_query_string,
|
params=setup.url.raw_query_string,
|
||||||
headers=tuple(setup.headers.items()),
|
headers=tuple(setup.headers.items()),
|
||||||
cookies=setup.cookies.jar,
|
cookies=setup.cookies.jar,
|
||||||
timeout=timeout,
|
timeout=self._get_timeout(setup.timeout),
|
||||||
)
|
)
|
||||||
return Response(
|
return Response(
|
||||||
response.status_code,
|
response.status_code,
|
||||||
@@ -128,15 +160,6 @@ class Session(HTTPClientSession):
|
|||||||
*,
|
*,
|
||||||
chunk_size: int = 1024,
|
chunk_size: int = 1024,
|
||||||
) -> AsyncGenerator[Response, None]:
|
) -> AsyncGenerator[Response, None]:
|
||||||
if isinstance(setup.timeout, Timeout):
|
|
||||||
timeout = httpx.Timeout(
|
|
||||||
timeout=setup.timeout.total,
|
|
||||||
connect=setup.timeout.connect,
|
|
||||||
read=setup.timeout.read,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
timeout = httpx.Timeout(setup.timeout)
|
|
||||||
|
|
||||||
async with self.client.stream(
|
async with self.client.stream(
|
||||||
setup.method,
|
setup.method,
|
||||||
str(setup.url),
|
str(setup.url),
|
||||||
@@ -148,7 +171,7 @@ class Session(HTTPClientSession):
|
|||||||
params=setup.url.raw_query_string,
|
params=setup.url.raw_query_string,
|
||||||
headers=tuple(setup.headers.items()),
|
headers=tuple(setup.headers.items()),
|
||||||
cookies=setup.cookies.jar,
|
cookies=setup.cookies.jar,
|
||||||
timeout=timeout,
|
timeout=self._get_timeout(setup.timeout),
|
||||||
) as response:
|
) as response:
|
||||||
response_headers = response.headers.multi_items()
|
response_headers = response.headers.multi_items()
|
||||||
async for chunk in response.aiter_bytes(chunk_size=chunk_size):
|
async for chunk in response.aiter_bytes(chunk_size=chunk_size):
|
||||||
@@ -216,9 +239,9 @@ class Mixin(HTTPClientMixin):
|
|||||||
params: QueryTypes = None,
|
params: QueryTypes = None,
|
||||||
headers: HeaderTypes = None,
|
headers: HeaderTypes = None,
|
||||||
cookies: CookieTypes = None,
|
cookies: CookieTypes = None,
|
||||||
version: Union[str, HTTPVersion] = HTTPVersion.H11,
|
version: str | HTTPVersion = HTTPVersion.H11,
|
||||||
timeout: TimeoutTypes = None,
|
timeout: TimeoutTypes = None,
|
||||||
proxy: Optional[str] = None,
|
proxy: str | None = None,
|
||||||
) -> Session:
|
) -> Session:
|
||||||
return Session(
|
return Session(
|
||||||
params=params,
|
params=params,
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ FrontMatter:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import signal
|
import signal
|
||||||
from typing import Optional
|
|
||||||
from typing_extensions import override
|
from typing_extensions import override
|
||||||
|
|
||||||
import anyio
|
import anyio
|
||||||
@@ -112,7 +111,7 @@ class Driver(BaseDriver):
|
|||||||
if not self.should_exit.is_set():
|
if not self.should_exit.is_set():
|
||||||
logger.info("Application startup completed.")
|
logger.info("Application startup completed.")
|
||||||
|
|
||||||
async def _listen_exit(self, tg: Optional[TaskGroup] = None):
|
async def _listen_exit(self, tg: TaskGroup | None = None):
|
||||||
await self.should_exit.wait()
|
await self.should_exit.wait()
|
||||||
|
|
||||||
if tg is not None:
|
if tg is not None:
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ FrontMatter:
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from typing import Any, Optional, Union, cast
|
from typing import Any, cast
|
||||||
from typing_extensions import override
|
from typing_extensions import override
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
@@ -65,13 +65,13 @@ class Config(BaseModel):
|
|||||||
|
|
||||||
quart_reload: bool = False
|
quart_reload: bool = False
|
||||||
"""开启/关闭冷重载"""
|
"""开启/关闭冷重载"""
|
||||||
quart_reload_dirs: Optional[list[str]] = None
|
quart_reload_dirs: list[str] | None = None
|
||||||
"""重载监控文件夹列表,默认为 uvicorn 默认值"""
|
"""重载监控文件夹列表,默认为 uvicorn 默认值"""
|
||||||
quart_reload_delay: float = 0.25
|
quart_reload_delay: float = 0.25
|
||||||
"""重载延迟,默认为 uvicorn 默认值"""
|
"""重载延迟,默认为 uvicorn 默认值"""
|
||||||
quart_reload_includes: Optional[list[str]] = None
|
quart_reload_includes: list[str] | None = None
|
||||||
"""要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值"""
|
"""要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值"""
|
||||||
quart_reload_excludes: Optional[list[str]] = None
|
quart_reload_excludes: list[str] | None = None
|
||||||
"""不要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值"""
|
"""不要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值"""
|
||||||
quart_extra: dict[str, Any] = {}
|
quart_extra: dict[str, Any] = {}
|
||||||
"""传递给 `Quart` 的其他参数。"""
|
"""传递给 `Quart` 的其他参数。"""
|
||||||
@@ -141,10 +141,10 @@ class Driver(BaseDriver, ASGIMixin):
|
|||||||
@override
|
@override
|
||||||
def run(
|
def run(
|
||||||
self,
|
self,
|
||||||
host: Optional[str] = None,
|
host: str | None = None,
|
||||||
port: Optional[int] = None,
|
port: int | None = None,
|
||||||
*args,
|
*args,
|
||||||
app: Optional[str] = None,
|
app: str | None = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
"""使用 `uvicorn` 启动 Quart"""
|
"""使用 `uvicorn` 启动 Quart"""
|
||||||
@@ -257,7 +257,7 @@ class WebSocket(BaseWebSocket):
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
@catch_closed
|
@catch_closed
|
||||||
async def receive(self) -> Union[str, bytes]:
|
async def receive(self) -> str | bytes:
|
||||||
return await self.websocket.receive()
|
return await self.websocket.receive()
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -17,19 +17,26 @@ FrontMatter:
|
|||||||
description: nonebot.drivers.websockets 模块
|
description: nonebot.drivers.websockets 模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from collections.abc import AsyncGenerator
|
from collections.abc import AsyncGenerator, Callable
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
import logging
|
import logging
|
||||||
from types import CoroutineType
|
from types import CoroutineType
|
||||||
from typing import TYPE_CHECKING, Any, Callable, TypeVar, Union
|
from typing import TYPE_CHECKING, Any, TypeVar
|
||||||
from typing_extensions import ParamSpec, override
|
from typing_extensions import ParamSpec, override
|
||||||
|
|
||||||
from nonebot.drivers import Request, Timeout, WebSocketClientMixin, combine_driver
|
from nonebot.drivers import (
|
||||||
|
DEFAULT_TIMEOUT,
|
||||||
|
Request,
|
||||||
|
Timeout,
|
||||||
|
WebSocketClientMixin,
|
||||||
|
combine_driver,
|
||||||
|
)
|
||||||
from nonebot.drivers import WebSocket as BaseWebSocket
|
from nonebot.drivers import WebSocket as BaseWebSocket
|
||||||
from nonebot.drivers.none import Driver as NoneDriver
|
from nonebot.drivers.none import Driver as NoneDriver
|
||||||
from nonebot.exception import WebSocketClosed
|
from nonebot.exception import WebSocketClosed
|
||||||
from nonebot.log import LoguruHandler
|
from nonebot.log import LoguruHandler
|
||||||
|
from nonebot.utils import UNSET, UnsetType, exclude_unset
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from websockets import ClientConnection, ConnectionClosed, connect
|
from websockets import ClientConnection, ConnectionClosed, connect
|
||||||
@@ -70,16 +77,45 @@ class Mixin(WebSocketClientMixin):
|
|||||||
@override
|
@override
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def websocket(self, setup: Request) -> AsyncGenerator["WebSocket", None]:
|
async def websocket(self, setup: Request) -> AsyncGenerator["WebSocket", None]:
|
||||||
|
timeout_kwargs: dict[str, float | None | UnsetType] = {}
|
||||||
if isinstance(setup.timeout, Timeout):
|
if isinstance(setup.timeout, Timeout):
|
||||||
timeout = setup.timeout.total or setup.timeout.connect or setup.timeout.read
|
open_timeout = (
|
||||||
else:
|
setup.timeout.connect or setup.timeout.read or setup.timeout.total
|
||||||
timeout = setup.timeout
|
)
|
||||||
|
timeout_kwargs = {
|
||||||
|
"open_timeout": open_timeout,
|
||||||
|
"close_timeout": setup.timeout.close,
|
||||||
|
"ping_timeout": setup.timeout.ping,
|
||||||
|
}
|
||||||
|
|
||||||
|
elif setup.timeout is not UNSET:
|
||||||
|
timeout_kwargs = {
|
||||||
|
"open_timeout": setup.timeout,
|
||||||
|
"close_timeout": setup.timeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
if not timeout_kwargs:
|
||||||
|
open_timeout = (
|
||||||
|
DEFAULT_TIMEOUT.connect or DEFAULT_TIMEOUT.read or DEFAULT_TIMEOUT.total
|
||||||
|
)
|
||||||
|
timeout_kwargs = {
|
||||||
|
"open_timeout": open_timeout,
|
||||||
|
"close_timeout": DEFAULT_TIMEOUT.close,
|
||||||
|
"ping_timeout": DEFAULT_TIMEOUT.ping,
|
||||||
|
}
|
||||||
|
|
||||||
|
kwargs = exclude_unset(
|
||||||
|
{
|
||||||
|
**timeout_kwargs,
|
||||||
|
"ping_interval": setup.ping_interval,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
connection = connect(
|
connection = connect(
|
||||||
str(setup.url),
|
str(setup.url),
|
||||||
additional_headers={**setup.headers, **setup.cookies.as_header(setup)},
|
additional_headers={**setup.headers, **setup.cookies.as_header(setup)},
|
||||||
proxy=setup.proxy if setup.proxy is not None else True,
|
proxy=setup.proxy if setup.proxy is not None else True,
|
||||||
open_timeout=timeout,
|
**kwargs, # type: ignore
|
||||||
)
|
)
|
||||||
async with connection as ws:
|
async with connection as ws:
|
||||||
yield WebSocket(request=setup, websocket=ws)
|
yield WebSocket(request=setup, websocket=ws)
|
||||||
@@ -108,7 +144,7 @@ class WebSocket(BaseWebSocket):
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
@catch_closed
|
@catch_closed
|
||||||
async def receive(self) -> Union[str, bytes]:
|
async def receive(self) -> str | bytes:
|
||||||
return await self.websocket.recv()
|
return await self.websocket.recv()
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ FrontMatter:
|
|||||||
description: nonebot.exception 模块
|
description: nonebot.exception 模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Any, Optional
|
from typing import Any
|
||||||
|
|
||||||
from nonebot.compat import ModelField
|
from nonebot.compat import ModelField
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ class NoneBotException(Exception):
|
|||||||
class ParserExit(NoneBotException):
|
class ParserExit(NoneBotException):
|
||||||
"""{ref}`nonebot.rule.shell_command` 处理消息失败时返回的异常。"""
|
"""{ref}`nonebot.rule.shell_command` 处理消息失败时返回的异常。"""
|
||||||
|
|
||||||
def __init__(self, status: int = 0, message: Optional[str] = None) -> None:
|
def __init__(self, status: int = 0, message: str | None = None) -> None:
|
||||||
self.status = status
|
self.status = status
|
||||||
self.message = message
|
self.message = message
|
||||||
|
|
||||||
@@ -232,7 +232,7 @@ class DriverException(NoneBotException):
|
|||||||
class WebSocketClosed(DriverException):
|
class WebSocketClosed(DriverException):
|
||||||
"""WebSocket 连接已关闭。"""
|
"""WebSocket 连接已关闭。"""
|
||||||
|
|
||||||
def __init__(self, code: int, reason: Optional[str] = None) -> None:
|
def __init__(self, code: int, reason: str | None = None) -> None:
|
||||||
self.code = code
|
self.code = code
|
||||||
self.reason = reason
|
self.reason = reason
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import abc
|
import abc
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from typing import TYPE_CHECKING, Any, ClassVar, Optional, Protocol, Union
|
from typing import TYPE_CHECKING, Any, ClassVar, Protocol
|
||||||
|
|
||||||
import anyio
|
import anyio
|
||||||
from exceptiongroup import BaseExceptionGroup, catch
|
from exceptiongroup import BaseExceptionGroup, catch
|
||||||
@@ -77,7 +77,7 @@ class Bot(abc.ABC):
|
|||||||
|
|
||||||
result: Any = None
|
result: Any = None
|
||||||
skip_calling_api: bool = False
|
skip_calling_api: bool = False
|
||||||
exception: Optional[Exception] = None
|
exception: Exception | None = None
|
||||||
|
|
||||||
if self._calling_api_hook:
|
if self._calling_api_hook:
|
||||||
logger.debug("Running CallingAPI hooks...")
|
logger.debug("Running CallingAPI hooks...")
|
||||||
@@ -180,7 +180,7 @@ class Bot(abc.ABC):
|
|||||||
async def send(
|
async def send(
|
||||||
self,
|
self,
|
||||||
event: "Event",
|
event: "Event",
|
||||||
message: Union[str, "Message", "MessageSegment"],
|
message: "str | Message | MessageSegment",
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""调用机器人基础发送消息接口
|
"""调用机器人基础发送消息接口
|
||||||
|
|||||||
@@ -5,11 +5,9 @@ from dataclasses import asdict, dataclass, field
|
|||||||
from typing import ( # noqa: UP035
|
from typing import ( # noqa: UP035
|
||||||
Any,
|
Any,
|
||||||
Generic,
|
Generic,
|
||||||
Optional,
|
|
||||||
SupportsIndex,
|
SupportsIndex,
|
||||||
Type,
|
Type,
|
||||||
TypeVar,
|
TypeVar,
|
||||||
Union,
|
|
||||||
overload,
|
overload,
|
||||||
)
|
)
|
||||||
from typing_extensions import Self
|
from typing_extensions import Self
|
||||||
@@ -51,10 +49,10 @@ class MessageSegment(abc.ABC, Generic[TM]):
|
|||||||
) -> bool:
|
) -> bool:
|
||||||
return not self == other
|
return not self == other
|
||||||
|
|
||||||
def __add__(self, other: Union[str, Self, Iterable[Self]]) -> TM:
|
def __add__(self, other: str | Self | Iterable[Self]) -> TM:
|
||||||
return self.get_message_class()(self) + other
|
return self.get_message_class()(self) + other
|
||||||
|
|
||||||
def __radd__(self, other: Union[str, Self, Iterable[Self]]) -> TM:
|
def __radd__(self, other: str | Self | Iterable[Self]) -> TM:
|
||||||
return self.get_message_class()(other) + self
|
return self.get_message_class()(other) + self
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -87,7 +85,7 @@ class MessageSegment(abc.ABC, Generic[TM]):
|
|||||||
def items(self):
|
def items(self):
|
||||||
return asdict(self).items()
|
return asdict(self).items()
|
||||||
|
|
||||||
def join(self, iterable: Iterable[Union[Self, TM]]) -> TM:
|
def join(self, iterable: Iterable[Self | TM]) -> TM:
|
||||||
return self.get_message_class()(self).join(iterable)
|
return self.get_message_class()(self).join(iterable)
|
||||||
|
|
||||||
def copy(self) -> Self:
|
def copy(self) -> Self:
|
||||||
@@ -109,7 +107,7 @@ class Message(list[TMS], abc.ABC):
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
message: Union[str, None, Iterable[TMS], TMS] = None,
|
message: str | None | Iterable[TMS] | TMS = None,
|
||||||
):
|
):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
if message is None:
|
if message is None:
|
||||||
@@ -124,7 +122,7 @@ class Message(list[TMS], abc.ABC):
|
|||||||
self.extend(self._construct(message)) # pragma: no cover
|
self.extend(self._construct(message)) # pragma: no cover
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def template(cls, format_string: Union[str, TM]) -> MessageTemplate[Self]:
|
def template(cls, format_string: str | TM) -> MessageTemplate[Self]:
|
||||||
"""创建消息模板。
|
"""创建消息模板。
|
||||||
|
|
||||||
用法和 `str.format` 大致相同,支持以 `Message` 对象作为消息模板并输出消息对象。
|
用法和 `str.format` 大致相同,支持以 `Message` 对象作为消息模板并输出消息对象。
|
||||||
@@ -177,17 +175,17 @@ class Message(list[TMS], abc.ABC):
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def __add__( # pyright: ignore[reportIncompatibleMethodOverride]
|
def __add__( # pyright: ignore[reportIncompatibleMethodOverride]
|
||||||
self, other: Union[str, TMS, Iterable[TMS]]
|
self, other: str | TMS | Iterable[TMS]
|
||||||
) -> Self:
|
) -> Self:
|
||||||
result = self.copy()
|
result = self.copy()
|
||||||
result += other
|
result += other
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def __radd__(self, other: Union[str, TMS, Iterable[TMS]]) -> Self:
|
def __radd__(self, other: str | TMS | Iterable[TMS]) -> Self:
|
||||||
result = self.__class__(other)
|
result = self.__class__(other)
|
||||||
return result + self
|
return result + self
|
||||||
|
|
||||||
def __iadd__(self, other: Union[str, TMS, Iterable[TMS]]) -> Self:
|
def __iadd__(self, other: str | TMS | Iterable[TMS]) -> Self:
|
||||||
if isinstance(other, str):
|
if isinstance(other, str):
|
||||||
self.extend(self._construct(other))
|
self.extend(self._construct(other))
|
||||||
elif isinstance(other, MessageSegment):
|
elif isinstance(other, MessageSegment):
|
||||||
@@ -255,14 +253,8 @@ class Message(list[TMS], abc.ABC):
|
|||||||
|
|
||||||
def __getitem__( # pyright: ignore[reportIncompatibleMethodOverride]
|
def __getitem__( # pyright: ignore[reportIncompatibleMethodOverride]
|
||||||
self,
|
self,
|
||||||
args: Union[
|
args: str | tuple[str, int] | tuple[str, slice] | int | slice,
|
||||||
str,
|
) -> TMS | Self:
|
||||||
tuple[str, int],
|
|
||||||
tuple[str, slice],
|
|
||||||
int,
|
|
||||||
slice,
|
|
||||||
],
|
|
||||||
) -> Union[TMS, Self]:
|
|
||||||
arg1, arg2 = args if isinstance(args, tuple) else (args, None)
|
arg1, arg2 = args if isinstance(args, tuple) else (args, None)
|
||||||
if isinstance(arg1, int) and arg2 is None:
|
if isinstance(arg1, int) and arg2 is None:
|
||||||
return super().__getitem__(arg1)
|
return super().__getitem__(arg1)
|
||||||
@@ -278,7 +270,7 @@ class Message(list[TMS], abc.ABC):
|
|||||||
raise ValueError("Incorrect arguments to slice") # pragma: no cover
|
raise ValueError("Incorrect arguments to slice") # pragma: no cover
|
||||||
|
|
||||||
def __contains__( # pyright: ignore[reportIncompatibleMethodOverride]
|
def __contains__( # pyright: ignore[reportIncompatibleMethodOverride]
|
||||||
self, value: Union[TMS, str]
|
self, value: TMS | str
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""检查消息段是否存在
|
"""检查消息段是否存在
|
||||||
|
|
||||||
@@ -291,11 +283,11 @@ class Message(list[TMS], abc.ABC):
|
|||||||
return next((seg for seg in self if seg.type == value), None) is not None
|
return next((seg for seg in self if seg.type == value), None) is not None
|
||||||
return super().__contains__(value)
|
return super().__contains__(value)
|
||||||
|
|
||||||
def has(self, value: Union[TMS, str]) -> bool:
|
def has(self, value: TMS | str) -> bool:
|
||||||
"""与 {ref}``__contains__` <nonebot.adapters.Message.__contains__>` 相同"""
|
"""与 {ref}``__contains__` <nonebot.adapters.Message.__contains__>` 相同"""
|
||||||
return value in self
|
return value in self
|
||||||
|
|
||||||
def index(self, value: Union[TMS, str], *args: SupportsIndex) -> int:
|
def index(self, value: TMS | str, *args: SupportsIndex) -> int:
|
||||||
"""索引消息段
|
"""索引消息段
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -315,7 +307,7 @@ class Message(list[TMS], abc.ABC):
|
|||||||
return super().index(first_segment, *args)
|
return super().index(first_segment, *args)
|
||||||
return super().index(value, *args)
|
return super().index(value, *args)
|
||||||
|
|
||||||
def get(self, type_: str, count: Optional[int] = None) -> Self:
|
def get(self, type_: str, count: int | None = None) -> Self:
|
||||||
"""获取指定类型的消息段
|
"""获取指定类型的消息段
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -339,7 +331,7 @@ class Message(list[TMS], abc.ABC):
|
|||||||
filtered.append(seg)
|
filtered.append(seg)
|
||||||
return filtered
|
return filtered
|
||||||
|
|
||||||
def count(self, value: Union[TMS, str]) -> int:
|
def count(self, value: TMS | str) -> int:
|
||||||
"""计算指定消息段的个数
|
"""计算指定消息段的个数
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -350,7 +342,7 @@ class Message(list[TMS], abc.ABC):
|
|||||||
"""
|
"""
|
||||||
return len(self[value]) if isinstance(value, str) else super().count(value)
|
return len(self[value]) if isinstance(value, str) else super().count(value)
|
||||||
|
|
||||||
def only(self, value: Union[TMS, str]) -> bool:
|
def only(self, value: TMS | str) -> bool:
|
||||||
"""检查消息中是否仅包含指定消息段
|
"""检查消息中是否仅包含指定消息段
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -364,7 +356,7 @@ class Message(list[TMS], abc.ABC):
|
|||||||
return all(seg == value for seg in self)
|
return all(seg == value for seg in self)
|
||||||
|
|
||||||
def append( # pyright: ignore[reportIncompatibleMethodOverride]
|
def append( # pyright: ignore[reportIncompatibleMethodOverride]
|
||||||
self, obj: Union[str, TMS]
|
self, obj: str | TMS
|
||||||
) -> Self:
|
) -> Self:
|
||||||
"""添加一个消息段到消息数组末尾。
|
"""添加一个消息段到消息数组末尾。
|
||||||
|
|
||||||
@@ -380,7 +372,7 @@ class Message(list[TMS], abc.ABC):
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
def extend( # pyright: ignore[reportIncompatibleMethodOverride]
|
def extend( # pyright: ignore[reportIncompatibleMethodOverride]
|
||||||
self, obj: Union[Self, Iterable[TMS]]
|
self, obj: Self | Iterable[TMS]
|
||||||
) -> Self:
|
) -> Self:
|
||||||
"""拼接一个消息数组或多个消息段到消息数组末尾。
|
"""拼接一个消息数组或多个消息段到消息数组末尾。
|
||||||
|
|
||||||
@@ -391,7 +383,7 @@ class Message(list[TMS], abc.ABC):
|
|||||||
self.append(segment)
|
self.append(segment)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def join(self, iterable: Iterable[Union[TMS, Self]]) -> Self:
|
def join(self, iterable: Iterable[TMS | Self]) -> Self:
|
||||||
"""将多个消息连接并将自身作为分割
|
"""将多个消息连接并将自身作为分割
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
from _string import formatter_field_name_split # type: ignore
|
from _string import formatter_field_name_split # type: ignore
|
||||||
from collections.abc import Mapping, Sequence
|
from collections.abc import Callable, Mapping, Sequence
|
||||||
import functools
|
import functools
|
||||||
from string import Formatter
|
from string import Formatter
|
||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
Any,
|
Any,
|
||||||
Callable,
|
|
||||||
Generic,
|
Generic,
|
||||||
Optional,
|
TypeAlias,
|
||||||
TypeVar,
|
TypeVar,
|
||||||
Union,
|
|
||||||
cast,
|
cast,
|
||||||
overload,
|
overload,
|
||||||
)
|
)
|
||||||
from typing_extensions import TypeAlias
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .message import Message, MessageSegment
|
from .message import Message, MessageSegment
|
||||||
@@ -50,15 +47,15 @@ class MessageTemplate(Formatter, Generic[TF]):
|
|||||||
@overload
|
@overload
|
||||||
def __init__(
|
def __init__(
|
||||||
self: "MessageTemplate[TM]",
|
self: "MessageTemplate[TM]",
|
||||||
template: Union[str, TM],
|
template: str | TM,
|
||||||
factory: type[TM],
|
factory: type[TM],
|
||||||
private_getattr: bool = False,
|
private_getattr: bool = False,
|
||||||
) -> None: ...
|
) -> None: ...
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
template: Union[str, TM],
|
template: str | TM,
|
||||||
factory: Union[type[str], type[TM]] = str,
|
factory: type[str] | type[TM] = str,
|
||||||
private_getattr: bool = False,
|
private_getattr: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.template: TF = template # type: ignore
|
self.template: TF = template # type: ignore
|
||||||
@@ -70,7 +67,7 @@ class MessageTemplate(Formatter, Generic[TF]):
|
|||||||
return f"MessageTemplate({self.template!r}, factory={self.factory!r})"
|
return f"MessageTemplate({self.template!r}, factory={self.factory!r})"
|
||||||
|
|
||||||
def add_format_spec(
|
def add_format_spec(
|
||||||
self, spec: FormatSpecFunc_T, name: Optional[str] = None
|
self, spec: FormatSpecFunc_T, name: str | None = None
|
||||||
) -> FormatSpecFunc_T:
|
) -> FormatSpecFunc_T:
|
||||||
name = name or spec.__name__
|
name = name or spec.__name__
|
||||||
if name in self.format_specs:
|
if name in self.format_specs:
|
||||||
@@ -126,7 +123,7 @@ class MessageTemplate(Formatter, Generic[TF]):
|
|||||||
format_string: str,
|
format_string: str,
|
||||||
args: Sequence[Any],
|
args: Sequence[Any],
|
||||||
kwargs: Mapping[str, Any],
|
kwargs: Mapping[str, Any],
|
||||||
used_args: set[Union[int, str]],
|
used_args: set[int | str],
|
||||||
auto_arg_index: int = 0,
|
auto_arg_index: int = 0,
|
||||||
) -> tuple[TF, int]:
|
) -> tuple[TF, int]:
|
||||||
results: list[Any] = [self.factory()]
|
results: list[Any] = [self.factory()]
|
||||||
@@ -180,7 +177,7 @@ class MessageTemplate(Formatter, Generic[TF]):
|
|||||||
|
|
||||||
def get_field(
|
def get_field(
|
||||||
self, field_name: str, args: Sequence[Any], kwargs: Mapping[str, Any]
|
self, field_name: str, args: Sequence[Any], kwargs: Mapping[str, Any]
|
||||||
) -> tuple[Any, Union[int, str]]:
|
) -> tuple[Any, int | str]:
|
||||||
first, rest = formatter_field_name_split(field_name)
|
first, rest = formatter_field_name_split(field_name)
|
||||||
obj = self.get_value(first, args, kwargs)
|
obj = self.get_value(first, args, kwargs)
|
||||||
|
|
||||||
@@ -192,7 +189,7 @@ class MessageTemplate(Formatter, Generic[TF]):
|
|||||||
return obj, first
|
return obj, first
|
||||||
|
|
||||||
def format_field(self, value: Any, format_spec: str) -> Any:
|
def format_field(self, value: Any, format_spec: str) -> Any:
|
||||||
formatter: Optional[FormatSpecFunc] = self.format_specs.get(format_spec)
|
formatter: FormatSpecFunc | None = self.format_specs.get(format_spec)
|
||||||
if formatter is None and not issubclass(self.factory, str):
|
if formatter is None and not issubclass(self.factory, str):
|
||||||
segment_class: type["MessageSegment"] = self.factory.get_segment_class()
|
segment_class: type["MessageSegment"] = self.factory.get_segment_class()
|
||||||
method = getattr(segment_class, format_spec, None)
|
method = getattr(segment_class, format_spec, None)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from .abstract import ReverseDriver as ReverseDriver
|
|||||||
from .abstract import ReverseMixin as ReverseMixin
|
from .abstract import ReverseMixin as ReverseMixin
|
||||||
from .abstract import WebSocketClientMixin as WebSocketClientMixin
|
from .abstract import WebSocketClientMixin as WebSocketClientMixin
|
||||||
from .combine import combine_driver as combine_driver
|
from .combine import combine_driver as combine_driver
|
||||||
|
from .model import DEFAULT_TIMEOUT as DEFAULT_TIMEOUT
|
||||||
from .model import URL as URL
|
from .model import URL as URL
|
||||||
from .model import ContentTypes as ContentTypes
|
from .model import ContentTypes as ContentTypes
|
||||||
from .model import Cookies as Cookies
|
from .model import Cookies as Cookies
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
from collections.abc import Awaitable, Iterable
|
from collections.abc import Awaitable, Callable, Iterable
|
||||||
from types import TracebackType
|
from types import TracebackType
|
||||||
from typing import Any, Callable, Optional, Union, cast
|
from typing import Any, TypeAlias, cast
|
||||||
from typing_extensions import TypeAlias
|
|
||||||
|
|
||||||
import anyio
|
import anyio
|
||||||
from anyio.abc import TaskGroup
|
from anyio.abc import TaskGroup
|
||||||
@@ -11,12 +10,12 @@ from nonebot.utils import is_coroutine_callable, run_sync
|
|||||||
|
|
||||||
SYNC_LIFESPAN_FUNC: TypeAlias = Callable[[], Any]
|
SYNC_LIFESPAN_FUNC: TypeAlias = Callable[[], Any]
|
||||||
ASYNC_LIFESPAN_FUNC: TypeAlias = Callable[[], Awaitable[Any]]
|
ASYNC_LIFESPAN_FUNC: TypeAlias = Callable[[], Awaitable[Any]]
|
||||||
LIFESPAN_FUNC: TypeAlias = Union[SYNC_LIFESPAN_FUNC, ASYNC_LIFESPAN_FUNC]
|
LIFESPAN_FUNC: TypeAlias = SYNC_LIFESPAN_FUNC | ASYNC_LIFESPAN_FUNC
|
||||||
|
|
||||||
|
|
||||||
class Lifespan:
|
class Lifespan:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._task_group: Optional[TaskGroup] = None
|
self._task_group: TaskGroup | None = None
|
||||||
|
|
||||||
self._startup_funcs: list[LIFESPAN_FUNC] = []
|
self._startup_funcs: list[LIFESPAN_FUNC] = []
|
||||||
self._ready_funcs: list[LIFESPAN_FUNC] = []
|
self._ready_funcs: list[LIFESPAN_FUNC] = []
|
||||||
@@ -72,9 +71,9 @@ class Lifespan:
|
|||||||
async def shutdown(
|
async def shutdown(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
exc_type: Optional[type[BaseException]] = None,
|
exc_type: type[BaseException] | None = None,
|
||||||
exc_val: Optional[BaseException] = None,
|
exc_val: BaseException | None = None,
|
||||||
exc_tb: Optional[TracebackType] = None,
|
exc_tb: TracebackType | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
if self._shutdown_funcs:
|
if self._shutdown_funcs:
|
||||||
# reverse shutdown funcs to ensure stack order
|
# reverse shutdown funcs to ensure stack order
|
||||||
@@ -93,8 +92,8 @@ class Lifespan:
|
|||||||
|
|
||||||
async def __aexit__(
|
async def __aexit__(
|
||||||
self,
|
self,
|
||||||
exc_type: Optional[type[BaseException]],
|
exc_type: type[BaseException] | None,
|
||||||
exc_val: Optional[BaseException],
|
exc_val: BaseException | None,
|
||||||
exc_tb: Optional[TracebackType],
|
exc_tb: TracebackType | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
await self.shutdown(exc_type=exc_type, exc_val=exc_val, exc_tb=exc_tb)
|
await self.shutdown(exc_type=exc_type, exc_val=exc_val, exc_tb=exc_tb)
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import abc
|
|||||||
from collections.abc import AsyncGenerator
|
from collections.abc import AsyncGenerator
|
||||||
from contextlib import AsyncExitStack, asynccontextmanager
|
from contextlib import AsyncExitStack, asynccontextmanager
|
||||||
from types import TracebackType
|
from types import TracebackType
|
||||||
from typing import TYPE_CHECKING, Any, ClassVar, Optional, Union
|
from typing import TYPE_CHECKING, Any, ClassVar, TypeAlias
|
||||||
from typing_extensions import Self, TypeAlias
|
from typing_extensions import Self
|
||||||
|
|
||||||
from anyio import CancelScope, create_task_group
|
from anyio import CancelScope, create_task_group
|
||||||
from anyio.abc import TaskGroup
|
from anyio.abc import TaskGroup
|
||||||
@@ -19,7 +19,13 @@ from nonebot.typing import (
|
|||||||
T_BotDisconnectionHook,
|
T_BotDisconnectionHook,
|
||||||
T_DependencyCache,
|
T_DependencyCache,
|
||||||
)
|
)
|
||||||
from nonebot.utils import escape_tag, flatten_exception_group, run_coro_with_catch
|
from nonebot.utils import (
|
||||||
|
UNSET,
|
||||||
|
UnsetType,
|
||||||
|
escape_tag,
|
||||||
|
flatten_exception_group,
|
||||||
|
run_coro_with_catch,
|
||||||
|
)
|
||||||
|
|
||||||
from ._lifespan import LIFESPAN_FUNC, Lifespan
|
from ._lifespan import LIFESPAN_FUNC, Lifespan
|
||||||
from .model import (
|
from .model import (
|
||||||
@@ -245,9 +251,9 @@ class HTTPClientSession(abc.ABC):
|
|||||||
params: QueryTypes = None,
|
params: QueryTypes = None,
|
||||||
headers: HeaderTypes = None,
|
headers: HeaderTypes = None,
|
||||||
cookies: CookieTypes = None,
|
cookies: CookieTypes = None,
|
||||||
version: Union[str, HTTPVersion] = HTTPVersion.H11,
|
version: str | HTTPVersion = HTTPVersion.H11,
|
||||||
timeout: TimeoutTypes = None,
|
timeout: TimeoutTypes | UnsetType = UNSET,
|
||||||
proxy: Optional[str] = None,
|
proxy: str | None = None,
|
||||||
):
|
):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@@ -283,9 +289,9 @@ class HTTPClientSession(abc.ABC):
|
|||||||
|
|
||||||
async def __aexit__(
|
async def __aexit__(
|
||||||
self,
|
self,
|
||||||
exc_type: Optional[type[BaseException]],
|
exc_type: type[BaseException] | None,
|
||||||
exc: Optional[BaseException],
|
exc: BaseException | None,
|
||||||
tb: Optional[TracebackType],
|
tb: TracebackType | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
await self.close()
|
await self.close()
|
||||||
|
|
||||||
@@ -315,9 +321,9 @@ class HTTPClientMixin(ForwardMixin):
|
|||||||
params: QueryTypes = None,
|
params: QueryTypes = None,
|
||||||
headers: HeaderTypes = None,
|
headers: HeaderTypes = None,
|
||||||
cookies: CookieTypes = None,
|
cookies: CookieTypes = None,
|
||||||
version: Union[str, HTTPVersion] = HTTPVersion.H11,
|
version: str | HTTPVersion = HTTPVersion.H11,
|
||||||
timeout: TimeoutTypes = None,
|
timeout: TimeoutTypes = None,
|
||||||
proxy: Optional[str] = None,
|
proxy: str | None = None,
|
||||||
) -> HTTPClientSession:
|
) -> HTTPClientSession:
|
||||||
"""获取一个 HTTP 会话"""
|
"""获取一个 HTTP 会话"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from typing import TYPE_CHECKING, TypeVar, Union, overload
|
from typing import TYPE_CHECKING, TypeVar, overload
|
||||||
|
|
||||||
from .abstract import Driver, Mixin
|
from .abstract import Driver, Mixin
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ def combine_driver(
|
|||||||
|
|
||||||
def combine_driver(
|
def combine_driver(
|
||||||
driver: type[D], *mixins: type[Mixin]
|
driver: type[D], *mixins: type[Mixin]
|
||||||
) -> Union[type[D], type["CombinedDriver"]]:
|
) -> type[D] | type["CombinedDriver"]:
|
||||||
"""将一个驱动器和多个混入类合并。"""
|
"""将一个驱动器和多个混入类合并。"""
|
||||||
# check first
|
# check first
|
||||||
if not issubclass(driver, Driver):
|
if not issubclass(driver, Driver):
|
||||||
|
|||||||
@@ -1,48 +1,59 @@
|
|||||||
import abc
|
import abc
|
||||||
from collections.abc import Awaitable, Iterator, Mapping, MutableMapping
|
from collections.abc import Awaitable, Callable, Iterator, Mapping, MutableMapping
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from http.cookiejar import Cookie, CookieJar
|
from http.cookiejar import Cookie, CookieJar
|
||||||
from typing import IO, Any, Callable, Optional, Union
|
from typing import IO, Any, TypeAlias
|
||||||
from typing_extensions import TypeAlias
|
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
from multidict import CIMultiDict
|
from multidict import CIMultiDict
|
||||||
from yarl import URL as URL
|
from yarl import URL as URL
|
||||||
|
|
||||||
RawURL: TypeAlias = tuple[bytes, bytes, Optional[int], bytes]
|
from nonebot.utils import UNSET, UnsetType
|
||||||
|
|
||||||
SimpleQuery: TypeAlias = Union[str, int, float]
|
|
||||||
QueryVariable: TypeAlias = Union[SimpleQuery, list[SimpleQuery]]
|
|
||||||
QueryTypes: TypeAlias = Union[
|
|
||||||
None, str, Mapping[str, QueryVariable], list[tuple[str, SimpleQuery]]
|
|
||||||
]
|
|
||||||
|
|
||||||
HeaderTypes: TypeAlias = Union[
|
@dataclass
|
||||||
None,
|
class Timeout:
|
||||||
CIMultiDict[str],
|
"""Request 超时配置。"""
|
||||||
dict[str, str],
|
|
||||||
list[tuple[str, str]],
|
|
||||||
]
|
|
||||||
|
|
||||||
CookieTypes: TypeAlias = Union[
|
total: float | None | UnsetType = UNSET
|
||||||
None, "Cookies", CookieJar, dict[str, str], list[tuple[str, str]]
|
connect: float | None | UnsetType = UNSET
|
||||||
]
|
read: float | None | UnsetType = UNSET
|
||||||
|
close: float | None | UnsetType = UNSET
|
||||||
|
ping: float | None | UnsetType = UNSET
|
||||||
|
|
||||||
ContentTypes: TypeAlias = Union[str, bytes, None]
|
|
||||||
DataTypes: TypeAlias = Union[dict, None]
|
DEFAULT_TIMEOUT = Timeout(total=None, connect=5.0, read=30.0, close=10.0, ping=20.0)
|
||||||
FileContent: TypeAlias = Union[IO[bytes], bytes]
|
|
||||||
FileType: TypeAlias = tuple[Optional[str], FileContent, Optional[str]]
|
|
||||||
FileTypes: TypeAlias = Union[
|
RawURL: TypeAlias = tuple[bytes, bytes, int | None, bytes]
|
||||||
# file (or bytes)
|
|
||||||
FileContent,
|
SimpleQuery: TypeAlias = str | int | float
|
||||||
# (filename, file (or bytes))
|
QueryVariable: TypeAlias = SimpleQuery | list[SimpleQuery]
|
||||||
tuple[Optional[str], FileContent],
|
QueryTypes: TypeAlias = (
|
||||||
# (filename, file (or bytes), content_type)
|
None | str | Mapping[str, QueryVariable] | list[tuple[str, SimpleQuery]]
|
||||||
FileType,
|
)
|
||||||
]
|
|
||||||
FilesTypes: TypeAlias = Union[dict[str, FileTypes], list[tuple[str, FileTypes]], None]
|
HeaderTypes: TypeAlias = (
|
||||||
TimeoutTypes: TypeAlias = Union[float, "Timeout", None]
|
None | CIMultiDict[str] | dict[str, str] | list[tuple[str, str]]
|
||||||
|
)
|
||||||
|
|
||||||
|
CookieTypes: TypeAlias = (
|
||||||
|
"None | Cookies | CookieJar | dict[str, str] | list[tuple[str, str]]"
|
||||||
|
)
|
||||||
|
|
||||||
|
ContentTypes: TypeAlias = str | bytes | None
|
||||||
|
DataTypes: TypeAlias = dict | None
|
||||||
|
FileContent: TypeAlias = IO[bytes] | bytes
|
||||||
|
FileType: TypeAlias = tuple[str | None, FileContent, str | None]
|
||||||
|
FileTypes: TypeAlias = (
|
||||||
|
FileContent # file (or bytes)
|
||||||
|
| tuple[str | None, FileContent] # (filename, file (or bytes))
|
||||||
|
| FileType # (filename, file (or bytes), content_type)
|
||||||
|
)
|
||||||
|
FilesTypes: TypeAlias = dict[str, FileTypes] | list[tuple[str, FileTypes]] | None
|
||||||
|
TimeoutTypes: TypeAlias = float | Timeout | None
|
||||||
|
PingIntervalTypes: TypeAlias = float | None
|
||||||
|
|
||||||
|
|
||||||
class HTTPVersion(Enum):
|
class HTTPVersion(Enum):
|
||||||
@@ -51,20 +62,11 @@ class HTTPVersion(Enum):
|
|||||||
H2 = "2"
|
H2 = "2"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Timeout:
|
|
||||||
"""Request 超时配置。"""
|
|
||||||
|
|
||||||
total: Optional[float] = None
|
|
||||||
connect: Optional[float] = None
|
|
||||||
read: Optional[float] = None
|
|
||||||
|
|
||||||
|
|
||||||
class Request:
|
class Request:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
method: Union[str, bytes],
|
method: str | bytes,
|
||||||
url: Union["URL", str, RawURL],
|
url: "URL | str | RawURL",
|
||||||
*,
|
*,
|
||||||
params: QueryTypes = None,
|
params: QueryTypes = None,
|
||||||
headers: HeaderTypes = None,
|
headers: HeaderTypes = None,
|
||||||
@@ -73,9 +75,10 @@ class Request:
|
|||||||
data: DataTypes = None,
|
data: DataTypes = None,
|
||||||
json: Any = None,
|
json: Any = None,
|
||||||
files: FilesTypes = None,
|
files: FilesTypes = None,
|
||||||
version: Union[str, HTTPVersion] = HTTPVersion.H11,
|
version: str | HTTPVersion = HTTPVersion.H11,
|
||||||
timeout: TimeoutTypes = None,
|
timeout: TimeoutTypes | UnsetType = UNSET,
|
||||||
proxy: Optional[str] = None,
|
proxy: str | None = None,
|
||||||
|
ping_interval: PingIntervalTypes | UnsetType = UNSET,
|
||||||
):
|
):
|
||||||
# method
|
# method
|
||||||
self.method: str = (
|
self.method: str = (
|
||||||
@@ -86,9 +89,11 @@ class Request:
|
|||||||
# http version
|
# http version
|
||||||
self.version: HTTPVersion = HTTPVersion(version)
|
self.version: HTTPVersion = HTTPVersion(version)
|
||||||
# timeout
|
# timeout
|
||||||
self.timeout: TimeoutTypes = timeout
|
self.timeout: TimeoutTypes | UnsetType = timeout
|
||||||
# proxy
|
# proxy
|
||||||
self.proxy: Optional[str] = proxy
|
self.proxy: str | None = proxy
|
||||||
|
# ping interval
|
||||||
|
self.ping_interval: PingIntervalTypes | UnsetType = ping_interval
|
||||||
|
|
||||||
# url
|
# url
|
||||||
if isinstance(url, tuple):
|
if isinstance(url, tuple):
|
||||||
@@ -117,7 +122,7 @@ class Request:
|
|||||||
self.content: ContentTypes = content
|
self.content: ContentTypes = content
|
||||||
self.data: DataTypes = data
|
self.data: DataTypes = data
|
||||||
self.json: Any = json
|
self.json: Any = json
|
||||||
self.files: Optional[list[tuple[str, FileType]]] = None
|
self.files: list[tuple[str, FileType]] | None = None
|
||||||
if files:
|
if files:
|
||||||
self.files = []
|
self.files = []
|
||||||
files_ = files.items() if isinstance(files, dict) else files
|
files_ = files.items() if isinstance(files, dict) else files
|
||||||
@@ -140,7 +145,7 @@ class Response:
|
|||||||
*,
|
*,
|
||||||
headers: HeaderTypes = None,
|
headers: HeaderTypes = None,
|
||||||
content: ContentTypes = None,
|
content: ContentTypes = None,
|
||||||
request: Optional[Request] = None,
|
request: Request | None = None,
|
||||||
):
|
):
|
||||||
# status code
|
# status code
|
||||||
self.status_code: int = status_code
|
self.status_code: int = status_code
|
||||||
@@ -153,7 +158,7 @@ class Response:
|
|||||||
self.content: ContentTypes = content
|
self.content: ContentTypes = content
|
||||||
|
|
||||||
# request
|
# request
|
||||||
self.request: Optional[Request] = request
|
self.request: Request | None = request
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"{self.__class__.__name__}(status_code={self.status_code!r})"
|
return f"{self.__class__.__name__}(status_code={self.status_code!r})"
|
||||||
@@ -183,7 +188,7 @@ class WebSocket(abc.ABC):
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
async def receive(self) -> Union[str, bytes]:
|
async def receive(self) -> str | bytes:
|
||||||
"""接收一条 WebSocket text/bytes 信息"""
|
"""接收一条 WebSocket text/bytes 信息"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@@ -197,7 +202,7 @@ class WebSocket(abc.ABC):
|
|||||||
"""接收一条 WebSocket binary 信息"""
|
"""接收一条 WebSocket binary 信息"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
async def send(self, data: Union[str, bytes]) -> None:
|
async def send(self, data: str | bytes) -> None:
|
||||||
"""发送一条 WebSocket text/bytes 信息"""
|
"""发送一条 WebSocket text/bytes 信息"""
|
||||||
if isinstance(data, str):
|
if isinstance(data, str):
|
||||||
await self.send_text(data)
|
await self.send_text(data)
|
||||||
@@ -258,11 +263,11 @@ class Cookies(MutableMapping):
|
|||||||
def get( # pyright: ignore[reportIncompatibleMethodOverride]
|
def get( # pyright: ignore[reportIncompatibleMethodOverride]
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
default: Optional[str] = None,
|
default: str | None = None,
|
||||||
domain: Optional[str] = None,
|
domain: str | None = None,
|
||||||
path: Optional[str] = None,
|
path: str | None = None,
|
||||||
) -> Optional[str]:
|
) -> str | None:
|
||||||
value: Optional[str] = None
|
value: str | None = None
|
||||||
for cookie in self.jar:
|
for cookie in self.jar:
|
||||||
if (
|
if (
|
||||||
cookie.name == name
|
cookie.name == name
|
||||||
@@ -277,7 +282,7 @@ class Cookies(MutableMapping):
|
|||||||
return default if value is None else value
|
return default if value is None else value
|
||||||
|
|
||||||
def delete(
|
def delete(
|
||||||
self, name: str, domain: Optional[str] = None, path: Optional[str] = None
|
self, name: str, domain: str | None = None, path: str | None = None
|
||||||
) -> None:
|
) -> None:
|
||||||
if domain is not None and path is not None:
|
if domain is not None and path is not None:
|
||||||
return self.jar.clear(domain, path, name)
|
return self.jar.clear(domain, path, name)
|
||||||
@@ -293,7 +298,7 @@ class Cookies(MutableMapping):
|
|||||||
for cookie in remove:
|
for cookie in remove:
|
||||||
self.jar.clear(cookie.domain, cookie.path, cookie.name)
|
self.jar.clear(cookie.domain, cookie.path, cookie.name)
|
||||||
|
|
||||||
def clear(self, domain: Optional[str] = None, path: Optional[str] = None) -> None:
|
def clear(self, domain: str | None = None, path: str | None = None) -> None:
|
||||||
self.jar.clear(domain, path)
|
self.jar.clear(domain, path)
|
||||||
|
|
||||||
def update( # pyright: ignore[reportIncompatibleMethodOverride]
|
def update( # pyright: ignore[reportIncompatibleMethodOverride]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from collections.abc import ItemsView, Iterator, KeysView, MutableMapping, ValuesView
|
from collections.abc import ItemsView, Iterator, KeysView, MutableMapping, ValuesView
|
||||||
from typing import TYPE_CHECKING, Optional, TypeVar, Union, overload
|
from typing import TYPE_CHECKING, TypeVar, overload
|
||||||
|
|
||||||
from .provider import DEFAULT_PROVIDER_CLASS, MatcherProvider
|
from .provider import DEFAULT_PROVIDER_CLASS, MatcherProvider
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ class MatcherManager(MutableMapping[int, list[type["Matcher"]]]):
|
|||||||
return self.provider.items()
|
return self.provider.items()
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def get(self, key: int) -> Optional[list[type["Matcher"]]]: ...
|
def get(self, key: int) -> list[type["Matcher"]] | None: ...
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def get(
|
def get(
|
||||||
@@ -60,11 +60,11 @@ class MatcherManager(MutableMapping[int, list[type["Matcher"]]]):
|
|||||||
) -> list[type["Matcher"]]: ...
|
) -> list[type["Matcher"]]: ...
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def get(self, key: int, default: T) -> Union[list[type["Matcher"]], T]: ...
|
def get(self, key: int, default: T) -> list[type["Matcher"]] | T: ...
|
||||||
|
|
||||||
def get(
|
def get(
|
||||||
self, key: int, default: Optional[T] = None
|
self, key: int, default: T | None = None
|
||||||
) -> Optional[Union[list[type["Matcher"]], T]]:
|
) -> list[type["Matcher"]] | T | None:
|
||||||
return self.provider.get(key, default)
|
return self.provider.get(key, default)
|
||||||
|
|
||||||
def pop( # pyright: ignore[reportIncompatibleMethodOverride]
|
def pop( # pyright: ignore[reportIncompatibleMethodOverride]
|
||||||
|
|||||||
@@ -13,10 +13,8 @@ from typing import ( # noqa: UP035
|
|||||||
Callable,
|
Callable,
|
||||||
ClassVar,
|
ClassVar,
|
||||||
NoReturn,
|
NoReturn,
|
||||||
Optional,
|
|
||||||
Type,
|
Type,
|
||||||
TypeVar,
|
TypeVar,
|
||||||
Union,
|
|
||||||
overload,
|
overload,
|
||||||
)
|
)
|
||||||
from typing_extensions import Self
|
from typing_extensions import Self
|
||||||
@@ -87,15 +85,15 @@ current_handler: ContextVar[Dependent[Any]] = ContextVar("current_handler")
|
|||||||
class MatcherSource:
|
class MatcherSource:
|
||||||
"""Matcher 源代码上下文信息"""
|
"""Matcher 源代码上下文信息"""
|
||||||
|
|
||||||
plugin_id: Optional[str] = None
|
plugin_id: str | None = None
|
||||||
"""事件响应器所在插件标识符"""
|
"""事件响应器所在插件标识符"""
|
||||||
module_name: Optional[str] = None
|
module_name: str | None = None
|
||||||
"""事件响应器所在插件模块的路径名"""
|
"""事件响应器所在插件模块的路径名"""
|
||||||
lineno: Optional[int] = None
|
lineno: int | None = None
|
||||||
"""事件响应器所在行号"""
|
"""事件响应器所在行号"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def plugin(self) -> Optional["Plugin"]:
|
def plugin(self) -> "Plugin | None":
|
||||||
"""事件响应器所在插件"""
|
"""事件响应器所在插件"""
|
||||||
from nonebot.plugin import get_plugin
|
from nonebot.plugin import get_plugin
|
||||||
|
|
||||||
@@ -103,17 +101,17 @@ class MatcherSource:
|
|||||||
return get_plugin(self.plugin_id)
|
return get_plugin(self.plugin_id)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def plugin_name(self) -> Optional[str]:
|
def plugin_name(self) -> str | None:
|
||||||
"""事件响应器所在插件名"""
|
"""事件响应器所在插件名"""
|
||||||
return self.plugin and self.plugin.name
|
return self.plugin and self.plugin.name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def module(self) -> Optional[ModuleType]:
|
def module(self) -> ModuleType | None:
|
||||||
if self.module_name is not None:
|
if self.module_name is not None:
|
||||||
return sys.modules.get(self.module_name)
|
return sys.modules.get(self.module_name)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def file(self) -> Optional[Path]:
|
def file(self) -> Path | None:
|
||||||
if self.module is not None and (file := inspect.getsourcefile(self.module)):
|
if self.module is not None and (file := inspect.getsourcefile(self.module)):
|
||||||
return Path(file).absolute()
|
return Path(file).absolute()
|
||||||
|
|
||||||
@@ -121,8 +119,8 @@ class MatcherSource:
|
|||||||
class MatcherMeta(type):
|
class MatcherMeta(type):
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
type: str
|
type: str
|
||||||
_source: Optional[MatcherSource]
|
_source: MatcherSource | None
|
||||||
module_name: Optional[str]
|
module_name: str | None
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return (
|
return (
|
||||||
@@ -140,7 +138,7 @@ class MatcherMeta(type):
|
|||||||
class Matcher(metaclass=MatcherMeta):
|
class Matcher(metaclass=MatcherMeta):
|
||||||
"""事件响应器类"""
|
"""事件响应器类"""
|
||||||
|
|
||||||
_source: ClassVar[Optional[MatcherSource]] = None
|
_source: ClassVar[MatcherSource | None] = None
|
||||||
|
|
||||||
type: ClassVar[str] = ""
|
type: ClassVar[str] = ""
|
||||||
"""事件响应器类型"""
|
"""事件响应器类型"""
|
||||||
@@ -156,15 +154,15 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
"""事件响应器是否阻止事件传播"""
|
"""事件响应器是否阻止事件传播"""
|
||||||
temp: ClassVar[bool] = False
|
temp: ClassVar[bool] = False
|
||||||
"""事件响应器是否为临时"""
|
"""事件响应器是否为临时"""
|
||||||
expire_time: ClassVar[Optional[datetime]] = None
|
expire_time: ClassVar[datetime | None] = None
|
||||||
"""事件响应器过期时间点"""
|
"""事件响应器过期时间点"""
|
||||||
|
|
||||||
_default_state: ClassVar[T_State] = {}
|
_default_state: ClassVar[T_State] = {}
|
||||||
"""事件响应器默认状态"""
|
"""事件响应器默认状态"""
|
||||||
|
|
||||||
_default_type_updater: ClassVar[Optional[Dependent[str]]] = None
|
_default_type_updater: ClassVar[Dependent[str] | None] = None
|
||||||
"""事件响应器类型更新函数"""
|
"""事件响应器类型更新函数"""
|
||||||
_default_permission_updater: ClassVar[Optional[Dependent[Permission]]] = None
|
_default_permission_updater: ClassVar[Dependent[Permission] | None] = None
|
||||||
"""事件响应器权限更新函数"""
|
"""事件响应器权限更新函数"""
|
||||||
|
|
||||||
HANDLER_PARAM_TYPES: ClassVar[tuple[Type[Param], ...]] = ( # noqa: UP006
|
HANDLER_PARAM_TYPES: ClassVar[tuple[Type[Param], ...]] = ( # noqa: UP006
|
||||||
@@ -197,22 +195,22 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
def new(
|
def new(
|
||||||
cls,
|
cls,
|
||||||
type_: str = "",
|
type_: str = "",
|
||||||
rule: Optional[Rule] = None,
|
rule: Rule | None = None,
|
||||||
permission: Optional[Permission] = None,
|
permission: Permission | None = None,
|
||||||
handlers: Optional[list[Union[T_Handler, Dependent[Any]]]] = None,
|
handlers: list[T_Handler | Dependent[Any]] | None = None,
|
||||||
temp: bool = False,
|
temp: bool = False,
|
||||||
priority: int = 1,
|
priority: int = 1,
|
||||||
block: bool = False,
|
block: bool = False,
|
||||||
*,
|
*,
|
||||||
plugin: Optional["Plugin"] = None,
|
plugin: "Plugin | None" = None,
|
||||||
module: Optional[ModuleType] = None,
|
module: ModuleType | None = None,
|
||||||
source: Optional[MatcherSource] = None,
|
source: MatcherSource | None = None,
|
||||||
expire_time: Optional[Union[datetime, timedelta]] = None,
|
expire_time: datetime | timedelta | None = None,
|
||||||
default_state: Optional[T_State] = None,
|
default_state: T_State | None = None,
|
||||||
default_type_updater: Optional[Union[T_TypeUpdater, Dependent[str]]] = None,
|
default_type_updater: T_TypeUpdater | Dependent[str] | None = None,
|
||||||
default_permission_updater: Optional[
|
default_permission_updater: T_PermissionUpdater
|
||||||
Union[T_PermissionUpdater, Dependent[Permission]]
|
| Dependent[Permission]
|
||||||
] = None,
|
| None = None,
|
||||||
) -> Type[Self]: # noqa: UP006
|
) -> Type[Self]: # noqa: UP006
|
||||||
"""
|
"""
|
||||||
创建一个新的事件响应器,并存储至 `matchers <#matchers>`_
|
创建一个新的事件响应器,并存储至 `matchers <#matchers>`_
|
||||||
@@ -332,27 +330,27 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
matchers[cls.priority].remove(cls)
|
matchers[cls.priority].remove(cls)
|
||||||
|
|
||||||
@classproperty
|
@classproperty
|
||||||
def plugin(cls) -> Optional["Plugin"]:
|
def plugin(cls) -> "Plugin | None":
|
||||||
"""事件响应器所在插件"""
|
"""事件响应器所在插件"""
|
||||||
return cls._source and cls._source.plugin
|
return cls._source and cls._source.plugin
|
||||||
|
|
||||||
@classproperty
|
@classproperty
|
||||||
def plugin_id(cls) -> Optional[str]:
|
def plugin_id(cls) -> str | None:
|
||||||
"""事件响应器所在插件标识符"""
|
"""事件响应器所在插件标识符"""
|
||||||
return cls._source and cls._source.plugin_id
|
return cls._source and cls._source.plugin_id
|
||||||
|
|
||||||
@classproperty
|
@classproperty
|
||||||
def plugin_name(cls) -> Optional[str]:
|
def plugin_name(cls) -> str | None:
|
||||||
"""事件响应器所在插件名"""
|
"""事件响应器所在插件名"""
|
||||||
return cls._source and cls._source.plugin_name
|
return cls._source and cls._source.plugin_name
|
||||||
|
|
||||||
@classproperty
|
@classproperty
|
||||||
def module(cls) -> Optional[ModuleType]:
|
def module(cls) -> ModuleType | None:
|
||||||
"""事件响应器所在插件模块"""
|
"""事件响应器所在插件模块"""
|
||||||
return cls._source and cls._source.module
|
return cls._source and cls._source.module
|
||||||
|
|
||||||
@classproperty
|
@classproperty
|
||||||
def module_name(cls) -> Optional[str]:
|
def module_name(cls) -> str | None:
|
||||||
"""事件响应器所在插件模块路径"""
|
"""事件响应器所在插件模块路径"""
|
||||||
return cls._source and cls._source.module_name
|
return cls._source and cls._source.module_name
|
||||||
|
|
||||||
@@ -361,8 +359,8 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
cls,
|
cls,
|
||||||
bot: Bot,
|
bot: Bot,
|
||||||
event: Event,
|
event: Event,
|
||||||
stack: Optional[AsyncExitStack] = None,
|
stack: AsyncExitStack | None = None,
|
||||||
dependency_cache: Optional[T_DependencyCache] = None,
|
dependency_cache: T_DependencyCache | None = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""检查是否满足触发权限
|
"""检查是否满足触发权限
|
||||||
|
|
||||||
@@ -386,8 +384,8 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
bot: Bot,
|
bot: Bot,
|
||||||
event: Event,
|
event: Event,
|
||||||
state: T_State,
|
state: T_State,
|
||||||
stack: Optional[AsyncExitStack] = None,
|
stack: AsyncExitStack | None = None,
|
||||||
dependency_cache: Optional[T_DependencyCache] = None,
|
dependency_cache: T_DependencyCache | None = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""检查是否满足匹配规则
|
"""检查是否满足匹配规则
|
||||||
|
|
||||||
@@ -432,7 +430,7 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def append_handler(
|
def append_handler(
|
||||||
cls, handler: T_Handler, parameterless: Optional[Iterable[Any]] = None
|
cls, handler: T_Handler, parameterless: Iterable[Any] | None = None
|
||||||
) -> Dependent[Any]:
|
) -> Dependent[Any]:
|
||||||
handler_ = Dependent[Any].parse(
|
handler_ = Dependent[Any].parse(
|
||||||
call=handler,
|
call=handler,
|
||||||
@@ -444,7 +442,7 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def handle(
|
def handle(
|
||||||
cls, parameterless: Optional[Iterable[Any]] = None
|
cls, parameterless: Iterable[Any] | None = None
|
||||||
) -> Callable[[T_Handler], T_Handler]:
|
) -> Callable[[T_Handler], T_Handler]:
|
||||||
"""装饰一个函数来向事件响应器直接添加一个处理函数
|
"""装饰一个函数来向事件响应器直接添加一个处理函数
|
||||||
|
|
||||||
@@ -460,7 +458,7 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def receive(
|
def receive(
|
||||||
cls, id: str = "", parameterless: Optional[Iterable[Any]] = None
|
cls, id: str = "", parameterless: Iterable[Any] | None = None
|
||||||
) -> Callable[[T_Handler], T_Handler]:
|
) -> Callable[[T_Handler], T_Handler]:
|
||||||
"""装饰一个函数来指示 NoneBot 在接收用户新的一条消息后继续运行该函数
|
"""装饰一个函数来指示 NoneBot 在接收用户新的一条消息后继续运行该函数
|
||||||
|
|
||||||
@@ -503,8 +501,8 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
def got(
|
def got(
|
||||||
cls,
|
cls,
|
||||||
key: str,
|
key: str,
|
||||||
prompt: Optional[Union[str, Message, MessageSegment, MessageTemplate]] = None,
|
prompt: str | Message | MessageSegment | MessageTemplate | None = None,
|
||||||
parameterless: Optional[Iterable[Any]] = None,
|
parameterless: Iterable[Any] | None = None,
|
||||||
) -> Callable[[T_Handler], T_Handler]:
|
) -> Callable[[T_Handler], T_Handler]:
|
||||||
"""装饰一个函数来指示 NoneBot 获取一个参数 `key`
|
"""装饰一个函数来指示 NoneBot 获取一个参数 `key`
|
||||||
|
|
||||||
@@ -550,7 +548,7 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
@classmethod
|
@classmethod
|
||||||
async def send(
|
async def send(
|
||||||
cls,
|
cls,
|
||||||
message: Union[str, Message, MessageSegment, MessageTemplate],
|
message: str | Message | MessageSegment | MessageTemplate,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""发送一条消息给当前交互用户
|
"""发送一条消息给当前交互用户
|
||||||
@@ -572,7 +570,7 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
@classmethod
|
@classmethod
|
||||||
async def finish(
|
async def finish(
|
||||||
cls,
|
cls,
|
||||||
message: Optional[Union[str, Message, MessageSegment, MessageTemplate]] = None,
|
message: str | Message | MessageSegment | MessageTemplate | None = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> NoReturn:
|
) -> NoReturn:
|
||||||
"""发送一条消息给当前交互用户并结束当前事件响应器
|
"""发送一条消息给当前交互用户并结束当前事件响应器
|
||||||
@@ -589,7 +587,7 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
@classmethod
|
@classmethod
|
||||||
async def pause(
|
async def pause(
|
||||||
cls,
|
cls,
|
||||||
prompt: Optional[Union[str, Message, MessageSegment, MessageTemplate]] = None,
|
prompt: str | Message | MessageSegment | MessageTemplate | None = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> NoReturn:
|
) -> NoReturn:
|
||||||
"""发送一条消息给当前交互用户并暂停事件响应器,在接收用户新的一条消息后继续下一个处理函数
|
"""发送一条消息给当前交互用户并暂停事件响应器,在接收用户新的一条消息后继续下一个处理函数
|
||||||
@@ -613,7 +611,7 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
@classmethod
|
@classmethod
|
||||||
async def reject(
|
async def reject(
|
||||||
cls,
|
cls,
|
||||||
prompt: Optional[Union[str, Message, MessageSegment, MessageTemplate]] = None,
|
prompt: str | Message | MessageSegment | MessageTemplate | None = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> NoReturn:
|
) -> NoReturn:
|
||||||
"""最近使用 `got` / `receive` 接收的消息不符合预期,
|
"""最近使用 `got` / `receive` 接收的消息不符合预期,
|
||||||
@@ -643,7 +641,7 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
async def reject_arg(
|
async def reject_arg(
|
||||||
cls,
|
cls,
|
||||||
key: str,
|
key: str,
|
||||||
prompt: Optional[Union[str, Message, MessageSegment, MessageTemplate]] = None,
|
prompt: str | Message | MessageSegment | MessageTemplate | None = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> NoReturn:
|
) -> NoReturn:
|
||||||
"""最近使用 `got` 接收的消息不符合预期,
|
"""最近使用 `got` 接收的消息不符合预期,
|
||||||
@@ -668,7 +666,7 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
async def reject_receive(
|
async def reject_receive(
|
||||||
cls,
|
cls,
|
||||||
id: str = "",
|
id: str = "",
|
||||||
prompt: Optional[Union[str, Message, MessageSegment, MessageTemplate]] = None,
|
prompt: str | Message | MessageSegment | MessageTemplate | None = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> NoReturn:
|
) -> NoReturn:
|
||||||
"""最近使用 `receive` 接收的消息不符合预期,
|
"""最近使用 `receive` 接收的消息不符合预期,
|
||||||
@@ -698,14 +696,12 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
raise SkippedException
|
raise SkippedException
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def get_receive(self, id: str) -> Union[Event, None]: ...
|
def get_receive(self, id: str) -> Event | None: ...
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def get_receive(self, id: str, default: T) -> Union[Event, T]: ...
|
def get_receive(self, id: str, default: T) -> Event | T: ...
|
||||||
|
|
||||||
def get_receive(
|
def get_receive(self, id: str, default: T | None = None) -> Event | T | None:
|
||||||
self, id: str, default: Optional[T] = None
|
|
||||||
) -> Optional[Union[Event, T]]:
|
|
||||||
"""获取一个 `receive` 事件
|
"""获取一个 `receive` 事件
|
||||||
|
|
||||||
如果没有找到对应的事件,返回 `default` 值
|
如果没有找到对应的事件,返回 `default` 值
|
||||||
@@ -718,14 +714,12 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
self.state[LAST_RECEIVE_KEY] = event
|
self.state[LAST_RECEIVE_KEY] = event
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def get_last_receive(self) -> Union[Event, None]: ...
|
def get_last_receive(self) -> Event | None: ...
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def get_last_receive(self, default: T) -> Union[Event, T]: ...
|
def get_last_receive(self, default: T) -> Event | T: ...
|
||||||
|
|
||||||
def get_last_receive(
|
def get_last_receive(self, default: T | None = None) -> Event | T | None:
|
||||||
self, default: Optional[T] = None
|
|
||||||
) -> Optional[Union[Event, T]]:
|
|
||||||
"""获取最近一次 `receive` 事件
|
"""获取最近一次 `receive` 事件
|
||||||
|
|
||||||
如果没有事件,返回 `default` 值
|
如果没有事件,返回 `default` 值
|
||||||
@@ -733,14 +727,12 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
return self.state.get(LAST_RECEIVE_KEY, default)
|
return self.state.get(LAST_RECEIVE_KEY, default)
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def get_arg(self, key: str) -> Union[Message, None]: ...
|
def get_arg(self, key: str) -> Message | None: ...
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def get_arg(self, key: str, default: T) -> Union[Message, T]: ...
|
def get_arg(self, key: str, default: T) -> Message | T: ...
|
||||||
|
|
||||||
def get_arg(
|
def get_arg(self, key: str, default: T | None = None) -> Message | T | None:
|
||||||
self, key: str, default: Optional[T] = None
|
|
||||||
) -> Optional[Union[Message, T]]:
|
|
||||||
"""获取一个 `got` 消息
|
"""获取一个 `got` 消息
|
||||||
|
|
||||||
如果没有找到对应的消息,返回 `default` 值
|
如果没有找到对应的消息,返回 `default` 值
|
||||||
@@ -758,12 +750,12 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
self.state[REJECT_TARGET] = target
|
self.state[REJECT_TARGET] = target
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def get_target(self) -> Union[str, None]: ...
|
def get_target(self) -> str | None: ...
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def get_target(self, default: T) -> Union[str, T]: ...
|
def get_target(self, default: T) -> str | T: ...
|
||||||
|
|
||||||
def get_target(self, default: Optional[T] = None) -> Optional[Union[str, T]]:
|
def get_target(self, default: T | None = None) -> str | T | None:
|
||||||
return self.state.get(REJECT_TARGET, default)
|
return self.state.get(REJECT_TARGET, default)
|
||||||
|
|
||||||
def stop_propagation(self):
|
def stop_propagation(self):
|
||||||
@@ -774,8 +766,8 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
self,
|
self,
|
||||||
bot: Bot,
|
bot: Bot,
|
||||||
event: Event,
|
event: Event,
|
||||||
stack: Optional[AsyncExitStack] = None,
|
stack: AsyncExitStack | None = None,
|
||||||
dependency_cache: Optional[T_DependencyCache] = None,
|
dependency_cache: T_DependencyCache | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
updater = self.__class__._default_type_updater
|
updater = self.__class__._default_type_updater
|
||||||
return (
|
return (
|
||||||
@@ -795,8 +787,8 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
self,
|
self,
|
||||||
bot: Bot,
|
bot: Bot,
|
||||||
event: Event,
|
event: Event,
|
||||||
stack: Optional[AsyncExitStack] = None,
|
stack: AsyncExitStack | None = None,
|
||||||
dependency_cache: Optional[T_DependencyCache] = None,
|
dependency_cache: T_DependencyCache | None = None,
|
||||||
) -> Permission:
|
) -> Permission:
|
||||||
if updater := self.__class__._default_permission_updater:
|
if updater := self.__class__._default_permission_updater:
|
||||||
return await updater(
|
return await updater(
|
||||||
@@ -832,8 +824,8 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
bot: Bot,
|
bot: Bot,
|
||||||
event: Event,
|
event: Event,
|
||||||
state: T_State,
|
state: T_State,
|
||||||
stack: Optional[AsyncExitStack] = None,
|
stack: AsyncExitStack | None = None,
|
||||||
dependency_cache: Optional[T_DependencyCache] = None,
|
dependency_cache: T_DependencyCache | None = None,
|
||||||
):
|
):
|
||||||
logger.trace(
|
logger.trace(
|
||||||
f"{self} run with incoming args: "
|
f"{self} run with incoming args: "
|
||||||
@@ -877,16 +869,14 @@ class Matcher(metaclass=MatcherMeta):
|
|||||||
bot: Bot,
|
bot: Bot,
|
||||||
event: Event,
|
event: Event,
|
||||||
state: T_State,
|
state: T_State,
|
||||||
stack: Optional[AsyncExitStack] = None,
|
stack: AsyncExitStack | None = None,
|
||||||
dependency_cache: Optional[T_DependencyCache] = None,
|
dependency_cache: T_DependencyCache | None = None,
|
||||||
):
|
):
|
||||||
exc: Optional[Union[FinishedException, RejectedException, PausedException]] = (
|
exc: FinishedException | RejectedException | PausedException | None = None
|
||||||
None
|
|
||||||
)
|
|
||||||
|
|
||||||
def _handle_special_exception(
|
def _handle_special_exception(
|
||||||
exc_group: BaseExceptionGroup[
|
exc_group: BaseExceptionGroup[
|
||||||
Union[FinishedException, RejectedException, PausedException]
|
FinishedException | RejectedException | PausedException
|
||||||
],
|
],
|
||||||
):
|
):
|
||||||
nonlocal exc
|
nonlocal exc
|
||||||
|
|||||||
+40
-46
@@ -1,3 +1,4 @@
|
|||||||
|
from collections.abc import Callable
|
||||||
from contextlib import AsyncExitStack, asynccontextmanager, contextmanager
|
from contextlib import AsyncExitStack, asynccontextmanager, contextmanager
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
import inspect
|
import inspect
|
||||||
@@ -5,13 +6,12 @@ from typing import (
|
|||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
Annotated,
|
Annotated,
|
||||||
Any,
|
Any,
|
||||||
Callable,
|
|
||||||
Literal,
|
Literal,
|
||||||
Optional,
|
|
||||||
Union,
|
|
||||||
cast,
|
cast,
|
||||||
|
get_args,
|
||||||
|
get_origin,
|
||||||
)
|
)
|
||||||
from typing_extensions import Self, get_args, get_origin, override
|
from typing_extensions import Self, override
|
||||||
|
|
||||||
import anyio
|
import anyio
|
||||||
from exceptiongroup import BaseExceptionGroup, catch
|
from exceptiongroup import BaseExceptionGroup, catch
|
||||||
@@ -47,10 +47,10 @@ if TYPE_CHECKING:
|
|||||||
class DependsInner:
|
class DependsInner:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
dependency: Optional[T_Handler] = None,
|
dependency: T_Handler | None = None,
|
||||||
*,
|
*,
|
||||||
use_cache: bool = True,
|
use_cache: bool = True,
|
||||||
validate: Union[bool, PydanticFieldInfo] = False,
|
validate: bool | PydanticFieldInfo = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.dependency = dependency
|
self.dependency = dependency
|
||||||
self.use_cache = use_cache
|
self.use_cache = use_cache
|
||||||
@@ -64,10 +64,10 @@ class DependsInner:
|
|||||||
|
|
||||||
|
|
||||||
def Depends(
|
def Depends(
|
||||||
dependency: Optional[T_Handler] = None,
|
dependency: T_Handler | None = None,
|
||||||
*,
|
*,
|
||||||
use_cache: bool = True,
|
use_cache: bool = True,
|
||||||
validate: Union[bool, PydanticFieldInfo] = False,
|
validate: bool | PydanticFieldInfo = False,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""子依赖装饰器
|
"""子依赖装饰器
|
||||||
|
|
||||||
@@ -113,7 +113,7 @@ class DependencyCache:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._state = CacheState.PENDING
|
self._state = CacheState.PENDING
|
||||||
self._result: Any = None
|
self._result: Any = None
|
||||||
self._exception: Optional[BaseException] = None
|
self._exception: BaseException | None = None
|
||||||
self._waiter = anyio.Event()
|
self._waiter = anyio.Event()
|
||||||
|
|
||||||
def done(self) -> bool:
|
def done(self) -> bool:
|
||||||
@@ -129,7 +129,7 @@ class DependencyCache:
|
|||||||
raise self._exception
|
raise self._exception
|
||||||
return self._result
|
return self._result
|
||||||
|
|
||||||
def exception(self) -> Optional[BaseException]:
|
def exception(self) -> BaseException | None:
|
||||||
"""获取子依赖异常"""
|
"""获取子依赖异常"""
|
||||||
|
|
||||||
if self._state != CacheState.FINISHED:
|
if self._state != CacheState.FINISHED:
|
||||||
@@ -192,7 +192,7 @@ class DependParam(Param):
|
|||||||
cls,
|
cls,
|
||||||
sub_dependent: Dependent[Any],
|
sub_dependent: Dependent[Any],
|
||||||
use_cache: bool,
|
use_cache: bool,
|
||||||
validate: Union[bool, PydanticFieldInfo],
|
validate: bool | PydanticFieldInfo,
|
||||||
) -> Self:
|
) -> Self:
|
||||||
return cls._inherit_construct(
|
return cls._inherit_construct(
|
||||||
validate if isinstance(validate, PydanticFieldInfo) else None,
|
validate if isinstance(validate, PydanticFieldInfo) else None,
|
||||||
@@ -205,7 +205,7 @@ class DependParam(Param):
|
|||||||
@override
|
@override
|
||||||
def _check_param(
|
def _check_param(
|
||||||
cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...]
|
cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...]
|
||||||
) -> Optional[Self]:
|
) -> Self | None:
|
||||||
type_annotation, depends_inner = param.annotation, None
|
type_annotation, depends_inner = param.annotation, None
|
||||||
# extract type annotation and dependency from Annotated
|
# extract type annotation and dependency from Annotated
|
||||||
if get_origin(param.annotation) is Annotated:
|
if get_origin(param.annotation) is Annotated:
|
||||||
@@ -245,7 +245,7 @@ class DependParam(Param):
|
|||||||
@override
|
@override
|
||||||
def _check_parameterless(
|
def _check_parameterless(
|
||||||
cls, value: Any, allow_types: tuple[type[Param], ...]
|
cls, value: Any, allow_types: tuple[type[Param], ...]
|
||||||
) -> Optional["Param"]:
|
) -> "Param | None":
|
||||||
if isinstance(value, DependsInner):
|
if isinstance(value, DependsInner):
|
||||||
assert value.dependency, "Dependency cannot be empty"
|
assert value.dependency, "Dependency cannot be empty"
|
||||||
dependent = Dependent[Any].parse(
|
dependent = Dependent[Any].parse(
|
||||||
@@ -256,8 +256,8 @@ class DependParam(Param):
|
|||||||
@override
|
@override
|
||||||
async def _solve(
|
async def _solve(
|
||||||
self,
|
self,
|
||||||
stack: Optional[AsyncExitStack] = None,
|
stack: AsyncExitStack | None = None,
|
||||||
dependency_cache: Optional[T_DependencyCache] = None,
|
dependency_cache: T_DependencyCache | None = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
use_cache: bool = self.use_cache
|
use_cache: bool = self.use_cache
|
||||||
@@ -267,7 +267,7 @@ class DependParam(Param):
|
|||||||
call = cast(Callable[..., Any], sub_dependent.call)
|
call = cast(Callable[..., Any], sub_dependent.call)
|
||||||
|
|
||||||
# solve sub dependency with current cache
|
# solve sub dependency with current cache
|
||||||
exc: Optional[BaseExceptionGroup[SkippedException]] = None
|
exc: BaseExceptionGroup[SkippedException] | None = None
|
||||||
|
|
||||||
def _handle_skipped(exc_group: BaseExceptionGroup[SkippedException]):
|
def _handle_skipped(exc_group: BaseExceptionGroup[SkippedException]):
|
||||||
nonlocal exc
|
nonlocal exc
|
||||||
@@ -332,9 +332,7 @@ class BotParam(Param):
|
|||||||
为保证兼容性,本注入还会解析名为 `bot` 且没有类型注解的参数。
|
为保证兼容性,本注入还会解析名为 `bot` 且没有类型注解的参数。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, *args, checker: ModelField | None = None, **kwargs: Any) -> None:
|
||||||
self, *args, checker: Optional[ModelField] = None, **kwargs: Any
|
|
||||||
) -> None:
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.checker = checker
|
self.checker = checker
|
||||||
|
|
||||||
@@ -349,12 +347,12 @@ class BotParam(Param):
|
|||||||
@override
|
@override
|
||||||
def _check_param(
|
def _check_param(
|
||||||
cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...]
|
cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...]
|
||||||
) -> Optional[Self]:
|
) -> Self | None:
|
||||||
from nonebot.adapters import Bot
|
from nonebot.adapters import Bot
|
||||||
|
|
||||||
# param type is Bot(s) or subclass(es) of Bot or None
|
# param type is Bot(s) or subclass(es) of Bot or None
|
||||||
if generic_check_issubclass(param.annotation, Bot):
|
if generic_check_issubclass(param.annotation, Bot):
|
||||||
checker: Optional[ModelField] = None
|
checker: ModelField | None = None
|
||||||
if param.annotation is not Bot:
|
if param.annotation is not Bot:
|
||||||
checker = ModelField.construct(
|
checker = ModelField.construct(
|
||||||
name=param.name, annotation=param.annotation, field_info=FieldInfo()
|
name=param.name, annotation=param.annotation, field_info=FieldInfo()
|
||||||
@@ -386,9 +384,7 @@ class EventParam(Param):
|
|||||||
为保证兼容性,本注入还会解析名为 `event` 且没有类型注解的参数。
|
为保证兼容性,本注入还会解析名为 `event` 且没有类型注解的参数。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, *args, checker: ModelField | None = None, **kwargs: Any) -> None:
|
||||||
self, *args, checker: Optional[ModelField] = None, **kwargs: Any
|
|
||||||
) -> None:
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.checker = checker
|
self.checker = checker
|
||||||
|
|
||||||
@@ -403,12 +399,12 @@ class EventParam(Param):
|
|||||||
@override
|
@override
|
||||||
def _check_param(
|
def _check_param(
|
||||||
cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...]
|
cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...]
|
||||||
) -> Optional[Self]:
|
) -> Self | None:
|
||||||
from nonebot.adapters import Event
|
from nonebot.adapters import Event
|
||||||
|
|
||||||
# param type is Event(s) or subclass(es) of Event or None
|
# param type is Event(s) or subclass(es) of Event or None
|
||||||
if generic_check_issubclass(param.annotation, Event):
|
if generic_check_issubclass(param.annotation, Event):
|
||||||
checker: Optional[ModelField] = None
|
checker: ModelField | None = None
|
||||||
if param.annotation is not Event:
|
if param.annotation is not Event:
|
||||||
checker = ModelField.construct(
|
checker = ModelField.construct(
|
||||||
name=param.name, annotation=param.annotation, field_info=FieldInfo()
|
name=param.name, annotation=param.annotation, field_info=FieldInfo()
|
||||||
@@ -447,7 +443,7 @@ class StateParam(Param):
|
|||||||
@override
|
@override
|
||||||
def _check_param(
|
def _check_param(
|
||||||
cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...]
|
cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...]
|
||||||
) -> Optional[Self]:
|
) -> Self | None:
|
||||||
# param type is T_State
|
# param type is T_State
|
||||||
if origin_is_annotated(
|
if origin_is_annotated(
|
||||||
get_origin(param.annotation)
|
get_origin(param.annotation)
|
||||||
@@ -472,9 +468,7 @@ class MatcherParam(Param):
|
|||||||
为保证兼容性,本注入还会解析名为 `matcher` 且没有类型注解的参数。
|
为保证兼容性,本注入还会解析名为 `matcher` 且没有类型注解的参数。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, *args, checker: ModelField | None = None, **kwargs: Any) -> None:
|
||||||
self, *args, checker: Optional[ModelField] = None, **kwargs: Any
|
|
||||||
) -> None:
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.checker = checker
|
self.checker = checker
|
||||||
|
|
||||||
@@ -489,12 +483,12 @@ class MatcherParam(Param):
|
|||||||
@override
|
@override
|
||||||
def _check_param(
|
def _check_param(
|
||||||
cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...]
|
cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...]
|
||||||
) -> Optional[Self]:
|
) -> Self | None:
|
||||||
from nonebot.matcher import Matcher
|
from nonebot.matcher import Matcher
|
||||||
|
|
||||||
# param type is Matcher(s) or subclass(es) of Matcher or None
|
# param type is Matcher(s) or subclass(es) of Matcher or None
|
||||||
if generic_check_issubclass(param.annotation, Matcher):
|
if generic_check_issubclass(param.annotation, Matcher):
|
||||||
checker: Optional[ModelField] = None
|
checker: ModelField | None = None
|
||||||
if param.annotation is not Matcher:
|
if param.annotation is not Matcher:
|
||||||
checker = ModelField.construct(
|
checker = ModelField.construct(
|
||||||
name=param.name, annotation=param.annotation, field_info=FieldInfo()
|
name=param.name, annotation=param.annotation, field_info=FieldInfo()
|
||||||
@@ -520,31 +514,31 @@ class MatcherParam(Param):
|
|||||||
|
|
||||||
class ArgInner:
|
class ArgInner:
|
||||||
def __init__(
|
def __init__(
|
||||||
self, key: Optional[str], type: Literal["message", "str", "plaintext", "prompt"]
|
self, key: str | None, type: Literal["message", "str", "plaintext", "prompt"]
|
||||||
) -> None:
|
) -> None:
|
||||||
self.key: Optional[str] = key
|
self.key: str | None = key
|
||||||
self.type: Literal["message", "str", "plaintext", "prompt"] = type
|
self.type: Literal["message", "str", "plaintext", "prompt"] = type
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"ArgInner(key={self.key!r}, type={self.type!r})"
|
return f"ArgInner(key={self.key!r}, type={self.type!r})"
|
||||||
|
|
||||||
|
|
||||||
def Arg(key: Optional[str] = None) -> Any:
|
def Arg(key: str | None = None) -> Any:
|
||||||
"""Arg 参数消息"""
|
"""Arg 参数消息"""
|
||||||
return ArgInner(key, "message")
|
return ArgInner(key, "message")
|
||||||
|
|
||||||
|
|
||||||
def ArgStr(key: Optional[str] = None) -> str:
|
def ArgStr(key: str | None = None) -> str:
|
||||||
"""Arg 参数消息文本"""
|
"""Arg 参数消息文本"""
|
||||||
return ArgInner(key, "str") # type: ignore
|
return ArgInner(key, "str") # type: ignore
|
||||||
|
|
||||||
|
|
||||||
def ArgPlainText(key: Optional[str] = None) -> str:
|
def ArgPlainText(key: str | None = None) -> str:
|
||||||
"""Arg 参数消息纯文本"""
|
"""Arg 参数消息纯文本"""
|
||||||
return ArgInner(key, "plaintext") # type: ignore
|
return ArgInner(key, "plaintext") # type: ignore
|
||||||
|
|
||||||
|
|
||||||
def ArgPromptResult(key: Optional[str] = None) -> Any:
|
def ArgPromptResult(key: str | None = None) -> Any:
|
||||||
"""`arg` prompt 发送结果"""
|
"""`arg` prompt 发送结果"""
|
||||||
return ArgInner(key, "prompt")
|
return ArgInner(key, "prompt")
|
||||||
|
|
||||||
@@ -576,7 +570,7 @@ class ArgParam(Param):
|
|||||||
@override
|
@override
|
||||||
def _check_param(
|
def _check_param(
|
||||||
cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...]
|
cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...]
|
||||||
) -> Optional[Self]:
|
) -> Self | None:
|
||||||
if isinstance(param.default, ArgInner):
|
if isinstance(param.default, ArgInner):
|
||||||
return cls(key=param.default.key or param.name, type=param.default.type)
|
return cls(key=param.default.key or param.name, type=param.default.type)
|
||||||
elif get_origin(param.annotation) is Annotated:
|
elif get_origin(param.annotation) is Annotated:
|
||||||
@@ -598,18 +592,18 @@ class ArgParam(Param):
|
|||||||
else:
|
else:
|
||||||
raise ValueError(f"Unknown Arg type: {self.type}")
|
raise ValueError(f"Unknown Arg type: {self.type}")
|
||||||
|
|
||||||
def _solve_message(self, matcher: "Matcher") -> Optional["Message"]:
|
def _solve_message(self, matcher: "Matcher") -> "Message | None":
|
||||||
return matcher.get_arg(self.key)
|
return matcher.get_arg(self.key)
|
||||||
|
|
||||||
def _solve_str(self, matcher: "Matcher") -> Optional[str]:
|
def _solve_str(self, matcher: "Matcher") -> str | None:
|
||||||
message = matcher.get_arg(self.key)
|
message = matcher.get_arg(self.key)
|
||||||
return str(message) if message is not None else None
|
return str(message) if message is not None else None
|
||||||
|
|
||||||
def _solve_plaintext(self, matcher: "Matcher") -> Optional[str]:
|
def _solve_plaintext(self, matcher: "Matcher") -> str | None:
|
||||||
message = matcher.get_arg(self.key)
|
message = matcher.get_arg(self.key)
|
||||||
return message.extract_plain_text() if message is not None else None
|
return message.extract_plain_text() if message is not None else None
|
||||||
|
|
||||||
def _solve_prompt(self, matcher: "Matcher") -> Optional[Any]:
|
def _solve_prompt(self, matcher: "Matcher") -> Any | None:
|
||||||
return matcher.state.get(
|
return matcher.state.get(
|
||||||
REJECT_PROMPT_RESULT_KEY.format(key=ARG_KEY.format(key=self.key))
|
REJECT_PROMPT_RESULT_KEY.format(key=ARG_KEY.format(key=self.key))
|
||||||
)
|
)
|
||||||
@@ -630,7 +624,7 @@ class ExceptionParam(Param):
|
|||||||
@override
|
@override
|
||||||
def _check_param(
|
def _check_param(
|
||||||
cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...]
|
cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...]
|
||||||
) -> Optional[Self]:
|
) -> Self | None:
|
||||||
# param type is Exception(s) or subclass(es) of Exception or None
|
# param type is Exception(s) or subclass(es) of Exception or None
|
||||||
if generic_check_issubclass(param.annotation, Exception):
|
if generic_check_issubclass(param.annotation, Exception):
|
||||||
return cls()
|
return cls()
|
||||||
@@ -639,7 +633,7 @@ class ExceptionParam(Param):
|
|||||||
return cls()
|
return cls()
|
||||||
|
|
||||||
@override
|
@override
|
||||||
async def _solve(self, exception: Optional[Exception] = None, **kwargs: Any) -> Any:
|
async def _solve(self, exception: Exception | None = None, **kwargs: Any) -> Any:
|
||||||
return exception
|
return exception
|
||||||
|
|
||||||
|
|
||||||
@@ -658,7 +652,7 @@ class DefaultParam(Param):
|
|||||||
@override
|
@override
|
||||||
def _check_param(
|
def _check_param(
|
||||||
cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...]
|
cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...]
|
||||||
) -> Optional[Self]:
|
) -> Self | None:
|
||||||
if param.default != param.empty:
|
if param.default != param.empty:
|
||||||
return cls(default=param.default)
|
return cls(default=param.default)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from contextlib import AsyncExitStack
|
from contextlib import AsyncExitStack
|
||||||
from typing import ClassVar, NoReturn, Optional, Union
|
from typing import ClassVar, NoReturn
|
||||||
from typing_extensions import Self
|
from typing_extensions import Self
|
||||||
|
|
||||||
import anyio
|
import anyio
|
||||||
@@ -38,7 +38,7 @@ class Permission:
|
|||||||
DefaultParam,
|
DefaultParam,
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, *checkers: Union[T_PermissionChecker, Dependent[bool]]) -> None:
|
def __init__(self, *checkers: T_PermissionChecker | Dependent[bool]) -> None:
|
||||||
self.checkers: set[Dependent[bool]] = {
|
self.checkers: set[Dependent[bool]] = {
|
||||||
(
|
(
|
||||||
checker
|
checker
|
||||||
@@ -58,8 +58,8 @@ class Permission:
|
|||||||
self,
|
self,
|
||||||
bot: Bot,
|
bot: Bot,
|
||||||
event: Event,
|
event: Event,
|
||||||
stack: Optional[AsyncExitStack] = None,
|
stack: AsyncExitStack | None = None,
|
||||||
dependency_cache: Optional[T_DependencyCache] = None,
|
dependency_cache: T_DependencyCache | None = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""检查是否满足某个权限。
|
"""检查是否满足某个权限。
|
||||||
|
|
||||||
@@ -95,9 +95,7 @@ class Permission:
|
|||||||
def __and__(self, other: object) -> NoReturn:
|
def __and__(self, other: object) -> NoReturn:
|
||||||
raise RuntimeError("And operation between Permissions is not allowed.")
|
raise RuntimeError("And operation between Permissions is not allowed.")
|
||||||
|
|
||||||
def __or__(
|
def __or__(self, other: "Permission | T_PermissionChecker | None") -> "Permission":
|
||||||
self, other: Optional[Union["Permission", T_PermissionChecker]]
|
|
||||||
) -> "Permission":
|
|
||||||
if other is None:
|
if other is None:
|
||||||
return self
|
return self
|
||||||
elif isinstance(other, Permission):
|
elif isinstance(other, Permission):
|
||||||
@@ -105,9 +103,7 @@ class Permission:
|
|||||||
else:
|
else:
|
||||||
return Permission(*self.checkers, other)
|
return Permission(*self.checkers, other)
|
||||||
|
|
||||||
def __ror__(
|
def __ror__(self, other: "Permission | T_PermissionChecker | None") -> "Permission":
|
||||||
self, other: Optional[Union["Permission", T_PermissionChecker]]
|
|
||||||
) -> "Permission":
|
|
||||||
if other is None:
|
if other is None:
|
||||||
return self
|
return self
|
||||||
elif isinstance(other, Permission):
|
elif isinstance(other, Permission):
|
||||||
@@ -126,9 +122,7 @@ class User:
|
|||||||
|
|
||||||
__slots__ = ("perm", "users")
|
__slots__ = ("perm", "users")
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, users: tuple[str, ...], perm: Permission | None = None) -> None:
|
||||||
self, users: tuple[str, ...], perm: Optional[Permission] = None
|
|
||||||
) -> None:
|
|
||||||
self.users = users
|
self.users = users
|
||||||
self.perm = perm
|
self.perm = perm
|
||||||
|
|
||||||
@@ -149,7 +143,7 @@ class User:
|
|||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _clean_permission(cls, perm: Permission) -> Optional[Permission]:
|
def _clean_permission(cls, perm: Permission) -> Permission | None:
|
||||||
if len(perm.checkers) == 1 and isinstance(
|
if len(perm.checkers) == 1 and isinstance(
|
||||||
user_perm := next(iter(perm.checkers)).call, cls
|
user_perm := next(iter(perm.checkers)).call, cls
|
||||||
):
|
):
|
||||||
@@ -157,7 +151,7 @@ class User:
|
|||||||
return perm
|
return perm
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_event(cls, event: Event, perm: Optional[Permission] = None) -> Self:
|
def from_event(cls, event: Event, perm: Permission | None = None) -> Self:
|
||||||
"""从事件中获取会话 ID。
|
"""从事件中获取会话 ID。
|
||||||
|
|
||||||
如果 `perm` 中仅有 `User` 类型的权限检查函数,则会去除原有的会话 ID 限制。
|
如果 `perm` 中仅有 `User` 类型的权限检查函数,则会去除原有的会话 ID 限制。
|
||||||
@@ -169,7 +163,7 @@ class User:
|
|||||||
return cls((event.get_session_id(),), perm=perm and cls._clean_permission(perm))
|
return cls((event.get_session_id(),), perm=perm and cls._clean_permission(perm))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_permission(cls, *users: str, perm: Optional[Permission] = None) -> Self:
|
def from_permission(cls, *users: str, perm: Permission | None = None) -> Self:
|
||||||
"""指定会话与权限。
|
"""指定会话与权限。
|
||||||
|
|
||||||
如果 `perm` 中仅有 `User` 类型的权限检查函数,则会去除原有的会话 ID 限制。
|
如果 `perm` 中仅有 `User` 类型的权限检查函数,则会去除原有的会话 ID 限制。
|
||||||
@@ -181,7 +175,7 @@ class User:
|
|||||||
return cls(users, perm=perm and cls._clean_permission(perm))
|
return cls(users, perm=perm and cls._clean_permission(perm))
|
||||||
|
|
||||||
|
|
||||||
def USER(*users: str, perm: Optional[Permission] = None):
|
def USER(*users: str, perm: Permission | None = None):
|
||||||
"""匹配当前事件属于指定会话。
|
"""匹配当前事件属于指定会话。
|
||||||
|
|
||||||
如果 `perm` 中仅有 `User` 类型的权限检查函数,则会去除原有检查函数的会话 ID 限制。
|
如果 `perm` 中仅有 `User` 类型的权限检查函数,则会去除原有检查函数的会话 ID 限制。
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from contextlib import AsyncExitStack
|
from contextlib import AsyncExitStack
|
||||||
from typing import ClassVar, NoReturn, Optional, Union
|
from typing import ClassVar, NoReturn
|
||||||
|
|
||||||
import anyio
|
import anyio
|
||||||
from exceptiongroup import BaseExceptionGroup, catch
|
from exceptiongroup import BaseExceptionGroup, catch
|
||||||
@@ -38,7 +38,7 @@ class Rule:
|
|||||||
DefaultParam,
|
DefaultParam,
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, *checkers: Union[T_RuleChecker, Dependent[bool]]) -> None:
|
def __init__(self, *checkers: T_RuleChecker | Dependent[bool]) -> None:
|
||||||
self.checkers: set[Dependent[bool]] = {
|
self.checkers: set[Dependent[bool]] = {
|
||||||
(
|
(
|
||||||
checker
|
checker
|
||||||
@@ -59,8 +59,8 @@ class Rule:
|
|||||||
bot: Bot,
|
bot: Bot,
|
||||||
event: Event,
|
event: Event,
|
||||||
state: T_State,
|
state: T_State,
|
||||||
stack: Optional[AsyncExitStack] = None,
|
stack: AsyncExitStack | None = None,
|
||||||
dependency_cache: Optional[T_DependencyCache] = None,
|
dependency_cache: T_DependencyCache | None = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""检查是否符合所有规则
|
"""检查是否符合所有规则
|
||||||
|
|
||||||
@@ -101,7 +101,7 @@ class Rule:
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def __and__(self, other: Optional[Union["Rule", T_RuleChecker]]) -> "Rule":
|
def __and__(self, other: "Rule | T_RuleChecker | None") -> "Rule":
|
||||||
if other is None:
|
if other is None:
|
||||||
return self
|
return self
|
||||||
elif isinstance(other, Rule):
|
elif isinstance(other, Rule):
|
||||||
@@ -109,7 +109,7 @@ class Rule:
|
|||||||
else:
|
else:
|
||||||
return Rule(*self.checkers, other)
|
return Rule(*self.checkers, other)
|
||||||
|
|
||||||
def __rand__(self, other: Optional[Union["Rule", T_RuleChecker]]) -> "Rule":
|
def __rand__(self, other: "Rule | T_RuleChecker | None") -> "Rule":
|
||||||
if other is None:
|
if other is None:
|
||||||
return self
|
return self
|
||||||
elif isinstance(other, Rule):
|
elif isinstance(other, Rule):
|
||||||
|
|||||||
+17
-16
@@ -9,10 +9,11 @@ FrontMatter:
|
|||||||
description: nonebot.message 模块
|
description: nonebot.message 模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
import contextlib
|
import contextlib
|
||||||
from contextlib import AsyncExitStack
|
from contextlib import AsyncExitStack
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import TYPE_CHECKING, Any, Callable, Optional
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
import anyio
|
import anyio
|
||||||
from exceptiongroup import BaseExceptionGroup, catch
|
from exceptiongroup import BaseExceptionGroup, catch
|
||||||
@@ -153,8 +154,8 @@ async def _apply_event_preprocessors(
|
|||||||
bot: "Bot",
|
bot: "Bot",
|
||||||
event: "Event",
|
event: "Event",
|
||||||
state: T_State,
|
state: T_State,
|
||||||
stack: Optional[AsyncExitStack] = None,
|
stack: AsyncExitStack | None = None,
|
||||||
dependency_cache: Optional[T_DependencyCache] = None,
|
dependency_cache: T_DependencyCache | None = None,
|
||||||
show_log: bool = True,
|
show_log: bool = True,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""运行事件预处理。
|
"""运行事件预处理。
|
||||||
@@ -210,8 +211,8 @@ async def _apply_event_postprocessors(
|
|||||||
bot: "Bot",
|
bot: "Bot",
|
||||||
event: "Event",
|
event: "Event",
|
||||||
state: T_State,
|
state: T_State,
|
||||||
stack: Optional[AsyncExitStack] = None,
|
stack: AsyncExitStack | None = None,
|
||||||
dependency_cache: Optional[T_DependencyCache] = None,
|
dependency_cache: T_DependencyCache | None = None,
|
||||||
show_log: bool = True,
|
show_log: bool = True,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""运行事件后处理。
|
"""运行事件后处理。
|
||||||
@@ -257,8 +258,8 @@ async def _apply_run_preprocessors(
|
|||||||
event: "Event",
|
event: "Event",
|
||||||
state: T_State,
|
state: T_State,
|
||||||
matcher: Matcher,
|
matcher: Matcher,
|
||||||
stack: Optional[AsyncExitStack] = None,
|
stack: AsyncExitStack | None = None,
|
||||||
dependency_cache: Optional[T_DependencyCache] = None,
|
dependency_cache: T_DependencyCache | None = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""运行事件响应器运行前处理。
|
"""运行事件响应器运行前处理。
|
||||||
|
|
||||||
@@ -315,9 +316,9 @@ async def _apply_run_postprocessors(
|
|||||||
bot: "Bot",
|
bot: "Bot",
|
||||||
event: "Event",
|
event: "Event",
|
||||||
matcher: Matcher,
|
matcher: Matcher,
|
||||||
exception: Optional[Exception] = None,
|
exception: Exception | None = None,
|
||||||
stack: Optional[AsyncExitStack] = None,
|
stack: AsyncExitStack | None = None,
|
||||||
dependency_cache: Optional[T_DependencyCache] = None,
|
dependency_cache: T_DependencyCache | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""运行事件响应器运行后处理。
|
"""运行事件响应器运行后处理。
|
||||||
|
|
||||||
@@ -365,8 +366,8 @@ async def _check_matcher(
|
|||||||
bot: "Bot",
|
bot: "Bot",
|
||||||
event: "Event",
|
event: "Event",
|
||||||
state: T_State,
|
state: T_State,
|
||||||
stack: Optional[AsyncExitStack] = None,
|
stack: AsyncExitStack | None = None,
|
||||||
dependency_cache: Optional[T_DependencyCache] = None,
|
dependency_cache: T_DependencyCache | None = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""检查事件响应器是否符合运行条件。
|
"""检查事件响应器是否符合运行条件。
|
||||||
|
|
||||||
@@ -416,8 +417,8 @@ async def _run_matcher(
|
|||||||
bot: "Bot",
|
bot: "Bot",
|
||||||
event: "Event",
|
event: "Event",
|
||||||
state: T_State,
|
state: T_State,
|
||||||
stack: Optional[AsyncExitStack] = None,
|
stack: AsyncExitStack | None = None,
|
||||||
dependency_cache: Optional[T_DependencyCache] = None,
|
dependency_cache: T_DependencyCache | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""运行事件响应器。
|
"""运行事件响应器。
|
||||||
|
|
||||||
@@ -482,8 +483,8 @@ async def check_and_run_matcher(
|
|||||||
bot: "Bot",
|
bot: "Bot",
|
||||||
event: "Event",
|
event: "Event",
|
||||||
state: T_State,
|
state: T_State,
|
||||||
stack: Optional[AsyncExitStack] = None,
|
stack: AsyncExitStack | None = None,
|
||||||
dependency_cache: Optional[T_DependencyCache] = None,
|
dependency_cache: T_DependencyCache | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""检查并运行事件响应器。
|
"""检查并运行事件响应器。
|
||||||
|
|
||||||
|
|||||||
+12
-11
@@ -7,8 +7,9 @@ FrontMatter:
|
|||||||
description: nonebot.params 模块
|
description: nonebot.params 模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
from re import Match
|
from re import Match
|
||||||
from typing import Any, Callable, Literal, Optional, Union, overload
|
from typing import Any, Literal, overload
|
||||||
|
|
||||||
from nonebot.adapters import Event, Message, MessageSegment
|
from nonebot.adapters import Event, Message, MessageSegment
|
||||||
from nonebot.consts import (
|
from nonebot.consts import (
|
||||||
@@ -136,7 +137,7 @@ def ShellCommandArgs() -> Any:
|
|||||||
return Depends(_shell_command_args, use_cache=False)
|
return Depends(_shell_command_args, use_cache=False)
|
||||||
|
|
||||||
|
|
||||||
def _shell_command_argv(state: T_State) -> list[Union[str, MessageSegment]]:
|
def _shell_command_argv(state: T_State) -> list[str | MessageSegment]:
|
||||||
return state[SHELL_ARGV]
|
return state[SHELL_ARGV]
|
||||||
|
|
||||||
|
|
||||||
@@ -155,11 +156,11 @@ def RegexMatched() -> Match[str]:
|
|||||||
|
|
||||||
|
|
||||||
def _regex_str(
|
def _regex_str(
|
||||||
groups: tuple[Union[str, int], ...],
|
groups: tuple[str | int, ...],
|
||||||
) -> Callable[[T_State], Union[str, tuple[Union[str, Any], ...], Any]]:
|
) -> Callable[[T_State], str | tuple[str | Any, ...] | Any]:
|
||||||
def _regex_str_dependency(
|
def _regex_str_dependency(
|
||||||
state: T_State,
|
state: T_State,
|
||||||
) -> Union[str, tuple[Union[str, Any], ...], Any]:
|
) -> str | tuple[str | Any, ...] | Any:
|
||||||
return _regex_matched(state).group(*groups)
|
return _regex_matched(state).group(*groups)
|
||||||
|
|
||||||
return _regex_str_dependency
|
return _regex_str_dependency
|
||||||
@@ -170,16 +171,16 @@ def RegexStr(group: Literal[0] = 0, /) -> str: ...
|
|||||||
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def RegexStr(group: Union[str, int], /) -> Union[str, Any]: ...
|
def RegexStr(group: str | int, /) -> str | Any: ...
|
||||||
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def RegexStr(
|
def RegexStr(
|
||||||
group1: Union[str, int], group2: Union[str, int], /, *groups: Union[str, int]
|
group1: str | int, group2: str | int, /, *groups: str | int
|
||||||
) -> tuple[Union[str, Any], ...]: ...
|
) -> tuple[str | Any, ...]: ...
|
||||||
|
|
||||||
|
|
||||||
def RegexStr(*groups: Union[str, int]) -> Union[str, tuple[Union[str, Any], ...], Any]:
|
def RegexStr(*groups: str | int) -> str | tuple[str | Any, ...] | Any:
|
||||||
"""正则匹配结果文本"""
|
"""正则匹配结果文本"""
|
||||||
return Depends(_regex_str(groups), use_cache=False)
|
return Depends(_regex_str(groups), use_cache=False)
|
||||||
|
|
||||||
@@ -238,7 +239,7 @@ def Keyword() -> str:
|
|||||||
return Depends(_keyword, use_cache=False)
|
return Depends(_keyword, use_cache=False)
|
||||||
|
|
||||||
|
|
||||||
def Received(id: Optional[str] = None, default: Any = None) -> Any:
|
def Received(id: str | None = None, default: Any = None) -> Any:
|
||||||
"""`receive` 事件参数"""
|
"""`receive` 事件参数"""
|
||||||
|
|
||||||
def _received(matcher: "Matcher") -> Any:
|
def _received(matcher: "Matcher") -> Any:
|
||||||
@@ -256,7 +257,7 @@ def LastReceived(default: Any = None) -> Any:
|
|||||||
return Depends(_last_received, use_cache=False)
|
return Depends(_last_received, use_cache=False)
|
||||||
|
|
||||||
|
|
||||||
def ReceivePromptResult(id: Optional[str] = None) -> Any:
|
def ReceivePromptResult(id: str | None = None) -> Any:
|
||||||
"""`receive` prompt 发送结果"""
|
"""`receive` prompt 发送结果"""
|
||||||
|
|
||||||
def _receive_prompt_result(matcher: "Matcher") -> Any:
|
def _receive_prompt_result(matcher: "Matcher") -> Any:
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ FrontMatter:
|
|||||||
from contextvars import ContextVar
|
from contextvars import ContextVar
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from typing import Optional, TypeVar
|
from typing import TypeVar
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ C = TypeVar("C", bound=BaseModel)
|
|||||||
|
|
||||||
_plugins: dict[str, "Plugin"] = {}
|
_plugins: dict[str, "Plugin"] = {}
|
||||||
_managers: list["PluginManager"] = []
|
_managers: list["PluginManager"] = []
|
||||||
_current_plugin: ContextVar[Optional["Plugin"]] = ContextVar(
|
_current_plugin: ContextVar["Plugin | None"] = ContextVar(
|
||||||
"_current_plugin", default=None
|
"_current_plugin", default=None
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -71,8 +71,8 @@ def _controlled_modules() -> dict[str, str]:
|
|||||||
|
|
||||||
|
|
||||||
def _find_parent_plugin_id(
|
def _find_parent_plugin_id(
|
||||||
module_name: str, controlled_modules: Optional[dict[str, str]] = None
|
module_name: str, controlled_modules: dict[str, str] | None = None
|
||||||
) -> Optional[str]:
|
) -> str | None:
|
||||||
if controlled_modules is None:
|
if controlled_modules is None:
|
||||||
controlled_modules = _controlled_modules()
|
controlled_modules = _controlled_modules()
|
||||||
available = {
|
available = {
|
||||||
@@ -85,7 +85,7 @@ def _find_parent_plugin_id(
|
|||||||
|
|
||||||
|
|
||||||
def _module_name_to_plugin_id(
|
def _module_name_to_plugin_id(
|
||||||
module_name: str, controlled_modules: Optional[dict[str, str]] = None
|
module_name: str, controlled_modules: dict[str, str] | None = None
|
||||||
) -> str:
|
) -> str:
|
||||||
plugin_name = _module_name_to_plugin_name(module_name)
|
plugin_name = _module_name_to_plugin_name(module_name)
|
||||||
if parent_plugin_id := _find_parent_plugin_id(module_name, controlled_modules):
|
if parent_plugin_id := _find_parent_plugin_id(module_name, controlled_modules):
|
||||||
@@ -132,7 +132,7 @@ def _revert_plugin(plugin: "Plugin") -> None:
|
|||||||
parent_plugin.sub_plugins.discard(plugin)
|
parent_plugin.sub_plugins.discard(plugin)
|
||||||
|
|
||||||
|
|
||||||
def get_plugin(plugin_id: str) -> Optional["Plugin"]:
|
def get_plugin(plugin_id: str) -> "Plugin | None":
|
||||||
"""获取已经导入的某个插件。
|
"""获取已经导入的某个插件。
|
||||||
|
|
||||||
如果为 `load_plugins` 文件夹导入的插件,则为文件(夹)名。
|
如果为 `load_plugins` 文件夹导入的插件,则为文件(夹)名。
|
||||||
@@ -145,7 +145,7 @@ def get_plugin(plugin_id: str) -> Optional["Plugin"]:
|
|||||||
return _plugins.get(plugin_id)
|
return _plugins.get(plugin_id)
|
||||||
|
|
||||||
|
|
||||||
def get_plugin_by_module_name(module_name: str) -> Optional["Plugin"]:
|
def get_plugin_by_module_name(module_name: str) -> "Plugin | None":
|
||||||
"""通过模块名获取已经导入的某个插件。
|
"""通过模块名获取已经导入的某个插件。
|
||||||
|
|
||||||
如果提供的模块名为某个插件的子模块,同样会返回该插件。
|
如果提供的模块名为某个插件的子模块,同样会返回该插件。
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ from itertools import chain
|
|||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from typing import Optional, Union
|
|
||||||
|
|
||||||
from nonebot.log import logger
|
from nonebot.log import logger
|
||||||
from nonebot.utils import path_to_module_name
|
from nonebot.utils import path_to_module_name
|
||||||
@@ -27,7 +26,7 @@ except ModuleNotFoundError: # pragma: py-lt-311
|
|||||||
import tomli as tomllib # pyright: ignore[reportMissingImports]
|
import tomli as tomllib # pyright: ignore[reportMissingImports]
|
||||||
|
|
||||||
|
|
||||||
def load_plugin(module_path: Union[str, Path]) -> Optional[Plugin]:
|
def load_plugin(module_path: str | Path) -> Plugin | None:
|
||||||
"""加载单个插件,可以是本地插件或是通过 `pip` 安装的插件。
|
"""加载单个插件,可以是本地插件或是通过 `pip` 安装的插件。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -159,7 +158,7 @@ def load_from_toml(file_path: str, encoding: str = "utf-8") -> set[Plugin]:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def load_builtin_plugin(name: str) -> Optional[Plugin]:
|
def load_builtin_plugin(name: str) -> Plugin | None:
|
||||||
"""导入 NoneBot 内置插件。
|
"""导入 NoneBot 内置插件。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -177,7 +176,7 @@ def load_builtin_plugins(*plugins: str) -> set[Plugin]:
|
|||||||
return load_all_plugins([f"nonebot.plugins.{p}" for p in plugins], [])
|
return load_all_plugins([f"nonebot.plugins.{p}" for p in plugins], [])
|
||||||
|
|
||||||
|
|
||||||
def _find_manager_by_name(name: str) -> Optional[PluginManager]:
|
def _find_manager_by_name(name: str) -> PluginManager | None:
|
||||||
for manager in reversed(_managers):
|
for manager in reversed(_managers):
|
||||||
if (
|
if (
|
||||||
name in manager.controlled_modules
|
name in manager.controlled_modules
|
||||||
@@ -217,7 +216,7 @@ def require(name: str) -> ModuleType:
|
|||||||
return plugin.module
|
return plugin.module
|
||||||
|
|
||||||
|
|
||||||
def inherit_supported_adapters(*names: str) -> Optional[set[str]]:
|
def inherit_supported_adapters(*names: str) -> set[str] | None:
|
||||||
"""获取已加载插件的适配器支持状态集合。
|
"""获取已加载插件的适配器支持状态集合。
|
||||||
|
|
||||||
如果传入了多个插件名称,返回值会自动取交集。
|
如果传入了多个插件名称,返回值会自动取交集。
|
||||||
@@ -229,7 +228,7 @@ def inherit_supported_adapters(*names: str) -> Optional[set[str]]:
|
|||||||
RuntimeError: 插件未加载
|
RuntimeError: 插件未加载
|
||||||
ValueError: 插件缺少元数据
|
ValueError: 插件缺少元数据
|
||||||
"""
|
"""
|
||||||
final_supported: Optional[set[str]] = None
|
final_supported: set[str] | None = None
|
||||||
|
|
||||||
for name in names:
|
for name in names:
|
||||||
plugin = get_plugin(_module_name_to_plugin_id(name))
|
plugin = get_plugin(_module_name_to_plugin_id(name))
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ from pathlib import Path
|
|||||||
import pkgutil
|
import pkgutil
|
||||||
import sys
|
import sys
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from nonebot.log import logger
|
from nonebot.log import logger
|
||||||
from nonebot.utils import escape_tag, path_to_module_name
|
from nonebot.utils import escape_tag, path_to_module_name
|
||||||
@@ -43,8 +42,8 @@ class PluginManager:
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
plugins: Optional[Iterable[str]] = None,
|
plugins: Iterable[str] | None = None,
|
||||||
search_path: Optional[Iterable[str]] = None,
|
search_path: Iterable[str] | None = None,
|
||||||
):
|
):
|
||||||
# simple plugin not in search path
|
# simple plugin not in search path
|
||||||
self.plugins: set[str] = set(plugins or [])
|
self.plugins: set[str] = set(plugins or [])
|
||||||
@@ -154,7 +153,7 @@ class PluginManager:
|
|||||||
|
|
||||||
return self.available_plugins
|
return self.available_plugins
|
||||||
|
|
||||||
def load_plugin(self, name: str) -> Optional[Plugin]:
|
def load_plugin(self, name: str) -> Plugin | None:
|
||||||
"""加载指定插件。
|
"""加载指定插件。
|
||||||
|
|
||||||
可以使用完整插件模块名或者插件标识符加载。
|
可以使用完整插件模块名或者插件标识符加载。
|
||||||
@@ -211,8 +210,8 @@ class PluginFinder(MetaPathFinder):
|
|||||||
def find_spec(
|
def find_spec(
|
||||||
self,
|
self,
|
||||||
fullname: str,
|
fullname: str,
|
||||||
path: Optional[Sequence[str]],
|
path: Sequence[str] | None,
|
||||||
target: Optional[ModuleType] = None,
|
target: ModuleType | None = None,
|
||||||
):
|
):
|
||||||
if _managers:
|
if _managers:
|
||||||
module_spec = PathFinder.find_spec(fullname, path, target)
|
module_spec = PathFinder.find_spec(fullname, path, target)
|
||||||
@@ -235,7 +234,7 @@ class PluginLoader(SourceFileLoader):
|
|||||||
self.loaded = False
|
self.loaded = False
|
||||||
super().__init__(fullname, path)
|
super().__init__(fullname, path)
|
||||||
|
|
||||||
def create_module(self, spec) -> Optional[ModuleType]:
|
def create_module(self, spec) -> ModuleType | None:
|
||||||
if self.name in sys.modules:
|
if self.name in sys.modules:
|
||||||
self.loaded = True
|
self.loaded = True
|
||||||
return sys.modules[self.name]
|
return sys.modules[self.name]
|
||||||
@@ -263,7 +262,7 @@ class PluginLoader(SourceFileLoader):
|
|||||||
_current_plugin.reset(_plugin_token)
|
_current_plugin.reset(_plugin_token)
|
||||||
|
|
||||||
# get plugin metadata
|
# get plugin metadata
|
||||||
metadata: Optional[PluginMetadata] = getattr(module, "__plugin_meta__", None)
|
metadata: PluginMetadata | None = getattr(module, "__plugin_meta__", None)
|
||||||
plugin.metadata = metadata
|
plugin.metadata = metadata
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ FrontMatter:
|
|||||||
import contextlib
|
import contextlib
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from typing import TYPE_CHECKING, Any, Optional, Type # noqa: UP035
|
from typing import TYPE_CHECKING, Any, Type # noqa: UP035
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
@@ -33,13 +33,13 @@ class PluginMetadata:
|
|||||||
"""插件功能介绍"""
|
"""插件功能介绍"""
|
||||||
usage: str
|
usage: str
|
||||||
"""插件使用方法"""
|
"""插件使用方法"""
|
||||||
type: Optional[str] = None
|
type: str | None = None
|
||||||
"""插件类型,用于商店分类"""
|
"""插件类型,用于商店分类"""
|
||||||
homepage: Optional[str] = None
|
homepage: str | None = None
|
||||||
"""插件主页"""
|
"""插件主页"""
|
||||||
config: Optional[Type[BaseModel]] = None # noqa: UP006
|
config: Type[BaseModel] | None = None # noqa: UP006
|
||||||
"""插件配置项"""
|
"""插件配置项"""
|
||||||
supported_adapters: Optional[set[str]] = None
|
supported_adapters: set[str] | None = None
|
||||||
"""插件支持的适配器模块路径
|
"""插件支持的适配器模块路径
|
||||||
|
|
||||||
格式为 `<module>[:<Adapter>]`,`~` 为 `nonebot.adapters.` 的缩写。
|
格式为 `<module>[:<Adapter>]`,`~` 为 `nonebot.adapters.` 的缩写。
|
||||||
@@ -49,7 +49,7 @@ class PluginMetadata:
|
|||||||
extra: dict[Any, Any] = field(default_factory=dict)
|
extra: dict[Any, Any] = field(default_factory=dict)
|
||||||
"""插件额外信息,可由插件编写者自由扩展定义"""
|
"""插件额外信息,可由插件编写者自由扩展定义"""
|
||||||
|
|
||||||
def get_supported_adapters(self) -> Optional[set[Type["Adapter"]]]: # noqa: UP006
|
def get_supported_adapters(self) -> set[Type["Adapter"]] | None: # noqa: UP006
|
||||||
"""获取当前已安装的插件支持适配器类列表"""
|
"""获取当前已安装的插件支持适配器类列表"""
|
||||||
if self.supported_adapters is None:
|
if self.supported_adapters is None:
|
||||||
return None
|
return None
|
||||||
@@ -77,11 +77,11 @@ class Plugin:
|
|||||||
"""导入该插件的插件管理器"""
|
"""导入该插件的插件管理器"""
|
||||||
matcher: set[type[Matcher]] = field(default_factory=set)
|
matcher: set[type[Matcher]] = field(default_factory=set)
|
||||||
"""插件加载时定义的 `Matcher`"""
|
"""插件加载时定义的 `Matcher`"""
|
||||||
parent_plugin: Optional["Plugin"] = None
|
parent_plugin: "Plugin | None" = None
|
||||||
"""父插件"""
|
"""父插件"""
|
||||||
sub_plugins: set["Plugin"] = field(default_factory=set)
|
sub_plugins: set["Plugin"] = field(default_factory=set)
|
||||||
"""子插件集合"""
|
"""子插件集合"""
|
||||||
metadata: Optional[PluginMetadata] = None
|
metadata: PluginMetadata | None = None
|
||||||
"""插件元信息"""
|
"""插件元信息"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
+44
-48
@@ -11,7 +11,7 @@ from datetime import datetime, timedelta
|
|||||||
import inspect
|
import inspect
|
||||||
import re
|
import re
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from typing import Any, Optional, Union
|
from typing import Any
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
from nonebot.adapters import Event
|
from nonebot.adapters import Event
|
||||||
@@ -48,7 +48,7 @@ def store_matcher(matcher: type[Matcher]) -> None:
|
|||||||
plugin.matcher.add(matcher)
|
plugin.matcher.add(matcher)
|
||||||
|
|
||||||
|
|
||||||
def get_matcher_plugin(depth: int = 1) -> Optional[Plugin]: # pragma: no cover
|
def get_matcher_plugin(depth: int = 1) -> Plugin | None: # pragma: no cover
|
||||||
"""获取事件响应器定义所在插件。
|
"""获取事件响应器定义所在插件。
|
||||||
|
|
||||||
**Deprecated**, 请使用 {ref}`nonebot.plugin.on.get_matcher_source` 获取信息。
|
**Deprecated**, 请使用 {ref}`nonebot.plugin.on.get_matcher_source` 获取信息。
|
||||||
@@ -63,7 +63,7 @@ def get_matcher_plugin(depth: int = 1) -> Optional[Plugin]: # pragma: no cover
|
|||||||
return (source := get_matcher_source(depth + 1)) and source.plugin
|
return (source := get_matcher_source(depth + 1)) and source.plugin
|
||||||
|
|
||||||
|
|
||||||
def get_matcher_module(depth: int = 1) -> Optional[ModuleType]: # pragma: no cover
|
def get_matcher_module(depth: int = 1) -> ModuleType | None: # pragma: no cover
|
||||||
"""获取事件响应器定义所在模块。
|
"""获取事件响应器定义所在模块。
|
||||||
|
|
||||||
**Deprecated**, 请使用 {ref}`nonebot.plugin.on.get_matcher_source` 获取信息。
|
**Deprecated**, 请使用 {ref}`nonebot.plugin.on.get_matcher_source` 获取信息。
|
||||||
@@ -78,7 +78,7 @@ def get_matcher_module(depth: int = 1) -> Optional[ModuleType]: # pragma: no co
|
|||||||
return (source := get_matcher_source(depth + 1)) and source.module
|
return (source := get_matcher_source(depth + 1)) and source.module
|
||||||
|
|
||||||
|
|
||||||
def get_matcher_source(depth: int = 0) -> Optional[MatcherSource]:
|
def get_matcher_source(depth: int = 0) -> MatcherSource | None:
|
||||||
"""获取事件响应器定义所在源码信息。
|
"""获取事件响应器定义所在源码信息。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -99,7 +99,7 @@ def get_matcher_source(depth: int = 0) -> Optional[MatcherSource]:
|
|||||||
module_name = (module := inspect.getmodule(frame)) and module.__name__
|
module_name = (module := inspect.getmodule(frame)) and module.__name__
|
||||||
|
|
||||||
# matcher defined when plugin loading
|
# matcher defined when plugin loading
|
||||||
plugin: Optional["Plugin"] = _current_plugin.get()
|
plugin: Plugin | None = _current_plugin.get()
|
||||||
# matcher defined when plugin running
|
# matcher defined when plugin running
|
||||||
if plugin is None and module_name:
|
if plugin is None and module_name:
|
||||||
plugin = get_plugin_by_module_name(module_name)
|
plugin = get_plugin_by_module_name(module_name)
|
||||||
@@ -113,15 +113,15 @@ def get_matcher_source(depth: int = 0) -> Optional[MatcherSource]:
|
|||||||
|
|
||||||
def on(
|
def on(
|
||||||
type: str = "",
|
type: str = "",
|
||||||
rule: Optional[Union[Rule, T_RuleChecker]] = None,
|
rule: Rule | T_RuleChecker | None = None,
|
||||||
permission: Optional[Union[Permission, T_PermissionChecker]] = None,
|
permission: Permission | T_PermissionChecker | None = None,
|
||||||
*,
|
*,
|
||||||
handlers: Optional[list[Union[T_Handler, Dependent[Any]]]] = None,
|
handlers: list[T_Handler | Dependent[Any]] | None = None,
|
||||||
temp: bool = False,
|
temp: bool = False,
|
||||||
expire_time: Optional[Union[datetime, timedelta]] = None,
|
expire_time: datetime | timedelta | None = None,
|
||||||
priority: int = 1,
|
priority: int = 1,
|
||||||
block: bool = False,
|
block: bool = False,
|
||||||
state: Optional[T_State] = None,
|
state: T_State | None = None,
|
||||||
_depth: int = 0,
|
_depth: int = 0,
|
||||||
) -> type[Matcher]:
|
) -> type[Matcher]:
|
||||||
"""注册一个基础事件响应器,可自定义类型。
|
"""注册一个基础事件响应器,可自定义类型。
|
||||||
@@ -219,8 +219,8 @@ def on_request(*args, _depth: int = 0, **kwargs) -> type[Matcher]:
|
|||||||
|
|
||||||
|
|
||||||
def on_startswith(
|
def on_startswith(
|
||||||
msg: Union[str, tuple[str, ...]],
|
msg: str | tuple[str, ...],
|
||||||
rule: Optional[Union[Rule, T_RuleChecker]] = None,
|
rule: Rule | T_RuleChecker | None = None,
|
||||||
ignorecase: bool = False,
|
ignorecase: bool = False,
|
||||||
_depth: int = 0,
|
_depth: int = 0,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
@@ -243,8 +243,8 @@ def on_startswith(
|
|||||||
|
|
||||||
|
|
||||||
def on_endswith(
|
def on_endswith(
|
||||||
msg: Union[str, tuple[str, ...]],
|
msg: str | tuple[str, ...],
|
||||||
rule: Optional[Union[Rule, T_RuleChecker]] = None,
|
rule: Rule | T_RuleChecker | None = None,
|
||||||
ignorecase: bool = False,
|
ignorecase: bool = False,
|
||||||
_depth: int = 0,
|
_depth: int = 0,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
@@ -267,8 +267,8 @@ def on_endswith(
|
|||||||
|
|
||||||
|
|
||||||
def on_fullmatch(
|
def on_fullmatch(
|
||||||
msg: Union[str, tuple[str, ...]],
|
msg: str | tuple[str, ...],
|
||||||
rule: Optional[Union[Rule, T_RuleChecker]] = None,
|
rule: Rule | T_RuleChecker | None = None,
|
||||||
ignorecase: bool = False,
|
ignorecase: bool = False,
|
||||||
_depth: int = 0,
|
_depth: int = 0,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
@@ -292,7 +292,7 @@ def on_fullmatch(
|
|||||||
|
|
||||||
def on_keyword(
|
def on_keyword(
|
||||||
keywords: set[str],
|
keywords: set[str],
|
||||||
rule: Optional[Union[Rule, T_RuleChecker]] = None,
|
rule: Rule | T_RuleChecker | None = None,
|
||||||
_depth: int = 0,
|
_depth: int = 0,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> type[Matcher]:
|
) -> type[Matcher]:
|
||||||
@@ -313,10 +313,10 @@ def on_keyword(
|
|||||||
|
|
||||||
|
|
||||||
def on_command(
|
def on_command(
|
||||||
cmd: Union[str, tuple[str, ...]],
|
cmd: str | tuple[str, ...],
|
||||||
rule: Optional[Union[Rule, T_RuleChecker]] = None,
|
rule: Rule | T_RuleChecker | None = None,
|
||||||
aliases: Optional[set[Union[str, tuple[str, ...]]]] = None,
|
aliases: set[str | tuple[str, ...]] | None = None,
|
||||||
force_whitespace: Optional[Union[str, bool]] = None,
|
force_whitespace: str | bool | None = None,
|
||||||
_depth: int = 0,
|
_depth: int = 0,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> type[Matcher]:
|
) -> type[Matcher]:
|
||||||
@@ -348,10 +348,10 @@ def on_command(
|
|||||||
|
|
||||||
|
|
||||||
def on_shell_command(
|
def on_shell_command(
|
||||||
cmd: Union[str, tuple[str, ...]],
|
cmd: str | tuple[str, ...],
|
||||||
rule: Optional[Union[Rule, T_RuleChecker]] = None,
|
rule: Rule | T_RuleChecker | None = None,
|
||||||
aliases: Optional[set[Union[str, tuple[str, ...]]]] = None,
|
aliases: set[str | tuple[str, ...]] | None = None,
|
||||||
parser: Optional[ArgumentParser] = None,
|
parser: ArgumentParser | None = None,
|
||||||
_depth: int = 0,
|
_depth: int = 0,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> type[Matcher]:
|
) -> type[Matcher]:
|
||||||
@@ -386,8 +386,8 @@ def on_shell_command(
|
|||||||
|
|
||||||
def on_regex(
|
def on_regex(
|
||||||
pattern: str,
|
pattern: str,
|
||||||
flags: Union[int, re.RegexFlag] = 0,
|
flags: int | re.RegexFlag = 0,
|
||||||
rule: Optional[Union[Rule, T_RuleChecker]] = None,
|
rule: Rule | T_RuleChecker | None = None,
|
||||||
_depth: int = 0,
|
_depth: int = 0,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> type[Matcher]:
|
) -> type[Matcher]:
|
||||||
@@ -411,8 +411,8 @@ def on_regex(
|
|||||||
|
|
||||||
|
|
||||||
def on_type(
|
def on_type(
|
||||||
types: Union[type[Event], tuple[type[Event], ...]],
|
types: type[Event] | tuple[type[Event], ...],
|
||||||
rule: Optional[Union[Rule, T_RuleChecker]] = None,
|
rule: Rule | T_RuleChecker | None = None,
|
||||||
*,
|
*,
|
||||||
_depth: int = 0,
|
_depth: int = 0,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
@@ -443,7 +443,7 @@ class _Group:
|
|||||||
"""其他传递给 `on` 的参数默认值"""
|
"""其他传递给 `on` 的参数默认值"""
|
||||||
|
|
||||||
def _get_final_kwargs(
|
def _get_final_kwargs(
|
||||||
self, update: dict[str, Any], *, exclude: Optional[set[str]] = None
|
self, update: dict[str, Any], *, exclude: set[str] | None = None
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""获取最终传递给 `on` 的参数
|
"""获取最终传递给 `on` 的参数
|
||||||
|
|
||||||
@@ -477,7 +477,7 @@ class CommandGroup(_Group):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, cmd: Union[str, tuple[str, ...]], prefix_aliases: bool = False, **kwargs
|
self, cmd: str | tuple[str, ...], prefix_aliases: bool = False, **kwargs
|
||||||
):
|
):
|
||||||
"""命令前缀"""
|
"""命令前缀"""
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
@@ -488,7 +488,7 @@ class CommandGroup(_Group):
|
|||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"CommandGroup(cmd={self.basecmd}, matchers={len(self.matchers)})"
|
return f"CommandGroup(cmd={self.basecmd}, matchers={len(self.matchers)})"
|
||||||
|
|
||||||
def command(self, cmd: Union[str, tuple[str, ...]], **kwargs) -> type[Matcher]:
|
def command(self, cmd: str | tuple[str, ...], **kwargs) -> type[Matcher]:
|
||||||
"""注册一个新的命令。新参数将会覆盖命令组默认值
|
"""注册一个新的命令。新参数将会覆盖命令组默认值
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -515,9 +515,7 @@ class CommandGroup(_Group):
|
|||||||
self.matchers.append(matcher)
|
self.matchers.append(matcher)
|
||||||
return matcher
|
return matcher
|
||||||
|
|
||||||
def shell_command(
|
def shell_command(self, cmd: str | tuple[str, ...], **kwargs) -> type[Matcher]:
|
||||||
self, cmd: Union[str, tuple[str, ...]], **kwargs
|
|
||||||
) -> type[Matcher]:
|
|
||||||
"""注册一个新的 `shell_like` 命令。新参数将会覆盖命令组默认值
|
"""注册一个新的 `shell_like` 命令。新参数将会覆盖命令组默认值
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -641,9 +639,7 @@ class MatcherGroup(_Group):
|
|||||||
self.matchers.append(matcher)
|
self.matchers.append(matcher)
|
||||||
return matcher
|
return matcher
|
||||||
|
|
||||||
def on_startswith(
|
def on_startswith(self, msg: str | tuple[str, ...], **kwargs) -> type[Matcher]:
|
||||||
self, msg: Union[str, tuple[str, ...]], **kwargs
|
|
||||||
) -> type[Matcher]:
|
|
||||||
"""注册一个消息事件响应器,并且当消息的**文本部分**以指定内容开头时响应。
|
"""注册一个消息事件响应器,并且当消息的**文本部分**以指定内容开头时响应。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -663,7 +659,7 @@ class MatcherGroup(_Group):
|
|||||||
self.matchers.append(matcher)
|
self.matchers.append(matcher)
|
||||||
return matcher
|
return matcher
|
||||||
|
|
||||||
def on_endswith(self, msg: Union[str, tuple[str, ...]], **kwargs) -> type[Matcher]:
|
def on_endswith(self, msg: str | tuple[str, ...], **kwargs) -> type[Matcher]:
|
||||||
"""注册一个消息事件响应器,并且当消息的**文本部分**以指定内容结尾时响应。
|
"""注册一个消息事件响应器,并且当消息的**文本部分**以指定内容结尾时响应。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -683,7 +679,7 @@ class MatcherGroup(_Group):
|
|||||||
self.matchers.append(matcher)
|
self.matchers.append(matcher)
|
||||||
return matcher
|
return matcher
|
||||||
|
|
||||||
def on_fullmatch(self, msg: Union[str, tuple[str, ...]], **kwargs) -> type[Matcher]:
|
def on_fullmatch(self, msg: str | tuple[str, ...], **kwargs) -> type[Matcher]:
|
||||||
"""注册一个消息事件响应器,并且当消息的**文本部分**与指定内容完全一致时响应。
|
"""注册一个消息事件响应器,并且当消息的**文本部分**与指定内容完全一致时响应。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -724,9 +720,9 @@ class MatcherGroup(_Group):
|
|||||||
|
|
||||||
def on_command(
|
def on_command(
|
||||||
self,
|
self,
|
||||||
cmd: Union[str, tuple[str, ...]],
|
cmd: str | tuple[str, ...],
|
||||||
aliases: Optional[set[Union[str, tuple[str, ...]]]] = None,
|
aliases: set[str | tuple[str, ...]] | None = None,
|
||||||
force_whitespace: Optional[Union[str, bool]] = None,
|
force_whitespace: str | bool | None = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> type[Matcher]:
|
) -> type[Matcher]:
|
||||||
"""注册一个消息事件响应器,并且当消息以指定命令开头时响应。
|
"""注册一个消息事件响应器,并且当消息以指定命令开头时响应。
|
||||||
@@ -755,9 +751,9 @@ class MatcherGroup(_Group):
|
|||||||
|
|
||||||
def on_shell_command(
|
def on_shell_command(
|
||||||
self,
|
self,
|
||||||
cmd: Union[str, tuple[str, ...]],
|
cmd: str | tuple[str, ...],
|
||||||
aliases: Optional[set[Union[str, tuple[str, ...]]]] = None,
|
aliases: set[str | tuple[str, ...]] | None = None,
|
||||||
parser: Optional[ArgumentParser] = None,
|
parser: ArgumentParser | None = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> type[Matcher]:
|
) -> type[Matcher]:
|
||||||
"""注册一个支持 `shell_like` 解析参数的命令消息事件响应器。
|
"""注册一个支持 `shell_like` 解析参数的命令消息事件响应器。
|
||||||
@@ -786,7 +782,7 @@ class MatcherGroup(_Group):
|
|||||||
return matcher
|
return matcher
|
||||||
|
|
||||||
def on_regex(
|
def on_regex(
|
||||||
self, pattern: str, flags: Union[int, re.RegexFlag] = 0, **kwargs
|
self, pattern: str, flags: int | re.RegexFlag = 0, **kwargs
|
||||||
) -> type[Matcher]:
|
) -> type[Matcher]:
|
||||||
"""注册一个消息事件响应器,并且当消息匹配正则表达式时响应。
|
"""注册一个消息事件响应器,并且当消息匹配正则表达式时响应。
|
||||||
|
|
||||||
@@ -810,7 +806,7 @@ class MatcherGroup(_Group):
|
|||||||
return matcher
|
return matcher
|
||||||
|
|
||||||
def on_type(
|
def on_type(
|
||||||
self, types: Union[type[Event], tuple[type[Event]]], **kwargs
|
self, types: type[Event] | tuple[type[Event]], **kwargs
|
||||||
) -> type[Matcher]:
|
) -> type[Matcher]:
|
||||||
"""注册一个事件响应器,并且当事件为指定类型时响应。
|
"""注册一个事件响应器,并且当事件为指定类型时响应。
|
||||||
|
|
||||||
|
|||||||
+37
-39
@@ -24,10 +24,8 @@ from typing import (
|
|||||||
IO,
|
IO,
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
NamedTuple,
|
NamedTuple,
|
||||||
Optional,
|
|
||||||
TypedDict,
|
TypedDict,
|
||||||
TypeVar,
|
TypeVar,
|
||||||
Union,
|
|
||||||
cast,
|
cast,
|
||||||
overload,
|
overload,
|
||||||
)
|
)
|
||||||
@@ -61,11 +59,11 @@ T = TypeVar("T")
|
|||||||
|
|
||||||
|
|
||||||
class CMD_RESULT(TypedDict):
|
class CMD_RESULT(TypedDict):
|
||||||
command: Optional[tuple[str, ...]]
|
command: tuple[str, ...] | None
|
||||||
raw_command: Optional[str]
|
raw_command: str | None
|
||||||
command_arg: Optional[Message]
|
command_arg: Message | None
|
||||||
command_start: Optional[str]
|
command_start: str | None
|
||||||
command_whitespace: Optional[str]
|
command_whitespace: str | None
|
||||||
|
|
||||||
|
|
||||||
class TRIE_VALUE(NamedTuple):
|
class TRIE_VALUE(NamedTuple):
|
||||||
@@ -179,7 +177,7 @@ class StartswithRule:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def startswith(msg: Union[str, tuple[str, ...]], ignorecase: bool = False) -> Rule:
|
def startswith(msg: str | tuple[str, ...], ignorecase: bool = False) -> Rule:
|
||||||
"""匹配消息纯文本开头。
|
"""匹配消息纯文本开头。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -234,7 +232,7 @@ class EndswithRule:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def endswith(msg: Union[str, tuple[str, ...]], ignorecase: bool = False) -> Rule:
|
def endswith(msg: str | tuple[str, ...], ignorecase: bool = False) -> Rule:
|
||||||
"""匹配消息纯文本结尾。
|
"""匹配消息纯文本结尾。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -288,7 +286,7 @@ class FullmatchRule:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def fullmatch(msg: Union[str, tuple[str, ...]], ignorecase: bool = False) -> Rule:
|
def fullmatch(msg: str | tuple[str, ...], ignorecase: bool = False) -> Rule:
|
||||||
"""完全匹配消息。
|
"""完全匹配消息。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -360,7 +358,7 @@ class CommandRule:
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
cmds: list[tuple[str, ...]],
|
cmds: list[tuple[str, ...]],
|
||||||
force_whitespace: Optional[Union[str, bool]] = None,
|
force_whitespace: str | bool | None = None,
|
||||||
):
|
):
|
||||||
self.cmds = tuple(cmds)
|
self.cmds = tuple(cmds)
|
||||||
self.force_whitespace = force_whitespace
|
self.force_whitespace = force_whitespace
|
||||||
@@ -378,9 +376,9 @@ class CommandRule:
|
|||||||
|
|
||||||
async def __call__(
|
async def __call__(
|
||||||
self,
|
self,
|
||||||
cmd: Optional[tuple[str, ...]] = Command(),
|
cmd: tuple[str, ...] | None = Command(),
|
||||||
cmd_arg: Optional[Message] = CommandArg(),
|
cmd_arg: Message | None = CommandArg(),
|
||||||
cmd_whitespace: Optional[str] = CommandWhitespace(),
|
cmd_whitespace: str | None = CommandWhitespace(),
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if cmd not in self.cmds:
|
if cmd not in self.cmds:
|
||||||
return False
|
return False
|
||||||
@@ -392,8 +390,8 @@ class CommandRule:
|
|||||||
|
|
||||||
|
|
||||||
def command(
|
def command(
|
||||||
*cmds: Union[str, tuple[str, ...]],
|
*cmds: str | tuple[str, ...],
|
||||||
force_whitespace: Optional[Union[str, bool]] = None,
|
force_whitespace: str | bool | None = None,
|
||||||
) -> Rule:
|
) -> Rule:
|
||||||
"""匹配消息命令。
|
"""匹配消息命令。
|
||||||
|
|
||||||
@@ -456,36 +454,36 @@ class ArgumentParser(ArgParser):
|
|||||||
@overload
|
@overload
|
||||||
def parse_known_args(
|
def parse_known_args(
|
||||||
self,
|
self,
|
||||||
args: Optional[Sequence[Union[str, MessageSegment]]] = None,
|
args: Sequence[str | MessageSegment] | None = None,
|
||||||
namespace: None = None,
|
namespace: None = None,
|
||||||
) -> tuple[Namespace, list[Union[str, MessageSegment]]]: ...
|
) -> tuple[Namespace, list[str | MessageSegment]]: ...
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def parse_known_args(
|
def parse_known_args(
|
||||||
self, args: Optional[Sequence[Union[str, MessageSegment]]], namespace: T
|
self, args: Sequence[str | MessageSegment] | None, namespace: T
|
||||||
) -> tuple[T, list[Union[str, MessageSegment]]]: ...
|
) -> tuple[T, list[str | MessageSegment]]: ...
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def parse_known_args(
|
def parse_known_args(
|
||||||
self, *, namespace: T
|
self, *, namespace: T
|
||||||
) -> tuple[T, list[Union[str, MessageSegment]]]: ...
|
) -> tuple[T, list[str | MessageSegment]]: ...
|
||||||
|
|
||||||
def parse_known_args( # pyright: ignore[reportIncompatibleMethodOverride]
|
def parse_known_args( # pyright: ignore[reportIncompatibleMethodOverride]
|
||||||
self,
|
self,
|
||||||
args: Optional[Sequence[Union[str, MessageSegment]]] = None,
|
args: Sequence[str | MessageSegment] | None = None,
|
||||||
namespace: Optional[T] = None,
|
namespace: T | None = None,
|
||||||
) -> tuple[Union[Namespace, T], list[Union[str, MessageSegment]]]: ...
|
) -> tuple[Namespace | T, list[str | MessageSegment]]: ...
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def parse_args(
|
def parse_args(
|
||||||
self,
|
self,
|
||||||
args: Optional[Sequence[Union[str, MessageSegment]]] = None,
|
args: Sequence[str | MessageSegment] | None = None,
|
||||||
namespace: None = None,
|
namespace: None = None,
|
||||||
) -> Namespace: ...
|
) -> Namespace: ...
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def parse_args(
|
def parse_args(
|
||||||
self, args: Optional[Sequence[Union[str, MessageSegment]]], namespace: T
|
self, args: Sequence[str | MessageSegment] | None, namespace: T
|
||||||
) -> T: ...
|
) -> T: ...
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
@@ -493,29 +491,29 @@ class ArgumentParser(ArgParser):
|
|||||||
|
|
||||||
def parse_args(
|
def parse_args(
|
||||||
self,
|
self,
|
||||||
args: Optional[Sequence[Union[str, MessageSegment]]] = None,
|
args: Sequence[str | MessageSegment] | None = None,
|
||||||
namespace: Optional[T] = None,
|
namespace: T | None = None,
|
||||||
) -> Union[Namespace, T]:
|
) -> Namespace | T:
|
||||||
result, argv = self.parse_known_args(args, namespace)
|
result, argv = self.parse_known_args(args, namespace)
|
||||||
if argv:
|
if argv:
|
||||||
msg = gettext("unrecognized arguments: %s")
|
msg = gettext("unrecognized arguments: %s")
|
||||||
self.error(msg % " ".join(map(str, argv)))
|
self.error(msg % " ".join(map(str, argv)))
|
||||||
return cast(Union[Namespace, T], result)
|
return cast(Namespace | T, result)
|
||||||
|
|
||||||
def _parse_optional(
|
def _parse_optional(
|
||||||
self, arg_string: Union[str, MessageSegment]
|
self, arg_string: str | MessageSegment
|
||||||
) -> Optional[tuple[Optional[Action], str, Optional[str]]]:
|
) -> tuple[Action | None, str, str | None] | None:
|
||||||
return (
|
return (
|
||||||
super()._parse_optional(arg_string) if isinstance(arg_string, str) else None
|
super()._parse_optional(arg_string) if isinstance(arg_string, str) else None
|
||||||
)
|
)
|
||||||
|
|
||||||
def _print_message(self, message: str, file: Optional[IO[str]] = None): # type: ignore
|
def _print_message(self, message: str, file: IO[str] | None = None): # type: ignore
|
||||||
if (msg := parser_message.get(None)) is not None:
|
if (msg := parser_message.get(None)) is not None:
|
||||||
parser_message.set(msg + message)
|
parser_message.set(msg + message)
|
||||||
else:
|
else:
|
||||||
super()._print_message(message, file)
|
super()._print_message(message, file)
|
||||||
|
|
||||||
def exit(self, status: int = 0, message: Optional[str] = None):
|
def exit(self, status: int = 0, message: str | None = None):
|
||||||
if message:
|
if message:
|
||||||
self._print_message(message)
|
self._print_message(message)
|
||||||
raise ParserExit(status=status, message=parser_message.get(None))
|
raise ParserExit(status=status, message=parser_message.get(None))
|
||||||
@@ -531,7 +529,7 @@ class ShellCommandRule:
|
|||||||
|
|
||||||
__slots__ = ("cmds", "parser")
|
__slots__ = ("cmds", "parser")
|
||||||
|
|
||||||
def __init__(self, cmds: list[tuple[str, ...]], parser: Optional[ArgumentParser]):
|
def __init__(self, cmds: list[tuple[str, ...]], parser: ArgumentParser | None):
|
||||||
self.cmds = tuple(cmds)
|
self.cmds = tuple(cmds)
|
||||||
self.parser = parser
|
self.parser = parser
|
||||||
|
|
||||||
@@ -551,8 +549,8 @@ class ShellCommandRule:
|
|||||||
async def __call__(
|
async def __call__(
|
||||||
self,
|
self,
|
||||||
state: T_State,
|
state: T_State,
|
||||||
cmd: Optional[tuple[str, ...]] = Command(),
|
cmd: tuple[str, ...] | None = Command(),
|
||||||
msg: Optional[Message] = CommandArg(),
|
msg: Message | None = CommandArg(),
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if cmd not in self.cmds or msg is None:
|
if cmd not in self.cmds or msg is None:
|
||||||
return False
|
return False
|
||||||
@@ -589,7 +587,7 @@ class ShellCommandRule:
|
|||||||
|
|
||||||
|
|
||||||
def shell_command(
|
def shell_command(
|
||||||
*cmds: Union[str, tuple[str, ...]], parser: Optional[ArgumentParser] = None
|
*cmds: str | tuple[str, ...], parser: ArgumentParser | None = None
|
||||||
) -> Rule:
|
) -> Rule:
|
||||||
"""匹配 `shell_like` 形式的消息命令。
|
"""匹配 `shell_like` 形式的消息命令。
|
||||||
|
|
||||||
@@ -695,7 +693,7 @@ class RegexRule:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def regex(regex: str, flags: Union[int, re.RegexFlag] = 0) -> Rule:
|
def regex(regex: str, flags: int | re.RegexFlag = 0) -> Rule:
|
||||||
"""匹配符合正则表达式的消息字符串。
|
"""匹配符合正则表达式的消息字符串。
|
||||||
|
|
||||||
可以通过 {ref}`nonebot.params.RegexStr` 获取匹配成功的字符串,
|
可以通过 {ref}`nonebot.params.RegexStr` 获取匹配成功的字符串,
|
||||||
|
|||||||
+11
-31
@@ -15,9 +15,9 @@ FrontMatter:
|
|||||||
import sys
|
import sys
|
||||||
import types
|
import types
|
||||||
import typing as t
|
import typing as t
|
||||||
from typing import TYPE_CHECKING, TypeVar
|
from typing import TYPE_CHECKING, TypeAlias, TypeVar, get_args, get_origin
|
||||||
import typing_extensions as t_ext
|
import typing_extensions as t_ext
|
||||||
from typing_extensions import ParamSpec, TypeAlias, get_args, get_origin, override
|
from typing_extensions import ParamSpec, override
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -43,31 +43,15 @@ def overrides(InterfaceClass: object):
|
|||||||
return override
|
return override
|
||||||
|
|
||||||
|
|
||||||
if sys.version_info < (3, 10):
|
def type_has_args(type_: type[t.Any]) -> bool:
|
||||||
|
return isinstance(type_, (t._GenericAlias, types.GenericAlias, types.UnionType)) # type: ignore
|
||||||
def type_has_args(type_: type[t.Any]) -> bool:
|
|
||||||
"""判断类型是否有参数"""
|
|
||||||
return isinstance(type_, (t._GenericAlias, types.GenericAlias)) # type: ignore
|
|
||||||
|
|
||||||
else:
|
|
||||||
|
|
||||||
def type_has_args(type_: type[t.Any]) -> bool:
|
|
||||||
return isinstance(type_, (t._GenericAlias, types.GenericAlias, types.UnionType)) # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
if sys.version_info < (3, 10):
|
def origin_is_union(origin: type[t.Any] | None) -> bool:
|
||||||
|
return origin is t.Union or origin is types.UnionType
|
||||||
def origin_is_union(origin: t.Optional[type[t.Any]]) -> bool:
|
|
||||||
"""判断是否是 Union 类型"""
|
|
||||||
return origin is t.Union
|
|
||||||
|
|
||||||
else:
|
|
||||||
|
|
||||||
def origin_is_union(origin: t.Optional[type[t.Any]]) -> bool:
|
|
||||||
return origin is t.Union or origin is types.UnionType
|
|
||||||
|
|
||||||
|
|
||||||
def origin_is_literal(origin: t.Optional[type[t.Any]]) -> bool:
|
def origin_is_literal(origin: type[t.Any] | None) -> bool:
|
||||||
"""判断是否是 Literal 类型"""
|
"""判断是否是 Literal 类型"""
|
||||||
return origin is t.Literal or origin is t_ext.Literal
|
return origin is t.Literal or origin is t_ext.Literal
|
||||||
|
|
||||||
@@ -84,14 +68,12 @@ def all_literal_values(type_: type[t.Any]) -> list[t.Any]:
|
|||||||
return [x for value in _literal_values(type_) for x in all_literal_values(value)]
|
return [x for value in _literal_values(type_) for x in all_literal_values(value)]
|
||||||
|
|
||||||
|
|
||||||
def origin_is_annotated(origin: t.Optional[type[t.Any]]) -> bool:
|
def origin_is_annotated(origin: type[t.Any] | None) -> bool:
|
||||||
"""判断是否是 Annotated 类型"""
|
"""判断是否是 Annotated 类型"""
|
||||||
return origin is t_ext.Annotated
|
return origin is t_ext.Annotated
|
||||||
|
|
||||||
|
|
||||||
NONE_TYPES = {None, type(None), t.Literal[None], t_ext.Literal[None]} # noqa: PYI061
|
NONE_TYPES = {None, type(None), t.Literal[None], t_ext.Literal[None], types.NoneType} # noqa: PYI061
|
||||||
if sys.version_info >= (3, 10):
|
|
||||||
NONE_TYPES.add(types.NoneType)
|
|
||||||
|
|
||||||
|
|
||||||
def is_none_type(type_: type[t.Any]) -> bool:
|
def is_none_type(type_: type[t.Any]) -> bool:
|
||||||
@@ -133,9 +115,7 @@ _STATE_FLAG = StateFlag()
|
|||||||
T_State: TypeAlias = t.Annotated[dict[t.Any, t.Any], _STATE_FLAG]
|
T_State: TypeAlias = t.Annotated[dict[t.Any, t.Any], _STATE_FLAG]
|
||||||
"""事件处理状态 State 类型"""
|
"""事件处理状态 State 类型"""
|
||||||
|
|
||||||
_DependentCallable: TypeAlias = t.Union[
|
_DependentCallable: TypeAlias = t.Callable[..., T] | t.Callable[..., t.Awaitable[T]]
|
||||||
t.Callable[..., T], t.Callable[..., t.Awaitable[T]]
|
|
||||||
]
|
|
||||||
|
|
||||||
# driver hooks
|
# driver hooks
|
||||||
T_BotConnectionHook: TypeAlias = _DependentCallable[t.Any]
|
T_BotConnectionHook: TypeAlias = _DependentCallable[t.Any]
|
||||||
@@ -163,7 +143,7 @@ T_CallingAPIHook: TypeAlias = t.Callable[
|
|||||||
]
|
]
|
||||||
"""`bot.call_api` 钩子函数"""
|
"""`bot.call_api` 钩子函数"""
|
||||||
T_CalledAPIHook: TypeAlias = t.Callable[
|
T_CalledAPIHook: TypeAlias = t.Callable[
|
||||||
["Bot", t.Optional[Exception], str, dict[str, t.Any], t.Any], t.Awaitable[t.Any]
|
["Bot", Exception | None, str, dict[str, t.Any], t.Any], t.Awaitable[t.Any]
|
||||||
]
|
]
|
||||||
"""`bot.call_api` 后执行的函数,参数分别为 bot, exception, api, data, result"""
|
"""`bot.call_api` 后执行的函数,参数分别为 bot, exception, api, data, result"""
|
||||||
|
|
||||||
|
|||||||
+82
-13
@@ -8,18 +8,37 @@ FrontMatter:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from collections.abc import AsyncGenerator, Coroutine, Generator, Mapping, Sequence
|
from collections.abc import (
|
||||||
|
AsyncGenerator,
|
||||||
|
Callable,
|
||||||
|
Coroutine,
|
||||||
|
Generator,
|
||||||
|
Mapping,
|
||||||
|
Sequence,
|
||||||
|
)
|
||||||
import contextlib
|
import contextlib
|
||||||
from contextlib import AbstractContextManager, asynccontextmanager
|
from contextlib import AbstractContextManager, asynccontextmanager
|
||||||
import dataclasses
|
import dataclasses
|
||||||
|
from enum import Enum
|
||||||
from functools import partial, wraps
|
from functools import partial, wraps
|
||||||
import importlib
|
import importlib
|
||||||
import inspect
|
import inspect
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
from typing import Any, Callable, Generic, Optional, TypeVar, Union, overload
|
from typing import (
|
||||||
from typing_extensions import ParamSpec, get_args, get_origin, override
|
Any,
|
||||||
|
Final,
|
||||||
|
Generic,
|
||||||
|
Literal,
|
||||||
|
TypeAlias,
|
||||||
|
TypeVar,
|
||||||
|
final,
|
||||||
|
get_args,
|
||||||
|
get_origin,
|
||||||
|
overload,
|
||||||
|
)
|
||||||
|
from typing_extensions import ParamSpec, override
|
||||||
|
|
||||||
import anyio
|
import anyio
|
||||||
import anyio.to_thread
|
import anyio.to_thread
|
||||||
@@ -35,6 +54,8 @@ from nonebot.typing import (
|
|||||||
type_has_args,
|
type_has_args,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from .compat import custom_validation
|
||||||
|
|
||||||
P = ParamSpec("P")
|
P = ParamSpec("P")
|
||||||
R = TypeVar("R")
|
R = TypeVar("R")
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
@@ -43,6 +64,54 @@ V = TypeVar("V")
|
|||||||
E = TypeVar("E", bound=BaseException)
|
E = TypeVar("E", bound=BaseException)
|
||||||
|
|
||||||
|
|
||||||
|
@final
|
||||||
|
@custom_validation
|
||||||
|
class Unset(Enum):
|
||||||
|
_UNSET = "<UNSET>"
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return "<UNSET>"
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.__repr__()
|
||||||
|
|
||||||
|
def __bool__(self) -> Literal[False]:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __copy__(self):
|
||||||
|
return self._UNSET
|
||||||
|
|
||||||
|
def __deepcopy__(self, memo: dict[int, Any]):
|
||||||
|
return self._UNSET
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def __get_validators__(cls):
|
||||||
|
yield cls._validate
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _validate(cls, value: Any):
|
||||||
|
if value is not cls._UNSET:
|
||||||
|
raise ValueError(f"{value!r} is not UNSET")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
UnsetType: TypeAlias = Literal[Unset._UNSET]
|
||||||
|
|
||||||
|
UNSET: Final[UnsetType] = Unset._UNSET
|
||||||
|
|
||||||
|
|
||||||
|
def exclude_unset(data: Any) -> Any:
|
||||||
|
if isinstance(data, dict):
|
||||||
|
return data.__class__(
|
||||||
|
(k, exclude_unset(v)) for k, v in data.items() if v is not UNSET
|
||||||
|
)
|
||||||
|
elif isinstance(data, list):
|
||||||
|
return data.__class__(exclude_unset(i) for i in data if i is not UNSET)
|
||||||
|
elif data is UNSET:
|
||||||
|
return None
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
def escape_tag(s: str) -> str:
|
def escape_tag(s: str) -> str:
|
||||||
"""用于记录带颜色日志时转义 `<tag>` 类型特殊标签
|
"""用于记录带颜色日志时转义 `<tag>` 类型特殊标签
|
||||||
|
|
||||||
@@ -73,7 +142,7 @@ def deep_update(
|
|||||||
|
|
||||||
|
|
||||||
def lenient_issubclass(
|
def lenient_issubclass(
|
||||||
cls: Any, class_or_tuple: Union[type[Any], tuple[type[Any], ...]]
|
cls: Any, class_or_tuple: type[Any] | tuple[type[Any], ...]
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""检查 cls 是否是 class_or_tuple 中的一个类型子类并忽略类型错误。"""
|
"""检查 cls 是否是 class_or_tuple 中的一个类型子类并忽略类型错误。"""
|
||||||
try:
|
try:
|
||||||
@@ -83,7 +152,7 @@ def lenient_issubclass(
|
|||||||
|
|
||||||
|
|
||||||
def generic_check_issubclass(
|
def generic_check_issubclass(
|
||||||
cls: Any, class_or_tuple: Union[type[Any], tuple[type[Any], ...]]
|
cls: Any, class_or_tuple: type[Any] | tuple[type[Any], ...]
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""检查 cls 是否是 class_or_tuple 中的一个类型子类。
|
"""检查 cls 是否是 class_or_tuple 中的一个类型子类。
|
||||||
|
|
||||||
@@ -141,7 +210,7 @@ def type_is_complex(type_: type[Any]) -> bool:
|
|||||||
return _type_is_complex_inner(type_) or _type_is_complex_inner(origin)
|
return _type_is_complex_inner(type_) or _type_is_complex_inner(origin)
|
||||||
|
|
||||||
|
|
||||||
def _type_is_complex_inner(type_: Optional[type[Any]]) -> bool:
|
def _type_is_complex_inner(type_: type[Any] | None) -> bool:
|
||||||
if lenient_issubclass(type_, (str, bytes)):
|
if lenient_issubclass(type_, (str, bytes)):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -212,7 +281,7 @@ async def run_coro_with_catch(
|
|||||||
coro: Coroutine[Any, Any, T],
|
coro: Coroutine[Any, Any, T],
|
||||||
exc: tuple[type[Exception], ...],
|
exc: tuple[type[Exception], ...],
|
||||||
return_on_err: None = None,
|
return_on_err: None = None,
|
||||||
) -> Union[T, None]: ...
|
) -> T | None: ...
|
||||||
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
@@ -220,14 +289,14 @@ async def run_coro_with_catch(
|
|||||||
coro: Coroutine[Any, Any, T],
|
coro: Coroutine[Any, Any, T],
|
||||||
exc: tuple[type[Exception], ...],
|
exc: tuple[type[Exception], ...],
|
||||||
return_on_err: R,
|
return_on_err: R,
|
||||||
) -> Union[T, R]: ...
|
) -> T | R: ...
|
||||||
|
|
||||||
|
|
||||||
async def run_coro_with_catch(
|
async def run_coro_with_catch(
|
||||||
coro: Coroutine[Any, Any, T],
|
coro: Coroutine[Any, Any, T],
|
||||||
exc: tuple[type[Exception], ...],
|
exc: tuple[type[Exception], ...],
|
||||||
return_on_err: Optional[R] = None,
|
return_on_err: R | None = None,
|
||||||
) -> Optional[Union[T, R]]:
|
) -> T | R | None:
|
||||||
"""运行协程并当遇到指定异常时返回指定值。
|
"""运行协程并当遇到指定异常时返回指定值。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -288,7 +357,7 @@ def path_to_module_name(path: Path) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def resolve_dot_notation(
|
def resolve_dot_notation(
|
||||||
obj_str: str, default_attr: str, default_prefix: Optional[str] = None
|
obj_str: str, default_attr: str, default_prefix: str | None = None
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""解析并导入点分表示法的对象"""
|
"""解析并导入点分表示法的对象"""
|
||||||
modulename, _, cls = obj_str.partition(":")
|
modulename, _, cls = obj_str.partition(":")
|
||||||
@@ -309,7 +378,7 @@ class classproperty(Generic[T]):
|
|||||||
def __init__(self, func: Callable[[Any], T]) -> None:
|
def __init__(self, func: Callable[[Any], T]) -> None:
|
||||||
self.func = func
|
self.func = func
|
||||||
|
|
||||||
def __get__(self, instance: Any, owner: Optional[type[Any]] = None) -> T:
|
def __get__(self, instance: Any, owner: type[Any] | None = None) -> T:
|
||||||
return self.func(type(instance) if owner is None else owner)
|
return self.func(type(instance) if owner is None else owner)
|
||||||
|
|
||||||
|
|
||||||
@@ -339,7 +408,7 @@ def logger_wrapper(logger_name: str):
|
|||||||
- exception: 异常信息
|
- exception: 异常信息
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def log(level: str, message: str, exception: Optional[Exception] = None):
|
def log(level: str, message: str, exception: Exception | None = None):
|
||||||
logger.opt(colors=True, exception=exception).log(
|
logger.opt(colors=True, exception=exception).log(
|
||||||
level, f"<m>{escape_tag(logger_name)}</m> | {message}"
|
level, f"<m>{escape_tag(logger_name)}</m> | {message}"
|
||||||
)
|
)
|
||||||
|
|||||||
+5
-6
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "nonebot2"
|
name = "nonebot2"
|
||||||
version = "2.4.4"
|
version = "2.5.0"
|
||||||
description = "An asynchronous python bot framework."
|
description = "An asynchronous python bot framework."
|
||||||
authors = [{ name = "yanyongyu", email = "yyy@nonebot.dev" }]
|
authors = [{ name = "yanyongyu", email = "yyy@nonebot.dev" }]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
@@ -13,7 +13,7 @@ classifiers = [
|
|||||||
"Operating System :: OS Independent",
|
"Operating System :: OS Independent",
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
]
|
]
|
||||||
requires-python = ">=3.9, <4.0"
|
requires-python = ">=3.10, <4.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"yarl >=1.7.2, <2.0.0",
|
"yarl >=1.7.2, <2.0.0",
|
||||||
"anyio >=4.4.0, <5.0.0",
|
"anyio >=4.4.0, <5.0.0",
|
||||||
@@ -44,7 +44,7 @@ all = [
|
|||||||
dev = [
|
dev = [
|
||||||
{ include-group = "test" },
|
{ include-group = "test" },
|
||||||
{ include-group = "docs" },
|
{ include-group = "docs" },
|
||||||
"ruff >=0.14.0, <0.15.0",
|
"ruff >=0.15.0, <0.16.0",
|
||||||
"nonemoji >=0.1.2, <0.2.0",
|
"nonemoji >=0.1.2, <0.2.0",
|
||||||
"pre-commit >=4.0.0, <5.0.0",
|
"pre-commit >=4.0.0, <5.0.0",
|
||||||
]
|
]
|
||||||
@@ -57,7 +57,7 @@ test = [
|
|||||||
"pytest-xdist >=3.0.2, <4.0.0",
|
"pytest-xdist >=3.0.2, <4.0.0",
|
||||||
"coverage-conditional-plugin >=0.9.0, <0.10.0",
|
"coverage-conditional-plugin >=0.9.0, <0.10.0",
|
||||||
]
|
]
|
||||||
docs = ["nb-autodoc >=1.0.0a5, <2.0.0"]
|
docs = ["nb-autodoc >=1.0.4, <2.0.0"]
|
||||||
pydantic-v1 = ["pydantic >=1.10.0, <2.0.0"]
|
pydantic-v1 = ["pydantic >=1.10.0, <2.0.0"]
|
||||||
pydantic-v2 = ["pydantic >=2.0.0, <3.0.0"]
|
pydantic-v2 = ["pydantic >=2.0.0, <3.0.0"]
|
||||||
|
|
||||||
@@ -83,7 +83,6 @@ filterwarnings = ["error", "ignore::DeprecationWarning"]
|
|||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
line-length = 88
|
line-length = 88
|
||||||
target-version = "py39"
|
|
||||||
|
|
||||||
[tool.ruff.format]
|
[tool.ruff.format]
|
||||||
line-ending = "lf"
|
line-ending = "lf"
|
||||||
@@ -126,7 +125,7 @@ mark-parentheses = false
|
|||||||
keep-runtime-typing = true
|
keep-runtime-typing = true
|
||||||
|
|
||||||
[tool.pyright]
|
[tool.pyright]
|
||||||
pythonVersion = "3.9"
|
pythonVersion = "3.10"
|
||||||
pythonPlatform = "All"
|
pythonPlatform = "All"
|
||||||
defineConstant = { PYDANTIC_V2 = true }
|
defineConstant = { PYDANTIC_V2 = true }
|
||||||
executionEnvironments = [
|
executionEnvironments = [
|
||||||
|
|||||||
+5
-4
@@ -1,10 +1,11 @@
|
|||||||
from collections.abc import Generator
|
from collections.abc import Callable, Generator
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
from typing import TYPE_CHECKING, Callable, TypeVar
|
from types import EllipsisType
|
||||||
|
from typing import TYPE_CHECKING, TypeVar
|
||||||
from typing_extensions import ParamSpec
|
from typing_extensions import ParamSpec
|
||||||
|
|
||||||
from nonebug import NONEBOT_INIT_KWARGS
|
from nonebug import NONEBOT_INIT_KWARGS
|
||||||
@@ -50,12 +51,12 @@ def anyio_backend(request: pytest.FixtureRequest):
|
|||||||
|
|
||||||
|
|
||||||
def run_once(func: Callable[P, R]) -> Callable[P, R]:
|
def run_once(func: Callable[P, R]) -> Callable[P, R]:
|
||||||
result = ...
|
result: R | EllipsisType = ...
|
||||||
|
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
def _wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
def _wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
||||||
nonlocal result
|
nonlocal result
|
||||||
if result is not Ellipsis:
|
if result is not ...:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
result = func(*args, **kwargs)
|
result = func(*args, **kwargs)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
import socket
|
import socket
|
||||||
from typing import TypeVar, Union
|
from typing import TypeVar
|
||||||
|
|
||||||
from werkzeug import Request, Response
|
from werkzeug import Request, Response
|
||||||
from werkzeug.datastructures import MultiDict
|
from werkzeug.datastructures import MultiDict
|
||||||
@@ -36,7 +36,7 @@ def json_safe(string, content_type="application/octet-stream") -> str:
|
|||||||
).decode("utf-8")
|
).decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
def flattern(d: "MultiDict[K, V]") -> dict[K, Union[V, list[V]]]:
|
def flattern(d: "MultiDict[K, V]") -> dict[K, V | list[V]]:
|
||||||
return {k: v[0] if len(v) == 1 else v for k, v in d.to_dict(flat=False).items()}
|
return {k: v[0] if len(v) == 1 else v for k, v in d.to_dict(flat=False).items()}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from typing import TypeVar, Union
|
from typing import TypeVar
|
||||||
|
|
||||||
from nonebot.adapters import Bot
|
from nonebot.adapters import Bot
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ async def sub_bot(b: FooBot) -> FooBot:
|
|||||||
class BarBot(Bot): ...
|
class BarBot(Bot): ...
|
||||||
|
|
||||||
|
|
||||||
async def union_bot(b: Union[FooBot, BarBot]) -> Union[FooBot, BarBot]:
|
async def union_bot(b: FooBot | BarBot) -> FooBot | BarBot:
|
||||||
return b
|
return b
|
||||||
|
|
||||||
|
|
||||||
@@ -46,4 +46,4 @@ async def generic_bot_none(b: CB) -> CB:
|
|||||||
return b
|
return b
|
||||||
|
|
||||||
|
|
||||||
async def not_bot(b: Union[int, Bot]): ...
|
async def not_bot(b: int | Bot): ...
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from typing import TypeVar, Union
|
from typing import TypeVar
|
||||||
|
|
||||||
from nonebot.adapters import Event, Message
|
from nonebot.adapters import Event, Message
|
||||||
from nonebot.params import EventMessage, EventPlainText, EventToMe, EventType
|
from nonebot.params import EventMessage, EventPlainText, EventToMe, EventType
|
||||||
@@ -29,7 +29,7 @@ async def sub_event(e: FooEvent) -> FooEvent:
|
|||||||
class BarEvent(Event): ...
|
class BarEvent(Event): ...
|
||||||
|
|
||||||
|
|
||||||
async def union_event(e: Union[FooEvent, BarEvent]) -> Union[FooEvent, BarEvent]:
|
async def union_event(e: FooEvent | BarEvent) -> FooEvent | BarEvent:
|
||||||
return e
|
return e
|
||||||
|
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ async def generic_event_none(e: CE) -> CE:
|
|||||||
return e
|
return e
|
||||||
|
|
||||||
|
|
||||||
async def not_event(e: Union[int, Event]): ...
|
async def not_event(e: int | Event): ...
|
||||||
|
|
||||||
|
|
||||||
async def event_type(t: str = EventType()) -> str:
|
async def event_type(t: str = EventType()) -> str:
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
from typing import Union
|
async def exc(e: Exception, x: ValueError | TypeError) -> Exception:
|
||||||
|
|
||||||
|
|
||||||
async def exc(e: Exception, x: Union[ValueError, TypeError]) -> Exception:
|
|
||||||
assert e == x
|
assert e == x
|
||||||
return e
|
return e
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from typing import Any, TypeVar, Union
|
from typing import Any, TypeVar
|
||||||
|
|
||||||
from nonebot.adapters import Event
|
from nonebot.adapters import Event
|
||||||
from nonebot.matcher import Matcher
|
from nonebot.matcher import Matcher
|
||||||
@@ -36,8 +36,8 @@ class BarMatcher(Matcher): ...
|
|||||||
|
|
||||||
|
|
||||||
async def union_matcher(
|
async def union_matcher(
|
||||||
m: Union[FooMatcher, BarMatcher],
|
m: FooMatcher | BarMatcher,
|
||||||
) -> Union[FooMatcher, BarMatcher]:
|
) -> FooMatcher | BarMatcher:
|
||||||
return m
|
return m
|
||||||
|
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ async def generic_matcher_none(m: CM) -> CM:
|
|||||||
return m
|
return m
|
||||||
|
|
||||||
|
|
||||||
async def not_matcher(m: Union[int, Matcher]): ...
|
async def not_matcher(m: int | Matcher): ...
|
||||||
|
|
||||||
|
|
||||||
async def receive(e: Event = Received("test")) -> Event:
|
async def receive(e: Event = Received("test")) -> Event:
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
from typing import Optional
|
|
||||||
|
|
||||||
from nonebot.adapters import Bot, Event, Message
|
from nonebot.adapters import Bot, Event, Message
|
||||||
from nonebot.matcher import Matcher
|
from nonebot.matcher import Matcher
|
||||||
from nonebot.params import Arg, Depends
|
from nonebot.params import Arg, Depends
|
||||||
@@ -12,11 +10,11 @@ def dependency():
|
|||||||
|
|
||||||
async def complex_priority(
|
async def complex_priority(
|
||||||
sub: int = Depends(dependency),
|
sub: int = Depends(dependency),
|
||||||
bot: Optional[Bot] = None,
|
bot: Bot | None = None,
|
||||||
event: Optional[Event] = None,
|
event: Event | None = None,
|
||||||
state: T_State = {},
|
state: T_State = {},
|
||||||
matcher: Optional[Matcher] = None,
|
matcher: Matcher | None = None,
|
||||||
arg: Message = Arg(),
|
arg: Message = Arg(),
|
||||||
exception: Optional[Exception] = None,
|
exception: Exception | None = None,
|
||||||
default: int = 1,
|
default: int = 1,
|
||||||
): ...
|
): ...
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from nonebug import App
|
from nonebug import App
|
||||||
import pytest
|
import pytest
|
||||||
@@ -19,8 +18,8 @@ from utils import FakeAdapter
|
|||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_adapter_connect(app: App, driver: Driver):
|
async def test_adapter_connect(app: App, driver: Driver):
|
||||||
last_connect_bot: Optional[Bot] = None
|
last_connect_bot: Bot | None = None
|
||||||
last_disconnect_bot: Optional[Bot] = None
|
last_disconnect_bot: Bot | None = None
|
||||||
|
|
||||||
def _fake_bot_connect(bot: Bot):
|
def _fake_bot_connect(bot: Bot):
|
||||||
nonlocal last_connect_bot
|
nonlocal last_connect_bot
|
||||||
@@ -75,8 +74,8 @@ async def test_adapter_connect(app: App, driver: Driver):
|
|||||||
indirect=True,
|
indirect=True,
|
||||||
)
|
)
|
||||||
def test_adapter_server(driver: Driver):
|
def test_adapter_server(driver: Driver):
|
||||||
last_http_setup: Optional[HTTPServerSetup] = None
|
last_http_setup: HTTPServerSetup | None = None
|
||||||
last_ws_setup: Optional[WebSocketServerSetup] = None
|
last_ws_setup: WebSocketServerSetup | None = None
|
||||||
|
|
||||||
def _fake_setup_http_server(setup: HTTPServerSetup):
|
def _fake_setup_http_server(setup: HTTPServerSetup):
|
||||||
nonlocal last_http_setup
|
nonlocal last_http_setup
|
||||||
@@ -142,7 +141,7 @@ def test_adapter_server(driver: Driver):
|
|||||||
indirect=True,
|
indirect=True,
|
||||||
)
|
)
|
||||||
async def test_adapter_http_client(driver: Driver):
|
async def test_adapter_http_client(driver: Driver):
|
||||||
last_request: Optional[Request] = None
|
last_request: Request | None = None
|
||||||
|
|
||||||
async def _fake_request(request: Request):
|
async def _fake_request(request: Request):
|
||||||
nonlocal last_request
|
nonlocal last_request
|
||||||
@@ -190,7 +189,7 @@ async def test_adapter_http_client(driver: Driver):
|
|||||||
)
|
)
|
||||||
async def test_adapter_websocket_client(driver: Driver):
|
async def test_adapter_websocket_client(driver: Driver):
|
||||||
_fake_ws = object()
|
_fake_ws = object()
|
||||||
_last_request: Optional[Request] = None
|
_last_request: Request | None = None
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def _fake_websocket(setup: Request):
|
async def _fake_websocket(setup: Request):
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from typing import Any, Optional
|
from typing import Any
|
||||||
|
|
||||||
import anyio
|
import anyio
|
||||||
from nonebug import App
|
from nonebug import App
|
||||||
@@ -123,7 +123,7 @@ async def test_bot_called_api_hook_simple(app: App):
|
|||||||
|
|
||||||
async def called_api_hook(
|
async def called_api_hook(
|
||||||
bot: Bot,
|
bot: Bot,
|
||||||
exception: Optional[Exception],
|
exception: Exception | None,
|
||||||
api: str,
|
api: str,
|
||||||
data: dict[str, Any],
|
data: dict[str, Any],
|
||||||
result: Any,
|
result: Any,
|
||||||
@@ -155,7 +155,7 @@ async def test_bot_called_api_hook_mock(app: App):
|
|||||||
|
|
||||||
async def called_api_hook(
|
async def called_api_hook(
|
||||||
bot: Bot,
|
bot: Bot,
|
||||||
exception: Optional[Exception],
|
exception: Exception | None,
|
||||||
api: str,
|
api: str,
|
||||||
data: dict[str, Any],
|
data: dict[str, Any],
|
||||||
result: Any,
|
result: Any,
|
||||||
@@ -201,7 +201,7 @@ async def test_bot_called_api_hook_multi_mock(app: App):
|
|||||||
|
|
||||||
async def called_api_hook1(
|
async def called_api_hook1(
|
||||||
bot: Bot,
|
bot: Bot,
|
||||||
exception: Optional[Exception],
|
exception: Exception | None,
|
||||||
api: str,
|
api: str,
|
||||||
data: dict[str, Any],
|
data: dict[str, Any],
|
||||||
result: Any,
|
result: Any,
|
||||||
@@ -214,7 +214,7 @@ async def test_bot_called_api_hook_multi_mock(app: App):
|
|||||||
|
|
||||||
async def called_api_hook2(
|
async def called_api_hook2(
|
||||||
bot: Bot,
|
bot: Bot,
|
||||||
exception: Optional[Exception],
|
exception: Exception | None,
|
||||||
api: str,
|
api: str,
|
||||||
data: dict[str, Any],
|
data: dict[str, Any],
|
||||||
result: Any,
|
result: Any,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import sys
|
import sys
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from nonebug import App
|
from nonebug import App
|
||||||
import pytest
|
import pytest
|
||||||
@@ -326,7 +325,7 @@ async def test_run_postprocessor(app: App, monkeypatch: pytest.MonkeyPatch):
|
|||||||
event: Event,
|
event: Event,
|
||||||
state: T_State,
|
state: T_State,
|
||||||
matcher: Matcher,
|
matcher: Matcher,
|
||||||
exception: Optional[Exception],
|
exception: Exception | None,
|
||||||
sub: int = Depends(_dependency),
|
sub: int = Depends(_dependency),
|
||||||
default: int = 1,
|
default: int = 1,
|
||||||
):
|
):
|
||||||
|
|||||||
+33
-4
@@ -1,5 +1,5 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Annotated, Any, Optional
|
from typing import Annotated, Any
|
||||||
|
|
||||||
from pydantic import BaseModel, ValidationError
|
from pydantic import BaseModel, ValidationError
|
||||||
import pytest
|
import pytest
|
||||||
@@ -73,12 +73,41 @@ def test_type_adapter():
|
|||||||
|
|
||||||
|
|
||||||
def test_model_dump():
|
def test_model_dump():
|
||||||
|
class NestedModel(BaseModel):
|
||||||
|
hidden: int
|
||||||
|
shown: int
|
||||||
|
|
||||||
class TestModel(BaseModel):
|
class TestModel(BaseModel):
|
||||||
test1: int
|
test1: int
|
||||||
test2: int
|
test2: int
|
||||||
|
nested: NestedModel
|
||||||
|
items: list[NestedModel]
|
||||||
|
|
||||||
assert model_dump(TestModel(test1=1, test2=2), include={"test1"}) == {"test1": 1}
|
model = TestModel(
|
||||||
assert model_dump(TestModel(test1=1, test2=2), exclude={"test1"}) == {"test2": 2}
|
test1=1,
|
||||||
|
test2=2,
|
||||||
|
nested=NestedModel(hidden=3, shown=4),
|
||||||
|
items=[NestedModel(hidden=5, shown=6)],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert model_dump(model, include={"test1"}) == {"test1": 1}
|
||||||
|
assert model_dump(model, exclude={"test1"}) == {
|
||||||
|
"test2": 2,
|
||||||
|
"nested": {"hidden": 3, "shown": 4},
|
||||||
|
"items": [{"hidden": 5, "shown": 6}],
|
||||||
|
}
|
||||||
|
assert model_dump(model, exclude={"nested": {"hidden"}}) == {
|
||||||
|
"test1": 1,
|
||||||
|
"test2": 2,
|
||||||
|
"nested": {"shown": 4},
|
||||||
|
"items": [{"hidden": 5, "shown": 6}],
|
||||||
|
}
|
||||||
|
assert model_dump(model, exclude={"items": {"__all__": {"hidden"}}}) == {
|
||||||
|
"test1": 1,
|
||||||
|
"test2": 2,
|
||||||
|
"nested": {"hidden": 3, "shown": 4},
|
||||||
|
"items": [{"shown": 6}],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_model_validator():
|
def test_model_validator():
|
||||||
@@ -144,7 +173,7 @@ def test_validate_json():
|
|||||||
test3: bool
|
test3: bool
|
||||||
test4: dict
|
test4: dict
|
||||||
test5: list
|
test5: list
|
||||||
test6: Optional[int]
|
test6: int | None
|
||||||
|
|
||||||
assert type_validate_json(
|
assert type_validate_json(
|
||||||
TestModel,
|
TestModel,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from typing import TYPE_CHECKING, Optional, Union
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
import pytest
|
import pytest
|
||||||
@@ -16,8 +16,8 @@ class Simple(BaseModel):
|
|||||||
|
|
||||||
class Example(BaseSettings):
|
class Example(BaseSettings):
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
_env_file: Optional[DOTENV_TYPE] = ".env", ".env.example"
|
_env_file: DOTENV_TYPE | None = ".env", ".env.example"
|
||||||
_env_nested_delimiter: Optional[str] = "__"
|
_env_nested_delimiter: str | None = "__"
|
||||||
|
|
||||||
if PYDANTIC_V2:
|
if PYDANTIC_V2:
|
||||||
model_config = SettingsConfig(
|
model_config = SettingsConfig(
|
||||||
@@ -32,10 +32,10 @@ class Example(BaseSettings):
|
|||||||
env_nested_delimiter = "__"
|
env_nested_delimiter = "__"
|
||||||
|
|
||||||
simple: str = ""
|
simple: str = ""
|
||||||
int_str: Union[int, str] = LegacyUnionField(default="")
|
int_str: int | str = LegacyUnionField(default="")
|
||||||
complex: list[int] = Field(default=[1])
|
complex: list[int] = Field(default=[1])
|
||||||
complex_none: Optional[list[int]] = None
|
complex_none: list[int] | None = None
|
||||||
complex_union: Union[int, list[int]] = 1
|
complex_union: int | list[int] = 1
|
||||||
nested: Simple = Simple()
|
nested: Simple = Simple()
|
||||||
nested_inner: Simple = Simple()
|
nested_inner: Simple = Simple()
|
||||||
aliased_simple: str = Field(default="", alias="alias_simple")
|
aliased_simple: str = Field(default="", alias="alias_simple")
|
||||||
|
|||||||
+339
-2
@@ -1,7 +1,8 @@
|
|||||||
from http.cookies import SimpleCookie
|
from http.cookies import SimpleCookie
|
||||||
import json
|
import json
|
||||||
from typing import Any, Optional
|
from typing import Any
|
||||||
|
|
||||||
|
from aiohttp import ClientSession, ClientWebSocketResponse, WSMessage, WSMsgType
|
||||||
import anyio
|
import anyio
|
||||||
from nonebug import App
|
from nonebug import App
|
||||||
import pytest
|
import pytest
|
||||||
@@ -21,8 +22,11 @@ from nonebot.drivers import (
|
|||||||
WebSocketClientMixin,
|
WebSocketClientMixin,
|
||||||
WebSocketServerSetup,
|
WebSocketServerSetup,
|
||||||
)
|
)
|
||||||
|
from nonebot.drivers.aiohttp import Session as AiohttpSession
|
||||||
|
from nonebot.drivers.aiohttp import WebSocket as AiohttpWebSocket
|
||||||
from nonebot.exception import WebSocketClosed
|
from nonebot.exception import WebSocketClosed
|
||||||
from nonebot.params import Depends
|
from nonebot.params import Depends
|
||||||
|
from nonebot.utils import UNSET
|
||||||
from utils import FakeAdapter
|
from utils import FakeAdapter
|
||||||
|
|
||||||
|
|
||||||
@@ -171,7 +175,7 @@ async def test_websocket_server(app: App, driver: Driver):
|
|||||||
async def test_cross_context(app: App, driver: Driver):
|
async def test_cross_context(app: App, driver: Driver):
|
||||||
assert isinstance(driver, ASGIMixin)
|
assert isinstance(driver, ASGIMixin)
|
||||||
|
|
||||||
ws: Optional[WebSocket] = None
|
ws: WebSocket | None = None
|
||||||
ws_ready = anyio.Event()
|
ws_ready = anyio.Event()
|
||||||
ws_should_close = anyio.Event()
|
ws_should_close = anyio.Event()
|
||||||
|
|
||||||
@@ -594,6 +598,46 @@ async def test_http_client_session(driver: Driver, server_url: URL):
|
|||||||
await anyio.sleep(1)
|
await anyio.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_aiohttp_stream_request_skip_empty_chunk() -> None:
|
||||||
|
class _FakeContent:
|
||||||
|
async def iter_chunked(self, _: int):
|
||||||
|
for chunk in (b"ab", b"", b"cd", b"e"):
|
||||||
|
yield chunk
|
||||||
|
|
||||||
|
class _FakeResponse:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.status = 200
|
||||||
|
self.headers = {"x-test": "1"}
|
||||||
|
self.content = _FakeContent()
|
||||||
|
|
||||||
|
class _FakeRequestContext:
|
||||||
|
async def __aenter__(self) -> _FakeResponse:
|
||||||
|
return _FakeResponse()
|
||||||
|
|
||||||
|
async def __aexit__(self, *args: object) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
class _FakeClient:
|
||||||
|
def request(self, *args: object, **kwargs: object) -> _FakeRequestContext:
|
||||||
|
return _FakeRequestContext()
|
||||||
|
|
||||||
|
session = AiohttpSession()
|
||||||
|
session._client = _FakeClient() # type: ignore[assignment]
|
||||||
|
|
||||||
|
chunks = []
|
||||||
|
async for resp in session.stream_request(
|
||||||
|
Request("GET", "https://example.com"), chunk_size=2
|
||||||
|
):
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.content
|
||||||
|
chunks.append(resp.content)
|
||||||
|
|
||||||
|
assert chunks == [b"ab", b"cd", b"e"]
|
||||||
|
assert b"".join(chunks) == b"abcde"
|
||||||
|
assert all(len(chunk) == 2 for chunk in chunks[:-1])
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"driver",
|
"driver",
|
||||||
@@ -627,6 +671,299 @@ async def test_websocket_client(driver: Driver, server_url: URL):
|
|||||||
await anyio.sleep(1)
|
await anyio.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("msg_type"),
|
||||||
|
[
|
||||||
|
pytest.param("CLOSE", id="aiohttp-close"),
|
||||||
|
pytest.param("CLOSING", id="aiohttp-closing"),
|
||||||
|
pytest.param("CLOSED", id="aiohttp-closed"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_aiohttp_websocket_close_frame(msg_type: str) -> None:
|
||||||
|
class DummyWS(ClientWebSocketResponse):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def close_code(self) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def closed(self) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def receive(self, timeout: float | None = None) -> WSMessage: # noqa: ASYNC109
|
||||||
|
return WSMessage(type=WSMsgType[msg_type], data=None, extra=None)
|
||||||
|
|
||||||
|
async with ClientSession() as session:
|
||||||
|
ws = AiohttpWebSocket(
|
||||||
|
request=Request("GET", "ws://example.com"),
|
||||||
|
session=session,
|
||||||
|
websocket=DummyWS(),
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(WebSocketClosed, match=r"code=1006"):
|
||||||
|
await ws.receive()
|
||||||
|
|
||||||
|
|
||||||
|
def test_timeout_unset_vs_none():
|
||||||
|
# default: all fields are UNSET
|
||||||
|
t = Timeout()
|
||||||
|
assert t.total is UNSET
|
||||||
|
assert t.connect is UNSET
|
||||||
|
assert t.read is UNSET
|
||||||
|
assert t.close is UNSET
|
||||||
|
|
||||||
|
# explicitly set to None
|
||||||
|
t = Timeout(close=None)
|
||||||
|
assert t.close is None
|
||||||
|
assert t.close is not UNSET
|
||||||
|
|
||||||
|
# explicitly set to a value
|
||||||
|
t = Timeout(total=5.0, close=None)
|
||||||
|
assert t.total == 5.0
|
||||||
|
assert t.close is None
|
||||||
|
assert t.connect is UNSET
|
||||||
|
assert t.read is UNSET
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"driver",
|
||||||
|
[
|
||||||
|
pytest.param("nonebot.drivers.httpx:Driver", id="httpx"),
|
||||||
|
pytest.param("nonebot.drivers.aiohttp:Driver", id="aiohttp"),
|
||||||
|
],
|
||||||
|
indirect=True,
|
||||||
|
)
|
||||||
|
async def test_http_client_timeout(driver: Driver, server_url: URL):
|
||||||
|
"""HTTP requests work with fully unset, partial, and None timeout fields."""
|
||||||
|
assert isinstance(driver, HTTPClientMixin)
|
||||||
|
|
||||||
|
# timeout not set, default timeout should apply
|
||||||
|
request = Request("POST", server_url, content="test")
|
||||||
|
response = await driver.request(request)
|
||||||
|
assert response.status_code == 200
|
||||||
|
async for resp in driver.stream_request(request, chunk_size=1024):
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
# timeout is float or none
|
||||||
|
request = Request("POST", server_url, content="test", timeout=10.0)
|
||||||
|
response = await driver.request(request)
|
||||||
|
assert response.status_code == 200
|
||||||
|
async for resp in driver.stream_request(request, chunk_size=1024):
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
# all fields unset, default timeout should apply
|
||||||
|
request = Request("POST", server_url, content="test", timeout=Timeout())
|
||||||
|
response = await driver.request(request)
|
||||||
|
assert response.status_code == 200
|
||||||
|
async for resp in driver.stream_request(request, chunk_size=1024):
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
# only total set
|
||||||
|
request = Request("POST", server_url, content="test", timeout=Timeout(total=10.0))
|
||||||
|
response = await driver.request(request)
|
||||||
|
assert response.status_code == 200
|
||||||
|
async for resp in driver.stream_request(request, chunk_size=1024):
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
# explicit None (no timeout)
|
||||||
|
request = Request(
|
||||||
|
"POST",
|
||||||
|
server_url,
|
||||||
|
content="test",
|
||||||
|
timeout=Timeout(total=None, connect=None, read=None),
|
||||||
|
)
|
||||||
|
response = await driver.request(request)
|
||||||
|
assert response.status_code == 200
|
||||||
|
async for resp in driver.stream_request(request, chunk_size=1024):
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
# session with timeout not set
|
||||||
|
session = driver.get_session()
|
||||||
|
async with session:
|
||||||
|
request = Request("POST", server_url, content="test")
|
||||||
|
response = await session.request(request)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
# session with float or none timeout
|
||||||
|
session = driver.get_session(timeout=10.0)
|
||||||
|
async with session:
|
||||||
|
request = Request("POST", server_url, content="test")
|
||||||
|
response = await session.request(request)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
# session with fully unset timeout
|
||||||
|
session = driver.get_session(timeout=Timeout())
|
||||||
|
async with session:
|
||||||
|
request = Request("POST", server_url, content="test")
|
||||||
|
response = await session.request(request)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
# session with timeout
|
||||||
|
session = driver.get_session(timeout=Timeout(total=10.0, connect=5.0, read=5.0))
|
||||||
|
async with session:
|
||||||
|
request = Request("POST", server_url, content="test")
|
||||||
|
response = await session.request(request)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
# session with timeout override
|
||||||
|
session = driver.get_session(timeout=Timeout(total=10.0))
|
||||||
|
async with session:
|
||||||
|
request = Request(
|
||||||
|
"POST", server_url, content="test", timeout=Timeout(total=20.0)
|
||||||
|
)
|
||||||
|
response = await session.request(request)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
# session with timeout float override
|
||||||
|
session = driver.get_session(timeout=Timeout(total=10.0))
|
||||||
|
async with session:
|
||||||
|
request = Request("POST", server_url, content="test", timeout=20.0)
|
||||||
|
response = await session.request(request)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"driver",
|
||||||
|
[
|
||||||
|
pytest.param("nonebot.drivers.websockets:Driver", id="websockets"),
|
||||||
|
pytest.param("nonebot.drivers.aiohttp:Driver", id="aiohttp"),
|
||||||
|
],
|
||||||
|
indirect=True,
|
||||||
|
)
|
||||||
|
async def test_websocket_client_timeout(driver: Driver, server_url: URL):
|
||||||
|
"""WebSocket connections work with fully unset, partial, and None timeout fields."""
|
||||||
|
assert isinstance(driver, WebSocketClientMixin)
|
||||||
|
|
||||||
|
ws_url = server_url.with_scheme("ws")
|
||||||
|
|
||||||
|
# timeout not set, default timeout should apply
|
||||||
|
request = Request("GET", ws_url)
|
||||||
|
async with driver.websocket(request) as ws:
|
||||||
|
await ws.send("quit")
|
||||||
|
with pytest.raises(WebSocketClosed):
|
||||||
|
await ws.receive()
|
||||||
|
|
||||||
|
await anyio.sleep(1)
|
||||||
|
|
||||||
|
# timeout is float or none
|
||||||
|
request = Request("GET", ws_url, timeout=10.0)
|
||||||
|
async with driver.websocket(request) as ws:
|
||||||
|
await ws.send("quit")
|
||||||
|
with pytest.raises(WebSocketClosed):
|
||||||
|
await ws.receive()
|
||||||
|
|
||||||
|
await anyio.sleep(1)
|
||||||
|
|
||||||
|
# all fields unset, default timeout should apply
|
||||||
|
request = Request("GET", ws_url, timeout=Timeout())
|
||||||
|
async with driver.websocket(request) as ws:
|
||||||
|
await ws.send("quit")
|
||||||
|
with pytest.raises(WebSocketClosed):
|
||||||
|
await ws.receive()
|
||||||
|
|
||||||
|
await anyio.sleep(1)
|
||||||
|
|
||||||
|
# close explicitly set to None (no close timeout)
|
||||||
|
request = Request("GET", ws_url, timeout=Timeout(close=None))
|
||||||
|
async with driver.websocket(request) as ws:
|
||||||
|
await ws.send("quit")
|
||||||
|
with pytest.raises(WebSocketClosed):
|
||||||
|
await ws.receive()
|
||||||
|
|
||||||
|
await anyio.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"driver",
|
||||||
|
[
|
||||||
|
pytest.param("nonebot.drivers.websockets:Driver", id="websockets"),
|
||||||
|
pytest.param("nonebot.drivers.aiohttp:Driver", id="aiohttp"),
|
||||||
|
],
|
||||||
|
indirect=True,
|
||||||
|
)
|
||||||
|
async def test_websocket_client_ping_timeout(driver: Driver, server_url: URL):
|
||||||
|
"""WebSocket connections work with different ping_timeout settings."""
|
||||||
|
assert isinstance(driver, WebSocketClientMixin)
|
||||||
|
|
||||||
|
ws_url = server_url.with_scheme("ws")
|
||||||
|
|
||||||
|
# ping timeout not set (UNSET), falls back to DEFAULT_TIMEOUT.ping
|
||||||
|
request = Request("GET", ws_url, timeout=Timeout())
|
||||||
|
async with driver.websocket(request) as ws:
|
||||||
|
await ws.send("quit")
|
||||||
|
with pytest.raises(WebSocketClosed):
|
||||||
|
await ws.receive()
|
||||||
|
|
||||||
|
await anyio.sleep(1)
|
||||||
|
|
||||||
|
# ping timeout explicitly set to None (disable ping timeout)
|
||||||
|
request = Request("GET", ws_url, timeout=Timeout(ping=None))
|
||||||
|
async with driver.websocket(request) as ws:
|
||||||
|
await ws.send("quit")
|
||||||
|
with pytest.raises(WebSocketClosed):
|
||||||
|
await ws.receive()
|
||||||
|
|
||||||
|
await anyio.sleep(1)
|
||||||
|
|
||||||
|
# ping timeout set to a float value
|
||||||
|
request = Request("GET", ws_url, timeout=Timeout(ping=20.0))
|
||||||
|
async with driver.websocket(request) as ws:
|
||||||
|
await ws.send("quit")
|
||||||
|
with pytest.raises(WebSocketClosed):
|
||||||
|
await ws.receive()
|
||||||
|
|
||||||
|
await anyio.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"driver",
|
||||||
|
[
|
||||||
|
pytest.param("nonebot.drivers.websockets:Driver", id="websockets"),
|
||||||
|
pytest.param("nonebot.drivers.aiohttp:Driver", id="aiohttp"),
|
||||||
|
],
|
||||||
|
indirect=True,
|
||||||
|
)
|
||||||
|
async def test_websocket_client_ping_interval(driver: Driver, server_url: URL):
|
||||||
|
"""WebSocket connections work with different ping_interval settings."""
|
||||||
|
assert isinstance(driver, WebSocketClientMixin)
|
||||||
|
|
||||||
|
ws_url = server_url.with_scheme("ws")
|
||||||
|
|
||||||
|
# ping_interval not set (UNSET), default behavior
|
||||||
|
request = Request("GET", ws_url)
|
||||||
|
async with driver.websocket(request) as ws:
|
||||||
|
await ws.send("quit")
|
||||||
|
with pytest.raises(WebSocketClosed):
|
||||||
|
await ws.receive()
|
||||||
|
|
||||||
|
await anyio.sleep(1)
|
||||||
|
|
||||||
|
# ping_interval explicitly set to None (disable ping)
|
||||||
|
request = Request("GET", ws_url, ping_interval=None)
|
||||||
|
async with driver.websocket(request) as ws:
|
||||||
|
await ws.send("quit")
|
||||||
|
with pytest.raises(WebSocketClosed):
|
||||||
|
await ws.receive()
|
||||||
|
|
||||||
|
await anyio.sleep(1)
|
||||||
|
|
||||||
|
# ping_interval set to a float value
|
||||||
|
request = Request("GET", ws_url, ping_interval=20.0)
|
||||||
|
async with driver.websocket(request) as ws:
|
||||||
|
await ws.send("quit")
|
||||||
|
with pytest.raises(WebSocketClosed):
|
||||||
|
await ws.receive()
|
||||||
|
|
||||||
|
await anyio.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("driver", "driver_type"),
|
("driver", "driver_type"),
|
||||||
[
|
[
|
||||||
|
|||||||
+2
-1
@@ -43,7 +43,8 @@ UNKNOWN_PARAM = "Unknown parameter"
|
|||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
@pytest.mark.xfail(
|
@pytest.mark.xfail(
|
||||||
((3, 13) <= sys.version_info < (3, 13, 8)) or (3, 14) <= sys.version_info,
|
((3, 13) <= sys.version_info < (3, 13, 8))
|
||||||
|
or ((3, 14) <= sys.version_info < (3, 14, 1)),
|
||||||
reason="CPython Bug, see python/cpython#137317, python/cpython#137862",
|
reason="CPython Bug, see python/cpython#137317, python/cpython#137862",
|
||||||
)
|
)
|
||||||
async def test_depend(app: App):
|
async def test_depend(app: App):
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
from typing import Optional
|
|
||||||
|
|
||||||
from nonebug import App
|
from nonebug import App
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -138,7 +136,7 @@ async def test_superuser(app: App, type: str, user_id: str, expected: bool):
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_user(
|
async def test_user(
|
||||||
app: App, session_ids: tuple[str, ...], session_id: Optional[str], expected: bool
|
app: App, session_ids: tuple[str, ...], session_id: str | None, expected: bool
|
||||||
):
|
):
|
||||||
dependent = next(iter(USER(*session_ids).checkers))
|
dependent = next(iter(USER(*session_ids).checkers))
|
||||||
checker = dependent.call
|
checker = dependent.call
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
from collections.abc import Callable
|
||||||
from dataclasses import asdict
|
from dataclasses import asdict
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import sys
|
import sys
|
||||||
from typing import Callable, TypeVar
|
from typing import TypeVar
|
||||||
from typing_extensions import ParamSpec
|
from typing_extensions import ParamSpec
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from typing import Callable, Optional
|
from collections.abc import Callable
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ from nonebot.typing import T_RuleChecker
|
|||||||
)
|
)
|
||||||
def test_on(
|
def test_on(
|
||||||
matcher_name: str,
|
matcher_name: str,
|
||||||
pre_rule_factory: Optional[Callable[[type[Event]], T_RuleChecker]],
|
pre_rule_factory: Callable[[type[Event]], T_RuleChecker] | None,
|
||||||
has_permission: bool,
|
has_permission: bool,
|
||||||
):
|
):
|
||||||
import plugins.plugin.matchers as module
|
import plugins.plugin.matchers as module
|
||||||
|
|||||||
+13
-14
@@ -1,6 +1,5 @@
|
|||||||
import re
|
import re
|
||||||
from re import Match
|
from re import Match
|
||||||
from typing import Optional, Union
|
|
||||||
|
|
||||||
from nonebug import App
|
from nonebug import App
|
||||||
import pytest
|
import pytest
|
||||||
@@ -163,10 +162,10 @@ async def test_trie(app: App):
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_startswith(
|
async def test_startswith(
|
||||||
msg: Union[str, tuple[str, ...]],
|
msg: str | tuple[str, ...],
|
||||||
ignorecase: bool,
|
ignorecase: bool,
|
||||||
type: str,
|
type: str,
|
||||||
text: Optional[str],
|
text: str | None,
|
||||||
expected: bool,
|
expected: bool,
|
||||||
):
|
):
|
||||||
test_startswith = startswith(msg, ignorecase)
|
test_startswith = startswith(msg, ignorecase)
|
||||||
@@ -203,10 +202,10 @@ async def test_startswith(
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_endswith(
|
async def test_endswith(
|
||||||
msg: Union[str, tuple[str, ...]],
|
msg: str | tuple[str, ...],
|
||||||
ignorecase: bool,
|
ignorecase: bool,
|
||||||
type: str,
|
type: str,
|
||||||
text: Optional[str],
|
text: str | None,
|
||||||
expected: bool,
|
expected: bool,
|
||||||
):
|
):
|
||||||
test_endswith = endswith(msg, ignorecase)
|
test_endswith = endswith(msg, ignorecase)
|
||||||
@@ -243,10 +242,10 @@ async def test_endswith(
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_fullmatch(
|
async def test_fullmatch(
|
||||||
msg: Union[str, tuple[str, ...]],
|
msg: str | tuple[str, ...],
|
||||||
ignorecase: bool,
|
ignorecase: bool,
|
||||||
type: str,
|
type: str,
|
||||||
text: Optional[str],
|
text: str | None,
|
||||||
expected: bool,
|
expected: bool,
|
||||||
):
|
):
|
||||||
test_fullmatch = fullmatch(msg, ignorecase)
|
test_fullmatch = fullmatch(msg, ignorecase)
|
||||||
@@ -281,7 +280,7 @@ async def test_fullmatch(
|
|||||||
async def test_keyword(
|
async def test_keyword(
|
||||||
kws: tuple[str, ...],
|
kws: tuple[str, ...],
|
||||||
type: str,
|
type: str,
|
||||||
text: Optional[str],
|
text: str | None,
|
||||||
expected: bool,
|
expected: bool,
|
||||||
):
|
):
|
||||||
test_keyword = keyword(*kws)
|
test_keyword = keyword(*kws)
|
||||||
@@ -324,10 +323,10 @@ async def test_keyword(
|
|||||||
)
|
)
|
||||||
async def test_command(
|
async def test_command(
|
||||||
cmds: tuple[tuple[str, ...]],
|
cmds: tuple[tuple[str, ...]],
|
||||||
force_whitespace: Optional[Union[str, bool]],
|
force_whitespace: str | bool | None,
|
||||||
cmd: tuple[str, ...],
|
cmd: tuple[str, ...],
|
||||||
whitespace: Optional[str],
|
whitespace: str | None,
|
||||||
arg_text: Optional[str],
|
arg_text: str | None,
|
||||||
expected: bool,
|
expected: bool,
|
||||||
):
|
):
|
||||||
test_command = command(*cmds, force_whitespace=force_whitespace)
|
test_command = command(*cmds, force_whitespace=force_whitespace)
|
||||||
@@ -492,9 +491,9 @@ async def test_shell_command():
|
|||||||
async def test_regex(
|
async def test_regex(
|
||||||
pattern: str,
|
pattern: str,
|
||||||
type: str,
|
type: str,
|
||||||
text: Optional[str],
|
text: str | None,
|
||||||
expected: bool,
|
expected: bool,
|
||||||
matched: Optional[Match[str]],
|
matched: Match[str] | None,
|
||||||
):
|
):
|
||||||
test_regex = regex(pattern)
|
test_regex = regex(pattern)
|
||||||
dependent = next(iter(test_regex.checkers))
|
dependent = next(iter(test_regex.checkers))
|
||||||
@@ -507,7 +506,7 @@ async def test_regex(
|
|||||||
event = make_fake_event(_type=type, _message=message)()
|
event = make_fake_event(_type=type, _message=message)()
|
||||||
state = {}
|
state = {}
|
||||||
assert await dependent(event=event, state=state) == expected
|
assert await dependent(event=event, state=state) == expected
|
||||||
result: Optional[Match[str]] = state.get(REGEX_MATCHED)
|
result: Match[str] | None = state.get(REGEX_MATCHED)
|
||||||
if matched is None:
|
if matched is None:
|
||||||
assert result is None
|
assert result is None
|
||||||
else:
|
else:
|
||||||
|
|||||||
+35
-1
@@ -1,9 +1,19 @@
|
|||||||
|
import copy
|
||||||
import json
|
import json
|
||||||
|
import pickle
|
||||||
from typing import ClassVar, Dict, List, Literal, TypeVar, Union # noqa: UP035
|
from typing import ClassVar, Dict, List, Literal, TypeVar, Union # noqa: UP035
|
||||||
|
|
||||||
|
from pydantic import ValidationError
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from nonebot.compat import type_validate_python
|
||||||
from nonebot.utils import (
|
from nonebot.utils import (
|
||||||
|
UNSET,
|
||||||
DataclassEncoder,
|
DataclassEncoder,
|
||||||
|
Unset,
|
||||||
|
UnsetType,
|
||||||
escape_tag,
|
escape_tag,
|
||||||
|
exclude_unset,
|
||||||
generic_check_issubclass,
|
generic_check_issubclass,
|
||||||
is_async_gen_callable,
|
is_async_gen_callable,
|
||||||
is_coroutine_callable,
|
is_coroutine_callable,
|
||||||
@@ -12,6 +22,29 @@ from nonebot.utils import (
|
|||||||
from utils import FakeMessage, FakeMessageSegment
|
from utils import FakeMessage, FakeMessageSegment
|
||||||
|
|
||||||
|
|
||||||
|
def test_unset():
|
||||||
|
assert isinstance(UNSET, Unset)
|
||||||
|
assert bool(UNSET) is False
|
||||||
|
assert copy.copy(UNSET) is UNSET
|
||||||
|
assert copy.deepcopy(UNSET) is UNSET
|
||||||
|
assert pickle.loads(pickle.dumps(UNSET)) is UNSET
|
||||||
|
assert type_validate_python(UnsetType, UNSET) is UNSET
|
||||||
|
|
||||||
|
with pytest.raises(ValidationError):
|
||||||
|
type_validate_python(UnsetType, 123)
|
||||||
|
|
||||||
|
|
||||||
|
def test_exclude_unset():
|
||||||
|
assert exclude_unset({"a": 1, "b": UNSET, "c": None, "d": {"x": UNSET}}) == {
|
||||||
|
"a": 1,
|
||||||
|
"c": None,
|
||||||
|
"d": {},
|
||||||
|
}
|
||||||
|
assert exclude_unset([1, UNSET, None, {"x": UNSET}]) == [1, None, {}]
|
||||||
|
assert exclude_unset(UNSET) is None
|
||||||
|
assert exclude_unset(123) == 123
|
||||||
|
|
||||||
|
|
||||||
def test_loguru_escape_tag():
|
def test_loguru_escape_tag():
|
||||||
assert escape_tag("<red>red</red>") == r"\<red>red\</red>"
|
assert escape_tag("<red>red</red>") == r"\<red>red\</red>"
|
||||||
assert escape_tag("<fg #fff>white</fg #fff>") == r"\<fg #fff>white\</fg #fff>"
|
assert escape_tag("<fg #fff>white</fg #fff>") == r"\<fg #fff>white\</fg #fff>"
|
||||||
@@ -23,7 +56,8 @@ def test_loguru_escape_tag():
|
|||||||
def test_generic_check_issubclass():
|
def test_generic_check_issubclass():
|
||||||
assert generic_check_issubclass(int, (int, float))
|
assert generic_check_issubclass(int, (int, float))
|
||||||
assert not generic_check_issubclass(str, (int, float))
|
assert not generic_check_issubclass(str, (int, float))
|
||||||
assert generic_check_issubclass(Union[int, float, None], (int, float))
|
assert generic_check_issubclass(Union[int, float, None], (int, float)) # noqa: UP007
|
||||||
|
assert generic_check_issubclass(int | float | None, (int, float))
|
||||||
assert generic_check_issubclass(Literal[1, 2, 3], int)
|
assert generic_check_issubclass(Literal[1, 2, 3], int)
|
||||||
assert not generic_check_issubclass(Literal[1, 2, "3"], int)
|
assert not generic_check_issubclass(Literal[1, 2, "3"], int)
|
||||||
assert generic_check_issubclass(List[int], list) # noqa: UP006
|
assert generic_check_issubclass(List[int], list) # noqa: UP006
|
||||||
|
|||||||
+6
-9
@@ -1,5 +1,4 @@
|
|||||||
from collections.abc import Iterable, Mapping
|
from collections.abc import Iterable, Mapping
|
||||||
from typing import Optional, Union
|
|
||||||
from typing_extensions import override
|
from typing_extensions import override
|
||||||
|
|
||||||
from pydantic import create_model
|
from pydantic import create_model
|
||||||
@@ -60,7 +59,7 @@ class FakeMessage(Message[FakeMessageSegment]):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@override
|
@override
|
||||||
def _construct(msg: Union[str, Iterable[Mapping]]):
|
def _construct(msg: str | Iterable[Mapping]):
|
||||||
if isinstance(msg, str):
|
if isinstance(msg, str):
|
||||||
yield FakeMessageSegment.text(msg)
|
yield FakeMessageSegment.text(msg)
|
||||||
else:
|
else:
|
||||||
@@ -69,21 +68,19 @@ class FakeMessage(Message[FakeMessageSegment]):
|
|||||||
return
|
return
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def __add__(
|
def __add__(self, other: str | FakeMessageSegment | Iterable[FakeMessageSegment]):
|
||||||
self, other: Union[str, FakeMessageSegment, Iterable[FakeMessageSegment]]
|
|
||||||
):
|
|
||||||
other = escape_text(other) if isinstance(other, str) else other
|
other = escape_text(other) if isinstance(other, str) else other
|
||||||
return super().__add__(other)
|
return super().__add__(other)
|
||||||
|
|
||||||
|
|
||||||
def make_fake_event(
|
def make_fake_event(
|
||||||
_base: Optional[type[Event]] = None,
|
_base: type[Event] | None = None,
|
||||||
_type: str = "message",
|
_type: str = "message",
|
||||||
_name: str = "test",
|
_name: str = "test",
|
||||||
_description: str = "test",
|
_description: str = "test",
|
||||||
_user_id: Optional[str] = "test",
|
_user_id: str | None = "test",
|
||||||
_session_id: Optional[str] = "test",
|
_session_id: str | None = "test",
|
||||||
_message: Optional[Message] = None,
|
_message: Message | None = None,
|
||||||
_to_me: bool = True,
|
_to_me: bool = True,
|
||||||
**fields,
|
**fields,
|
||||||
) -> type[Event]:
|
) -> type[Event]:
|
||||||
|
|||||||
@@ -46,15 +46,15 @@ __plugin_meta__ = PluginMetadata(
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
我们可以看到,插件元数据 `PluginMetadata` 有三个基本属性:插件名称、插件描述、插件使用方法。除此之外,还有几个可选的属性(具体填写见[发布插件](../developer/plugin-publishing.mdx#填写插件元数据)章节):
|
我们可以看到,插件元数据 `PluginMetadata` 有三个基本属性:插件名称、插件描述、插件使用方法。除此之外,还有几个可选的属性(具体填写见[发布插件](../developer/plugin-publishing.mdx#插件元数据)章节):
|
||||||
|
|
||||||
- `type`:插件类别,发布插件必填。当前有效类别有:`library`(为其他插件编写提供功能),`application`(向机器人用户提供功能);
|
- `type`:插件类别,发布插件必填。当前有效类别有:`library`(为其他插件编写提供功能),`application`(向机器人用户提供功能);
|
||||||
- `homepage`:插件项目主页,发布插件必填;
|
- `homepage`:插件项目主页,发布插件必填;
|
||||||
- `config`:插件的[配置类](../appendices/config.mdx#插件配置),如无配置类可不填;
|
- `config`:插件的[配置类](../appendices/config.mdx#插件配置),发布插件时如有配置类则必须填写;
|
||||||
- `supported_adapters`:支持的适配器模块名集合,若插件可以保证兼容所有适配器(即仅使用基本适配器功能)可不填写;
|
- `supported_adapters`:支持的适配器模块名集合,若插件只使用了 NoneBot 基本抽象,应显式填写 `None`;
|
||||||
- `extra`:一个字典,可以用于存储任意信息。其他插件可以通过约定 `extra` 字典的键名来达成收集某些特殊信息的目的。
|
- `extra`:一个字典,可以用于存储任意信息。其他插件可以通过约定 `extra` 字典的键名来达成收集某些特殊信息的目的。
|
||||||
|
|
||||||
请注意,这里的**插件名称**是供使用者或机器人用户查看的,与插件索引名称无关。**插件索引名称(插件模块名称)**仅用于 NoneBot 插件系统**内部索引**。
|
请注意,这里的**插件名称**是供使用者或机器人用户查看的人类可读名称,与插件索引名称无关。**插件索引名称(插件模块名称)**仅用于 NoneBot 插件系统**内部索引**。
|
||||||
|
|
||||||
## 获取插件信息
|
## 获取插件信息
|
||||||
|
|
||||||
|
|||||||
@@ -219,6 +219,7 @@ jobs:
|
|||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
type=sha
|
type=sha
|
||||||
|
type=raw,value=latest
|
||||||
|
|
||||||
- name: Build and Publish
|
- name: Build and Publish
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v4
|
||||||
|
|||||||
@@ -8,7 +8,13 @@ description: 在商店发布自己的插件
|
|||||||
import Tabs from "@theme/Tabs";
|
import Tabs from "@theme/Tabs";
|
||||||
import TabItem from "@theme/TabItem";
|
import TabItem from "@theme/TabItem";
|
||||||
|
|
||||||
NoneBot 为开发者提供了分享插件给大家使用的方式——商店。本章节将会介绍如何将我们写好的插件发布到商店。
|
NoneBot 为开发者提供了分享插件的官方商店。本指南囊括**从创建项目到发布到 PyPI,最终提交商店审核**的全过程。
|
||||||
|
|
||||||
|
:::warning 警告
|
||||||
|
如果你的插件只是满足自用需求,则完全可以选择**不发布插件**。发布插件**不是**使用插件的必要条件。
|
||||||
|
|
||||||
|
NoneBot 社区对于插件有一定质量要求,对于不符合要求的插件,社区成员将会要求修改,直至符合要求后才能通过审核;如果长期未更新修改,社区将会关闭当前请求,之后如需发布请重新提交发布插件请求。相应的要求会在本章节以下部分介绍。
|
||||||
|
:::
|
||||||
|
|
||||||
:::tip 提示
|
:::tip 提示
|
||||||
本章节仅包含插件发布流程指导,插件开发请查阅前述章节。
|
本章节仅包含插件发布流程指导,插件开发请查阅前述章节。
|
||||||
@@ -30,7 +36,7 @@ NoneBot 插件使用下述命名规范:
|
|||||||
### 项目结构
|
### 项目结构
|
||||||
|
|
||||||
:::tip 提示
|
:::tip 提示
|
||||||
本段所述的项目结构仅作推荐,不做强制要求,保证实际可用性即可。
|
本段所述的项目结构仅作推荐,不做强制要求。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
插件程序本身结构可参考[插件结构](../tutorial/create-plugin.md#插件结构)一节,唯一区别在于,插件包可以直接处于项目顶层。
|
插件程序本身结构可参考[插件结构](../tutorial/create-plugin.md#插件结构)一节,唯一区别在于,插件包可以直接处于项目顶层。
|
||||||
@@ -46,98 +52,267 @@ NoneBot 插件使用下述命名规范:
|
|||||||
└── 📜 README.md
|
└── 📜 README.md
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 第三方项目模板
|
功能开发可以在 `__init__.py` 中进行或在包内部创建其他模块并在 `__init__.py` 中导入。
|
||||||
|
|
||||||
一些社区用户可能会分享自己制作的项目模板方便大家使用,如:
|
### 从项目模板开始
|
||||||
|
|
||||||
|
为降低新手门槛,我们提供三条清晰、完整、可复制的发布路径。
|
||||||
|
|
||||||
|
:::tip 提示
|
||||||
|
你只需选择一条与你习惯一致的路径,**完整跟随即可成功发布**。无需在不同工具间切换或猜测配置。
|
||||||
|
:::
|
||||||
|
|
||||||
|
NoneBot 生态目前有如下插件项目模板:
|
||||||
|
|
||||||
- [A-kirami/nonebot-plugin-template](https://github.com/A-kirami/nonebot-plugin-template)
|
|
||||||
- [RF-Tar-Railt/nonebot-plugin-template](https://github.com/RF-Tar-Railt/nonebot-plugin-template)
|
- [RF-Tar-Railt/nonebot-plugin-template](https://github.com/RF-Tar-Railt/nonebot-plugin-template)
|
||||||
|
|
||||||
|
此路径使用 **PDM** 项目管理器,符合 PEP 621 标准,自动化程度高。
|
||||||
|
|
||||||
- [fllesser/nonebot-plugin-template](https://github.com/fllesser/nonebot-plugin-template)
|
- [fllesser/nonebot-plugin-template](https://github.com/fllesser/nonebot-plugin-template)
|
||||||
|
|
||||||
|
此路径使用 **uv** 项目管理器和 **PoeThePoet** 任务运行器,构建速度快,适合追求效率的开发者。
|
||||||
|
|
||||||
|
- [A-kirami/nonebot-plugin-template](https://github.com/A-kirami/nonebot-plugin-template)
|
||||||
|
|
||||||
|
此路径使用 **Poetry** 项目管理器,适合熟悉传统 Python 生态的开发者。
|
||||||
|
|
||||||
|
#### 1. 创建项目
|
||||||
|
|
||||||
|
1. 访问上述三个模板之一。
|
||||||
|
2. 点击 **“Use this template”** → **“Create a new repository”**。
|
||||||
|
3. 仓库名称填写:`nonebot-plugin-{your-plugin-name}`(此部分以 `nonebot-plugin-weather` 为例)。
|
||||||
|
4. 点击 **“Create repository from template”**。
|
||||||
|
|
||||||
|
#### 2. 配置发布权限
|
||||||
|
|
||||||
|
1. 进入新仓库 → **Settings** → **Actions** → **General**。
|
||||||
|
2. 在 **Workflow permissions** 下,勾选 **“Read and write permissions”** → 点击 **Save**。
|
||||||
|
|
||||||
|
#### 3. 全局替换项目信息
|
||||||
|
|
||||||
|
在仓库中点击 **“Add file”** → **“Create new file”**,创建一个空文件 `LICENSE`,选择开源协议并提交(此操作会触发工作流)。
|
||||||
|
|
||||||
|
然后在本地克隆仓库,使用编辑器对以下内容进行**全局替换**:
|
||||||
|
|
||||||
:::tip 提示
|
:::tip 提示
|
||||||
本文档**不保证**第三方模板的适用性。
|
此部分以“天气插件”为例,实际的替换内容应该根据你所创建的插件名称等相应调整。
|
||||||
|
|
||||||
根据项目模板提供的使用指导补全/修改相应内容后上传到 GitHub 即可。
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
### 插件依赖
|
| 原内容 | 替换为 |
|
||||||
|
| ------------------------------ | ---------------------------------- |
|
||||||
|
| `nonebot-plugin-template` | `nonebot-plugin-weather` |
|
||||||
|
| `nonebot_plugin_template` | `nonebot_plugin_weather` |
|
||||||
|
| `<your_plugin_humanized_name>` | `天气查询` |
|
||||||
|
| `<your_plugin_description>` | `查询指定城市的实时天气与未来预报` |
|
||||||
|
| `<your_github>` | `你的GitHub用户名` |
|
||||||
|
| `<your_email>` | `你的邮箱` |
|
||||||
|
|
||||||
本段指导填写插件依赖,避免不正确的依赖信息导致插件无法正常工作。
|
#### 4. 安装依赖与开发
|
||||||
|
|
||||||
依赖填写的基本原则:程序直接导入了什么第三方库,就添加什么第三方包依赖;能用哪些第三方库的特性,就根据使用的特性锁定第三方包版本。
|
<Tabs groupId="publish-path" defaultValue="pdm" values={[
|
||||||
|
{label: 'PDM + RF-Tar-Railt 模板', value: 'pdm'},
|
||||||
|
{label: 'uv + fllesser 模板', value: 'uv'},
|
||||||
|
{label: 'Poetry + A-kirami 模板', value: 'poetry'},
|
||||||
|
]}>
|
||||||
|
<TabItem value="pdm" label="PDM + RF-Tar-Railt 模板">
|
||||||
|
|
||||||
:::caution 注意
|
```bash
|
||||||
|
# 安装 PDM(若未安装)
|
||||||
|
curl -sSL https://pdm-project.org/install-pdm.py | python3 -
|
||||||
|
|
||||||
1. 插件需要添加 `nonebot2` 为依赖以避免“幽灵依赖”;
|
# 安装项目依赖(自动创建虚拟环境)
|
||||||
2. 插件需要将使用的适配器加入依赖列表,如:使用 OneBot 适配器的插件应添加 `nonebot-adapter-onebot` 依赖;
|
pdm sync
|
||||||
3. 由于 `nonebot` 是指 `nonebot1` **而非** `nonebot2`,因此要注意**不要**将 `nonebot` 添加为插件的依赖,以免造成冲突;
|
|
||||||
4. 尽可能避免使用 `==` 锁定单一版本,增强与其它插件的兼容性。
|
|
||||||
|
|
||||||
:::
|
# 添加新依赖(如 httpx)
|
||||||
|
pdm add httpx
|
||||||
### 填写插件元数据
|
|
||||||
|
|
||||||
请注意,插件发布要求**必须**填写元数据才能通过审核。
|
|
||||||
|
|
||||||
下面是一个示例:
|
|
||||||
|
|
||||||
```python title=nonebot_plugin_{your_plugin_name}/__init__.py
|
|
||||||
from nonebot.plugin import PluginMetadata
|
|
||||||
|
|
||||||
from .config import Config
|
|
||||||
|
|
||||||
__plugin_meta__ = PluginMetadata(
|
|
||||||
name="{插件名称}",
|
|
||||||
description="{插件介绍}",
|
|
||||||
usage="{插件用法}",
|
|
||||||
|
|
||||||
type="{插件分类}",
|
|
||||||
# 发布必填,当前有效类型有:`library`(为其他插件编写提供功能),`application`(向机器人用户提供功能)。
|
|
||||||
|
|
||||||
homepage="{项目主页}",
|
|
||||||
# 发布必填。
|
|
||||||
|
|
||||||
config=Config,
|
|
||||||
# 插件配置项类,如无需配置可不填写。
|
|
||||||
|
|
||||||
supported_adapters={"~onebot.v11", "~telegram"},
|
|
||||||
# 支持的适配器集合,其中 `~` 在此处代表前缀 `nonebot.adapters.`,其余适配器亦按此格式填写。
|
|
||||||
# 若插件可以保证兼容所有适配器(即仅使用基本适配器功能)可不填写,否则应该列出插件支持的适配器。
|
|
||||||
)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
:::caution 注意
|
</TabItem>
|
||||||
`__plugin_meta__` 变量**必须**处于插件最外层(如 `__init__.py` 中),否则无法正常识别。
|
|
||||||
|
|
||||||
一般做法是在 `__init__.py` 中定义 `__plugin_meta__`。
|
<TabItem value="uv" label="uv + fllesser 模板">
|
||||||
:::
|
|
||||||
|
|
||||||
:::tip 提示
|
```bash
|
||||||
带花括号 `{}` 的内容需要自行替换,注意**一定要把原有的花括号去掉**。
|
# 安装 uv(Windows)
|
||||||
:::
|
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
|
||||||
|
|
||||||
### 准备项目主页
|
# 安装 uv(macOS/Linux)
|
||||||
|
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||||
|
|
||||||
通常可以使用 GitHub 项目页面作为项目主页,在 `README.md` 文件中编写插件介绍等内容。
|
# 安装所有依赖(含 dev)
|
||||||
|
uv sync --all-groups -p 3.12
|
||||||
|
|
||||||
内容大致包括:
|
# 添加新依赖
|
||||||
|
uv add httpx
|
||||||
|
```
|
||||||
|
|
||||||
- 插件功能介绍
|
</TabItem>
|
||||||
- 安装方法(建议至少有 `nb-cli` 方式安装,**不要**使用旧式的 `bot.py` 配置)
|
|
||||||
- 插件配置项(若无可跳过)
|
|
||||||
- 插件设置的触发规则(若无可跳过)
|
|
||||||
- 插件的其它用法(按需编写)
|
|
||||||
|
|
||||||
:::tip 提示
|
<TabItem value="poetry" label="Poetry + A-kirami 模板">
|
||||||
可以参考[第三方项目模板](#第三方项目模板)。
|
|
||||||
:::
|
|
||||||
|
|
||||||
### 发布至 [PyPI](https://pypi.org)
|
```bash
|
||||||
|
# 安装 Poetry(推荐方式)
|
||||||
|
curl -sSL https://install.python-poetry.org | python3 -
|
||||||
|
|
||||||
|
# 安装项目依赖
|
||||||
|
poetry install
|
||||||
|
|
||||||
|
# 添加新依赖
|
||||||
|
poetry add httpx
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
#### 5. 更新版本并发布
|
||||||
|
|
||||||
|
<Tabs
|
||||||
|
groupId="publish-path-bump"
|
||||||
|
defaultValue="bump-my-version"
|
||||||
|
values={[
|
||||||
|
{ label: "使用 bump-my-version", value: "bump-my-version" },
|
||||||
|
{ label: "使用项目管理器", value: "bump-manager" },
|
||||||
|
{ label: "手动更新版本", value: "bump-manual" },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<TabItem value="bump-my-version" label="使用 bump-my-version">
|
||||||
|
|
||||||
|
[bump-my-version](https://github.com/callowayproject/bump-my-version) 是一个功能强大、可配置的 Python 项目版本更新工具,支持自动提交到 Git 等 VCS。
|
||||||
|
|
||||||
|
<Tabs groupId="publish-path" defaultValue="pdm" values={[
|
||||||
|
{label: 'PDM + RF-Tar-Railt 模板', value: 'pdm'},
|
||||||
|
{label: 'uv + fllesser 模板', value: 'uv'},
|
||||||
|
{label: 'Poetry + A-kirami 模板', value: 'poetry'},
|
||||||
|
]}>
|
||||||
|
<TabItem value="pdm" label="PDM + RF-Tar-Railt 模板">
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装 bump-my-version
|
||||||
|
pdm add --dev bump-my-version
|
||||||
|
|
||||||
|
# 更新 patch 版本
|
||||||
|
pdm run bump patch
|
||||||
|
|
||||||
|
# 推送 tag 触发发布
|
||||||
|
git push origin --tags
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem value="uv" label="uv + fllesser 模板">
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 更新 patch 版本
|
||||||
|
uv run poe bump patch
|
||||||
|
|
||||||
|
# 推送 tag 触发发布
|
||||||
|
git push origin --tags
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem value="poetry" label="Poetry + A-kirami 模板">
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装 bump-my-version
|
||||||
|
poetry add --dev bump-my-version
|
||||||
|
|
||||||
|
# 更新 patch 版本
|
||||||
|
poetry run bump patch
|
||||||
|
|
||||||
|
# 推送 tag 触发发布
|
||||||
|
git push origin --tags
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
<TabItem value="bump-manager" label="使用项目管理器">
|
||||||
|
|
||||||
|
<Tabs groupId="publish-path" defaultValue="pdm" values={[
|
||||||
|
{label: 'PDM + RF-Tar-Railt 模板', value: 'pdm'},
|
||||||
|
{label: 'uv + fllesser 模板', value: 'uv'},
|
||||||
|
{label: 'Poetry + A-kirami 模板', value: 'poetry'},
|
||||||
|
]}>
|
||||||
|
<TabItem value="pdm" label="PDM + RF-Tar-Railt 模板">
|
||||||
|
|
||||||
|
需要安装 PDM 插件 [pdm-bump](https://github.com/carstencodes/pdm-bump)。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装 pdm-bump
|
||||||
|
pdm self add pdm-bump
|
||||||
|
|
||||||
|
# 更新 patch 版本
|
||||||
|
pdm bump patch
|
||||||
|
|
||||||
|
# 推送 tag 触发发布
|
||||||
|
git push origin --tags
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem value="uv" label="uv + fllesser 模板">
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 更新 patch 版本
|
||||||
|
uv version --bump patch
|
||||||
|
|
||||||
|
# 创建相应提交与标签
|
||||||
|
git add pyproject.toml
|
||||||
|
git commit -m "chore: release v0.1.1" # 替换为实际的版本号
|
||||||
|
git tag v0.1.1 # 替换为实际的版本号
|
||||||
|
|
||||||
|
# 推送 tag 触发发布
|
||||||
|
git push origin --tags
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem value="poetry" label="Poetry + A-kirami 模板">
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 更新版本(自动提交并打标签)
|
||||||
|
poetry version patch
|
||||||
|
|
||||||
|
# 推送 tag 触发发布
|
||||||
|
git push origin --tags
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
<TabItem value="bump-manual" label="手动更新版本">
|
||||||
|
|
||||||
|
手动更新 `pyproject.toml` 中的 `version` 字段,然后推送 tag 触发发布工作流
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add pyproject.toml
|
||||||
|
git commit -m "chore: release v0.1.1" # 替换为实际的版本号
|
||||||
|
git tag v0.1.1 # 替换为实际的版本号
|
||||||
|
git push origin --tags
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
推送 `v*` 标签后,模板提供的 GitHub Actions 工作流将自动构建并发布到 PyPI。
|
||||||
|
|
||||||
|
#### 6. 发布到 [PyPI](https://pypi.org)
|
||||||
|
|
||||||
|
<Tabs groupId="publish-method" defaultValue="template" values={[
|
||||||
|
{label: '使用模板的自动发布工作流', value: 'template'},
|
||||||
|
{label: '手动发布', value: 'manual'},
|
||||||
|
]}>
|
||||||
|
<TabItem value="template" label="使用模板的自动发布工作流">
|
||||||
|
不同模板使用的发布方式可能不同,具体配置流程参考对应模板的详细使用指南。
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem value="manual" label="手动发布">
|
||||||
根据选用的构建系统,在项目的 `pyproject.toml` 中填入必要信息后进行构建与发布。
|
根据选用的构建系统,在项目的 `pyproject.toml` 中填入必要信息后进行构建与发布。
|
||||||
|
|
||||||
:::tip 提示
|
:::tip 提示
|
||||||
不同构建工具的使用可能存在差别。本文仅以 [`pdm`](https://pdm.fming.dev/latest/),
|
不同构建工具的使用可能存在差别。本文仅以 [`pdm`](https://pdm-project.org/zh/latest/),
|
||||||
[`poetry`](https://python-poetry.org/docs/), [`setuptools`](https://setuptools.pypa.io/en/latest/)
|
[`poetry`](https://python-poetry.org/docs/), [`setuptools`](https://setuptools.pypa.io/en/latest/)
|
||||||
构建系统**本地构建与发布**为示例讲解,其余构建/管理工具等和自动化构建的用法请读者自行探索。
|
构建系统**本地构建与发布**为示例讲解,其余构建/管理工具等和自动化构建的用法请读者自行探索。
|
||||||
:::
|
:::
|
||||||
@@ -179,10 +354,187 @@ twine upload dist/* # 只发布先前的构建
|
|||||||
</TabItem>
|
</TabItem>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
:::tip 提示
|
:::tip 提示
|
||||||
发布前建议自行测试构建包是否可用,避免遗漏代码文件或资源文件等问题。
|
发布前建议自行测试构建包是否可用,避免遗漏代码文件或资源文件等问题。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
## 基本要求
|
||||||
|
|
||||||
|
无论你选择哪条路径,以下内容**必须**完成,否则无法通过商店自动检查:
|
||||||
|
|
||||||
|
### 能够正确加载
|
||||||
|
|
||||||
|
插件包必须能够被 NoneBot 正确加载,在商店审核中会通过 **NoneFlow** 自动化加载测试进行。
|
||||||
|
|
||||||
|
#### 依赖其他插件
|
||||||
|
|
||||||
|
如果插件依赖其他插件提供的功能,则**必须**在代码中使用 `require()` 来引入该插件,然后才能 `import` 该插件提供的功能。具体细节参阅[跨插件访问](../advanced/requiring.md)。
|
||||||
|
|
||||||
|
使用示例如下:
|
||||||
|
|
||||||
|
```python title=nonebot_plugin_weather/__init__.py
|
||||||
|
from nonebot import require
|
||||||
|
|
||||||
|
require("nonebot_plugin_apscheduler")
|
||||||
|
|
||||||
|
from nonebot_plugin_apscheduler import scheduler
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 不能零配置加载的插件
|
||||||
|
|
||||||
|
如果插件需要必要配置项才能正常导入,则**必须**在商店提交表单中填写必要的配置项内容。
|
||||||
|
|
||||||
|
但一种更好的做法是,**将插件设计为零配置即可加载**(允许缺少必要配置项时插件仍能正常导入,但不执行需要相应配置项的功能),尤其是对于一些必要配置含有敏感信息(如密钥、Token、API Key 等)的插件。这样可以避免在商店提交表单时填写敏感信息的风险。
|
||||||
|
|
||||||
|
### 插件元数据
|
||||||
|
|
||||||
|
插件包**必须**填写元数据才能通过 **NoneFlow** 自动化检查。
|
||||||
|
|
||||||
|
下面是一个示例:
|
||||||
|
|
||||||
|
```python title=nonebot_plugin_weather/__init__.py
|
||||||
|
from nonebot.plugin import PluginMetadata
|
||||||
|
|
||||||
|
from .config import Config
|
||||||
|
|
||||||
|
__plugin_meta__ = PluginMetadata(
|
||||||
|
# 基本信息(必填)
|
||||||
|
name="天气查询", # 插件名称
|
||||||
|
description="查询指定城市的实时天气与未来预报", # 插件介绍
|
||||||
|
usage="发送【天气 城市名】获取天气信息", # 插件用法
|
||||||
|
|
||||||
|
# 发布额外信息
|
||||||
|
type="application", # 插件分类
|
||||||
|
# 发布必填,当前有效类型有:`library`(为其他插件编写提供功能),`application`(向机器人用户提供功能)。
|
||||||
|
|
||||||
|
homepage="https://github.com/你的用户名/nonebot-plugin-weather",
|
||||||
|
# 发布必填。
|
||||||
|
|
||||||
|
config=Config,
|
||||||
|
# 插件配置项类,如果有配置类则必须填写。
|
||||||
|
|
||||||
|
supported_adapters={"~onebot.v11"},
|
||||||
|
# 支持的适配器集合,其中 `~` 在此处代表前缀 `nonebot.adapters.`,其余适配器亦按此格式填写。
|
||||||
|
# 若插件只使用了 NoneBot 基本抽象,应显式填写 None,否则应该列出插件支持的适配器。
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
:::caution 注意
|
||||||
|
`__plugin_meta__` 变量**必须**处于插件最外层(如 `__init__.py` 中),否则无法正常识别。
|
||||||
|
|
||||||
|
一般做法是在 `__init__.py` 中定义 `__plugin_meta__`。
|
||||||
|
:::
|
||||||
|
|
||||||
|
#### 继承其他插件支持的适配器
|
||||||
|
|
||||||
|
如果你的插件依赖于其他插件提供的支持功能,而其他插件可能支持更少的适配器,这时就应该使用
|
||||||
|
[inherit_supported_adapters()](../api/plugin/load#inherit-supported-adapters) 函数来继承其他插件支持的适配器。
|
||||||
|
|
||||||
|
示例用法如下:
|
||||||
|
|
||||||
|
```python title=nonebot_plugin_weather/__init__.py
|
||||||
|
from nonebot import require
|
||||||
|
from nonebot.plugin import PluginMetadata, inherit_supported_adapters
|
||||||
|
|
||||||
|
from .config import Config
|
||||||
|
|
||||||
|
require("nonebot_plugin_alconna") # 必须先 require 才能被 inherit_supported_adapters 处理
|
||||||
|
|
||||||
|
__plugin_meta__ = PluginMetadata(
|
||||||
|
name="天气查询",
|
||||||
|
description="查询指定城市的实时天气与未来预报",
|
||||||
|
usage="发送【天气 城市名】获取天气信息",
|
||||||
|
type="application",
|
||||||
|
homepage="https://github.com/你的用户名/nonebot-plugin-weather",
|
||||||
|
config=Config,
|
||||||
|
|
||||||
|
supported_adapters=inherit_supported_adapters("nonebot_plugin_alconna"),
|
||||||
|
# 继承 nonebot_plugin_alconna 插件的适配器支持列表
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 准备项目主页
|
||||||
|
|
||||||
|
通常可以使用 GitHub 项目页面作为项目主页,在 `README.md` 文件中编写插件介绍等内容。
|
||||||
|
|
||||||
|
内容大致包括:
|
||||||
|
|
||||||
|
- 插件功能介绍;
|
||||||
|
- 安装方法
|
||||||
|
- **必须**有 NB-CLI 方式安装
|
||||||
|
- 可选依赖可以给出其他安装方式
|
||||||
|
- **不得**使用旧式的 `bot.py` 配置
|
||||||
|
- 插件配置项(如 `Config` 类字段,若无可跳过)
|
||||||
|
- 插件设置的触发规则(若无可跳过)
|
||||||
|
- 插件的其它用法(按需编写)
|
||||||
|
- 效果图、权限说明(按需编写)
|
||||||
|
|
||||||
|
## 质量要求
|
||||||
|
|
||||||
|
以下内容**强烈建议**完成,否则社区成员将会要求修改:
|
||||||
|
|
||||||
|
### 依赖管理原则
|
||||||
|
|
||||||
|
- **必须**包含 `nonebot2`。
|
||||||
|
- **必须**将插件直接使用的适配器加入依赖列表,如:使用 OneBot 适配器的插件应添加 `nonebot-adapter-onebot` 依赖;
|
||||||
|
- **禁止**使用 `==` 锁定单一版本,使用 `>=` 或 `~=`。
|
||||||
|
- **禁止**添加 `nonebot`(V1)作为依赖。
|
||||||
|
- 所有在代码中 `import` 的第三方库,必须在 `pyproject.toml` 的 `dependencies` 中列出。
|
||||||
|
|
||||||
|
### 避免误用同步操作
|
||||||
|
|
||||||
|
NoneBot 是一个异步框架,插件中**禁止**使用任何可能阻塞事件循环的同步操作,例如:
|
||||||
|
|
||||||
|
- 同步 HTTP 请求(如 `requests` 库);
|
||||||
|
|
||||||
|
**推荐**操作(以 `httpx` 为例):
|
||||||
|
|
||||||
|
```python
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
response = await client.get("https://api.example.com/data") # 异步操作,不阻塞机器人
|
||||||
|
```
|
||||||
|
|
||||||
|
**禁止**操作:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import requests
|
||||||
|
|
||||||
|
requests.get("https://api.example.com/data") # 同步操作,会阻塞机器人
|
||||||
|
```
|
||||||
|
|
||||||
|
- 其他可能长时间运行阻塞事件循环的操作。
|
||||||
|
|
||||||
|
### 本地文件存储
|
||||||
|
|
||||||
|
如果插件需要在本地存储数据、配置或缓存文件,**必须**使用 [`nonebot-plugin-localstore`](https://github.com/nonebot/plugin-localstore) 管理,具体细节参阅[本地存储](../best-practice/data-storing.md)章节。
|
||||||
|
|
||||||
|
参考示例:
|
||||||
|
|
||||||
|
```python title=nonebot_plugin_weather/__init__.py
|
||||||
|
from pathlib import Path
|
||||||
|
from nonebot import require
|
||||||
|
require("nonebot_plugin_localstore")
|
||||||
|
|
||||||
|
import nonebot_plugin_localstore as store
|
||||||
|
|
||||||
|
# 获取插件缓存文件(夹)路径
|
||||||
|
weather_cache_dir: Path = store.get_plugin_cache_dir()
|
||||||
|
weather_cache_file: Path = store.get_plugin_cache_file("cache.json")
|
||||||
|
|
||||||
|
# 获取插件配置文件(夹)路径
|
||||||
|
weather_config_dir: Path = store.get_plugin_config_dir()
|
||||||
|
weather_config_file: Path = store.get_plugin_config_file("config.toml")
|
||||||
|
|
||||||
|
# 获取插件数据文件(夹)路径
|
||||||
|
weather_data_dir: Path = store.get_plugin_data_dir()
|
||||||
|
weather_data_file: Path = store.get_plugin_data_file("resource-index.json")
|
||||||
|
```
|
||||||
|
|
||||||
## 商店审核
|
## 商店审核
|
||||||
|
|
||||||
### 提交申请
|
### 提交申请
|
||||||
@@ -201,6 +553,6 @@ twine upload dist/* # 只发布先前的构建
|
|||||||
若插件检查未通过或信息有误,**不必**关闭当前 Issue。只需更新插件并上传到 PyPI/修改信息后勾选插件测试勾选框即可重新触发插件检查。
|
若插件检查未通过或信息有误,**不必**关闭当前 Issue。只需更新插件并上传到 PyPI/修改信息后勾选插件测试勾选框即可重新触发插件检查。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
之后,NoneBot 的维护者和一些插件开发者会初步检查插件代码,帮助减少该插件的问题。
|
之后,NoneBot 的维护者和一些志愿者会初步检查插件代码,帮助减少该插件的问题。
|
||||||
|
|
||||||
完成这些步骤后,您的插件将会被自动合并到[商店](/store/plugins),而您也将成为 [**NoneBot 贡献者**](https://github.com/nonebot/nonebot2/graphs/contributors)的一员。
|
完成这些步骤后,您的插件将会被自动合并到[商店](/store/plugins),而您也将成为 [**NoneBot 贡献者**](https://github.com/nonebot/nonebot2/graphs/contributors)的一员。
|
||||||
|
|||||||
@@ -48,8 +48,10 @@ options:
|
|||||||
|
|
||||||
```tree title=Project
|
```tree title=Project
|
||||||
📦 awesome-bot
|
📦 awesome-bot
|
||||||
|
├── 📂 .venv
|
||||||
├── 📂 awesome_bot
|
├── 📂 awesome_bot
|
||||||
│ └── 📂 plugins
|
│ └── 📂 plugins
|
||||||
|
├── 📜 .env.prod
|
||||||
├── 📜 pyproject.toml
|
├── 📜 pyproject.toml
|
||||||
└── 📜 README.md
|
└── 📜 README.md
|
||||||
```
|
```
|
||||||
@@ -91,18 +93,20 @@ options:
|
|||||||
$ nb plugin create
|
$ nb plugin create
|
||||||
[?] 插件名称: weather
|
[?] 插件名称: weather
|
||||||
[?] 使用嵌套插件? (y/N) N
|
[?] 使用嵌套插件? (y/N) N
|
||||||
[?] 输出目录: awesome_bot/plugins
|
[?] 请输入插件存储位置: awesome_bot/plugins
|
||||||
```
|
```
|
||||||
|
|
||||||
`nb-cli` 会在 `awesome_bot/plugins` 目录下创建一个名为 `weather` 的文件夹,其中包含的文件将在稍后章节中用到。
|
`nb-cli` 会在 `awesome_bot/plugins` 目录下创建一个名为 `weather` 的文件夹,其中包含的文件将在稍后章节中用到。
|
||||||
|
|
||||||
```tree title=Project
|
```tree title=Project
|
||||||
📦 awesome-bot
|
📦 awesome-bot
|
||||||
|
├── 📂 .venv
|
||||||
├── 📂 awesome_bot
|
├── 📂 awesome_bot
|
||||||
│ └── 📂 plugins
|
│ └── 📂 plugins
|
||||||
| └── 📂 foo
|
| └── 📂 weather
|
||||||
| ├── 📜 __init__.py
|
| ├── 📜 __init__.py
|
||||||
| └── 📜 config.py
|
| └── 📜 config.py
|
||||||
|
├── 📜 .env.prod
|
||||||
├── 📜 pyproject.toml
|
├── 📜 pyproject.toml
|
||||||
└── 📜 README.md
|
└── 📜 README.md
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ import Messenger from "@site/src/components/Messenger";
|
|||||||
顾名思义,“事件处理函数装饰器”是一个[装饰器(decorator)](https://docs.python.org/zh-cn/3/glossary.html#term-decorator),那么它的使用方法也同[函数定义](https://docs.python.org/zh-cn/3/reference/compound_stmts.html#function-definitions)中所展示的包装用法相同。例如:
|
顾名思义,“事件处理函数装饰器”是一个[装饰器(decorator)](https://docs.python.org/zh-cn/3/glossary.html#term-decorator),那么它的使用方法也同[函数定义](https://docs.python.org/zh-cn/3/reference/compound_stmts.html#function-definitions)中所展示的包装用法相同。例如:
|
||||||
|
|
||||||
```python {6-8} title=weather/__init__.py
|
```python {6-8} title=weather/__init__.py
|
||||||
|
from nonebot import on_command
|
||||||
from nonebot.rule import to_me
|
from nonebot.rule import to_me
|
||||||
from nonebot.plugin import on_command
|
|
||||||
|
|
||||||
weather = on_command("天气", rule=to_me(), aliases={"weather", "查天气"}, priority=10, block=True)
|
weather = on_command("天气", rule=to_me(), aliases={"weather", "查天气"}, priority=10, block=True)
|
||||||
|
|
||||||
@@ -46,8 +46,8 @@ async def handle_function():
|
|||||||
事件响应器操作与事件处理函数装饰器类似,通常作为事件响应器 `Matcher` 的[类方法](https://docs.python.org/zh-cn/3/library/functions.html#classmethod)存在,因此事件响应器操作的调用方法也是 `Matcher.func()` 的形式。不过不同的是,事件响应器操作并不是装饰器,因此并不需要@进行标注。
|
事件响应器操作与事件处理函数装饰器类似,通常作为事件响应器 `Matcher` 的[类方法](https://docs.python.org/zh-cn/3/library/functions.html#classmethod)存在,因此事件响应器操作的调用方法也是 `Matcher.func()` 的形式。不过不同的是,事件响应器操作并不是装饰器,因此并不需要@进行标注。
|
||||||
|
|
||||||
```python {8,9} title=weather/__init__.py
|
```python {8,9} title=weather/__init__.py
|
||||||
|
from nonebot import on_command
|
||||||
from nonebot.rule import to_me
|
from nonebot.rule import to_me
|
||||||
from nonebot.plugin import on_command
|
|
||||||
|
|
||||||
weather = on_command("天气", rule=to_me(), aliases={"weather", "查天气"}, priority=10, block=True)
|
weather = on_command("天气", rule=to_me(), aliases={"weather", "查天气"}, priority=10, block=True)
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,133 @@ toc_max_heading_level: 2
|
|||||||
|
|
||||||
# 更新日志
|
# 更新日志
|
||||||
|
|
||||||
|
## 最近更新
|
||||||
|
|
||||||
|
### 🚀 新功能
|
||||||
|
|
||||||
|
- Feature: WS 支持 ping interval/timeout 配置 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#3964](https://github.com/nonebot/nonebot2/pull/3964))
|
||||||
|
|
||||||
|
### 💫 杂项
|
||||||
|
|
||||||
|
- CI: 移除 Pull Request Target 触发器 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#3946](https://github.com/nonebot/nonebot2/pull/3946))
|
||||||
|
|
||||||
|
### 🍻 插件发布
|
||||||
|
|
||||||
|
- Plugin: 酷我音乐 [@noneflow](https://github.com/noneflow) ([#3975](https://github.com/nonebot/nonebot2/pull/3975))
|
||||||
|
- Plugin: osu!mania 工具箱 [@noneflow](https://github.com/noneflow) ([#3977](https://github.com/nonebot/nonebot2/pull/3977))
|
||||||
|
- Plugin: LibAmritaCore [@noneflow](https://github.com/noneflow) ([#3942](https://github.com/nonebot/nonebot2/pull/3942))
|
||||||
|
- Plugin: 苏大电费查询 [@noneflow](https://github.com/noneflow) ([#3966](https://github.com/nonebot/nonebot2/pull/3966))
|
||||||
|
- Plugin: 舞立方插件 [@noneflow](https://github.com/noneflow) ([#3956](https://github.com/nonebot/nonebot2/pull/3956))
|
||||||
|
- Plugin: 语录插件 [@noneflow](https://github.com/noneflow) ([#3963](https://github.com/nonebot/nonebot2/pull/3963))
|
||||||
|
- Plugin: 明日方舟猜干员游戏 [@noneflow](https://github.com/noneflow) ([#3939](https://github.com/nonebot/nonebot2/pull/3939))
|
||||||
|
- Plugin: 错误日志转发钉钉机器人 [@noneflow](https://github.com/noneflow) ([#3952](https://github.com/nonebot/nonebot2/pull/3952))
|
||||||
|
- Plugin: Bot状态监控 [@noneflow](https://github.com/noneflow) ([#3968](https://github.com/nonebot/nonebot2/pull/3968))
|
||||||
|
- Plugin: tg-stickers-downloads [@noneflow](https://github.com/noneflow) ([#3954](https://github.com/nonebot/nonebot2/pull/3954))
|
||||||
|
- Plugin: 12306车票查询 [@noneflow](https://github.com/noneflow) ([#3948](https://github.com/nonebot/nonebot2/pull/3948))
|
||||||
|
- Plugin: 会话配置 [@noneflow](https://github.com/noneflow) ([#3938](https://github.com/nonebot/nonebot2/pull/3938))
|
||||||
|
|
||||||
|
## v2.5.0
|
||||||
|
|
||||||
|
### 💥 破坏性变更
|
||||||
|
|
||||||
|
- Remove: 移除 Python 3.9 支持 [@shoucandanghehe](https://github.com/shoucandanghehe) ([#3860](https://github.com/nonebot/nonebot2/pull/3860))
|
||||||
|
|
||||||
|
### 🚀 新功能
|
||||||
|
|
||||||
|
- Feature: 放宽 pydantic compat model dump 类型 [@shoucandanghehe](https://github.com/shoucandanghehe) ([#3898](https://github.com/nonebot/nonebot2/pull/3898))
|
||||||
|
|
||||||
|
### 🐛 Bug 修复
|
||||||
|
|
||||||
|
- Fix: 修正 http/websocket client timeout [@StarHeartHunt](https://github.com/StarHeartHunt) ([#3923](https://github.com/nonebot/nonebot2/pull/3923))
|
||||||
|
- Fix: 修复 aiohttp 流式响应分块大小不固定问题 [@KeepingRunning](https://github.com/KeepingRunning) ([#3919](https://github.com/nonebot/nonebot2/pull/3919))
|
||||||
|
- Fix: aiohttp 驱动未处理 WSMsgType.CLOSED 类型 [@shoucandanghehe](https://github.com/shoucandanghehe) ([#3862](https://github.com/nonebot/nonebot2/pull/3862))
|
||||||
|
|
||||||
|
### 📝 文档
|
||||||
|
|
||||||
|
- Docs: 升级 Docusaurus 3.9.2 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#3861](https://github.com/nonebot/nonebot2/pull/3861))
|
||||||
|
- Docs: 修复插件元数据链接错误 [@yanyongyu](https://github.com/yanyongyu) ([#3894](https://github.com/nonebot/nonebot2/pull/3894))
|
||||||
|
- Docs: 完善对「发布插件」章节的文档描述 [@NCBM](https://github.com/NCBM) ([#3865](https://github.com/nonebot/nonebot2/pull/3865))
|
||||||
|
- Docs: Docker 部署镜像添加 latest tag [@AhsokaTano26](https://github.com/AhsokaTano26) ([#3787](https://github.com/nonebot/nonebot2/pull/3787))
|
||||||
|
- Docs: 调整文档 `on_command` import 路径 [@Xfjie314](https://github.com/Xfjie314) ([#3747](https://github.com/nonebot/nonebot2/pull/3747))
|
||||||
|
- Docs: 修复插件编写准备文档中的文本错误 [@Xfjie314](https://github.com/Xfjie314) ([#3746](https://github.com/nonebot/nonebot2/pull/3746))
|
||||||
|
- Docs: 修复格式化导致的语法错误 [@yanyongyu](https://github.com/yanyongyu) ([#3737](https://github.com/nonebot/nonebot2/pull/3737))
|
||||||
|
|
||||||
|
### 💫 杂项
|
||||||
|
|
||||||
|
- Plugin: 删除旧插件 group-config [@USTC-XeF2](https://github.com/USTC-XeF2) ([#3934](https://github.com/nonebot/nonebot2/pull/3934))
|
||||||
|
- Fix: 修正云湖适配器的 module_name [@he0119](https://github.com/he0119) ([#3937](https://github.com/nonebot/nonebot2/pull/3937))
|
||||||
|
- Dev: 修复 devcontainer corepack 安装 yarn 卡死 [@yanyongyu](https://github.com/yanyongyu) ([#3893](https://github.com/nonebot/nonebot2/pull/3893))
|
||||||
|
- Plugin: skland 插件添加标签 [@FrostN0v0](https://github.com/FrostN0v0) ([#3853](https://github.com/nonebot/nonebot2/pull/3853))
|
||||||
|
- CI: 修改 `test_depend` cpython 版本范围 [@yanyongyu](https://github.com/yanyongyu) ([#3828](https://github.com/nonebot/nonebot2/pull/3828))
|
||||||
|
- Plugin: 删除插件 nonebot_plugin_acmd [@hlfzsi](https://github.com/hlfzsi) ([#3750](https://github.com/nonebot/nonebot2/pull/3750))
|
||||||
|
|
||||||
|
### 🍻 插件发布
|
||||||
|
|
||||||
|
- Plugin: TS3 Tracker [@noneflow](https://github.com/noneflow) ([#3902](https://github.com/nonebot/nonebot2/pull/3902))
|
||||||
|
- Plugin: CS2 Radar [@noneflow](https://github.com/noneflow) ([#3908](https://github.com/nonebot/nonebot2/pull/3908))
|
||||||
|
- Plugin: 舞萌服务器监控 [@noneflow](https://github.com/noneflow) ([#3910](https://github.com/nonebot/nonebot2/pull/3910))
|
||||||
|
- Plugin: Endfield [@noneflow](https://github.com/noneflow) ([#3884](https://github.com/nonebot/nonebot2/pull/3884))
|
||||||
|
- Plugin: AppStore游戏榜单 [@noneflow](https://github.com/noneflow) ([#3896](https://github.com/nonebot/nonebot2/pull/3896))
|
||||||
|
- Plugin: 全自动膜拜机 [@noneflow](https://github.com/noneflow) ([#3906](https://github.com/nonebot/nonebot2/pull/3906))
|
||||||
|
- Plugin: 计算器:游戏 [@noneflow](https://github.com/noneflow) ([#3904](https://github.com/nonebot/nonebot2/pull/3904))
|
||||||
|
- Plugin: nonebot-plugin-sentry-transaction [@noneflow](https://github.com/noneflow) ([#3912](https://github.com/nonebot/nonebot2/pull/3912))
|
||||||
|
- Plugin: Codex [@noneflow](https://github.com/noneflow) ([#3889](https://github.com/nonebot/nonebot2/pull/3889))
|
||||||
|
- Plugin: nonebot-plugin-nbnhhsh [@noneflow](https://github.com/noneflow) ([#3887](https://github.com/nonebot/nonebot2/pull/3887))
|
||||||
|
- Plugin: mc服务器白名单管理工具 [@noneflow](https://github.com/noneflow) ([#3813](https://github.com/nonebot/nonebot2/pull/3813))
|
||||||
|
- Plugin: 特朗普社媒监控 [@noneflow](https://github.com/noneflow) ([#3882](https://github.com/nonebot/nonebot2/pull/3882))
|
||||||
|
- Plugin: The Betterest Mute Cat [@noneflow](https://github.com/noneflow) ([#3869](https://github.com/nonebot/nonebot2/pull/3869))
|
||||||
|
- Plugin: nonebot-plugin-cardimg [@noneflow](https://github.com/noneflow) ([#3857](https://github.com/nonebot/nonebot2/pull/3857))
|
||||||
|
- Plugin: Nonebot-Plugin-Rikka [@noneflow](https://github.com/noneflow) ([#3875](https://github.com/nonebot/nonebot2/pull/3875))
|
||||||
|
- Plugin: nonebot-plugin-peek [@noneflow](https://github.com/noneflow) ([#3859](https://github.com/nonebot/nonebot2/pull/3859))
|
||||||
|
- Plugin: 自动合成emoji [@noneflow](https://github.com/noneflow) ([#3867](https://github.com/nonebot/nonebot2/pull/3867))
|
||||||
|
- Plugin: 今日doro结局 [@noneflow](https://github.com/noneflow) ([#3852](https://github.com/nonebot/nonebot2/pull/3852))
|
||||||
|
- Plugin: Phira Server Manager [@noneflow](https://github.com/noneflow) ([#3855](https://github.com/nonebot/nonebot2/pull/3855))
|
||||||
|
- Plugin: Shiro Web Console [@noneflow](https://github.com/noneflow) ([#3832](https://github.com/nonebot/nonebot2/pull/3832))
|
||||||
|
- Plugin: 群聊拟人 [@noneflow](https://github.com/noneflow) ([#3820](https://github.com/nonebot/nonebot2/pull/3820))
|
||||||
|
- Plugin: BitTorrent磁力搜索 [@noneflow](https://github.com/noneflow) ([#3844](https://github.com/nonebot/nonebot2/pull/3844))
|
||||||
|
- Plugin: MCP Client [@noneflow](https://github.com/noneflow) ([#3842](https://github.com/nonebot/nonebot2/pull/3842))
|
||||||
|
- Plugin: Dice Helper [@noneflow](https://github.com/noneflow) ([#3840](https://github.com/nonebot/nonebot2/pull/3840))
|
||||||
|
- Plugin: Uniconfig-配置文件管理器 [@noneflow](https://github.com/noneflow) ([#3849](https://github.com/nonebot/nonebot2/pull/3849))
|
||||||
|
- Plugin: osugreek [@noneflow](https://github.com/noneflow) ([#3836](https://github.com/nonebot/nonebot2/pull/3836))
|
||||||
|
- Plugin: 基于QQ音乐歌单的音乐推荐 [@noneflow](https://github.com/noneflow) ([#3838](https://github.com/nonebot/nonebot2/pull/3838))
|
||||||
|
- Plugin: nonebot-plugin-bili2mp4 [@noneflow](https://github.com/noneflow) ([#3792](https://github.com/nonebot/nonebot2/pull/3792))
|
||||||
|
- Plugin: 战地6战绩查询 [@noneflow](https://github.com/noneflow) ([#3815](https://github.com/nonebot/nonebot2/pull/3815))
|
||||||
|
- Plugin: Tavily Search [@noneflow](https://github.com/noneflow) ([#3834](https://github.com/nonebot/nonebot2/pull/3834))
|
||||||
|
- Plugin: 群消息中继 [@noneflow](https://github.com/noneflow) ([#3804](https://github.com/nonebot/nonebot2/pull/3804))
|
||||||
|
- Plugin: 互联网异常事件监测 [@noneflow](https://github.com/noneflow) ([#3831](https://github.com/nonebot/nonebot2/pull/3831))
|
||||||
|
- Plugin: 词汇黑名单审查 [@noneflow](https://github.com/noneflow) ([#3817](https://github.com/nonebot/nonebot2/pull/3817))
|
||||||
|
- Plugin: 汉化进度记录 [@noneflow](https://github.com/noneflow) ([#3807](https://github.com/nonebot/nonebot2/pull/3807))
|
||||||
|
- Plugin: nonebot-plugin-ai-groupmate [@noneflow](https://github.com/noneflow) ([#3766](https://github.com/nonebot/nonebot2/pull/3766))
|
||||||
|
- Plugin: nonebot_plugin_boardgamehelper [@noneflow](https://github.com/noneflow) ([#3800](https://github.com/nonebot/nonebot2/pull/3800))
|
||||||
|
- Plugin: 即梦绘画 [@noneflow](https://github.com/noneflow) ([#3797](https://github.com/nonebot/nonebot2/pull/3797))
|
||||||
|
- Plugin: 快捷回复 [@noneflow](https://github.com/noneflow) ([#3795](https://github.com/nonebot/nonebot2/pull/3795))
|
||||||
|
- Plugin: pErithacus [@noneflow](https://github.com/noneflow) ([#3767](https://github.com/nonebot/nonebot2/pull/3767))
|
||||||
|
- Plugin: MC服务器状态查询 [@noneflow](https://github.com/noneflow) ([#3781](https://github.com/nonebot/nonebot2/pull/3781))
|
||||||
|
- Plugin: Instagram RapidAPI 解析 [@noneflow](https://github.com/noneflow) ([#3784](https://github.com/nonebot/nonebot2/pull/3784))
|
||||||
|
- Plugin: 今天是什么小猪 [@noneflow](https://github.com/noneflow) ([#3773](https://github.com/nonebot/nonebot2/pull/3773))
|
||||||
|
- Plugin: 御神签 [@noneflow](https://github.com/noneflow) ([#3777](https://github.com/nonebot/nonebot2/pull/3777))
|
||||||
|
- Plugin: 火车迷铁路工具箱 [@noneflow](https://github.com/noneflow) ([#3770](https://github.com/nonebot/nonebot2/pull/3770))
|
||||||
|
- Plugin: TerraLink [@noneflow](https://github.com/noneflow) ([#3775](https://github.com/nonebot/nonebot2/pull/3775))
|
||||||
|
- Plugin: 安安说 [@noneflow](https://github.com/noneflow) ([#3726](https://github.com/nonebot/nonebot2/pull/3726))
|
||||||
|
- Plugin: 安安的素描本聊天框 [@noneflow](https://github.com/noneflow) ([#3762](https://github.com/nonebot/nonebot2/pull/3762))
|
||||||
|
- Plugin: manosoba-reply-generator [@noneflow](https://github.com/noneflow) ([#3753](https://github.com/nonebot/nonebot2/pull/3753))
|
||||||
|
- Plugin: 模板绘图 [@noneflow](https://github.com/noneflow) ([#3752](https://github.com/nonebot/nonebot2/pull/3752))
|
||||||
|
- Plugin: iPinfo [@noneflow](https://github.com/noneflow) ([#3759](https://github.com/nonebot/nonebot2/pull/3759))
|
||||||
|
- Plugin: 电子课程表 [@noneflow](https://github.com/noneflow) ([#3743](https://github.com/nonebot/nonebot2/pull/3743))
|
||||||
|
- Plugin: 魔裁 Memes [@noneflow](https://github.com/noneflow) ([#3755](https://github.com/nonebot/nonebot2/pull/3755))
|
||||||
|
- Plugin: 图像对称处理 [@noneflow](https://github.com/noneflow) ([#3748](https://github.com/nonebot/nonebot2/pull/3748))
|
||||||
|
- Plugin: 每日人品 [@noneflow](https://github.com/noneflow) ([#3735](https://github.com/nonebot/nonebot2/pull/3735))
|
||||||
|
- Plugin: nonebot_plugin_markdown2img [@noneflow](https://github.com/noneflow) ([#3730](https://github.com/nonebot/nonebot2/pull/3730))
|
||||||
|
- Plugin: B站解析助手 [@noneflow](https://github.com/noneflow) ([#3728](https://github.com/nonebot/nonebot2/pull/3728))
|
||||||
|
|
||||||
|
### 🍻 机器人发布
|
||||||
|
|
||||||
|
- Bot: Rosmontis.io [@noneflow](https://github.com/noneflow) ([#3878](https://github.com/nonebot/nonebot2/pull/3878))
|
||||||
|
|
||||||
|
### 🍻 适配器发布
|
||||||
|
|
||||||
|
- Adapter: 云湖适配器 [@noneflow](https://github.com/noneflow) ([#3741](https://github.com/nonebot/nonebot2/pull/3741))
|
||||||
|
|
||||||
## v2.4.4
|
## v2.4.4
|
||||||
|
|
||||||
### 🚀 新功能
|
### 🚀 新功能
|
||||||
|
|||||||
@@ -1,141 +0,0 @@
|
|||||||
---
|
|
||||||
sidebar_position: 1
|
|
||||||
description: Alconna 命令解析拓展
|
|
||||||
|
|
||||||
slug: /best-practice/alconna/
|
|
||||||
---
|
|
||||||
|
|
||||||
import Tabs from "@theme/Tabs";
|
|
||||||
import TabItem from "@theme/TabItem";
|
|
||||||
|
|
||||||
# Alconna 插件
|
|
||||||
|
|
||||||
[`nonebot-plugin-alconna`](https://github.com/nonebot/plugin-alconna) 是一类提供了拓展响应规则的插件。
|
|
||||||
该插件使用 [Alconna](https://github.com/ArcletProject/Alconna) 作为命令解析器,
|
|
||||||
是一个简单、灵活、高效的命令参数解析器,并且不局限于解析命令式字符串。
|
|
||||||
|
|
||||||
该插件提供了一类新的事件响应器辅助函数 `on_alconna`,以及 `AlconnaResult` 等依赖注入函数。
|
|
||||||
|
|
||||||
该插件声明了一个 `Matcher` 的子类 `AlconnaMatcher`,并在 `AlconnaMatcher` 中添加了一些新的方法,例如:
|
|
||||||
|
|
||||||
- `assign`:基于 `Alconna` 解析结果,执行满足目标路径的处理函数
|
|
||||||
- `dispatch`:类似 `CommandGroup`,对目标路径创建一个新的 `AlconnaMatcher`,并将解析结果分配给该 `AlconnaMatcher`
|
|
||||||
- `got_path`:类似 `got`,但是可以指定目标路径,并且能够验证解析结果是否可用
|
|
||||||
- ...
|
|
||||||
|
|
||||||
基于 `Alconna` 的特性,该插件同时提供了一系列便捷的消息段标注。
|
|
||||||
标注可用于在 `Alconna` 中匹配消息中除 text 外的其他消息段,也可用于快速创建各适配器下的消息段。所有标注位于 `nonebot_plugin_alconna.adapters` 中。
|
|
||||||
|
|
||||||
该插件同时通过提供 `UniMessage` (通用消息模型) 实现了**跨平台接收和发送消息**的功能。
|
|
||||||
|
|
||||||
## 安装插件
|
|
||||||
|
|
||||||
在使用前请先安装 `nonebot-plugin-alconna` 插件至项目环境中,可参考[获取商店插件](../../tutorial/store.mdx#安装插件)来了解并选择安装插件的方式。如:
|
|
||||||
|
|
||||||
在**项目目录**下执行以下命令:
|
|
||||||
|
|
||||||
<Tabs groupId="install">
|
|
||||||
<TabItem value="cli" label="使用 nb-cli">
|
|
||||||
|
|
||||||
```shell
|
|
||||||
nb plugin install nonebot-plugin-alconna
|
|
||||||
```
|
|
||||||
|
|
||||||
</TabItem>
|
|
||||||
<TabItem value="pip" label="使用 pip">
|
|
||||||
|
|
||||||
```shell
|
|
||||||
pip install nonebot-plugin-alconna
|
|
||||||
```
|
|
||||||
|
|
||||||
</TabItem>
|
|
||||||
|
|
||||||
<TabItem value="pdm" label="使用 pdm">
|
|
||||||
|
|
||||||
```shell
|
|
||||||
pdm add nonebot-plugin-alconna
|
|
||||||
```
|
|
||||||
|
|
||||||
</TabItem>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
## 导入插件
|
|
||||||
|
|
||||||
由于 `nonebot-plugin-alconna` 作为插件,因此需要在使用前对其进行**加载**并**导入**其中的 `on_alconna` 来使用命令拓展。使用 `require` 方法可轻松完成这一过程,可参考 [跨插件访问](../../advanced/requiring.md) 一节进行了解。
|
|
||||||
|
|
||||||
```python
|
|
||||||
from nonebot import require
|
|
||||||
|
|
||||||
require("nonebot_plugin_alconna")
|
|
||||||
|
|
||||||
from nonebot_plugin_alconna import on_alconna
|
|
||||||
```
|
|
||||||
|
|
||||||
## 使用插件
|
|
||||||
|
|
||||||
在前面的[深入指南](../../appendices/session-control.mdx)中,我们已经得到了一个天气插件。
|
|
||||||
现在我们将使用 `Alconna` 来改写这个插件。
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>插件示例</summary>
|
|
||||||
|
|
||||||
```python title=weather/__init__.py
|
|
||||||
from nonebot import on_command
|
|
||||||
from nonebot.rule import to_me
|
|
||||||
from nonebot.matcher import Matcher
|
|
||||||
from nonebot.adapters import Message
|
|
||||||
from nonebot.params import CommandArg, ArgPlainText
|
|
||||||
|
|
||||||
weather = on_command("天气", rule=to_me(), aliases={"weather", "天气预报"})
|
|
||||||
|
|
||||||
@weather.handle()
|
|
||||||
async def handle_function(matcher: Matcher, args: Message = CommandArg()):
|
|
||||||
if args.extract_plain_text():
|
|
||||||
matcher.set_arg("location", args)
|
|
||||||
|
|
||||||
@weather.got("location", prompt="请输入地名")
|
|
||||||
async def got_location(location: str = ArgPlainText()):
|
|
||||||
if location not in ["北京", "上海", "广州", "深圳"]:
|
|
||||||
await weather.reject(f"你想查询的城市 {location} 暂不支持,请重新输入!")
|
|
||||||
await weather.finish(f"今天{location}的天气是...")
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
```python {5-9,13-15,17-18}
|
|
||||||
from nonebot.rule import to_me
|
|
||||||
from arclet.alconna import Alconna, Args
|
|
||||||
from nonebot_plugin_alconna import Match, on_alconna
|
|
||||||
|
|
||||||
weather = on_alconna(
|
|
||||||
Alconna("天气", Args["location?", str]),
|
|
||||||
aliases={"weather", "天气预报"},
|
|
||||||
rule=to_me(),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@weather.handle()
|
|
||||||
async def handle_function(location: Match[str]):
|
|
||||||
if location.available:
|
|
||||||
weather.set_path_arg("location", location.result)
|
|
||||||
|
|
||||||
@weather.got_path("location", prompt="请输入地名")
|
|
||||||
async def got_location(location: str):
|
|
||||||
if location not in ["北京", "上海", "广州", "深圳"]:
|
|
||||||
await weather.reject(f"你想查询的城市 {location} 暂不支持,请重新输入!")
|
|
||||||
await weather.finish(f"今天{location}的天气是...")
|
|
||||||
```
|
|
||||||
|
|
||||||
在上面的代码中,我们使用 `Alconna` 来解析命令,`on_alconna` 用来创建响应器,使用 `Match` 来获取解析结果。
|
|
||||||
|
|
||||||
关于更多 `Alconna` 的使用方法,可参考 [Alconna 文档](https://arclet.top/docs/tutorial/alconna),
|
|
||||||
或阅读 [Alconna 基本介绍](./command.md) 一节。
|
|
||||||
|
|
||||||
关于更多 `on_alconna` 的使用方法,可参考 [插件文档](https://github.com/nonebot/plugin-alconna/blob/master/docs.md),
|
|
||||||
或阅读 [响应规则的使用](./matcher.mdx) 一节。
|
|
||||||
|
|
||||||
## 交流与反馈
|
|
||||||
|
|
||||||
QQ 交流群: [🔗 链接](https://jq.qq.com/?_wv=1027&k=PUPOnCSH)
|
|
||||||
|
|
||||||
友链: [📚 文档](https://graiax.cn/guide/message_parser/alconna.html)
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"label": "Alconna 命令解析拓展",
|
|
||||||
"position": 6
|
|
||||||
}
|
|
||||||
@@ -1,607 +0,0 @@
|
|||||||
---
|
|
||||||
sidebar_position: 3
|
|
||||||
description: 响应规则的使用
|
|
||||||
---
|
|
||||||
|
|
||||||
import Messenger from "@site/src/components/Messenger";
|
|
||||||
|
|
||||||
# Alconna 插件
|
|
||||||
|
|
||||||
展示:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from nonebot_plugin_alconna import At, Image, on_alconna
|
|
||||||
from arclet.alconna import Args, Option, Alconna, Arparma, MultiVar, Subcommand
|
|
||||||
|
|
||||||
|
|
||||||
alc = Alconna(
|
|
||||||
["/", "!"],
|
|
||||||
"role-group",
|
|
||||||
Subcommand(
|
|
||||||
"add",
|
|
||||||
Args["name", str],
|
|
||||||
Option("member", Args["target", MultiVar(At)]),
|
|
||||||
),
|
|
||||||
Option("list"),
|
|
||||||
Option("icon", Args["icon", Image])
|
|
||||||
)
|
|
||||||
rg = on_alconna(alc, auto_send_output=True)
|
|
||||||
|
|
||||||
|
|
||||||
@rg.handle()
|
|
||||||
async def _(result: Arparma):
|
|
||||||
if result.find("list"):
|
|
||||||
img: bytes = await gen_role_group_list_image()
|
|
||||||
await rg.finish(Image(raw=img))
|
|
||||||
if result.find("add"):
|
|
||||||
group = await create_role_group(result.query[str]("add.name"))
|
|
||||||
if result.find("add.member"):
|
|
||||||
ats = result.query[tuple[At, ...]]("add.member.target")
|
|
||||||
group.extend(member.target for member in ats)
|
|
||||||
await rg.finish("添加成功")
|
|
||||||
```
|
|
||||||
|
|
||||||
## 响应器使用
|
|
||||||
|
|
||||||
本插件基于 **Alconna**,为 **Nonebot** 提供了一类新的事件响应器辅助函数 `on_alconna`:
|
|
||||||
|
|
||||||
```python
|
|
||||||
def on_alconna(
|
|
||||||
command: Alconna | str,
|
|
||||||
skip_for_unmatch: bool = True,
|
|
||||||
auto_send_output: bool = False,
|
|
||||||
aliases: set[str | tuple[str, ...]] | None = None,
|
|
||||||
comp_config: CompConfig | None = None,
|
|
||||||
extensions: list[type[Extension] | Extension] | None = None,
|
|
||||||
exclude_ext: list[type[Extension] | str] | None = None,
|
|
||||||
use_origin: bool = False,
|
|
||||||
use_cmd_start: bool = False,
|
|
||||||
use_cmd_sep: bool = False,
|
|
||||||
**kwargs,
|
|
||||||
...,
|
|
||||||
):
|
|
||||||
```
|
|
||||||
|
|
||||||
- `command`: Alconna 命令或字符串,字符串将通过 `AlconnaFormat` 转换为 Alconna 命令
|
|
||||||
- `skip_for_unmatch`: 是否在命令不匹配时跳过该响应
|
|
||||||
- `auto_send_output`: 是否自动发送输出信息并跳过响应
|
|
||||||
- `aliases`: 命令别名, 作用类似于 `on_command` 中的 aliases
|
|
||||||
- `comp_config`: 补全会话配置, 不传入则不启用补全会话
|
|
||||||
- `extensions`: 需要加载的匹配扩展, 可以是扩展类或扩展实例
|
|
||||||
- `exclude_ext`: 需要排除的匹配扩展, 可以是扩展类或扩展的id
|
|
||||||
- `use_origin`: 是否使用未经 to_me 等处理过的消息
|
|
||||||
- `use_cmd_start`: 是否使用 COMMAND_START 作为命令前缀
|
|
||||||
- `use_cmd_sep`: 是否使用 COMMAND_SEP 作为命令分隔符
|
|
||||||
|
|
||||||
`on_alconna` 返回的是 `Matcher` 的子类 `AlconnaMatcher` ,其拓展了如下方法:
|
|
||||||
|
|
||||||
- `.assign(path, value, or_not)`: 用于对包含多个选项/子命令的命令的分派处理(具体请看[条件控制](./matcher.mdx#条件控制))
|
|
||||||
- `.got_path(path, prompt, middleware)`: 在 `got` 方法的基础上,会以 path 对应的参数为准,读取传入 message 的最后一个消息段并验证转换
|
|
||||||
- `.set_path_arg(key, value)`, `.get_path_arg(key)`: 类似 `set_arg` 和 `got_arg`,为 `got_path` 的特化版本
|
|
||||||
- `.reject_path(path[, prompt, fallback])`: 类似于 `reject_arg`,对应 `got_path`
|
|
||||||
- `.dispatch`: 同样的分派处理,但是是类似 `CommandGroup` 一样返回新的 `AlconnaMatcher`
|
|
||||||
- `.got`, `send`, `reject`, ... : 拓展了 prompt 类型,即支持使用 `UniMessage` 作为 prompt
|
|
||||||
|
|
||||||
实例:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from nonebot import require
|
|
||||||
require("nonebot_plugin_alconna")
|
|
||||||
|
|
||||||
from arclet.alconna import Alconna, Option, Args
|
|
||||||
from nonebot_plugin_alconna import on_alconna, Match, UniMessage
|
|
||||||
|
|
||||||
|
|
||||||
login = on_alconna(Alconna(["/"], "login", Args["password?", str], Option("-r|--recall"))) # 这里["/"]指命令前缀必须是/
|
|
||||||
|
|
||||||
# /login -r 触发
|
|
||||||
@login.assign("recall")
|
|
||||||
async def login_exit():
|
|
||||||
await login.finish("已退出")
|
|
||||||
|
|
||||||
# /login xxx 触发
|
|
||||||
@login.assign("password")
|
|
||||||
async def login_handle(pw: Match[str]):
|
|
||||||
if pw.available:
|
|
||||||
login.set_path_arg("password", pw.result)
|
|
||||||
|
|
||||||
# /login 触发
|
|
||||||
@login.got_path("password", prompt=UniMessage.template("{:At(user, $event.get_user_id())} 请输入密码"))
|
|
||||||
async def login_got(password: str):
|
|
||||||
assert password
|
|
||||||
await login.send("登录成功")
|
|
||||||
```
|
|
||||||
|
|
||||||
## 依赖注入
|
|
||||||
|
|
||||||
本插件提供了一系列依赖注入函数,便于在响应函数中获取解析结果:
|
|
||||||
|
|
||||||
- `AlconnaResult`: `CommandResult` 类型的依赖注入函数
|
|
||||||
- `AlconnaMatches`: `Arparma` 类型的依赖注入函数
|
|
||||||
- `AlconnaDuplication`: `Duplication` 类型的依赖注入函数
|
|
||||||
- `AlconnaMatch`: `Match` 类型的依赖注入函数
|
|
||||||
- `AlconnaQuery`: `Query` 类型的依赖注入函数
|
|
||||||
|
|
||||||
同时,基于 [`Annotated` 支持](https://github.com/nonebot/nonebot2/pull/1832),添加了两类注解:
|
|
||||||
|
|
||||||
- `AlcMatches`:同 `AlconnaMatches`
|
|
||||||
- `AlcResult`:同 `AlconnaResult`
|
|
||||||
|
|
||||||
可以看到,本插件提供了几类额外的模型:
|
|
||||||
|
|
||||||
- `CommandResult`: 解析结果,包括了源命令 `source: Alconna` ,解析结果 `result: Arparma`,以及可能的输出信息 `output: str | None` 字段
|
|
||||||
- `Match`: 匹配项,表示参数是否存在于 `all_matched_args` 内,可用 `Match.available` 判断是否匹配,`Match.result` 获取匹配的值
|
|
||||||
- `Query`: 查询项,表示参数是否可由 `Arparma.query` 查询并获得结果,可用 `Query.available` 判断是否查询成功,`Query.result` 获取查询结果
|
|
||||||
|
|
||||||
**Alconna** 默认依赖注入的目标参数皆不需要使用依赖注入函数, 该效果对于 `AlconnaMatcher.got_path` 下的 Arg 同样有效:
|
|
||||||
|
|
||||||
```python
|
|
||||||
async def handle(
|
|
||||||
result: CommandResult,
|
|
||||||
arp: Arparma,
|
|
||||||
dup: Duplication,
|
|
||||||
source: Alconna,
|
|
||||||
abc: str, # 类似 Match, 但是若匹配结果不存在对应字段则跳过该 handler
|
|
||||||
foo: Match[str],
|
|
||||||
bar: Query[int] = Query("ttt.bar", 0) # Query 仍然需要一个默认值来传递 path 参数
|
|
||||||
):
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
:::note
|
|
||||||
|
|
||||||
如果你更喜欢 Depends 式的依赖注入,`nonebot_plugin_alconna` 同时提供了一系列的依赖注入函数,他们包括:
|
|
||||||
|
|
||||||
- `AlconnaResult`: `CommandResult` 类型的依赖注入函数
|
|
||||||
- `AlconnaMatches`: `Arparma` 类型的依赖注入函数
|
|
||||||
- `AlconnaDuplication`: `Duplication` 类型的依赖注入函数
|
|
||||||
- `AlconnaMatch`: `Match` 类型的依赖注入函数,其能够额外传入一个 middleware 函数来处理得到的参数
|
|
||||||
- `AlconnaQuery`: `Query` 类型的依赖注入函数,其能够额外传入一个 middleware 函数来处理得到的参数
|
|
||||||
- `AlconnaExecResult`: 提供挂载在命令上的 callback 的返回结果 (`Dict[str, Any]`) 的依赖注入函数
|
|
||||||
- `AlconnaExtension`: 提供指定类型的 `Extension` 的依赖注入函数
|
|
||||||
|
|
||||||
:::
|
|
||||||
|
|
||||||
实例:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from nonebot import require
|
|
||||||
require("nonebot_plugin_alconna")
|
|
||||||
|
|
||||||
from nonebot_plugin_alconna import (
|
|
||||||
on_alconna,
|
|
||||||
Match,
|
|
||||||
Query,
|
|
||||||
AlconnaMatch,
|
|
||||||
AlcResult
|
|
||||||
)
|
|
||||||
from arclet.alconna import Alconna, Args, Option, Arparma
|
|
||||||
|
|
||||||
|
|
||||||
test = on_alconna(
|
|
||||||
Alconna(
|
|
||||||
"test",
|
|
||||||
Option("foo", Args["bar", int]),
|
|
||||||
Option("baz", Args["qux", bool, False])
|
|
||||||
),
|
|
||||||
auto_send_output=True
|
|
||||||
)
|
|
||||||
|
|
||||||
@test.handle()
|
|
||||||
async def handle_test1(result: AlcResult):
|
|
||||||
await test.send(f"matched: {result.matched}")
|
|
||||||
await test.send(f"maybe output: {result.output}")
|
|
||||||
|
|
||||||
@test.handle()
|
|
||||||
async def handle_test2(result: Arparma):
|
|
||||||
await test.send(f"head result: {result.header_result}")
|
|
||||||
await test.send(f"args: {result.all_matched_args}")
|
|
||||||
|
|
||||||
@test.handle()
|
|
||||||
async def handle_test3(bar: Match[int] = AlconnaMatch("bar")):
|
|
||||||
if bar.available:
|
|
||||||
await test.send(f"foo={bar.result}")
|
|
||||||
|
|
||||||
@test.handle()
|
|
||||||
async def handle_test4(qux: Query[bool] = Query("baz.qux", False)):
|
|
||||||
if qux.available:
|
|
||||||
await test.send(f"baz.qux={qux.result}")
|
|
||||||
```
|
|
||||||
|
|
||||||
## 多平台适配
|
|
||||||
|
|
||||||
本插件提供了通用消息段标注, 通用消息段序列, 使插件使用者可以忽略平台之间字段的差异
|
|
||||||
|
|
||||||
响应器使用示例中使用了消息段标注,其中 `At` 属于通用标注,而 `Image` 属于 `onebot12` 适配器下的标注。
|
|
||||||
|
|
||||||
具体介绍和使用请查看 [通用信息组件](./uniseg.mdx#通用消息段)
|
|
||||||
|
|
||||||
本插件为以下适配器提供了专门的适配器标注:
|
|
||||||
|
|
||||||
| 协议名称 | 路径 |
|
|
||||||
| ------------------------------------------------------------------- | ------------------------------------ |
|
|
||||||
| [OneBot 协议](https://github.com/nonebot/adapter-onebot) | adapters.onebot11, adapters.onebot12 |
|
|
||||||
| [Telegram](https://github.com/nonebot/adapter-telegram) | adapters.telegram |
|
|
||||||
| [飞书](https://github.com/nonebot/adapter-feishu) | adapters.feishu |
|
|
||||||
| [GitHub](https://github.com/nonebot/adapter-github) | adapters.github |
|
|
||||||
| [QQ bot](https://github.com/nonebot/adapter-qq) | adapters.qq |
|
|
||||||
| [钉钉](https://github.com/nonebot/adapter-ding) | adapters.ding |
|
|
||||||
| [Dodo](https://github.com/nonebot/adapter-dodo) | adapters.dodo |
|
|
||||||
| [Console](https://github.com/nonebot/adapter-console) | adapters.console |
|
|
||||||
| [开黑啦](https://github.com/Tian-que/nonebot-adapter-kaiheila) | adapters.kook |
|
|
||||||
| [Mirai](https://github.com/ieew/nonebot_adapter_mirai2) | adapters.mirai |
|
|
||||||
| [Ntchat](https://github.com/JustUndertaker/adapter-ntchat) | adapters.ntchat |
|
|
||||||
| [MineCraft](https://github.com/17TheWord/nonebot-adapter-minecraft) | adapters.minecraft |
|
|
||||||
| [BiliBili Live](https://github.com/wwweww/adapter-bilibili) | adapters.bilibili |
|
|
||||||
| [Walle-Q](https://github.com/onebot-walle/nonebot_adapter_walleq) | adapters.onebot12 |
|
|
||||||
| [Discord](https://github.com/nonebot/adapter-discord) | adapters.discord |
|
|
||||||
| [Red 协议](https://github.com/nonebot/adapter-red) | adapters.red |
|
|
||||||
| [Satori 协议](https://github.com/nonebot/adapter-satori) | adapters.satori |
|
|
||||||
|
|
||||||
## 条件控制
|
|
||||||
|
|
||||||
本插件可以通过 `assign` 来控制一个具体的响应函数是否在不满足条件时跳过响应。
|
|
||||||
|
|
||||||
```python
|
|
||||||
...
|
|
||||||
from nonebot import require
|
|
||||||
require("nonebot_plugin_alconna")
|
|
||||||
...
|
|
||||||
|
|
||||||
from arclet.alconna import Alconna, Subcommand, Option, Args
|
|
||||||
from nonebot_plugin_alconna import on_alconna, CommandResult
|
|
||||||
|
|
||||||
|
|
||||||
pip = Alconna(
|
|
||||||
"pip",
|
|
||||||
Subcommand(
|
|
||||||
"install", Args["pak", str],
|
|
||||||
Option("--upgrade"),
|
|
||||||
Option("--force-reinstall")
|
|
||||||
),
|
|
||||||
Subcommand("list", Option("--out-dated"))
|
|
||||||
)
|
|
||||||
|
|
||||||
pip_cmd = on_alconna(pip)
|
|
||||||
|
|
||||||
# 仅在命令为 `pip install pip` 时响应
|
|
||||||
@pip_cmd.assign("install.pak", "pip")
|
|
||||||
async def update(res: CommandResult):
|
|
||||||
...
|
|
||||||
|
|
||||||
# 仅在命令为 `pip list` 时响应
|
|
||||||
@pip_cmd.assign("list")
|
|
||||||
async def list_(res: CommandResult):
|
|
||||||
...
|
|
||||||
|
|
||||||
# 在命令为 `pip install xxx` 时响应
|
|
||||||
@pip_cmd.assign("install")
|
|
||||||
async def install(res: CommandResult):
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
此外,使用 `AlconnaMatcher.dispatch` 还能像 `CommandGroup` 一样为每个分发设置独立的 matcher:
|
|
||||||
|
|
||||||
```python
|
|
||||||
update_cmd = pip_cmd.dispatch("install.pak", "pip")
|
|
||||||
|
|
||||||
@update_cmd.handle()
|
|
||||||
async def update(arp: CommandResult):
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
另外,`AlconnaMatcher` 有类似于 `got` 的 `got_path`:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from nonebot_plugin_alconna import At, Match, UniMessage, on_alconna
|
|
||||||
|
|
||||||
|
|
||||||
test_cmd = on_alconna(Alconna("test", Args["target?", Union[str, At]]))
|
|
||||||
|
|
||||||
@test_cmd.handle()
|
|
||||||
async def tt_h(target: Match[Union[str, At]]):
|
|
||||||
if target.available:
|
|
||||||
test_cmd.set_path_arg("target", target.result)
|
|
||||||
|
|
||||||
@test_cmd.got_path("target", prompt="请输入目标")
|
|
||||||
async def tt(target: Union[str, At]):
|
|
||||||
await test_cmd.send(UniMessage(["ok\n", target]))
|
|
||||||
```
|
|
||||||
|
|
||||||
`got_path` 与 `assign`,`Match`,`Query` 等地方一样,都需要指明 `path` 参数 (即对应 Arg 验证的路径)
|
|
||||||
|
|
||||||
`got_path` 会获取消息的最后一个消息段并转为 path 对应的类型,例如示例中 `target` 对应的 Arg 里要求 str 或 At,则 got 后用户输入的消息只有为 text 或 at 才能进入处理函数。
|
|
||||||
|
|
||||||
:::tip
|
|
||||||
|
|
||||||
`path` 支持 ~XXX 语法,其会把 ~ 替换为可能的父级路径:
|
|
||||||
|
|
||||||
```python
|
|
||||||
pip = Alconna(
|
|
||||||
"pip",
|
|
||||||
Subcommand(
|
|
||||||
"install",
|
|
||||||
Args["pak", str],
|
|
||||||
Option("--upgrade|-U"),
|
|
||||||
Option("--force-reinstall"),
|
|
||||||
),
|
|
||||||
Subcommand("list", Option("--out-dated")),
|
|
||||||
)
|
|
||||||
|
|
||||||
pipcmd = on_alconna(pip)
|
|
||||||
pip_install_cmd = pipcmd.dispatch("install")
|
|
||||||
|
|
||||||
|
|
||||||
@pip_install_cmd.assign("~upgrade")
|
|
||||||
async def pip1_u(pak: Query[str] = Query("~pak")):
|
|
||||||
await pip_install_cmd.finish(f"pip upgrading {pak.result}...")
|
|
||||||
```
|
|
||||||
|
|
||||||
:::
|
|
||||||
|
|
||||||
## 响应器创建装饰
|
|
||||||
|
|
||||||
本插件提供了一个 `funcommand` 装饰器, 其用于将一个接受任意参数, 返回 `str` 或 `Message` 或 `MessageSegment` 的函数转换为命令响应器:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from nonebot_plugin_alconna import funcommand
|
|
||||||
|
|
||||||
|
|
||||||
@funcommand()
|
|
||||||
async def echo(msg: str):
|
|
||||||
return msg
|
|
||||||
```
|
|
||||||
|
|
||||||
其等同于:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from arclet.alconna import Alconna, Args
|
|
||||||
from nonebot_plugin_alconna import on_alconna, AlconnaMatch, Match
|
|
||||||
|
|
||||||
|
|
||||||
echo = on_alconna(Alconna("echo", Args["msg", str]))
|
|
||||||
|
|
||||||
@echo.handle()
|
|
||||||
async def echo_exit(msg: Match[str] = AlconnaMatch("msg")):
|
|
||||||
await echo.finish(msg.result)
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
## 类Koishi构造器
|
|
||||||
|
|
||||||
本插件提供了一个 `Command` 构造器,其基于 `arclet.alconna.tools` 中的 `AlconnaString`, 以类似 `Koishi` 中注册命令的方式来构建一个 **AlconnaMatcher** :
|
|
||||||
|
|
||||||
```python
|
|
||||||
from nonebot_plugin_alconna import Command, Arparma
|
|
||||||
|
|
||||||
|
|
||||||
book = (
|
|
||||||
Command("book", "测试")
|
|
||||||
.option("writer", "-w <id:int>")
|
|
||||||
.option("writer", "--anonymous", {"id": 0})
|
|
||||||
.usage("book [-w <id:int> | --anonymous]")
|
|
||||||
.shortcut("测试", {"args": ["--anonymous"]})
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
|
|
||||||
@book.handle()
|
|
||||||
async def _(arp: Arparma):
|
|
||||||
await book.send(str(arp.options))
|
|
||||||
```
|
|
||||||
|
|
||||||
甚至,你可以设置 `action` 来设定响应行为:
|
|
||||||
|
|
||||||
```python
|
|
||||||
book = (
|
|
||||||
Command("book", "测试")
|
|
||||||
.option("writer", "-w <id:int>")
|
|
||||||
.option("writer", "--anonymous", {"id": 0})
|
|
||||||
.usage("book [-w <id:int> | --anonymous]")
|
|
||||||
.shortcut("测试", {"args": ["--anonymous"]})
|
|
||||||
.action(lambda options: str(options)) # 会自动通过 bot.send 发送
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 返回值中间件
|
|
||||||
|
|
||||||
在 `AlconnaMatch`,`AlconnaQuery` 或 `got_path` 中,你可以使用 `middleware` 参数来传入一个对返回值进行处理的函数:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from nonebot_plugin_alconna import image_fetch
|
|
||||||
|
|
||||||
|
|
||||||
mask_cmd = on_alconna(
|
|
||||||
Alconna("search", Args["img?", Image]),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@mask_cmd.handle()
|
|
||||||
async def mask_h(matcher: AlconnaMatcher, img: Match[bytes] = AlconnaMatch("img", image_fetch)):
|
|
||||||
result = await search_img(img.result)
|
|
||||||
await matcher.send(result.content)
|
|
||||||
```
|
|
||||||
|
|
||||||
其中,`image_fetch` 是一个中间件,其接受一个 `Image` 对象,并提取图片的二进制数据返回。
|
|
||||||
|
|
||||||
## 匹配拓展
|
|
||||||
|
|
||||||
本插件提供了一个 `Extension` 类,其用于自定义 AlconnaMatcher 的部分行为
|
|
||||||
|
|
||||||
例如一个 `LLMExtension` 可以如下实现 (仅举例):
|
|
||||||
|
|
||||||
```python
|
|
||||||
from nonebot_plugin_alconna import Extension, Alconna, on_alconna, Interface
|
|
||||||
|
|
||||||
|
|
||||||
class LLMExtension(Extension):
|
|
||||||
@property
|
|
||||||
def priority(self) -> int:
|
|
||||||
return 10
|
|
||||||
|
|
||||||
@property
|
|
||||||
def id(self) -> str:
|
|
||||||
return "LLMExtension"
|
|
||||||
|
|
||||||
def __init__(self, llm):
|
|
||||||
self.llm = llm
|
|
||||||
|
|
||||||
def post_init(self, alc: Alconna) -> None:
|
|
||||||
self.llm.add_context(alc.command, alc.meta.description)
|
|
||||||
|
|
||||||
async def receive_wrapper(self, bot, event, receive):
|
|
||||||
resp = await self.llm.input(str(receive))
|
|
||||||
return receive.__class__(resp.content)
|
|
||||||
|
|
||||||
def before_catch(self, name, annotation, default):
|
|
||||||
return name == "llm"
|
|
||||||
|
|
||||||
def catch(self, interface: Interface):
|
|
||||||
if interface.name == "llm":
|
|
||||||
return self.llm
|
|
||||||
|
|
||||||
matcher = on_alconna(
|
|
||||||
Alconna(...),
|
|
||||||
extensions=[LLMExtension(LLM)]
|
|
||||||
)
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
那么添加了 `LLMExtension` 的响应器便能接受任何能通过 llm 翻译为具体命令的自然语言消息,同时可以在响应器中为所有 `llm` 参数注入模型变量。
|
|
||||||
|
|
||||||
目前 `Extension` 的功能有:
|
|
||||||
|
|
||||||
- `validate`: 对于事件的来源适配器或 bot 选择是否接受响应
|
|
||||||
- `output_converter`: 输出信息的自定义转换方法
|
|
||||||
- `message_provider`: 从传入事件中自定义提取消息的方法
|
|
||||||
- `receive_provider`: 对传入的消息 (Message 或 UniMessage) 的额外处理
|
|
||||||
- `context_provider`: 对命令上下文的额外处理
|
|
||||||
- `permission_check`: 命令对消息解析并确认头部匹配(即确认选择响应)时对发送者的权限判断
|
|
||||||
- `parse_wrapper`: 对命令解析结果的额外处理
|
|
||||||
- `send_wrapper`: 对发送的消息 (Message 或 UniMessage) 的额外处理
|
|
||||||
- `before_catch`: 自定义依赖注入的绑定确认函数
|
|
||||||
- `catch`: 自定义依赖注入处理函数
|
|
||||||
- `post_init`: 响应器创建后对命令对象的额外处理
|
|
||||||
|
|
||||||
例如内置的 `DiscordSlashExtension`,其可自动将 Alconna 对象翻译成 slash 指令并注册,且将收到的指令交互事件转为指令供命令解析:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from nonebot_plugin_alconna import Match, on_alconna
|
|
||||||
from nonebot_plugin_alconna.builtins.extensions.discord import DiscordSlashExtension
|
|
||||||
|
|
||||||
|
|
||||||
alc = Alconna(
|
|
||||||
["/"],
|
|
||||||
"permission",
|
|
||||||
Subcommand("add", Args["plugin", str]["priority?", int]),
|
|
||||||
Option("remove", Args["plugin", str]["time?", int]),
|
|
||||||
meta=CommandMeta(description="权限管理"),
|
|
||||||
)
|
|
||||||
|
|
||||||
matcher = on_alconna(alc, extensions=[DiscordSlashExtension()])
|
|
||||||
|
|
||||||
@matcher.assign("add")
|
|
||||||
async def add(plugin: Match[str], priority: Match[int]):
|
|
||||||
await matcher.finish(f"added {plugin.result} with {priority.result if priority.available else 0}")
|
|
||||||
|
|
||||||
@matcher.assign("remove")
|
|
||||||
async def remove(plugin: Match[str], time: Match[int]):
|
|
||||||
await matcher.finish(f"removed {plugin.result} with {time.result if time.available else -1}")
|
|
||||||
```
|
|
||||||
|
|
||||||
目前插件提供了 4 个内置的 `Extension`,它们在 `nonebot_plugin_alconna.builtins.extensions` 下:
|
|
||||||
|
|
||||||
- `ReplyRecordExtension`: 将消息事件中的回复暂存在 extension 中,使得解析用的消息不带回复信息,同时可以在后续的处理中获取回复信息。
|
|
||||||
- `DiscordSlashExtension`: 将 Alconna 的命令自动转换为 Discord 的 Slash Command,并将 Slash Command 的交互事件转换为消息交给 Alconna 处理。
|
|
||||||
- `MarkdownOutputExtension`: 将 Alconna 的自动输出转换为 Markdown 格式
|
|
||||||
- `TelegramSlashExtension`: 将 Alconna 的命令注册在 Telegram 上以获得提示。
|
|
||||||
|
|
||||||
:::tip
|
|
||||||
|
|
||||||
全局的 Extension 可延迟加载 (即若有全局拓展加载于部分 AlconnaMatcher 之后,这部分响应器会被追加拓展)
|
|
||||||
|
|
||||||
:::
|
|
||||||
|
|
||||||
## 补全会话
|
|
||||||
|
|
||||||
补全会话基于 [`半自动补全`](./command.md#半自动补全),用于指令参数缺失或参数错误时给予交互式提示,类似于 `got-reject`:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from nonebot_plugin_alconna import Alconna, Args, Field, At, on_alconna
|
|
||||||
|
|
||||||
alc = Alconna(
|
|
||||||
"添加教师",
|
|
||||||
Args["name", str, Field(completion=lambda: "请输入姓名")],
|
|
||||||
Args["phone", int, Field(completion=lambda: "请输入手机号")],
|
|
||||||
Args["at", [str, At], Field(completion=lambda: "请输入教师号")],
|
|
||||||
)
|
|
||||||
|
|
||||||
cmd = on_alconna(alc, comp_config={"lite": True}, skip_for_unmatch=False)
|
|
||||||
|
|
||||||
@cmd.handle()
|
|
||||||
async def handle(result: Arparma):
|
|
||||||
cmd.finish("添加成功")
|
|
||||||
```
|
|
||||||
|
|
||||||
此时,当用户输入 `添加教师` 时,会自动提示用户输入姓名,手机号和教师号,用户输入后会自动进入下一个提示:
|
|
||||||
|
|
||||||
<Messenger
|
|
||||||
msgs={[
|
|
||||||
{ position: "right", msg: "添加教师" },
|
|
||||||
{ position: "left", msg: "以下是建议的输入: \n- name: 请输入姓名" },
|
|
||||||
{ position: "right", msg: "foo" },
|
|
||||||
{ position: "left", msg: "以下是建议的输入: \n- phone: 请输入手机号" },
|
|
||||||
{ position: "right", msg: "12345" },
|
|
||||||
{ position: "left", msg: "以下是建议的输入: \n- at: 请输入教师号" },
|
|
||||||
{ position: "right", msg: "@me" },
|
|
||||||
{ position: "left", msg: "添加成功" },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
补全会话配置如下:
|
|
||||||
|
|
||||||
```python
|
|
||||||
class CompConfig(TypedDict):
|
|
||||||
tab: NotRequired[str]
|
|
||||||
"""用于切换提示的指令的名称"""
|
|
||||||
enter: NotRequired[str]
|
|
||||||
"""用于输入提示的指令的名称"""
|
|
||||||
exit: NotRequired[str]
|
|
||||||
"""用于退出会话的指令的名称"""
|
|
||||||
timeout: NotRequired[int]
|
|
||||||
"""超时时间"""
|
|
||||||
hide_tabs: NotRequired[bool]
|
|
||||||
"""是否隐藏所有提示"""
|
|
||||||
hides: NotRequired[Set[Literal["tab", "enter", "exit"]]]
|
|
||||||
"""隐藏的指令"""
|
|
||||||
disables: NotRequired[Set[Literal["tab", "enter", "exit"]]]
|
|
||||||
"""禁用的指令"""
|
|
||||||
lite: NotRequired[bool]
|
|
||||||
"""是否使用简洁版本的补全会话(相当于同时配置 disables、hides、hide_tabs)"""
|
|
||||||
```
|
|
||||||
|
|
||||||
## 内置插件
|
|
||||||
|
|
||||||
类似于 Nonebot 本身提供的内置插件,`nonebot_plugin_alconna` 提供了两个内置插件:`echo` 和 `help`。
|
|
||||||
|
|
||||||
你可以用本插件的 `load_builtin_plugin(s)` 来加载它们:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from nonebot_plugin_alconna import load_builtin_plugins
|
|
||||||
|
|
||||||
load_builtin_plugins("echo", "help")
|
|
||||||
```
|
|
||||||
|
|
||||||
其中 `help` 仅能列出所有 Alconna 指令。
|
|
||||||
|
|
||||||
<Messenger
|
|
||||||
msgs={[
|
|
||||||
{ position: "right", msg: "/帮助" },
|
|
||||||
{
|
|
||||||
position: "left",
|
|
||||||
msg: "# 当前可用的命令有:\n 0 /echo : echo 指令\n 1 /help : 显示所有命令帮助\n# 输入'命令名 -h|--help' 查看特定命令的语法",
|
|
||||||
},
|
|
||||||
{ position: "right", msg: "/echo [图片]" },
|
|
||||||
{ position: "left", msg: "[图片]" },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
@@ -1,590 +0,0 @@
|
|||||||
---
|
|
||||||
sidebar_position: 5
|
|
||||||
description: 通用消息组件
|
|
||||||
---
|
|
||||||
|
|
||||||
import Tabs from "@theme/Tabs";
|
|
||||||
import TabItem from "@theme/TabItem";
|
|
||||||
|
|
||||||
# 通用消息组件
|
|
||||||
|
|
||||||
`uniseg` 模块属于 `nonebot-plugin-alconna` 的子插件,其提供了一套通用的消息组件,用于在 `nonebot-plugin-alconna` 下构建通用消息。
|
|
||||||
|
|
||||||
## 通用消息段
|
|
||||||
|
|
||||||
适配器下的消息段标注会匹配适配器特定的 `MessageSegment`, 而通用消息段与适配器消息段的区别在于:
|
|
||||||
通用消息段会匹配多个适配器中相似类型的消息段,并返回 `uniseg` 模块中定义的 [`Segment` 模型](https://nonebot.dev/docs/next/best-practice/alconna/utils#%E9%80%9A%E7%94%A8%E6%B6%88%E6%81%AF%E6%AE%B5), 以达到**跨平台接收消息**的作用。
|
|
||||||
|
|
||||||
`nonebot-plugin-alconna.uniseg` 提供了类似 `MessageSegment` 的通用消息段,并可在 `Alconna` 下直接标注使用:
|
|
||||||
|
|
||||||
```python
|
|
||||||
class Segment:
|
|
||||||
"""基类标注"""
|
|
||||||
children: List["Segment"]
|
|
||||||
|
|
||||||
class Text(Segment):
|
|
||||||
"""Text对象, 表示一类文本元素"""
|
|
||||||
text: str
|
|
||||||
styles: Dict[Tuple[int, int], List[str]]
|
|
||||||
|
|
||||||
class At(Segment):
|
|
||||||
"""At对象, 表示一类提醒某用户的元素"""
|
|
||||||
flag: Literal["user", "role", "channel"]
|
|
||||||
target: str
|
|
||||||
display: Optional[str]
|
|
||||||
|
|
||||||
class AtAll(Segment):
|
|
||||||
"""AtAll对象, 表示一类提醒所有人的元素"""
|
|
||||||
here: bool
|
|
||||||
|
|
||||||
class Emoji(Segment):
|
|
||||||
"""Emoji对象, 表示一类表情元素"""
|
|
||||||
id: str
|
|
||||||
name: Optional[str]
|
|
||||||
|
|
||||||
class Media(Segment):
|
|
||||||
url: Optional[str]
|
|
||||||
id: Optional[str]
|
|
||||||
path: Optional[Union[str, Path]]
|
|
||||||
raw: Optional[Union[bytes, BytesIO]]
|
|
||||||
mimetype: Optional[str]
|
|
||||||
name: str
|
|
||||||
|
|
||||||
to_url: ClassVar[Optional[MediaToUrl]]
|
|
||||||
|
|
||||||
class Image(Media):
|
|
||||||
"""Image对象, 表示一类图片元素"""
|
|
||||||
|
|
||||||
class Audio(Media):
|
|
||||||
"""Audio对象, 表示一类音频元素"""
|
|
||||||
duration: Optional[int]
|
|
||||||
|
|
||||||
class Voice(Media):
|
|
||||||
"""Voice对象, 表示一类语音元素"""
|
|
||||||
duration: Optional[int]
|
|
||||||
|
|
||||||
class Video(Media):
|
|
||||||
"""Video对象, 表示一类视频元素"""
|
|
||||||
|
|
||||||
class File(Segment):
|
|
||||||
"""File对象, 表示一类文件元素"""
|
|
||||||
id: str
|
|
||||||
name: Optional[str]
|
|
||||||
|
|
||||||
class Reply(Segment):
|
|
||||||
"""Reply对象,表示一类回复消息"""
|
|
||||||
id: str
|
|
||||||
"""此处不一定是消息ID,可能是其他ID,如消息序号等"""
|
|
||||||
msg: Optional[Union[Message, str]]
|
|
||||||
origin: Optional[Any]
|
|
||||||
|
|
||||||
class Reference(Segment):
|
|
||||||
"""Reference对象,表示一类引用消息。转发消息 (Forward) 也属于此类"""
|
|
||||||
id: Optional[str]
|
|
||||||
"""此处不一定是消息ID,可能是其他ID,如消息序号等"""
|
|
||||||
children: List[Union[RefNode, CustomNode]]
|
|
||||||
|
|
||||||
class Hyper(Segment):
|
|
||||||
"""Hyper对象,表示一类超级消息。如卡片消息、ark消息、小程序等"""
|
|
||||||
format: Literal["xml", "json"]
|
|
||||||
raw: Optional[str]
|
|
||||||
content: Optional[Union[dict, list]]
|
|
||||||
|
|
||||||
class Other(Segment):
|
|
||||||
"""其他 Segment"""
|
|
||||||
origin: MessageSegment
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
:::tip
|
|
||||||
|
|
||||||
或许你注意到了 `Segment` 上有一个 `children` 属性。
|
|
||||||
|
|
||||||
这是因为在 [`Satori`](https://satori.js.org/zh-CN/) 协议的规定下,一类元素可以用其子元素来代表一类兼容性消息
|
|
||||||
(例如,qq 的商场表情在某些平台上可以用图片代替)。
|
|
||||||
|
|
||||||
为此,本插件提供了两种方式来表达 "获取子元素" 的方法:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from nonebot_plugin_alconna.builtins.uniseg.chronocat import MarketFace
|
|
||||||
from nonebot_plugin_alconna import Args, Image, Alconna, select, select_first
|
|
||||||
|
|
||||||
# 表示这个指令需要的图片要么直接是 Image 要么是在 MarketFace 元素内的 Image
|
|
||||||
alc1 = Alconna("make_meme", Args["img", [Image, Image.from_(MarketFace)]])
|
|
||||||
|
|
||||||
# 表示这个指令需要的图片会在目标元素下进行搜索,将所有符合 Image 的元素选出来并将第一个作为结果
|
|
||||||
alc2 = Alconna("make_meme", Args["img", select(Image, index=0)]) # 也可以使用 select_first(Image)
|
|
||||||
```
|
|
||||||
|
|
||||||
:::
|
|
||||||
|
|
||||||
## 通用消息序列
|
|
||||||
|
|
||||||
`nonebot-plugin-alconna.uniseg` 同时提供了一个类似于 `Message` 的 `UniMessage` 类型,其元素为经过通用标注转换后的通用消息段。
|
|
||||||
|
|
||||||
你可以用如下方式获取 `UniMessage`:
|
|
||||||
|
|
||||||
<Tabs groupId="get_unimsg">
|
|
||||||
<TabItem value="depend" label="使用依赖注入">
|
|
||||||
|
|
||||||
通过提供的 `UniversalMessage` 或 `UniMsg` 依赖注入器来获取 `UniMessage`。
|
|
||||||
|
|
||||||
```python
|
|
||||||
from nonebot_plugin_alconna.uniseg import UniMsg, At, Reply
|
|
||||||
|
|
||||||
|
|
||||||
matcher = on_xxx(...)
|
|
||||||
|
|
||||||
@matcher.handle()
|
|
||||||
async def _(msg: UniMsg):
|
|
||||||
reply = msg[Reply, 0]
|
|
||||||
print(reply.origin)
|
|
||||||
if msg.has(At):
|
|
||||||
ats = msg.get(At)
|
|
||||||
print(ats)
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
</TabItem>
|
|
||||||
<TabItem value="method" label="使用 UniMessage.generate">
|
|
||||||
|
|
||||||
注意,`generate` 方法在响应器以外的地方如果不传入 `event` 与 `bot` 则无法处理 reply。
|
|
||||||
|
|
||||||
```python
|
|
||||||
from nonebot import Message, EventMessage
|
|
||||||
from nonebot_plugin_alconna.uniseg import UniMessage
|
|
||||||
|
|
||||||
|
|
||||||
matcher = on_xxx(...)
|
|
||||||
|
|
||||||
@matcher.handle()
|
|
||||||
async def _(message: Message = EventMessage()):
|
|
||||||
msg = await UniMessage.generate(message=message)
|
|
||||||
msg1 = UniMessage.generate_without_reply(message=message)
|
|
||||||
```
|
|
||||||
|
|
||||||
</TabItem>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
不仅如此,你还可以通过 `UniMessage` 的 `export` 与 `send` 方法来**跨平台发送消息**。
|
|
||||||
|
|
||||||
`UniMessage.export` 会通过传入的 `bot: Bot` 参数,或上下文中的 `Bot` 对象读取适配器信息,并使用对应的生成方法把通用消息转为适配器对应的消息序列:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from nonebot import Bot, on_command
|
|
||||||
from nonebot_plugin_alconna.uniseg import Image, UniMessage
|
|
||||||
|
|
||||||
|
|
||||||
test = on_command("test")
|
|
||||||
|
|
||||||
@test.handle()
|
|
||||||
async def handle_test():
|
|
||||||
await test.send(await UniMessage(Image(path="path/to/img")).export())
|
|
||||||
```
|
|
||||||
|
|
||||||
除此之外 `UniMessage.send` 方法基于 `UniMessage.export` 并调用各适配器下的发送消息方法,返回一个 `Receipt` 对象,用于修改/撤回消息:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from nonebot import Bot, on_command
|
|
||||||
from nonebot_plugin_alconna.uniseg import UniMessage
|
|
||||||
|
|
||||||
|
|
||||||
test = on_command("test")
|
|
||||||
|
|
||||||
@test.handle()
|
|
||||||
async def handle():
|
|
||||||
receipt = await UniMessage.text("hello!").send(at_sender=True, reply_to=True)
|
|
||||||
await receipt.recall(delay=1)
|
|
||||||
```
|
|
||||||
|
|
||||||
而在 `AlconnaMatcher` 下,`got`, `send`, `reject` 等可以发送消息的方法皆支持使用 `UniMessage`,不需要手动调用 export 方法:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from arclet.alconna import Alconna, Args
|
|
||||||
from nonebot_plugin_alconna import Match, AlconnaMatcher, on_alconna
|
|
||||||
from nonebot_plugin_alconna.uniseg import At, UniMessage
|
|
||||||
|
|
||||||
|
|
||||||
test_cmd = on_alconna(Alconna("test", Args["target?", At]))
|
|
||||||
|
|
||||||
@test_cmd.handle()
|
|
||||||
async def tt_h(matcher: AlconnaMatcher, target: Match[At]):
|
|
||||||
if target.available:
|
|
||||||
matcher.set_path_arg("target", target.result)
|
|
||||||
|
|
||||||
@test_cmd.got_path("target", prompt="请输入目标")
|
|
||||||
async def tt(target: At):
|
|
||||||
await test_cmd.send(UniMessage([target, "\ndone."]))
|
|
||||||
```
|
|
||||||
|
|
||||||
:::caution
|
|
||||||
|
|
||||||
在响应器以外的地方,除非启用了 `alconna_apply_fetch_targets` 配置项,否则 `bot` 参数必须手动传入。
|
|
||||||
|
|
||||||
:::
|
|
||||||
|
|
||||||
### 构造
|
|
||||||
|
|
||||||
如同 `Message`, `UniMessage` 可以传入单个字符串/消息段,或可迭代的字符串/消息段:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from nonebot_plugin_alconna.uniseg import UniMessage, At
|
|
||||||
|
|
||||||
|
|
||||||
msg = UniMessage("Hello")
|
|
||||||
msg1 = UniMessage(At("user", "124"))
|
|
||||||
msg2 = UniMessage(["Hello", At("user", "124")])
|
|
||||||
```
|
|
||||||
|
|
||||||
`UniMessage` 上同时存在便捷方法,令其可以链式地添加消息段:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from nonebot_plugin_alconna.uniseg import UniMessage, At, Image
|
|
||||||
|
|
||||||
|
|
||||||
msg = UniMessage.text("Hello").at("124").image(path="/path/to/img")
|
|
||||||
assert msg == UniMessage(
|
|
||||||
["Hello", At("user", "124"), Image(path="/path/to/img")]
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 拼接消息
|
|
||||||
|
|
||||||
`str`、`UniMessage`、`Segment` 对象之间可以直接相加,相加均会返回一个新的 `UniMessage` 对象:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# 消息序列与消息段相加
|
|
||||||
UniMessage("text") + Text("text")
|
|
||||||
# 消息序列与字符串相加
|
|
||||||
UniMessage([Text("text")]) + "text"
|
|
||||||
# 消息序列与消息序列相加
|
|
||||||
UniMessage("text") + UniMessage([Text("text")])
|
|
||||||
# 字符串与消息序列相加
|
|
||||||
"text" + UniMessage([Text("text")])
|
|
||||||
# 消息段与消息段相加
|
|
||||||
Text("text") + Text("text")
|
|
||||||
# 消息段与字符串相加
|
|
||||||
Text("text") + "text"
|
|
||||||
# 消息段与消息序列相加
|
|
||||||
Text("text") + UniMessage([Text("text")])
|
|
||||||
# 字符串与消息段相加
|
|
||||||
"text" + Text("text")
|
|
||||||
```
|
|
||||||
|
|
||||||
如果需要在当前消息序列后直接拼接新的消息段,可以使用 `Message.append`、`Message.extend` 方法,或者使用自加:
|
|
||||||
|
|
||||||
```python
|
|
||||||
msg = UniMessage([Text("text")])
|
|
||||||
# 自加
|
|
||||||
msg += "text"
|
|
||||||
msg += Text("text")
|
|
||||||
msg += UniMessage([Text("text")])
|
|
||||||
# 附加
|
|
||||||
msg.append(Text("text"))
|
|
||||||
# 扩展
|
|
||||||
msg.extend([Text("text")])
|
|
||||||
```
|
|
||||||
|
|
||||||
### 使用消息模板
|
|
||||||
|
|
||||||
`UniMessage.template` 同样类似于 `Message.template`,可以用于格式化消息,大体用法参考 [消息模板](../../tutorial/message#使用消息模板)。
|
|
||||||
|
|
||||||
这里额外说明 `UniMessage.template` 的拓展控制符
|
|
||||||
|
|
||||||
相比 `Message`,UniMessage 对于 `{:XXX}` 做了另一类拓展。其能够识别例如 At(xxx, yyy) 或 Emoji(aaa, bbb)的字符串并执行
|
|
||||||
|
|
||||||
以 At(...) 为例:
|
|
||||||
|
|
||||||
```python title=使用通用消息段的拓展控制符
|
|
||||||
>>> from nonebot_plugin_alconna.uniseg import UniMessage
|
|
||||||
>>> UniMessage.template("{:At(user, target)}").format(target="123")
|
|
||||||
UniMessage(At("user", "123"))
|
|
||||||
>>> UniMessage.template("{:At(type=user, target=id)}").format(id="123")
|
|
||||||
UniMessage(At("user", "123"))
|
|
||||||
>>> UniMessage.template("{:At(type=user, target=123)}").format()
|
|
||||||
UniMessage(At("user", "123"))
|
|
||||||
```
|
|
||||||
|
|
||||||
而在 `AlconnaMatcher` 中,`{:XXX}` 更进一步地提供了获取 `event` 和 `bot` 中的属性的功能:
|
|
||||||
|
|
||||||
```python title=在AlconnaMatcher中使用通用消息段的拓展控制符
|
|
||||||
from arclet.alconna import Alconna, Args
|
|
||||||
from nonebot_plugin_alconna import At, Match, UniMessage, AlconnaMatcher, on_alconna
|
|
||||||
|
|
||||||
|
|
||||||
test_cmd = on_alconna(Alconna("test", Args["target?", At]))
|
|
||||||
|
|
||||||
@test_cmd.handle()
|
|
||||||
async def tt_h(matcher: AlconnaMatcher, target: Match[At]):
|
|
||||||
if target.available:
|
|
||||||
matcher.set_path_arg("target", target.result)
|
|
||||||
|
|
||||||
@test_cmd.got_path(
|
|
||||||
"target",
|
|
||||||
prompt=UniMessage.template("{:At(user, $event.get_user_id())} 请确认目标")
|
|
||||||
)
|
|
||||||
async def tt():
|
|
||||||
await test_cmd.send(
|
|
||||||
UniMessage.template("{:At(user, $event.get_user_id())} 已确认目标为 {target}")
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
另外也有 `$message_id` 与 `$target` 两个特殊值。
|
|
||||||
|
|
||||||
### 检查消息段
|
|
||||||
|
|
||||||
我们可以通过 `in` 运算符或消息序列的 `has` 方法来:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# 是否存在消息段
|
|
||||||
At("user", "1234") in message
|
|
||||||
# 是否存在指定类型的消息段
|
|
||||||
At in message
|
|
||||||
```
|
|
||||||
|
|
||||||
我们还可以使用 `only` 方法来检查消息中是否仅包含指定的消息段:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# 是否都为 "test"
|
|
||||||
message.only("test")
|
|
||||||
# 是否仅包含指定类型的消息段
|
|
||||||
message.only(Text)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 获取消息纯文本
|
|
||||||
|
|
||||||
类似于 `Message.extract_plain_text()`,用于获取通用消息的纯文本:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from nonebot_plugin_alconna.uniseg import UniMessage, At
|
|
||||||
|
|
||||||
|
|
||||||
# 提取消息纯文本字符串
|
|
||||||
assert UniMessage(
|
|
||||||
[At("user", "1234"), "text"]
|
|
||||||
).extract_plain_text() == "text"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 遍历
|
|
||||||
|
|
||||||
通用消息序列继承自 `List[Segment]` ,因此可以使用 `for` 循环遍历消息段:
|
|
||||||
|
|
||||||
```python
|
|
||||||
for segment in message: # type: Segment
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
### 过滤、索引与切片
|
|
||||||
|
|
||||||
消息序列对列表的索引与切片进行了增强,在原有列表 `int` 索引与 `slice` 切片的基础上,支持 `type` 过滤索引与切片:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from nonebot_plugin_alconna.uniseg import UniMessage, At, Text, Reply
|
|
||||||
|
|
||||||
|
|
||||||
message = UniMessage(
|
|
||||||
[
|
|
||||||
Reply(...),
|
|
||||||
"text1",
|
|
||||||
At("user", "1234"),
|
|
||||||
"text2"
|
|
||||||
]
|
|
||||||
)
|
|
||||||
# 索引
|
|
||||||
message[0] == Reply(...)
|
|
||||||
# 切片
|
|
||||||
message[0:2] == UniMessage([Reply(...), Text("text1")])
|
|
||||||
# 类型过滤
|
|
||||||
message[At] == Message([At("user", "1234")])
|
|
||||||
# 类型索引
|
|
||||||
message[At, 0] == At("user", "1234")
|
|
||||||
# 类型切片
|
|
||||||
message[Text, 0:2] == UniMessage([Text("text1"), Text("text2")])
|
|
||||||
```
|
|
||||||
|
|
||||||
我们也可以通过消息序列的 `include`、`exclude` 方法进行类型过滤:
|
|
||||||
|
|
||||||
```python
|
|
||||||
message.include(Text, At)
|
|
||||||
message.exclude(Reply)
|
|
||||||
```
|
|
||||||
|
|
||||||
同样的,消息序列对列表的 `index`、`count` 方法也进行了增强,可以用于索引指定类型的消息段:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# 指定类型首个消息段索引
|
|
||||||
message.index(Text) == 1
|
|
||||||
# 指定类型消息段数量
|
|
||||||
message.count(Text) == 2
|
|
||||||
```
|
|
||||||
|
|
||||||
此外,消息序列添加了一个 `get` 方法,可以用于获取指定类型指定个数的消息段:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# 获取指定类型指定个数的消息段
|
|
||||||
message.get(Text, 1) == UniMessage([Text("test1")])
|
|
||||||
```
|
|
||||||
|
|
||||||
## 消息发送
|
|
||||||
|
|
||||||
前面提到,通用消息可用 `UniMessage.send` 发送自身:
|
|
||||||
|
|
||||||
```python
|
|
||||||
async def send(
|
|
||||||
self,
|
|
||||||
target: Union[Event, Target, None] = None,
|
|
||||||
bot: Optional[Bot] = None,
|
|
||||||
fallback: bool = True,
|
|
||||||
at_sender: Union[str, bool] = False,
|
|
||||||
reply_to: Union[str, bool] = False,
|
|
||||||
) -> Receipt:
|
|
||||||
```
|
|
||||||
|
|
||||||
实际上,`UniMessage` 同时提供了获取消息事件 id 与消息发送对象的方法:
|
|
||||||
|
|
||||||
<Tabs groupId="get_unimsg">
|
|
||||||
<TabItem value="depend" label="使用依赖注入">
|
|
||||||
|
|
||||||
通过提供的 `MessageTarget`, `MessageId` 或 `MsgTarget`, `MsgId` 依赖注入器来获取消息事件 id 与消息发送对象。
|
|
||||||
|
|
||||||
```python
|
|
||||||
from nonebot_plugin_alconna.uniseg import MessageId, MsgTarget
|
|
||||||
|
|
||||||
|
|
||||||
matcher = on_xxx(...)
|
|
||||||
|
|
||||||
@matcher.handle()
|
|
||||||
asycn def _(target: MsgTarget, msg_id: MessageId):
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
</TabItem>
|
|
||||||
<TabItem value="method" label="使用 UniMessage 的方法">
|
|
||||||
|
|
||||||
```python
|
|
||||||
from nonebot import Event, Bot
|
|
||||||
from nonebot_plugin_alconna.uniseg import UniMessage, Target
|
|
||||||
|
|
||||||
|
|
||||||
matcher = on_xxx(...)
|
|
||||||
|
|
||||||
@matcher.handle()
|
|
||||||
asycn def _(bot: Bot, event: Event):
|
|
||||||
target: Target = UniMessage.get_target(event, bot)
|
|
||||||
msg_id: str = UniMessage.get_message_id(event, bot)
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
</TabItem>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
`send`, `get_target`, `get_message_id` 中与 `event`, `bot` 相关的参数都会尝试从上下文中获取对象。
|
|
||||||
|
|
||||||
### 消息发送对象
|
|
||||||
|
|
||||||
消息发送对象是用来描述响应消息时的发送对象或者主动发送消息时的目标对象的对象,它包含了以下属性:
|
|
||||||
|
|
||||||
```python
|
|
||||||
class Target:
|
|
||||||
id: str
|
|
||||||
"""目标id;若为群聊则为group_id或者channel_id,若为私聊则为user_id"""
|
|
||||||
parent_id: str
|
|
||||||
"""父级id;若为频道则为guild_id,其他情况下可能为空字符串(例如 Feishu 下可作为部门 id)"""
|
|
||||||
channel: bool
|
|
||||||
"""是否为频道,仅当目标平台符合频道概念时"""
|
|
||||||
private: bool
|
|
||||||
"""是否为私聊"""
|
|
||||||
source: str
|
|
||||||
"""可能的事件id"""
|
|
||||||
self_id: Union[str, None]
|
|
||||||
"""机器人id,若为 None 则 Bot 对象会随机选择"""
|
|
||||||
selector: Union[Callable[[Bot], Awaitable[bool]], None]
|
|
||||||
"""选择器,用于在多个 Bot 对象中选择特定 Bot"""
|
|
||||||
extra: Dict[str, Any]
|
|
||||||
"""额外信息,用于适配器扩展"""
|
|
||||||
```
|
|
||||||
|
|
||||||
其构造时需要如下参数:
|
|
||||||
|
|
||||||
- `id` 为目标id;若为群聊则为 group_id 或者 channel_id,若为私聊则为user_id
|
|
||||||
- `parent_id` 为父级id;若为频道则为 guild_id,其他情况下可能为空字符串(例如 Feishu 下可作为部门 id)
|
|
||||||
- `channel` 为是否为频道,仅当目标平台符合频道概念时
|
|
||||||
- `private` 为是否为私聊
|
|
||||||
- `source` 为可能的事件id
|
|
||||||
- `self_id` 为机器人id,若为 None 则 Bot 对象会随机选择
|
|
||||||
- `selector` 为选择器,用于在多个 Bot 对象中选择特定 Bot
|
|
||||||
- `scope` 为适配器范围,用于传入内置的特定选择器
|
|
||||||
- `adapter` 为适配器名称,若为 None 则需要明确指定 Bot 对象
|
|
||||||
- `platform` 为平台名称,仅当目标适配器存在多个平台时使用
|
|
||||||
- `extra` 为额外信息,用于适配器扩展
|
|
||||||
|
|
||||||
通过 `Target` 对象,我们可以在 `UniMessage.send` 中指定发送对象:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from nonebot_plugin_alconna.uniseg import UniMessage, MsgTarget, Target, SupportScope
|
|
||||||
|
|
||||||
|
|
||||||
matcher = on_xxx(...)
|
|
||||||
|
|
||||||
@matcher.handle()
|
|
||||||
async def _(target: MsgTarget):
|
|
||||||
await UniMessage("Hello!").send(target=target)
|
|
||||||
target1 = Target("xxxx", scope=SupportScope.qq_client)
|
|
||||||
await UniMessage("Hello!").send(target=target1)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 主动发送消息
|
|
||||||
|
|
||||||
`UniMessage.send` 也可以用于主动发送消息:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from nonebot_plugin_alconna.uniseg import UniMessage, Target, SupportScope
|
|
||||||
from nonebot import get_driver
|
|
||||||
|
|
||||||
|
|
||||||
driver = get_driver()
|
|
||||||
|
|
||||||
@driver.on_startup
|
|
||||||
async def on_startup():
|
|
||||||
target = Target("xxxx", scope=SupportScope.qq_client)
|
|
||||||
await UniMessage("Hello!").send(target=target)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 自定义消息段
|
|
||||||
|
|
||||||
`uniseg` 提供了部分方法来允许用户自定义 Segment 的序列化和反序列化:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
from nonebot.adapters import Bot
|
|
||||||
from nonebot.adapters import MessageSegment as BaseMessageSegment
|
|
||||||
from nonebot.adapters.satori import Custom, Message, MessageSegment
|
|
||||||
|
|
||||||
from nonebot_plugin_alconna.uniseg.builder import MessageBuilder
|
|
||||||
from nonebot_plugin_alconna.uniseg.exporter import MessageExporter
|
|
||||||
from nonebot_plugin_alconna.uniseg import Segment, custom_handler, custom_register
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class MarketFace(Segment):
|
|
||||||
tabId: str
|
|
||||||
faceId: str
|
|
||||||
key: str
|
|
||||||
|
|
||||||
|
|
||||||
@custom_register(MarketFace, "chronocat:marketface")
|
|
||||||
def mfbuild(builder: MessageBuilder, seg: BaseMessageSegment):
|
|
||||||
if not isinstance(seg, Custom):
|
|
||||||
raise ValueError("MarketFace can only be built from Satori Message")
|
|
||||||
return MarketFace(**seg.data)(*builder.generate(seg.children))
|
|
||||||
|
|
||||||
|
|
||||||
@custom_handler(MarketFace)
|
|
||||||
async def mfexport(exporter: MessageExporter, seg: MarketFace, bot: Bot, fallback: bool):
|
|
||||||
if exporter.get_message_type() is Message:
|
|
||||||
return MessageSegment("chronocat:marketface", seg.data)(await exporter.export(seg.children, bot, fallback))
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
具体而言,你可以使用 `custom_register` 来增加一个从 MessageSegment 到 Segment 的处理方法;使用 `custom_handler` 来增加一个从 Segment 到 MessageSegment 的处理方法。
|
|
||||||
@@ -1,206 +0,0 @@
|
|||||||
---
|
|
||||||
sidebar_position: 0
|
|
||||||
description: 在商店发布自己的插件
|
|
||||||
---
|
|
||||||
|
|
||||||
# 发布插件
|
|
||||||
|
|
||||||
import Tabs from "@theme/Tabs";
|
|
||||||
import TabItem from "@theme/TabItem";
|
|
||||||
|
|
||||||
NoneBot 为开发者提供了分享插件给大家使用的方式——商店。本章节将会介绍如何将我们写好的插件发布到商店。
|
|
||||||
|
|
||||||
:::tip 提示
|
|
||||||
本章节仅包含插件发布流程指导,插件开发请查阅前述章节。
|
|
||||||
:::
|
|
||||||
|
|
||||||
## 准备工作
|
|
||||||
|
|
||||||
### 插件命名规范
|
|
||||||
|
|
||||||
NoneBot 插件使用下述命名规范:
|
|
||||||
|
|
||||||
- 对于**项目名**,建议统一以 `nonebot-plugin-` 开头,之后为拟定的插件名字,词间用横杠 `-` 分隔;
|
|
||||||
- **项目名**用于代码仓库名称、PyPI 包的发布名称等;
|
|
||||||
- 本文使用 `nonebot-plugin-{your-plugin-name}` 为例。
|
|
||||||
- 对于**模块名**,建议与**项目名**一致,但词间用下划线 `_` 分隔,即统一以 `nonebot_plugin_` 开头,之后为拟定的插件名字;
|
|
||||||
- **模块名**用于程序导入使用,应为插件文件(夹)的名称;
|
|
||||||
- 本文使用 `nonebot_plugin_{your_plugin_name}` 为例。
|
|
||||||
|
|
||||||
### 项目结构
|
|
||||||
|
|
||||||
:::tip 提示
|
|
||||||
本段所述的项目结构仅作推荐,不做强制要求,保证实际可用性即可。
|
|
||||||
:::
|
|
||||||
|
|
||||||
插件程序本身结构可参考[插件结构](../tutorial/create-plugin.md#插件结构)一节,唯一区别在于,插件包可以直接处于项目顶层。
|
|
||||||
|
|
||||||
插件项目的一种组织结构如下:
|
|
||||||
|
|
||||||
```tree
|
|
||||||
📦 nonebot-plugin-{your-plugin-name}
|
|
||||||
├── 📂 nonebot_plugin_{your_plugin_name}
|
|
||||||
│ ├── 📜 __init__.py
|
|
||||||
│ └── 📜 config.py
|
|
||||||
├── 📜 pyproject.toml
|
|
||||||
└── 📜 README.md
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 第三方项目模板
|
|
||||||
|
|
||||||
一些社区用户可能会分享自己制作的项目模板方便大家使用,如:
|
|
||||||
|
|
||||||
- [A-kirami/nonebot-plugin-template](https://github.com/A-kirami/nonebot-plugin-template)
|
|
||||||
- [RF-Tar-Railt/nonebot-plugin-template](https://github.com/RF-Tar-Railt/nonebot-plugin-template)
|
|
||||||
- [fllesser/nonebot-plugin-template](https://github.com/fllesser/nonebot-plugin-template)
|
|
||||||
|
|
||||||
:::tip 提示
|
|
||||||
本文档**不保证**第三方模板的适用性。
|
|
||||||
|
|
||||||
根据项目模板提供的使用指导补全/修改相应内容后上传到 GitHub 即可。
|
|
||||||
:::
|
|
||||||
|
|
||||||
### 插件依赖
|
|
||||||
|
|
||||||
本段指导填写插件依赖,避免不正确的依赖信息导致插件无法正常工作。
|
|
||||||
|
|
||||||
依赖填写的基本原则:程序直接导入了什么第三方库,就添加什么第三方包依赖;能用哪些第三方库的特性,就根据使用的特性锁定第三方包版本。
|
|
||||||
|
|
||||||
:::caution 注意
|
|
||||||
|
|
||||||
1. 插件需要添加 `nonebot2` 为依赖以避免“幽灵依赖”;
|
|
||||||
2. 插件需要将使用的适配器加入依赖列表,如:使用 OneBot 适配器的插件应添加 `nonebot-adapter-onebot` 依赖;
|
|
||||||
3. 由于 `nonebot` 是指 `nonebot1` **而非** `nonebot2`,因此要注意**不要**将 `nonebot` 添加为插件的依赖,以免造成冲突;
|
|
||||||
4. 尽可能避免使用 `==` 锁定单一版本,增强与其它插件的兼容性。
|
|
||||||
|
|
||||||
:::
|
|
||||||
|
|
||||||
### 填写插件元数据
|
|
||||||
|
|
||||||
请注意,插件发布要求**必须**填写元数据才能通过审核。
|
|
||||||
|
|
||||||
下面是一个示例:
|
|
||||||
|
|
||||||
```python title=nonebot_plugin_{your_plugin_name}/__init__.py
|
|
||||||
from nonebot.plugin import PluginMetadata
|
|
||||||
|
|
||||||
from .config import Config
|
|
||||||
|
|
||||||
__plugin_meta__ = PluginMetadata(
|
|
||||||
name="{插件名称}",
|
|
||||||
description="{插件介绍}",
|
|
||||||
usage="{插件用法}",
|
|
||||||
|
|
||||||
type="{插件分类}",
|
|
||||||
# 发布必填,当前有效类型有:`library`(为其他插件编写提供功能),`application`(向机器人用户提供功能)。
|
|
||||||
|
|
||||||
homepage="{项目主页}",
|
|
||||||
# 发布必填。
|
|
||||||
|
|
||||||
config=Config,
|
|
||||||
# 插件配置项类,如无需配置可不填写。
|
|
||||||
|
|
||||||
supported_adapters={"~onebot.v11", "~telegram"},
|
|
||||||
# 支持的适配器集合,其中 `~` 在此处代表前缀 `nonebot.adapters.`,其余适配器亦按此格式填写。
|
|
||||||
# 若插件可以保证兼容所有适配器(即仅使用基本适配器功能)可不填写,否则应该列出插件支持的适配器。
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
:::caution 注意
|
|
||||||
`__plugin_meta__` 变量**必须**处于插件最外层(如 `__init__.py` 中),否则无法正常识别。
|
|
||||||
|
|
||||||
一般做法是在 `__init__.py` 中定义 `__plugin_meta__`。
|
|
||||||
:::
|
|
||||||
|
|
||||||
:::tip 提示
|
|
||||||
带花括号 `{}` 的内容需要自行替换,注意**一定要把原有的花括号去掉**。
|
|
||||||
:::
|
|
||||||
|
|
||||||
### 准备项目主页
|
|
||||||
|
|
||||||
通常可以使用 GitHub 项目页面作为项目主页,在 `README.md` 文件中编写插件介绍等内容。
|
|
||||||
|
|
||||||
内容大致包括:
|
|
||||||
|
|
||||||
- 插件功能介绍
|
|
||||||
- 安装方法(建议至少有 `nb-cli` 方式安装,**不要**使用旧式的 `bot.py` 配置)
|
|
||||||
- 插件配置项(若无可跳过)
|
|
||||||
- 插件设置的触发规则(若无可跳过)
|
|
||||||
- 插件的其它用法(按需编写)
|
|
||||||
|
|
||||||
:::tip 提示
|
|
||||||
可以参考[第三方项目模板](#第三方项目模板)。
|
|
||||||
:::
|
|
||||||
|
|
||||||
### 发布至 [PyPI](https://pypi.org)
|
|
||||||
|
|
||||||
根据选用的构建系统,在项目的 `pyproject.toml` 中填入必要信息后进行构建与发布。
|
|
||||||
|
|
||||||
:::tip 提示
|
|
||||||
不同构建工具的使用可能存在差别。本文仅以 [`pdm`](https://pdm.fming.dev/latest/),
|
|
||||||
[`poetry`](https://python-poetry.org/docs/), [`setuptools`](https://setuptools.pypa.io/en/latest/)
|
|
||||||
构建系统**本地构建与发布**为示例讲解,其余构建/管理工具等和自动化构建的用法请读者自行探索。
|
|
||||||
:::
|
|
||||||
|
|
||||||
<Tabs groupId="publishMethod">
|
|
||||||
<TabItem value="poetry" label="Poetry" default>
|
|
||||||
|
|
||||||
```bash
|
|
||||||
poetry publish --build # 构建并发布
|
|
||||||
|
|
||||||
# 等效于以下两个命令
|
|
||||||
poetry build # 只构建
|
|
||||||
poetry publish # 只发布先前的构建
|
|
||||||
```
|
|
||||||
|
|
||||||
</TabItem>
|
|
||||||
|
|
||||||
<TabItem value="pdm" label="PDM" default>
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pdm publish # 构建并发布
|
|
||||||
|
|
||||||
# 等效于以下两个命令
|
|
||||||
pdm build # 只构建
|
|
||||||
pdm publish --no-build # 只发布先前的构建
|
|
||||||
```
|
|
||||||
|
|
||||||
</TabItem>
|
|
||||||
|
|
||||||
<TabItem value="setuptools" label="Setuptools (PEP 517)" default>
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install build twine # 安装通用构建与发布工具
|
|
||||||
|
|
||||||
python -m build --sdist --wheel . # 只构建
|
|
||||||
twine upload dist/* # 只发布先前的构建
|
|
||||||
```
|
|
||||||
|
|
||||||
</TabItem>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
:::tip 提示
|
|
||||||
发布前建议自行测试构建包是否可用,避免遗漏代码文件或资源文件等问题。
|
|
||||||
:::
|
|
||||||
|
|
||||||
## 商店审核
|
|
||||||
|
|
||||||
### 提交申请
|
|
||||||
|
|
||||||
完成在 PyPI 的插件发布流程后,前往[商店](/store/plugins)页面,切换到插件页签,点击 **发布插件** 按钮。
|
|
||||||
|
|
||||||
在弹出的插件信息提交表单内,填入您所要发布的相应插件信息。请注意,如果插件需要必要配置项才能正常导入,请在“插件配置项”中填写必要的内容(请勿填写密钥等敏感信息)。
|
|
||||||
|
|
||||||
完成填写后,点击 **发布** 按钮,这将自动跳转 NoneBot 仓库内的“发布插件”页面。确认信息无误后点击页面下方的 `Submit new issue` 按钮进行最终提交即可。
|
|
||||||
|
|
||||||
### 等待插件审核
|
|
||||||
|
|
||||||
插件发布 Issue 创建后,将会经过 **NoneFlow Bot** 的自动前置检查,以确保插件信息正确无误、插件能被正确加载。
|
|
||||||
|
|
||||||
:::tip 提示
|
|
||||||
若插件检查未通过或信息有误,**不必**关闭当前 Issue。只需更新插件并上传到 PyPI/修改信息后勾选插件测试勾选框即可重新触发插件检查。
|
|
||||||
:::
|
|
||||||
|
|
||||||
之后,NoneBot 的维护者和一些插件开发者会初步检查插件代码,帮助减少该插件的问题。
|
|
||||||
|
|
||||||
完成这些步骤后,您的插件将会被自动合并到[商店](/store/plugins),而您也将成为 [**NoneBot 贡献者**](https://github.com/nonebot/nonebot2/graphs/contributors)的一员。
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
---
|
|
||||||
sidebar_position: 2
|
|
||||||
description: 配置编辑器以获得最佳体验
|
|
||||||
---
|
|
||||||
|
|
||||||
# 编辑器支持
|
|
||||||
|
|
||||||
框架基于 [PEP484](https://www.python.org/dev/peps/pep-0484/)、[PEP 561](https://www.python.org/dev/peps/pep-0561/)、[PEP8](https://www.python.org/dev/peps/pep-0008/) 等规范进行开发并且**拥有完整类型注解**。框架使用 Pyright(Pylance)工具进行类型检查,确保代码可以被编辑器正确解析。
|
|
||||||
|
|
||||||
## 编辑器推荐配置
|
|
||||||
|
|
||||||
### Visual Studio Code
|
|
||||||
|
|
||||||
在 Visual Studio Code 中,可以使用 Pylance Language Server 并启用 `Type Checking` 配置以达到最佳开发体验。
|
|
||||||
|
|
||||||
1. 在 VSCode 插件视图搜索并安装 `Python (ms-python.python)` 和 `Pylance (ms-python.vscode-pylance)` 插件。
|
|
||||||
2. 修改 VSCode 配置
|
|
||||||
在 VSCode 设置视图搜索配置项 `Python: Language Server` 并将其值设置为 `Pylance`,搜索配置项 `Python > Analysis: Type Checking Mode` 并将其值设置为 `basic`。
|
|
||||||
|
|
||||||
或者向项目 `.vscode` 文件夹中配置文件添加以下内容:
|
|
||||||
|
|
||||||
```json title=settings.json
|
|
||||||
{
|
|
||||||
"python.languageServer": "Pylance",
|
|
||||||
"python.analysis.typeCheckingMode": "basic"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 其他
|
|
||||||
|
|
||||||
欢迎提交 Pull Request 添加其他编辑器配置推荐。点击左下角 `Edit this page` 前往编辑。
|
|
||||||
@@ -46,15 +46,15 @@ __plugin_meta__ = PluginMetadata(
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
我们可以看到,插件元数据 `PluginMetadata` 有三个基本属性:插件名称、插件描述、插件使用方法。除此之外,还有几个可选的属性(具体填写见[发布插件](../developer/plugin-publishing.mdx#填写插件元数据)章节):
|
我们可以看到,插件元数据 `PluginMetadata` 有三个基本属性:插件名称、插件描述、插件使用方法。除此之外,还有几个可选的属性(具体填写见[发布插件](../developer/plugin-publishing.mdx#插件元数据)章节):
|
||||||
|
|
||||||
- `type`:插件类别,发布插件必填。当前有效类别有:`library`(为其他插件编写提供功能),`application`(向机器人用户提供功能);
|
- `type`:插件类别,发布插件必填。当前有效类别有:`library`(为其他插件编写提供功能),`application`(向机器人用户提供功能);
|
||||||
- `homepage`:插件项目主页,发布插件必填;
|
- `homepage`:插件项目主页,发布插件必填;
|
||||||
- `config`:插件的[配置类](../appendices/config.mdx#插件配置),如无配置类可不填;
|
- `config`:插件的[配置类](../appendices/config.mdx#插件配置),发布插件时如有配置类则必须填写;
|
||||||
- `supported_adapters`:支持的适配器模块名集合,若插件可以保证兼容所有适配器(即仅使用基本适配器功能)可不填写;
|
- `supported_adapters`:支持的适配器模块名集合,若插件只使用了 NoneBot 基本抽象,应显式填写 `None`;
|
||||||
- `extra`:一个字典,可以用于存储任意信息。其他插件可以通过约定 `extra` 字典的键名来达成收集某些特殊信息的目的。
|
- `extra`:一个字典,可以用于存储任意信息。其他插件可以通过约定 `extra` 字典的键名来达成收集某些特殊信息的目的。
|
||||||
|
|
||||||
请注意,这里的**插件名称**是供使用者或机器人用户查看的,与插件索引名称无关。**插件索引名称(插件模块名称)**仅用于 NoneBot 插件系统**内部索引**。
|
请注意,这里的**插件名称**是供使用者或机器人用户查看的人类可读名称,与插件索引名称无关。**插件索引名称(插件模块名称)**仅用于 NoneBot 插件系统**内部索引**。
|
||||||
|
|
||||||
## 获取插件信息
|
## 获取插件信息
|
||||||
|
|
||||||
|
|||||||
@@ -219,6 +219,7 @@ jobs:
|
|||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
type=sha
|
type=sha
|
||||||
|
type=raw,value=latest
|
||||||
|
|
||||||
- name: Build and Publish
|
- name: Build and Publish
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v4
|
||||||
|
|||||||
@@ -8,7 +8,13 @@ description: 在商店发布自己的插件
|
|||||||
import Tabs from "@theme/Tabs";
|
import Tabs from "@theme/Tabs";
|
||||||
import TabItem from "@theme/TabItem";
|
import TabItem from "@theme/TabItem";
|
||||||
|
|
||||||
NoneBot 为开发者提供了分享插件给大家使用的方式——商店。本章节将会介绍如何将我们写好的插件发布到商店。
|
NoneBot 为开发者提供了分享插件的官方商店。本指南囊括**从创建项目到发布到 PyPI,最终提交商店审核**的全过程。
|
||||||
|
|
||||||
|
:::warning 警告
|
||||||
|
如果你的插件只是满足自用需求,则完全可以选择**不发布插件**。发布插件**不是**使用插件的必要条件。
|
||||||
|
|
||||||
|
NoneBot 社区对于插件有一定质量要求,对于不符合要求的插件,社区成员将会要求修改,直至符合要求后才能通过审核;如果长期未更新修改,社区将会关闭当前请求,之后如需发布请重新提交发布插件请求。相应的要求会在本章节以下部分介绍。
|
||||||
|
:::
|
||||||
|
|
||||||
:::tip 提示
|
:::tip 提示
|
||||||
本章节仅包含插件发布流程指导,插件开发请查阅前述章节。
|
本章节仅包含插件发布流程指导,插件开发请查阅前述章节。
|
||||||
@@ -30,7 +36,7 @@ NoneBot 插件使用下述命名规范:
|
|||||||
### 项目结构
|
### 项目结构
|
||||||
|
|
||||||
:::tip 提示
|
:::tip 提示
|
||||||
本段所述的项目结构仅作推荐,不做强制要求,保证实际可用性即可。
|
本段所述的项目结构仅作推荐,不做强制要求。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
插件程序本身结构可参考[插件结构](../tutorial/create-plugin.md#插件结构)一节,唯一区别在于,插件包可以直接处于项目顶层。
|
插件程序本身结构可参考[插件结构](../tutorial/create-plugin.md#插件结构)一节,唯一区别在于,插件包可以直接处于项目顶层。
|
||||||
@@ -46,98 +52,267 @@ NoneBot 插件使用下述命名规范:
|
|||||||
└── 📜 README.md
|
└── 📜 README.md
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 第三方项目模板
|
功能开发可以在 `__init__.py` 中进行或在包内部创建其他模块并在 `__init__.py` 中导入。
|
||||||
|
|
||||||
一些社区用户可能会分享自己制作的项目模板方便大家使用,如:
|
### 从项目模板开始
|
||||||
|
|
||||||
|
为降低新手门槛,我们提供三条清晰、完整、可复制的发布路径。
|
||||||
|
|
||||||
|
:::tip 提示
|
||||||
|
你只需选择一条与你习惯一致的路径,**完整跟随即可成功发布**。无需在不同工具间切换或猜测配置。
|
||||||
|
:::
|
||||||
|
|
||||||
|
NoneBot 生态目前有如下插件项目模板:
|
||||||
|
|
||||||
- [A-kirami/nonebot-plugin-template](https://github.com/A-kirami/nonebot-plugin-template)
|
|
||||||
- [RF-Tar-Railt/nonebot-plugin-template](https://github.com/RF-Tar-Railt/nonebot-plugin-template)
|
- [RF-Tar-Railt/nonebot-plugin-template](https://github.com/RF-Tar-Railt/nonebot-plugin-template)
|
||||||
|
|
||||||
|
此路径使用 **PDM** 项目管理器,符合 PEP 621 标准,自动化程度高。
|
||||||
|
|
||||||
- [fllesser/nonebot-plugin-template](https://github.com/fllesser/nonebot-plugin-template)
|
- [fllesser/nonebot-plugin-template](https://github.com/fllesser/nonebot-plugin-template)
|
||||||
|
|
||||||
|
此路径使用 **uv** 项目管理器和 **PoeThePoet** 任务运行器,构建速度快,适合追求效率的开发者。
|
||||||
|
|
||||||
|
- [A-kirami/nonebot-plugin-template](https://github.com/A-kirami/nonebot-plugin-template)
|
||||||
|
|
||||||
|
此路径使用 **Poetry** 项目管理器,适合熟悉传统 Python 生态的开发者。
|
||||||
|
|
||||||
|
#### 1. 创建项目
|
||||||
|
|
||||||
|
1. 访问上述三个模板之一。
|
||||||
|
2. 点击 **“Use this template”** → **“Create a new repository”**。
|
||||||
|
3. 仓库名称填写:`nonebot-plugin-{your-plugin-name}`(此部分以 `nonebot-plugin-weather` 为例)。
|
||||||
|
4. 点击 **“Create repository from template”**。
|
||||||
|
|
||||||
|
#### 2. 配置发布权限
|
||||||
|
|
||||||
|
1. 进入新仓库 → **Settings** → **Actions** → **General**。
|
||||||
|
2. 在 **Workflow permissions** 下,勾选 **“Read and write permissions”** → 点击 **Save**。
|
||||||
|
|
||||||
|
#### 3. 全局替换项目信息
|
||||||
|
|
||||||
|
在仓库中点击 **“Add file”** → **“Create new file”**,创建一个空文件 `LICENSE`,选择开源协议并提交(此操作会触发工作流)。
|
||||||
|
|
||||||
|
然后在本地克隆仓库,使用编辑器对以下内容进行**全局替换**:
|
||||||
|
|
||||||
:::tip 提示
|
:::tip 提示
|
||||||
本文档**不保证**第三方模板的适用性。
|
此部分以“天气插件”为例,实际的替换内容应该根据你所创建的插件名称等相应调整。
|
||||||
|
|
||||||
根据项目模板提供的使用指导补全/修改相应内容后上传到 GitHub 即可。
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
### 插件依赖
|
| 原内容 | 替换为 |
|
||||||
|
| ------------------------------ | ---------------------------------- |
|
||||||
|
| `nonebot-plugin-template` | `nonebot-plugin-weather` |
|
||||||
|
| `nonebot_plugin_template` | `nonebot_plugin_weather` |
|
||||||
|
| `<your_plugin_humanized_name>` | `天气查询` |
|
||||||
|
| `<your_plugin_description>` | `查询指定城市的实时天气与未来预报` |
|
||||||
|
| `<your_github>` | `你的GitHub用户名` |
|
||||||
|
| `<your_email>` | `你的邮箱` |
|
||||||
|
|
||||||
本段指导填写插件依赖,避免不正确的依赖信息导致插件无法正常工作。
|
#### 4. 安装依赖与开发
|
||||||
|
|
||||||
依赖填写的基本原则:程序直接导入了什么第三方库,就添加什么第三方包依赖;能用哪些第三方库的特性,就根据使用的特性锁定第三方包版本。
|
<Tabs groupId="publish-path" defaultValue="pdm" values={[
|
||||||
|
{label: 'PDM + RF-Tar-Railt 模板', value: 'pdm'},
|
||||||
|
{label: 'uv + fllesser 模板', value: 'uv'},
|
||||||
|
{label: 'Poetry + A-kirami 模板', value: 'poetry'},
|
||||||
|
]}>
|
||||||
|
<TabItem value="pdm" label="PDM + RF-Tar-Railt 模板">
|
||||||
|
|
||||||
:::caution 注意
|
```bash
|
||||||
|
# 安装 PDM(若未安装)
|
||||||
|
curl -sSL https://pdm-project.org/install-pdm.py | python3 -
|
||||||
|
|
||||||
1. 插件需要添加 `nonebot2` 为依赖以避免“幽灵依赖”;
|
# 安装项目依赖(自动创建虚拟环境)
|
||||||
2. 插件需要将使用的适配器加入依赖列表,如:使用 OneBot 适配器的插件应添加 `nonebot-adapter-onebot` 依赖;
|
pdm sync
|
||||||
3. 由于 `nonebot` 是指 `nonebot1` **而非** `nonebot2`,因此要注意**不要**将 `nonebot` 添加为插件的依赖,以免造成冲突;
|
|
||||||
4. 尽可能避免使用 `==` 锁定单一版本,增强与其它插件的兼容性。
|
|
||||||
|
|
||||||
:::
|
# 添加新依赖(如 httpx)
|
||||||
|
pdm add httpx
|
||||||
### 填写插件元数据
|
|
||||||
|
|
||||||
请注意,插件发布要求**必须**填写元数据才能通过审核。
|
|
||||||
|
|
||||||
下面是一个示例:
|
|
||||||
|
|
||||||
```python title=nonebot_plugin_{your_plugin_name}/__init__.py
|
|
||||||
from nonebot.plugin import PluginMetadata
|
|
||||||
|
|
||||||
from .config import Config
|
|
||||||
|
|
||||||
__plugin_meta__ = PluginMetadata(
|
|
||||||
name="{插件名称}",
|
|
||||||
description="{插件介绍}",
|
|
||||||
usage="{插件用法}",
|
|
||||||
|
|
||||||
type="{插件分类}",
|
|
||||||
# 发布必填,当前有效类型有:`library`(为其他插件编写提供功能),`application`(向机器人用户提供功能)。
|
|
||||||
|
|
||||||
homepage="{项目主页}",
|
|
||||||
# 发布必填。
|
|
||||||
|
|
||||||
config=Config,
|
|
||||||
# 插件配置项类,如无需配置可不填写。
|
|
||||||
|
|
||||||
supported_adapters={"~onebot.v11", "~telegram"},
|
|
||||||
# 支持的适配器集合,其中 `~` 在此处代表前缀 `nonebot.adapters.`,其余适配器亦按此格式填写。
|
|
||||||
# 若插件可以保证兼容所有适配器(即仅使用基本适配器功能)可不填写,否则应该列出插件支持的适配器。
|
|
||||||
)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
:::caution 注意
|
</TabItem>
|
||||||
`__plugin_meta__` 变量**必须**处于插件最外层(如 `__init__.py` 中),否则无法正常识别。
|
|
||||||
|
|
||||||
一般做法是在 `__init__.py` 中定义 `__plugin_meta__`。
|
<TabItem value="uv" label="uv + fllesser 模板">
|
||||||
:::
|
|
||||||
|
|
||||||
:::tip 提示
|
```bash
|
||||||
带花括号 `{}` 的内容需要自行替换,注意**一定要把原有的花括号去掉**。
|
# 安装 uv(Windows)
|
||||||
:::
|
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
|
||||||
|
|
||||||
### 准备项目主页
|
# 安装 uv(macOS/Linux)
|
||||||
|
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||||
|
|
||||||
通常可以使用 GitHub 项目页面作为项目主页,在 `README.md` 文件中编写插件介绍等内容。
|
# 安装所有依赖(含 dev)
|
||||||
|
uv sync --all-groups -p 3.12
|
||||||
|
|
||||||
内容大致包括:
|
# 添加新依赖
|
||||||
|
uv add httpx
|
||||||
|
```
|
||||||
|
|
||||||
- 插件功能介绍
|
</TabItem>
|
||||||
- 安装方法(建议至少有 `nb-cli` 方式安装,**不要**使用旧式的 `bot.py` 配置)
|
|
||||||
- 插件配置项(若无可跳过)
|
|
||||||
- 插件设置的触发规则(若无可跳过)
|
|
||||||
- 插件的其它用法(按需编写)
|
|
||||||
|
|
||||||
:::tip 提示
|
<TabItem value="poetry" label="Poetry + A-kirami 模板">
|
||||||
可以参考[第三方项目模板](#第三方项目模板)。
|
|
||||||
:::
|
|
||||||
|
|
||||||
### 发布至 [PyPI](https://pypi.org)
|
```bash
|
||||||
|
# 安装 Poetry(推荐方式)
|
||||||
|
curl -sSL https://install.python-poetry.org | python3 -
|
||||||
|
|
||||||
|
# 安装项目依赖
|
||||||
|
poetry install
|
||||||
|
|
||||||
|
# 添加新依赖
|
||||||
|
poetry add httpx
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
#### 5. 更新版本并发布
|
||||||
|
|
||||||
|
<Tabs
|
||||||
|
groupId="publish-path-bump"
|
||||||
|
defaultValue="bump-my-version"
|
||||||
|
values={[
|
||||||
|
{ label: "使用 bump-my-version", value: "bump-my-version" },
|
||||||
|
{ label: "使用项目管理器", value: "bump-manager" },
|
||||||
|
{ label: "手动更新版本", value: "bump-manual" },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<TabItem value="bump-my-version" label="使用 bump-my-version">
|
||||||
|
|
||||||
|
[bump-my-version](https://github.com/callowayproject/bump-my-version) 是一个功能强大、可配置的 Python 项目版本更新工具,支持自动提交到 Git 等 VCS。
|
||||||
|
|
||||||
|
<Tabs groupId="publish-path" defaultValue="pdm" values={[
|
||||||
|
{label: 'PDM + RF-Tar-Railt 模板', value: 'pdm'},
|
||||||
|
{label: 'uv + fllesser 模板', value: 'uv'},
|
||||||
|
{label: 'Poetry + A-kirami 模板', value: 'poetry'},
|
||||||
|
]}>
|
||||||
|
<TabItem value="pdm" label="PDM + RF-Tar-Railt 模板">
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装 bump-my-version
|
||||||
|
pdm add --dev bump-my-version
|
||||||
|
|
||||||
|
# 更新 patch 版本
|
||||||
|
pdm run bump patch
|
||||||
|
|
||||||
|
# 推送 tag 触发发布
|
||||||
|
git push origin --tags
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem value="uv" label="uv + fllesser 模板">
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 更新 patch 版本
|
||||||
|
uv run poe bump patch
|
||||||
|
|
||||||
|
# 推送 tag 触发发布
|
||||||
|
git push origin --tags
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem value="poetry" label="Poetry + A-kirami 模板">
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装 bump-my-version
|
||||||
|
poetry add --dev bump-my-version
|
||||||
|
|
||||||
|
# 更新 patch 版本
|
||||||
|
poetry run bump patch
|
||||||
|
|
||||||
|
# 推送 tag 触发发布
|
||||||
|
git push origin --tags
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
<TabItem value="bump-manager" label="使用项目管理器">
|
||||||
|
|
||||||
|
<Tabs groupId="publish-path" defaultValue="pdm" values={[
|
||||||
|
{label: 'PDM + RF-Tar-Railt 模板', value: 'pdm'},
|
||||||
|
{label: 'uv + fllesser 模板', value: 'uv'},
|
||||||
|
{label: 'Poetry + A-kirami 模板', value: 'poetry'},
|
||||||
|
]}>
|
||||||
|
<TabItem value="pdm" label="PDM + RF-Tar-Railt 模板">
|
||||||
|
|
||||||
|
需要安装 PDM 插件 [pdm-bump](https://github.com/carstencodes/pdm-bump)。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装 pdm-bump
|
||||||
|
pdm self add pdm-bump
|
||||||
|
|
||||||
|
# 更新 patch 版本
|
||||||
|
pdm bump patch
|
||||||
|
|
||||||
|
# 推送 tag 触发发布
|
||||||
|
git push origin --tags
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem value="uv" label="uv + fllesser 模板">
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 更新 patch 版本
|
||||||
|
uv version --bump patch
|
||||||
|
|
||||||
|
# 创建相应提交与标签
|
||||||
|
git add pyproject.toml
|
||||||
|
git commit -m "chore: release v0.1.1" # 替换为实际的版本号
|
||||||
|
git tag v0.1.1 # 替换为实际的版本号
|
||||||
|
|
||||||
|
# 推送 tag 触发发布
|
||||||
|
git push origin --tags
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem value="poetry" label="Poetry + A-kirami 模板">
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 更新版本(自动提交并打标签)
|
||||||
|
poetry version patch
|
||||||
|
|
||||||
|
# 推送 tag 触发发布
|
||||||
|
git push origin --tags
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
<TabItem value="bump-manual" label="手动更新版本">
|
||||||
|
|
||||||
|
手动更新 `pyproject.toml` 中的 `version` 字段,然后推送 tag 触发发布工作流
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add pyproject.toml
|
||||||
|
git commit -m "chore: release v0.1.1" # 替换为实际的版本号
|
||||||
|
git tag v0.1.1 # 替换为实际的版本号
|
||||||
|
git push origin --tags
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
推送 `v*` 标签后,模板提供的 GitHub Actions 工作流将自动构建并发布到 PyPI。
|
||||||
|
|
||||||
|
#### 6. 发布到 [PyPI](https://pypi.org)
|
||||||
|
|
||||||
|
<Tabs groupId="publish-method" defaultValue="template" values={[
|
||||||
|
{label: '使用模板的自动发布工作流', value: 'template'},
|
||||||
|
{label: '手动发布', value: 'manual'},
|
||||||
|
]}>
|
||||||
|
<TabItem value="template" label="使用模板的自动发布工作流">
|
||||||
|
不同模板使用的发布方式可能不同,具体配置流程参考对应模板的详细使用指南。
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem value="manual" label="手动发布">
|
||||||
根据选用的构建系统,在项目的 `pyproject.toml` 中填入必要信息后进行构建与发布。
|
根据选用的构建系统,在项目的 `pyproject.toml` 中填入必要信息后进行构建与发布。
|
||||||
|
|
||||||
:::tip 提示
|
:::tip 提示
|
||||||
不同构建工具的使用可能存在差别。本文仅以 [`pdm`](https://pdm.fming.dev/latest/),
|
不同构建工具的使用可能存在差别。本文仅以 [`pdm`](https://pdm-project.org/zh/latest/),
|
||||||
[`poetry`](https://python-poetry.org/docs/), [`setuptools`](https://setuptools.pypa.io/en/latest/)
|
[`poetry`](https://python-poetry.org/docs/), [`setuptools`](https://setuptools.pypa.io/en/latest/)
|
||||||
构建系统**本地构建与发布**为示例讲解,其余构建/管理工具等和自动化构建的用法请读者自行探索。
|
构建系统**本地构建与发布**为示例讲解,其余构建/管理工具等和自动化构建的用法请读者自行探索。
|
||||||
:::
|
:::
|
||||||
@@ -179,10 +354,187 @@ twine upload dist/* # 只发布先前的构建
|
|||||||
</TabItem>
|
</TabItem>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
:::tip 提示
|
:::tip 提示
|
||||||
发布前建议自行测试构建包是否可用,避免遗漏代码文件或资源文件等问题。
|
发布前建议自行测试构建包是否可用,避免遗漏代码文件或资源文件等问题。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
## 基本要求
|
||||||
|
|
||||||
|
无论你选择哪条路径,以下内容**必须**完成,否则无法通过商店自动检查:
|
||||||
|
|
||||||
|
### 能够正确加载
|
||||||
|
|
||||||
|
插件包必须能够被 NoneBot 正确加载,在商店审核中会通过 **NoneFlow** 自动化加载测试进行。
|
||||||
|
|
||||||
|
#### 依赖其他插件
|
||||||
|
|
||||||
|
如果插件依赖其他插件提供的功能,则**必须**在代码中使用 `require()` 来引入该插件,然后才能 `import` 该插件提供的功能。具体细节参阅[跨插件访问](../advanced/requiring.md)。
|
||||||
|
|
||||||
|
使用示例如下:
|
||||||
|
|
||||||
|
```python title=nonebot_plugin_weather/__init__.py
|
||||||
|
from nonebot import require
|
||||||
|
|
||||||
|
require("nonebot_plugin_apscheduler")
|
||||||
|
|
||||||
|
from nonebot_plugin_apscheduler import scheduler
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 不能零配置加载的插件
|
||||||
|
|
||||||
|
如果插件需要必要配置项才能正常导入,则**必须**在商店提交表单中填写必要的配置项内容。
|
||||||
|
|
||||||
|
但一种更好的做法是,**将插件设计为零配置即可加载**(允许缺少必要配置项时插件仍能正常导入,但不执行需要相应配置项的功能),尤其是对于一些必要配置含有敏感信息(如密钥、Token、API Key 等)的插件。这样可以避免在商店提交表单时填写敏感信息的风险。
|
||||||
|
|
||||||
|
### 插件元数据
|
||||||
|
|
||||||
|
插件包**必须**填写元数据才能通过 **NoneFlow** 自动化检查。
|
||||||
|
|
||||||
|
下面是一个示例:
|
||||||
|
|
||||||
|
```python title=nonebot_plugin_weather/__init__.py
|
||||||
|
from nonebot.plugin import PluginMetadata
|
||||||
|
|
||||||
|
from .config import Config
|
||||||
|
|
||||||
|
__plugin_meta__ = PluginMetadata(
|
||||||
|
# 基本信息(必填)
|
||||||
|
name="天气查询", # 插件名称
|
||||||
|
description="查询指定城市的实时天气与未来预报", # 插件介绍
|
||||||
|
usage="发送【天气 城市名】获取天气信息", # 插件用法
|
||||||
|
|
||||||
|
# 发布额外信息
|
||||||
|
type="application", # 插件分类
|
||||||
|
# 发布必填,当前有效类型有:`library`(为其他插件编写提供功能),`application`(向机器人用户提供功能)。
|
||||||
|
|
||||||
|
homepage="https://github.com/你的用户名/nonebot-plugin-weather",
|
||||||
|
# 发布必填。
|
||||||
|
|
||||||
|
config=Config,
|
||||||
|
# 插件配置项类,如果有配置类则必须填写。
|
||||||
|
|
||||||
|
supported_adapters={"~onebot.v11"},
|
||||||
|
# 支持的适配器集合,其中 `~` 在此处代表前缀 `nonebot.adapters.`,其余适配器亦按此格式填写。
|
||||||
|
# 若插件只使用了 NoneBot 基本抽象,应显式填写 None,否则应该列出插件支持的适配器。
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
:::caution 注意
|
||||||
|
`__plugin_meta__` 变量**必须**处于插件最外层(如 `__init__.py` 中),否则无法正常识别。
|
||||||
|
|
||||||
|
一般做法是在 `__init__.py` 中定义 `__plugin_meta__`。
|
||||||
|
:::
|
||||||
|
|
||||||
|
#### 继承其他插件支持的适配器
|
||||||
|
|
||||||
|
如果你的插件依赖于其他插件提供的支持功能,而其他插件可能支持更少的适配器,这时就应该使用
|
||||||
|
[inherit_supported_adapters()](../api/plugin/load#inherit-supported-adapters) 函数来继承其他插件支持的适配器。
|
||||||
|
|
||||||
|
示例用法如下:
|
||||||
|
|
||||||
|
```python title=nonebot_plugin_weather/__init__.py
|
||||||
|
from nonebot import require
|
||||||
|
from nonebot.plugin import PluginMetadata, inherit_supported_adapters
|
||||||
|
|
||||||
|
from .config import Config
|
||||||
|
|
||||||
|
require("nonebot_plugin_alconna") # 必须先 require 才能被 inherit_supported_adapters 处理
|
||||||
|
|
||||||
|
__plugin_meta__ = PluginMetadata(
|
||||||
|
name="天气查询",
|
||||||
|
description="查询指定城市的实时天气与未来预报",
|
||||||
|
usage="发送【天气 城市名】获取天气信息",
|
||||||
|
type="application",
|
||||||
|
homepage="https://github.com/你的用户名/nonebot-plugin-weather",
|
||||||
|
config=Config,
|
||||||
|
|
||||||
|
supported_adapters=inherit_supported_adapters("nonebot_plugin_alconna"),
|
||||||
|
# 继承 nonebot_plugin_alconna 插件的适配器支持列表
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 准备项目主页
|
||||||
|
|
||||||
|
通常可以使用 GitHub 项目页面作为项目主页,在 `README.md` 文件中编写插件介绍等内容。
|
||||||
|
|
||||||
|
内容大致包括:
|
||||||
|
|
||||||
|
- 插件功能介绍;
|
||||||
|
- 安装方法
|
||||||
|
- **必须**有 NB-CLI 方式安装
|
||||||
|
- 可选依赖可以给出其他安装方式
|
||||||
|
- **不得**使用旧式的 `bot.py` 配置
|
||||||
|
- 插件配置项(如 `Config` 类字段,若无可跳过)
|
||||||
|
- 插件设置的触发规则(若无可跳过)
|
||||||
|
- 插件的其它用法(按需编写)
|
||||||
|
- 效果图、权限说明(按需编写)
|
||||||
|
|
||||||
|
## 质量要求
|
||||||
|
|
||||||
|
以下内容**强烈建议**完成,否则社区成员将会要求修改:
|
||||||
|
|
||||||
|
### 依赖管理原则
|
||||||
|
|
||||||
|
- **必须**包含 `nonebot2`。
|
||||||
|
- **必须**将插件直接使用的适配器加入依赖列表,如:使用 OneBot 适配器的插件应添加 `nonebot-adapter-onebot` 依赖;
|
||||||
|
- **禁止**使用 `==` 锁定单一版本,使用 `>=` 或 `~=`。
|
||||||
|
- **禁止**添加 `nonebot`(V1)作为依赖。
|
||||||
|
- 所有在代码中 `import` 的第三方库,必须在 `pyproject.toml` 的 `dependencies` 中列出。
|
||||||
|
|
||||||
|
### 避免误用同步操作
|
||||||
|
|
||||||
|
NoneBot 是一个异步框架,插件中**禁止**使用任何可能阻塞事件循环的同步操作,例如:
|
||||||
|
|
||||||
|
- 同步 HTTP 请求(如 `requests` 库);
|
||||||
|
|
||||||
|
**推荐**操作(以 `httpx` 为例):
|
||||||
|
|
||||||
|
```python
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
response = await client.get("https://api.example.com/data") # 异步操作,不阻塞机器人
|
||||||
|
```
|
||||||
|
|
||||||
|
**禁止**操作:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import requests
|
||||||
|
|
||||||
|
requests.get("https://api.example.com/data") # 同步操作,会阻塞机器人
|
||||||
|
```
|
||||||
|
|
||||||
|
- 其他可能长时间运行阻塞事件循环的操作。
|
||||||
|
|
||||||
|
### 本地文件存储
|
||||||
|
|
||||||
|
如果插件需要在本地存储数据、配置或缓存文件,**必须**使用 [`nonebot-plugin-localstore`](https://github.com/nonebot/plugin-localstore) 管理,具体细节参阅[本地存储](../best-practice/data-storing.md)章节。
|
||||||
|
|
||||||
|
参考示例:
|
||||||
|
|
||||||
|
```python title=nonebot_plugin_weather/__init__.py
|
||||||
|
from pathlib import Path
|
||||||
|
from nonebot import require
|
||||||
|
require("nonebot_plugin_localstore")
|
||||||
|
|
||||||
|
import nonebot_plugin_localstore as store
|
||||||
|
|
||||||
|
# 获取插件缓存文件(夹)路径
|
||||||
|
weather_cache_dir: Path = store.get_plugin_cache_dir()
|
||||||
|
weather_cache_file: Path = store.get_plugin_cache_file("cache.json")
|
||||||
|
|
||||||
|
# 获取插件配置文件(夹)路径
|
||||||
|
weather_config_dir: Path = store.get_plugin_config_dir()
|
||||||
|
weather_config_file: Path = store.get_plugin_config_file("config.toml")
|
||||||
|
|
||||||
|
# 获取插件数据文件(夹)路径
|
||||||
|
weather_data_dir: Path = store.get_plugin_data_dir()
|
||||||
|
weather_data_file: Path = store.get_plugin_data_file("resource-index.json")
|
||||||
|
```
|
||||||
|
|
||||||
## 商店审核
|
## 商店审核
|
||||||
|
|
||||||
### 提交申请
|
### 提交申请
|
||||||
@@ -201,6 +553,6 @@ twine upload dist/* # 只发布先前的构建
|
|||||||
若插件检查未通过或信息有误,**不必**关闭当前 Issue。只需更新插件并上传到 PyPI/修改信息后勾选插件测试勾选框即可重新触发插件检查。
|
若插件检查未通过或信息有误,**不必**关闭当前 Issue。只需更新插件并上传到 PyPI/修改信息后勾选插件测试勾选框即可重新触发插件检查。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
之后,NoneBot 的维护者和一些插件开发者会初步检查插件代码,帮助减少该插件的问题。
|
之后,NoneBot 的维护者和一些志愿者会初步检查插件代码,帮助减少该插件的问题。
|
||||||
|
|
||||||
完成这些步骤后,您的插件将会被自动合并到[商店](/store/plugins),而您也将成为 [**NoneBot 贡献者**](https://github.com/nonebot/nonebot2/graphs/contributors)的一员。
|
完成这些步骤后,您的插件将会被自动合并到[商店](/store/plugins),而您也将成为 [**NoneBot 贡献者**](https://github.com/nonebot/nonebot2/graphs/contributors)的一员。
|
||||||
|
|||||||
@@ -48,8 +48,10 @@ options:
|
|||||||
|
|
||||||
```tree title=Project
|
```tree title=Project
|
||||||
📦 awesome-bot
|
📦 awesome-bot
|
||||||
|
├── 📂 .venv
|
||||||
├── 📂 awesome_bot
|
├── 📂 awesome_bot
|
||||||
│ └── 📂 plugins
|
│ └── 📂 plugins
|
||||||
|
├── 📜 .env.prod
|
||||||
├── 📜 pyproject.toml
|
├── 📜 pyproject.toml
|
||||||
└── 📜 README.md
|
└── 📜 README.md
|
||||||
```
|
```
|
||||||
@@ -91,18 +93,20 @@ options:
|
|||||||
$ nb plugin create
|
$ nb plugin create
|
||||||
[?] 插件名称: weather
|
[?] 插件名称: weather
|
||||||
[?] 使用嵌套插件? (y/N) N
|
[?] 使用嵌套插件? (y/N) N
|
||||||
[?] 输出目录: awesome_bot/plugins
|
[?] 请输入插件存储位置: awesome_bot/plugins
|
||||||
```
|
```
|
||||||
|
|
||||||
`nb-cli` 会在 `awesome_bot/plugins` 目录下创建一个名为 `weather` 的文件夹,其中包含的文件将在稍后章节中用到。
|
`nb-cli` 会在 `awesome_bot/plugins` 目录下创建一个名为 `weather` 的文件夹,其中包含的文件将在稍后章节中用到。
|
||||||
|
|
||||||
```tree title=Project
|
```tree title=Project
|
||||||
📦 awesome-bot
|
📦 awesome-bot
|
||||||
|
├── 📂 .venv
|
||||||
├── 📂 awesome_bot
|
├── 📂 awesome_bot
|
||||||
│ └── 📂 plugins
|
│ └── 📂 plugins
|
||||||
| └── 📂 foo
|
| └── 📂 weather
|
||||||
| ├── 📜 __init__.py
|
| ├── 📜 __init__.py
|
||||||
| └── 📜 config.py
|
| └── 📜 config.py
|
||||||
|
├── 📜 .env.prod
|
||||||
├── 📜 pyproject.toml
|
├── 📜 pyproject.toml
|
||||||
└── 📜 README.md
|
└── 📜 README.md
|
||||||
```
|
```
|
||||||
@@ -189,12 +193,16 @@ nonebot.load_from_json("plugin_config.json", encoding="utf-8")
|
|||||||
|
|
||||||
### `load_from_toml`
|
### `load_from_toml`
|
||||||
|
|
||||||
通过 TOML 文件加载插件,是 [`load_all_plugins`](#load_all_plugins) 的 TOML 变种。通过读取 TOML 文件中的 `[tool.nonebot]` Table 中的 `plugins` 和 `plugin_dirs` Array 进行加载。例如:
|
通过 TOML 文件加载插件,是 [`load_all_plugins`](#load_all_plugins) 的 TOML 变种。通过读取 TOML 文件中的 `[tool.nonebot]` Table 中的 `plugin_dirs` Array 与
|
||||||
|
`[tool.nonebot.plugins]` Table 中的多个 Array 进行加载。例如:
|
||||||
|
|
||||||
```toml title=plugin_config.toml
|
```toml title=plugin_config.toml
|
||||||
[tool.nonebot]
|
[tool.nonebot]
|
||||||
plugins = ["path.to.your.plugin"]
|
|
||||||
plugin_dirs = ["path/to/your/plugins"]
|
plugin_dirs = ["path/to/your/plugins"]
|
||||||
|
|
||||||
|
[tool.nonebot.plugins]
|
||||||
|
"@local" = ["path.to.your.plugin"] # 本地插件等非插件商店来源的插件
|
||||||
|
"nonebot-plugin-someplugin" = ["nonebot_plugin_someplugin"] # 插件商店来源的插件
|
||||||
```
|
```
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ import Messenger from "@site/src/components/Messenger";
|
|||||||
顾名思义,“事件处理函数装饰器”是一个[装饰器(decorator)](https://docs.python.org/zh-cn/3/glossary.html#term-decorator),那么它的使用方法也同[函数定义](https://docs.python.org/zh-cn/3/reference/compound_stmts.html#function-definitions)中所展示的包装用法相同。例如:
|
顾名思义,“事件处理函数装饰器”是一个[装饰器(decorator)](https://docs.python.org/zh-cn/3/glossary.html#term-decorator),那么它的使用方法也同[函数定义](https://docs.python.org/zh-cn/3/reference/compound_stmts.html#function-definitions)中所展示的包装用法相同。例如:
|
||||||
|
|
||||||
```python {6-8} title=weather/__init__.py
|
```python {6-8} title=weather/__init__.py
|
||||||
|
from nonebot import on_command
|
||||||
from nonebot.rule import to_me
|
from nonebot.rule import to_me
|
||||||
from nonebot.plugin import on_command
|
|
||||||
|
|
||||||
weather = on_command("天气", rule=to_me(), aliases={"weather", "查天气"}, priority=10, block=True)
|
weather = on_command("天气", rule=to_me(), aliases={"weather", "查天气"}, priority=10, block=True)
|
||||||
|
|
||||||
@@ -46,8 +46,8 @@ async def handle_function():
|
|||||||
事件响应器操作与事件处理函数装饰器类似,通常作为事件响应器 `Matcher` 的[类方法](https://docs.python.org/zh-cn/3/library/functions.html#classmethod)存在,因此事件响应器操作的调用方法也是 `Matcher.func()` 的形式。不过不同的是,事件响应器操作并不是装饰器,因此并不需要@进行标注。
|
事件响应器操作与事件处理函数装饰器类似,通常作为事件响应器 `Matcher` 的[类方法](https://docs.python.org/zh-cn/3/library/functions.html#classmethod)存在,因此事件响应器操作的调用方法也是 `Matcher.func()` 的形式。不过不同的是,事件响应器操作并不是装饰器,因此并不需要@进行标注。
|
||||||
|
|
||||||
```python {8,9} title=weather/__init__.py
|
```python {8,9} title=weather/__init__.py
|
||||||
|
from nonebot import on_command
|
||||||
from nonebot.rule import to_me
|
from nonebot.rule import to_me
|
||||||
from nonebot.plugin import on_command
|
|
||||||
|
|
||||||
weather = on_command("天气", rule=to_me(), aliases={"weather", "查天气"}, priority=10, block=True)
|
weather = on_command("天气", rule=to_me(), aliases={"weather", "查天气"}, priority=10, block=True)
|
||||||
|
|
||||||
|
|||||||
@@ -46,15 +46,15 @@ __plugin_meta__ = PluginMetadata(
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
我们可以看到,插件元数据 `PluginMetadata` 有三个基本属性:插件名称、插件描述、插件使用方法。除此之外,还有几个可选的属性(具体填写见[发布插件](../developer/plugin-publishing.mdx#填写插件元数据)章节):
|
我们可以看到,插件元数据 `PluginMetadata` 有三个基本属性:插件名称、插件描述、插件使用方法。除此之外,还有几个可选的属性(具体填写见[发布插件](../developer/plugin-publishing.mdx#插件元数据)章节):
|
||||||
|
|
||||||
- `type`:插件类别,发布插件必填。当前有效类别有:`library`(为其他插件编写提供功能),`application`(向机器人用户提供功能);
|
- `type`:插件类别,发布插件必填。当前有效类别有:`library`(为其他插件编写提供功能),`application`(向机器人用户提供功能);
|
||||||
- `homepage`:插件项目主页,发布插件必填;
|
- `homepage`:插件项目主页,发布插件必填;
|
||||||
- `config`:插件的[配置类](../appendices/config.mdx#插件配置),如无配置类可不填;
|
- `config`:插件的[配置类](../appendices/config.mdx#插件配置),发布插件时如有配置类则必须填写;
|
||||||
- `supported_adapters`:支持的适配器模块名集合,若插件可以保证兼容所有适配器(即仅使用基本适配器功能)可不填写;
|
- `supported_adapters`:支持的适配器模块名集合,若插件只使用了 NoneBot 基本抽象,应显式填写 `None`;
|
||||||
- `extra`:一个字典,可以用于存储任意信息。其他插件可以通过约定 `extra` 字典的键名来达成收集某些特殊信息的目的。
|
- `extra`:一个字典,可以用于存储任意信息。其他插件可以通过约定 `extra` 字典的键名来达成收集某些特殊信息的目的。
|
||||||
|
|
||||||
请注意,这里的**插件名称**是供使用者或机器人用户查看的,与插件索引名称无关。**插件索引名称(插件模块名称)**仅用于 NoneBot 插件系统**内部索引**。
|
请注意,这里的**插件名称**是供使用者或机器人用户查看的人类可读名称,与插件索引名称无关。**插件索引名称(插件模块名称)**仅用于 NoneBot 插件系统**内部索引**。
|
||||||
|
|
||||||
## 获取插件信息
|
## 获取插件信息
|
||||||
|
|
||||||
|
|||||||
@@ -219,6 +219,7 @@ jobs:
|
|||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
type=sha
|
type=sha
|
||||||
|
type=raw,value=latest
|
||||||
|
|
||||||
- name: Build and Publish
|
- name: Build and Publish
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v4
|
||||||
|
|||||||
@@ -8,7 +8,13 @@ description: 在商店发布自己的插件
|
|||||||
import Tabs from "@theme/Tabs";
|
import Tabs from "@theme/Tabs";
|
||||||
import TabItem from "@theme/TabItem";
|
import TabItem from "@theme/TabItem";
|
||||||
|
|
||||||
NoneBot 为开发者提供了分享插件给大家使用的方式——商店。本章节将会介绍如何将我们写好的插件发布到商店。
|
NoneBot 为开发者提供了分享插件的官方商店。本指南囊括**从创建项目到发布到 PyPI,最终提交商店审核**的全过程。
|
||||||
|
|
||||||
|
:::warning 警告
|
||||||
|
如果你的插件只是满足自用需求,则完全可以选择**不发布插件**。发布插件**不是**使用插件的必要条件。
|
||||||
|
|
||||||
|
NoneBot 社区对于插件有一定质量要求,对于不符合要求的插件,社区成员将会要求修改,直至符合要求后才能通过审核;如果长期未更新修改,社区将会关闭当前请求,之后如需发布请重新提交发布插件请求。相应的要求会在本章节以下部分介绍。
|
||||||
|
:::
|
||||||
|
|
||||||
:::tip 提示
|
:::tip 提示
|
||||||
本章节仅包含插件发布流程指导,插件开发请查阅前述章节。
|
本章节仅包含插件发布流程指导,插件开发请查阅前述章节。
|
||||||
@@ -30,7 +36,7 @@ NoneBot 插件使用下述命名规范:
|
|||||||
### 项目结构
|
### 项目结构
|
||||||
|
|
||||||
:::tip 提示
|
:::tip 提示
|
||||||
本段所述的项目结构仅作推荐,不做强制要求,保证实际可用性即可。
|
本段所述的项目结构仅作推荐,不做强制要求。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
插件程序本身结构可参考[插件结构](../tutorial/create-plugin.md#插件结构)一节,唯一区别在于,插件包可以直接处于项目顶层。
|
插件程序本身结构可参考[插件结构](../tutorial/create-plugin.md#插件结构)一节,唯一区别在于,插件包可以直接处于项目顶层。
|
||||||
@@ -46,98 +52,267 @@ NoneBot 插件使用下述命名规范:
|
|||||||
└── 📜 README.md
|
└── 📜 README.md
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 第三方项目模板
|
功能开发可以在 `__init__.py` 中进行或在包内部创建其他模块并在 `__init__.py` 中导入。
|
||||||
|
|
||||||
一些社区用户可能会分享自己制作的项目模板方便大家使用,如:
|
### 从项目模板开始
|
||||||
|
|
||||||
|
为降低新手门槛,我们提供三条清晰、完整、可复制的发布路径。
|
||||||
|
|
||||||
|
:::tip 提示
|
||||||
|
你只需选择一条与你习惯一致的路径,**完整跟随即可成功发布**。无需在不同工具间切换或猜测配置。
|
||||||
|
:::
|
||||||
|
|
||||||
|
NoneBot 生态目前有如下插件项目模板:
|
||||||
|
|
||||||
- [A-kirami/nonebot-plugin-template](https://github.com/A-kirami/nonebot-plugin-template)
|
|
||||||
- [RF-Tar-Railt/nonebot-plugin-template](https://github.com/RF-Tar-Railt/nonebot-plugin-template)
|
- [RF-Tar-Railt/nonebot-plugin-template](https://github.com/RF-Tar-Railt/nonebot-plugin-template)
|
||||||
|
|
||||||
|
此路径使用 **PDM** 项目管理器,符合 PEP 621 标准,自动化程度高。
|
||||||
|
|
||||||
- [fllesser/nonebot-plugin-template](https://github.com/fllesser/nonebot-plugin-template)
|
- [fllesser/nonebot-plugin-template](https://github.com/fllesser/nonebot-plugin-template)
|
||||||
|
|
||||||
|
此路径使用 **uv** 项目管理器和 **PoeThePoet** 任务运行器,构建速度快,适合追求效率的开发者。
|
||||||
|
|
||||||
|
- [A-kirami/nonebot-plugin-template](https://github.com/A-kirami/nonebot-plugin-template)
|
||||||
|
|
||||||
|
此路径使用 **Poetry** 项目管理器,适合熟悉传统 Python 生态的开发者。
|
||||||
|
|
||||||
|
#### 1. 创建项目
|
||||||
|
|
||||||
|
1. 访问上述三个模板之一。
|
||||||
|
2. 点击 **“Use this template”** → **“Create a new repository”**。
|
||||||
|
3. 仓库名称填写:`nonebot-plugin-{your-plugin-name}`(此部分以 `nonebot-plugin-weather` 为例)。
|
||||||
|
4. 点击 **“Create repository from template”**。
|
||||||
|
|
||||||
|
#### 2. 配置发布权限
|
||||||
|
|
||||||
|
1. 进入新仓库 → **Settings** → **Actions** → **General**。
|
||||||
|
2. 在 **Workflow permissions** 下,勾选 **“Read and write permissions”** → 点击 **Save**。
|
||||||
|
|
||||||
|
#### 3. 全局替换项目信息
|
||||||
|
|
||||||
|
在仓库中点击 **“Add file”** → **“Create new file”**,创建一个空文件 `LICENSE`,选择开源协议并提交(此操作会触发工作流)。
|
||||||
|
|
||||||
|
然后在本地克隆仓库,使用编辑器对以下内容进行**全局替换**:
|
||||||
|
|
||||||
:::tip 提示
|
:::tip 提示
|
||||||
本文档**不保证**第三方模板的适用性。
|
此部分以“天气插件”为例,实际的替换内容应该根据你所创建的插件名称等相应调整。
|
||||||
|
|
||||||
根据项目模板提供的使用指导补全/修改相应内容后上传到 GitHub 即可。
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
### 插件依赖
|
| 原内容 | 替换为 |
|
||||||
|
| ------------------------------ | ---------------------------------- |
|
||||||
|
| `nonebot-plugin-template` | `nonebot-plugin-weather` |
|
||||||
|
| `nonebot_plugin_template` | `nonebot_plugin_weather` |
|
||||||
|
| `<your_plugin_humanized_name>` | `天气查询` |
|
||||||
|
| `<your_plugin_description>` | `查询指定城市的实时天气与未来预报` |
|
||||||
|
| `<your_github>` | `你的GitHub用户名` |
|
||||||
|
| `<your_email>` | `你的邮箱` |
|
||||||
|
|
||||||
本段指导填写插件依赖,避免不正确的依赖信息导致插件无法正常工作。
|
#### 4. 安装依赖与开发
|
||||||
|
|
||||||
依赖填写的基本原则:程序直接导入了什么第三方库,就添加什么第三方包依赖;能用哪些第三方库的特性,就根据使用的特性锁定第三方包版本。
|
<Tabs groupId="publish-path" defaultValue="pdm" values={[
|
||||||
|
{label: 'PDM + RF-Tar-Railt 模板', value: 'pdm'},
|
||||||
|
{label: 'uv + fllesser 模板', value: 'uv'},
|
||||||
|
{label: 'Poetry + A-kirami 模板', value: 'poetry'},
|
||||||
|
]}>
|
||||||
|
<TabItem value="pdm" label="PDM + RF-Tar-Railt 模板">
|
||||||
|
|
||||||
:::caution 注意
|
```bash
|
||||||
|
# 安装 PDM(若未安装)
|
||||||
|
curl -sSL https://pdm-project.org/install-pdm.py | python3 -
|
||||||
|
|
||||||
1. 插件需要添加 `nonebot2` 为依赖以避免“幽灵依赖”;
|
# 安装项目依赖(自动创建虚拟环境)
|
||||||
2. 插件需要将使用的适配器加入依赖列表,如:使用 OneBot 适配器的插件应添加 `nonebot-adapter-onebot` 依赖;
|
pdm sync
|
||||||
3. 由于 `nonebot` 是指 `nonebot1` **而非** `nonebot2`,因此要注意**不要**将 `nonebot` 添加为插件的依赖,以免造成冲突;
|
|
||||||
4. 尽可能避免使用 `==` 锁定单一版本,增强与其它插件的兼容性。
|
|
||||||
|
|
||||||
:::
|
# 添加新依赖(如 httpx)
|
||||||
|
pdm add httpx
|
||||||
### 填写插件元数据
|
|
||||||
|
|
||||||
请注意,插件发布要求**必须**填写元数据才能通过审核。
|
|
||||||
|
|
||||||
下面是一个示例:
|
|
||||||
|
|
||||||
```python title=nonebot_plugin_{your_plugin_name}/__init__.py
|
|
||||||
from nonebot.plugin import PluginMetadata
|
|
||||||
|
|
||||||
from .config import Config
|
|
||||||
|
|
||||||
__plugin_meta__ = PluginMetadata(
|
|
||||||
name="{插件名称}",
|
|
||||||
description="{插件介绍}",
|
|
||||||
usage="{插件用法}",
|
|
||||||
|
|
||||||
type="{插件分类}",
|
|
||||||
# 发布必填,当前有效类型有:`library`(为其他插件编写提供功能),`application`(向机器人用户提供功能)。
|
|
||||||
|
|
||||||
homepage="{项目主页}",
|
|
||||||
# 发布必填。
|
|
||||||
|
|
||||||
config=Config,
|
|
||||||
# 插件配置项类,如无需配置可不填写。
|
|
||||||
|
|
||||||
supported_adapters={"~onebot.v11", "~telegram"},
|
|
||||||
# 支持的适配器集合,其中 `~` 在此处代表前缀 `nonebot.adapters.`,其余适配器亦按此格式填写。
|
|
||||||
# 若插件可以保证兼容所有适配器(即仅使用基本适配器功能)可不填写,否则应该列出插件支持的适配器。
|
|
||||||
)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
:::caution 注意
|
</TabItem>
|
||||||
`__plugin_meta__` 变量**必须**处于插件最外层(如 `__init__.py` 中),否则无法正常识别。
|
|
||||||
|
|
||||||
一般做法是在 `__init__.py` 中定义 `__plugin_meta__`。
|
<TabItem value="uv" label="uv + fllesser 模板">
|
||||||
:::
|
|
||||||
|
|
||||||
:::tip 提示
|
```bash
|
||||||
带花括号 `{}` 的内容需要自行替换,注意**一定要把原有的花括号去掉**。
|
# 安装 uv(Windows)
|
||||||
:::
|
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
|
||||||
|
|
||||||
### 准备项目主页
|
# 安装 uv(macOS/Linux)
|
||||||
|
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||||
|
|
||||||
通常可以使用 GitHub 项目页面作为项目主页,在 `README.md` 文件中编写插件介绍等内容。
|
# 安装所有依赖(含 dev)
|
||||||
|
uv sync --all-groups -p 3.12
|
||||||
|
|
||||||
内容大致包括:
|
# 添加新依赖
|
||||||
|
uv add httpx
|
||||||
|
```
|
||||||
|
|
||||||
- 插件功能介绍
|
</TabItem>
|
||||||
- 安装方法(建议至少有 `nb-cli` 方式安装,**不要**使用旧式的 `bot.py` 配置)
|
|
||||||
- 插件配置项(若无可跳过)
|
|
||||||
- 插件设置的触发规则(若无可跳过)
|
|
||||||
- 插件的其它用法(按需编写)
|
|
||||||
|
|
||||||
:::tip 提示
|
<TabItem value="poetry" label="Poetry + A-kirami 模板">
|
||||||
可以参考[第三方项目模板](#第三方项目模板)。
|
|
||||||
:::
|
|
||||||
|
|
||||||
### 发布至 [PyPI](https://pypi.org)
|
```bash
|
||||||
|
# 安装 Poetry(推荐方式)
|
||||||
|
curl -sSL https://install.python-poetry.org | python3 -
|
||||||
|
|
||||||
|
# 安装项目依赖
|
||||||
|
poetry install
|
||||||
|
|
||||||
|
# 添加新依赖
|
||||||
|
poetry add httpx
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
#### 5. 更新版本并发布
|
||||||
|
|
||||||
|
<Tabs
|
||||||
|
groupId="publish-path-bump"
|
||||||
|
defaultValue="bump-my-version"
|
||||||
|
values={[
|
||||||
|
{ label: "使用 bump-my-version", value: "bump-my-version" },
|
||||||
|
{ label: "使用项目管理器", value: "bump-manager" },
|
||||||
|
{ label: "手动更新版本", value: "bump-manual" },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<TabItem value="bump-my-version" label="使用 bump-my-version">
|
||||||
|
|
||||||
|
[bump-my-version](https://github.com/callowayproject/bump-my-version) 是一个功能强大、可配置的 Python 项目版本更新工具,支持自动提交到 Git 等 VCS。
|
||||||
|
|
||||||
|
<Tabs groupId="publish-path" defaultValue="pdm" values={[
|
||||||
|
{label: 'PDM + RF-Tar-Railt 模板', value: 'pdm'},
|
||||||
|
{label: 'uv + fllesser 模板', value: 'uv'},
|
||||||
|
{label: 'Poetry + A-kirami 模板', value: 'poetry'},
|
||||||
|
]}>
|
||||||
|
<TabItem value="pdm" label="PDM + RF-Tar-Railt 模板">
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装 bump-my-version
|
||||||
|
pdm add --dev bump-my-version
|
||||||
|
|
||||||
|
# 更新 patch 版本
|
||||||
|
pdm run bump patch
|
||||||
|
|
||||||
|
# 推送 tag 触发发布
|
||||||
|
git push origin --tags
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem value="uv" label="uv + fllesser 模板">
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 更新 patch 版本
|
||||||
|
uv run poe bump patch
|
||||||
|
|
||||||
|
# 推送 tag 触发发布
|
||||||
|
git push origin --tags
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem value="poetry" label="Poetry + A-kirami 模板">
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装 bump-my-version
|
||||||
|
poetry add --dev bump-my-version
|
||||||
|
|
||||||
|
# 更新 patch 版本
|
||||||
|
poetry run bump patch
|
||||||
|
|
||||||
|
# 推送 tag 触发发布
|
||||||
|
git push origin --tags
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
<TabItem value="bump-manager" label="使用项目管理器">
|
||||||
|
|
||||||
|
<Tabs groupId="publish-path" defaultValue="pdm" values={[
|
||||||
|
{label: 'PDM + RF-Tar-Railt 模板', value: 'pdm'},
|
||||||
|
{label: 'uv + fllesser 模板', value: 'uv'},
|
||||||
|
{label: 'Poetry + A-kirami 模板', value: 'poetry'},
|
||||||
|
]}>
|
||||||
|
<TabItem value="pdm" label="PDM + RF-Tar-Railt 模板">
|
||||||
|
|
||||||
|
需要安装 PDM 插件 [pdm-bump](https://github.com/carstencodes/pdm-bump)。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装 pdm-bump
|
||||||
|
pdm self add pdm-bump
|
||||||
|
|
||||||
|
# 更新 patch 版本
|
||||||
|
pdm bump patch
|
||||||
|
|
||||||
|
# 推送 tag 触发发布
|
||||||
|
git push origin --tags
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem value="uv" label="uv + fllesser 模板">
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 更新 patch 版本
|
||||||
|
uv version --bump patch
|
||||||
|
|
||||||
|
# 创建相应提交与标签
|
||||||
|
git add pyproject.toml
|
||||||
|
git commit -m "chore: release v0.1.1" # 替换为实际的版本号
|
||||||
|
git tag v0.1.1 # 替换为实际的版本号
|
||||||
|
|
||||||
|
# 推送 tag 触发发布
|
||||||
|
git push origin --tags
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem value="poetry" label="Poetry + A-kirami 模板">
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 更新版本(自动提交并打标签)
|
||||||
|
poetry version patch
|
||||||
|
|
||||||
|
# 推送 tag 触发发布
|
||||||
|
git push origin --tags
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
<TabItem value="bump-manual" label="手动更新版本">
|
||||||
|
|
||||||
|
手动更新 `pyproject.toml` 中的 `version` 字段,然后推送 tag 触发发布工作流
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add pyproject.toml
|
||||||
|
git commit -m "chore: release v0.1.1" # 替换为实际的版本号
|
||||||
|
git tag v0.1.1 # 替换为实际的版本号
|
||||||
|
git push origin --tags
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
推送 `v*` 标签后,模板提供的 GitHub Actions 工作流将自动构建并发布到 PyPI。
|
||||||
|
|
||||||
|
#### 6. 发布到 [PyPI](https://pypi.org)
|
||||||
|
|
||||||
|
<Tabs groupId="publish-method" defaultValue="template" values={[
|
||||||
|
{label: '使用模板的自动发布工作流', value: 'template'},
|
||||||
|
{label: '手动发布', value: 'manual'},
|
||||||
|
]}>
|
||||||
|
<TabItem value="template" label="使用模板的自动发布工作流">
|
||||||
|
不同模板使用的发布方式可能不同,具体配置流程参考对应模板的详细使用指南。
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem value="manual" label="手动发布">
|
||||||
根据选用的构建系统,在项目的 `pyproject.toml` 中填入必要信息后进行构建与发布。
|
根据选用的构建系统,在项目的 `pyproject.toml` 中填入必要信息后进行构建与发布。
|
||||||
|
|
||||||
:::tip 提示
|
:::tip 提示
|
||||||
不同构建工具的使用可能存在差别。本文仅以 [`pdm`](https://pdm.fming.dev/latest/),
|
不同构建工具的使用可能存在差别。本文仅以 [`pdm`](https://pdm-project.org/zh/latest/),
|
||||||
[`poetry`](https://python-poetry.org/docs/), [`setuptools`](https://setuptools.pypa.io/en/latest/)
|
[`poetry`](https://python-poetry.org/docs/), [`setuptools`](https://setuptools.pypa.io/en/latest/)
|
||||||
构建系统**本地构建与发布**为示例讲解,其余构建/管理工具等和自动化构建的用法请读者自行探索。
|
构建系统**本地构建与发布**为示例讲解,其余构建/管理工具等和自动化构建的用法请读者自行探索。
|
||||||
:::
|
:::
|
||||||
@@ -179,10 +354,187 @@ twine upload dist/* # 只发布先前的构建
|
|||||||
</TabItem>
|
</TabItem>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
:::tip 提示
|
:::tip 提示
|
||||||
发布前建议自行测试构建包是否可用,避免遗漏代码文件或资源文件等问题。
|
发布前建议自行测试构建包是否可用,避免遗漏代码文件或资源文件等问题。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
## 基本要求
|
||||||
|
|
||||||
|
无论你选择哪条路径,以下内容**必须**完成,否则无法通过商店自动检查:
|
||||||
|
|
||||||
|
### 能够正确加载
|
||||||
|
|
||||||
|
插件包必须能够被 NoneBot 正确加载,在商店审核中会通过 **NoneFlow** 自动化加载测试进行。
|
||||||
|
|
||||||
|
#### 依赖其他插件
|
||||||
|
|
||||||
|
如果插件依赖其他插件提供的功能,则**必须**在代码中使用 `require()` 来引入该插件,然后才能 `import` 该插件提供的功能。具体细节参阅[跨插件访问](../advanced/requiring.md)。
|
||||||
|
|
||||||
|
使用示例如下:
|
||||||
|
|
||||||
|
```python title=nonebot_plugin_weather/__init__.py
|
||||||
|
from nonebot import require
|
||||||
|
|
||||||
|
require("nonebot_plugin_apscheduler")
|
||||||
|
|
||||||
|
from nonebot_plugin_apscheduler import scheduler
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 不能零配置加载的插件
|
||||||
|
|
||||||
|
如果插件需要必要配置项才能正常导入,则**必须**在商店提交表单中填写必要的配置项内容。
|
||||||
|
|
||||||
|
但一种更好的做法是,**将插件设计为零配置即可加载**(允许缺少必要配置项时插件仍能正常导入,但不执行需要相应配置项的功能),尤其是对于一些必要配置含有敏感信息(如密钥、Token、API Key 等)的插件。这样可以避免在商店提交表单时填写敏感信息的风险。
|
||||||
|
|
||||||
|
### 插件元数据
|
||||||
|
|
||||||
|
插件包**必须**填写元数据才能通过 **NoneFlow** 自动化检查。
|
||||||
|
|
||||||
|
下面是一个示例:
|
||||||
|
|
||||||
|
```python title=nonebot_plugin_weather/__init__.py
|
||||||
|
from nonebot.plugin import PluginMetadata
|
||||||
|
|
||||||
|
from .config import Config
|
||||||
|
|
||||||
|
__plugin_meta__ = PluginMetadata(
|
||||||
|
# 基本信息(必填)
|
||||||
|
name="天气查询", # 插件名称
|
||||||
|
description="查询指定城市的实时天气与未来预报", # 插件介绍
|
||||||
|
usage="发送【天气 城市名】获取天气信息", # 插件用法
|
||||||
|
|
||||||
|
# 发布额外信息
|
||||||
|
type="application", # 插件分类
|
||||||
|
# 发布必填,当前有效类型有:`library`(为其他插件编写提供功能),`application`(向机器人用户提供功能)。
|
||||||
|
|
||||||
|
homepage="https://github.com/你的用户名/nonebot-plugin-weather",
|
||||||
|
# 发布必填。
|
||||||
|
|
||||||
|
config=Config,
|
||||||
|
# 插件配置项类,如果有配置类则必须填写。
|
||||||
|
|
||||||
|
supported_adapters={"~onebot.v11"},
|
||||||
|
# 支持的适配器集合,其中 `~` 在此处代表前缀 `nonebot.adapters.`,其余适配器亦按此格式填写。
|
||||||
|
# 若插件只使用了 NoneBot 基本抽象,应显式填写 None,否则应该列出插件支持的适配器。
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
:::caution 注意
|
||||||
|
`__plugin_meta__` 变量**必须**处于插件最外层(如 `__init__.py` 中),否则无法正常识别。
|
||||||
|
|
||||||
|
一般做法是在 `__init__.py` 中定义 `__plugin_meta__`。
|
||||||
|
:::
|
||||||
|
|
||||||
|
#### 继承其他插件支持的适配器
|
||||||
|
|
||||||
|
如果你的插件依赖于其他插件提供的支持功能,而其他插件可能支持更少的适配器,这时就应该使用
|
||||||
|
[inherit_supported_adapters()](../api/plugin/load#inherit-supported-adapters) 函数来继承其他插件支持的适配器。
|
||||||
|
|
||||||
|
示例用法如下:
|
||||||
|
|
||||||
|
```python title=nonebot_plugin_weather/__init__.py
|
||||||
|
from nonebot import require
|
||||||
|
from nonebot.plugin import PluginMetadata, inherit_supported_adapters
|
||||||
|
|
||||||
|
from .config import Config
|
||||||
|
|
||||||
|
require("nonebot_plugin_alconna") # 必须先 require 才能被 inherit_supported_adapters 处理
|
||||||
|
|
||||||
|
__plugin_meta__ = PluginMetadata(
|
||||||
|
name="天气查询",
|
||||||
|
description="查询指定城市的实时天气与未来预报",
|
||||||
|
usage="发送【天气 城市名】获取天气信息",
|
||||||
|
type="application",
|
||||||
|
homepage="https://github.com/你的用户名/nonebot-plugin-weather",
|
||||||
|
config=Config,
|
||||||
|
|
||||||
|
supported_adapters=inherit_supported_adapters("nonebot_plugin_alconna"),
|
||||||
|
# 继承 nonebot_plugin_alconna 插件的适配器支持列表
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 准备项目主页
|
||||||
|
|
||||||
|
通常可以使用 GitHub 项目页面作为项目主页,在 `README.md` 文件中编写插件介绍等内容。
|
||||||
|
|
||||||
|
内容大致包括:
|
||||||
|
|
||||||
|
- 插件功能介绍;
|
||||||
|
- 安装方法
|
||||||
|
- **必须**有 NB-CLI 方式安装
|
||||||
|
- 可选依赖可以给出其他安装方式
|
||||||
|
- **不得**使用旧式的 `bot.py` 配置
|
||||||
|
- 插件配置项(如 `Config` 类字段,若无可跳过)
|
||||||
|
- 插件设置的触发规则(若无可跳过)
|
||||||
|
- 插件的其它用法(按需编写)
|
||||||
|
- 效果图、权限说明(按需编写)
|
||||||
|
|
||||||
|
## 质量要求
|
||||||
|
|
||||||
|
以下内容**强烈建议**完成,否则社区成员将会要求修改:
|
||||||
|
|
||||||
|
### 依赖管理原则
|
||||||
|
|
||||||
|
- **必须**包含 `nonebot2`。
|
||||||
|
- **必须**将插件直接使用的适配器加入依赖列表,如:使用 OneBot 适配器的插件应添加 `nonebot-adapter-onebot` 依赖;
|
||||||
|
- **禁止**使用 `==` 锁定单一版本,使用 `>=` 或 `~=`。
|
||||||
|
- **禁止**添加 `nonebot`(V1)作为依赖。
|
||||||
|
- 所有在代码中 `import` 的第三方库,必须在 `pyproject.toml` 的 `dependencies` 中列出。
|
||||||
|
|
||||||
|
### 避免误用同步操作
|
||||||
|
|
||||||
|
NoneBot 是一个异步框架,插件中**禁止**使用任何可能阻塞事件循环的同步操作,例如:
|
||||||
|
|
||||||
|
- 同步 HTTP 请求(如 `requests` 库);
|
||||||
|
|
||||||
|
**推荐**操作(以 `httpx` 为例):
|
||||||
|
|
||||||
|
```python
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
response = await client.get("https://api.example.com/data") # 异步操作,不阻塞机器人
|
||||||
|
```
|
||||||
|
|
||||||
|
**禁止**操作:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import requests
|
||||||
|
|
||||||
|
requests.get("https://api.example.com/data") # 同步操作,会阻塞机器人
|
||||||
|
```
|
||||||
|
|
||||||
|
- 其他可能长时间运行阻塞事件循环的操作。
|
||||||
|
|
||||||
|
### 本地文件存储
|
||||||
|
|
||||||
|
如果插件需要在本地存储数据、配置或缓存文件,**必须**使用 [`nonebot-plugin-localstore`](https://github.com/nonebot/plugin-localstore) 管理,具体细节参阅[本地存储](../best-practice/data-storing.md)章节。
|
||||||
|
|
||||||
|
参考示例:
|
||||||
|
|
||||||
|
```python title=nonebot_plugin_weather/__init__.py
|
||||||
|
from pathlib import Path
|
||||||
|
from nonebot import require
|
||||||
|
require("nonebot_plugin_localstore")
|
||||||
|
|
||||||
|
import nonebot_plugin_localstore as store
|
||||||
|
|
||||||
|
# 获取插件缓存文件(夹)路径
|
||||||
|
weather_cache_dir: Path = store.get_plugin_cache_dir()
|
||||||
|
weather_cache_file: Path = store.get_plugin_cache_file("cache.json")
|
||||||
|
|
||||||
|
# 获取插件配置文件(夹)路径
|
||||||
|
weather_config_dir: Path = store.get_plugin_config_dir()
|
||||||
|
weather_config_file: Path = store.get_plugin_config_file("config.toml")
|
||||||
|
|
||||||
|
# 获取插件数据文件(夹)路径
|
||||||
|
weather_data_dir: Path = store.get_plugin_data_dir()
|
||||||
|
weather_data_file: Path = store.get_plugin_data_file("resource-index.json")
|
||||||
|
```
|
||||||
|
|
||||||
## 商店审核
|
## 商店审核
|
||||||
|
|
||||||
### 提交申请
|
### 提交申请
|
||||||
@@ -201,6 +553,6 @@ twine upload dist/* # 只发布先前的构建
|
|||||||
若插件检查未通过或信息有误,**不必**关闭当前 Issue。只需更新插件并上传到 PyPI/修改信息后勾选插件测试勾选框即可重新触发插件检查。
|
若插件检查未通过或信息有误,**不必**关闭当前 Issue。只需更新插件并上传到 PyPI/修改信息后勾选插件测试勾选框即可重新触发插件检查。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
之后,NoneBot 的维护者和一些插件开发者会初步检查插件代码,帮助减少该插件的问题。
|
之后,NoneBot 的维护者和一些志愿者会初步检查插件代码,帮助减少该插件的问题。
|
||||||
|
|
||||||
完成这些步骤后,您的插件将会被自动合并到[商店](/store/plugins),而您也将成为 [**NoneBot 贡献者**](https://github.com/nonebot/nonebot2/graphs/contributors)的一员。
|
完成这些步骤后,您的插件将会被自动合并到[商店](/store/plugins),而您也将成为 [**NoneBot 贡献者**](https://github.com/nonebot/nonebot2/graphs/contributors)的一员。
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import TabItem from "@theme/TabItem";
|
|||||||
1. (可选)创建虚拟环境,以 venv 为例
|
1. (可选)创建虚拟环境,以 venv 为例
|
||||||
|
|
||||||
<Tabs groupId="platform">
|
<Tabs groupId="platform">
|
||||||
<TabItem value="windows" label="Windows" default>
|
<TabItem value="windows" label="Windows" default>
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 创建虚拟环境
|
# 创建虚拟环境
|
||||||
@@ -44,7 +44,7 @@ import TabItem from "@theme/TabItem";
|
|||||||
```
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem value="linux/macos" label="Linux/macOS">
|
<TabItem value="linux/macos" label="Linux/macOS">
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 创建虚拟环境
|
# 创建虚拟环境
|
||||||
@@ -54,26 +54,26 @@ import TabItem from "@theme/TabItem";
|
|||||||
```
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
2. 安装 nonebot2 以及驱动器,以 Fastapi 驱动器为例
|
2. 安装 nonebot2 以及驱动器,以 Fastapi 驱动器为例
|
||||||
|
|
||||||
<Tabs groupId="platform">
|
<Tabs groupId="platform">
|
||||||
<TabItem value="windows" label="Windows" default>
|
<TabItem value="windows" label="Windows" default>
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install "nonebot2[fastapi]"
|
pip install "nonebot2[fastapi]"
|
||||||
```
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem value="linux/macos" label="Linux/macOS">
|
<TabItem value="linux/macos" label="Linux/macOS">
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install "nonebot2[fastapi]"
|
pip install "nonebot2[fastapi]"
|
||||||
```
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
驱动器包名可以在 [驱动器商店](/store/drivers) 中找到,请替换上文方括号中的内容。
|
驱动器包名可以在 [驱动器商店](/store/drivers) 中找到,请替换上文方括号中的内容。
|
||||||
|
|
||||||
|
|||||||
@@ -48,8 +48,10 @@ options:
|
|||||||
|
|
||||||
```tree title=Project
|
```tree title=Project
|
||||||
📦 awesome-bot
|
📦 awesome-bot
|
||||||
|
├── 📂 .venv
|
||||||
├── 📂 awesome_bot
|
├── 📂 awesome_bot
|
||||||
│ └── 📂 plugins
|
│ └── 📂 plugins
|
||||||
|
├── 📜 .env.prod
|
||||||
├── 📜 pyproject.toml
|
├── 📜 pyproject.toml
|
||||||
└── 📜 README.md
|
└── 📜 README.md
|
||||||
```
|
```
|
||||||
@@ -91,18 +93,20 @@ options:
|
|||||||
$ nb plugin create
|
$ nb plugin create
|
||||||
[?] 插件名称: weather
|
[?] 插件名称: weather
|
||||||
[?] 使用嵌套插件? (y/N) N
|
[?] 使用嵌套插件? (y/N) N
|
||||||
[?] 输出目录: awesome_bot/plugins
|
[?] 请输入插件存储位置: awesome_bot/plugins
|
||||||
```
|
```
|
||||||
|
|
||||||
`nb-cli` 会在 `awesome_bot/plugins` 目录下创建一个名为 `weather` 的文件夹,其中包含的文件将在稍后章节中用到。
|
`nb-cli` 会在 `awesome_bot/plugins` 目录下创建一个名为 `weather` 的文件夹,其中包含的文件将在稍后章节中用到。
|
||||||
|
|
||||||
```tree title=Project
|
```tree title=Project
|
||||||
📦 awesome-bot
|
📦 awesome-bot
|
||||||
|
├── 📂 .venv
|
||||||
├── 📂 awesome_bot
|
├── 📂 awesome_bot
|
||||||
│ └── 📂 plugins
|
│ └── 📂 plugins
|
||||||
| └── 📂 foo
|
| └── 📂 weather
|
||||||
| ├── 📜 __init__.py
|
| ├── 📜 __init__.py
|
||||||
| └── 📜 config.py
|
| └── 📜 config.py
|
||||||
|
├── 📜 .env.prod
|
||||||
├── 📜 pyproject.toml
|
├── 📜 pyproject.toml
|
||||||
└── 📜 README.md
|
└── 📜 README.md
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ import Messenger from "@site/src/components/Messenger";
|
|||||||
顾名思义,“事件处理函数装饰器”是一个[装饰器(decorator)](https://docs.python.org/zh-cn/3/glossary.html#term-decorator),那么它的使用方法也同[函数定义](https://docs.python.org/zh-cn/3/reference/compound_stmts.html#function-definitions)中所展示的包装用法相同。例如:
|
顾名思义,“事件处理函数装饰器”是一个[装饰器(decorator)](https://docs.python.org/zh-cn/3/glossary.html#term-decorator),那么它的使用方法也同[函数定义](https://docs.python.org/zh-cn/3/reference/compound_stmts.html#function-definitions)中所展示的包装用法相同。例如:
|
||||||
|
|
||||||
```python {6-8} title=weather/__init__.py
|
```python {6-8} title=weather/__init__.py
|
||||||
|
from nonebot import on_command
|
||||||
from nonebot.rule import to_me
|
from nonebot.rule import to_me
|
||||||
from nonebot.plugin import on_command
|
|
||||||
|
|
||||||
weather = on_command("天气", rule=to_me(), aliases={"weather", "查天气"}, priority=10, block=True)
|
weather = on_command("天气", rule=to_me(), aliases={"weather", "查天气"}, priority=10, block=True)
|
||||||
|
|
||||||
@@ -46,8 +46,8 @@ async def handle_function():
|
|||||||
事件响应器操作与事件处理函数装饰器类似,通常作为事件响应器 `Matcher` 的[类方法](https://docs.python.org/zh-cn/3/library/functions.html#classmethod)存在,因此事件响应器操作的调用方法也是 `Matcher.func()` 的形式。不过不同的是,事件响应器操作并不是装饰器,因此并不需要@进行标注。
|
事件响应器操作与事件处理函数装饰器类似,通常作为事件响应器 `Matcher` 的[类方法](https://docs.python.org/zh-cn/3/library/functions.html#classmethod)存在,因此事件响应器操作的调用方法也是 `Matcher.func()` 的形式。不过不同的是,事件响应器操作并不是装饰器,因此并不需要@进行标注。
|
||||||
|
|
||||||
```python {8,9} title=weather/__init__.py
|
```python {8,9} title=weather/__init__.py
|
||||||
|
from nonebot import on_command
|
||||||
from nonebot.rule import to_me
|
from nonebot.rule import to_me
|
||||||
from nonebot.plugin import on_command
|
|
||||||
|
|
||||||
weather = on_command("天气", rule=to_me(), aliases={"weather", "查天气"}, priority=10, block=True)
|
weather = on_command("天气", rule=to_me(), aliases={"weather", "查天气"}, priority=10, block=True)
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user