mirror of
https://github.com/nonebot/nonebot2.git
synced 2026-05-30 15:42:41 +00:00
Compare commits
98 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 04671cea11 | |||
| 014998d050 | |||
| e615714e1d | |||
| 5aa2b3d965 | |||
| 56e8196d34 | |||
| bc2fe15b27 | |||
| 197a3002b3 | |||
| c621cced34 | |||
| 172fdc9a09 | |||
| dbb80712d6 | |||
| 4f4397d975 | |||
| 42b789cb77 | |||
| 55193fa9ef | |||
| 29b74585d5 | |||
| aee5b257ca | |||
| 2af3954b38 | |||
| 4eaab97125 | |||
| 3bc31fff9e | |||
| 0a2bd00a38 | |||
| dc916fd4e2 | |||
| 533156aa06 | |||
| 1d1c695f19 | |||
| e49609d517 | |||
| 4bb4afa13c | |||
| 1fbcd07f3e | |||
| 08a969903e | |||
| b18f39ff14 | |||
| 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 |
@@ -3,10 +3,10 @@
|
||||
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
|
||||
"features": {
|
||||
"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": {}
|
||||
},
|
||||
"postCreateCommand": "./scripts/setup-envs.sh",
|
||||
"postCreateCommand": "corepack enable && COREPACK_ENABLE_DOWNLOAD_PROMPT=0 ./scripts/setup-envs.sh",
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"settings": {
|
||||
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
- name: Upload coverage report
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@v6
|
||||
with:
|
||||
env_vars: OS,PYTHON_VERSION,PYDANTIC_VERSION
|
||||
files: ./tests/coverage.xml
|
||||
|
||||
@@ -2,17 +2,14 @@ name: Release Drafter
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
pull_request_target:
|
||||
branches:
|
||||
- master
|
||||
types:
|
||||
- closed
|
||||
tags:
|
||||
- v*
|
||||
|
||||
jobs:
|
||||
update-release-draft:
|
||||
if: github.event_name == 'pull_request_target'
|
||||
if: github.ref == 'refs/heads/master'
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: pull-request-changelog
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Set Commit Status
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
github.rest.repos.createCommitStatus({
|
||||
@@ -78,7 +78,7 @@ jobs:
|
||||
:rocket: Deployed to ${{ steps.deploy.outputs.deploy-url }}
|
||||
|
||||
- name: Set Commit Status
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
if: always()
|
||||
with:
|
||||
script: |
|
||||
|
||||
@@ -7,7 +7,7 @@ ci:
|
||||
autoupdate_commit_msg: ":arrow_up: auto update by pre-commit hooks"
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.15.4
|
||||
rev: v0.15.12
|
||||
hooks:
|
||||
- id: ruff-check
|
||||
args: [--fix]
|
||||
|
||||
@@ -21,7 +21,7 @@ _✨ 跨平台 Python 异步机器人框架 ✨_
|
||||
<a href="https://pypi.python.org/pypi/nonebot2">
|
||||
<img src="https://img.shields.io/pypi/v/nonebot2?logo=python&logoColor=edb641" alt="pypi">
|
||||
</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">
|
||||
<img src="https://img.shields.io/badge/code%20style-black-000000.svg?logo=python&logoColor=edb641" alt="black">
|
||||
</a>
|
||||
|
||||
+16
-1
@@ -339,7 +339,7 @@
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"module_name": "yunhu",
|
||||
"module_name": "nonebot.adapters.yunhu",
|
||||
"project_link": "nonebot-adapter-yunhu",
|
||||
"name": "云湖适配器",
|
||||
"desc": "云湖的NoneBot适配器",
|
||||
@@ -353,4 +353,19 @@
|
||||
],
|
||||
"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
|
||||
},
|
||||
]
|
||||
|
||||
+475
-10
@@ -8297,13 +8297,6 @@
|
||||
],
|
||||
"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",
|
||||
"project_link": "nonebot-plugin-luoguluck",
|
||||
@@ -10488,10 +10481,482 @@
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"module_name": "nonebot_plugin_cs2bridge",
|
||||
"project_link": "nonebot-plugin-cs2bridge",
|
||||
"author_id": 264679009,
|
||||
"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
|
||||
},
|
||||
{
|
||||
"module_name": "nonebot_plugin_hermes",
|
||||
"project_link": "nonebot-plugin-hermes",
|
||||
"author_id": 1080807,
|
||||
"tags": [
|
||||
{
|
||||
"label": "hermes",
|
||||
"color": "#cf3131"
|
||||
}
|
||||
],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"module_name": "nonebot_plugin_uma",
|
||||
"project_link": "nonebot-plugin-umamusume",
|
||||
"author_id": 30062345,
|
||||
"tags": [],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"module_name": "nonebot_plugin_namepy",
|
||||
"project_link": "nonebot-plugin-namepy",
|
||||
"author_id": 175370943,
|
||||
"tags": [
|
||||
{
|
||||
"label": "命名工具",
|
||||
"color": "#5fa8d3"
|
||||
}
|
||||
],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"module_name": "nonebot_plugin_dotcharacter",
|
||||
"project_link": "nonebot-plugin-dotcharacter",
|
||||
"author_id": 86188856,
|
||||
"tags": [
|
||||
{
|
||||
"label": "ai",
|
||||
"color": "#ea5252"
|
||||
}
|
||||
],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"module_name": "nonebot_plugin_bililive",
|
||||
"project_link": "nonebot-plugin-bililive",
|
||||
"author_id": 271566727,
|
||||
"tags": [
|
||||
{
|
||||
"label": "bilibili",
|
||||
"color": "#ea5252"
|
||||
}
|
||||
],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"module_name": "nonebot_plugin_b50_analysis",
|
||||
"project_link": "nonebot-plugin-b50-analysis",
|
||||
"author_id": 145742863,
|
||||
"tags": [
|
||||
{
|
||||
"label": "maimai",
|
||||
"color": "#ea5252"
|
||||
}
|
||||
],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"module_name": "nonebot_plugin_picstatus_ng",
|
||||
"project_link": "nonebot-plugin-picstatus-ng",
|
||||
"author_id": 72241996,
|
||||
"tags": [
|
||||
{
|
||||
"label": "server",
|
||||
"color": "#8bff00"
|
||||
},
|
||||
{
|
||||
"label": "PicStatus",
|
||||
"color": "#ea5252"
|
||||
}
|
||||
],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"module_name": "nonebot_plugin_catcake",
|
||||
"project_link": "nonebot-plugin-catcake",
|
||||
"author_id": 191975746,
|
||||
"tags": [
|
||||
{
|
||||
"label": "崩坏:星穹铁道",
|
||||
"color": "#d01b7a"
|
||||
}
|
||||
],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"module_name": "nonebot_plugin_onebot2tg",
|
||||
"project_link": "nonebot-plugin-onebot2tg",
|
||||
"author_id": 50368309,
|
||||
"tags": [
|
||||
{
|
||||
"label": "telegram",
|
||||
"color": "#ea5252"
|
||||
},
|
||||
{
|
||||
"label": "qq",
|
||||
"color": "#ea5252"
|
||||
},
|
||||
{
|
||||
"label": "QQ群",
|
||||
"color": "#ea5252"
|
||||
}
|
||||
],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"module_name": "nonebot_plugin_set_essence_msg",
|
||||
"project_link": "nonebot-plugin-set-essence-msg",
|
||||
"author_id": 32678715,
|
||||
"tags": [
|
||||
{
|
||||
"label": "精华消息",
|
||||
"color": "#ea5252"
|
||||
},
|
||||
{
|
||||
"label": "群管理",
|
||||
"color": "#ea5252"
|
||||
}
|
||||
],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"module_name": "nonebot_plugin_maimai_raking",
|
||||
"project_link": "nonebot-plugin-maimai-raking",
|
||||
"author_id": 72530683,
|
||||
"tags": [
|
||||
{
|
||||
"label": "maimai",
|
||||
"color": "#ea5252"
|
||||
},
|
||||
{
|
||||
"label": "舞萌",
|
||||
"color": "#ea5252"
|
||||
}
|
||||
],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"module_name": "nonebot_plugin_permission",
|
||||
"project_link": "nonebot-plugin-permission",
|
||||
"author_id": 42648639,
|
||||
"tags": [
|
||||
{
|
||||
"label": "权限控制",
|
||||
"color": "#1e5d58"
|
||||
}
|
||||
],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"module_name": "nonebot_plugin_group_heat",
|
||||
"project_link": "nonebot-plugin-group-heat",
|
||||
"author_id": 277260326,
|
||||
"tags": [
|
||||
{
|
||||
"label": "热度",
|
||||
"color": "#ea5252"
|
||||
}
|
||||
],
|
||||
"is_official": false
|
||||
},
|
||||
]
|
||||
|
||||
+21
-8
@@ -19,7 +19,9 @@ from typing import (
|
||||
Generic,
|
||||
Literal,
|
||||
Protocol,
|
||||
TypeAlias,
|
||||
TypeVar,
|
||||
cast,
|
||||
get_args,
|
||||
get_origin,
|
||||
overload,
|
||||
@@ -44,6 +46,16 @@ if TYPE_CHECKING:
|
||||
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__ = (
|
||||
"DEFAULT_CONFIG",
|
||||
"PYDANTIC_V2",
|
||||
@@ -230,16 +242,17 @@ if PYDANTIC_V2: # pragma: pydantic-v2
|
||||
|
||||
def model_dump(
|
||||
model: BaseModel,
|
||||
include: set[str] | None = None,
|
||||
exclude: set[str] | None = None,
|
||||
include: ModelDumpIncEx = None,
|
||||
exclude: ModelDumpIncEx = None,
|
||||
by_alias: bool = False,
|
||||
exclude_unset: bool = False,
|
||||
exclude_defaults: bool = False,
|
||||
exclude_none: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
return model.model_dump(
|
||||
include=include,
|
||||
exclude=exclude,
|
||||
# Nested types cannot be inferred correctly
|
||||
include=cast(Any, include),
|
||||
exclude=cast(Any, exclude),
|
||||
by_alias=by_alias,
|
||||
exclude_unset=exclude_unset,
|
||||
exclude_defaults=exclude_defaults,
|
||||
@@ -457,16 +470,16 @@ else: # pragma: pydantic-v1
|
||||
|
||||
def model_dump(
|
||||
model: BaseModel,
|
||||
include: set[str] | None = None,
|
||||
exclude: set[str] | None = None,
|
||||
include: ModelDumpIncEx = None,
|
||||
exclude: ModelDumpIncEx = None,
|
||||
by_alias: bool = False,
|
||||
exclude_unset: bool = False,
|
||||
exclude_defaults: bool = False,
|
||||
exclude_none: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
return model.dict(
|
||||
include=include,
|
||||
exclude=exclude,
|
||||
include=cast(Any, include),
|
||||
exclude=cast(Any, exclude),
|
||||
by_alias=by_alias,
|
||||
exclude_unset=exclude_unset,
|
||||
exclude_defaults=exclude_defaults,
|
||||
|
||||
@@ -9,6 +9,7 @@ FrontMatter:
|
||||
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 ASGIMixin as ASGIMixin
|
||||
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
|
||||
|
||||
__autodoc__ = {
|
||||
"DEFAULT_TIMEOUT": True,
|
||||
"URL": True,
|
||||
"Cookies": True,
|
||||
"Request": True,
|
||||
|
||||
+108
-34
@@ -38,6 +38,7 @@ from nonebot.drivers import WebSocket as BaseWebSocket
|
||||
from nonebot.drivers.none import Driver as NoneDriver
|
||||
from nonebot.exception import WebSocketClosed
|
||||
from nonebot.internal.driver import (
|
||||
DEFAULT_TIMEOUT,
|
||||
Cookies,
|
||||
CookieTypes,
|
||||
HeaderTypes,
|
||||
@@ -45,6 +46,8 @@ from nonebot.internal.driver import (
|
||||
Timeout,
|
||||
TimeoutTypes,
|
||||
)
|
||||
from nonebot.log import logger
|
||||
from nonebot.utils import UNSET, UnsetType, exclude_unset
|
||||
|
||||
try:
|
||||
import aiohttp
|
||||
@@ -63,7 +66,7 @@ class Session(HTTPClientSession):
|
||||
headers: HeaderTypes = None,
|
||||
cookies: CookieTypes = None,
|
||||
version: str | HTTPVersion = HTTPVersion.H11,
|
||||
timeout: TimeoutTypes = None,
|
||||
timeout: TimeoutTypes | UnsetType = UNSET,
|
||||
proxy: str | None = None,
|
||||
):
|
||||
self._client: aiohttp.ClientSession | None = None
|
||||
@@ -85,15 +88,32 @@ class Session(HTTPClientSession):
|
||||
else:
|
||||
raise RuntimeError(f"Unsupported HTTP version: {version}")
|
||||
|
||||
_timeout = None
|
||||
if isinstance(timeout, Timeout):
|
||||
self._timeout = aiohttp.ClientTimeout(
|
||||
total=timeout.total,
|
||||
connect=timeout.connect,
|
||||
sock_read=timeout.read,
|
||||
timeout_kwargs: dict[str, float | None] = exclude_unset(
|
||||
{
|
||||
"total": timeout.total,
|
||||
"connect": timeout.connect,
|
||||
"sock_read": timeout.read,
|
||||
}
|
||||
)
|
||||
else:
|
||||
self._timeout = aiohttp.ClientTimeout(timeout)
|
||||
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:
|
||||
_timeout = aiohttp.ClientTimeout(
|
||||
**exclude_unset(
|
||||
{
|
||||
"total": DEFAULT_TIMEOUT.total,
|
||||
"connect": DEFAULT_TIMEOUT.connect,
|
||||
"sock_read": DEFAULT_TIMEOUT.read,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
self._timeout = _timeout
|
||||
self._proxy = proxy
|
||||
|
||||
@property
|
||||
@@ -102,6 +122,25 @@ class Session(HTTPClientSession):
|
||||
raise RuntimeError("Session is not initialized")
|
||||
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
|
||||
async def request(self, setup: Request) -> Response:
|
||||
if self._params:
|
||||
@@ -121,15 +160,6 @@ class Session(HTTPClientSession):
|
||||
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(
|
||||
setup.method,
|
||||
url,
|
||||
@@ -138,7 +168,7 @@ class Session(HTTPClientSession):
|
||||
cookies=cookies,
|
||||
headers=setup.headers,
|
||||
proxy=setup.proxy or self._proxy,
|
||||
timeout=timeout,
|
||||
timeout=self._get_timeout(setup.timeout),
|
||||
) as response:
|
||||
return Response(
|
||||
response.status,
|
||||
@@ -171,15 +201,6 @@ class Session(HTTPClientSession):
|
||||
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(
|
||||
setup.method,
|
||||
url,
|
||||
@@ -188,14 +209,29 @@ class Session(HTTPClientSession):
|
||||
cookies=cookies,
|
||||
headers=setup.headers,
|
||||
proxy=setup.proxy or self._proxy,
|
||||
timeout=timeout,
|
||||
timeout=self._get_timeout(setup.timeout),
|
||||
) as response:
|
||||
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):
|
||||
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(
|
||||
response.status,
|
||||
headers=response_headers,
|
||||
content=chunk,
|
||||
content=bytes(buffer),
|
||||
request=setup,
|
||||
)
|
||||
|
||||
@@ -255,13 +291,49 @@ class Mixin(HTTPClientMixin, WebSocketClientMixin):
|
||||
else:
|
||||
raise RuntimeError(f"Unsupported HTTP version: {setup.version}")
|
||||
|
||||
timeout = None
|
||||
if isinstance(setup.timeout, Timeout):
|
||||
timeout = aiohttp.ClientWSTimeout(
|
||||
ws_receive=setup.timeout.read, # type: ignore
|
||||
ws_close=setup.timeout.total, # type: ignore
|
||||
timeout_kwargs: dict[str, float | None] = exclude_unset(
|
||||
{
|
||||
"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 session.ws_connect(
|
||||
@@ -270,6 +342,8 @@ class Mixin(HTTPClientMixin, WebSocketClientMixin):
|
||||
timeout=timeout,
|
||||
headers=setup.headers,
|
||||
proxy=setup.proxy,
|
||||
autoping=heartbeat is not None,
|
||||
heartbeat=heartbeat,
|
||||
) as ws:
|
||||
yield WebSocket(request=setup, session=session, websocket=ws)
|
||||
|
||||
@@ -280,7 +354,7 @@ class Mixin(HTTPClientMixin, WebSocketClientMixin):
|
||||
headers: HeaderTypes = None,
|
||||
cookies: CookieTypes = None,
|
||||
version: str | HTTPVersion = HTTPVersion.H11,
|
||||
timeout: TimeoutTypes = None,
|
||||
timeout: TimeoutTypes | UnsetType = UNSET,
|
||||
proxy: str | None = None,
|
||||
) -> Session:
|
||||
return Session(
|
||||
|
||||
+50
-27
@@ -34,6 +34,7 @@ from nonebot.drivers import (
|
||||
)
|
||||
from nonebot.drivers.none import Driver as NoneDriver
|
||||
from nonebot.internal.driver import (
|
||||
DEFAULT_TIMEOUT,
|
||||
Cookies,
|
||||
CookieTypes,
|
||||
HeaderTypes,
|
||||
@@ -41,6 +42,7 @@ from nonebot.internal.driver import (
|
||||
Timeout,
|
||||
TimeoutTypes,
|
||||
)
|
||||
from nonebot.utils import UNSET, UnsetType, exclude_unset
|
||||
|
||||
try:
|
||||
import httpx
|
||||
@@ -59,7 +61,7 @@ class Session(HTTPClientSession):
|
||||
headers: HeaderTypes = None,
|
||||
cookies: CookieTypes = None,
|
||||
version: str | HTTPVersion = HTTPVersion.H11,
|
||||
timeout: TimeoutTypes = None,
|
||||
timeout: TimeoutTypes | UnsetType = UNSET,
|
||||
proxy: str | None = None,
|
||||
):
|
||||
self._client: httpx.AsyncClient | None = None
|
||||
@@ -73,15 +75,34 @@ class Session(HTTPClientSession):
|
||||
self._cookies = Cookies(cookies)
|
||||
self._version = HTTPVersion(version)
|
||||
|
||||
_timeout = None
|
||||
if isinstance(timeout, Timeout):
|
||||
self._timeout = httpx.Timeout(
|
||||
timeout=timeout.total,
|
||||
connect=timeout.connect,
|
||||
read=timeout.read,
|
||||
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,
|
||||
}
|
||||
)
|
||||
else:
|
||||
self._timeout = httpx.Timeout(timeout)
|
||||
if timeout_kwargs:
|
||||
_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
|
||||
|
||||
@property
|
||||
@@ -90,17 +111,28 @@ class Session(HTTPClientSession):
|
||||
raise RuntimeError("Session is not initialized")
|
||||
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
|
||||
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(
|
||||
setup.method,
|
||||
str(setup.url),
|
||||
@@ -112,7 +144,7 @@ class Session(HTTPClientSession):
|
||||
params=setup.url.raw_query_string,
|
||||
headers=tuple(setup.headers.items()),
|
||||
cookies=setup.cookies.jar,
|
||||
timeout=timeout,
|
||||
timeout=self._get_timeout(setup.timeout),
|
||||
)
|
||||
return Response(
|
||||
response.status_code,
|
||||
@@ -128,15 +160,6 @@ class Session(HTTPClientSession):
|
||||
*,
|
||||
chunk_size: int = 1024,
|
||||
) -> 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(
|
||||
setup.method,
|
||||
str(setup.url),
|
||||
@@ -148,7 +171,7 @@ class Session(HTTPClientSession):
|
||||
params=setup.url.raw_query_string,
|
||||
headers=tuple(setup.headers.items()),
|
||||
cookies=setup.cookies.jar,
|
||||
timeout=timeout,
|
||||
timeout=self._get_timeout(setup.timeout),
|
||||
) as response:
|
||||
response_headers = response.headers.multi_items()
|
||||
async for chunk in response.aiter_bytes(chunk_size=chunk_size):
|
||||
|
||||
@@ -25,11 +25,18 @@ from types import CoroutineType
|
||||
from typing import TYPE_CHECKING, Any, TypeVar
|
||||
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.none import Driver as NoneDriver
|
||||
from nonebot.exception import WebSocketClosed
|
||||
from nonebot.log import LoguruHandler
|
||||
from nonebot.utils import UNSET, UnsetType, exclude_unset
|
||||
|
||||
try:
|
||||
from websockets import ClientConnection, ConnectionClosed, connect
|
||||
@@ -70,16 +77,45 @@ class Mixin(WebSocketClientMixin):
|
||||
@override
|
||||
@asynccontextmanager
|
||||
async def websocket(self, setup: Request) -> AsyncGenerator["WebSocket", None]:
|
||||
timeout_kwargs: dict[str, float | None | UnsetType] = {}
|
||||
if isinstance(setup.timeout, Timeout):
|
||||
timeout = setup.timeout.total or setup.timeout.connect or setup.timeout.read
|
||||
else:
|
||||
timeout = setup.timeout
|
||||
open_timeout = (
|
||||
setup.timeout.connect or setup.timeout.read or setup.timeout.total
|
||||
)
|
||||
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(
|
||||
str(setup.url),
|
||||
additional_headers={**setup.headers, **setup.cookies.as_header(setup)},
|
||||
proxy=setup.proxy if setup.proxy is not None else True,
|
||||
open_timeout=timeout,
|
||||
**kwargs, # type: ignore
|
||||
)
|
||||
async with connection as ws:
|
||||
yield WebSocket(request=setup, websocket=ws)
|
||||
|
||||
@@ -9,6 +9,7 @@ from .abstract import ReverseDriver as ReverseDriver
|
||||
from .abstract import ReverseMixin as ReverseMixin
|
||||
from .abstract import WebSocketClientMixin as WebSocketClientMixin
|
||||
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 ContentTypes as ContentTypes
|
||||
from .model import Cookies as Cookies
|
||||
|
||||
@@ -19,7 +19,13 @@ from nonebot.typing import (
|
||||
T_BotDisconnectionHook,
|
||||
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 .model import (
|
||||
@@ -246,7 +252,7 @@ class HTTPClientSession(abc.ABC):
|
||||
headers: HeaderTypes = None,
|
||||
cookies: CookieTypes = None,
|
||||
version: str | HTTPVersion = HTTPVersion.H11,
|
||||
timeout: TimeoutTypes = None,
|
||||
timeout: TimeoutTypes | UnsetType = UNSET,
|
||||
proxy: str | None = None,
|
||||
):
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -9,14 +9,21 @@ import urllib.request
|
||||
from multidict import CIMultiDict
|
||||
from yarl import URL as URL
|
||||
|
||||
from nonebot.utils import UNSET, UnsetType
|
||||
|
||||
|
||||
@dataclass
|
||||
class Timeout:
|
||||
"""Request 超时配置。"""
|
||||
|
||||
total: float | None = None
|
||||
connect: float | None = None
|
||||
read: float | None = None
|
||||
total: float | None | UnsetType = UNSET
|
||||
connect: float | None | UnsetType = UNSET
|
||||
read: float | None | UnsetType = UNSET
|
||||
close: float | None | UnsetType = UNSET
|
||||
ping: float | None | UnsetType = UNSET
|
||||
|
||||
|
||||
DEFAULT_TIMEOUT = Timeout(total=None, connect=5.0, read=30.0, close=10.0, ping=20.0)
|
||||
|
||||
|
||||
RawURL: TypeAlias = tuple[bytes, bytes, int | None, bytes]
|
||||
@@ -46,6 +53,7 @@ FileTypes: TypeAlias = (
|
||||
)
|
||||
FilesTypes: TypeAlias = dict[str, FileTypes] | list[tuple[str, FileTypes]] | None
|
||||
TimeoutTypes: TypeAlias = float | Timeout | None
|
||||
PingIntervalTypes: TypeAlias = float | None
|
||||
|
||||
|
||||
class HTTPVersion(Enum):
|
||||
@@ -68,8 +76,9 @@ class Request:
|
||||
json: Any = None,
|
||||
files: FilesTypes = None,
|
||||
version: str | HTTPVersion = HTTPVersion.H11,
|
||||
timeout: TimeoutTypes = None,
|
||||
timeout: TimeoutTypes | UnsetType = UNSET,
|
||||
proxy: str | None = None,
|
||||
ping_interval: PingIntervalTypes | UnsetType = UNSET,
|
||||
):
|
||||
# method
|
||||
self.method: str = (
|
||||
@@ -80,9 +89,11 @@ class Request:
|
||||
# http version
|
||||
self.version: HTTPVersion = HTTPVersion(version)
|
||||
# timeout
|
||||
self.timeout: TimeoutTypes = timeout
|
||||
self.timeout: TimeoutTypes | UnsetType = timeout
|
||||
# proxy
|
||||
self.proxy: str | None = proxy
|
||||
# ping interval
|
||||
self.ping_interval: PingIntervalTypes | UnsetType = ping_interval
|
||||
|
||||
# url
|
||||
if isinstance(url, tuple):
|
||||
|
||||
@@ -19,6 +19,7 @@ from collections.abc import (
|
||||
import contextlib
|
||||
from contextlib import AbstractContextManager, asynccontextmanager
|
||||
import dataclasses
|
||||
from enum import Enum
|
||||
from functools import partial, wraps
|
||||
import importlib
|
||||
import inspect
|
||||
@@ -27,8 +28,12 @@ from pathlib import Path
|
||||
import re
|
||||
from typing import (
|
||||
Any,
|
||||
Final,
|
||||
Generic,
|
||||
Literal,
|
||||
TypeAlias,
|
||||
TypeVar,
|
||||
final,
|
||||
get_args,
|
||||
get_origin,
|
||||
overload,
|
||||
@@ -49,6 +54,8 @@ from nonebot.typing import (
|
||||
type_has_args,
|
||||
)
|
||||
|
||||
from .compat import custom_validation
|
||||
|
||||
P = ParamSpec("P")
|
||||
R = TypeVar("R")
|
||||
T = TypeVar("T")
|
||||
@@ -57,6 +64,54 @@ V = TypeVar("V")
|
||||
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:
|
||||
"""用于记录带颜色日志时转义 `<tag>` 类型特殊标签
|
||||
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "nonebot2"
|
||||
version = "2.4.4"
|
||||
version = "2.5.0"
|
||||
description = "An asynchronous python bot framework."
|
||||
authors = [{ name = "yanyongyu", email = "yyy@nonebot.dev" }]
|
||||
license = "MIT"
|
||||
@@ -44,7 +44,7 @@ all = [
|
||||
dev = [
|
||||
{ include-group = "test" },
|
||||
{ include-group = "docs" },
|
||||
"ruff >=0.14.0, <0.15.0",
|
||||
"ruff >=0.15.0, <0.16.0",
|
||||
"nonemoji >=0.1.2, <0.2.0",
|
||||
"pre-commit >=4.0.0, <5.0.0",
|
||||
]
|
||||
@@ -57,7 +57,7 @@ test = [
|
||||
"pytest-xdist >=3.0.2, <4.0.0",
|
||||
"coverage-conditional-plugin >=0.9.0, <0.10.0",
|
||||
]
|
||||
docs = ["nb-autodoc >=1.0.3, <2.0.0"]
|
||||
docs = ["nb-autodoc >=1.0.4, <2.0.0"]
|
||||
pydantic-v1 = ["pydantic >=1.10.0, <2.0.0"]
|
||||
pydantic-v2 = ["pydantic >=2.0.0, <3.0.0"]
|
||||
|
||||
|
||||
+31
-2
@@ -73,12 +73,41 @@ def test_type_adapter():
|
||||
|
||||
|
||||
def test_model_dump():
|
||||
class NestedModel(BaseModel):
|
||||
hidden: int
|
||||
shown: int
|
||||
|
||||
class TestModel(BaseModel):
|
||||
test1: int
|
||||
test2: int
|
||||
nested: NestedModel
|
||||
items: list[NestedModel]
|
||||
|
||||
assert model_dump(TestModel(test1=1, test2=2), include={"test1"}) == {"test1": 1}
|
||||
assert model_dump(TestModel(test1=1, test2=2), exclude={"test1"}) == {"test2": 2}
|
||||
model = TestModel(
|
||||
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():
|
||||
|
||||
@@ -22,9 +22,11 @@ from nonebot.drivers import (
|
||||
WebSocketClientMixin,
|
||||
WebSocketServerSetup,
|
||||
)
|
||||
from nonebot.drivers.aiohttp import Session as AiohttpSession
|
||||
from nonebot.drivers.aiohttp import WebSocket as AiohttpWebSocket
|
||||
from nonebot.exception import WebSocketClosed
|
||||
from nonebot.params import Depends
|
||||
from nonebot.utils import UNSET
|
||||
from utils import FakeAdapter
|
||||
|
||||
|
||||
@@ -596,6 +598,46 @@ async def test_http_client_session(driver: Driver, server_url: URL):
|
||||
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.parametrize(
|
||||
"driver",
|
||||
@@ -665,6 +707,263 @@ async def test_aiohttp_websocket_close_frame(msg_type: str) -> None:
|
||||
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(
|
||||
("driver", "driver_type"),
|
||||
[
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
import copy
|
||||
import json
|
||||
import pickle
|
||||
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 (
|
||||
UNSET,
|
||||
DataclassEncoder,
|
||||
Unset,
|
||||
UnsetType,
|
||||
escape_tag,
|
||||
exclude_unset,
|
||||
generic_check_issubclass,
|
||||
is_async_gen_callable,
|
||||
is_coroutine_callable,
|
||||
@@ -12,6 +22,29 @@ from nonebot.utils import (
|
||||
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():
|
||||
assert escape_tag("<red>red</red>") == r"\<red>red\</red>"
|
||||
assert escape_tag("<fg #fff>white</fg #fff>") == r"\<fg #fff>white\</fg #fff>"
|
||||
|
||||
@@ -46,7 +46,7 @@ __plugin_meta__ = PluginMetadata(
|
||||
)
|
||||
```
|
||||
|
||||
我们可以看到,插件元数据 `PluginMetadata` 有三个基本属性:插件名称、插件描述、插件使用方法。除此之外,还有几个可选的属性(具体填写见[发布插件](../developer/plugin-publishing.mdx#填写插件元数据)章节):
|
||||
我们可以看到,插件元数据 `PluginMetadata` 有三个基本属性:插件名称、插件描述、插件使用方法。除此之外,还有几个可选的属性(具体填写见[发布插件](../developer/plugin-publishing.mdx#插件元数据)章节):
|
||||
|
||||
- `type`:插件类别,发布插件必填。当前有效类别有:`library`(为其他插件编写提供功能),`application`(向机器人用户提供功能);
|
||||
- `homepage`:插件项目主页,发布插件必填;
|
||||
|
||||
@@ -7,16 +7,65 @@ 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: Permission [@noneflow](https://github.com/noneflow) ([#4022](https://github.com/nonebot/nonebot2/pull/4022))
|
||||
- Plugin: 舞萌排行榜 [@noneflow](https://github.com/noneflow) ([#4013](https://github.com/nonebot/nonebot2/pull/4013))
|
||||
- Plugin: 精华消息管理 [@noneflow](https://github.com/noneflow) ([#4015](https://github.com/nonebot/nonebot2/pull/4015))
|
||||
- Plugin: nonebot-plugin-onebot2tg [@noneflow](https://github.com/noneflow) ([#4020](https://github.com/nonebot/nonebot2/pull/4020))
|
||||
- Plugin: 猫猫糕公开版 [@noneflow](https://github.com/noneflow) ([#4011](https://github.com/nonebot/nonebot2/pull/4011))
|
||||
- Plugin: PicStatus-ng [@noneflow](https://github.com/noneflow) ([#4004](https://github.com/nonebot/nonebot2/pull/4004))
|
||||
- Plugin: B50分析 [@noneflow](https://github.com/noneflow) ([#4002](https://github.com/nonebot/nonebot2/pull/4002))
|
||||
- Plugin: BiliLive [@noneflow](https://github.com/noneflow) ([#3970](https://github.com/nonebot/nonebot2/pull/3970))
|
||||
- Plugin: dot-skill 角色扮演 [@noneflow](https://github.com/noneflow) ([#3996](https://github.com/nonebot/nonebot2/pull/3996))
|
||||
- Plugin: 编程命名风格转换 [@noneflow](https://github.com/noneflow) ([#3990](https://github.com/nonebot/nonebot2/pull/3990))
|
||||
- Plugin: 赛马娘插件 [@noneflow](https://github.com/noneflow) ([#3983](https://github.com/nonebot/nonebot2/pull/3983))
|
||||
- Plugin: Hermes Agent [@noneflow](https://github.com/noneflow) ([#3988](https://github.com/nonebot/nonebot2/pull/3988))
|
||||
- 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))
|
||||
|
||||
### 🍻 适配器发布
|
||||
|
||||
- Adapter: WxClaw [@noneflow](https://github.com/noneflow) ([#3944](https://github.com/nonebot/nonebot2/pull/3944))
|
||||
|
||||
## 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))
|
||||
@@ -25,12 +74,25 @@ toc_max_heading_level: 2
|
||||
|
||||
### 💫 杂项
|
||||
|
||||
- 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))
|
||||
|
||||
@@ -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,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,7 +46,7 @@ __plugin_meta__ = PluginMetadata(
|
||||
)
|
||||
```
|
||||
|
||||
我们可以看到,插件元数据 `PluginMetadata` 有三个基本属性:插件名称、插件描述、插件使用方法。除此之外,还有几个可选的属性(具体填写见[发布插件](../developer/plugin-publishing.mdx#填写插件元数据)章节):
|
||||
我们可以看到,插件元数据 `PluginMetadata` 有三个基本属性:插件名称、插件描述、插件使用方法。除此之外,还有几个可选的属性(具体填写见[发布插件](../developer/plugin-publishing.mdx#插件元数据)章节):
|
||||
|
||||
- `type`:插件类别,发布插件必填。当前有效类别有:`library`(为其他插件编写提供功能),`application`(向机器人用户提供功能);
|
||||
- `homepage`:插件项目主页,发布插件必填;
|
||||
|
||||
@@ -46,7 +46,7 @@ __plugin_meta__ = PluginMetadata(
|
||||
)
|
||||
```
|
||||
|
||||
我们可以看到,插件元数据 `PluginMetadata` 有三个基本属性:插件名称、插件描述、插件使用方法。除此之外,还有几个可选的属性(具体填写见[发布插件](../developer/plugin-publishing.mdx#填写插件元数据)章节):
|
||||
我们可以看到,插件元数据 `PluginMetadata` 有三个基本属性:插件名称、插件描述、插件使用方法。除此之外,还有几个可选的属性(具体填写见[发布插件](../developer/plugin-publishing.mdx#插件元数据)章节):
|
||||
|
||||
- `type`:插件类别,发布插件必填。当前有效类别有:`library`(为其他插件编写提供功能),`application`(向机器人用户提供功能);
|
||||
- `homepage`:插件项目主页,发布插件必填;
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@ __plugin_meta__ = PluginMetadata(
|
||||
)
|
||||
```
|
||||
|
||||
我们可以看到,插件元数据 `PluginMetadata` 有三个基本属性:插件名称、插件描述、插件使用方法。除此之外,还有几个可选的属性(具体填写见[发布插件](../developer/plugin-publishing.mdx#填写插件元数据)章节):
|
||||
我们可以看到,插件元数据 `PluginMetadata` 有三个基本属性:插件名称、插件描述、插件使用方法。除此之外,还有几个可选的属性(具体填写见[发布插件](../developer/plugin-publishing.mdx#插件元数据)章节):
|
||||
|
||||
- `type`:插件类别,发布插件必填。当前有效类别有:`library`(为其他插件编写提供功能),`application`(向机器人用户提供功能);
|
||||
- `homepage`:插件项目主页,发布插件必填;
|
||||
+2
-2
@@ -623,7 +623,7 @@ description: nonebot.adapters 模块
|
||||
### _method_ `__add__(other)` {#MessageSegment---add--}
|
||||
|
||||
- **参数**
|
||||
- `other` (str | TMS | Iterable[TMS])
|
||||
- `other` (str | Self | Iterable[Self])
|
||||
|
||||
- **返回**
|
||||
- TM
|
||||
@@ -668,7 +668,7 @@ description: nonebot.adapters 模块
|
||||
### _method_ `join(iterable)` {#MessageSegment-join}
|
||||
|
||||
- **参数**
|
||||
- `iterable` (Iterable[TMS | TM])
|
||||
- `iterable` (Iterable[Self | TM])
|
||||
|
||||
- **返回**
|
||||
- TM
|
||||
+19
-12
@@ -11,6 +11,12 @@ description: nonebot.compat 模块
|
||||
|
||||
为兼容 Pydantic V1 与 V2 版本,定义了一系列兼容函数与类供使用。
|
||||
|
||||
## _var_ `ModelDumpIncEx` {#ModelDumpIncEx}
|
||||
|
||||
- **类型:** set[int] | set[str] | dict[int, [ModelDumpIncEx](#ModelDumpIncEx)] | dict[str, [ModelDumpIncEx](#ModelDumpIncEx)] | None
|
||||
|
||||
- **说明:** Common include/exclude shape accepted by all supported pydantic versions.
|
||||
|
||||
## _var_ `Required` {#Required}
|
||||
|
||||
- **类型:** untyped
|
||||
@@ -31,6 +37,17 @@ description: nonebot.compat 模块
|
||||
|
||||
- **说明:** Default config for validations
|
||||
|
||||
## _def_ `LegacyUnionField(<auto>)` {#LegacyUnionField}
|
||||
|
||||
- **说明:** Mark field to use legacy left to right union mode
|
||||
|
||||
- **参数**
|
||||
|
||||
auto
|
||||
|
||||
- **返回**
|
||||
- untyped
|
||||
|
||||
## _class_ `FieldInfo(default=PydanticUndefined, **kwargs)` {#FieldInfo}
|
||||
|
||||
- **说明:** FieldInfo class with extra property for compatibility with pydantic v1
|
||||
@@ -111,16 +128,6 @@ description: nonebot.compat 模块
|
||||
- **返回**
|
||||
- Any
|
||||
|
||||
## _def_ `extract_field_info(field_info)` {#extract-field-info}
|
||||
|
||||
- **说明:** Get FieldInfo init kwargs from a FieldInfo instance.
|
||||
|
||||
- **参数**
|
||||
- `field_info` (BaseFieldInfo)
|
||||
|
||||
- **返回**
|
||||
- dict[str, Any]
|
||||
|
||||
## _def_ `model_fields(model)` {#model-fields}
|
||||
|
||||
- **说明:** Get field list of a model.
|
||||
@@ -146,9 +153,9 @@ description: nonebot.compat 模块
|
||||
- **参数**
|
||||
- `model` (BaseModel)
|
||||
|
||||
- `include` (set[str] | None)
|
||||
- `include` ([ModelDumpIncEx](#ModelDumpIncEx))
|
||||
|
||||
- `exclude` (set[str] | None)
|
||||
- `exclude` ([ModelDumpIncEx](#ModelDumpIncEx))
|
||||
|
||||
- `by_alias` (bool)
|
||||
|
||||
+24
-4
@@ -19,7 +19,7 @@ pip install nonebot2[aiohttp]
|
||||
本驱动仅支持客户端连接
|
||||
:::
|
||||
|
||||
## _class_ `Session(params=None, headers=None, cookies=None, version=HTTPVersion.H11, timeout=None, proxy=None)` {#Session}
|
||||
## _class_ `Session(params=None, headers=None, cookies=None, version=HTTPVersion.H11, timeout=UNSET, proxy=None)` {#Session}
|
||||
|
||||
- **参数**
|
||||
- `params` (QueryTypes)
|
||||
@@ -30,7 +30,7 @@ pip install nonebot2[aiohttp]
|
||||
|
||||
- `version` (str | [HTTPVersion](index.md#HTTPVersion))
|
||||
|
||||
- `timeout` (float | None)
|
||||
- `timeout` (TimeoutTypes | UnsetType)
|
||||
|
||||
- `proxy` (str | None)
|
||||
|
||||
@@ -42,6 +42,16 @@ pip install nonebot2[aiohttp]
|
||||
- **返回**
|
||||
- [Response](index.md#Response)
|
||||
|
||||
### _method_ `stream_request(setup, *, chunk_size=1024)` {#Session-stream-request}
|
||||
|
||||
- **参数**
|
||||
- `setup` ([Request](index.md#Request))
|
||||
|
||||
- `chunk_size` (int)
|
||||
|
||||
- **返回**
|
||||
- AsyncGenerator[[Response](index.md#Response), None]
|
||||
|
||||
### _async method_ `setup()` {#Session-setup}
|
||||
|
||||
- **参数**
|
||||
@@ -76,6 +86,16 @@ pip install nonebot2[aiohttp]
|
||||
- **返回**
|
||||
- [Response](index.md#Response)
|
||||
|
||||
### _method_ `stream_request(setup, *, chunk_size=1024)` {#Mixin-stream-request}
|
||||
|
||||
- **参数**
|
||||
- `setup` ([Request](index.md#Request))
|
||||
|
||||
- `chunk_size` (int)
|
||||
|
||||
- **返回**
|
||||
- AsyncGenerator[[Response](index.md#Response), None]
|
||||
|
||||
### _method_ `websocket(setup)` {#Mixin-websocket}
|
||||
|
||||
- **参数**
|
||||
@@ -84,7 +104,7 @@ pip install nonebot2[aiohttp]
|
||||
- **返回**
|
||||
- AsyncGenerator[[WebSocket](index.md#WebSocket), None]
|
||||
|
||||
### _method_ `get_session(params=None, headers=None, cookies=None, version=HTTPVersion.H11, timeout=None, proxy=None)` {#Mixin-get-session}
|
||||
### _method_ `get_session(params=None, headers=None, cookies=None, version=HTTPVersion.H11, timeout=UNSET, proxy=None)` {#Mixin-get-session}
|
||||
|
||||
- **参数**
|
||||
- `params` (QueryTypes)
|
||||
@@ -95,7 +115,7 @@ pip install nonebot2[aiohttp]
|
||||
|
||||
- `version` (str | [HTTPVersion](index.md#HTTPVersion))
|
||||
|
||||
- `timeout` (float | None)
|
||||
- `timeout` (TimeoutTypes | UnsetType)
|
||||
|
||||
- `proxy` (str | None)
|
||||
|
||||
+23
-3
@@ -19,7 +19,7 @@ pip install nonebot2[httpx]
|
||||
本驱动仅支持客户端 HTTP 连接
|
||||
:::
|
||||
|
||||
## _class_ `Session(params=None, headers=None, cookies=None, version=HTTPVersion.H11, timeout=None, proxy=None)` {#Session}
|
||||
## _class_ `Session(params=None, headers=None, cookies=None, version=HTTPVersion.H11, timeout=UNSET, proxy=None)` {#Session}
|
||||
|
||||
- **参数**
|
||||
- `params` (QueryTypes)
|
||||
@@ -30,7 +30,7 @@ pip install nonebot2[httpx]
|
||||
|
||||
- `version` (str | [HTTPVersion](index.md#HTTPVersion))
|
||||
|
||||
- `timeout` (float | None)
|
||||
- `timeout` (TimeoutTypes | UnsetType)
|
||||
|
||||
- `proxy` (str | None)
|
||||
|
||||
@@ -42,6 +42,16 @@ pip install nonebot2[httpx]
|
||||
- **返回**
|
||||
- [Response](index.md#Response)
|
||||
|
||||
### _method_ `stream_request(setup, *, chunk_size=1024)` {#Session-stream-request}
|
||||
|
||||
- **参数**
|
||||
- `setup` ([Request](index.md#Request))
|
||||
|
||||
- `chunk_size` (int)
|
||||
|
||||
- **返回**
|
||||
- AsyncGenerator[[Response](index.md#Response), None]
|
||||
|
||||
### _async method_ `setup()` {#Session-setup}
|
||||
|
||||
- **参数**
|
||||
@@ -76,6 +86,16 @@ pip install nonebot2[httpx]
|
||||
- **返回**
|
||||
- [Response](index.md#Response)
|
||||
|
||||
### _method_ `stream_request(setup, *, chunk_size=1024)` {#Mixin-stream-request}
|
||||
|
||||
- **参数**
|
||||
- `setup` ([Request](index.md#Request))
|
||||
|
||||
- `chunk_size` (int)
|
||||
|
||||
- **返回**
|
||||
- AsyncGenerator[[Response](index.md#Response), None]
|
||||
|
||||
### _method_ `get_session(params=None, headers=None, cookies=None, version=HTTPVersion.H11, timeout=None, proxy=None)` {#Mixin-get-session}
|
||||
|
||||
- **参数**
|
||||
@@ -87,7 +107,7 @@ pip install nonebot2[httpx]
|
||||
|
||||
- `version` (str | [HTTPVersion](index.md#HTTPVersion))
|
||||
|
||||
- `timeout` (float | None)
|
||||
- `timeout` (TimeoutTypes)
|
||||
|
||||
- `proxy` (str | None)
|
||||
|
||||
+27
-5
@@ -11,6 +11,10 @@ description: nonebot.drivers 模块
|
||||
|
||||
各驱动请继承以下基类。
|
||||
|
||||
## _var_ `DEFAULT_TIMEOUT` {#DEFAULT-TIMEOUT}
|
||||
|
||||
- **类型:** untyped
|
||||
|
||||
## _abstract class_ `ASGIMixin(<auto>)` {#ASGIMixin}
|
||||
|
||||
- **说明**
|
||||
@@ -279,6 +283,18 @@ description: nonebot.drivers 模块
|
||||
- **返回**
|
||||
- [Response](#Response)
|
||||
|
||||
### _abstract method_ `stream_request(setup, *, chunk_size=1024)` {#HTTPClientMixin-stream-request}
|
||||
|
||||
- **说明:** 发送一个 HTTP 流式请求
|
||||
|
||||
- **参数**
|
||||
- `setup` ([Request](#Request))
|
||||
|
||||
- `chunk_size` (int)
|
||||
|
||||
- **返回**
|
||||
- AsyncGenerator[[Response](#Response), None]
|
||||
|
||||
### _abstract method_ `get_session(params=None, headers=None, cookies=None, version=HTTPVersion.H11, timeout=None, proxy=None)` {#HTTPClientMixin-get-session}
|
||||
|
||||
- **说明:** 获取一个 HTTP 会话
|
||||
@@ -292,7 +308,7 @@ description: nonebot.drivers 模块
|
||||
|
||||
- `version` (str | [HTTPVersion](#HTTPVersion))
|
||||
|
||||
- `timeout` (float | None)
|
||||
- `timeout` (TimeoutTypes)
|
||||
|
||||
- `proxy` (str | None)
|
||||
|
||||
@@ -309,8 +325,6 @@ description: nonebot.drivers 模块
|
||||
|
||||
## _enum_ `HTTPVersion` {#HTTPVersion}
|
||||
|
||||
- **说明:** An enumeration.
|
||||
|
||||
- **参数**
|
||||
|
||||
auto
|
||||
@@ -334,7 +348,7 @@ description: nonebot.drivers 模块
|
||||
|
||||
- **说明:** 混入驱动类型名称
|
||||
|
||||
## _class_ `Request(method, url, *, params=None, headers=None, cookies=None, content=None, data=None, json=None, files=None, version=HTTPVersion.H11, timeout=None, proxy=None)` {#Request}
|
||||
## _class_ `Request(method, url, *, params=None, headers=None, cookies=None, content=None, data=None, json=None, files=None, version=HTTPVersion.H11, timeout=UNSET, proxy=None)` {#Request}
|
||||
|
||||
- **参数**
|
||||
- `method` (str | bytes)
|
||||
@@ -357,7 +371,7 @@ description: nonebot.drivers 模块
|
||||
|
||||
- `version` (str | HTTPVersion)
|
||||
|
||||
- `timeout` (float | None)
|
||||
- `timeout` (TimeoutTypes | UnsetType)
|
||||
|
||||
- `proxy` (str | None)
|
||||
|
||||
@@ -386,6 +400,14 @@ description: nonebot.drivers 模块
|
||||
|
||||
- **说明:** 服务端混入基类。
|
||||
|
||||
- **参数**
|
||||
|
||||
auto
|
||||
|
||||
## _class_ `Timeout(<auto>)` {#Timeout}
|
||||
|
||||
- **说明:** Request 超时配置。
|
||||
|
||||
- **参数**
|
||||
|
||||
auto
|
||||
+1
-1
@@ -50,7 +50,7 @@ pip install nonebot2[websockets]
|
||||
- **参数**
|
||||
- `request` ([Request](index.md#Request))
|
||||
|
||||
- `websocket` (WebSocketClientProtocol)
|
||||
- `websocket` (ClientConnection)
|
||||
|
||||
### _async method_ `accept()` {#WebSocket-accept}
|
||||
|
||||
+10
-1
@@ -584,7 +584,16 @@ description: nonebot.matcher 模块
|
||||
- **返回**
|
||||
- list[type[[Matcher](#Matcher)]] | None
|
||||
|
||||
**2.** `(key, default) -> list[type[Matcher]] | T`
|
||||
**2.** `(key, default) -> list[type[Matcher]]`
|
||||
- **参数**
|
||||
- `key` (int)
|
||||
|
||||
- `default` (list[type[[Matcher](#Matcher)]])
|
||||
|
||||
- **返回**
|
||||
- list[type[[Matcher](#Matcher)]]
|
||||
|
||||
**3.** `(key, default) -> list[type[Matcher]] | T`
|
||||
- **参数**
|
||||
- `key` (int)
|
||||
|
||||
+13
@@ -80,6 +80,19 @@ description: nonebot.plugin.load 模块
|
||||
|
||||
- **用法**
|
||||
|
||||
新格式:
|
||||
|
||||
```toml title=pyproject.toml
|
||||
[tool.nonebot]
|
||||
plugin_dirs = ["some_dir"]
|
||||
|
||||
[tool.nonebot.plugins]
|
||||
some-store-plugin = ["some_store_plugin"]
|
||||
"@local" = ["some_local_plugin"]
|
||||
```
|
||||
|
||||
旧格式:
|
||||
|
||||
```toml title=pyproject.toml
|
||||
[tool.nonebot]
|
||||
plugins = ["some_plugin"]
|
||||
+6
@@ -132,6 +132,12 @@ description: nonebot.plugin.model 模块
|
||||
|
||||
- **说明:** 子插件集合
|
||||
|
||||
### _class-var_ `metadata` {#Plugin-metadata}
|
||||
|
||||
- **类型:** PluginMetadata | None
|
||||
|
||||
- **说明:** 插件元信息
|
||||
|
||||
### _property_ `id_` {#Plugin-id-}
|
||||
|
||||
- **类型:** str
|
||||
+8
@@ -74,6 +74,14 @@ description: nonebot.typing 模块
|
||||
|
||||
- **说明:** 判断是否是 None 类型
|
||||
|
||||
- **参数**
|
||||
- `type_` (type[Any])
|
||||
|
||||
- **返回**
|
||||
- bool
|
||||
|
||||
## _def_ `is_type_alias_type(type_)` {#is-type-alias-type}
|
||||
|
||||
- **参数**
|
||||
- `type_` (type[Any])
|
||||
|
||||
+19
-3
@@ -9,6 +9,20 @@ description: nonebot.utils 模块
|
||||
|
||||
本模块包含了 NoneBot 的一些工具函数
|
||||
|
||||
## _enum_ `Unset` {#Unset}
|
||||
|
||||
- **参数**
|
||||
|
||||
auto
|
||||
|
||||
## _def_ `exclude_unset(data)` {#exclude-unset}
|
||||
|
||||
- **参数**
|
||||
- `data` (Any)
|
||||
|
||||
- **返回**
|
||||
- Any
|
||||
|
||||
## _def_ `escape_tag(s)` {#escape-tag}
|
||||
|
||||
- **说明**
|
||||
@@ -54,13 +68,15 @@ description: nonebot.utils 模块
|
||||
检查 cls 是否是 class_or_tuple 中的一个类型子类。
|
||||
|
||||
特别的:
|
||||
- 如果 cls 是 `typing.TypeVar` 类型,
|
||||
则会检查其 `__bound__` 或 `__constraints__`
|
||||
是否是 class_or_tuple 中一个类型的子类或 None。
|
||||
- 如果 cls 是 `typing.Union` 或 `types.UnionType` 类型,
|
||||
则会检查其中的所有类型是否是 class_or_tuple 中一个类型的子类或 None。
|
||||
- 如果 cls 是 `typing.Literal` 类型,
|
||||
则会检查其中的所有值是否是 class_or_tuple 中一个类型的实例。
|
||||
- 如果 cls 是 `typing.TypeVar` 类型,
|
||||
则会检查其 `__bound__` 或 `__constraints__`
|
||||
是否是 class_or_tuple 中一个类型的子类或 None。
|
||||
- 如果 cls 是 `typing.List`、`typing.Dict` 等泛型类型,
|
||||
则会检查其原始类型是否是 class_or_tuple 中一个类型的子类。
|
||||
|
||||
- **参数**
|
||||
- `cls` (Any)
|
||||
+9
-3
@@ -84,7 +84,7 @@ export CUSTOM_CONFIG='config in environment variables'
|
||||
那最终 NoneBot 所读取的内容为环境变量中的内容,即 `config in environment variables`。
|
||||
|
||||
:::caution 注意
|
||||
NoneBot 不会自发读取未被定义的配置项的环境变量,如果需要读取某一环境变量需要在 dotenv 配置文件中进行声明。
|
||||
如果一个环境变量既不是 NoneBot 的[**内置配置项**](#内置配置项),也不是任何插件所定义的[**插件配置**](#插件配置),那么 NoneBot 不会自发读取该环境变量,需要在 dotenv 配置文件中先行声明。
|
||||
:::
|
||||
|
||||
### dotenv 配置文件
|
||||
@@ -242,11 +242,17 @@ weather = on_command(
|
||||
|
||||
这种方式可以简洁、高效地读取配置项,同时也可以设置默认值或者在运行时对配置项进行合法性检查,防止由于配置项导致的插件出错等情况出现。
|
||||
|
||||
:::tip 提示
|
||||
:::tip 可配置的事件响应优先级
|
||||
发布插件应该为自身的事件响应器提供可配置的优先级,以便插件使用者可以自定义多个插件间的响应顺序。
|
||||
:::
|
||||
|
||||
由于插件配置项是从全局配置中读取的,通常我们需要在配置项名称前面添加前缀名,以防止配置项冲突。例如在上方的示例中,我们就添加了配置项前缀 `weather_`。但是这样会导致在使用配置项时过长的变量名,因此我们可以使用 `pydantic` 的 `alias` 或者通过配置 scope 来简化配置项名称。这里我们以 scope 配置为例:
|
||||
:::tip 插件配置获取逻辑
|
||||
无论是否在 dotenv 文件中声明了插件配置项,使用 `get_plugin_config` 获取插件配置模型中定义的配置项时都遵循[**配置项的加载**](#配置项的加载)一节中的优先级顺序进行读取。
|
||||
:::
|
||||
|
||||
### 避免插件配置名称冲突
|
||||
|
||||
由于插件配置项是从全局配置和环境变量中读取的,通常我们需要在配置项名称前面添加前缀名,以防止配置项冲突。例如在上方的示例中,我们就添加了配置项前缀 `weather_`。但是这样会导致使用配置项时变量名过长,此时我们可以使用 `pydantic` 的 `alias` 或者通过配置 scope 来简化配置项名称。这里我们以 scope 配置为例:
|
||||
|
||||
```python title=weather/config.py
|
||||
from pydantic import BaseModel
|
||||
@@ -0,0 +1,162 @@
|
||||
import Tabs from "@theme/Tabs";
|
||||
import TabItem from "@theme/TabItem";
|
||||
|
||||
# Alconna 插件
|
||||
|
||||
[`nonebot-plugin-alconna`](https://github.com/nonebot/plugin-alconna) 是一类极大地提升了 NoneBot 开发体验的插件。
|
||||
|
||||
该插件可分为三个部分:
|
||||
|
||||
- 增强的命令解析: 基于 [Alconna](https://github.com/ArcletProject/Alconna), 提供一类新的事件响应器辅助函数 `on_alconna`. 相比 `on_command`, `on_shell`, `on_regex` 等函数,`on_alconna` 提供了更强大的命令解析能力与诸多特性。
|
||||
- 通用消息组件: 实现了跨平台接收、发送、撤回、编辑、表态消息的功能。
|
||||
- `UniMessage` 通用消息模型,支持各适配器下的消息转换和导出,发送。
|
||||
- `Text`, `Image`, `At` 等通用消息段模型,既与 `UniMessage` 配合使用,又能用于 `Alconna` 的命令解析。
|
||||
- `message_recall`, `message_edit`, `message_reaction` 等功能函数。
|
||||
- `Target` 通用消息目标模型,并通过该模型进行主动消息发送。
|
||||
- `UniMsg`, `MsgId`, `MsgTarget`, `at_in`, `at_me` 等提供给 nonebot 使用的依赖注入和 `Rule`。
|
||||
- 内置功能插件:基于上述部分实现的内置功能插件。
|
||||
- `echo`: 通过 `on_alconna` 实现的 echo 插件,支持回显回复消息。
|
||||
- `help`: 列出所有 `on_alconna` 事件响应器的帮助信息或其对应的插件信息。
|
||||
- `lang`: 切换 `Alconna` 使用的语言
|
||||
- `switch`: 禁用/启用某个指令
|
||||
- `with`: 针对具有多个子命令的指令,通过 `with` 在当前会话中载入命令头以节省输入。
|
||||
|
||||
以最新版本为例 (v0.59), 本插件已支持 NoneBot 生态中几乎所有的适配器, 包括:
|
||||
|
||||
| 协议名称 | 路径 |
|
||||
| ------------------------------------------------------------------- | ------------------------------------ |
|
||||
| [OneBot 协议](https://onebot.dev/) | adapters.onebot11, adapters.onebot12 |
|
||||
| [Telegram](https://core.telegram.org/bots/api) | adapters.telegram |
|
||||
| [飞书](https://open.feishu.cn/document/home/index) | adapters.feishu |
|
||||
| [GitHub](https://docs.github.com/en/developers/apps) | adapters.github |
|
||||
| [QQ bot](https://github.com/nonebot/adapter-qq) | adapters.qq |
|
||||
| [钉钉](https://open.dingtalk.com/document/) | adapters.ding |
|
||||
| [Console](https://github.com/nonebot/adapter-console) | adapters.console |
|
||||
| [开黑啦](https://developer.kookapp.cn/) | adapters.kook |
|
||||
| [Mirai](https://docs.mirai.mamoe.net/mirai-api-http/) | adapters.mirai |
|
||||
| [Ntchat](https://github.com/JustUndertaker/adapter-ntchat) | adapters.ntchat |
|
||||
| [MineCraft](https://github.com/17TheWord/nonebot-adapter-minecraft) | adapters.minecraft |
|
||||
| [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 |
|
||||
| [Dodo IM](https://github.com/nonebot/adapter-dodo) | adapters.dodo |
|
||||
| [Kritor](https://github.com/nonebot/adapter-kritor) | adapters.kritor |
|
||||
| [Tailchat](https://github.com/eya46/nonebot-adapter-tailchat) | adapters.tailchat |
|
||||
| [Mail](https://github.com/mobyw/nonebot-adapter-mail) | adapters.mail |
|
||||
| [微信公众号](https://github.com/YangRucheng/nonebot-adapter-wxmp) | adapters.wxmp |
|
||||
| [黑盒语音](https://github.com/lclbm/adapter-heybox) | adapters.heybox |
|
||||
| [Milky](https://github.com/nonebot/adapter-milky) | adapters.milky |
|
||||
| [EFChat](https://github.com/molanp/nonebot_adapter_efchat) | adapters.efchat |
|
||||
|
||||
## 安装插件
|
||||
|
||||
在使用前请先安装 `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` 作为插件,因此需要在使用前对其进行**加载**。使用 `require` 方法可轻松完成这一过程,可参考 [跨插件访问](../../advanced/requiring.md) 一节进行了解。
|
||||
|
||||
```python
|
||||
from nonebot import require
|
||||
|
||||
require("nonebot_plugin_alconna")
|
||||
|
||||
from nonebot_plugin_alconna import ...
|
||||
```
|
||||
|
||||
## 使用插件
|
||||
|
||||
在前面的[深入指南](../../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/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)
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"label": "命令解析拓展",
|
||||
"position": 6
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
---
|
||||
sidebar_position: 7
|
||||
description: 内置组件
|
||||
---
|
||||
|
||||
import Messenger from "@site/src/components/Messenger";
|
||||
|
||||
# 内置组件
|
||||
|
||||
`nonebot_plugin_alconna` 插件提供了一系列内置组件以提升开发者和用户体验。
|
||||
|
||||
## 内置插件
|
||||
|
||||
类似于 Nonebot 本身提供的内置插件,`nonebot_plugin_alconna` 提供了多个内置插件。
|
||||
|
||||
### 加载
|
||||
|
||||
你可以用本插件的 `load_builtin_plugin(s)` 来加载它们:
|
||||
|
||||
```python
|
||||
from nonebot_plugin_alconna import load_builtin_plugin, load_builtin_plugins
|
||||
|
||||
load_builtin_plugins("echo")
|
||||
load_builtin_plugins("help", "with")
|
||||
```
|
||||
|
||||
### 使用
|
||||
|
||||
#### echo
|
||||
|
||||
`echo` 插件能将用户发送的消息原样返回。
|
||||
|
||||
<Messenger
|
||||
msgs={[
|
||||
{ position: "right", msg: "/echo hello world!" },
|
||||
{ position: "left", msg: "hello world!" },
|
||||
{ position: "right", msg: "/echo [图片]" },
|
||||
{ position: "left", msg: "[图片]" },
|
||||
]}
|
||||
/>
|
||||
|
||||
#### help
|
||||
|
||||
`help` 插件能列出所有 Alconna 指令。同时还能查询某个指令对应的插件信息。
|
||||
|
||||
<Messenger
|
||||
msgs={[
|
||||
{ position: "right", msg: "/帮助" },
|
||||
{
|
||||
position: "left",
|
||||
msg: "# 当前可用的命令有:\n 【0】/echo : echo 指令\n 【1】/help : 显示所有命令帮助\n# 输入'命令名 -h|--help' 查看特定命令的语法",
|
||||
},
|
||||
{ position: "right", msg: "/help --plugin-info echo" },
|
||||
{
|
||||
position: "left",
|
||||
msg: "插件名称: echo\n插件标识: nonebot_plugin_alconna:echo\n插件模块: nonebot-plugin-alconna\n插件版本: 0.57.2\n插件路径: nonebot_plugin_alconna.builtins.plugins.echo",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
help 插件的帮助信息如下:
|
||||
|
||||
```
|
||||
/help <query: str = -1>
|
||||
## 注释
|
||||
query: 选择某条命令的id或者名称查看具体帮助
|
||||
显示所有命令帮助
|
||||
用法:
|
||||
可以使用 --hide 参数来显示隐藏命令,使用 -P 参数来显示命令所属插件名称
|
||||
|
||||
可用的子命令有:
|
||||
* 是否列出命令所属命名空间
|
||||
-N│--namespace│命名空间 [target: str]
|
||||
## 注释
|
||||
target: 指定的命名空间
|
||||
该子命令内可用的选项有:
|
||||
* 列出所有命名空间
|
||||
--list
|
||||
可用的选项有:
|
||||
* 查看指定页数的命令帮助
|
||||
--page <index: int>
|
||||
* 查看命令所属插件的信息
|
||||
-P│插件信息│--plugin-info
|
||||
* 是否列出隐藏命令
|
||||
隐藏│-H│--hide
|
||||
```
|
||||
|
||||
#### lang
|
||||
|
||||
`lang` 插件能切换 i18n 的语言设置。
|
||||
|
||||
<Messenger
|
||||
msgs={[
|
||||
{ position: "right", msg: "/lang list" },
|
||||
{
|
||||
position: "left",
|
||||
msg: "支持的语言列表:\n * en-US\n * zh-CN",
|
||||
},
|
||||
{ position: "right", msg: "/lang switch en-US" },
|
||||
{ position: "left", msg: "Switch to 'en-US' successfully." },
|
||||
]}
|
||||
/>
|
||||
|
||||
lang 插件的帮助信息如下:
|
||||
|
||||
```
|
||||
/lang
|
||||
i18n配置相关功能
|
||||
|
||||
可用的选项有:
|
||||
* 查看支持的语言列表
|
||||
list [name: str]
|
||||
* 切换语言
|
||||
switch [locale: str]
|
||||
```
|
||||
|
||||
其中 `list` 选项可以查找某一插件下的语言支持情况 (例如 `/lang list nonebot_plugin_alconna`)。
|
||||
|
||||
#### switch
|
||||
|
||||
`switch` 插件能用来启用/禁用某个命令,其使用方法与 `help` 类似。
|
||||
|
||||
<Messenger
|
||||
msgs={[
|
||||
{ position: "right", msg: "/disable" },
|
||||
{
|
||||
position: "left",
|
||||
msg: "【0】/echo : echo 指令\n【1】/help : 显示所有命令帮助\n【2】/lang : i18n配置相关功能",
|
||||
},
|
||||
{ position: "right", msg: "/disable 0" },
|
||||
{ position: "left", msg: "已禁用 /echo" },
|
||||
{ position: "right", msg: "/echo 1234" },
|
||||
{ position: "right", msg: "/enable echo" },
|
||||
{ position: "left", msg: "已启用 /echo" },
|
||||
{ position: "right", msg: "/echo 1234" },
|
||||
{ position: "left", msg: "1234" },
|
||||
]}
|
||||
/>
|
||||
|
||||
#### with
|
||||
|
||||
`with` 插件能在当前会话中设置一个局部命令前缀,以便于有多个子命令的指令使用。
|
||||
|
||||
<Messenger
|
||||
msgs={[
|
||||
{ position: "right", msg: "/with" },
|
||||
{
|
||||
position: "left",
|
||||
msg: "当前群组未设置前缀",
|
||||
},
|
||||
{ position: "right", msg: "/with lang" },
|
||||
{ position: "left", msg: "设置前缀成功" },
|
||||
{ position: "right", msg: "list" },
|
||||
{
|
||||
position: "left",
|
||||
msg: "支持的语言列表:\n * en-US\n * zh-CN",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
with 插件的帮助信息如下:
|
||||
|
||||
```
|
||||
.with [name: str]
|
||||
with 指令
|
||||
用法:
|
||||
设置局部命令前缀
|
||||
|
||||
可用的选项有:
|
||||
* 设置可能的生效时间
|
||||
--expire│expire <time: datetime>
|
||||
* 取消当前前缀
|
||||
unset│--unset
|
||||
|
||||
快捷命令:
|
||||
'[.]局部前缀' => [.]with
|
||||
```
|
||||
|
||||
### 配置
|
||||
|
||||
内置插件也有其配置项,并且均以 `NBP_ALC` 开头。
|
||||
|
||||
- `nbp_alc_echo_tome`: 是否让 `echo` 插件的消息经过 `to_me` 处理
|
||||
- `nbp_alc_page_size`: `help` 与 `switch` 插件的共同配置项,表示每页显示的命令数量
|
||||
- `nbp_alc_help_text`: `help` 指令的指令名,默认为 "help"
|
||||
- `nbp_alc_help_alias`: `help` 指令的别名,默认为 "帮助", "命令帮助"
|
||||
- `nbp_alc_help_all_alias`: `help` 指令显示隐藏指令时的别名,默认为 "所有帮助", "所有命令帮助"
|
||||
- `nbp_alc_switch_enable`: `switch` 插件的 `enable` 指令的指令名,默认为 "enable"
|
||||
- `nbp_alc_switch_enable_alias`: `switch` 插件的 `enable` 指令的别名,默认为 "启用", "启用指令"
|
||||
- `nbp_alc_switch_disable`: `switch` 插件的 `disable` 指令的指令名,默认为 "disable"
|
||||
- `nbp_alc_switch_disable_alias`: `switch` 插件的 `disable` 指令的别名,默认为 "disable", "禁用", "禁用指令"
|
||||
- `nbp_alc_with_text`: `with` 插件的指令名,默认为 "with"
|
||||
- `nbp_alc_with_alias`: `with` 插件的别名,默认为 "局部前缀"
|
||||
|
||||
## 内置匹配拓展
|
||||
|
||||
目前插件提供了 5 个内置的 `Extension`,它们在 `nonebot_plugin_alconna.builtins.extensions` 下:
|
||||
|
||||
### ReplyRecordExtension
|
||||
|
||||
`ReplyRecordExtension` 可将消息事件中的回复暂存在 extension 中,使得解析用的消息不带回复信息,同时可以在后续的处理中获取回复信息:
|
||||
|
||||
```python
|
||||
from nonebot_plugin_alconna import MsgId, on_alconna
|
||||
from nonebot_plugin_alconna.builtins.extensions import ReplyRecordExtension
|
||||
|
||||
matcher = on_alconna("...", extensions=[ReplyRecordExtension()])
|
||||
|
||||
@matcher.handle()
|
||||
async def handle(msg_id: MsgId, ext: ReplyRecordExtension):
|
||||
if reply := ext.get_reply(msg_id):
|
||||
...
|
||||
else:
|
||||
...
|
||||
```
|
||||
|
||||
### ReplyMergeExtension
|
||||
|
||||
`ReplyMergeExtension` 可将消息事件中的回复指向的原消息合并到当前消息中作为一部分参数:
|
||||
|
||||
```python
|
||||
from nonebot_plugin_alconna import Match, on_alconna
|
||||
from nonebot_plugin_alconna.builtins.extensions.reply import ReplyMergeExtension
|
||||
|
||||
matcher = on_alconna("...", extensions=[ReplyMergeExtension()])
|
||||
|
||||
@matcher.handle()
|
||||
async def handle(content: Match[str]):
|
||||
...
|
||||
```
|
||||
|
||||
其构造时可传入两个参数:
|
||||
|
||||
- `add_left`: 否在当前消息的左侧合并回复消息,默认为 False
|
||||
- `sep`: 合并时的分隔符,默认为空格
|
||||
|
||||
### DiscordSlashExtension
|
||||
|
||||
`DiscordSlashExtension` 可自动将 Alconna 对象翻译成 Discord 的 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], ext: DiscordSlashExtension):
|
||||
await ext.send_followup_msg(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}")
|
||||
```
|
||||
|
||||
### MarkdownOutputExtension
|
||||
|
||||
`MarkdownOutputExtension` 可将 Alconna 的自动输出转换为 Markdown 格式
|
||||
|
||||
其构造时可传入两个参数:
|
||||
|
||||
- `escape_dot`: 是否转义句中的点号(用来避免被识别为 url)
|
||||
- `text_to_image` 将文本转换为图片的函数,可不传入。一般用来设置渲染 markdown 为图片的函数
|
||||
|
||||
### TelegramSlashExtension
|
||||
|
||||
`TelegramSlashExtension` 可将 Alconna 的命令注册在 Telegram 上以获得提示,类似于 `DiscordSlashExtension`。
|
||||
|
||||
```python
|
||||
from nonebot_plugin_alconna import on_alconna
|
||||
from nonebot.adapters.telegram.model import BotCommandScopeChat
|
||||
from nonebot_plugin_alconna.builtins.extensions.telegram import TelegramSlashExtension
|
||||
|
||||
TelegramSlashExtension.set_scope(BotCommandScopeChat())
|
||||
|
||||
matcher = on_alconna("...", extensions=[TelegramSlashExtension()])
|
||||
```
|
||||
|
||||
## 内置自定义消息段
|
||||
|
||||
目前插件提供了 3 个内置的 `Segment`,它们在 `nonebot_plugin_alconna.builtins.segments` 下:
|
||||
|
||||
- `Markdown`: 可以传入 **markdown模板** 的元素
|
||||
- `MarketFace`: 特指 QQ 的商城表情
|
||||
- `MusicShare`: 特指 QQ 的音乐分享卡片
|
||||
+41
-17
@@ -7,7 +7,7 @@ description: Alconna 基本介绍
|
||||
|
||||
[`Alconna`](https://github.com/ArcletProject/Alconna) 隶属于 `ArcletProject`,是一个简单、灵活、高效的命令参数解析器, 并且不局限于解析命令式字符串。
|
||||
|
||||
我们通过一个例子来讲解 **Alconna** 的核心 —— `Args`, `Subcommand`, `Option`:
|
||||
我们先通过一个例子来讲解 **Alconna** 的核心 —— `Args`, `Subcommand`, `Option`:
|
||||
|
||||
```python
|
||||
from arclet.alconna import Alconna, Args, Subcommand, Option
|
||||
@@ -38,14 +38,16 @@ print(res.all_matched_args)
|
||||
|
||||
命令头是指命令的前缀 (Prefix) 与命令名 (Command) 的组合,例如 !help 中的 ! 与 help。
|
||||
|
||||
命令构造时, `Alconna([prefix], command)` 与 `Alconna(command, [prefix])` 是等价的。
|
||||
|
||||
| 前缀 | 命令名 | 匹配内容 | 说明 |
|
||||
| :--------------------------: | :--------: | :---------------------------------------------------------: | :--------------: |
|
||||
| - | "foo" | `"foo"` | 无前缀的纯文字头 |
|
||||
| - | 123 | `123` | 无前缀的元素头 |
|
||||
| - | "re:\d{2}" | `"32"` | 无前缀的正则头 |
|
||||
| - | int | `123` 或 `"456"` | 无前缀的类型头 |
|
||||
| [int, bool] | - | `True` 或 `123` | 无名的元素类头 |
|
||||
| ["foo", "bar"] | - | `"foo"` 或 `"bar"` | 无名的纯文字头 |
|
||||
| 不传入 | "foo" | `"foo"` | 无前缀的纯文字头 |
|
||||
| 不传入 | 123 | `123` | 无前缀的元素头 |
|
||||
| 不传入 | "re:\d{2}" | `"32"` | 无前缀的正则头 |
|
||||
| 不传入 | int | `123` 或 `"456"` | 无前缀的类型头 |
|
||||
| [int, bool] | 不传入 | `True` 或 `123` | 无名的元素类头 |
|
||||
| ["foo", "bar"] | 不传入 | `"foo"` 或 `"bar"` | 无名的纯文字头 |
|
||||
| ["foo", "bar"] | "baz" | `"foobaz"` 或 `"barbaz"` | 纯文字头 |
|
||||
| [int, bool] | "foo" | `[123, "foo"]` 或 `[False, "foo"]` | 类型头 |
|
||||
| [123, 4567] | "foo" | `[123, "foo"]` 或 `[4567, "foo"]` | 元素头 |
|
||||
@@ -64,9 +66,6 @@ print(res.all_matched_args)
|
||||
除了通过传入 `re:xxx` 来使用正则表达式外,Alconna 还提供了一种更加简洁的方式来使用正则表达式,称为 Bracket Header:
|
||||
|
||||
```python
|
||||
from alconna import Alconna
|
||||
|
||||
|
||||
alc = Alconna(".rd{roll:int}")
|
||||
assert alc.parse(".rd123").header["roll"] == 123
|
||||
```
|
||||
@@ -206,6 +205,18 @@ args = Args["foo", BasePattern("@\d+")]
|
||||
|
||||
:::
|
||||
|
||||
#### AllParam
|
||||
|
||||
`AllParam` 是一个特殊的标注,用于告知解析器该参数接收命令中在此位置之后的所有参数并**结束解析**,可以认为是**泛匹配参数**。
|
||||
|
||||
`AllParam` 可直接使用 (`Args["xxx", AllParam]`), 也可以传入指定的接收类型 (`Args["xxx", AllParam(str)]`)。
|
||||
|
||||
:::tip
|
||||
|
||||
在 `nonebot_plugin_alconna` 下,`AllParam` 的返回值为 [`UniMessage`](./uniseg/message.mdx)
|
||||
|
||||
:::
|
||||
|
||||
### default
|
||||
|
||||
`default` 传入的是该参数的默认值或者 `Field`,以携带对于该参数的更多信息。
|
||||
@@ -271,7 +282,7 @@ opt2 = Option("--foo", default=OptionResult(value=False, args={"bar": 1}))
|
||||
- `append`,`append_value`
|
||||
- `count`
|
||||
|
||||
## 解析结果(Arparma)
|
||||
## 解析结果
|
||||
|
||||
`Alconna.parse` 会返回由 **Arparma** 承载的解析结果
|
||||
|
||||
@@ -291,18 +302,31 @@ opt2 = Option("--foo", default=OptionResult(value=False, args={"bar": 1}))
|
||||
- other_args: 除主参数外的其他解析结果
|
||||
- all_matched_args: 所有 Args 的解析结果
|
||||
|
||||
### 路径查询
|
||||
|
||||
`Arparma` 同时提供了便捷的查询方法 `query[type]()`,会根据传入的 `path` 查找参数并返回
|
||||
|
||||
`path` 支持如下:
|
||||
|
||||
- `main_args`, `options`, ...: 返回对应的属性
|
||||
- `args`: 返回 all_matched_args
|
||||
- `main_args.xxx`, `options.xxx`, ...: 返回字典中 `xxx`键对应的值
|
||||
- `args.xxx`: 返回 all_matched_args 中 `xxx`键对应的值
|
||||
- `options.foo`, `foo`: 返回选项 `foo` 的解析结果 (OptionResult)
|
||||
- `options.foo.value`, `foo.value`: 返回选项 `foo` 的解析值
|
||||
- `options.foo.args`, `foo.args`: 返回选项 `foo` 的解析参数字典
|
||||
- `options.foo.args.bar`, `foo.bar`: 返回选项 `foo` 的参数字典中 `bar` 键对应的值 ...
|
||||
- `args.<key>`: 返回 all_matched_args 中 `key` 键对应的值
|
||||
- `main_args.<key>`: 返回主命令的解析参数字典中 `key` 键对应的值
|
||||
- `<node>`: 返回选项/子命令 `node` 的解析结果 (OptionResult | SubcommandResult)
|
||||
- `<node>.value`: 返回选项/子命令 `node` 的解析值
|
||||
- `<node>.args`: 返回选项/子命令 `node` 的解析参数字典
|
||||
- `<node>.<key>`, `<node>.args.<key>`: 返回选项/子命令 `node` 的参数字典中 `key` 键对应的值
|
||||
|
||||
以及:
|
||||
|
||||
- `options.<opt>`: 返回选项 `opt` 的解析结果 (OptionResult)
|
||||
- `options.<opt>.value`: 返回选项 `opt` 的解析值
|
||||
- `options.<opt>.args`: 返回选项 `opt` 的解析参数字典
|
||||
- `options.<opt>.<key>`, `options.<node>.args.<key>`: 返回选项 `opt` 的参数字典中 `key` 键对应的值
|
||||
- `subcommands.<subcmd>`: 返回子命令 `subcmd` 的解析结果 (SubcommandResult)
|
||||
- `subcommands.<subcmd>.value`: 返回子命令 `subcmd` 的解析值
|
||||
- `subcommands.<subcmd>.args`: 返回子命令 `subcmd` 的解析参数字典
|
||||
- `subcommands.<subcmd>.<key>`, `subcommands.<node>.args.<key>`: 返回子命令 `subcmd` 的参数字典中 `key` 键对应的值
|
||||
|
||||
## 元数据(CommandMeta)
|
||||
|
||||
+38
-9
@@ -7,8 +7,8 @@ description: 配置项
|
||||
|
||||
## alconna_auto_send_output
|
||||
|
||||
- **类型**: `bool`
|
||||
- **默认值**: `False`
|
||||
- **类型**: `bool | None`
|
||||
- **默认值**: `None`
|
||||
|
||||
是否全局启用输出信息自动发送,不启用则会在触发特殊内置选项后仍然将解析结果传递至响应器。
|
||||
|
||||
@@ -19,12 +19,12 @@ description: 配置项
|
||||
|
||||
是否读取 Nonebot 的配置项 `COMMAND_START` 来作为全局的 Alconna 命令前缀
|
||||
|
||||
## alconna_auto_completion
|
||||
## alconna_global_completion
|
||||
|
||||
- **类型**: `bool`
|
||||
- **默认值**: `False`
|
||||
- **类型**: [`CompConfig | None`](./matcher.mdx#补全会话)
|
||||
- **默认值**: `None`
|
||||
|
||||
是否全局启用命令自动补全,启用后会在参数缺失或触发 `--comp` 选项时自自动启用交互式补全。
|
||||
全局的补全会话配置 (不代表全局启用补全会话)。
|
||||
|
||||
## alconna_use_origin
|
||||
|
||||
@@ -42,10 +42,13 @@ description: 配置项
|
||||
|
||||
## alconna_global_extensions
|
||||
|
||||
- **类型**: `List[str]`
|
||||
- **类型**: `list[str]`
|
||||
- **默认值**: `[]`
|
||||
|
||||
全局加载的扩展,路径以 . 分隔,如 `foo.bar.baz:DemoExtension`。
|
||||
全局加载的扩展,其读取路径以 . 分隔,如 `foo.bar.baz:DemoExtension`。
|
||||
|
||||
对于内置扩展,路径为 `nonebot_plugin_alconna.builtins.extensions` 下的模块名,如 `ReplyMergeExtension`,可以使用 `@` 来缩写路径,
|
||||
如 `@reply:ReplyMergeExtension`。
|
||||
|
||||
## alconna_context_style
|
||||
|
||||
@@ -73,4 +76,30 @@ description: 配置项
|
||||
- **类型**: `bool`
|
||||
- **默认值**: `False`
|
||||
|
||||
是否启动时拉取一次发送对象列表。
|
||||
是否启动时拉取一次[发送对象](./uniseg/utils.mdx#发送对象)列表。
|
||||
|
||||
## alconna_builtin_plugins
|
||||
|
||||
- **类型**: `set[str]`
|
||||
- **默认值**: `set()`
|
||||
|
||||
需要加载的内置插件列表。
|
||||
|
||||
## alconna_conflict_resolver
|
||||
|
||||
- **类型**: `Literal["raise", "default", "ignore", "replace"]`
|
||||
- **默认值**: `"default"`
|
||||
|
||||
命令冲突解决策略,决定当不同插件之间或者同一插件之间存在两个以上相同的命令时的处理方式:
|
||||
|
||||
- `default`: 默认处理方式,保留两个命令
|
||||
- `raise`: 抛出异常
|
||||
- `ignore`: 忽略较新的命令
|
||||
- `replace`: 替换较旧的命令
|
||||
|
||||
## alconna_response_self
|
||||
|
||||
- **类型**: `bool`
|
||||
- **默认值**: `False`
|
||||
|
||||
是否让响应器处理由 bot 自身发送的消息。
|
||||
@@ -0,0 +1,678 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
description: 响应规则的使用
|
||||
---
|
||||
|
||||
import Messenger from "@site/src/components/Messenger";
|
||||
import Tabs from "@theme/Tabs";
|
||||
import TabItem from "@theme/TabItem";
|
||||
|
||||
# `on_alconna` 响应器
|
||||
|
||||
`nonebot_plugin_alconna` 插件本体的大部分功能都围绕着 `on_alconna` 响应器展开。
|
||||
|
||||
该响应器类似于 `on_command`,基于 `Alconna` 解析器来解析命令。
|
||||
|
||||
以下是一个简单的 `on_alconna` 响应器的例子:
|
||||
|
||||
```python
|
||||
from nonebot_plugin_alconna import At, Image, Match, on_alconna
|
||||
from arclet.alconna import Args, Option, Alconna, MultiVar, Subcommand
|
||||
|
||||
|
||||
alc = Alconna(
|
||||
"role-group",
|
||||
Subcommand(
|
||||
"add|添加",
|
||||
Args["name", str],
|
||||
Option("member", Args["target", MultiVar(At)]),
|
||||
dest="add",
|
||||
compact=True,
|
||||
),
|
||||
Option("list"),
|
||||
Option("icon", Args["icon", Image])
|
||||
)
|
||||
rg = on_alconna(alc, use_command_start=True, aliases={"角色组"})
|
||||
|
||||
|
||||
@rg.assign("list")
|
||||
async def list_role_group():
|
||||
img: bytes = await gen_role_group_list_image()
|
||||
await rg.finish(Image(raw=img))
|
||||
|
||||
@rg.assign("add")
|
||||
async def _(name: str, target: Match[tuple[At, ...]]):
|
||||
group = await create_role_group(name)
|
||||
if target.available:
|
||||
ats: tuple[At, ...] = target.result
|
||||
group.extend(member.target for member in ats)
|
||||
await rg.finish("添加成功")
|
||||
```
|
||||
|
||||
<Messenger
|
||||
msgs={[
|
||||
{ position: "right", msg: "/role-group list" },
|
||||
{
|
||||
position: "left",
|
||||
msg: "[图片]",
|
||||
},
|
||||
{ position: "right", msg: "/角色组 添加foo @bar @baz" },
|
||||
{ position: "left", msg: "添加成功" },
|
||||
]}
|
||||
/>
|
||||
|
||||
## 声明
|
||||
|
||||
`on_alconna` 的参数如下:
|
||||
|
||||
```python
|
||||
def on_alconna(
|
||||
command: Alconna | str,
|
||||
rule: Rule | T_RuleChecker | None = None,
|
||||
skip_for_unmatch: bool = True,
|
||||
auto_send_output: bool | None = None,
|
||||
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 | None = None,
|
||||
use_cmd_start: bool | None = None,
|
||||
use_cmd_sep: bool | None = None,
|
||||
response_self: bool | None = None,
|
||||
**kwargs: Any,
|
||||
) -> type[AlconnaMatcher]:
|
||||
...
|
||||
```
|
||||
|
||||
- `command`: Alconna 命令或字符串,字符串将通过 `AlconnaFormat` 转换为 Alconna 命令
|
||||
- `rule`: 事件响应规则, 详见 [响应器规则](../../advanced/matcher.md#事件响应规则)
|
||||
- `skip_for_unmatch`: 是否在命令不匹配时跳过该响应, 默认为 `True`
|
||||
- `auto_send_output`: 是否自动发送输出信息并跳过该响应。
|
||||
- `True`:自动发送输出信息并跳过该响应
|
||||
- `False`:不自动发送输出信息,而是传递进行处理
|
||||
- `None`:跟随全局配置项 `alconna_auto_send_output`,默认值为 `True`
|
||||
- `aliases`: 命令别名, 作用类似于 `on_command` 中的 aliases
|
||||
- `comp_config`: 补全会话配置, 不传入则不启用补全会话
|
||||
- `extensions`: 需要加载的匹配扩展, 可以是扩展类或扩展实例
|
||||
- `exclude_ext`: 需要排除的匹配扩展, 可以是扩展类或扩展的id
|
||||
- `use_origin`: 是否使用未经 to_me 等处理过的消息。`None` 时跟随全局配置项 `alconna_use_origin`,默认值为 `False`
|
||||
- `use_cmd_start`: 是否使用 COMMAND_START 作为命令前缀。`None` 时跟随全局配置项 `alconna_use_command_start`,默认值为 `False`
|
||||
- `use_cmd_sep`: 是否使用 COMMAND_SEP 作为命令分隔符。`None` 时跟随全局配置项 `alconna_use_command_sep`,默认值为 `False`
|
||||
- `response_self`: 是否响应自身消息。`None` 时跟随全局配置项 `alconna_response_self`,默认值为 `False`
|
||||
|
||||
`on_alconna` 返回的是 `Matcher` 的子类 `AlconnaMatcher` ,其拓展了如下方法:
|
||||
|
||||
- `.assign(path, value, or_not)`: 用于对包含多个选项/子命令的命令的分派处理
|
||||
- `.dispatch`: 同样的分派处理,但是是类似 `CommandGroup` 一样返回新的 `AlconnaMatcher`
|
||||
- `.got_path(path, prompt, middleware)`: 在 `got` 方法的基础上,会以 path 对应的参数为准,读取传入 message 的最后一个消息段并验证转换
|
||||
- `.got`, `send`, `reject`, ... : 拓展了 prompt 类型,即支持使用 `UniMessage` 作为 prompt
|
||||
- ...
|
||||
|
||||
除了标准的创建方式,本插件也提供了 `funcommand` 和 `Command` 两种快捷方式来创建 `AlconnaMatcher`, 详见 [快捷方式](./shortcut.md)。
|
||||
|
||||
## 依赖注入
|
||||
|
||||
`AlconnaMatcher` 的特性之一是拓展了依赖注入的功能。
|
||||
|
||||
### 注入模型
|
||||
|
||||
插件提供了几种用来处理解析结果的模型:
|
||||
|
||||
- `CommandResult`: 用于快捷访问命令解析结果
|
||||
- `result (Arparma)`: 解析结果
|
||||
- `source (Alconna)`: 源命令
|
||||
- `matched (bool)`: 是否匹配
|
||||
- `context (dict)`: 命令的上下文
|
||||
- `output (str | None)`: 命令的输出
|
||||
- `Match`: 匹配项,表示参数是否存在于 `Arparma.all_matched_args` 内,可用 `Match.available` 判断是否匹配,`Match.result` 获取匹配的值
|
||||
- `Match` 只能查找到 `Arparma.all_matched_args` 中的参数。对于特定选项/子命令的参数,需要使用 `Query` 来查询
|
||||
- `Query`: 查询项,表示参数是否可由 `Arparma.query` 查询并获得结果,可用 `Query.available` 判断是否查询成功,`Query.result` 获取查询结果
|
||||
- `Query` 除了查询参数,也可以查询某个选项/子命令是否存在
|
||||
|
||||
### 编写
|
||||
|
||||
```python
|
||||
async def handle(
|
||||
result: CommandResult,
|
||||
arp: Arparma,
|
||||
dup: Duplication,
|
||||
source: Alconna,
|
||||
ext: Extension,
|
||||
exts: SelectedExtensions,
|
||||
abc: str,
|
||||
foo: Match[str],
|
||||
bar: Query[int] = Query("ttt.bar", 0)
|
||||
):
|
||||
...
|
||||
```
|
||||
|
||||
`AlconnaMatcher` 的依赖注入拓展支持以下情况:
|
||||
|
||||
- `xxx: CommandResult`
|
||||
- `xxx: Arparma`:命令的[解析结果](./command.md#解析结果)
|
||||
- `xxx: Duplication`:命令的解析结果的 [`Duplication`](./command.md#duplication)
|
||||
- `xxx: Alconna`:命令的源命令
|
||||
- `<key>: Match[<type>]`:上述的匹配项,使用 `key` 作为查询路径
|
||||
- `xxx: Query[<type>] = Query(<path>, default)`:上述的查询项,必需声明默认值以设置查询路径 `path`
|
||||
- 当用来查询选项/子命令是否存在时,可不写 `Query[<type>]`
|
||||
- `xxx: Extension`:当前 `AlconnaMatcher` 使用的指定类型的匹配扩展
|
||||
- `xxx: SelectedExtensions`:当前 `AlconnaMatcher` 使用的所有可用的匹配扩展
|
||||
- `<key>: <type>`: 其他情况
|
||||
- 当 `key` 的名称是 "ctx" 或 "context" 并且类型为 `dict` 时,会注入命令的上下文
|
||||
- 当 `key` 存在于命令的上下文中时,会注入对应的值
|
||||
- 当 `key` 存在于 `Arparma` 的 `all_matched_args` 中时,会注入对应的值, 类似于 `Match` 的用法,但当该值不存在时将跳过响应器。
|
||||
- 当 `key` 属于 `got_path` 的参数时,会注入对应的值
|
||||
- 当 `key` 被某个 `Extension.before_catch` 确认为需要注入的参数时,会调用 `Extension.catch` 来注入对应的值
|
||||
|
||||
:::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 AlconnaQuery, AlcResult, Match, Query, on_alconna
|
||||
from arclet.alconna import Alconna, Args, Option, Arparma
|
||||
|
||||
|
||||
test = on_alconna(
|
||||
Alconna(
|
||||
"test",
|
||||
Option("foo", Args["bar", int]),
|
||||
Option("baz", Args["qux", bool, False])
|
||||
)
|
||||
)
|
||||
|
||||
@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]):
|
||||
if bar.available:
|
||||
await test.send(f"foo={bar.result}")
|
||||
|
||||
@test.handle()
|
||||
async def handle_test4(qux: Query[bool] = AlconnaQuery("baz.qux", False)):
|
||||
if qux.available:
|
||||
await test.send(f"baz.qux={qux.result}")
|
||||
```
|
||||
|
||||
## 条件控制
|
||||
|
||||
### `assign` 方法
|
||||
|
||||
`AlconnaMatcher` 的 `assign` 方法与 `handle` 类似,但是可以控制响应函数是否在不满足条件时跳过响应。
|
||||
|
||||
`assign` 方法的参数如下:
|
||||
|
||||
```python
|
||||
def assign(
|
||||
cls,
|
||||
path: str,
|
||||
value: Any = _seminal,
|
||||
or_not: bool = False,
|
||||
additional: CHECK | None = None,
|
||||
parameterless: Iterable[Any] | None = None,
|
||||
):
|
||||
...
|
||||
```
|
||||
|
||||
- `path`: 指定的[查询路径](./command.md#路径查询)
|
||||
- "$main" 表示没有任何选项/子命令匹配的时候
|
||||
- "\~XX" 时会把 "\~" 替换为父级路径
|
||||
- `value`: 可能的指定查询值
|
||||
- `or_not`: 是否同时处理没有查询成功的情况
|
||||
- `additional`: 额外的条件检查函数
|
||||
|
||||
例如:
|
||||
|
||||
```python
|
||||
# 处理没有任何选项/子命令匹配的情况
|
||||
@rg.assign("$main")
|
||||
async def handle_main(): ...
|
||||
|
||||
# 处理 list 选项
|
||||
@rg.assign("list")
|
||||
async def handle_list(): ...
|
||||
|
||||
# 处理 add 选项,且 name 为 admin
|
||||
@rg.assign("add.name", "admin")
|
||||
async def handle_add_admin(): ...
|
||||
```
|
||||
|
||||
### `dispatch` 方法
|
||||
|
||||
此外,使用 `.dispatch` 还能像 `CommandGroup` 一样为每个分发设置独立的 matcher:
|
||||
|
||||
```python
|
||||
rg_list_cmd = rg.dispatch("list")
|
||||
|
||||
@rg_list_cmd.handle()
|
||||
async def handle_list(): ...
|
||||
```
|
||||
|
||||
`dispatch` 的参数与 `assign` 相同。
|
||||
|
||||
当使用 `dispatch` 时,父级路径表示为传入 `dispatch` 的 `path`:
|
||||
|
||||
```python
|
||||
rg_add_cmd = rg.dispatch("add")
|
||||
|
||||
# 此时 ~name 表示 add.name
|
||||
@rg_add_cmd.assign("~name", "admin")
|
||||
async def handle_add_admin(): ...
|
||||
```
|
||||
|
||||
:::tip
|
||||
|
||||
在 `dispatch` 下, `Query` 的 `path` 也同样支持 `~` 前缀来表示父级路径
|
||||
|
||||
```python
|
||||
@rg_add_cmd.assign("~name", "admin")
|
||||
async def handle_add_admin(target: Query[tuple[At, ...]] = Query("~target")):
|
||||
if target.available:
|
||||
await rg.send(f"添加成功: {target.result}")
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### `got_path` 方法
|
||||
|
||||
另外,`AlconnaMatcher` 有类似于 [`got`](../../appendices/session-control.mdx#got) 的 `got_path` 与配套的 `get_path_arg`, `set_path_arg`:
|
||||
|
||||
```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 才能进入处理函数。
|
||||
|
||||
`got_path` 中可以使用依赖注入函数 `AlconnaArg`, 类似于 [`Arg`](../../advanced/dependency.mdx#arg).
|
||||
|
||||
### `prompt` 方法
|
||||
|
||||
基于 [`Waiter`](https://github.com/RF-Tar-Railt/nonebot-plugin-waiter) 插件,`AlconnaMatcher` 提供了 `prompt` 方法来实现更灵活的交互式提示。
|
||||
|
||||
```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:
|
||||
await test_cmd.finish(UniMessage(["ok\n", target]))
|
||||
resp = await test_cmd.prompt("请输入目标", timeout=30) # 等待 30 秒
|
||||
if resp is None:
|
||||
await test_cmd.finish("超时")
|
||||
await test_cmd.finish(UniMessage(["ok\n", resp[-1]]))
|
||||
```
|
||||
|
||||
## 返回值中间件
|
||||
|
||||
在 `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` 对象,并提取图片的二进制数据返回。
|
||||
|
||||
## i18n
|
||||
|
||||
本插件基于 `tarina.lang` 模块提供了 i18n 的支持,参见 [Lang 用法](https://github.com/nonebot/plugin-alconna/discussions/50)。
|
||||
|
||||
当你编写完语言文件后,你便可以通过 `AlconnaMatcher.i18n` 来快速地将语言文件中的内容转为 UniMessage.
|
||||
|
||||
<Tabs groupId="i18n">
|
||||
<TabItem value="zh" label="中文">
|
||||
|
||||
```yaml title="zh-CN.yml"
|
||||
# 中文语言文件
|
||||
demo:
|
||||
command:
|
||||
role-group:
|
||||
add: 添加 {name} 成功!
|
||||
```
|
||||
|
||||
<Messenger
|
||||
msgs={[
|
||||
{ position: "right", msg: "/角色组 添加 foo" },
|
||||
{ position: "left", msg: "添加 foo 成功!" },
|
||||
]}
|
||||
/>
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="en" label="英文">
|
||||
|
||||
```yaml title="en-US.yml"
|
||||
# 英文语言文件
|
||||
demo:
|
||||
command:
|
||||
role-group:
|
||||
add: Add {name} successfully!
|
||||
```
|
||||
|
||||
<Messenger
|
||||
msgs={[
|
||||
{ position: "right", msg: "/role-group add foo" },
|
||||
{ position: "left", msg: "Add foo successfully!" },
|
||||
]}
|
||||
/>
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
```python title="使用 i18n"
|
||||
@rg.assign("add")
|
||||
async def handle_add(name: str):
|
||||
await rg.i18n("demo", "command.role-group.add", name=name).finish()
|
||||
```
|
||||
|
||||
## 匹配测试
|
||||
|
||||
`AlconnaMatcher.test` 方法允许你在 NoneBot 启动时对命令进行测试。
|
||||
|
||||
```python
|
||||
def test(
|
||||
cls,
|
||||
message: str | UniMessage,
|
||||
expected: dict[str, Any] | None = None,
|
||||
prefix: bool = True
|
||||
): ...
|
||||
```
|
||||
|
||||
- `message`: 测试的消息
|
||||
- `expected`: 预期的解析结果,若为 None 则表示只测试是否匹配
|
||||
- `prefix`: 是否使用命令前缀,默认为 True
|
||||
|
||||
## 匹配拓展
|
||||
|
||||
本插件提供了一个 `Extension` 类,其用于自定义 AlconnaMatcher 的部分行为
|
||||
|
||||
目前 `Extension` 的功能有:
|
||||
|
||||
- `validate`: 对于事件的来源适配器或 bot 选择是否接受响应
|
||||
- `output_converter`: 输出信息的自定义转换方法
|
||||
- `message_provider`: 从传入事件中自定义提取消息的方法
|
||||
- `receive_provider`: 对传入的消息 (UniMessage) 的额外处理
|
||||
- `context_provider`: 对命令上下文的额外处理
|
||||
- `permission_check`: 命令对消息解析并确认头部匹配(即确认选择响应)时对发送者的权限判断
|
||||
- `parse_wrapper`: 对命令解析结果的额外处理
|
||||
- `send_wrapper`: 对发送的消息 (Message 或 UniMessage) 的额外处理
|
||||
- `before_catch`: 自定义依赖注入的绑定确认函数
|
||||
- `catch`: 自定义依赖注入处理函数
|
||||
- `post_init`: 响应器创建后对命令对象的额外处理
|
||||
|
||||
:::tip
|
||||
|
||||
Extension 可以通过 `add_global_extension` 方法来全局添加。
|
||||
|
||||
```python
|
||||
from nonebot_plugin_alconna import add_global_extension
|
||||
from nonebot_plugin_alconna.builtins.extensions.telegram import TelegramSlashExtension
|
||||
|
||||
add_global_extension(TelegramSlashExtension)
|
||||
```
|
||||
|
||||
全局的 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` 参数注入模型变量。
|
||||
|
||||
### validate
|
||||
|
||||
```python
|
||||
def validate(self, bot: Bot, event: Event) -> bool: ...
|
||||
```
|
||||
|
||||
默认情况下,`validate` 方法会筛选 `event.get_type()` 为 `message` 的情况,表示接受消息事件。
|
||||
|
||||
### output_converter
|
||||
|
||||
```python
|
||||
async def output_converter(self, output_type: OutputType, content: str) -> UniMessage: ...
|
||||
```
|
||||
|
||||
依据输出信息的类型,将字符串转换为消息对象以便发送。
|
||||
|
||||
其中 `OutputType` 为 "help", "shortcut", "completion", "error" 其中之一。
|
||||
|
||||
该方法只会调用一次,即对于多个 Extension,选择优先级靠前且实现了该方法的 Extension。
|
||||
|
||||
### message_provider
|
||||
|
||||
```python
|
||||
async def message_provider(
|
||||
self, event: Event, state: T_State, bot: Bot, use_origin: bool = False
|
||||
) -> UniMessage | None:...
|
||||
```
|
||||
|
||||
该方法用于从事件中提取消息,默认情况下会使用 `event.get_message()` 来获取消息。
|
||||
|
||||
该方法可能会调用多次,即对于多个 Extension,选择优先级靠前且实现了该方法的 Extension,若调用的返回值不为 `None` 则作为结果。
|
||||
|
||||
:::caution
|
||||
|
||||
该方法的默认实现对结果 (UniMessage) 会进行缓存。`Extension` 的实现也应尽量实现缓存机制。
|
||||
|
||||
:::
|
||||
|
||||
### receive_provider
|
||||
|
||||
```python
|
||||
async def receive_provider(self, bot: Bot, event: Event, command: Alconna, receive: UniMessage) -> UniMessage: ...
|
||||
```
|
||||
|
||||
该方法用于对传入的消息 (UniMessage) 进行额外处理,默认情况下会返回原始消息。
|
||||
|
||||
该方法会调用多次,即对于多个 Extension,前一个 Extension 的返回值会作为下一个 Extension 的输入。
|
||||
|
||||
### context_provider
|
||||
|
||||
```python
|
||||
async def context_provider(self, ctx: dict[str, Any], bot: Bot, event: Event, state: T_State) -> dict[str, Any]:
|
||||
```
|
||||
|
||||
该方法用于提取命令上下文,默认情况下会返回 `ctx` 本身。
|
||||
|
||||
该方法会调用多次,即对于多个 Extension,前一个 Extension 的返回值会作为下一个 Extension 的输入。
|
||||
|
||||
### permission_check
|
||||
|
||||
```python
|
||||
async def permission_check(self, bot: Bot, event: Event, command: Alconna) -> bool: ...
|
||||
```
|
||||
|
||||
该方法用于对发送者的权限进行检查,默认情况下会返回 `True`。
|
||||
|
||||
该方法可能会调用多次,即对于多个 Extension,若调用的返回值不为 `True` 则结束判断。
|
||||
|
||||
### parse_wrapper
|
||||
|
||||
```python
|
||||
async def parse_wrapper(self, bot: Bot, state: T_State, event: Event, res: Arparma) -> None: ...
|
||||
```
|
||||
|
||||
该方法用于对命令解析结果进行额外处理。
|
||||
|
||||
该方法会调用多次,即对于多个 Extension,会并发地调用该方法。
|
||||
|
||||
### send_wrapper
|
||||
|
||||
```python
|
||||
async def send_wrapper(self, bot: Bot, event: Event, send: TMessage) -> TMessage: ...
|
||||
```
|
||||
|
||||
该方法用于对 `AlconnaMatcher.send` 或 `UniMessage.send` 发送的消息 (str 或 Message 或 UniMessage) 进行额外处理,默认情况下会返回原始消息。
|
||||
|
||||
该方法会调用多次,即对于多个 Extension,前一个 Extension 的返回值会作为下一个 Extension 的输入。
|
||||
|
||||
由于需要保证输入与输出的类型一致,该方法内需要自行判断类型。
|
||||
|
||||
### before_catch
|
||||
|
||||
```python
|
||||
def before_catch(self, name: str, annotation: type, default: Any) -> bool: ...
|
||||
```
|
||||
|
||||
该方法用于响应函数中某个参数是否需要绑定到该 Extension 上。
|
||||
|
||||
### catch
|
||||
|
||||
```python
|
||||
async def catch(self, interface: Interface) -> Any: ...
|
||||
```
|
||||
|
||||
该方法用于注入经过 `before_catch` 确认的参数。其中 `Interface` 的定义为
|
||||
|
||||
```python
|
||||
class Interface(Generic[TE]):
|
||||
event: TE
|
||||
state: T_State
|
||||
name: str
|
||||
annotation: Any
|
||||
default: Any
|
||||
```
|
||||
|
||||
## 补全会话
|
||||
|
||||
补全会话基于 [`半自动补全`](./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)"""
|
||||
block: NotRequired[bool]
|
||||
"""进行补全会话时是否阻塞响应器"""
|
||||
```
|
||||
@@ -0,0 +1,121 @@
|
||||
---
|
||||
sidebar_position: 6
|
||||
description: 快捷方式
|
||||
---
|
||||
|
||||
# 快捷方式声明
|
||||
|
||||
针对 `Alconna` 编写对于入门开发者来说较为复杂的问题,本插件提供了一些快捷方式来简化开发者的工作。
|
||||
|
||||
## 装饰器构造器
|
||||
|
||||
本插件提供了一个 `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)
|
||||
|
||||
```
|
||||
|
||||
相比于 `on_alconna`, `funcommand` 增加了三个参数 `name`, `prefixes` 和 `description`。
|
||||
|
||||
## 类 Koishi 构造器
|
||||
|
||||
本插件提供了一个 `Command` 构造器,其基于 `arclet.alconna.tools` 中的 `AlconnaString`, 以类似 `Koishi` 中[注册命令](https://koishi.chat/zh-CN/guide/basic/command.html)的方式来构建一个 **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()
|
||||
)
|
||||
```
|
||||
|
||||
### 参数类型
|
||||
|
||||
`Command` 的参数类型也如 `koishi` 一样,**必选参数** 用尖括号包裹,**可选参数** 用方括号包裹:
|
||||
|
||||
- `foo` 表示参数 `foo`, 类型为 Any
|
||||
- `foo:int` 表示参数 `foo`, 类型为 int
|
||||
- `foo:int=1` 表示参数 `foo`, 类型为 int, 默认值为 1
|
||||
- `...foo` 表示[泛匹配参数](command.md#allparam)
|
||||
- `foo:str+`, `foo:str*` 表示[变长参数](command.md#multivar-与-keywordvar) `foo`, 类型为 str
|
||||
- `foo:+str`, `foo:text` 表示参数 `foo`, 类型为 str, 并且将包含空格 (即将变长参数的结果用空格合并)
|
||||
|
||||
特别的,针对类型部分,本插件拓展了如下内容:
|
||||
|
||||
- `foo:At`, `foo:Image`, ... 表示类型为[通用消息段](./uniseg/segment.md)
|
||||
- `foo:select(Image).first` 表示获取子元素类型
|
||||
- `foo:Dot(Image, 'url')` 表示类型为 `Image`,并且只获取 `url` 属性
|
||||
|
||||
### 从文件加载
|
||||
|
||||
`Command` 支持读取 `json` 或 `yaml` 文件来加载命令:
|
||||
|
||||
```yml title="book.yml"
|
||||
command: book
|
||||
help: 测试
|
||||
options:
|
||||
- name: writer
|
||||
opt: "-w <id:int>"
|
||||
- name: writer
|
||||
opt: "--anonymous"
|
||||
default:
|
||||
id: 1
|
||||
usage: book [-w <id:int> | --anonymous]
|
||||
shortcuts:
|
||||
- key: 测试
|
||||
args: ["--anonymous"]
|
||||
actions:
|
||||
- params: ["options"]
|
||||
code: |
|
||||
return str(options)
|
||||
```
|
||||
|
||||
```python title="加载"
|
||||
from nonebot_plugin_alconna import command_from_yaml
|
||||
|
||||
book = command_from_yaml("book.yml")
|
||||
```
|
||||
@@ -0,0 +1,203 @@
|
||||
# 通用消息组件
|
||||
|
||||
`uniseg` 模块属于 `nonebot-plugin-alconna` 的子插件。
|
||||
|
||||
通用消息组件内容较多,故分为了一个示例以及数个专题。
|
||||
|
||||
## 示例
|
||||
|
||||
### 导入
|
||||
|
||||
一般情况下,你只需要从 `nonebot_plugin_alconna.uniseg` 中导入 `UniMessage` 即可:
|
||||
|
||||
```python
|
||||
from nonebot_plugin_alconna.uniseg import UniMessage
|
||||
```
|
||||
|
||||
### 构建
|
||||
|
||||
你可以通过 `UniMessage` 上的快捷方法来链式构造消息:
|
||||
|
||||
```python
|
||||
message = (
|
||||
UniMessage.text("hello world")
|
||||
.at("1234567890")
|
||||
.image(url="https://example.com/image.png")
|
||||
)
|
||||
```
|
||||
|
||||
也可以通过导入通用消息段来构建消息:
|
||||
|
||||
```python
|
||||
from nonebot_plugin_alconna import Text, At, Image, UniMessage
|
||||
|
||||
message = UniMessage(
|
||||
[
|
||||
Text("hello world"),
|
||||
At("user", "1234567890"),
|
||||
Image(url="https://example.com/image.png"),
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
更深入一点,比如你想要发送一条包含多个按钮的消息,你可以这样做:
|
||||
|
||||
```python
|
||||
from nonebot_plugin_alconna import Button, UniMessage
|
||||
|
||||
message = (
|
||||
UniMessage.text("hello world")
|
||||
.keyboard(
|
||||
Button("link1", url="https://example.com/1"),
|
||||
Button("link2", url="https://example.com/2"),
|
||||
Button("link3", url="https://example.com/3"),
|
||||
row=3,
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
### 发送
|
||||
|
||||
你可以通过 `.send` 方法来发送消息:
|
||||
|
||||
```python
|
||||
@matcher.handle()
|
||||
async def _():
|
||||
message = UniMessage.text("hello world").image(url="https://example.com/image.png")
|
||||
await message.send()
|
||||
# 类似于 `matcher.finish`
|
||||
await message.finish()
|
||||
```
|
||||
|
||||
你可以通过参数来让消息 @ 发送者:
|
||||
|
||||
```python
|
||||
@matcher.handle()
|
||||
async def _():
|
||||
message = UniMessage.text("hello world").image(url="https://example.com/image.png")
|
||||
await message.send(at_sender=True)
|
||||
```
|
||||
|
||||
或者回复消息:
|
||||
|
||||
```python
|
||||
@matcher.handle()
|
||||
async def _():
|
||||
message = UniMessage.text("hello world").image(url="https://example.com/image.png")
|
||||
await message.send(reply_to=True)
|
||||
```
|
||||
|
||||
### 撤回,编辑,表态
|
||||
|
||||
你可以通过 `message_recall`, `message_edit` 和 `message_reaction` 方法来撤回,编辑和表态消息事件。
|
||||
|
||||
```python
|
||||
from nonebot_plugin_alconna import message_recall, message_edit, message_reaction
|
||||
|
||||
@matcher.handle()
|
||||
async def _():
|
||||
await message_edit(UniMessage.text("hello world"))
|
||||
await message_reaction("👍")
|
||||
await message_recall()
|
||||
```
|
||||
|
||||
你也可以对你自己发送的消息进行撤回,编辑和表态:
|
||||
|
||||
```python
|
||||
@matcher.handle()
|
||||
async def _():
|
||||
message = UniMessage.text("hello world").image(url="https://example.com/image.png")
|
||||
receipt = await message.send()
|
||||
await receipt.edit(UniMessage.text("hello world!"))
|
||||
await receipt.reaction("👍")
|
||||
await receipt.recall(delay=5) # 5秒后撤回
|
||||
```
|
||||
|
||||
### 处理消息
|
||||
|
||||
通过依赖注入,你可以在事件处理器中获取通用消息:
|
||||
|
||||
```python
|
||||
from nonebot_plugin_alconna import UniMsg
|
||||
|
||||
@matcher.handle()
|
||||
async def _(msg: UniMsg):
|
||||
...
|
||||
```
|
||||
|
||||
然后你可以通过 `UniMessage` 的方法来处理消息.
|
||||
|
||||
例如,你想知道消息中是否包含图片,你可以这样做:
|
||||
|
||||
```python
|
||||
ans1 = Image in message
|
||||
ans2 = message.has(Image)
|
||||
ans3 = message.only(Image)
|
||||
```
|
||||
|
||||
或者,提取所有的图片:
|
||||
|
||||
```python
|
||||
imgs_1 = message[Image]
|
||||
imgs_2 = message.get(Image)
|
||||
imgs_3 = message.include(Image)
|
||||
imgs_4 = message.select(Image)
|
||||
imgs_5 = message.filter(lambda x: x.type == "image")
|
||||
imgs_6 = message.tranform({"image": True})
|
||||
```
|
||||
|
||||
而后,如果你想提取出所有的图片链接,你可以这样做:
|
||||
|
||||
```python
|
||||
urls = imgs.map(lambda x: x.url)
|
||||
```
|
||||
|
||||
如果你想知道消息是否符合某个前缀,你可以这样做:
|
||||
|
||||
```python
|
||||
@matcher.handle()
|
||||
async def _(msg: UniMsg):
|
||||
if msg.startswith("hello"):
|
||||
await matcher.finish("hello world")
|
||||
else:
|
||||
await matcher.finish("not hello world")
|
||||
```
|
||||
|
||||
或者你想接着去除掉前缀:
|
||||
|
||||
```python
|
||||
@matcher.handle()
|
||||
async def _(msg: UniMsg):
|
||||
if msg.startswith("hello"):
|
||||
msg = msg.removeprefix("hello")
|
||||
await matcher.finish(msg)
|
||||
else:
|
||||
await matcher.finish("not hello world")
|
||||
```
|
||||
|
||||
### 持久化
|
||||
|
||||
假设你在编写一个词库查询插件,你可以通过 `UniMessage.dump` 方法来将消息序列化为 JSON 格式:
|
||||
|
||||
```python
|
||||
from nonebot_plugin_alconna import UniMsg
|
||||
|
||||
@matcher.handle()
|
||||
async def _(msg: UniMsg):
|
||||
data: list[dict] = msg.dump()
|
||||
# 你可以将 data 存储到数据库或者 JSON 文件中
|
||||
```
|
||||
|
||||
而后你可以通过 `UniMessage.load` 方法来将 JSON 格式的消息反序列化为 `UniMessage` 对象:
|
||||
|
||||
```python
|
||||
from nonebot_plugin_alconna import UniMessage
|
||||
|
||||
@matcher.handle()
|
||||
async def _():
|
||||
data = [
|
||||
{"type": "text", "text": "hello world"},
|
||||
{"type": "image", "url": "https://example.com/image.png"},
|
||||
]
|
||||
message = UniMessage.load(data)
|
||||
```
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"label": "通用消息组件",
|
||||
"position": 5
|
||||
}
|
||||
@@ -0,0 +1,518 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
description: 消息序列
|
||||
---
|
||||
|
||||
import Tabs from "@theme/Tabs";
|
||||
import TabItem from "@theme/TabItem";
|
||||
|
||||
# 通用消息序列
|
||||
|
||||
`uniseg` 提供了一个类似于 `Message` 的 `UniMessage` 类型,其元素为[通用消息段](./segment.md)。
|
||||
|
||||
你可以用如下方式获取 `UniMessage`:
|
||||
|
||||
<Tabs groupId="get_unimsg">
|
||||
<TabItem value="depend" label="使用依赖注入">
|
||||
|
||||
通过提供的 `UniversalMessage` 或基于 [`Annotated` 支持](https://github.com/nonebot/nonebot2/pull/1832)的 `UniMsg` 依赖注入器来获取 `UniMessage`。
|
||||
|
||||
```python
|
||||
from nonebot_plugin_alconna.uniseg import UniMsg, At, Text
|
||||
|
||||
|
||||
matcher = on_xxx(...)
|
||||
|
||||
@matcher.handle()
|
||||
async def _(msg: UniMsg):
|
||||
text = msg[Text, 0]
|
||||
print(text.text)
|
||||
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`, `.finish` 方法基于 `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)
|
||||
```
|
||||
|
||||
`UniMessage.send` 的定义如下:
|
||||
|
||||
```python
|
||||
async def send(
|
||||
self,
|
||||
target: Event | Target | None = None,
|
||||
bot: Bot | None = None,
|
||||
fallback: bool | FallbackStrategy = FallbackStrategy.rollback,
|
||||
at_sender: str | bool = False,
|
||||
reply_to: str | bool | Reply | None = False,
|
||||
**kwargs: Any,
|
||||
) -> Receipt:
|
||||
...
|
||||
```
|
||||
|
||||
- `target`: 发送目标,支持事件和[发送对象](./utils.mdx#发送对象),不传入时会尝试从响应器上下文中获取。
|
||||
- `bot`: 发送消息使用的 Bot 对象,若不传入则会尝试从响应器上下文中获取。
|
||||
- `fallback`: [回退策略](#回退策略)。
|
||||
- `at_sender`: 是否提醒发送者,默认为 `False`。当类型为 `str` 时,表示指定用户的 id。
|
||||
- `reply_to`: 是否回复消息,默认为 `False`。
|
||||
- `str` 表示消息 id。
|
||||
- `bool` 表示是否回复当前消息。此时 `target` 不能是[发送对象](./utils.mdx#发送对象)。
|
||||
- `Reply` 表示直接使用回复元素。
|
||||
- `**kwargs`: 各 `Bot.send` 的特定参数。
|
||||
|
||||
而在 `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."]))
|
||||
```
|
||||
|
||||
### 回退策略
|
||||
|
||||
`send` 方法的 `fallback` 参数用于指定回退策略(即当前适配器不支持的消息段如何处理):
|
||||
|
||||
- `FallbackStrategy.ignore`: 忽略未转换的消息段
|
||||
- `FallbackStrategy.to_text`: 将未转换的消息段转为文本元素
|
||||
- `FallbackStrategy.rollback`: 从未转换消息段的子元素中提取可能的可发送消息段
|
||||
- `FallbackStrategy.forbid`: 抛出异常
|
||||
- `FallbackStrategy.auto`: 插件自动选择策略
|
||||
|
||||
另外 `fallback` 传入 `bool` 时,`True` 等价于 `FallbackStrategy.auto`,`False` 等价于 `FallbackStrategy.forbid`。
|
||||
|
||||
### 主动发送消息
|
||||
|
||||
`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)
|
||||
```
|
||||
|
||||
:::caution
|
||||
|
||||
在响应器以外的地方,除非启用了 `alconna_apply_fetch_targets` 配置项,否则 `bot` 参数必须手动传入。
|
||||
|
||||
:::
|
||||
|
||||
### Receipt 对象
|
||||
|
||||
`send` 方法返回的 `Receipt` 对象可以用于修改/撤回/表态消息:
|
||||
|
||||
```python
|
||||
async def handle():
|
||||
receipt = await UniMessage.text("hello!").send(at_sender=True, reply_to=True)
|
||||
await receipt.recall(delay=1)
|
||||
recept1 = await UniMessage.text("hello!").send(at_sender=True, reply_to=True)
|
||||
await recept1.edit("world!")
|
||||
```
|
||||
|
||||
`Receipt` 对象拥有以下方法:
|
||||
|
||||
- `recallable`: 表明是否可以撤回
|
||||
- `recall`: 撤回消息
|
||||
- `editable`: 表明是否可以修改
|
||||
- `edit`: 修改消息
|
||||
- `reactionable`: 表明是否可以表态
|
||||
- `reaction`: 表态消息
|
||||
- `get_reply`: 生成对已经发送的消息的回复元素
|
||||
- `send`, `finish`: 发送消息
|
||||
- `reply`: 回复已经发送的消息
|
||||
|
||||
## 构造
|
||||
|
||||
如同 `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")]
|
||||
)
|
||||
```
|
||||
|
||||
### 使用消息模板
|
||||
|
||||
`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` 两个特殊值。
|
||||
|
||||
:::tip
|
||||
|
||||
注意到上述代码中的 `{target}` 了吗?
|
||||
|
||||
在 `AlconnaMatcher` 中,`UniMessage.template` 的格式化方法会自动将 `Arparma.all_matched_args`、 `state` 中的变量传入到 `format` 方法中,因此你可以直接使用上述变量。
|
||||
|
||||
:::
|
||||
|
||||
### 拼接消息
|
||||
|
||||
`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")])
|
||||
```
|
||||
|
||||
## 操作
|
||||
|
||||
### 检查消息段
|
||||
|
||||
我们可以通过 `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
|
||||
# 提取消息纯文本字符串
|
||||
assert UniMessage(
|
||||
[At("user", "1234"), "text"]
|
||||
).extract_plain_text() == "text"
|
||||
```
|
||||
|
||||
### 遍历
|
||||
|
||||
通用消息序列继承自 `List[Segment]` ,因此可以使用 `for` 循环遍历消息段:
|
||||
|
||||
```python
|
||||
for segment in message: # type: Segment
|
||||
...
|
||||
```
|
||||
|
||||
### 过滤、索引与切片
|
||||
|
||||
消息序列对列表的索引与切片进行了增强,在原有列表 `int` 索引与 `slice` 切片的基础上,支持 `type` 过滤索引与切片:
|
||||
|
||||
```python
|
||||
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)
|
||||
```
|
||||
|
||||
或者使用 `filter` 方法:
|
||||
|
||||
```python
|
||||
message.filter(lambda x: isinstance(x, At) and x.flag == "user") # 仅保留 At("user", xxx) 的消息段
|
||||
```
|
||||
|
||||
同样的,消息序列对列表的 `index`、`count` 方法也进行了增强,可以用于索引指定类型的消息段:
|
||||
|
||||
```python
|
||||
# 指定类型首个消息段索引
|
||||
message.index(Text) == 1
|
||||
# 指定类型消息段数量
|
||||
message.count(Text) == 2
|
||||
```
|
||||
|
||||
此外,消息序列添加了一个 `get` 方法,可以用于获取指定类型指定个数的消息段:
|
||||
|
||||
```python
|
||||
# 获取指定类型指定个数的消息段
|
||||
message.get(Text, 1) == UniMessage([Text("test1")])
|
||||
```
|
||||
|
||||
### 嵌套提取
|
||||
|
||||
消息序列的 `select` 方法可以递归地从消息中选择指定类型的消息段:
|
||||
|
||||
```python
|
||||
message = UniMessage(
|
||||
[
|
||||
Text("text1"),
|
||||
Image(url="url1")(
|
||||
Text("text2"),
|
||||
)
|
||||
]
|
||||
)
|
||||
assert message.select(Text) == UniMessage(
|
||||
[
|
||||
Text("text1"),
|
||||
Text("text2")
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
### 转换
|
||||
|
||||
消息序列的 `map` 方法可以简单地将消息段转换为指定类型的数据:
|
||||
|
||||
```python
|
||||
# 转换消息段为另一类型的消息段,此时返回结果仍是 UniMessage
|
||||
message.map(lambda x: Text(x.target)) # 转换为 Text 消息段
|
||||
# 转换消息段为另一类型的数据,此时返回结果为 list[T]
|
||||
message.map(lambda x: x.target) # 转换为 list[str]
|
||||
```
|
||||
|
||||
在此之上,消息序列还提供了 `transform` 和 `transform_async` 方法,允许你传入转换规则,将消息段转换为另一类型的消息段,并返回一个新的消息序列:
|
||||
|
||||
```python
|
||||
rule = {
|
||||
"text": True,
|
||||
"at": lambda attrs, children: Text(attrs["target"])
|
||||
}
|
||||
message.transform(rule)
|
||||
```
|
||||
|
||||
转换规则的类型一般为 `dict[str, Transformer]`,以消息元素类型的名称为键,定义方式如下:
|
||||
|
||||
```typescript
|
||||
type Fragment = Segment | Segment[];
|
||||
type Render<T> = (attrs: dict, children: Segment[]) => T;
|
||||
type Transformer = boolean | Fragment | Render<boolean | Fragment>;
|
||||
```
|
||||
|
||||
### 字符串操作
|
||||
|
||||
类似于 `str`,消息序列可以通过如下方法来操作消息内的文本部分:
|
||||
|
||||
- `split`,
|
||||
- `replace`,
|
||||
- `startwith`, `endswith`,
|
||||
- `removeprefix`, `removesuffix`,
|
||||
- `strip`, `lstrip`, `rstrip`,
|
||||
|
||||
```python
|
||||
msg = UniMessage.text("foo bar").at("1234").text("baz qux")
|
||||
# 分割,返回分割结果,类型为 list[UniMessage]
|
||||
parts = msg.split(" ")
|
||||
# 替换,返回替换结果,类型为 UniMessage。新文本可以用 str 或 Text 来替换
|
||||
new_msg = msg.replace("ba", "baaa")
|
||||
# 前缀/后缀检查
|
||||
msg.startswith("foo") # True
|
||||
msg.endswith("qux") # True
|
||||
# 去除前缀/后缀
|
||||
msg1 = msg.removeprefix("foo") # UniMessage([Text(" bar"), At("user", "1234"), Text("baz qux")])
|
||||
msg2 = msg.removesuffix("qux") # UniMessage([Text("foo bar"), At("user", "1234"), Text("baz ")])
|
||||
# 去除空格
|
||||
msg1 = msg1.lstrip() # UniMessage([Text("bar"), At("user", "1234"), Text("baz qux")])
|
||||
msg2 = msg2.rstrip() # UniMessage([Text("foo bar"), At("user", "1234"), Text("baz")])
|
||||
```
|
||||
|
||||
## 持久化
|
||||
|
||||
特别的,`UniMessage` 还支持消息持久化,具体来说为 `dump` 与 `load` 方法:
|
||||
|
||||
```python
|
||||
msg = UniMessage.text("Hello").image(url="url")
|
||||
data = msg.dump() # [{"type": "text", "text": "Hello"}, {"type": "image", "url": "url"}]
|
||||
|
||||
assert UniMessage.load(data) == msg
|
||||
```
|
||||
|
||||
### dump
|
||||
|
||||
`dump` 方法的定义如下:
|
||||
|
||||
```python
|
||||
def dump(self, media_save_dir: str | Path | bool | None = None, json: bool = False) -> str | list[dict[str, Any]]: ...
|
||||
```
|
||||
|
||||
其中,`media_save_dir` 用于指定持久化的媒体文件存储目录:
|
||||
|
||||
- 若 `media_save_dir` 为 str 或 Path,则会将媒体文件保存到指定目录下。
|
||||
- 若 `media_save_dir` 为 False,则不会保存媒体文件。
|
||||
- 若 `media_save_dir` 为 True,则会将文件数据转为 base64 编码。
|
||||
- 若不指定 `media_save_dir`,则会尝试导入 [`nonebot_plugin_localstore`](../../data-storing.md) 并使用其提供的路径。否则 (即 `localstore` 未安装),将会尝试使用当前工作目录。
|
||||
|
||||
### load
|
||||
|
||||
`load` 方法的定义如下:
|
||||
|
||||
```python
|
||||
@classmethod
|
||||
def load(cls, data: str | list[dict[str, Any]]) -> UniMessage: ...
|
||||
```
|
||||
|
||||
其中 `data` 应符合 JSON 格式。
|
||||
@@ -0,0 +1,222 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
description: 消息段
|
||||
---
|
||||
|
||||
# 通用消息段
|
||||
|
||||
通用消息段是对各适配器中的消息段的抽象总结。其可用于 Alconna 命令的参数定义,也可用于消息的构建和解析。
|
||||
|
||||
```python
|
||||
from nonebot_plugin_alconna import Alconna, Args, Image, on_alconna
|
||||
|
||||
meme = on_alconna(Alconna("make_meme", Args["name", str]["img", Image]))
|
||||
|
||||
@meme.handle()
|
||||
async def _(img: Image):
|
||||
...
|
||||
```
|
||||
|
||||
## 模型定义
|
||||
|
||||
> **注意**: 本节的内容经过简化。实际情况以源码为准。
|
||||
|
||||
```python
|
||||
class Segment:
|
||||
"""基类标注"""
|
||||
@property
|
||||
def type(self) -> str: ...
|
||||
@property
|
||||
def data(self) -> [str, Any]: ...
|
||||
@property
|
||||
def children(self) -> list["Segment"]: ...
|
||||
|
||||
class Text(Segment):
|
||||
"""Text对象, 表示一类文本元素"""
|
||||
text: str
|
||||
styles: dict[tuple[int, int], list[str]]
|
||||
|
||||
def cover(self, text: str): ...
|
||||
def mark(self, start: Optional[int] = None, end: Optional[int] = None, *styles: 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):
|
||||
id: Optional[str]
|
||||
url: 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对象, 表示一类图片元素"""
|
||||
width: Optional[int]
|
||||
height: Optional[int]
|
||||
|
||||
class Audio(Media):
|
||||
"""Audio对象, 表示一类音频元素"""
|
||||
duration: Optional[float]
|
||||
|
||||
class Voice(Media):
|
||||
"""Voice对象, 表示一类语音元素"""
|
||||
duration: Optional[float]
|
||||
|
||||
class Video(Media):
|
||||
"""Video对象, 表示一类视频元素"""
|
||||
thumbnail: Optional[Image]
|
||||
duration: Optional[float]
|
||||
|
||||
class File(Media):
|
||||
"""File对象, 表示一类文件元素"""
|
||||
|
||||
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 Reference(Segment):
|
||||
"""Reference对象,表示一类引用消息。转发消息 (Forward) 也属于此类"""
|
||||
id: Optional[str]
|
||||
nodes: Sequence[Union[RefNode, CustomNode]]
|
||||
|
||||
class Button(Segment):
|
||||
"""Button对象,表示一类按钮消息"""
|
||||
flag: Literal["action", "link", "input", "enter"]
|
||||
"""
|
||||
- 点击 action 类型的按钮时会触发一个关于 按钮回调 事件,该事件的 button 资源会包含上述 id
|
||||
- 点击 link 类型的按钮时会打开一个链接或者小程序,该链接的地址为 `url`
|
||||
- 点击 input 类型的按钮时会在用户的输入框中填充 `text`
|
||||
- 点击 enter 类型的按钮时会直接发送 `text`
|
||||
"""
|
||||
label: Union[str, Text]
|
||||
"""按钮上的文字"""
|
||||
clicked_label: Optional[str]
|
||||
"""点击后按钮上的文字"""
|
||||
id: Optional[str]
|
||||
url: Optional[str]
|
||||
text: Optional[str]
|
||||
style: Optional[str]
|
||||
"""
|
||||
仅建议使用下列值:primary, secondary, success, warning, danger, info, link, grey, blue
|
||||
|
||||
此处规定 `grey` 与 `secondary` 等同, `blue` 与 `primary` 等同
|
||||
"""
|
||||
permission: Union[Literal["admin", "all"], list[At]] = "all"
|
||||
"""
|
||||
- admin: 仅管理者可操作
|
||||
- all: 所有人可操作
|
||||
- list[At]: 指定用户/身份组可操作
|
||||
"""
|
||||
|
||||
class Keyboard(Segment):
|
||||
"""Keyboard对象,表示一行按钮元素"""
|
||||
id: Optional[str]
|
||||
"""此处一般用来表示模板id,特殊情况下可能表示例如 bot_appid 等"""
|
||||
buttons: Optional[list[Button]]
|
||||
row: Optional[int]
|
||||
"""当消息中只写有一个 Keyboard 时可根据此参数约定按钮组的列数"""
|
||||
|
||||
class Other(Segment):
|
||||
"""其他 Segment"""
|
||||
origin: MessageSegment
|
||||
|
||||
class I18n(Segment):
|
||||
"""特殊的 Segment,用于 i18n 消息"""
|
||||
item_or_scope: Union[LangItem, str]
|
||||
type_: Optional[str] = None
|
||||
|
||||
def tp(self) -> UniMessageTemplate: ...
|
||||
```
|
||||
|
||||
:::tip
|
||||
|
||||
或许你注意到了 `Segment` 上有一个 `children` 属性。
|
||||
|
||||
这是因为在 [`Satori`](https://satori.js.org/zh-CN/) 协议的规定下,一类元素可以用其子元素来代表一类兼容性消息
|
||||
(例如,qq 的商场表情在某些平台上可以用图片代替)。
|
||||
|
||||
为此,本插件提供了 `select` 方法来表达 "命令中获取子元素" 的方法:
|
||||
|
||||
```python
|
||||
from nonebot_plugin_alconna import Args, Image, Alconna, select
|
||||
from nonebot_plugin_alconna.builtins.uniseg.market_face import MarketFace
|
||||
|
||||
# 表示这个指令需要的图片会在目标元素下进行搜索,将所有符合 Image 的元素选出来并将第一个作为结果
|
||||
alc1 = Alconna("make_meme", Args["name", str]["img", select(Image).first]) # 也可以使用 select(Image).nth(0)
|
||||
|
||||
# 表示这个指令需要的图片要么直接是 Image 要么是在 MarketFace 元素内的 Image
|
||||
alc2 = Alconna("make_meme", Args["name", str]["img", [Image, select(Image).from_(MarketFace)]])
|
||||
```
|
||||
|
||||
也可以参考通用消息的 [`嵌套提取`](./message.mdx#嵌套提取)
|
||||
|
||||
:::
|
||||
|
||||
## 自定义消息段
|
||||
|
||||
`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 的处理方法。
|
||||
@@ -0,0 +1,282 @@
|
||||
---
|
||||
sidebar_position: 4
|
||||
description: 辅助模型
|
||||
---
|
||||
|
||||
import Tabs from "@theme/Tabs";
|
||||
import TabItem from "@theme/TabItem";
|
||||
|
||||
# 辅助功能
|
||||
|
||||
`uniseg` 模块同时提供了多种方法以通用消息操作。
|
||||
|
||||
:::note
|
||||
|
||||
这些方法中与 `event`, `bot` 相关的参数都会尝试从上下文中获取对象。
|
||||
|
||||
:::
|
||||
|
||||
## 消息事件 ID
|
||||
|
||||
消息事件 ID 是用来标识当前消息事件的唯一 ID,通常用于回复/撤回/编辑/表态当前消息。
|
||||
|
||||
<Tabs groupId="get_msgid">
|
||||
<TabItem value="depend" label="使用依赖注入">
|
||||
|
||||
通过提供的 `MessageId` 或 `MsgId` 依赖注入器来获取消息事件 id。
|
||||
|
||||
```python
|
||||
from nonebot_plugin_alconna.uniseg import MsgId
|
||||
|
||||
|
||||
matcher = on_xxx(...)
|
||||
|
||||
@matcher.handle()
|
||||
asycn def _(msg_id: MsgId):
|
||||
...
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="method" label="使用获取函数">
|
||||
|
||||
```python
|
||||
from nonebot import Event, Bot
|
||||
from nonebot_plugin_alconna.uniseg import get_message_id
|
||||
|
||||
|
||||
matcher = on_xxx(...)
|
||||
|
||||
@matcher.handle()
|
||||
asycn def _(bot: Bot, event: Event):
|
||||
msg_id: str = get_message_id(event, bot)
|
||||
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
:::caution
|
||||
|
||||
该方法获取的消息事件 ID 不推荐直接用于各适配器的 API 调用中,可能会操作失败。
|
||||
|
||||
:::
|
||||
|
||||
## 发送对象
|
||||
|
||||
消息发送对象是用来描述当前消息事件的可发送对象或者主动发送消息时的目标对象,它包含了以下属性:
|
||||
|
||||
```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: str | None
|
||||
"""机器人id,若为 None 则 Bot 对象会随机选择"""
|
||||
selector: Callable[[Bot], Awaitable[bool]] | None
|
||||
"""选择器,用于在多个 Bot 对象中选择特定 Bot"""
|
||||
extra: dict[str, Any]
|
||||
"""额外信息,用于适配器扩展"""
|
||||
```
|
||||
|
||||
<Tabs groupId="get_target">
|
||||
<TabItem value="depend" label="使用依赖注入">
|
||||
|
||||
通过提供的 `MessageTarget` 或 `MsgTarget` 依赖注入器来获取消息发送对象。
|
||||
|
||||
```python
|
||||
from nonebot_plugin_alconna.uniseg import MsgTarget
|
||||
|
||||
|
||||
matcher = on_xxx(...)
|
||||
|
||||
@matcher.handle()
|
||||
asycn def _(target: MsgTarget):
|
||||
...
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="method" label="使用获取函数">
|
||||
|
||||
```python
|
||||
from nonebot import Event, Bot
|
||||
from nonebot_plugin_alconna.uniseg import Target, get_target
|
||||
|
||||
|
||||
matcher = on_xxx(...)
|
||||
|
||||
@matcher.handle()
|
||||
asycn def _(bot: Bot, event: Event):
|
||||
target: Target = get_target(event, bot)
|
||||
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
主动构造一个发送对象时,则需要如下参数:
|
||||
|
||||
- `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)
|
||||
# 主动发送消息给群号为 12345 的 QQ 群聊
|
||||
target1 = Target("12345", scope=SupportScope.qq_client)
|
||||
await UniMessage("Hello!").send(target=target1)
|
||||
```
|
||||
|
||||
### 选择器
|
||||
|
||||
一般来说,主动发送消息时,`UniMessage.send` 或 `Target.self_id` 应指定一个 `Bot` 对象。但是这样会加重开发者的负担。
|
||||
|
||||
因此,我们提供了选择器来帮助开发者选择一个 `Bot` 对象。当然,这并非说明一定需要传入 `selector` 参数。
|
||||
|
||||
事实上,构造 `Target` 对象时,`self_id`, `scope`, `adapter` 和 `platform` 都会参与到 `selector` 的构造中。
|
||||
|
||||
:::tip
|
||||
|
||||
你其实可以使用 `Target` 来帮你筛选 `Bot` 对象:
|
||||
|
||||
```python
|
||||
async def _():
|
||||
target = Target("12345", scope=SupportScope.qq_client)
|
||||
bot = await target.select()
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
若配置了 [`alconna_apply_fetch_targets`](../config.md#alconna_apply_fetch_targets) 选项,则在启动时会主动拉取一次发送对象列表。即对于
|
||||
某一主动构造的 `Target` 对象,插件将其与拉取下来的众多发送对象进行匹配,并选择第一个符合条件的发送对象,以选择对应的 Bot 对象。
|
||||
|
||||
## 撤回消息
|
||||
|
||||
通过 `message_recall` 方法来撤回消息事件。
|
||||
|
||||
```python
|
||||
from nonebot_plugin_alconna.uniseg import message_recall
|
||||
|
||||
matcher = on_xxx(...)
|
||||
|
||||
@matcher.handle()
|
||||
async def _(msg_id: MsgId):
|
||||
await message_recall(msg_id)
|
||||
```
|
||||
|
||||
`message_recall` 方法的参数如下:
|
||||
|
||||
```python
|
||||
async def message_recall(
|
||||
message_id: str | None = None,
|
||||
event: Event | None = None,
|
||||
bot: Bot | None = None,
|
||||
adapter: str | None = None
|
||||
): ...
|
||||
```
|
||||
|
||||
当 `message_id` 为 `None` 时,插件会尝试从 `event` 中获取消息事件 ID。
|
||||
|
||||
## 编辑消息
|
||||
|
||||
通过 `message_edit` 方法来编辑消息事件。
|
||||
|
||||
```python
|
||||
from nonebot_plugin_alconna.uniseg import UniMessage, message_edit
|
||||
|
||||
matcher = on_xxx(...)
|
||||
|
||||
@matcher.handle()
|
||||
async def _():
|
||||
await message_edit(UniMessage.text("1234"))
|
||||
```
|
||||
|
||||
`message_edit` 方法的参数如下:
|
||||
|
||||
```python
|
||||
async def message_edit(
|
||||
msg: UniMessage,
|
||||
message_id: str | None = None,
|
||||
event: Event | None = None,
|
||||
bot: Bot | None = None,
|
||||
adapter: str | None = None,
|
||||
): ...
|
||||
```
|
||||
|
||||
当 `message_id` 为 `None` 时,插件会尝试从 `event` 中获取消息事件 ID。
|
||||
|
||||
## 表态消息
|
||||
|
||||
:::caution
|
||||
|
||||
该方法属于实验性功能。其接口可能会在未来的版本中发生变化。
|
||||
|
||||
:::
|
||||
|
||||
通过 `message_reaction` 方法来表态消息事件。
|
||||
|
||||
```python
|
||||
from nonebot_plugin_alconna.uniseg import message_reaction
|
||||
|
||||
matcher = on_xxx(...)
|
||||
|
||||
@matcher.handle()
|
||||
async def _():
|
||||
await message_reaction("👍")
|
||||
```
|
||||
|
||||
`message_reaction` 方法的参数如下:
|
||||
|
||||
```python
|
||||
async def message_reaction(
|
||||
reaction: str | Emoji,
|
||||
message_id: str | None = None,
|
||||
event: Event | None = None,
|
||||
bot: Bot | None = None,
|
||||
adapter: str | None = None,
|
||||
delete: bool = False,
|
||||
): ...
|
||||
```
|
||||
|
||||
当 `message_id` 为 `None` 时,插件会尝试从 `event` 中获取消息事件 ID。
|
||||
|
||||
`delete` 参数表示是否删除**自己的**表态消息,默认为 `False`。
|
||||
|
||||
## 响应规则
|
||||
|
||||
`uniseg` 模块提供了两个响应规则:
|
||||
|
||||
- `at_in`: 是否在消息中 @ 了指定的用户
|
||||
- `at_me`: 是否在消息中 @ 了机器人
|
||||
|
||||
相较于 NoneBot 内置的 `to_me` 规则,`at_me` 规则只会在消息中 @ 机器人时触发。
|
||||
|
||||
```python
|
||||
from nonebot_plugin_alconna.uniseg import at_me
|
||||
|
||||
matcher = on_xxx(..., rule=at_me())
|
||||
```
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user