Repository: nonebot/nonebot2 Branch: master Commit: 080cbca069c3 Files: 632 Total size: 2.8 MB Directory structure: gitextract_x5tywv6m/ ├── .devcontainer/ │ └── devcontainer.json ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── adapter_publish.yml │ │ ├── bot_publish.yml │ │ ├── bug_report.yml │ │ ├── config.yml │ │ ├── document.yml │ │ ├── feature_request.yml │ │ └── plugin_publish.yml │ ├── actions/ │ │ ├── build-api-doc/ │ │ │ └── action.yml │ │ ├── setup-node/ │ │ │ └── action.yml │ │ └── setup-python/ │ │ └── action.yml │ ├── dependabot.yml │ ├── release-drafter.yml │ └── workflows/ │ ├── codecov.yml │ ├── noneflow.yml │ ├── pyright.yml │ ├── release-drafter.yml │ ├── release.yml │ ├── ruff.yml │ ├── website-deploy.yml │ ├── website-preview-cd.yml │ └── website-preview-ci.yml ├── .gitignore ├── .markdownlint.yaml ├── .pre-commit-config.yaml ├── .prettierignore ├── .prettierrc ├── .stylelintrc.js ├── .yarnrc ├── CHANGELOG.md ├── CITATION.cff ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── assets/ │ ├── adapters.json5 │ ├── bots.json5 │ ├── drivers.json5 │ └── plugins.json5 ├── nonebot/ │ ├── __init__.py │ ├── adapters/ │ │ └── __init__.py │ ├── compat.py │ ├── config.py │ ├── consts.py │ ├── dependencies/ │ │ ├── __init__.py │ │ └── utils.py │ ├── drivers/ │ │ ├── __init__.py │ │ ├── aiohttp.py │ │ ├── fastapi.py │ │ ├── httpx.py │ │ ├── none.py │ │ ├── quart.py │ │ └── websockets.py │ ├── exception.py │ ├── internal/ │ │ ├── __init__.py │ │ ├── adapter/ │ │ │ ├── __init__.py │ │ │ ├── adapter.py │ │ │ ├── bot.py │ │ │ ├── event.py │ │ │ ├── message.py │ │ │ └── template.py │ │ ├── driver/ │ │ │ ├── __init__.py │ │ │ ├── _lifespan.py │ │ │ ├── abstract.py │ │ │ ├── combine.py │ │ │ └── model.py │ │ ├── matcher/ │ │ │ ├── __init__.py │ │ │ ├── manager.py │ │ │ ├── matcher.py │ │ │ └── provider.py │ │ ├── params.py │ │ ├── permission.py │ │ └── rule.py │ ├── log.py │ ├── matcher.py │ ├── message.py │ ├── params.py │ ├── permission.py │ ├── plugin/ │ │ ├── __init__.py │ │ ├── load.py │ │ ├── manager.py │ │ ├── model.py │ │ ├── on.py │ │ └── on.pyi │ ├── plugins/ │ │ ├── echo.py │ │ └── single_session.py │ ├── py.typed │ ├── rule.py │ ├── typing.py │ └── utils.py ├── package.json ├── packages/ │ └── nonebot-plugin-docs/ │ ├── README.md │ ├── nonebot_plugin_docs/ │ │ ├── __init__.py │ │ └── drivers/ │ │ └── fastapi.py │ └── pyproject.toml ├── pyproject.toml ├── scripts/ │ ├── build-api-docs.sh │ ├── run-tests.sh │ └── setup-envs.sh ├── tests/ │ ├── .coveragerc │ ├── bad_plugins/ │ │ └── bad_plugin.py │ ├── conftest.py │ ├── dynamic/ │ │ ├── manager.py │ │ ├── path.py │ │ ├── require_not_declared.py │ │ ├── require_not_loaded/ │ │ │ ├── __init__.py │ │ │ ├── subplugin1.py │ │ │ └── subplugin2.py │ │ └── simple.py │ ├── fake_server.py │ ├── plugins/ │ │ ├── _hidden.py │ │ ├── export.py │ │ ├── matcher/ │ │ │ ├── __init__.py │ │ │ ├── matcher_expire.py │ │ │ ├── matcher_info.py │ │ │ ├── matcher_permission.py │ │ │ ├── matcher_process.py │ │ │ └── matcher_type.py │ │ ├── metadata.py │ │ ├── metadata_2.py │ │ ├── metadata_3.py │ │ ├── nested/ │ │ │ ├── __init__.py │ │ │ └── plugins/ │ │ │ ├── nested_subplugin.py │ │ │ └── nested_subplugin2.py │ │ ├── param/ │ │ │ ├── __init__.py │ │ │ ├── param_arg.py │ │ │ ├── param_bot.py │ │ │ ├── param_default.py │ │ │ ├── param_depend.py │ │ │ ├── param_event.py │ │ │ ├── param_exception.py │ │ │ ├── param_matcher.py │ │ │ ├── param_state.py │ │ │ └── priority.py │ │ ├── plugin/ │ │ │ ├── __init__.py │ │ │ └── matchers.py │ │ └── require.py │ ├── plugins.empty.toml │ ├── plugins.invalid.json │ ├── plugins.invalid.toml │ ├── plugins.json │ ├── plugins.legacy.toml │ ├── plugins.toml │ ├── pyproject.toml │ ├── python_3_12/ │ │ ├── plugins/ │ │ │ └── aliased_param/ │ │ │ ├── __init__.py │ │ │ ├── param_arg.py │ │ │ ├── param_bot.py │ │ │ ├── param_depend.py │ │ │ ├── param_event.py │ │ │ ├── param_exception.py │ │ │ ├── param_matcher.py │ │ │ └── param_state.py │ │ └── pyproject.toml │ ├── test_adapters/ │ │ ├── test_adapter.py │ │ ├── test_bot.py │ │ ├── test_message.py │ │ └── test_template.py │ ├── test_broadcast.py │ ├── test_compat.py │ ├── test_config.py │ ├── test_driver.py │ ├── test_echo.py │ ├── test_init.py │ ├── test_matcher/ │ │ ├── test_matcher.py │ │ └── test_provider.py │ ├── test_param.py │ ├── test_permission.py │ ├── test_plugin/ │ │ ├── test_get.py │ │ ├── test_load.py │ │ ├── test_manager.py │ │ └── test_on.py │ ├── test_rule.py │ ├── test_single_session.py │ ├── test_utils.py │ └── utils.py ├── tsconfig.json └── website/ ├── docs/ │ ├── README.md │ ├── advanced/ │ │ ├── adapter.md │ │ ├── dependency.mdx │ │ ├── driver.md │ │ ├── matcher-provider.md │ │ ├── matcher.md │ │ ├── plugin-info.md │ │ ├── plugin-nesting.md │ │ ├── requiring.md │ │ ├── routing.md │ │ ├── runtime-hook.md │ │ └── session-updating.md │ ├── api/ │ │ ├── .gitkeep │ │ ├── adapters/ │ │ │ └── _category_.json │ │ ├── dependencies/ │ │ │ └── _category_.json │ │ ├── drivers/ │ │ │ └── _category_.json │ │ └── plugin/ │ │ └── _category_.json │ ├── appendices/ │ │ ├── api-calling.mdx │ │ ├── config.mdx │ │ ├── log.md │ │ ├── overload.md │ │ ├── permission.mdx │ │ ├── rule.md │ │ ├── session-control.mdx │ │ ├── session-state.md │ │ └── whats-next.md │ ├── best-practice/ │ │ ├── alconna/ │ │ │ ├── README.mdx │ │ │ ├── _category_.json │ │ │ ├── builtins.mdx │ │ │ ├── command.md │ │ │ ├── config.md │ │ │ ├── matcher.mdx │ │ │ ├── shortcut.md │ │ │ └── uniseg/ │ │ │ ├── README.md │ │ │ ├── _category_.json │ │ │ ├── message.mdx │ │ │ ├── segment.md │ │ │ └── utils.mdx │ │ ├── data-storing.md │ │ ├── database/ │ │ │ ├── README.mdx │ │ │ ├── _category_.json │ │ │ ├── developer/ │ │ │ │ ├── README.md │ │ │ │ ├── _category_.json │ │ │ │ ├── dependency.md │ │ │ │ └── test.md │ │ │ └── user.md │ │ ├── deployment.mdx │ │ ├── error-tracking.md │ │ ├── htmlkit-render.md │ │ ├── multi-adapter.mdx │ │ ├── scheduler.md │ │ └── testing/ │ │ ├── README.mdx │ │ ├── _category_.json │ │ ├── behavior.mdx │ │ └── mock-network.md │ ├── community/ │ │ ├── contact.md │ │ └── contributing.md │ ├── developer/ │ │ ├── adapter-writing.md │ │ └── plugin-publishing.mdx │ ├── editor-support.md │ ├── ospp/ │ │ ├── 2021.md │ │ ├── 2022.md │ │ ├── 2023.md │ │ ├── 2024.md │ │ └── 2025.md │ ├── quick-start.mdx │ └── tutorial/ │ ├── application.mdx │ ├── create-plugin.md │ ├── event-data.mdx │ ├── fundamentals.md │ ├── handler.mdx │ ├── matcher.md │ ├── message.md │ └── store.mdx ├── docusaurus.config.ts ├── package.json ├── sidebars.ts ├── src/ │ ├── changelog/ │ │ └── changelog.md │ ├── components/ │ │ ├── Asciinema/ │ │ │ ├── container.tsx │ │ │ ├── index.tsx │ │ │ └── styles.css │ │ ├── Form/ │ │ │ ├── Adapter.tsx │ │ │ ├── Bot.tsx │ │ │ ├── Items/ │ │ │ │ └── Tag/ │ │ │ │ ├── index.tsx │ │ │ │ └── styles.css │ │ │ ├── Plugin.tsx │ │ │ ├── index.tsx │ │ │ └── styles.css │ │ ├── Home/ │ │ │ ├── Feature.tsx │ │ │ ├── Hero.tsx │ │ │ ├── index.tsx │ │ │ └── styles.css │ │ ├── Messenger/ │ │ │ ├── index.tsx │ │ │ └── styles.css │ │ ├── Modal/ │ │ │ ├── index.tsx │ │ │ └── styles.css │ │ ├── Paginate/ │ │ │ ├── index.tsx │ │ │ └── styles.css │ │ ├── Resource/ │ │ │ ├── Avatar/ │ │ │ │ └── index.tsx │ │ │ ├── Card/ │ │ │ │ ├── index.tsx │ │ │ │ └── styles.css │ │ │ ├── DetailCard/ │ │ │ │ ├── index.tsx │ │ │ │ ├── styles.css │ │ │ │ └── types.ts │ │ │ ├── Tag/ │ │ │ │ ├── index.tsx │ │ │ │ └── styles.css │ │ │ └── ValidStatus/ │ │ │ └── index.tsx │ │ ├── Searcher/ │ │ │ ├── index.tsx │ │ │ └── styles.css │ │ ├── Store/ │ │ │ ├── Content/ │ │ │ │ ├── Adapter.tsx │ │ │ │ ├── Bot.tsx │ │ │ │ ├── Driver.tsx │ │ │ │ └── Plugin.tsx │ │ │ ├── Layout.tsx │ │ │ ├── Toolbar.tsx │ │ │ └── styles.css │ │ └── Tag/ │ │ ├── index.tsx │ │ └── styles.css │ ├── libs/ │ │ ├── color.ts │ │ ├── filter.ts │ │ ├── search.ts │ │ ├── sorter.ts │ │ ├── store.ts │ │ ├── toolbar.ts │ │ └── valid.ts │ ├── pages/ │ │ ├── index.tsx │ │ └── store/ │ │ ├── adapters.tsx │ │ ├── bots.tsx │ │ ├── drivers.tsx │ │ ├── index.tsx │ │ └── plugins.tsx │ ├── plugins/ │ │ └── webpack-plugin.ts │ ├── theme/ │ │ ├── Footer/ │ │ │ └── Copyright/ │ │ │ └── index.tsx │ │ ├── Icon/ │ │ │ ├── Cloudflare.tsx │ │ │ └── Netlify.tsx │ │ └── Page/ │ │ └── TOC/ │ │ └── Container/ │ │ ├── index.tsx │ │ └── styles.css │ └── types/ │ ├── adapter.ts │ ├── bot.ts │ ├── driver.ts │ ├── plugin.ts │ └── tag.ts ├── static/ │ ├── manifest.json │ ├── service-worker.js │ └── uwu.js ├── tailwind.config.ts ├── tsconfig.json ├── versioned_docs/ │ ├── version-2.4.2/ │ │ ├── README.md │ │ ├── advanced/ │ │ │ ├── adapter.md │ │ │ ├── dependency.mdx │ │ │ ├── driver.md │ │ │ ├── matcher-provider.md │ │ │ ├── matcher.md │ │ │ ├── plugin-info.md │ │ │ ├── plugin-nesting.md │ │ │ ├── requiring.md │ │ │ ├── routing.md │ │ │ ├── runtime-hook.md │ │ │ └── session-updating.md │ │ ├── api/ │ │ │ ├── .gitkeep │ │ │ ├── adapters/ │ │ │ │ ├── _category_.json │ │ │ │ └── index.md │ │ │ ├── compat.md │ │ │ ├── config.md │ │ │ ├── consts.md │ │ │ ├── dependencies/ │ │ │ │ ├── _category_.json │ │ │ │ ├── index.md │ │ │ │ └── utils.md │ │ │ ├── drivers/ │ │ │ │ ├── _category_.json │ │ │ │ ├── aiohttp.md │ │ │ │ ├── fastapi.md │ │ │ │ ├── httpx.md │ │ │ │ ├── index.md │ │ │ │ ├── none.md │ │ │ │ ├── quart.md │ │ │ │ └── websockets.md │ │ │ ├── exception.md │ │ │ ├── index.md │ │ │ ├── log.md │ │ │ ├── matcher.md │ │ │ ├── message.md │ │ │ ├── params.md │ │ │ ├── permission.md │ │ │ ├── plugin/ │ │ │ │ ├── _category_.json │ │ │ │ ├── index.md │ │ │ │ ├── load.md │ │ │ │ ├── manager.md │ │ │ │ ├── model.md │ │ │ │ └── on.md │ │ │ ├── rule.md │ │ │ ├── typing.md │ │ │ └── utils.md │ │ ├── appendices/ │ │ │ ├── api-calling.mdx │ │ │ ├── config.mdx │ │ │ ├── log.md │ │ │ ├── overload.md │ │ │ ├── permission.mdx │ │ │ ├── rule.md │ │ │ ├── session-control.mdx │ │ │ ├── session-state.md │ │ │ └── whats-next.md │ │ ├── best-practice/ │ │ │ ├── alconna/ │ │ │ │ ├── README.mdx │ │ │ │ ├── _category_.json │ │ │ │ ├── command.md │ │ │ │ ├── config.md │ │ │ │ ├── matcher.mdx │ │ │ │ └── uniseg.mdx │ │ │ ├── data-storing.md │ │ │ ├── database/ │ │ │ │ ├── README.mdx │ │ │ │ ├── _category_.json │ │ │ │ ├── developer/ │ │ │ │ │ ├── README.md │ │ │ │ │ ├── _category_.json │ │ │ │ │ ├── dependency.md │ │ │ │ │ └── test.md │ │ │ │ └── user.md │ │ │ ├── deployment.mdx │ │ │ ├── error-tracking.md │ │ │ ├── htmlkit-render.md │ │ │ ├── multi-adapter.mdx │ │ │ ├── scheduler.md │ │ │ └── testing/ │ │ │ ├── README.mdx │ │ │ ├── _category_.json │ │ │ ├── behavior.mdx │ │ │ └── mock-network.md │ │ ├── community/ │ │ │ ├── contact.md │ │ │ └── contributing.md │ │ ├── developer/ │ │ │ ├── adapter-writing.md │ │ │ └── plugin-publishing.mdx │ │ ├── editor-support.md │ │ ├── ospp/ │ │ │ ├── 2021.md │ │ │ ├── 2022.md │ │ │ ├── 2023.md │ │ │ ├── 2024.md │ │ │ └── 2025.md │ │ ├── quick-start.mdx │ │ └── tutorial/ │ │ ├── application.md │ │ ├── create-plugin.md │ │ ├── event-data.mdx │ │ ├── fundamentals.md │ │ ├── handler.mdx │ │ ├── matcher.md │ │ ├── message.md │ │ └── store.mdx │ ├── version-2.4.3/ │ │ ├── README.md │ │ ├── advanced/ │ │ │ ├── adapter.md │ │ │ ├── dependency.mdx │ │ │ ├── driver.md │ │ │ ├── matcher-provider.md │ │ │ ├── matcher.md │ │ │ ├── plugin-info.md │ │ │ ├── plugin-nesting.md │ │ │ ├── requiring.md │ │ │ ├── routing.md │ │ │ ├── runtime-hook.md │ │ │ └── session-updating.md │ │ ├── api/ │ │ │ ├── .gitkeep │ │ │ ├── adapters/ │ │ │ │ ├── _category_.json │ │ │ │ └── index.md │ │ │ ├── compat.md │ │ │ ├── config.md │ │ │ ├── consts.md │ │ │ ├── dependencies/ │ │ │ │ ├── _category_.json │ │ │ │ ├── index.md │ │ │ │ └── utils.md │ │ │ ├── drivers/ │ │ │ │ ├── _category_.json │ │ │ │ ├── aiohttp.md │ │ │ │ ├── fastapi.md │ │ │ │ ├── httpx.md │ │ │ │ ├── index.md │ │ │ │ ├── none.md │ │ │ │ ├── quart.md │ │ │ │ └── websockets.md │ │ │ ├── exception.md │ │ │ ├── index.md │ │ │ ├── log.md │ │ │ ├── matcher.md │ │ │ ├── message.md │ │ │ ├── params.md │ │ │ ├── permission.md │ │ │ ├── plugin/ │ │ │ │ ├── _category_.json │ │ │ │ ├── index.md │ │ │ │ ├── load.md │ │ │ │ ├── manager.md │ │ │ │ ├── model.md │ │ │ │ └── on.md │ │ │ ├── rule.md │ │ │ ├── typing.md │ │ │ └── utils.md │ │ ├── appendices/ │ │ │ ├── api-calling.mdx │ │ │ ├── config.mdx │ │ │ ├── log.md │ │ │ ├── overload.md │ │ │ ├── permission.mdx │ │ │ ├── rule.md │ │ │ ├── session-control.mdx │ │ │ ├── session-state.md │ │ │ └── whats-next.md │ │ ├── best-practice/ │ │ │ ├── alconna/ │ │ │ │ ├── README.mdx │ │ │ │ ├── _category_.json │ │ │ │ ├── builtins.mdx │ │ │ │ ├── command.md │ │ │ │ ├── config.md │ │ │ │ ├── matcher.mdx │ │ │ │ ├── shortcut.md │ │ │ │ └── uniseg/ │ │ │ │ ├── README.md │ │ │ │ ├── _category_.json │ │ │ │ ├── message.mdx │ │ │ │ ├── segment.md │ │ │ │ └── utils.mdx │ │ │ ├── data-storing.md │ │ │ ├── database/ │ │ │ │ ├── README.mdx │ │ │ │ ├── _category_.json │ │ │ │ ├── developer/ │ │ │ │ │ ├── README.md │ │ │ │ │ ├── _category_.json │ │ │ │ │ ├── dependency.md │ │ │ │ │ └── test.md │ │ │ │ └── user.md │ │ │ ├── deployment.mdx │ │ │ ├── error-tracking.md │ │ │ ├── htmlkit-render.md │ │ │ ├── multi-adapter.mdx │ │ │ ├── scheduler.md │ │ │ └── testing/ │ │ │ ├── README.mdx │ │ │ ├── _category_.json │ │ │ ├── behavior.mdx │ │ │ └── mock-network.md │ │ ├── community/ │ │ │ ├── contact.md │ │ │ └── contributing.md │ │ ├── developer/ │ │ │ ├── adapter-writing.md │ │ │ └── plugin-publishing.mdx │ │ ├── editor-support.md │ │ ├── ospp/ │ │ │ ├── 2021.md │ │ │ ├── 2022.md │ │ │ ├── 2023.md │ │ │ ├── 2024.md │ │ │ └── 2025.md │ │ ├── quick-start.mdx │ │ └── tutorial/ │ │ ├── application.md │ │ ├── create-plugin.md │ │ ├── event-data.mdx │ │ ├── fundamentals.md │ │ ├── handler.mdx │ │ ├── matcher.md │ │ ├── message.md │ │ └── store.mdx │ └── version-2.4.4/ │ ├── README.md │ ├── advanced/ │ │ ├── adapter.md │ │ ├── dependency.mdx │ │ ├── driver.md │ │ ├── matcher-provider.md │ │ ├── matcher.md │ │ ├── plugin-info.md │ │ ├── plugin-nesting.md │ │ ├── requiring.md │ │ ├── routing.md │ │ ├── runtime-hook.md │ │ └── session-updating.md │ ├── api/ │ │ ├── .gitkeep │ │ ├── adapters/ │ │ │ ├── _category_.json │ │ │ └── index.md │ │ ├── compat.md │ │ ├── config.md │ │ ├── consts.md │ │ ├── dependencies/ │ │ │ ├── _category_.json │ │ │ ├── index.md │ │ │ └── utils.md │ │ ├── drivers/ │ │ │ ├── _category_.json │ │ │ ├── aiohttp.md │ │ │ ├── fastapi.md │ │ │ ├── httpx.md │ │ │ ├── index.md │ │ │ ├── none.md │ │ │ ├── quart.md │ │ │ └── websockets.md │ │ ├── exception.md │ │ ├── index.md │ │ ├── log.md │ │ ├── matcher.md │ │ ├── message.md │ │ ├── params.md │ │ ├── permission.md │ │ ├── plugin/ │ │ │ ├── _category_.json │ │ │ ├── index.md │ │ │ ├── load.md │ │ │ ├── manager.md │ │ │ ├── model.md │ │ │ └── on.md │ │ ├── rule.md │ │ ├── typing.md │ │ └── utils.md │ ├── appendices/ │ │ ├── api-calling.mdx │ │ ├── config.mdx │ │ ├── log.md │ │ ├── overload.md │ │ ├── permission.mdx │ │ ├── rule.md │ │ ├── session-control.mdx │ │ ├── session-state.md │ │ └── whats-next.md │ ├── best-practice/ │ │ ├── alconna/ │ │ │ ├── README.mdx │ │ │ ├── _category_.json │ │ │ ├── builtins.mdx │ │ │ ├── command.md │ │ │ ├── config.md │ │ │ ├── matcher.mdx │ │ │ ├── shortcut.md │ │ │ └── uniseg/ │ │ │ ├── README.md │ │ │ ├── _category_.json │ │ │ ├── message.mdx │ │ │ ├── segment.md │ │ │ └── utils.mdx │ │ ├── data-storing.md │ │ ├── database/ │ │ │ ├── README.mdx │ │ │ ├── _category_.json │ │ │ ├── developer/ │ │ │ │ ├── README.md │ │ │ │ ├── _category_.json │ │ │ │ ├── dependency.md │ │ │ │ └── test.md │ │ │ └── user.md │ │ ├── deployment.mdx │ │ ├── error-tracking.md │ │ ├── htmlkit-render.md │ │ ├── multi-adapter.mdx │ │ ├── scheduler.md │ │ └── testing/ │ │ ├── README.mdx │ │ ├── _category_.json │ │ ├── behavior.mdx │ │ └── mock-network.md │ ├── community/ │ │ ├── contact.md │ │ └── contributing.md │ ├── developer/ │ │ ├── adapter-writing.md │ │ └── plugin-publishing.mdx │ ├── editor-support.md │ ├── ospp/ │ │ ├── 2021.md │ │ ├── 2022.md │ │ ├── 2023.md │ │ ├── 2024.md │ │ └── 2025.md │ ├── quick-start.mdx │ └── tutorial/ │ ├── application.mdx │ ├── create-plugin.md │ ├── event-data.mdx │ ├── fundamentals.md │ ├── handler.mdx │ ├── matcher.md │ ├── message.md │ └── store.mdx ├── versioned_sidebars/ │ ├── version-2.4.2-sidebars.json │ ├── version-2.4.3-sidebars.json │ └── version-2.4.4-sidebars.json └── versions.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .devcontainer/devcontainer.json ================================================ { "name": "Ubuntu", "image": "mcr.microsoft.com/devcontainers/base:ubuntu", "features": { "ghcr.io/jsburckhardt/devcontainer-features/uv:1": {}, "ghcr.io/devcontainers/features/node:1": {}, "ghcr.io/meaningful-ooo/devcontainer-features/fish:2": {} }, "postCreateCommand": "corepack enable && COREPACK_ENABLE_DOWNLOAD_PROMPT=0 ./scripts/setup-envs.sh", "customizations": { "vscode": { "settings": { "python.analysis.diagnosticMode": "workspace", "[python]": { "editor.defaultFormatter": "charliermarsh.ruff", "editor.codeActionsOnSave": { "source.fixAll.ruff": "explicit", "source.organizeImports": "explicit" } }, "[javascript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[html]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[typescript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[javascriptreact]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[typescriptreact]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, "files.exclude": { "**/__pycache__": true }, "files.watcherExclude": { "**/target/**": true, "**/__pycache__": true } }, "extensions": [ "ms-python.python", "ms-python.vscode-pylance", "charliermarsh.ruff", "EditorConfig.EditorConfig", "esbenp.prettier-vscode", "bradlc.vscode-tailwindcss" ] } } } ================================================ FILE: .editorconfig ================================================ # http://editorconfig.org root = true [*] indent_style = space indent_size = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true # The JSON files contain newlines inconsistently [*.json] insert_final_newline = ignore # Minified JavaScript files shouldn't be changed [**.min.js] indent_style = ignore insert_final_newline = ignore # Makefiles always use tabs for indentation [Makefile] indent_style = tab # Batch files use tabs for indentation [*.bat] indent_style = tab [*.md] trim_trailing_whitespace = false # Matches the exact files either package.json or .travis.yml [{package.json,.travis.yml}] indent_size = 2 [{*.py,*.pyi}] indent_size = 4 ================================================ FILE: .eslintignore ================================================ dist node_modules .yarn .history build lib ================================================ FILE: .eslintrc.js ================================================ const OFF = 0; const WARNING = 1; const ERROR = 2; // Prevent importing lodash, usually for browser bundle size reasons const LodashImportPatterns = ["lodash", "lodash.**", "lodash/**"]; module.exports = { root: true, env: { browser: true, commonjs: true, node: true, }, parser: "@typescript-eslint/parser", parserOptions: {}, globals: { JSX: true, }, extends: [ "eslint:recommended", "plugin:react-hooks/recommended", "airbnb", "plugin:@typescript-eslint/recommended", // 'plugin:@typescript-eslint/recommended-requiring-type-checking', // 'plugin:@typescript-eslint/strict', "plugin:regexp/recommended", "prettier", "plugin:@docusaurus/all", ], settings: { "import/resolver": { node: { extensions: [".js", ".jsx", ".ts", ".tsx"], }, }, }, reportUnusedDisableDirectives: true, plugins: ["react-hooks", "@typescript-eslint", "regexp", "@docusaurus"], rules: { "react/jsx-uses-react": OFF, // JSX runtime: automatic "react/react-in-jsx-scope": OFF, // JSX runtime: automatic "array-callback-return": WARNING, camelcase: WARNING, "class-methods-use-this": OFF, // It's a way of allowing private variables. curly: [WARNING, "all"], "global-require": WARNING, "lines-between-class-members": OFF, "max-classes-per-file": OFF, "max-len": [ WARNING, { code: Infinity, // Code width is already enforced by Prettier tabWidth: 2, comments: 80, ignoreUrls: true, ignorePattern: "(eslint-disable|@)", }, ], "arrow-body-style": OFF, "no-await-in-loop": OFF, "no-case-declarations": WARNING, "no-console": OFF, "no-constant-binary-expression": ERROR, "no-continue": OFF, "no-control-regex": WARNING, "no-else-return": OFF, "no-empty": [WARNING, { allowEmptyCatch: true }], "no-lonely-if": WARNING, "no-nested-ternary": WARNING, "no-param-reassign": [WARNING, { props: false }], "no-prototype-builtins": WARNING, "no-restricted-exports": OFF, "no-restricted-properties": [ ERROR, .../** @type {[string, string][]} */ ([ // TODO: TS doesn't make Boolean a narrowing function yet, // so filter(Boolean) is problematic type-wise // ['compact', 'Array#filter(Boolean)'], ["concat", "Array#concat"], ["drop", "Array#slice(n)"], ["dropRight", "Array#slice(0, -n)"], ["fill", "Array#fill"], ["filter", "Array#filter"], ["find", "Array#find"], ["findIndex", "Array#findIndex"], ["first", "foo[0]"], ["flatten", "Array#flat"], ["flattenDeep", "Array#flat(Infinity)"], ["flatMap", "Array#flatMap"], ["fromPairs", "Object.fromEntries"], ["head", "foo[0]"], ["indexOf", "Array#indexOf"], ["initial", "Array#slice(0, -1)"], ["join", "Array#join"], // Unfortunately there's no great alternative to _.last yet // Candidates: foo.slice(-1)[0]; foo[foo.length - 1] // Array#at is ES2022; could replace _.nth as well // ['last'], ["map", "Array#map"], ["reduce", "Array#reduce"], ["reverse", "Array#reverse"], ["slice", "Array#slice"], ["take", "Array#slice(0, n)"], ["takeRight", "Array#slice(-n)"], ["tail", "Array#slice(1)"], ]).map(([property, alternative]) => ({ object: "_", property, message: `Use ${alternative} instead.`, })), ...[ "readdirSync", "readFileSync", "statSync", "lstatSync", "existsSync", "pathExistsSync", "realpathSync", "mkdirSync", "mkdirpSync", "mkdirsSync", "writeFileSync", "writeJsonSync", "outputFileSync", "outputJsonSync", "moveSync", "copySync", "copyFileSync", "ensureFileSync", "ensureDirSync", "ensureLinkSync", "ensureSymlinkSync", "unlinkSync", "removeSync", "emptyDirSync", ].map((property) => ({ object: "fs", property, message: "Do not use sync fs methods.", })), ], "no-restricted-syntax": [ WARNING, // Copied from airbnb, removed for...of statement, added export all { selector: "ForInStatement", message: "for..in loops iterate over the entire prototype chain, which is virtually never what you want. Use Object.{keys,values,entries}, and iterate over the resulting array.", }, { selector: "LabeledStatement", message: "Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand.", }, { selector: "WithStatement", message: "`with` is disallowed in strict mode because it makes code impossible to predict and optimize.", }, { selector: "ExportAllDeclaration", message: "Export all does't work well if imported in ESM due to how they are transpiled, and they can also lead to unexpected exposure of internal methods.", }, // TODO make an internal plugin to ensure this // { // selector: // @ 'ExportDefaultDeclaration > Identifier, ExportNamedDeclaration[source=null] > ExportSpecifier', // message: 'Export in one statement' // }, ...["path", "fs-extra", "webpack", "lodash"].map((m) => ({ selector: `ImportDeclaration[importKind=value]:has(Literal[value=${m}]) > ImportSpecifier[importKind=value]`, message: "Default-import this, both for readability and interoperability with ESM", })), ], "no-template-curly-in-string": WARNING, "no-unused-expressions": [ WARNING, { allowTaggedTemplates: true, allowShortCircuit: true }, ], "no-useless-escape": WARNING, "no-void": [ERROR, { allowAsStatement: true }], "prefer-destructuring": WARNING, "prefer-named-capture-group": WARNING, "prefer-template": WARNING, yoda: WARNING, "import/extensions": OFF, // This rule doesn't yet support resolving .js imports when the actual file // is .ts. Plus it's not all that useful when our code is fully TS-covered. "import/no-unresolved": [ OFF, { // Ignore certain webpack aliases because they can't be resolved ignore: [ "^@theme", "^@docusaurus", "^@generated", "^@site", "^@testing-utils", ], }, ], "import/order": [ WARNING, { groups: [ "builtin", "external", "internal", ["parent", "sibling", "index"], "type", ], "newlines-between": "always", pathGroups: [ // always put css import to the last, ref: // https://github.com/import-js/eslint-plugin-import/issues/1239 { pattern: "*.+(css|sass|less|scss|pcss|styl)", group: "unknown", patternOptions: { matchBase: true }, position: "after", }, { pattern: "react", group: "builtin", position: "before" }, { pattern: "react-dom", group: "builtin", position: "before" }, { pattern: "react-dom/**", group: "builtin", position: "before" }, { pattern: "stream", group: "builtin", position: "before" }, { pattern: "fs-extra", group: "builtin" }, { pattern: "lodash", group: "external", position: "before" }, { pattern: "clsx", group: "external", position: "before" }, // 'Bit weird to not use the `import/internal-regex` option, but this // way, we can make `import type { Props } from "@theme/*"` appear // before `import styles from "styles.module.css"`, which is what we // always did. This should be removable once we stop using ambient // module declarations for theme aliases. { pattern: "@theme/**", group: "internal" }, { pattern: "@site/**", group: "internal" }, { pattern: "@theme-init/**", group: "internal" }, { pattern: "@theme-original/**", group: "internal" }, { pattern: "@/components/**", group: "internal" }, { pattern: "@/libs/**", group: "internal" }, { pattern: "@/types/**", group: "type" }, ], pathGroupsExcludedImportTypes: [], // example: let `import './nprogress.css';` after importing others // in `packages/docusaurus-theme-classic/src/nprogress.ts` // see more: https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/order.md#warnonunassignedimports-truefalse warnOnUnassignedImports: true, }, ], "import/prefer-default-export": OFF, "jsx-a11y/click-events-have-key-events": WARNING, "jsx-a11y/no-noninteractive-element-interactions": WARNING, "jsx-a11y/html-has-lang": OFF, "react-hooks/rules-of-hooks": ERROR, "react-hooks/exhaustive-deps": ERROR, // Sometimes we do need the props as a whole, e.g. when spreading "react/destructuring-assignment": OFF, "react/function-component-definition": [ WARNING, { namedComponents: "function-declaration", unnamedComponents: "arrow-function", }, ], "react/jsx-filename-extension": OFF, "react/jsx-key": [ERROR, { checkFragmentShorthand: true }], "react/jsx-no-useless-fragment": [ERROR, { allowExpressions: true }], "react/jsx-props-no-spreading": OFF, "react/no-array-index-key": OFF, // We build a static site, and nearly all components don't change. "react/no-unstable-nested-components": [WARNING, { allowAsProps: true }], "react/prefer-stateless-function": WARNING, "react/prop-types": OFF, "react/require-default-props": [ ERROR, { ignoreFunctionalComponents: true }, ], "@typescript-eslint/consistent-type-definitions": OFF, "@typescript-eslint/require-await": OFF, "@typescript-eslint/ban-ts-comment": [ ERROR, { "ts-expect-error": "allow-with-description" }, ], "@typescript-eslint/consistent-indexed-object-style": OFF, "@typescript-eslint/consistent-type-imports": [ WARNING, { disallowTypeAnnotations: false }, ], "@typescript-eslint/explicit-module-boundary-types": WARNING, "@typescript-eslint/method-signature-style": ERROR, "@typescript-eslint/no-empty-function": OFF, "@typescript-eslint/no-empty-interface": [ ERROR, { allowSingleExtends: true, }, ], "@typescript-eslint/no-inferrable-types": OFF, "@typescript-eslint/no-namespace": [WARNING, { allowDeclarations: true }], "no-use-before-define": OFF, "@typescript-eslint/no-use-before-define": [ ERROR, { functions: false, classes: false, variables: true }, ], "@typescript-eslint/no-non-null-assertion": OFF, "no-redeclare": OFF, "@typescript-eslint/no-redeclare": ERROR, "no-shadow": OFF, "@typescript-eslint/no-shadow": ERROR, "no-unused-vars": OFF, // We don't provide any escape hatches for this rule. Rest siblings and // function placeholder params are always ignored, and any other unused // locals must be justified with a disable comment. "@typescript-eslint/no-unused-vars": [ERROR, { ignoreRestSiblings: true }], "@typescript-eslint/prefer-optional-chain": ERROR, "@docusaurus/no-html-links": ERROR, "@docusaurus/prefer-docusaurus-heading": ERROR, "@docusaurus/no-untranslated-text": [ WARNING, { ignoredStrings: [ "·", "-", "—", "×", "​", // zwj: ​ "@", "WebContainers", "Twitter", "GitHub", "Dev.to", "1.x", ], }, ], }, overrides: [ { files: ["packages/*/src/theme/**/*.{js,ts,tsx}"], excludedFiles: "*.test.{js,ts,tsx}", rules: { "no-restricted-imports": [ "error", { patterns: LodashImportPatterns.concat( // Prevents relative imports between React theme components [ "../**", "./**", // Allows relative styles module import with consistent filename "!./styles.module.css", // Allows relative tailwind css import with consistent filename "!./styles.css", ] ), }, ], }, }, { files: ["packages/*/src/theme/**/*.{js,ts,tsx}"], rules: { "import/no-named-export": ERROR, }, }, { files: ["*.d.ts"], rules: { "import/no-duplicates": OFF, }, }, { files: ["*.{ts,tsx}"], rules: { "no-undef": OFF, "import/no-import-module-exports": OFF, }, }, { files: ["*.{js,mjs,cjs}"], rules: { // Make JS code directly runnable in Node. "@typescript-eslint/no-var-requires": OFF, "@typescript-eslint/explicit-module-boundary-types": OFF, }, }, { // Internal files where extraneous deps don't matter much at long as // they run files: ["website/**"], rules: { "import/no-extraneous-dependencies": OFF, }, }, ], }; ================================================ FILE: .gitattributes ================================================ website/versioned_*/** linguist-documentation ================================================ FILE: .github/FUNDING.yml ================================================ open_collective: nonebot custom: ["https://afdian.com/@nonebot"] ================================================ FILE: .github/ISSUE_TEMPLATE/adapter_publish.yml ================================================ name: 发布适配器 title: "Adapter: {name}" description: 发布适配器到 NoneBot 官方商店 labels: ["Adapter", "Publish"] body: - type: markdown attributes: value: | # 发布须知 非特殊情况下,请通过 [NoneBot 适配器商店](https://nonebot.dev/store/adapters) 的发布表单进行插件发布信息填写。 - type: input id: name attributes: label: 适配器名称 description: 适配器名称 validations: required: true - type: input id: description attributes: label: 适配器描述 description: 适配器描述 validations: required: true - type: input id: pypi attributes: label: PyPI 项目名 description: PyPI 项目名 placeholder: e.g. nonebot-adapter-xxx validations: required: true - type: input id: module attributes: label: 适配器 import 包名 description: 适配器 import 包名 placeholder: e.g. nonebot_adapter_xxx validations: required: true - type: input id: homepage attributes: label: 适配器项目仓库/主页链接 description: 适配器项目仓库/主页链接 placeholder: e.g. https://github.com/xxx/xxx validations: required: true - type: input id: tags attributes: label: 标签 description: 标签 placeholder: 'e.g. [{"label": "标签名", "color": "#ea5252"}]' value: "[]" validations: required: true ================================================ FILE: .github/ISSUE_TEMPLATE/bot_publish.yml ================================================ name: 发布机器人 title: "Bot: {name}" description: 发布机器人到 NoneBot 官方商店 labels: ["Bot", "Publish"] body: - type: markdown attributes: value: | # 发布须知 非特殊情况下,请通过 [NoneBot 机器人商店](https://nonebot.dev/store/bots) 的发布表单进行插件发布信息填写。 - type: input id: name attributes: label: 机器人名称 description: 机器人名称 validations: required: true - type: input id: description attributes: label: 机器人描述 description: 机器人描述 validations: required: true - type: input id: homepage attributes: label: 机器人项目仓库/主页链接 description: 机器人项目仓库/主页链接 placeholder: e.g. https://github.com/xxx/xxx - type: input id: tags attributes: label: 标签 description: 标签 placeholder: 'e.g. [{"label": "标签名", "color": "#ea5252"}]' value: "[]" validations: required: true ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ name: Bug 反馈 title: "Bug: 出现异常" description: 提交 Bug 反馈以帮助我们改进代码 labels: ["bug"] body: - type: dropdown id: env-os attributes: label: 操作系统 description: 选择运行 NoneBot 的系统 options: - Windows - MacOS - Linux - Other validations: required: true - type: input id: env-python-ver attributes: label: Python 版本 description: 填写运行 NoneBot 的 Python 版本 placeholder: e.g. 3.11.0 validations: required: true - type: input id: env-nb-ver attributes: label: NoneBot 版本 description: 填写 NoneBot 版本 placeholder: e.g. 2.0.0 validations: required: true - type: input id: env-adapter attributes: label: 适配器 description: 填写使用的适配器以及版本 placeholder: e.g. OneBot v11 2.2.2 validations: required: true - type: input id: env-protocol attributes: label: 协议端 description: 填写连接 NoneBot 的协议端及版本 placeholder: e.g. go-cqhttp 1.0.0 validations: required: true - type: textarea id: describe attributes: label: 描述问题 description: 清晰简洁地说明问题是什么 validations: required: true - type: textarea id: reproduction attributes: label: 复现步骤 description: 提供能复现此问题的详细操作步骤 placeholder: | 1. 首先…… 2. 然后…… 3. 发生…… validations: required: true - type: textarea id: expected attributes: label: 期望的结果 description: 清晰简洁地描述你期望发生的事情 - type: textarea id: logs attributes: label: 截图或日志 description: 提供有助于诊断问题的任何日志和截图 ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: NoneBot 论坛 url: https://discussions.nonebot.dev/ about: 前往 NoneBot 论坛提问 ================================================ FILE: .github/ISSUE_TEMPLATE/document.yml ================================================ name: 文档改进 title: "Docs: 描述" description: 文档错误及改进意见反馈 labels: ["documentation"] body: - type: textarea id: problem attributes: label: 描述问题或主题 validations: required: true - type: textarea id: improve attributes: label: 需做出的修改 validations: required: true ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.yml ================================================ name: 功能建议 title: "Feature: 功能描述" description: 提出关于项目新功能的想法 labels: ["enhancement"] body: - type: textarea id: problem attributes: label: 希望能解决的问题 description: 在使用中遇到什么问题而需要新的功能? validations: required: true - type: textarea id: feature attributes: label: 描述所需要的功能 description: 请说明需要的功能或解决方法 validations: required: true ================================================ FILE: .github/ISSUE_TEMPLATE/plugin_publish.yml ================================================ name: 发布插件 title: "Plugin: {name}" description: 发布插件到 NoneBot 官方商店 labels: ["Plugin", "Publish"] body: - type: markdown attributes: value: | # 发布须知 非特殊情况下,请通过 [NoneBot 插件商店](https://nonebot.dev/store/plugins) 的发布表单进行插件发布信息填写。 在发布前请阅读 [NoneBot 插件发布流程指导](https://nonebot.dev/docs/developer/plugin-publishing) 并确保满足其中所述条件。 - type: input id: pypi attributes: label: PyPI 项目名 description: PyPI 项目名 placeholder: e.g. nonebot-plugin-xxx validations: required: true - type: input id: module attributes: label: 插件模块名 description: 加载插件时所使用的模块名称 placeholder: e.g. nonebot_plugin_apscheduler validations: required: true - type: input id: tags attributes: label: 标签 description: 标签 placeholder: 'e.g. [{"label": "标签名", "color": "#ea5252"}]' value: "[]" validations: required: true - type: textarea id: config attributes: label: 插件配置项 description: 插件配置项 render: dotenv placeholder: | # e.g. # KEY=VALUE # KEY2=VALUE2 ================================================ FILE: .github/actions/build-api-doc/action.yml ================================================ name: Build API Doc description: Build API Doc runs: using: "composite" steps: - run: | uv run --no-sync bash ./scripts/build-api-docs.sh shell: bash ================================================ FILE: .github/actions/setup-node/action.yml ================================================ name: Setup Node description: Setup Node runs: using: "composite" steps: - uses: actions/setup-node@v6 with: node-version: lts/* cache: yarn - run: yarn install --frozen-lockfile shell: bash ================================================ FILE: .github/actions/setup-python/action.yml ================================================ name: Setup Python description: Setup Python inputs: python-version: description: Python version required: false default: "3.12" env-group: description: Environment group required: false default: "pydantic-v2" runs: using: "composite" steps: - uses: astral-sh/setup-uv@v7 with: python-version: ${{ inputs.python-version }} cache-suffix: ${{ inputs.env-group }} - run: | uv sync --all-extras --locked --group ${{ inputs.env-group }} shell: bash ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: github-actions directory: "/" schedule: interval: daily groups: actions: patterns: - "*" - package-ecosystem: github-actions directory: "/.github/actions/build-api-doc" schedule: interval: daily groups: actions: patterns: - "*" - package-ecosystem: github-actions directory: "/.github/actions/setup-node" schedule: interval: daily groups: actions: patterns: - "*" - package-ecosystem: github-actions directory: "/.github/actions/setup-python" schedule: interval: daily groups: actions: patterns: - "*" - package-ecosystem: devcontainers directory: "/" schedule: interval: daily groups: devcontainers: patterns: - "*" ================================================ FILE: .github/release-drafter.yml ================================================ template: $CHANGES category-template: "### $TITLE" name-template: "Release v$RESOLVED_VERSION 🌈" tag-template: "v$RESOLVED_VERSION" change-template: "- $TITLE [@$AUTHOR](https://github.com/$AUTHOR) ([#$NUMBER]($URL))" change-title-escapes: '\<&' exclude-labels: - "dependencies" - "skip-changelog" categories: - title: "💥 破坏性变更" labels: - "Breaking" - title: "🚀 新功能" labels: - "feature" - "enhancement" - title: "🐛 Bug 修复" labels: - "fix" - "bugfix" - "bug" - title: "📝 文档" labels: - "documentation" - title: "💫 杂项" - title: "🍻 插件发布" label: "Plugin" - title: "🍻 机器人发布" label: "Bot" - title: "🍻 适配器发布" label: "Adapter" version-resolver: major: labels: - "major" minor: labels: - "minor" patch: labels: - "patch" default: patch ================================================ FILE: .github/workflows/codecov.yml ================================================ name: Code Coverage on: push: branches: - master pull_request: paths: - "envs/**" - "nonebot/**" - "packages/**" - "tests/**" - ".github/actions/setup-python/**" - ".github/workflows/codecov.yml" - "pyproject.toml" - "uv.lock" jobs: test: name: Test Coverage runs-on: ${{ matrix.os }} concurrency: group: test-coverage-${{ github.ref }}-${{ matrix.os }}-${{ matrix.python-version }}-${{ matrix.env }} cancel-in-progress: true strategy: fail-fast: false matrix: python-version: ["3.10", "3.11", "3.12", "3.13"] os: [ubuntu-latest, windows-latest, macos-latest] env: [pydantic-v1, pydantic-v2] env: OS: ${{ matrix.os }} PYTHON_VERSION: ${{ matrix.python-version }} PYDANTIC_VERSION: ${{ matrix.env }} steps: - uses: actions/checkout@v6 - name: Setup Python environment uses: ./.github/actions/setup-python with: python-version: ${{ matrix.python-version }} env-group: ${{ matrix.env }} - name: Run Pytest run: | uv run --no-sync bash ./scripts/run-tests.sh - name: Upload test results uses: codecov/test-results-action@v1 with: env_vars: OS,PYTHON_VERSION,PYDANTIC_VERSION files: ./tests/junit.xml flags: unittests env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - name: Upload coverage report uses: codecov/codecov-action@v5 with: env_vars: OS,PYTHON_VERSION,PYDANTIC_VERSION files: ./tests/coverage.xml flags: unittests fail_ci_if_error: true env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} ================================================ FILE: .github/workflows/noneflow.yml ================================================ name: NoneFlow on: issues: types: [opened, reopened, edited] issue_comment: types: [created] pull_request_target: types: [closed] pull_request_review: types: [submitted] concurrency: group: ${{ github.workflow }}-${{ github.event.issue.number && format('publish/issue{0}', github.event.issue.number) || github.head_ref || github.run_id }} cancel-in-progress: ${{ startsWith(github.head_ref, 'publish/issue')}} jobs: noneflow: runs-on: ubuntu-latest name: noneflow # do not run on forked PRs, do not run on not related issues, do not run on pr comments if: | !( ( github.event.pull_request && ( github.event.pull_request.head.repo.fork || !( contains(github.event.pull_request.labels.*.name, 'Plugin') || contains(github.event.pull_request.labels.*.name, 'Adapter') || contains(github.event.pull_request.labels.*.name, 'Bot') ) ) ) || ( github.event_name == 'issue_comment' && github.event.issue.pull_request ) ) steps: - name: Generate token id: generate-token uses: tibdex/github-app-token@v2 with: app_id: ${{ secrets.APP_ID }} private_key: ${{ secrets.APP_KEY }} - name: Checkout Code uses: actions/checkout@v6 with: token: ${{ steps.generate-token.outputs.token }} - name: NoneFlow uses: docker://ghcr.io/nonebot/noneflow:latest with: config: > { "base": "master", "plugin_path": "assets/plugins.json5", "bot_path": "assets/bots.json5", "adapter_path": "assets/adapters.json5", "registry_repository": "nonebot/registry", "artifact_path": "artifact" } env: APP_ID: ${{ secrets.APP_ID }} PRIVATE_KEY: ${{ secrets.APP_KEY }} - name: Upload Artifact uses: actions/upload-artifact@v7 with: name: noneflow path: artifact/* if-no-files-found: ignore ================================================ FILE: .github/workflows/pyright.yml ================================================ name: Pyright Lint on: push: branches: - master pull_request: paths: - "envs/**" - "nonebot/**" - "packages/**" - "tests/**" - ".github/actions/setup-python/**" - ".github/workflows/pyright.yml" - "pyproject.toml" - "uv.lock" jobs: pyright: name: Pyright Lint runs-on: ubuntu-latest concurrency: group: pyright-${{ github.ref }}-${{ matrix.env }} cancel-in-progress: true strategy: matrix: env: [pydantic-v1, pydantic-v2] fail-fast: false steps: - uses: actions/checkout@v6 - name: Setup Python environment uses: ./.github/actions/setup-python with: env-group: ${{ matrix.env }} - run: | echo "$(dirname $(uv python find))" >> $GITHUB_PATH if [ "${{ matrix.env }}" = "pydantic-v1" ]; then sed -i 's/PYDANTIC_V2 = true/PYDANTIC_V2 = false/g' ./pyproject.toml fi shell: bash - name: Run Pyright uses: jakebailey/pyright-action@v3 with: pylance-version: latest-release ================================================ FILE: .github/workflows/release-drafter.yml ================================================ name: Release Drafter on: push: tags: - v* pull_request_target: branches: - master types: - closed jobs: update-release-draft: if: github.event_name == 'pull_request_target' runs-on: ubuntu-latest concurrency: group: pull-request-changelog cancel-in-progress: true steps: - name: Generate token id: generate-token uses: tibdex/github-app-token@v2 with: app_id: ${{ secrets.APP_ID }} private_key: ${{ secrets.APP_KEY }} - uses: actions/checkout@v6 with: token: ${{ steps.generate-token.outputs.token }} - name: Setup Node Environment uses: ./.github/actions/setup-node - uses: release-drafter/release-drafter@v6.0.0 id: release-drafter env: GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }} - name: Update Changelog uses: docker://ghcr.io/nonebot/auto-changelog:master with: changelog_file: website/src/changelog/changelog.md latest_changes_position: '# 更新日志\n\n' latest_changes_title: "## 最近更新" replace_regex: '(?<=## 最近更新\n)[\s\S]*?(?=\n## )' changelog_body: ${{ steps.release-drafter.outputs.body }} commit_and_push: false - name: Commit and Push run: | yarn prettier git config user.name noneflow[bot] git config user.email 129742071+noneflow[bot]@users.noreply.github.com git add . git diff-index --quiet HEAD || git commit -m ":memo: Update changelog" git push release: if: startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest permissions: id-token: write contents: write steps: - name: Generate token id: generate-token uses: tibdex/github-app-token@v2 with: app_id: ${{ secrets.APP_ID }} private_key: ${{ secrets.APP_KEY }} - uses: actions/checkout@v6 - name: Setup Python Environment uses: ./.github/actions/setup-python - name: Setup Node Environment uses: ./.github/actions/setup-node - name: Build API Doc uses: ./.github/actions/build-api-doc - name: Get Version id: version run: | echo "VERSION=$(uv version --short)" >> $GITHUB_OUTPUT echo "TAG_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT - name: Check Version if: steps.version.outputs.VERSION != steps.version.outputs.TAG_VERSION run: exit 1 - uses: release-drafter/release-drafter@v6.0.0 with: name: Release ${{ steps.version.outputs.TAG_NAME }} 🌈 tag: ${{ steps.version.outputs.TAG_NAME }} publish: true env: GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }} - name: Build Package run: | uv build uv publish - name: Publish package to GitHub run: | gh release upload --clobber ${{ steps.version.outputs.TAG_NAME }} dist/*.tar.gz dist/*.whl env: GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }} - name: Build and Publish Doc Package run: | yarn build:plugin --out-dir ../packages/nonebot-plugin-docs/nonebot_plugin_docs/dist cd packages/nonebot-plugin-docs/ uv version ${{ steps.version.outputs.VERSION }} uv build uv publish - name: Publish Doc Package to GitHub run: | cd packages/nonebot-plugin-docs/ gh release upload --clobber ${{ steps.version.outputs.TAG_NAME }} dist/*.tar.gz dist/*.whl env: GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }} ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: workflow_dispatch: jobs: build: runs-on: ubuntu-latest steps: - name: Generate token id: generate-token uses: tibdex/github-app-token@v2 with: app_id: ${{ secrets.APP_ID }} private_key: ${{ secrets.APP_KEY }} - uses: actions/checkout@v6 with: token: ${{ steps.generate-token.outputs.token }} - name: Setup Python Environment uses: ./.github/actions/setup-python - name: Setup Node Environment uses: ./.github/actions/setup-node - name: Build API Doc uses: ./.github/actions/build-api-doc - run: echo "TAG_NAME=v$(uv version --short)" >> $GITHUB_ENV - name: Archive Changelog uses: docker://ghcr.io/nonebot/auto-changelog:master with: changelog_file: website/src/changelog/changelog.md archive_regex: '(?<=## )最近更新(?=\n)' archive_title: ${{ env.TAG_NAME }} commit_and_push: false - name: Archive Files run: | yarn archive $(uv version --short) yarn prettier - name: Push Tag run: | git config user.name noneflow[bot] git config user.email 129742071+noneflow[bot]@users.noreply.github.com git add . git commit -m ":bookmark: Release $(uv version --short)" git tag ${{ env.TAG_NAME }} git push && git push --tags ================================================ FILE: .github/workflows/ruff.yml ================================================ name: Ruff Lint on: push: branches: - master pull_request: paths: - "envs/**" - "nonebot/**" - "packages/**" - "tests/**" - ".github/actions/setup-python/**" - ".github/workflows/ruff.yml" - "pyproject.toml" - "uv.lock" jobs: ruff: name: Ruff Lint runs-on: ubuntu-latest concurrency: group: ruff-${{ github.ref }} cancel-in-progress: true steps: - uses: actions/checkout@v6 - name: Run Ruff Lint uses: astral-sh/ruff-action@v3 ================================================ FILE: .github/workflows/website-deploy.yml ================================================ name: Site Deploy on: push: branches: - master jobs: publish: runs-on: ubuntu-latest concurrency: group: website-deploy-${{ github.ref }} cancel-in-progress: true steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Setup Python Environment uses: ./.github/actions/setup-python - name: Setup Node Environment uses: ./.github/actions/setup-node - name: Build API Doc uses: ./.github/actions/build-api-doc - name: Build Doc run: yarn build - name: Get Branch Name run: echo "BRANCH_NAME=$(echo ${GITHUB_REF#refs/heads/})" >> $GITHUB_ENV - name: Deploy to Netlify uses: nwtgck/actions-netlify@v3 with: publish-dir: "./website/build" production-deploy: true github-token: ${{ secrets.GITHUB_TOKEN }} deploy-message: "Deploy ${{ env.BRANCH_NAME }}@${{ github.sha }}" enable-commit-comment: false alias: ${{ env.BRANCH_NAME }} env: NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} NETLIFY_SITE_ID: ${{ secrets.SITE_ID }} ================================================ FILE: .github/workflows/website-preview-cd.yml ================================================ name: Site Deploy (Preview CD) on: workflow_run: workflows: ["Site Deploy (Preview CI)"] types: - completed jobs: preview-cd: runs-on: ubuntu-latest concurrency: group: pull-request-preview-${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }} cancel-in-progress: true if: ${{ github.event.workflow_run.conclusion == 'success' }} environment: pull request permissions: actions: read statuses: write pull-requests: write steps: - name: Set Commit Status uses: actions/github-script@v8 with: script: | github.rest.repos.createCommitStatus({ owner: context.repo.owner, repo: context.repo.repo, sha: context.payload.workflow_run.head_sha, context: 'Website Preview', description: 'Deploying...', state: 'pending', }) - name: Download Artifact uses: actions/download-artifact@v8 with: name: website-preview github-token: ${{ secrets.GITHUB_TOKEN }} run-id: ${{ github.event.workflow_run.id }} - name: Restore Context run: | PR_NUMBER=$(cat ./pr-number) if ! [[ "${PR_NUMBER}" =~ ^[0-9]+$ ]]; then echo "Invalid PR number: ${PR_NUMBER}" exit 1 fi echo "PR_NUMBER=${PR_NUMBER}" >> "${GITHUB_ENV}" - name: Set Deploy Name run: | echo "DEPLOY_NAME=deploy-preview-${PR_NUMBER}" >> "${GITHUB_ENV}" - name: Deploy to Netlify id: deploy uses: nwtgck/actions-netlify@v3 with: publish-dir: ./website/build production-deploy: false deploy-message: "Deploy ${{ env.DEPLOY_NAME }}@${{ github.event.workflow_run.head_sha }}" alias: ${{ env.DEPLOY_NAME }} env: NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} NETLIFY_SITE_ID: ${{ secrets.SITE_ID }} # action netlify has no pull request context, so we need to comment by ourselves - name: Comment on Pull Request uses: marocchino/sticky-pull-request-comment@v3 with: header: website number: ${{ env.PR_NUMBER }} message: | :rocket: Deployed to ${{ steps.deploy.outputs.deploy-url }} - name: Set Commit Status uses: actions/github-script@v8 if: always() with: script: | if (`${{ job.status }}` === 'success') { github.rest.repos.createCommitStatus({ owner: context.repo.owner, repo: context.repo.repo, sha: context.payload.workflow_run.head_sha, context: 'Website Preview', description: `Deployed to ${{ steps.deploy.outputs.deploy-url }}`, state: 'success', target_url: `${{ steps.deploy.outputs.deploy-url }}`, }) } else { github.rest.repos.createCommitStatus({ owner: context.repo.owner, repo: context.repo.repo, sha: context.payload.workflow_run.head_sha, context: 'Website Preview', description: `Deploy ${{ job.status }}`, state: 'failure', }) } ================================================ FILE: .github/workflows/website-preview-ci.yml ================================================ name: Site Deploy (Preview CI) on: pull_request: jobs: preview-ci: runs-on: ubuntu-latest concurrency: group: pull-request-preview-${{ github.event.number }} cancel-in-progress: true steps: - uses: actions/checkout@v6 with: ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 - name: Setup Python Environment uses: ./.github/actions/setup-python - name: Setup Node Environment uses: ./.github/actions/setup-node - name: Build API Doc uses: ./.github/actions/build-api-doc - name: Build Doc run: yarn build - name: Export Context run: | echo "${{ github.event.pull_request.number }}" > ./pr-number - name: Upload Artifact uses: actions/upload-artifact@v7 with: name: website-preview path: | ./website/build ./pr-number retention-days: 1 ================================================ FILE: .gitignore ================================================ # ----- Project ----- .idea .vscode dev docs_build/_build !tests/.env .docusaurus website/docs/api/**/*.md website/src/pages/changelog/**/* # Created by https://www.toptal.com/developers/gitignore/api/python,node,visualstudiocode,jetbrains,macos,windows,linux # Edit at https://www.toptal.com/developers/gitignore?templates=python,node,visualstudiocode,jetbrains,macos,windows,linux ### JetBrains ### # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 # User-specific stuff .idea/**/workspace.xml .idea/**/tasks.xml .idea/**/usage.statistics.xml .idea/**/dictionaries .idea/**/shelf # AWS User-specific .idea/**/aws.xml # Generated files .idea/**/contentModel.xml # Sensitive or high-churn files .idea/**/dataSources/ .idea/**/dataSources.ids .idea/**/dataSources.local.xml .idea/**/sqlDataSources.xml .idea/**/dynamic.xml .idea/**/uiDesigner.xml .idea/**/dbnavigator.xml # Gradle .idea/**/gradle.xml .idea/**/libraries # Gradle and Maven with auto-import # When using Gradle or Maven with auto-import, you should exclude module files, # since they will be recreated, and may cause churn. Uncomment if using # auto-import. # .idea/artifacts # .idea/compiler.xml # .idea/jarRepositories.xml # .idea/modules.xml # .idea/*.iml # .idea/modules # *.iml # *.ipr # CMake cmake-build-*/ # Mongo Explorer plugin .idea/**/mongoSettings.xml # File-based project format *.iws # IntelliJ out/ # mpeltonen/sbt-idea plugin .idea_modules/ # JIRA plugin atlassian-ide-plugin.xml # Cursive Clojure plugin .idea/replstate.xml # Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties fabric.properties # Editor-based Rest Client .idea/httpRequests # Android studio 3.1+ serialized cache file .idea/caches/build_file_checksums.ser ### JetBrains Patch ### # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 # *.iml # modules.xml # .idea/misc.xml # *.ipr # Sonarlint plugin # https://plugins.jetbrains.com/plugin/7973-sonarlint .idea/**/sonarlint/ # SonarQube Plugin # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin .idea/**/sonarIssues.xml # Markdown Navigator plugin # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced .idea/**/markdown-navigator.xml .idea/**/markdown-navigator-enh.xml .idea/**/markdown-navigator/ # Cache file creation bug # See https://youtrack.jetbrains.com/issue/JBR-2257 .idea/$CACHE_FILE$ # CodeStream plugin # https://plugins.jetbrains.com/plugin/12206-codestream .idea/codestream.xml ### Linux ### *~ # temporary files which can be created if a process still has a handle open of a deleted file .fuse_hidden* # KDE directory preferences .directory # Linux trash folder which might appear on any partition or disk .Trash-* # .nfs files are created when an open file is removed but is still being accessed .nfs* ### macOS ### # General .DS_Store .AppleDouble .LSOverride # Icon must end with two \r # Icon # Thumbnails ._* # Files that might appear in the root of a volume .DocumentRevisions-V100 .fseventsd .Spotlight-V100 .TemporaryItems .Trashes .VolumeIcon.icns .com.apple.timemachine.donotpresent # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk ### Node ### # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* .pnpm-debug.log* # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage *.lcov # nyc test coverage .nyc_output # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # Snowpack dependency directory (https://snowpack.dev/) web_modules/ # TypeScript cache *.tsbuildinfo # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Microbundle cache .rpt2_cache/ .rts2_cache_cjs/ .rts2_cache_es/ .rts2_cache_umd/ # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variables file .env .env.test .env.production # parcel-bundler cache (https://parceljs.org/) .cache .parcel-cache # Next.js build output .next out # Nuxt.js build / generate output .nuxt dist # Gatsby files .cache/ # Comment in the public line in if your project uses Gatsby and not Next.js # https://nextjs.org/blog/next-9-1#public-directory-support # public # vuepress build output .vuepress/dist # Serverless directories .serverless/ # FuseBox cache .fusebox/ # DynamoDB Local files .dynamodb/ # TernJS port file .tern-port # Stores VSCode versions used for testing VSCode extensions .vscode-test # yarn v2 .yarn/cache .yarn/unplugged .yarn/build-state.yml .yarn/install-state.gz .pnp.* ### Node Patch ### # Serverless Webpack directories .webpack/ # Optional stylelint cache .stylelintcache # SvelteKit build / generate output .svelte-kit ### Python ### # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ ### VisualStudioCode ### .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json *.code-workspace # Local History for Visual Studio Code .history/ ### VisualStudioCode Patch ### # Ignore all local history of files .history .ionide # Support for Project snippet scope !.vscode/*.code-snippets ### Windows ### # Windows thumbnail cache files Thumbs.db Thumbs.db:encryptable ehthumbs.db ehthumbs_vista.db # Dump file *.stackdump # Folder config file [Dd]esktop.ini # Recycle Bin used on file shares $RECYCLE.BIN/ # Windows Installer files *.cab *.msi *.msix *.msm *.msp # Windows shortcuts *.lnk # End of https://www.toptal.com/developers/gitignore/api/python,node,visualstudiocode,jetbrains,macos,windows,linux ================================================ FILE: .markdownlint.yaml ================================================ MD013: false MD024: # 重复标题 siblings_only: true MD033: false # 允许 html ================================================ FILE: .pre-commit-config.yaml ================================================ default_install_hook_types: [pre-commit, prepare-commit-msg] ci: autofix_commit_msg: ":rotating_light: auto fix by pre-commit hooks" autofix_prs: true autoupdate_branch: master autoupdate_schedule: monthly 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 hooks: - id: ruff-check args: [--fix] stages: [pre-commit] - id: ruff-format stages: [pre-commit] - repo: https://github.com/nonebot/nonemoji rev: v0.1.4 hooks: - id: nonemoji stages: [prepare-commit-msg] ================================================ FILE: .prettierignore ================================================ .github/**/*.md website/docs/tutorial/application.mdx website/versioned_docs/*/tutorial/application.mdx ================================================ FILE: .prettierrc ================================================ { "tabWidth": 2, "useTabs": false, "endOfLine": "lf", "arrowParens": "always", "singleQuote": false, "trailingComma": "es5", "semi": true, "overrides": [ { "files": [ "**/devcontainer.json", "**/tsconfig.json", "**/tsconfig.*.json" ], "options": { "parser": "json" } } ] } ================================================ FILE: .stylelintrc.js ================================================ module.exports = { extends: ["stylelint-config-standard", "stylelint-prettier/recommended"], overrides: [ { files: ["*.css"], rules: { "function-no-unknown": [true, { ignoreFunctions: ["theme"] }], "selector-class-pattern": [ "^([a-z][a-z0-9]*)(-[a-z0-9]+)*$", { resolveNestedSelectors: true, message: (selector) => `Expected class selector "${selector}" to be kebab-case`, }, ], }, }, { files: ["*.module.css"], rules: { "selector-class-pattern": [ "^[a-z][a-zA-Z0-9]+$", { message: (selector) => `Expected class selector "${selector}" to be lowerCamelCase`, }, ], }, }, ], }; ================================================ FILE: .yarnrc ================================================ registry "https://registry.npmjs.org/" ================================================ FILE: CHANGELOG.md ================================================ # Changelog See [changelog.md](./website/src/changelog/changelog.md) or ================================================ FILE: CITATION.cff ================================================ # This CITATION.cff file was generated with cffinit. # Visit https://bit.ly/cffinit to generate yours today! cff-version: 1.2.0 title: NoneBot message: >- If you use this software, please cite it using the metadata from this file. type: software authors: - given-names: Yongyu family-names: Yan email: yyy@nonebot.dev - name: NoneBot Team email: contact@nonebot.dev website: 'https://github.com/nonebot' repository-code: 'https://github.com/nonebot/nonebot2' url: 'https://nonebot.dev/' abstract: >- NoneBot, an asynchronous multi-platform chatbot framework written in Python keywords: - nonebot - chatbot - pydantic license: MIT ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # NoneBot2 贡献者公约 ## 我们的承诺 身为项目成员、贡献者、负责人,我们保证参与此社区的每个人都不受骚扰,不论其年龄、体型、身体条件、民族、性征、性别认同与表现、经验水平、教育程度、社会地位、国籍、相貌、种族、宗教信仰及性取向如何。 我们承诺致力于建设开放、友善、多元、包容、健康的社区环境。 ## 我们的准则 有助于促进本社区积极环境的行为包括但不限于: - 与人为善、推己及人 - 尊重不同的主张、观点和经历 - 积极提出、耐心接受有益批评 - 面对过失,承担责任、认真道歉、从中学习 - 关注社区共同诉求,而非一己私利 不当行为包括但不限于: - 发布与性有关的言论或图像,以及任何形式的献殷勤或勾引 - 挑衅行为、侮辱或贬损的言论、人身及政治攻击 - 公开或私下骚扰 - 未获明确授权擅自发布他人的资料,如地址、电子邮箱等 - 其他有理由认定为违反职业操守的不当行为 ## 落实之义务 社区负责人有责任诠释什么是“妥当行为”,并据此准则,妥善公正地认定与处置不当、威胁、冒犯及有害的行为。 社区负责人有权利和义务删除、编辑、拒绝违背本公约的评论(comment)、提交(commit)、代码、维基(wiki)编辑、问题(issue)等贡献。如有必要,需告知采取措施的理由。 ## 适用范围 此行为标准适用于本社区全部场合,以及在其他场合代表本社区的个人。 代表本社区的情形包括但不限于:使用官方电子邮件与社交平台、作为指定代表参与在线或线下活动。 ## 贯彻落实 如遇滥用、骚扰等不当行为,请通过 contact@nonebot.dev 向我们举报。我们将迅速审议并调查全部投诉。 社区全体负责人有义务保密举报者信息。 ## 指导方针 社区负责人将依据下列方案判断并处置违纪行为: ### 一、督促 **社区影响**:用语不当、举止不符合道德或不受社区欢迎。 **处理意见**:由社区负责人予以非公开的书面警告,阐明违纪事由、解释举止如何不妥。或要求公开道歉。 ### 二、警告 **社区影响**:一起或多起事件中的违纪行为。 **处理意见**:警告继续违纪的后果、违纪者在特定时间内禁止与当事人往来、不得擅自与社区执法者往来,禁令涵盖社区内外、社交网络在内的一切联络。如有违反,可致封禁乃至开除。 ### 三、封禁 **社区影响**:严重违纪行为,包括屡教不改。 **处理意见**:违纪者在特定时间内禁止与社区的任何往来或公开联络,禁止任何与当事人公开或私下往来,不得擅自与社区管理者往来。如有违反,可导致开除。 ### 四、开除 **社区影响**:典型违纪行为,例如屡教不改、骚扰某个人、敌对或贬低某个群体。 **处理意见**:无限期禁止违纪者与项目社区的一切公开往来。 ## 来源 本行为标准改编自[参与者公约][homepage]2.0 版,可在此查阅:[https://www.contributor-covenant.org/zh-cn/version/2/0/code_of_conduct.html][v2.0] 指导方针借鉴自[Mozilla 纪检分级][mozilla coc]。 此行为标准常见问题请洽:[https://www.contributor-covenant.org/faq][faq]。 另有诸译本:[https://www.contributor-covenant.org/translations][translations]。 [homepage]: https://www.contributor-covenant.org [v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html [mozilla coc]: https://github.com/mozilla/diversity [faq]: https://www.contributor-covenant.org/faq [translations]: https://www.contributor-covenant.org/translations ================================================ FILE: CONTRIBUTING.md ================================================ # NoneBot2 贡献指南 首先,感谢你愿意为 NoneBot2 贡献自己的一份力量! 本指南旨在引导你更规范地向 NoneBot2 提交贡献,请务必认真阅读。 ## 提交 Issue 在提交 Issue 前,我们建议你先查看 [FAQ](https://github.com/nonebot/discussions/discussions/13) 与 [已有的 Issues](https://github.com/nonebot/nonebot2/issues),以防重复提交。 ### 报告问题、故障与漏洞 如果你在使用过程中发现问题并确信是由 NoneBot2 引起的,欢迎提交 Issue。 ### 建议功能 为了让开发者更好地理解你的意图,请认真描述你所需要的特性,可能的话可以提出你认为可行的解决方案。 ## Pull Request NoneBot 使用 [uv](https://docs.astral.sh/uv/) 管理项目依赖,由于 pre-commit 也经其管理,所以在此一并说明。 下面的命令能在已安装 uv 和 yarn 的情况下帮你快速配置开发环境。 ```bash # 安装 python 依赖 uv sync --all-extras # 安装 pre-commit git hook uv run pre-commit install ``` ### 使用 GitHub Codespaces(Dev Container) [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new?hide_repo_select=true&ref=master&repo=289605524) ### Commit 规范 请确保你的每一个 commit 都能清晰地描述其意图,一个 commit 尽量只有一个意图。 NoneBot 的 commit message 格式遵循 [gitmoji](https://gitmoji.dev/) 规范,在创建 commit 时请牢记这一点。 或者使用 [nonemoji](https://github.com/nonebot/nonemoji) 代替 git 进行 commit,nonemoji 已默认作为项目开发依赖安装。 ```bash nonemoji commit [-e EMOJI] [-m MESSAGE] [-- ...] ``` ### 工作流概述 `master` 分支为 NoneBot 的开发分支,在任何情况下都请不要直接修改 `master` 分支,而是创建一个目标分支为 `nonebot:master` 的 Pull Request 来提交修改。Pull Request 标题请尽量更改成中文,以便自动生成更新日志。 如果你不是 NoneBot 团队的成员,可在 fork 本仓库后,向本仓库的 `master` 分支发起 Pull Request,注意遵循先前提到的 commit message 规范创建 commit。我们将在 code review 通过后通过 squash merge 方式将您的贡献合并到主分支。 ### 撰写文档 NoneBot2 的文档使用 [docusaurus](https://docusaurus.io/),它有一些 [Markdown 特性](https://docusaurus.io/zh-CN/docs/markdown-features) 可能会帮助到你。 如果你需要在本地预览修改后的文档,可以使用 yarn 安装文档依赖后启动 dev server,如下所示: ```bash yarn install yarn start ``` NoneBot2 文档并没有具体的行文风格规范,但我们建议你尽量写得简单易懂。 以下是比较重要的编写与排版规范。目前 NoneBot2 文档中仍有部分文档不完全遵守此规范,如果在阅读时发现欢迎提交 PR。 1. 中文与英文、数字、半角符号之间需要有空格。例:`NoneBot2 是一个可扩展的 Python 异步机器人框架。` 2. 若非英文整句,使用全角标点符号。例:`现在你可以看到机器人回复你:“Hello, World !”。` 3. 直引号`「」`和弯引号`“”`都可接受,但同一份文件里应使用同种引号。 4. **不要使用斜体**,你不需要一种与粗体不同的强调。除此之外,你也可以考虑使用 docusaurus 提供的[告示](https://docusaurus.io/zh-CN/docs/markdown-features/admonitions)功能。 5. 文档中应以“我们”指代机器人开发者,以“机器人用户”指代机器人的使用者。 以上由[社区创始人 richardchien 的中文排版规范](https://stdrc.cc/style-guides/chinese)补充修改得到。 如果你需要编辑器检查 Markdown 规范,可以在 VSCode 中安装 markdownlint 扩展。 ### 参与开发 NoneBot2 的代码风格遵循 [PEP 8](https://www.python.org/dev/peps/pep-0008/) 与 [PEP 484](https://www.python.org/dev/peps/pep-0484/) 规范,请确保你的代码风格和项目已有的代码保持一致,变量命名清晰,有适当的注释与测试代码。 ## 为社区做贡献 你可以在 NoneBot 商店上架自己的适配器、插件、机器人,具体步骤可参考 [发布插件](https://nonebot.dev/docs/developer/plugin-publishing) 一节。 我们仅对插件的兼容性进行简单测试,并会在下一个版本发布前对与该版本不兼容的插件作出处理。 虽然对插件的内容没有严格限制,但我们还是建议在上架插件之前先查看商店有无功能一致的插件。如果你想要上架商店的插件功能与现有插件不完全重合,请在插件说明中补充其与现有插件的区别。 同时,如果你参考或基于他人发行的代码进行开发,请注意遵守各代码所使用的开源许可协议。 ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2020 NoneBot Team Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================

nonebot

# NoneBot _✨ 跨平台 Python 异步机器人框架 ✨_

license pypi python black pyright ruff
codecov site pre-commit pyright ruff
onebot onebot QQ telegram feishu github
QQ Chat Group QQ Channel Telegram Channel Discord Server

文档 · 快速上手 · 文档打不开?

setup

## 简介 NoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架,它基于 Python 的类型注解和异步特性,能够为你的需求实现提供便捷灵活的支持。 ## 特色 - 异步优先:基于 Python 的异步特性,即使是~~非常~~大量的消息,也能吞吐自如 - 易于开发:配合 NB-CLI 脚手架,代码编写上手简单,没有过多的冗余代码,可以让开发者专注于业务逻辑 - 生而可靠:100% 类型注解覆盖,配合编辑器的类型推导功能,能将绝大多数的 Bug 杜绝在编辑器中 ([编辑器支持](https://nonebot.dev/docs/editor-support)) - 社区丰富:社区用户众多,直接和间接用户超过十万人,每天都有大量的活跃用户 ([社区资源](#社区资源)) - 海纳百川:一个框架,支持多个聊天软件平台,可自定义通信协议 | 协议名称 | 状态 | 注释 | | :-------------------------------------------------------------------------------------------------------------------: | :--: | :-----------------------------------------------------------------------: | | OneBot([仓库](https://github.com/nonebot/adapter-onebot),[协议](https://onebot.dev/)) | ✅ | 支持 QQ、TG、微信公众号、KOOK 等[平台](https://onebot.dev/ecosystem.html) | | Telegram([仓库](https://github.com/nonebot/adapter-telegram),[协议](https://core.telegram.org/bots/api)) | ✅ | | | 飞书([仓库](https://github.com/nonebot/adapter-feishu),[协议](https://open.feishu.cn/document/home/index)) | ✅ | | | GitHub([仓库](https://github.com/nonebot/adapter-github),[协议](https://docs.github.com/en/apps)) | ✅ | GitHub APP & OAuth APP | | QQ([仓库](https://github.com/nonebot/adapter-qq),[协议](https://bot.q.qq.com/wiki/)) | ✅ | QQ 官方接口调整较多 | | Console([仓库](https://github.com/nonebot/adapter-console)) | ✅ | 控制台交互 | | Red([仓库](https://github.com/nonebot/adapter-red),[协议](https://chrononeko.github.io/QQNTRedProtocol/)) | ✅ | QQNT 协议 | | Satori([仓库](https://github.com/nonebot/adapter-satori),[协议](https://satori.js.org/zh-CN)) | ✅ | 支持 Onebot、TG、飞书、微信公众号、Koishi 等 | | Discord([仓库](https://github.com/nonebot/adapter-discord),[协议](https://discord.com/developers/docs/intro)) | ✅ | Discord Bot 协议 | | DoDo([仓库](https://github.com/nonebot/adapter-dodo),[协议](https://open.imdodo.com/)) | ✅ | DoDo Bot 协议 | | Kritor([仓库](https://github.com/nonebot/adapter-kritor),[协议](https://github.com/KarinJS/kritor)) | ✅ | Kritor (OnebotX) 协议,QQNT 机器人接口标准 | | Mirai([仓库](https://github.com/nonebot/adapter-mirai),[协议](https://docs.mirai.mamoe.net/mirai-api-http/)) | ✅ | QQ 协议 | | Milky([仓库](https://github.com/nonebot/adapter-milky),[协议](https://milky.ntqqrev.org/)) | ✅ | QQNT 机器人应用接口标准 | | 钉钉([仓库](https://github.com/nonebot/adapter-ding),[协议](https://open.dingtalk.com/document/)) | 🤗 | 寻找 Maintainer(暂不可用) | | 开黑啦([仓库](https://github.com/Tian-que/nonebot-adapter-kaiheila),[协议](https://developer.kookapp.cn/)) | ↗️ | 由社区贡献 | | Ntchat([仓库](https://github.com/JustUndertaker/adapter-ntchat)) | ↗️ | 微信协议,由社区贡献 | | MineCraft([仓库](https://github.com/17TheWord/nonebot-adapter-minecraft)) | ↗️ | 由社区贡献 | | Walle-Q([仓库](https://github.com/onebot-walle/nonebot_adapter_walleq)) | ↗️ | QQ 协议,由社区贡献 | | Villa([仓库](https://github.com/CMHopeSunshine/nonebot-adapter-villa)) | ❌ | 米游社大别野 Bot 协议,官方已下线 | | Rocket.Chat([仓库](https://github.com/IUnlimit/nonebot-adapter-rocketchat),[协议](https://developer.rocket.chat/)) | ↗️ | Rocket.Chat Bot 协议,由社区贡献 | | Tailchat([仓库](https://github.com/eya46/nonebot-adapter-tailchat),[协议](https://tailchat.msgbyte.com/)) | ↗️ | Tailchat 开放平台 Bot 协议,由社区贡献 | | Mail([仓库](https://github.com/mobyw/nonebot-adapter-mail)) | ↗️ | 邮件收发协议,由社区贡献 | | 黑盒语音([仓库](https://github.com/lclbm/adapter-heybox),[协议](https://github.com/QingFengOpen/HeychatDoc)) | ↗️ | 黑盒语音机器人协议,由社区贡献 | | 微信公众平台([仓库](https://github.com/YangRucheng/nonebot-adapter-wxmp),[协议](https://developers.weixin.qq.com/doc/))| ↗️ | 微信公众平台协议,由社区贡献 | | Gewechat([仓库](https://github.com/Shine-Light/nonebot-adapter-gewechat),[协议](https://github.com/Devo919/Gewechat))| ❌ | Gewechat 微信协议,Gewechat不再维护及可用 | | EFChat([仓库](https://github.com/molanp/nonebot_adapter_efchat),[协议](https://irinu-live.melon.fish/efc-help/)) | ↗️ | 恒五聊平台协议,由社区贡献 | | VoceChat ([仓库](https://github.com/5656565566/nonebot-adapter-vocechat),[协议](https://doc.voce.chat/zh-cn/bot/bot-and-webhook)) | ↗️ | VoceChat 平台协议,由社区贡献 | | B站直播间([仓库](https://github.com/MingxuanGame/nonebot-adapter-bilibili-live),[Web API 协议](https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/live),[开放平台协议](https://open-live.bilibili.com/document)) | ↗️ | B站直播间(Web API/开放平台)协议,由社区贡献 | - 坚实后盾:支持多种 web 框架,可自定义替换、组合 | 驱动框架 | 类型 | | :-----------------------------------------------------------------: | :----: | | [FastAPI](https://fastapi.tiangolo.com/) | 服务端 | | [Quart](https://quart.palletsprojects.com/en/latest/)(异步 Flask) | 服务端 | | [aiohttp](https://docs.aiohttp.org/en/stable/) | 客户端 | | [httpx](https://www.python-httpx.org/) | 客户端 | | [websockets](https://websockets.readthedocs.io/en/stable/) | 客户端 | 更多:[概览](https://nonebot.dev/docs/) ## 什么不是 NoneBot2 NoneBot2 不是某个平台或者协议的具体实现,它只负责和已有协议适配器通信,并处理接收到的事件。所以,“NoneBot 有 blabla 平台的 blabla 功能吗?”这种问题是与 NoneBot2 无关的。请在相应平台的功能文档中确认,或与相应平台的协议适配开发者联系。 NoneBot2 不是 NoneBot1 的替代品。事实上,它们都在被积极的维护着。但是,如果你想尝试一些新功能,或者想要支持更多的平台,可以考虑使用 NoneBot2。 > ~~NoneBot2 和 NoneBot1 的区别,就像是 VisualStudio Code 和 VisualStudio 一样~~ ## 即刻开始 ~~完整~~文档可以在 [这里](https://nonebot.dev/) 查看。 懒得看文档?下面是快速安装指南: 1. 安装 [pipx](https://pypa.github.io/pipx/) ```bash python -m pip install --user pipx python -m pipx ensurepath ``` 2. 安装脚手架 ```bash pipx install nb-cli ``` 3. 使用脚手架创建项目 ```bash nb create ``` 4. 运行项目 ```bash nb run ``` ## 社区资源 ### 常见问题 - [常见问题解答(FAQ)](https://faq.nonebot.dev/) - [论坛(Discussion)](https://discussions.nonebot.dev/) ### 教程/实际项目/经验分享 - [awesome-nonebot](https://github.com/nonebot/awesome-nonebot) ### 插件 此外,NoneBot2 还有丰富的官方以及第三方现成的插件供大家使用: - [NoneBot-Plugin-Docs](https://github.com/nonebot/nonebot2/tree/master/packages/nonebot-plugin-docs):离线文档至本地项目使用 (别再说文档打不开了!) 在项目目录下执行: ```bash nb plugin install nonebot_plugin_docs ``` 或者尝试以下镜像: - [文档镜像(中国境内)](https://nb2.baka.icu) - 其他插件请查看 [商店](https://nonebot.dev/store/plugins) ## 许可证 `NoneBot` 采用 `MIT` 许可证进行开源 ```text THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` ## 贡献 请参考 [贡献指南](./CONTRIBUTING.md) ## 鸣谢 ### 赞助者 感谢以下产品对 NoneBot 项目提供的赞助:

GitHub      netlify      sentry

docker      algolia

JetBrains

感谢以下赞助者对 NoneBot 项目提供的资金支持: sponsors ### 开发者 感谢以下开发者对 NoneBot2 作出的贡献: contributors ================================================ FILE: assets/adapters.json5 ================================================ [ { "module_name": "nonebot.adapters.onebot.v11", "project_link": "nonebot-adapter-onebot", "name": "OneBot V11", "desc": "OneBot V11 协议", "author_id": 42488585, "homepage": "https://onebot.adapters.nonebot.dev/", "tags": [], "is_official": true }, { "module_name": "nonebot.adapters.ding", "project_link": "nonebot-adapter-ding", "name": "钉钉", "desc": "钉钉协议", "author_id": 1184028, "homepage": "https://github.com/nonebot/adapter-ding", "tags": [], "is_official": true }, { "module_name": "nonebot.adapters.feishu", "project_link": "nonebot-adapter-feishu", "name": "飞书", "desc": "飞书协议", "author_id": 14922941, "homepage": "https://github.com/nonebot/adapter-feishu", "tags": [], "is_official": true }, { "module_name": "nonebot.adapters.telegram", "project_link": "nonebot-adapter-telegram", "name": "Telegram", "desc": "Telegram 协议", "author_id": 50312681, "homepage": "https://github.com/nonebot/adapter-telegram", "tags": [], "is_official": true }, { "module_name": "nonebot.adapters.qq", "project_link": "nonebot-adapter-qq", "name": "QQ", "desc": "QQ 官方机器人", "author_id": 42488585, "homepage": "https://github.com/nonebot/adapter-qq", "tags": [], "is_official": true }, { "module_name": "nonebot.adapters.kaiheila", "project_link": "nonebot-adapter-kaiheila", "name": "开黑啦", "desc": "开黑啦协议适配", "author_id": 37477320, "homepage": "https://github.com/Tian-que/nonebot-adapter-kaiheila", "tags": [], "is_official": false }, { "module_name": "nonebot.adapters.mirai", "project_link": "nonebot-adapter-mirai", "name": "Mirai", "desc": "mirai-api-http v2 协议适配", "author_id": 42648639, "homepage": "https://github.com/nonebot/adapter-mirai", "tags": [], "is_official": true }, { "module_name": "nonebot.adapters.onebot.v12", "project_link": "nonebot-adapter-onebot", "name": "OneBot V12", "desc": "OneBot V12 协议", "author_id": 42488585, "homepage": "https://onebot.adapters.nonebot.dev/", "tags": [], "is_official": true }, { "module_name": "nonebot.adapters.console", "project_link": "nonebot-adapter-console", "name": "Console", "desc": "基于终端的交互式适配器", "author_id": 50488999, "homepage": "https://github.com/nonebot/adapter-console", "tags": [], "is_official": true }, { "module_name": "nonebot.adapters.github", "project_link": "nonebot-adapter-github", "name": "GitHub", "desc": "GitHub APP & OAuth APP integration", "author_id": 42488585, "homepage": "https://github.com/nonebot/adapter-github", "tags": [], "is_official": true }, { "module_name": "nonebot.adapters.ntchat", "project_link": "nonebot-adapter-ntchat", "name": "Ntchat", "desc": "pc hook的微信客户端适配", "author_id": 37363867, "homepage": "https://github.com/JustUndertaker/adapter-ntchat", "tags": [ { "label": "微信", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot.adapters.minecraft", "project_link": "nonebot-adapter-minecraft", "name": "Minecraft", "desc": "MineCraft通信适配,支持Rcon", "author_id": 54731914, "homepage": "https://github.com/17TheWord/nonebot-adapter-minecraft", "tags": [ { "label": "Minecraft", "color": "#4ef0ea" } ], "is_official": false }, { "module_name": "nonebot.adapters.bilibili", "project_link": "nonebot-adapter-bilibili", "name": "BilibiliLive", "desc": "b站直播间ws协议", "author_id": 39620657, "homepage": "https://github.com/wwweww/adapter-bilibili", "tags": [], "is_official": false }, { "module_name": "nonebot_adapter_walleq", "project_link": "nonebot-adapter-walleq", "name": "Walle-Q", "desc": "内置 QQ 协议实现", "author_id": 18395948, "homepage": "https://github.com/onebot-walle/nonebot_adapter_walleq", "tags": [ { "label": "QQ", "color": "#34a9cc" } ], "is_official": false }, { "module_name": "nonebot.adapters.villa", "project_link": "nonebot-adapter-villa", "name": "大别野", "desc": "米游社大别野官方Bot适配", "author_id": 63870437, "homepage": "https://github.com/CMHopeSunshine/nonebot-adapter-villa", "tags": [ { "label": "米哈游", "color": "#e10909" } ], "is_official": false }, { "module_name": "nonebot.adapters.red", "project_link": "nonebot-adapter-red", "name": "RedProtocol", "desc": "QQNT RedProtocol 适配", "author_id": 55650833, "homepage": "https://github.com/nonebot/adapter-red", "tags": [], "is_official": true }, { "module_name": "nonebot.adapters.discord", "project_link": "nonebot-adapter-discord", "name": "Discord", "desc": "Discord 官方 Bot 协议适配", "author_id": 63870437, "homepage": "https://github.com/nonebot/adapter-discord", "tags": [], "is_official": true }, { "module_name": "nonebot.adapters.satori", "project_link": "nonebot-adapter-satori", "name": "Satori", "desc": "Satori 协议适配器", "author_id": 42648639, "homepage": "https://github.com/nonebot/adapter-satori", "tags": [ { "label": "跨平台", "color": "#bf40bf" } ], "is_official": true }, { "module_name": "nonebot.adapters.dodo", "project_link": "nonebot-adapter-dodo", "name": "DoDo", "desc": "DoDo Bot 协议适配器", "author_id": 63870437, "homepage": "https://github.com/nonebot/adapter-dodo", "tags": [], "is_official": true }, { "module_name": "nonebot.adapters.rocketchat", "project_link": "nonebot-adapter-rocketchat", "name": "RocketChat", "desc": "RocketChat adapter for nonebot2", "author_id": 78360471, "homepage": "https://github.com/IUnlimit/nonebot-adapter-rocketchat", "tags": [], "is_official": false }, { "module_name": "nonebot.adapters.kritor", "project_link": "nonebot-adapter-kritor", "name": "Kritor", "desc": "Kritor 协议适配", "author_id": 42648639, "homepage": "https://github.com/nonebot/adapter-kritor", "tags": [ { "label": "QQNT", "color": "#35a7c9" } ], "is_official": true }, { "module_name": "nonebot_adapter_tailchat", "project_link": "nonebot-adapter-tailchat", "name": "Tailchat", "desc": "Tailchat 适配器", "author_id": 61458340, "homepage": "https://github.com/eya46/nonebot-adapter-tailchat", "tags": [], "is_official": false }, { "module_name": "nonebot.adapters.mail", "project_link": "nonebot-adapter-mail", "name": "Mail", "desc": "邮件收发协议", "author_id": 44370805, "homepage": "https://github.com/mobyw/nonebot-adapter-mail", "tags": [], "is_official": false }, { "module_name": "nonebot.adapters.heybox", "project_link": "nonebot-adapter-heybox", "name": "黑盒语音", "desc": "黑盒语音机器人适配", "author_id": 54730982, "homepage": "https://github.com/lclbm/adapter-heybox", "tags": [], "is_official": false }, { "module_name": "nonebot.adapters.wxmp", "project_link": "nonebot-adapter-wxmp", "name": "WXMP", "desc": "微信公众平台 客服适配器", "author_id": 60175467, "homepage": "https://github.com/YangRucheng/nonebot-adapter-wxmp", "tags": [ { "label": "微信公众平台", "color": "#843dbc" } ], "is_official": false }, { "module_name": "nonebot.adapters.milky", "project_link": "nonebot-adapter-milky", "name": "nonebot-adapter-milky", "desc": "Milky 协议适配器", "author_id": 42648639, "homepage": "https://github.com/nonebot/adapter-milky", "tags": [], "is_official": true }, { "module_name": "nonebot.adapters.efchat", "project_link": "nonebot-adapter-efchat", "name": "nonebot-adapter-efchat", "desc": "适用于EFChat(恒五聊)聊天室的nonebot适配器", "author_id": 104612722, "homepage": "https://github.com/molanp/nonebot_adapter_efchat", "tags": [ { "label": "EFChat", "color": "#ac6161" }, { "label": "恒五聊", "color": "#cc99ff" } ], "is_official": false }, { "module_name": "nonebot.adapters.vocechat", "project_link": "nonebot-adapter-vocechat", "name": "nonebot-adapter-vocechat", "desc": "Vocechat 协议适配器", "author_id": 56059687, "homepage": "https://github.com/5656565566/nonebot-adapter-vocechat", "tags": [], "is_official": false }, { "module_name": "nonebot.adapters.bilibili_live", "project_link": "nonebot-adapter-bilibili-live", "name": "B站直播间", "desc": "B 站直播间协议(Web API/开放平台)支持", "author_id": 68982190, "homepage": "https://github.com/MingxuanGame/nonebot-adapter-bilibili-live", "tags": [ { "label": "bilibili", "color": "#ff6699" } ], "is_official": false }, { "module_name": "yunhu", "project_link": "nonebot-adapter-yunhu", "name": "云湖适配器", "desc": "云湖的NoneBot适配器", "author_id": 104612722, "homepage": "https://github.com/molanp/nonebot-adapter-yunhu", "tags": [ { "label": "云湖", "color": "#8a74eb" } ], "is_official": false }, ] ================================================ FILE: assets/bots.json5 ================================================ [ { "name": "HarukaBot", "desc": "将B站UP主的动态和直播信息推送至QQ", "author_id": 36433929, "homepage": "https://github.com/SK-415/HarukaBot", "tags": [], "is_official": false }, { "name": "Omega Miya", "desc": "B站推送Pixiv搜图识番求签抽卡表情包还有其他杂七杂八的功能", "author_id": 41713304, "homepage": "https://github.com/Ailitonia/omega-miya", "tags": [], "is_official": false }, { "name": "Github Bot", "desc": "在QQ获取/处理Github repo/pr/issue", "author_id": 42488585, "homepage": "https://github.com/cscs181/QQ-GitHub-Bot", "tags": [], "is_official": false }, { "name": "YanXiBot", "desc": "动漫资源查找与娱乐机器人", "author_id": 50488999, "homepage": "https://github.com/Melodyknit/YanXiBot", "tags": [], "is_official": false }, { "name": "绪山真寻bot", "desc": "含有不少的娱乐功能同时稍稍有一些实用的功能 :P", "author_id": 45528451, "homepage": "https://github.com/HibiKier/zhenxun_bot", "tags": [], "is_official": false }, { "name": "ATRI", "desc": "高性能文爱萝卜子,糅杂了各类有趣小功能", "author_id": 37587870, "homepage": "https://github.com/Kyomotoi/ATRI", "tags": [], "is_official": false }, { "name": "dumbot傻瓜机器人", "desc": "猜一猜游戏、新闻一览、英文每日一词一短语等等,含一键启动及docker容器部署就绪", "author_id": 52522252, "homepage": "https://github.com/ffreemt/koyeb-nb2", "tags": [], "is_official": false }, { "name": "DicePP", "desc": "TRPG骰娘, 带先攻, 查询等功能, 主要面向DND5E. 面对骰主推出的船新版本, 内置Windows/Linux详细部署指南以及方便的自定义骰娘方法, 从回复文本到查询资料库都可轻松配置~", "author_id": 88259371, "homepage": "https://github.com/pear-studio/nonebot-dicepp", "tags": [], "is_official": false }, { "name": "SetuBot", "desc": "每个群配置文件独立,可以控制频率,socks http代理,R18开关,支持多tag,自建API lolicon Pixiv热度榜", "author_id": 39484884, "homepage": "https://github.com/yuban10703/setu-nonebot2", "tags": [], "is_official": false }, { "name": "剑网三bot", "desc": "网络游戏《剑侠情缘三》的群聊机器人,数据使用:www.jx3api.com", "author_id": 37363867, "homepage": "https://github.com/JustUndertaker/mini_jx3_bot", "tags": [ { "label": "剑网三", "color": "#5393ec" } ], "is_official": false }, { "name": "PixivBot", "desc": "顾名思义是Pixiv的bot(随机推荐插画、随机指定关键词插画、随机书签、查看排行榜、查看指定id插画)", "author_id": 17331698, "homepage": "https://github.com/ssttkkl/PixivBot", "tags": [], "is_official": false }, { "name": "SeaBot_QQ", "desc": "一个能够获取新闻资讯并推送至QQ的群聊机器人。", "author_id": 31682561, "homepage": "https://github.com/B1ue1nWh1te/SeaBot_QQ", "tags": [], "is_official": false }, { "name": "琪露诺Bot", "desc": "用QQ机器人控制Minecraft服务器!服务器状态查询/服务器白名单/插件列表/玩家查询/转发服务器消息/执行指令... 其他实用娱乐功能,三步即可成功部署的QQ bot", "author_id": 56951617, "homepage": "https://github.com/summerkirakira/CirnoBot", "tags": [ { "label": "Minecraft", "color": "#5393ec" } ], "is_official": false }, { "name": "Inkar Suki", "desc": "一个十分方便的Bot,支持包括Webhook、群管、剑网3等一系列功能,持续更新中……", "author_id": 68726147, "homepage": "https://github.com/HornCopper/Inkar-Suki", "tags": [ { "label": "Minecraft", "color": "#d03790" }, { "label": "GitHub", "color": "#374fd0" }, { "label": "剑网3", "color": "#ff0033" } ], "is_official": false }, { "name": "屑岛风Bot", "desc": "自家用屑Bot", "author_id": 71873002, "homepage": "https://github.com/kexue-z/Dao-bot", "tags": [], "is_official": false }, { "name": "LiteyukiBot-轻雪机器人", "desc": "一个有各种琐事功能的bot,有AI接口,能陪聊", "author_id": 79104275, "homepage": "https://github.com/snowyfirefly/Liteyuki", "tags": [ { "label": "可爱", "color": "#ffc0cb" }, { "label": "AI", "color": "#ea5252" } ], "is_official": false }, { "name": "nya_bot", "desc": "喵服——战魂铭人联机服务器兼机器人", "author_id": 31379266, "homepage": "https://github.com/nikissXI/nya_bot", "tags": [ { "label": "战魂铭人", "color": "#25aaf4" } ], "is_official": false }, { "name": "真宵Bot", "desc": "专注群聊的QQ机器人", "author_id": 71173418, "homepage": "https://github.com/Shine-Light/Nonebot_Bot_MayaFey", "tags": [ { "label": "QQ", "color": "#ea5252" }, { "label": "娱乐", "color": "#a46e49" }, { "label": "群管", "color": "#41aecb" } ], "is_official": false }, { "name": "SkadiBot", "desc": "明日方舟主题机器人—斯卡蒂", "author_id": 101615359, "homepage": "https://github.com/yuyuziYYZ/skadi_bot", "tags": [ { "label": "明日方舟", "color": "#a48888" }, { "label": "斯卡蒂", "color": "#a48888" }, { "label": "arknights", "color": "#a48888" } ], "is_official": false }, { "name": "小白机器人", "desc": "一个高度依赖数据库的群管理机器人", "author_id": 69745333, "homepage": "https://github.com/SDIJF1521/qqai", "tags": [ { "label": "群管理", "color": "#ea5252" }, { "label": "新人作品", "color": "#ea5252" } ], "is_official": false }, { "name": "LittlePaimon", "desc": "小派蒙,多功能原神机器人。", "author_id": 63870437, "homepage": "https://github.com/CMHopeSunshine/LittlePaimon", "tags": [ { "label": "原神", "color": "#7a52ea" } ], "is_official": false }, { "name": "IdhagnBot", "desc": "🐱🤖 一个以娱乐功能为主的缝合怪(划掉)QQ机器人,包含一定Furry要素但是不会卖萌(就是逊啦!)", "author_id": 17371317, "homepage": "https://github.com/su226/IdhagnBot", "tags": [], "is_official": false }, { "name": "hsbot", "desc": "服务于《炉石传说》玩家的机器人,上线至今已有加入十余个个炉石相关群聊,上千名用户使用,响应请求数万次。 数据使用:HSreplay, Fbigame, Hearthstone API", "author_id": 67055520, "homepage": "https://github.com/gzy02/hsbot", "tags": [ { "label": "炉石传说", "color": "#526fea" } ], "is_official": false }, { "name": "Bread Dog Bot", "desc": "Terraria TShock QQ 机器人", "author_id": 160252668, "homepage": "https://github.com/Qianyiovo/bread_dog_bot", "tags": [ { "label": "TShock", "color": "#ea5252" }, { "label": "泰拉瑞亚", "color": "#5dea52" }, { "label": "Terraria", "color": "#5dea52" } ], "is_official": false }, { "name": "RanBot", "desc": "不@会很安静的Bot", "author_id": 88923783, "homepage": "https://github.com/Hecatia-Hell-Workshop/RanBot", "tags": [], "is_official": false }, { "name": "辞辞(cici)Bot", "desc": "一个集成娱乐和群管为一体的机器人", "author_id": 90902259, "homepage": "https://github.com/mengxinyuan638/cici-bot", "tags": [ { "label": "辞辞Bot", "color": "#04de4d" }, { "label": "萌新源", "color": "#fd1c06" }, { "label": "群管", "color": "#06b8fd" } ], "is_official": false }, { "name": "SuzunoBot", "desc": "多功能音游bot,主要服务maimaiDX、Arcaea", "author_id": 29980586, "homepage": "https://github.com/Rinfair-CSP-A016/SuzunoBot-AGLAS", "tags": [ { "label": "maimaiDX", "color": "#189ede" }, { "label": "Arcaea", "color": "#d551ef" }, { "label": "coc", "color": "#7fe4d0" } ], "is_official": false }, { "name": "青岚", "desc": "基于NoneBot的与Minecraft Server互通消息的机器人", "author_id": 54731914, "homepage": "https://github.com/17TheWord/qinglan_bot", "tags": [ { "label": "MineCraft", "color": "#4ef0ea" } ], "is_official": false }, { "name": "ChensQBOTv2", "desc": "多功能QQ群机器人,权限管理/联ban/社工等等等等,以及拥有一个强大的开发者", "author_id": 116929900, "homepage": "https://github.com/cnchens/ChensQBOTv2", "tags": [], "is_official": false }, { "name": "koishi", "desc": "支持爬取 codeforces, atcoder, 牛客上程序设计赛事的 bot。", "author_id": 71639222, "homepage": "https://github.com/CupidsBow/koishi", "tags": [ { "label": "acm", "color": "#f71d1d" }, { "label": "codeforces", "color": "#1df721" }, { "label": "atcoder", "color": "#aa1df7" } ], "is_official": false }, { "name": "脑积水", "desc": "一个超级缝合怪...", "author_id": 66541860, "homepage": "https://github.com/zhulinyv/NJS", "tags": [ { "label": "脑积水", "color": "#ff00ac" } ], "is_official": false }, { "name": "LOVE酱", "desc": "为铁锈战争游戏群服务的虚拟少女,内置了爬取铁锈房间列表功能,以及游戏内单位查询功能,并制作了教学系统以及铁锈相关游戏群的收集功能。", "author_id": 106828088, "homepage": "https://github.com/allureluoli/LOVE-", "tags": [ { "label": "铁锈战争", "color": "#19e229" }, { "label": "RW", "color": "#19e229" } ], "is_official": false }, { "name": "fubot", "desc": "基于nonebot与go-cqhttp的QQ娱乐bot,提供群日常娱乐功能与舞萌DX游戏相关的信息查询功能。", "author_id": 54059896, "homepage": "https://github.com/HCskia/fu-Bot", "tags": [ { "label": "maimai", "color": "#52eaa5" } ], "is_official": false }, { "name": "桃桃酱", "desc": "一个会拆家的高性能缝合萝卜子", "author_id": 107618388, "homepage": "https://github.com/tkgs0/Momoko", "tags": [], "is_official": false }, { "name": "CoolQBot", "desc": "基于 NoneBot2 的聊天机器人", "author_id": 5219550, "homepage": "https://github.com/he0119/CoolQBot", "tags": [], "is_official": false }, { "name": "XDbot2", "desc": "简单的QQ功能型机器人", "author_id": 104149371, "homepage": "https://github.com/ITCraftDevelopmentTeam/XDbot2", "tags": [], "is_official": false }, { "name": "March7th", "desc": "三月七 - 崩坏:星穹铁道机器人", "author_id": 44370805, "homepage": "https://github.com/Mar-7th/March7th", "tags": [ { "label": "StarRail", "color": "#5a8ccc" }, { "label": "星穹铁道", "color": "#6faec6" } ], "is_official": false }, { "name": "ay机器人", "desc": "codeforces和洛谷卷王监视、股票监控、ai聊天", "author_id": 77315378, "homepage": "https://github.com/863109569/qqbot", "tags": [ { "label": "acm", "color": "#ea5252" }, { "label": "洛谷", "color": "#81ea52" }, { "label": "codeforces", "color": "#5261ea" } ], "is_official": false }, { "name": "狐尾", "desc": "一个整合了兽云祭api的机器人,支持账号令牌操作,以及上传兽图", "author_id": 99388013, "homepage": "https://github.com/bingqiu456/shouyun", "tags": [ { "label": "shouyun", "color": "#52ea7a" } ], "is_official": false }, { "name": "ReimeiBot-黎明机器人", "desc": "流星飞逝,黎明终将到来。", "author_id": 65395090, "homepage": "https://github.com/3rdBit/ReimeiBot", "tags": [], "is_official": false }, { "name": "web_bot", "desc": "把机器人搬到网络上", "author_id": 63489103, "homepage": "https://github.com/wsdtl/web_bot", "tags": [ { "label": "xiaonan", "color": "#775151" } ], "is_official": false }, { "name": "林汐", "desc": "多平台功能型Bot", "author_id": 110453675, "homepage": "https://github.com/netsora/SoraBot", "tags": [ { "label": "QQ频道", "color": "#f47070" }, { "label": "OneBot v11", "color": "#212121" } ], "is_official": false }, { "name": "米缸", "desc": "基于nonebot2的米缸Bot", "author_id": 13503375, "homepage": "https://github.com/LambdaYH/MigangBot", "tags": [], "is_official": false }, { "name": "不正经的妹妹", "desc": "一款功能丰富、简单易用、自定义性强、扩展性强的可爱的QQ娱乐机器人", "author_id": 104713034, "homepage": "https://github.com/itsevin/sister_bot", "tags": [], "is_official": false }, { "name": "星见Kirami", "desc": "🌟 读作 Kirami,写作星见,简明轻快的聊天机器人应用。", "author_id": 66513481, "homepage": "https://kiramibot.dev/", "tags": [], "is_official": false }, { "name": "OCNbot", "desc": "OI Contest Notifier bot,一个可以推送洛谷、cf、atcoder、牛客比赛通知的bot", "author_id": 91535478, "homepage": "https://github.com/ACnoway/OCNbot", "tags": [ { "label": "OI", "color": "#2fccff" }, { "label": "ACM", "color": "#ff0004" } ], "is_official": false }, { "name": "妃爱", "desc": "超可爱的妃爱QQ群聊机器人", "author_id": 52267304, "homepage": "https://github.com/jiangyuxiaoxiao/Hiyori", "tags": [], "is_official": false }, { "name": "芙芙", "desc": "供 Mooncell Wiki 协作使用的跨平台机器人", "author_id": 14922941, "homepage": "https://github.com/MooncellWiki/BotFooChan", "tags": [], "is_official": false }, { "name": "Sakiko", "desc": "基于 LiteLoaderBDS 的 Minecraft 基岩版 Bot", "author_id": 55650833, "homepage": "https://github.com/zhaomaoniu/Sakiko", "tags": [ { "label": "Minecraft", "color": "#6cc349" }, { "label": "BanGDream", "color": "#e70050" } ], "is_official": false }, { "name": "Minecraft_QQBot", "desc": "基于 NoneBot2 的 Minecraft 群服互联 QQ 机器人,支持多服务器多种方式连接。", "author_id": 90964775, "homepage": "https://github.com/Minecraft-QQBot/BotServer", "tags": [ { "label": "Minecraft", "color": "#ea5252" }, { "label": "娱乐", "color": "#37a7e7" } ], "is_official": false }, { "name": "小安提Bot", "desc": "服务于音游 舞萌DX 的多功能Bot", "author_id": 186144551, "homepage": "https://github.com/Ant1816/Ant1Bot", "tags": [ { "label": "maimaiDX", "color": "#52ea9a" }, { "label": "音游", "color": "#f74b18" } ], "is_official": false }, { "name": "CanrotBot", "desc": "有很多实用功能的bot,也有很多没什么用的娱乐功能;接入了大模型,并且有一部分功能可以被大模型调用。主打一个全都有(", "author_id": 18070676, "homepage": "https://github.com/wangyw15/CanrotBot", "tags": [], "is_official": false }, { "name": "Mio澪", "desc": "超可爱多功能Qbot", "author_id": 50508678, "homepage": "https://github.com/EienSakura/mio", "tags": [ { "label": "娱乐", "color": "#ea5252" } ], "is_official": false }, { "name": "AntiFraudBot", "desc": "反诈机器人", "author_id": 104713034, "homepage": "https://github.com/itsevin/AntiFraudBot", "tags": [ { "label": "反诈", "color": "#ea5252" } ], "is_official": false }, { "name": "Nekro Agent Bot", "desc": "基于生成式人工智能与沙盒技术的 Nekro Agent 代理执行 AI 机器人,支持聊天、识图、通用文件处理等扩展能力,提供了 WebUI 维护界面、一键部署脚本", "author_id": 57167362, "homepage": "https://github.com/KroMiose/nekro-agent", "tags": [ { "label": "聊天", "color": "#c65856" }, { "label": "大模型", "color": "#46a34c" }, { "label": "WebUI", "color": "#4a96c6" } ], "is_official": false }, { "name": "PickStarsBot", "desc": "欢迎使用PickStarsBot!这是一款基于NoneBot2构建的智能QQ机器人,提供丰富的功能,包括一言、历史上的今天、60秒早报等,快来试试吧!", "author_id": 183461085, "homepage": "https://github.com/PickStars308/PickStarsBot", "tags": [], "is_official": false }, { "name": "LiteBot", "desc": "Web功能/MC/数据功能Bot", "author_id": 67693593, "homepage": "https://github.com/LiteSuggarDEV/LiteBot-NEO/", "tags": [ { "label": "Minecraft", "color": "#ea5252" }, { "label": "Web", "color": "#ea5252" } ], "is_official": false }, { "name": "Muicebot", "desc": "Muice-Chatbot 的 Nonebot2 实现,支持调用主流大模型,支持 Function Call 和内置 MCP Host 实现", "author_id": 72406624, "homepage": "https://github.com/Moemu/MuiceBot", "tags": [ { "label": "LLM", "color": "#3e97ff" } ], "is_official": false }, { "name": "nsybot", "desc": "定时获取推特/bilibili等平台用户文章并推送到QQ群", "author_id": 148176849, "homepage": "https://github.com/AhsokaTano26/nsybot", "tags": [], "is_official": false }, { "name": "Amrita", "desc": "LLM聊天机器人框架", "author_id": 67693593, "homepage": "https://github.com/LiteSuggarDEV/Amrita", "tags": [ { "label": "聊天", "color": "#ea5252" }, { "label": "LLM", "color": "#5c86db" }, { "label": "快捷部署", "color": "#eebe0b" } ], "is_official": false }, { "name": "Rosmontis.io", "desc": "简单的机器人", "author_id": 225668725, "homepage": "https://github.com/com-wuqi/Rosmontis.io", "tags": [ { "label": "可爱", "color": "#ea5252" } ], "is_official": false }, ] ================================================ FILE: assets/drivers.json5 ================================================ [ { "module_name": "~none", "project_link": "", "name": "None", "desc": "None 驱动器", "author_id": 42488585, "homepage": "/docs/advanced/driver", "tags": [], "is_official": true }, { "module_name": "~fastapi", "project_link": "nonebot2[fastapi]", "name": "FastAPI", "desc": "FastAPI 驱动器", "author_id": 42488585, "homepage": "/docs/advanced/driver", "tags": [], "is_official": true }, { "module_name": "~quart", "project_link": "nonebot2[quart]", "name": "Quart", "desc": "Quart 驱动器", "author_id": 42488585, "homepage": "/docs/advanced/driver", "tags": [], "is_official": true }, { "module_name": "~httpx", "project_link": "nonebot2[httpx]", "name": "HTTPX", "desc": "HTTPX 驱动器", "author_id": 42488585, "homepage": "/docs/advanced/driver", "tags": [], "is_official": true }, { "module_name": "~websockets", "project_link": "nonebot2[websockets]", "name": "websockets", "desc": "websockets 驱动器", "author_id": 42488585, "homepage": "/docs/advanced/driver", "tags": [], "is_official": true }, { "module_name": "~aiohttp", "project_link": "nonebot2[aiohttp]", "name": "AIOHTTP", "desc": "AIOHTTP 驱动器", "author_id": 42488585, "homepage": "/docs/advanced/driver", "tags": [], "is_official": true }, ] ================================================ FILE: assets/plugins.json5 ================================================ [ { "module_name": "nonebot_plugin_status", "project_link": "nonebot-plugin-status", "author_id": 42488585, "tags": [ { "label": "server", "color": "#aeeaa8" } ], "is_official": true }, { "module_name": "haruka_bot", "project_link": "haruka-bot", "author_id": 36433929, "tags": [ { "label": "bilibili", "color": "#e55d80" } ], "is_official": false }, { "module_name": "nonebot_plugin_rauthman", "project_link": "nonebot-plugin-rauthman", "author_id": 59906398, "tags": [ { "label": "rule", "color": "#4ec9b0" } ], "is_official": false }, { "module_name": "nonebot_plugin_docs", "project_link": "nonebot-plugin-docs", "author_id": 63496654, "tags": [], "is_official": true }, { "module_name": "nonebot_plugin_sentry", "project_link": "nonebot-plugin-sentry", "author_id": 42488585, "tags": [ { "label": "log", "color": "#6be3ea" } ], "is_official": true }, { "module_name": "nonebot_plugin_apscheduler", "project_link": "nonebot-plugin-apscheduler", "author_id": 42488585, "tags": [], "is_official": true }, { "module_name": "nonebot_plugin_picsearcher", "project_link": "nonebot-plugin-picsearcher", "author_id": 50922489, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_navicat", "project_link": "nonebot-plugin-navicat", "author_id": 50922489, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_translator", "project_link": "nonebot-plugin-translator", "author_id": 59906398, "tags": [ { "label": "func", "color": "#dcdcaa" } ], "is_official": false }, { "module_name": "nonebot_plugin_mqtt", "project_link": "nonebot-plugin-mqtt", "author_id": 50922489, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_songpicker2", "project_link": "nonebot-plugin-songpicker2", "author_id": 20412597, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_strman", "project_link": "nonebot-plugin-strman", "author_id": 95678113, "tags": [], "is_official": false }, { "module_name": "nonebot_bison", "project_link": "nonebot-bison", "author_id": 23295345, "tags": [], "is_official": false }, { "module_name": "nonebot-plugin-ncm", "project_link": "nonebot-plugin-ncm", "author_id": 68675068, "tags": [ { "label": "Netease", "color": "#ec4141" } ], "is_official": false }, { "module_name": "nonebot_plugin_cocdicer", "project_link": "nonebot-plugin-cocdicer", "author_id": 18395948, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_guess", "project_link": "nonebot-plugin-guess", "author_id": 52522252, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_abbrreply", "project_link": "nonebot-plugin-abbrreply", "author_id": 49887895, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_biliav", "project_link": "nonebot_plugin_biliav", "author_id": 9247530, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_analysis_bilibili", "project_link": "nonebot-plugin-analysis-bilibili", "author_id": 36481080, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_localstore", "project_link": "nonebot-plugin-localstore", "author_id": 42488585, "tags": [], "is_official": true }, { "module_name": "nonebot_plugin_alconna", "project_link": "nonebot-plugin-alconna", "author_id": 42648639, "tags": [ { "label": "多适配器", "color": "#5280ea" }, { "label": "消息匹配", "color": "#ea6f52" }, { "label": "跨平台", "color": "#5452ea" } ], "is_official": true }, { "module_name": "nonebot_plugin_mcstatus", "project_link": "nonebot-plugin-mcstatus", "author_id": 50312681, "tags": [ { "label": "Minecraft", "color": "#80070B" } ], "is_official": false }, { "module_name": "nonebot_plugin_help", "project_link": "nonebot-plugin-help", "author_id": 41534161, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_alias", "project_link": "nonebot_plugin_alias", "author_id": 33149974, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_withdraw", "project_link": "nonebot_plugin_withdraw", "author_id": 33149974, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_pixivrank_search", "project_link": "nonebot-plugin-pixivrank-search", "author_id": 45528451, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_russian", "project_link": "nonebot-plugin-russian", "author_id": 45528451, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_setu", "project_link": "nonebot-plugin-setu", "author_id": 63199041, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_heweather", "project_link": "nonebot-plugin-heweather", "author_id": 71873002, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_autohelp", "project_link": "nonebot-plugin-autohelp", "author_id": 52522252, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_flexperm", "project_link": "nonebot-plugin-flexperm", "author_id": 13314764, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_epicfree", "project_link": "nonebot-plugin-epicfree", "author_id": 22407052, "tags": [], "is_official": false }, { "module_name": "ELF_RSS2", "project_link": "ELF-RSS", "author_id": 32663291, "tags": [], "is_official": false }, { "module_name": "nb2chan", "project_link": "nb2chan", "author_id": 16970614, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_setu_now", "project_link": "nonebot-plugin-setu-now", "author_id": 71873002, "tags": [], "is_official": false }, { "module_name": "leetcode", "project_link": "nonebot-plugin-leetcode", "author_id": 32358438, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_youthstudy", "project_link": "nonebot-plugin-youthstudy", "author_id": 63199041, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_shindan", "project_link": "nonebot_plugin_shindan", "author_id": 33149974, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_code", "project_link": "nonebot-plugin-code", "author_id": 51691024, "tags": [ { "label": "func", "color": "#dcdcaa" } ], "is_official": false }, { "module_name": "nonebot_plugin_picsbank", "project_link": "nonebot-plugin-picsbank", "author_id": 35657483, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_tvseries", "project_link": "nonebot-plugin-tvseries", "author_id": 71873002, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_lolmatch", "project_link": "nonebot_plugin_lolmatch", "author_id": 35657483, "tags": [], "is_official": false }, { "module_name": "OlivOS.nonebot", "project_link": "OlivOS.nb2", "author_id": 50312681, "tags": [ { "label": "OlivOS", "color": "#00a0ea" } ], "is_official": false }, { "module_name": "nonebot_plugin_htmlrender", "project_link": "nonebot-plugin-htmlrender", "author_id": 71873002, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_admin", "project_link": "nonebot-plugin-admin", "author_id": 51691024, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_memes", "project_link": "nonebot_plugin_memes", "author_id": 33149974, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_repeater", "project_link": "nonebot-plugin-repeater", "author_id": 29861280, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_directlinker", "project_link": "nonebot-plugin-directlinker", "author_id": 29861280, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_forwarder", "project_link": "nonebot-plugin-forwarder", "author_id": 29861280, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_roll", "project_link": "nonebot_plugin_roll", "author_id": 69038090, "tags": [ { "label": "roll", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_crazy_thursday", "project_link": "nonebot_plugin_crazy_thursday", "author_id": 69038090, "tags": [ { "label": "Thursday", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_covid19_news", "project_link": "nonebot-plugin-covid19-news", "author_id": 57753690, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_remake", "project_link": "nonebot_plugin_remake", "author_id": 33149974, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_weather_lite", "project_link": "nonebot-plugin-weather-lite", "author_id": 57033359, "tags": [ { "label": "天气", "color": "#6ec3d9" } ], "is_official": false }, { "module_name": "nonebot_plugin_fortune", "project_link": "nonebot-plugin-fortune", "author_id": 69038090, "tags": [ { "label": "fortune", "color": "#ea6f52" } ], "is_official": false }, { "module_name": "nonebot_plugin_tarot", "project_link": "nonebot_plugin_tarot", "author_id": 69038090, "tags": [ { "label": "tarot", "color": "#461264" } ], "is_official": false }, { "module_name": "nonebot_plugin_emojimix", "project_link": "nonebot_plugin_emojimix", "author_id": 33149974, "tags": [ { "label": "emoji", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_what2eat", "project_link": "nonebot-plugin-what2eat", "author_id": 69038090, "tags": [ { "label": "what2eat", "color": "#f09526" } ], "is_official": false }, { "module_name": "nonebot_plugin_datastore", "project_link": "nonebot-plugin-datastore", "author_id": 5219550, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_boardgame", "project_link": "nonebot_plugin_boardgame", "author_id": 33149974, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_wordcloud", "project_link": "nonebot-plugin-wordcloud", "author_id": 5219550, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_chatrecorder", "project_link": "nonebot_plugin_chatrecorder", "author_id": 33149974, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_antiflash", "project_link": "nonebot-plugin-antiflash", "author_id": 69038090, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_word_bank2", "project_link": "nonebot-plugin-word-bank2", "author_id": 71873002, "tags": [ { "label": "wordbank", "color": "#0b00ff" } ], "is_official": false }, { "module_name": "nonebot_plugin_txt2img", "project_link": "nonebot-plugin-txt2img", "author_id": 44370805, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_morning", "project_link": "nonebot-plugin-morning", "author_id": 69038090, "tags": [ { "label": "morning", "color": "#ebc025" } ], "is_official": false }, { "module_name": "nonebot_plugin_pixiv", "project_link": "nonebot-plugin-pixiv", "author_id": 49887895, "tags": [ { "label": " pixiv", "color": "#0096fa" }, { "label": "R18", "color": "#ffff00" } ], "is_official": false }, { "module_name": "YetAnotherPicSearch", "project_link": "YetAnotherPicSearch", "author_id": 23137034, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_gachalogs", "project_link": "nonebot-plugin-gachalogs", "author_id": 22407052, "tags": [ { "label": "Genshin", "color": "#ffd49f" } ], "is_official": false }, { "module_name": "nonebot_plugin_everyday_en", "project_link": "nonebot-plugin-everyday-en", "author_id": 81250368, "tags": [ { "label": "EveryDay", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_fire", "project_link": "nonebot-plugin-fire", "author_id": 45707511, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_qrcode", "project_link": "nonebot-plugin-qrcode", "author_id": 71873002, "tags": [ { "label": "QRcode", "color": "#0020ff" } ], "is_official": false }, { "module_name": "nonebot_plugin_ygo", "project_link": "nonebot-plugin-ygo", "author_id": 49887895, "tags": [ { "label": "游戏王", "color": "#ea5252" }, { "label": "口胡王", "color": "#ea5252" }, { "label": "ygo", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_bilibilibot", "project_link": "nonebot-plugin-bilibilibot", "author_id": 54183084, "tags": [ { "label": "bilibili", "color": "#f605dd" } ], "is_official": false }, { "module_name": "nonebot_plugin_color", "project_link": "nonebot-plugin-color", "author_id": 22407052, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_blackjack", "project_link": "nonebot-plugin-blackjack", "author_id": 30517062, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_arcaeabot", "project_link": "nonebot-plugin-arcaeabot", "author_id": 9484642, "tags": [ { "label": "Arcaea", "color": "#db52ea" } ], "is_official": false }, { "module_name": "nonebot_plugin_ddcheck", "project_link": "nonebot_plugin_ddcheck", "author_id": 33149974, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_leetcode2", "project_link": "nonebot-plugin-leetcode2", "author_id": 30568146, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_mediawiki", "project_link": "nonebot-plugin-mediawiki", "author_id": 68314080, "tags": [ { "label": "wiki", "color": "#679ff9" } ], "is_official": false }, { "module_name": "nonebot_plugin_wordle", "project_link": "nonebot_plugin_wordle", "author_id": 33149974, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_giyf", "project_link": "nonebot-plugin-giyf", "author_id": 68314080, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_abstract", "project_link": "nonebot-plugin-abstract", "author_id": 98074861, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_params", "project_link": "nonebot-plugin-params", "author_id": 48091591, "tags": [ { "label": "helper", "color": "#ffe873" } ], "is_official": false }, { "module_name": "nonebot_plugin_handle", "project_link": "nonebot_plugin_handle", "author_id": 33149974, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_minesweeper", "project_link": "nonebot_plugin_minesweeper", "author_id": 33149974, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_draw", "project_link": "nonebot-plugin-draw", "author_id": 98812723, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_randomtkk", "project_link": "nonebot-plugin-randomtkk", "author_id": 69038090, "tags": [ { "label": "Tan Kuku", "color": "#fdaf75" }, { "label": "Liyuu", "color": "#465dfd" } ], "is_official": false }, { "module_name": "nonebot_plugin_dida", "project_link": "nonebot-plugin-dida", "author_id": 54183084, "tags": [ { "label": "滴答清单", "color": "#007ffd" } ], "is_official": false }, { "module_name": "nonebot_plugin_alipayvoice", "project_link": "nonebot-plugin-alipayvoice", "author_id": 66513481, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_answersbook", "project_link": "nonebot-plugin-answersbook", "author_id": 66513481, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_hitokoto", "project_link": "nonebot-plugin-hitokoto", "author_id": 66513481, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_bilicover", "project_link": "nonebot-plugin-bilicover", "author_id": 66513481, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_cchess", "project_link": "nonebot_plugin_cchess", "author_id": 33149974, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_chess", "project_link": "nonebot_plugin_chess", "author_id": 33149974, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_charpic", "project_link": "nonebot-plugin-charpic", "author_id": 66518048, "tags": [ { "label": "字符画", "color": "#ea5252" }, { "label": "多平台适配", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_miragetank", "project_link": "nonebot-plugin-miragetank", "author_id": 66518048, "tags": [ { "label": "幻影坦克", "color": "#ea5252" }, { "label": "多平台适配", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_yulu", "project_link": "nonebot-plugin-yulu", "author_id": 99388013, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_maze", "project_link": "nonebot-plugin-maze", "author_id": 100039483, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_moyu", "project_link": "nonebot-plugin-moyu", "author_id": 66513481, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_mockingbird", "project_link": "nonebot-plugin-mockingbird", "author_id": 55268546, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_baidutranslate", "project_link": "nonebot-plugin-baidutranslate", "author_id": 52584526, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_tortoise_orm", "project_link": "nonebot-plugin-tortoise-orm", "author_id": 71873002, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_dailysign", "project_link": "nonebot-plugin-dailysign", "author_id": 71873002, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_tetris_stats", "project_link": "nonebot-plugin-tetris-stats", "author_id": 51957264, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_bilibili_viode", "project_link": "nonebot-plugin-bilibili-viode", "author_id": 61133548, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_imagetools", "project_link": "nonebot_plugin_imagetools", "author_id": 33149974, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_warframe_clock", "project_link": "nonebot-plugin-warframe-clock", "author_id": 124094085, "tags": [ { "label": "Warframe", "color": "#149090" } ], "is_official": false }, { "module_name": "hikari_bot", "project_link": "hikari-bot", "author_id": 48101337, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_who_at_me", "project_link": "nonebot-plugin-who-at-me", "author_id": 9484642, "tags": [ { "label": "群聊", "color": "#52afea" } ], "is_official": false }, { "module_name": "nonebot_plugin_covid_19_by", "project_link": "nonebot-plugin-covid-19-by", "author_id": 99388013, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_reboot", "project_link": "nonebot-plugin-reboot", "author_id": 22175295, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_setu4", "project_link": "nonebot-plugin-setu4", "author_id": 87489040, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_smart_reply", "project_link": "nonebot-plugin-smart-reply", "author_id": 87489040, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_today_in_history", "project_link": "nonebot-plugin-today-in-history", "author_id": 64363680, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_BitTorrent", "project_link": "nonebot-plugin-BitTorrent", "author_id": 87489040, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_namelist", "project_link": "nonebot-plugin-namelist", "author_id": 66513481, "tags": [ { "label": "黑名单", "color": "#323232" }, { "label": "白名单", "color": "#fafafa" } ], "is_official": false }, { "module_name": "nonebot_plugin_bread_shop", "project_link": "nonebot-plugin-bread-shop", "author_id": 62082723, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_PicMenu", "project_link": "nonebot-plugin-PicMenu", "author_id": 61297321, "tags": [ { "label": "menu", "color": "#753dc6" } ], "is_official": false }, { "module_name": "nonebot_plugin_horserace", "project_link": "nonebot-plugin-horserace", "author_id": 105840558, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_firexN", "project_link": "nonebot-plugin-firexN", "author_id": 94956933, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_bfinfo", "project_link": "nonebot-plugin-bfinfo", "author_id": 94956933, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_osubot", "project_link": "nonebot-plugin-osubot", "author_id": 30517062, "tags": [ { "label": "OSU", "color": "#eb5d9b" } ], "is_official": false }, { "module_name": "nonebot_plugin_acc_calculate", "project_link": "nonebot-plugin-acc-calculate", "author_id": 117957183, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_kawaii_robot", "project_link": "nonebot-plugin-kawaii-robot", "author_id": 51886078, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_addFriend", "project_link": "nonebot-plugin-addfriend", "author_id": 77319678, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_date_name", "project_link": "nonebot-plugin-date-name", "author_id": 99388013, "tags": [ { "label": "qun_card", "color": "#e552ea" } ], "is_official": false }, { "module_name": "nonebot_plugin_easyCommand", "project_link": "nonebot-plugin-easycommand", "author_id": 77319678, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_report", "project_link": "nonebot-plugin-report", "author_id": 61999173, "tags": [ { "label": "webhook", "color": "#51b3a8" }, { "label": "notify", "color": "#3985f7" } ], "is_official": false }, { "module_name": "nonebot_plugin_hammer_nbnhhsh", "project_link": "nonebot-plugin-hammer-nbnhhsh", "author_id": 15799382, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_mcqq", "project_link": "nonebot-plugin-mcqq", "author_id": 54731914, "tags": [ { "label": "Minecraft", "color": "#52ea6f" }, { "label": "消息互通", "color": "#52eadf" } ], "is_official": false }, { "module_name": "nonebot_plugin_covid_19_by_guild", "project_link": "nonebot-plugin-covid-19-by-guild", "author_id": 99388013, "tags": [ { "label": "疫情小助手", "color": "#526fea" } ], "is_official": false }, { "module_name": "nonebot_plugin_wiki", "project_link": "nonebot-plugin-wiki", "author_id": 60338092, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_groupmanager", "project_link": "nonebot-plugin-groupmanager", "author_id": 76118866, "tags": [ { "label": "简易群管", "color": "#53e950" }, { "label": "插件改良", "color": "#2b7be2" } ], "is_official": false }, { "module_name": "nonebot_plugin_game_collection", "project_link": "nonebot-plugin-game-collection", "author_id": 51886078, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_drawer", "project_link": "nonebot-plugin-drawer", "author_id": 35400185, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_jrrp-n", "project_link": "nonebot-plugin-jrrp-n", "author_id": 82658163, "tags": [ { "label": "每日人品", "color": "#ea5252" }, { "label": "jrrp", "color": "#529fea" } ], "is_official": false }, { "module_name": "nonebot_plugin_moegoe", "project_link": "nonebot-plugin-moegoe", "author_id": 10485632, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_pixivbot", "project_link": "nonebot-plugin-pixivbot", "author_id": 17331698, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_workscore", "project_link": "nonebot-plugin-workscore", "author_id": 51691024, "tags": [ { "label": "工作性价比计算器", "color": "#3898fc" } ], "is_official": false }, { "module_name": "nonebot_plugin_treehelp", "project_link": "nonebot-plugin-treehelp", "author_id": 5219550, "tags": [], "is_official": false }, { "module_name": "cqsat", "project_link": "nonebot-plugin-cqsat", "author_id": 51691024, "tags": [ { "label": "业余无线电", "color": "#ea5252" }, { "label": "HAM", "color": "#3898fc" }, { "label": "卫星追踪", "color": "#fca638" } ], "is_official": false }, { "module_name": "nonebot_plugin_course", "project_link": "nonebot-plugin-course", "author_id": 101713235, "tags": [ { "label": "课表", "color": "#6e9af2" } ], "is_official": false }, { "module_name": "nonebot_plugin_dialectlist", "project_link": "nonebot-plugin-dialectlist", "author_id": 91937041, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_offline_mahjong_helper", "project_link": "nonebot-plugin-offline-mahjong-helper", "author_id": 30568146, "tags": [ { "label": "Mahjong", "color": "#ea5252" }, { "label": "雀魂", "color": "#eaa452" }, { "label": "线下约桌", "color": "#52a6ea" } ], "is_official": false }, { "module_name": "nonebot_plugin_send", "project_link": "nonebot-plugin-send", "author_id": 34237511, "tags": [ { "label": "send", "color": "#ea5252" }, { "label": "notice", "color": "#ea5252" }, { "label": "公告", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_todo_nlp", "project_link": "nonebot-plugin-todo-nlp", "author_id": 52662784, "tags": [ { "label": "todo", "color": "#499bdd" }, { "label": "nlp", "color": "#83b279" } ], "is_official": false }, { "module_name": "nonebot_plugin_wordsnorote", "project_link": "nonebot-plugin-wordsnorote", "author_id": 94956933, "tags": [ { "label": "四六级", "color": "#24a0d8" } ], "is_official": false }, { "module_name": "nonebot_plugin_CyberSensoji", "project_link": "nonebot-plugin-CyberSensoji", "author_id": 80341233, "tags": [ { "label": "抽签", "color": "#52eadf" } ], "is_official": false }, { "module_name": "nonebot_plugin_gspanel", "project_link": "nonebot-plugin-gspanel", "author_id": 22407052, "tags": [ { "label": "Genshin", "color": "#ffd49f" } ], "is_official": false }, { "module_name": "nonebot_plugin_gsmaterial", "project_link": "nonebot-plugin-gsmaterial", "author_id": 22407052, "tags": [ { "label": "Genshin", "color": "#ffd49f" } ], "is_official": false }, { "module_name": "nonebot_plugin_mystool", "project_link": "nonebot-plugin-mystool", "author_id": 63289359, "tags": [ { "label": "米游社", "color": "#66e0ff" }, { "label": "原神", "color": "#faf3c4" } ], "is_official": false }, { "module_name": "nonebot_plugin_warframe", "project_link": "nonebot-plugin-warframe", "author_id": 54731914, "tags": [ { "label": "星际战甲", "color": "#ed3f3f" }, { "label": "WarFrame", "color": "#edea3f" } ], "is_official": false }, { "module_name": "nonebot_plugin_mcqq_server", "project_link": "nonebot-plugin-mcqq-server", "author_id": 51886078, "tags": [ { "label": "Minecraft", "color": "#52ea64" }, { "label": "消息互通", "color": "#52e5ea" } ], "is_official": false }, { "module_name": "nonebot_plugin_RealESRGAN", "project_link": "nonebot-plugin-RealESRGAN", "author_id": 78833215, "tags": [ { "label": "图像超分辨率重建", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot-plugin-wolf-kill", "project_link": "nonebot-plugin-wolf-kill", "author_id": 30973981, "tags": [], "is_official": false }, { "module_name": "iot", "project_link": "nonebot-plugin-iot", "author_id": 70781619, "tags": [ { "label": "物联网", "color": "#4b86d7" }, { "label": "天猫精灵", "color": "#4b86d7" }, { "label": "IOT", "color": "#4b86d7" } ], "is_official": false }, { "module_name": "nonebot_plugin_bwiki_navigator", "project_link": "nonebot-plugin-bwiki-navigator", "author_id": 41534161, "tags": [ { "label": "wiki", "color": "#29a5e3" } ], "is_official": false }, { "module_name": "nonebot_plugin_bottle", "project_link": "nonebot_plugin_bottle", "author_id": 97968466, "tags": [ { "label": "漂流瓶", "color": "#0893f2" } ], "is_official": false }, { "module_name": "nonebot_plugin_tts_gal", "project_link": "nonebot-plugin-tts-gal", "author_id": 89716406, "tags": [ { "label": "VITS", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_alicdk_get", "project_link": "nonebot-plugin-alicdk-get", "author_id": 53631287, "tags": [ { "label": "兑换码", "color": "#595fd6" }, { "label": "auto", "color": "#595fd6" }, { "label": "阿里云盘", "color": "#595fd6" } ], "is_official": false }, { "module_name": "nonebot_plugin_picstatus", "project_link": "nonebot-plugin-picstatus", "author_id": 59048777, "tags": [ { "label": "server", "color": "#8bff00" } ], "is_official": false }, { "module_name": "nonebot_plugin_tuling", "project_link": "nonebot-plugin-tuling", "author_id": 45281765, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_makemidi", "project_link": "nonebot-plugin-makemidi", "author_id": 79776324, "tags": [ { "label": "midi", "color": "#6515a8" } ], "is_official": false }, { "module_name": "nonebot_plugin_ocr", "project_link": "nonebot-plugin-ocr", "author_id": 111744697, "tags": [ { "label": "ocr ", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_listener", "project_link": "nonebot-plugin-listener", "author_id": 30973981, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_BiliRequestAll", "project_link": "nonebot-plugin-BiliRequestAll", "author_id": 112923496, "tags": [ { "label": "bilibili", "color": "#ea52e9" }, { "label": "request", "color": "#5eea52" } ], "is_official": false }, { "module_name": "nonebot_plugin_russian_ban", "project_link": "nonebot-plugin-russian-ban", "author_id": 51886078, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_ygo_trade", "project_link": "nonebot-plugin-ygo-trade", "author_id": 41512913, "tags": [ { "label": "游戏王", "color": "#ea5252" }, { "label": "YGO", "color": "#ea5252" }, { "label": "集换社", "color": "#eada52" } ], "is_official": false }, { "module_name": "nonebot_plugin_novelai", "project_link": "nonebot-plugin-novelai", "author_id": 34237511, "tags": [ { "label": "aidraw", "color": "#ffc646" }, { "label": "naifu", "color": "#ffc646" }, { "label": "webui", "color": "#ffc646" } ], "is_official": false }, { "module_name": "ayaka_games", "project_link": "ayaka-games", "author_id": 47290820, "tags": [ { "label": "小游戏", "color": "#e36306" } ], "is_official": false }, { "module_name": "ayaka_timezone", "project_link": "nonebot-plugin-ayaka-timezone", "author_id": 47290820, "tags": [ { "label": "timezone", "color": "#e36306" } ], "is_official": false }, { "module_name": "ayaka_prevent_bad_words", "project_link": "nonebot-plugin-ayaka-prevent-bad-words", "author_id": 47290820, "tags": [ { "label": "撤回", "color": "#e36306" } ], "is_official": false }, { "module_name": "nonebot_plugin_savor", "project_link": "nonebot-plugin-savor", "author_id": 66513481, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_kfcrazy", "project_link": "nonebot-plugin-kfcrazy", "author_id": 53631287, "tags": [ { "label": "肯德基", "color": "#d93b3b" }, { "label": "疯狂星期四", "color": "#e52124" }, { "label": "KFC", "color": "#cb5c5e" } ], "is_official": false }, { "module_name": "nonebot-plugin-random", "project_link": "nonebot-plugin-random", "author_id": 52129454, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_blacklist", "project_link": "nonebot-plugin-blacklist", "author_id": 107618388, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_antiinsult", "project_link": "nonebot-plugin-antiinsult", "author_id": 107618388, "tags": [ { "label": "被动", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_oddtext", "project_link": "nonebot-plugin-oddtext", "author_id": 33149974, "tags": [ { "label": "RCNB!", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_mahjong_scoreboard", "project_link": "nonebot-plugin-mahjong-scoreboard", "author_id": 17331698, "tags": [ { "label": "日麻", "color": "#4684d3" } ], "is_official": false }, { "module_name": "nonebot_plugin_cartoon", "project_link": "nonebot-plugin-cartoon", "author_id": 66513481, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_mahjong_utils", "project_link": "nonebot-plugin-mahjong-utils", "author_id": 17331698, "tags": [ { "label": "日麻", "color": "#edad34" } ], "is_official": false }, { "module_name": "nonebot_plugin_animeres", "project_link": "nonebot-plugin-animeres", "author_id": 50488999, "tags": [ { "label": "anime", "color": "#ec5252" } ], "is_official": false }, { "module_name": "nonebot-plugin-person", "project_link": "nonebot-plugin-person", "author_id": 52129454, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_apex_api_query", "project_link": "nonebot-plugin-apex-api-query", "author_id": 40495719, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_reborn", "project_link": "nonebot-plugin-reborn", "author_id": 113450723, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_searchBiliInfo", "project_link": "nonebot-plugin-searchbiliinfo", "author_id": 40910637, "tags": [ { "label": "bilibili", "color": "#e55d80" } ], "is_official": false }, { "module_name": "nonebot_plugin_colab_novelai", "project_link": "nonebot-plugin-colab-novelai", "author_id": 100039483, "tags": [ { "label": "NovelAI", "color": "#eacd52" } ], "is_official": false }, { "module_name": "nonebot_plugin_sky", "project_link": "nonebot-plugin-sky", "author_id": 53631287, "tags": [ { "label": "光遇", "color": "#7ebdf0" }, { "label": "攻略", "color": "#2079c1" } ], "is_official": false }, { "module_name": "nonebot_plugin_zyk_novelai", "project_link": "nonebot-plugin-zyk-novelai", "author_id": 110616928, "tags": [ { "label": "Free", "color": "#42e22f" }, { "label": "Simple", "color": "#e2d92f" }, { "label": "Novelai", "color": "#3e10e9" } ], "is_official": false }, { "module_name": "nonebot_plugin_repeep", "project_link": "nonebot-plugin-repeep", "author_id": 51946313, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_gscode", "project_link": "nonebot-plugin-gscode", "author_id": 22407052, "tags": [ { "label": "Genshin", "color": "#ffd49f" } ], "is_official": false }, { "module_name": "nonebot_plugin_note", "project_link": "nonebot-plugin-note", "author_id": 111600679, "tags": [], "is_official": false }, { "module_name": "nonebot-plugin-bilibili-image", "project_link": "nonebot-plugin-bilibili-image", "author_id": 52129454, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_trace_moe", "project_link": "nonebot-plugin-trace-moe", "author_id": 40910637, "tags": [ { "label": "trace", "color": "#191919" }, { "label": "image", "color": "#00a0ff" } ], "is_official": false }, { "module_name": "nonebot_plugin_zyk_music", "project_link": "nonebot-plugin-zyk-music", "author_id": 110616928, "tags": [ { "label": "Free", "color": "#26d019" }, { "label": "Simple", "color": "#b8c10d" }, { "label": "Music", "color": "#0d92c1" } ], "is_official": false }, { "module_name": "nonebot_plugin_chatgpt", "project_link": "nonebot-plugin-chatgpt", "author_id": 66513481, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_majsoul", "project_link": "nonebot-plugin-majsoul", "author_id": 17331698, "tags": [ { "label": "majsoul", "color": "#e54141" } ], "is_official": false }, { "module_name": "nonebot_plugin_remove_bg", "project_link": "nonebot-plugin-remove-bg", "author_id": 40910637, "tags": [ { "label": "img", "color": "#111111" }, { "label": "removeBG", "color": "#7a7a7a" } ], "is_official": false }, { "module_name": "nonebot_plugin_broadcast", "project_link": "nonebot-plugin-broadcast", "author_id": 66513481, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_exchangerate", "project_link": "nonebot-plugin-exchangerate", "author_id": 66513481, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_access_control", "project_link": "nonebot-plugin-access-control", "author_id": 17331698, "tags": [ { "label": "权限控制", "color": "#0e9763" } ], "is_official": false }, { "module_name": "nonebot_plugin_colormind", "project_link": "nonebot-plugin-colormind", "author_id": 40910637, "tags": [ { "label": "color", "color": "#ffffff" }, { "label": "配色", "color": "#fbff03" } ], "is_official": false }, { "module_name": "nonebot_plugin_abstain_diary", "project_link": "nonebot-plugin-abstain-diary", "author_id": 40910637, "tags": [ { "label": "戒", "color": "#ffffff" } ], "is_official": false }, { "module_name": "nonebot_plugin_backup", "project_link": "nonebot-plugin-backup", "author_id": 46314093, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_kuma_san", "project_link": "nonebot-plugin-kuma-san", "author_id": 30224828, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_gpt3", "project_link": "nonebot-plugin-gpt3", "author_id": 63803385, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_ikun_evolution", "project_link": "nonebot-plugin-ikun-evolution", "author_id": 11630758, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_random_ban", "project_link": "nonebot-plugin-random-ban", "author_id": 40910637, "tags": [ { "label": "禁言", "color": "#020202" }, { "label": "ban", "color": "#ffffff" } ], "is_official": false }, { "module_name": "nonebot_plugin_antirecall", "project_link": "nonebot-plugin-antirecall", "author_id": 105533056, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_mc_server_status", "project_link": "nonebot_plugin_mc_server_status", "author_id": 31379266, "tags": [ { "label": "Minecraft", "color": "#a438cd" } ], "is_official": false }, { "module_name": "nonebot_plugin_no_repeat", "project_link": "nonebot-plugin-no-repeat", "author_id": 47290820, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_bfchat", "project_link": "nonebot-plugin-bfchat", "author_id": 30611816, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_summon", "project_link": "nonebot-plugin-summon", "author_id": 66541860, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_ping", "project_link": "nonebot-plugin-ping", "author_id": 66541860, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_face2cartoonpic", "project_link": "nonebot-plugin-face2cartoonpic", "author_id": 96008766, "tags": [ { "label": "以图绘图", "color": "#72f15e" }, { "label": "腾讯云", "color": "#3785f1" } ], "is_official": false }, { "module_name": "nonebot_plugin_servicestate", "project_link": "nonebot-plugin-servicestate", "author_id": 44545625, "tags": [ { "label": "api", "color": "#52ea7f" }, { "label": "state", "color": "#52cfea" } ], "is_official": false }, { "module_name": "nonebot_plugin_animalVoice", "project_link": "nonebot-plugin-animalvoice", "author_id": 96008766, "tags": [ { "label": "切噜语~", "color": "#e75f9d" }, { "label": "兽语", "color": "#5fe5e7" }, { "label": "加密语言", "color": "#79e556" } ], "is_official": false }, { "module_name": "nonebot_plugin_ayaka_scan_cmd", "project_link": "nonebot-plugin-ayaka-scan-cmd", "author_id": 47290820, "tags": [ { "label": "命令探查", "color": "#e36306" } ], "is_official": false }, { "module_name": "nonebot_plugin_HttpCat", "project_link": "nonebot-plugin-HttpCat", "author_id": 96008766, "tags": [ { "label": "HttpCat", "color": "#1f4ddc" }, { "label": "http状态码", "color": "#dc1f1f" } ], "is_official": false }, { "module_name": "nonebot_plugin_revoke", "project_link": "nonebot-plugin-revoke", "author_id": 17331698, "tags": [ { "label": "gocqhttp", "color": "#52ea95" } ], "is_official": false }, { "module_name": "nonebot_plugin_setu_customization", "project_link": "nonebot_plugin_setu_customization", "author_id": 31379266, "tags": [ { "label": "色图", "color": "#e9ea52" } ], "is_official": false }, { "module_name": "nonebot_plugin_l4d2_server", "project_link": "nonebot-plugin-l4d2-server", "author_id": 70925546, "tags": [ { "label": "l4d2", "color": "#05ff00" }, { "label": "Alconna", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_learning_chat", "project_link": "nonebot-plugin-learning-chat", "author_id": 63870437, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_couplets", "project_link": "nonebot-plugin-couplets", "author_id": 63870437, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_Imagelabels", "project_link": "nonebot-plugin-Imagelabels", "author_id": 110215026, "tags": [ { "label": "Yolov5", "color": "#9a2828" }, { "label": "图像标注", "color": "#e981dc" } ], "is_official": false }, { "module_name": "nonebot_plugin_cloudsignx", "project_link": "nonebot-plugin-cloudsignx", "author_id": 42509185, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_imgexploration", "project_link": "nonebot-plugin-imgexploration", "author_id": 46257373, "tags": [ { "label": "搜图", "color": "#453df1" } ], "is_official": false }, { "module_name": "nonebot_plugin_hypixel", "project_link": "nonebot-plugin-hypixel", "author_id": 82658163, "tags": [ { "label": "MC", "color": "#6fea52" }, { "label": "Hypixel", "color": "#d5ea52" }, { "label": "Hyp", "color": "#d5ea52" } ], "is_official": false }, { "module_name": "nonebot_plugin_nowtime", "project_link": "nonebot_plugin_nowtime", "author_id": 106718176, "tags": [ { "label": "整点报时", "color": "#5eea52" }, { "label": "语音", "color": "#c84fdb" } ], "is_official": false }, { "module_name": "nonebot_plugin_cave", "project_link": "nonebot-plugin-cave", "author_id": 85006030, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_xingzuo", "project_link": "nonebot-plugin-xingzuo", "author_id": 90902259, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_BingImage", "project_link": "nonebot-plugin-BingImage", "author_id": 71204348, "tags": [ { "label": "风景图", "color": "#0ce354" }, { "label": "Bing", "color": "#0c43e3" }, { "label": "必应", "color": "#eddf13" } ], "is_official": false }, { "module_name": "nonebot_plugin_soup", "project_link": "nonebot-plugin-soup", "author_id": 42509185, "tags": [ { "label": "心灵鸡汤", "color": "#52eaea" }, { "label": "鸡汤", "color": "#ea529a" }, { "label": "毒鸡汤", "color": "#604a55" } ], "is_official": false }, { "module_name": "nonebot_plugin_yuanshen_notice", "project_link": "nonebot-plugin-yuanshen-notice", "author_id": 90902259, "tags": [ { "label": "原神", "color": "#ef3700" }, { "label": "公告", "color": "#00ef04" } ], "is_official": false }, { "module_name": "nonebot_plugin_bilibili_yuan", "project_link": "nonebot-plugin-bilibili-yuan", "author_id": 90902259, "tags": [ { "label": "bilibili", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_easy_translate", "project_link": "nonebot_plugin_easy_translate", "author_id": 31379266, "tags": [ { "label": "翻译", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_orangedice", "project_link": "nonebot-plugin-orangedice", "author_id": 63897047, "tags": [ { "label": "dice", "color": "#08c0bb" }, { "label": "COC", "color": "#a2bc0c" } ], "is_official": false }, { "module_name": "nonebot_plugin_record", "project_link": "nonebot-plugin-record", "author_id": 104713034, "tags": [ { "label": "语音", "color": "#fff35d" }, { "label": "语音识别", "color": "#37c0f6" }, { "label": "语音事件响应器", "color": "#18e13c" } ], "is_official": false }, { "module_name": "nonebot_plugin_nya_cook_menu", "project_link": "nonebot_plugin_nya_cook_menu", "author_id": 31379266, "tags": [ { "label": "菜谱", "color": "#e65de5" } ], "is_official": false }, { "module_name": "criminal_dance", "project_link": "criminal-dance", "author_id": 47290820, "tags": [ { "label": "文字版桌游", "color": "#e36306" } ], "is_official": false }, { "module_name": "nonebot_plugin_picmcstat", "project_link": "nonebot-plugin-picmcstat", "author_id": 59048777, "tags": [ { "label": "Minecraft", "color": "#7fbf55" } ], "is_official": false }, { "module_name": "nonebot_plugin_wantwords", "project_link": "nonebot-plugin-wantwords", "author_id": 81900789, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_pvz", "project_link": "nonebot-plugin-pvz", "author_id": 75836227, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_report_manager", "project_link": "nonebot-plugin-report-manager", "author_id": 75826243, "tags": [], "is_official": false }, { "module_name": "qinglan_bot", "project_link": "qinglan-bot", "author_id": 54731914, "tags": [ { "label": "MineCraft", "color": "#4ef0ea" } ], "is_official": false }, { "module_name": "nonebot_plugin_easy_group_manager", "project_link": "nonebot-plugin-easy-group-manager", "author_id": 66541860, "tags": [ { "label": "群管", "color": "#1eb262" }, { "label": "女生自用", "color": "#b21e82" } ], "is_official": false }, { "module_name": "nonebot_plugin_group_link_guild", "project_link": "nonebot-plugin-group-link-guild", "author_id": 54731914, "tags": [ { "label": "QQ群", "color": "#ea5252" }, { "label": "QQ频道", "color": "#52ead5" }, { "label": "消息互通", "color": "#50c545" } ], "is_official": false }, { "module_name": "nonebot-plugin-mcport", "project_link": "nonebot-plugin-mcport", "author_id": 107346913, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_xdu_support", "project_link": "nonebot-plugin-xdu-support", "author_id": 75836227, "tags": [ { "label": "大学校园", "color": "#52b5ea" } ], "is_official": false }, { "module_name": "nonebot_plugin_zyk_lightNVL", "project_link": "nonebot-plugin-zyk-lightNVL", "author_id": 110616928, "tags": [ { "label": "轻小说", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_dog", "project_link": "nonebot-plugin-dog", "author_id": 87823528, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_uuid", "project_link": "nonebot-plugin-uuid", "author_id": 60338092, "tags": [ { "label": "工具", "color": "#39c5bb" } ], "is_official": false }, { "module_name": "nonebot_plugin_naturel_gpt", "project_link": "nonebot-plugin-naturel-gpt", "author_id": 57167362, "tags": [ { "label": "GPT3", "color": "#66ccff" }, { "label": "OpenAi", "color": "#cc66ff" } ], "is_official": false }, { "module_name": "nonebot_plugin_impact", "project_link": "nonebot-plugin-impact", "author_id": 87489040, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_mcping", "project_link": "nonebot-plugin-mcping", "author_id": 54731914, "tags": [ { "label": "Minecraft", "color": "#47d754" }, { "label": "服务器状态", "color": "#d7cd47" } ], "is_official": false }, { "module_name": "nonebot_plugin_b23", "project_link": "nonebot-plugin-b23", "author_id": 61458340, "tags": [ { "label": "bilibili", "color": "#00aeec" }, { "label": "热搜", "color": "#00aeec" } ], "is_official": false }, { "module_name": "nonebot_plugin_autoreply", "project_link": "nonebot-plugin-autoreply", "author_id": 59048777, "tags": [ { "label": "自动回复", "color": "#ea881e" } ], "is_official": false }, { "module_name": "nonebot_plugin_setu_collection", "project_link": "nonebot_plugin_setu_collection", "author_id": 51886078, "tags": [ { "label": "LoliconAPI", "color": "#5adba8" }, { "label": "色图", "color": "#7ab2e1" } ], "is_official": false }, { "module_name": "nonebot_plugin_groupmate_waifu", "project_link": "nonebot-plugin-groupmate-waifu", "author_id": 51886078, "tags": [ { "label": "娶群友", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_jrrp2", "project_link": "nonebot-plugin-jrrp2", "author_id": 74699219, "tags": [ { "label": "每日人品", "color": "#ea5252" }, { "label": "jrrp", "color": "#5290ea" }, { "label": "jrrp2", "color": "#52bbea" } ], "is_official": false }, { "module_name": "nonebot_plugin_dicky_pk", "project_link": "nonebot-plugin-dicky-pk", "author_id": 107618388, "tags": [ { "label": "群聊小游戏", "color": "#ffd500" } ], "is_official": false }, { "module_name": "nonebot_plugin_eventmonitor", "project_link": "nonebot-plugin-eventmonitor", "author_id": 87823528, "tags": [ { "label": "QQGroup", "color": "#2885c0" } ], "is_official": false }, { "module_name": "nonebot_plugin_whateat_pic", "project_link": "nonebot-plugin-whateat-pic", "author_id": 106718176, "tags": [ { "label": "吃什么", "color": "#e4ea52" }, { "label": "喝什么", "color": "#52ea8b" } ], "is_official": false }, { "module_name": "nonebot_plugin_matcher_block", "project_link": "nonebot-plugin-matcher-block", "author_id": 51886078, "tags": [ { "label": "指令阻断", "color": "#525fea" } ], "is_official": false }, { "module_name": "nonebot_plugin_acm_reminder", "project_link": "nonebot_plugin_acm_reminder", "author_id": 63897047, "tags": [ { "label": "ACM", "color": "#3b8b74" } ], "is_official": false }, { "module_name": "nonebot_plugin_maimai", "project_link": "nonebot-plugin-maimai", "author_id": 70925546, "tags": [ { "label": "maimai", "color": "#5262ea" }, { "label": "Alconna", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_all4one", "project_link": "nonebot-plugin-all4one", "author_id": 50312681, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_gsabyss", "project_link": "nonebot-plugin-gsabyss", "author_id": 22407052, "tags": [ { "label": "Genshin", "color": "#ffd49f" } ], "is_official": false }, { "module_name": "nonebot_plugin_arktools", "project_link": "nonebot-plugin-arktools", "author_id": 52584526, "tags": [ { "label": "arknights", "color": "#22bbff" }, { "label": "game", "color": "#db905e" } ], "is_official": false }, { "module_name": "gartic_room", "project_link": "nonebot-plugin-gartic-room", "author_id": 47290820, "tags": [ { "label": "ayaka", "color": "#e36306" } ], "is_official": false }, { "module_name": "nonebot-plugin-resolver", "project_link": "nonebot-plugin-resolver", "author_id": 33365787, "tags": [ { "label": "bilibili", "color": "#f8a5c2" }, { "label": "tiktok", "color": "#303952" }, { "label": "twitter", "color": "#1b9cfc" } ], "is_official": false }, { "module_name": "nonebot_plugin_bing_chat", "project_link": "nonebot-plugin-bing-chat", "author_id": 69247286, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_saa", "project_link": "nonebot-plugin-send-anything-anywhere", "author_id": 23295345, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_random_stereotypes", "project_link": "nonebot-plugin-random-stereotypes", "author_id": 40910637, "tags": [ { "label": "语录", "color": "#6a6060" } ], "is_official": false }, { "module_name": "nonebot_plugin_xiuxian_2", "project_link": "nonebot-plugin-xiuxian-2", "author_id": 88731921, "tags": [ { "label": "文游", "color": "#ea5252" }, { "label": "修仙", "color": "#4e9f9f" } ], "is_official": false }, { "module_name": "nonebot_plugin_h2e", "project_link": "nonebot-plugin-h2e", "author_id": 59423752, "tags": [ { "label": "锻炼", "color": "#da4a4a" }, { "label": "what2eat", "color": "#99da4a" }, { "label": " how2exe", "color": "#99da4a" } ], "is_official": false }, { "module_name": "nonebot_plugin_oachat", "project_link": "nonebot-plugin-oachat", "author_id": 59423752, "tags": [ { "label": "OpenAI", "color": "#ea5252" }, { "label": "GPT3", "color": "#ea5252" }, { "label": "ChatBot", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_warframe_mode", "project_link": "nonebot-plugin-warframe-mode", "author_id": 73402119, "tags": [ { "label": "星际战甲", "color": "#ea5252" }, { "label": "warframe", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_bf1_groptools", "project_link": "nonebot-plugin-bf1-groptools", "author_id": 72740993, "tags": [ { "label": "战地一", "color": "#52eae4" } ], "is_official": false }, { "module_name": "nonebot_plugin_afd", "project_link": "nonebot-plugin-afd", "author_id": 54731914, "tags": [ { "label": "爱发电", "color": "#ea5252" }, { "label": "自动审核进群", "color": "#52eae9" } ], "is_official": false }, { "module_name": "nonebot_plugin_eventdone", "project_link": "nonebot-plugin-eventdone", "author_id": 105444165, "tags": [ { "label": "同意好友", "color": "#ba2d2d" } ], "is_official": false }, { "module_name": "nonebot_plugin_ncm_saying", "project_link": "nonebot-plugin-ncm-saying", "author_id": 78636812, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_60s", "project_link": "nonebot-plugin-60s", "author_id": 78636812, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_AutoRepeater", "project_link": "nonebot-plugin-AutoRepeater", "author_id": 59276590, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_ai_timetable", "project_link": "nonebot-plugin-ai-timetable", "author_id": 123555887, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_sanae", "project_link": "nonebot-plugin-sanae", "author_id": 36219542, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_arkrecord", "project_link": "nonebot-plugin-arkrecord", "author_id": 63400477, "tags": [ { "label": "明日方舟 ", "color": "#c39191" }, { "label": "游戏", "color": "#c39191" }, { "label": "抽卡", "color": "#c39191" } ], "is_official": false }, { "module_name": "nonebot_plugin_chatgpt_turbo", "project_link": "nonebot-plugin-chatgpt-turbo", "author_id": 16055526, "tags": [ { "label": "ChatGPT", "color": "#ea5252" }, { "label": "OpenAI", "color": "#52ea92" } ], "is_official": false }, { "module_name": "nonebot_plugin_chatgpt_on_qq", "project_link": "nonebot-plugin-chatgpt-on-qq", "author_id": 33772816, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_tuan_chatgpt", "project_link": "nonebot-plugin-tuan-chatgpt", "author_id": 32624562, "tags": [ { "label": "chat", "color": "#ff9d97" }, { "label": "chatgpt", "color": "#ff9d97" } ], "is_official": false }, { "module_name": "nonebot_plugin_chatpdf", "project_link": "nonebot-plugin-chatpdf", "author_id": 16055526, "tags": [ { "label": "ChatGPT", "color": "#ea5252" }, { "label": "ChatPDF", "color": "#6c7abd" } ], "is_official": false }, { "module_name": "nonebot_plugin_rimofun", "project_link": "nonebot-plugin-rimofun", "author_id": 59048777, "tags": [ { "label": "RimoChan", "color": "#f3e5bf" }, { "label": "bnhhsh", "color": "#ebbcc6" } ], "is_official": false }, { "module_name": "nonebot_plugin_customemote", "project_link": "nonebot-plugin-customemote", "author_id": 59276590, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_justsix", "project_link": "nonebot-plugin-justsix", "author_id": 127737368, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_simulator_xiuxian", "project_link": "nonebot-plugin-simulator-xiuxian", "author_id": 127736993, "tags": [ { "label": "文游", "color": "#4256da" }, { "label": "修仙1.0", "color": "#1d1b1c" } ], "is_official": false }, { "module_name": "nonebot_plugin_bracket", "project_link": "nonebot-plugin-bracket", "author_id": 33149974, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_gshisbanner", "project_link": "nonebot-plugin-gshisbanner", "author_id": 100580891, "tags": [ { "label": "原神", "color": "#ea5252" }, { "label": "卡池", "color": "#52ea56" } ], "is_official": false }, { "module_name": "nonebot_plugin_unoconv", "project_link": "nonebot-plugin-unoconv", "author_id": 57753690, "tags": [ { "label": "文件转换", "color": "#ea5252" }, { "label": "pdf转换", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_apexranklookup", "project_link": "nonebot-plugin-apexranklookup", "author_id": 22563214, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_randomnana", "project_link": "nonebot-plugin-randomnana", "author_id": 56375835, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_quote", "project_link": "nonebot-plugin-quote", "author_id": 32476024, "tags": [ { "label": "语录", "color": "#003f88" } ], "is_official": false }, { "module_name": "nonebot_plugin_memes_api", "project_link": "nonebot_plugin_memes_api", "author_id": 33149974, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_rrc", "project_link": "nonebot-plugin-rrc", "author_id": 88731921, "tags": [ { "label": "课堂", "color": "#ea5252" }, { "label": "抽人", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_hotsearch", "project_link": "nonebot-plugin-hotsearch", "author_id": 84057953, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_ai_interviewer", "project_link": "nonebot-plugin-ai-interviewer", "author_id": 16055526, "tags": [ { "label": "ChatGPT", "color": "#4366eb" }, { "label": "模拟面试", "color": "#af286f" } ], "is_official": false }, { "module_name": "nonebot_plugin_chatglm", "project_link": "nonebot-plugin-chatglm", "author_id": 34794409, "tags": [ { "label": "Chatbot", "color": "#4366eb" }, { "label": "ChatGLM", "color": "#af286f" } ], "is_official": false }, { "module_name": "nonebot_plugin_chatglm6b", "project_link": "nonebot-plugin-chatglm6b", "author_id": 117292352, "tags": [ { "label": "ChatGLM", "color": "#52d6ea" }, { "label": "AI Chat", "color": "#8e52ea" } ], "is_official": false }, { "module_name": "nonebot_plugin_helloworld", "project_link": "nonebot-plugin-helloworld", "author_id": 66513481, "tags": [ { "label": "good first plugin", "color": "#6c58f6" } ], "is_official": false }, { "module_name": "nonebot_plugin_overbracket", "project_link": "nonebot-plugin-overbracket", "author_id": 37037264, "tags": [ { "label": "useless", "color": "#0a930e" } ], "is_official": false }, { "module_name": "nonebot_plugin_miao", "project_link": "nonebot-plugin-miao", "author_id": 63870437, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_questionmark", "project_link": "nonebot-plugin-questionmark", "author_id": 52584526, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_genshin_cos", "project_link": "nonebot-plugin-genshin-cos", "author_id": 106718176, "tags": [ { "label": "原神", "color": "#f55400" }, { "label": "cos", "color": "#00d0f5" }, { "label": "coser", "color": "#eb1dd3" } ], "is_official": false }, { "module_name": "nonebot_plugin_chatgpt_plus", "project_link": "nonebot-plugin-chatgpt-plus", "author_id": 55268546, "tags": [ { "label": "ChatGPT", "color": "#ea5252" }, { "label": "GPT4", "color": "#2ecf57" } ], "is_official": false }, { "module_name": "nonebot_plugin_sayoroll", "project_link": "nonebot-plugin-sayoroll", "author_id": 52590027, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_gw2", "project_link": "nonebot-plugin-gw2", "author_id": 70925546, "tags": [ { "label": "gw2", "color": "#52ea5a" } ], "is_official": false }, { "module_name": "nonebot_plugin_today_waifu", "project_link": "nonebot-plugin-today-waifu", "author_id": 129576887, "tags": [ { "label": "娱乐", "color": "#eac752" }, { "label": "每日老婆", "color": "#9beff6" } ], "is_official": false }, { "module_name": "nonebot_plugin_sleep", "project_link": "nonebot-plugin-sleep", "author_id": 52590027, "tags": [], "is_official": false }, { "module_name": "nonebot_api_paddle", "project_link": "nonebot-api-paddle", "author_id": 69547456, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_chatppt", "project_link": "nonebot-plugin-chatppt", "author_id": 16055526, "tags": [ { "label": "ChatGPT", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_steam_game_status", "project_link": "nonebot-plugin-steam-game-status", "author_id": 57703506, "tags": [ { "label": "Steam", "color": "#6690a5" } ], "is_official": false }, { "module_name": "nonebot_plugin_bilichat", "project_link": "nonebot-plugin-bilichat", "author_id": 40534114, "tags": [ { "label": "哔哩哔哩", "color": "#ffc8ea" }, { "label": "ChatGPT", "color": "#75ffc0" } ], "is_official": false }, { "module_name": "GenshinUID", "project_link": "nonebot-plugin-genshinuid", "author_id": 55526518, "tags": [ { "label": "原神", "color": "#000000" }, { "label": "早柚核心", "color": "#7937a9" } ], "is_official": false }, { "module_name": "nonebot_plugin_blive_danmaku", "project_link": "nonebot-plugin-blive-danmaku", "author_id": 14540861, "tags": [ { "label": "bilibili", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_clock", "project_link": "nonebot-plugin-clock", "author_id": 57753690, "tags": [ { "label": "闹钟", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_SDGPT", "project_link": "nonebot-plugin-sdgpt", "author_id": 52259890, "tags": [ { "label": "chatGPT", "color": "#54b490" }, { "label": "novelai", "color": "#f0dc4e" } ], "is_official": false }, { "module_name": "nonebot_plugin_fuckyou", "project_link": "nonebot-plugin-fuckyou", "author_id": 59048777, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_pokemonfusion", "project_link": "nonebot-plugin-pokemonfusion", "author_id": 112180508, "tags": [], "is_official": false }, { "module_name": "tatarubot2", "project_link": "tatarubot2", "author_id": 44492123, "tags": [ { "label": "FF14", "color": "#5282ea" } ], "is_official": false }, { "module_name": "nonebot_plugin_osuverify", "project_link": "nonebot-plugin-osuverify", "author_id": 52590027, "tags": [ { "label": "OSU", "color": "#eb5d9b" } ], "is_official": false }, { "module_name": "nonebot_plugin_bilifan", "project_link": "nonebot-plugin-bilifan", "author_id": 70925546, "tags": [ { "label": "bilibili", "color": "#ec15c6" }, { "label": "Alconna", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_akinator", "project_link": "nonebot-plugin-akinator", "author_id": 59048777, "tags": [ { "label": "Akinator", "color": "#6599fe" }, { "label": "网络天才", "color": "#6599fe" } ], "is_official": false }, { "module_name": "nonebot_plugin_rename", "project_link": "nonebot-plugin-rename", "author_id": 100580891, "tags": [ { "label": "群名片", "color": "#466bed" } ], "is_official": false }, { "module_name": "nonebot_plugin_appinsights", "project_link": "nonebot-plugin-appinsights", "author_id": 41534161, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_brainfuck", "project_link": "nonebot-plugin-brainfuck", "author_id": 19896796, "tags": [ { "label": "dev", "color": "#ea5252" }, { "label": "bf", "color": "#529fea" } ], "is_official": false }, { "module_name": "nonebot-plugin-coderun", "project_link": "nonebot-plugin-coderun", "author_id": 91423824, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_abot_place", "project_link": "nonebot-plugin-abot-place", "author_id": 59153990, "tags": [ { "label": "ABot", "color": "#52aaea" }, { "label": "Place", "color": "#ea6dda" } ], "is_official": false }, { "module_name": "nonebot_plugin_megumin", "project_link": "nonebot-plugin-megumin", "author_id": 99666950, "tags": [ { "label": "小游戏", "color": "#f9971c" }, { "label": "Explosion", "color": "#f94b1c" } ], "is_official": false }, { "module_name": "nonebot_plugin_manga_translator", "project_link": "nonebot-plugin-manga-translator", "author_id": 123555887, "tags": [ { "label": "漫画翻译", "color": "#527bea" }, { "label": "图片翻译", "color": "#c452ea" } ], "is_official": false }, { "module_name": "nonebot_plugin_watermarker", "project_link": "nonebot-plugin-watermarker", "author_id": 91937041, "tags": [ { "label": "水印", "color": "#52eae7" }, { "label": "图片水印", "color": "#ef258f" } ], "is_official": false }, { "module_name": "nonebot_plugin_starrail_calendar", "project_link": "nonebot-plugin-starrail-calendar", "author_id": 17716585, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_wordle_help", "project_link": "nonebot-plugin-wordle-help", "author_id": 87489040, "tags": [], "is_official": false }, { "module_name": "nonebot-plugin-csgo-case-simulator", "project_link": "nonebot-plugin-csgo-case-simulator", "author_id": 11494827, "tags": [ { "label": "CSGO", "color": "#47aeff" }, { "label": "开箱模拟器", "color": "#ff4781" } ], "is_official": false }, { "module_name": "nonebot_plugin_callapi", "project_link": "nonebot-plugin-callapi", "author_id": 59048777, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_penguin", "project_link": "nonebot-plugin-penguin", "author_id": 57004769, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_spark_gpt", "project_link": "nonebot-plugin-spark-gpt", "author_id": 69547456, "tags": [ { "label": "多来源语言GPT", "color": "#5599ff" }, { "label": "多平台用户数据互通", "color": "#ffff77" } ], "is_official": false }, { "module_name": "nonebot_plugin_logpile", "project_link": "nonebot-plugin-logpile", "author_id": 66513481, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_arkgacha", "project_link": "nonebot-plugin-arkgacha", "author_id": 42648639, "tags": [ { "label": "game", "color": "#eaa852" }, { "label": "arknights", "color": "#5276ea" }, { "label": "抽卡", "color": "#c852ea" } ], "is_official": false }, { "module_name": "nonebot_plugin_og", "project_link": "nonebot-plugin-og", "author_id": 110453675, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_hoshino_sign", "project_link": "nonebot-plugin-hoshino-sign", "author_id": 66541860, "tags": [ { "label": "PCR", "color": "#ff0000" }, { "label": "签到", "color": "#008000" } ], "is_official": false }, { "module_name": "nonebot_plugin_multincm", "project_link": "nonebot-plugin-multincm", "author_id": 59048777, "tags": [ { "label": "网易云", "color": "#ea5252" }, { "label": "ncm", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_smallapi", "project_link": "nonebot-plugin-smallapi", "author_id": 91947491, "tags": [ { "label": "webapi", "color": "#52ea7d" }, { "label": "api", "color": "#52ea7d" } ], "is_official": false }, { "module_name": "nonebot_plugin_p5generator", "project_link": "nonebot-plugin-p5generator", "author_id": 58218656, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_sd_webui", "project_link": "nonebot-plugin-sd-webui", "author_id": 49489433, "tags": [ { "label": "sd", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_session", "project_link": "nonebot_plugin_session", "author_id": 33149974, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_pluginupdatecheck", "project_link": "nonebot-plugin-pluginupdatecheck", "author_id": 58218656, "tags": [ { "label": "便携安装", "color": "#e42828" } ], "is_official": false }, { "module_name": "nonebot_plugin_stockhelper", "project_link": "nonebot-plugin-stockhelper", "author_id": 77315378, "tags": [ { "label": "股票", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_githubcard", "project_link": "nonebot-plugin-githubcard", "author_id": 56375835, "tags": [ { "label": " Github", "color": "#171a21" } ], "is_official": false }, { "module_name": "nonebot_plugin_lua", "project_link": "nonebot-plugin-lua", "author_id": 50922489, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_cube", "project_link": "nonebot-plugin-cube", "author_id": 109729945, "tags": [ { "label": "魔方", "color": "#ea5252" }, { "label": "sqlite3", "color": "#c2ea52" } ], "is_official": false }, { "module_name": "nonebot_plugin_homo_mathematician", "project_link": "nonebot-plugin-homo-mathematician", "author_id": 87489040, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_puzzle", "project_link": "nonebot-plugin-puzzle", "author_id": 109729945, "tags": [ { "label": "数字华容道", "color": "#ea5252" }, { "label": "拼图游戏", "color": "#a7ea52" }, { "label": "puzzle", "color": "#52eadd" } ], "is_official": false }, { "module_name": "nonebot_plugin_herocard", "project_link": "nonebot-plugin-herocard", "author_id": 41467241, "tags": [ { "label": "文本提取", "color": "#6bc6bf" }, { "label": "日语", "color": "#53bbd8" }, { "label": "本子", "color": "#eea1b1" } ], "is_official": false }, { "module_name": "nonebot_plugin_nagabus", "project_link": "nonebot-plugin-nagabus", "author_id": 17331698, "tags": [ { "label": "日麻", "color": "#b52ee1" } ], "is_official": false }, { "module_name": "nonebot_plugin_random_draw", "project_link": "nonebot-plugin-random-draw", "author_id": 40910637, "tags": [ { "label": "随机", "color": "#ffe0aa" } ], "is_official": false }, { "module_name": "nonebot_plugin_stable_diffusion_diao", "project_link": "nonebot-plugin-stable-diffusion-diao", "author_id": 126318917, "tags": [ { "label": "AI绘图", "color": "#eaaf52" }, { "label": "SD", "color": "#eaaf52" } ], "is_official": false }, { "module_name": "nonebot_plugin_escape_url", "project_link": "nonebot-plugin-escape-url", "author_id": 17331698, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_twitter", "project_link": "nonebot-plugin-twitter", "author_id": 57703506, "tags": [ { "label": "twitter", "color": "#29a8dc" } ], "is_official": false }, { "module_name": "nonebot_plugin_pcrjjc", "project_link": "nonebot-plugin-pcrjjc", "author_id": 46278371, "tags": [ { "label": "公主连结", "color": "#778a1e" }, { "label": "pcrjjc", "color": "#778a1e" } ], "is_official": false }, { "module_name": "nonebot_plugin_audiocraft", "project_link": "nonebot-plugin-audiocraft", "author_id": 16055526, "tags": [ { "label": "AI", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_warthunder_player_check", "project_link": "nonebot-plugin-warthunder-player-check", "author_id": 110895144, "tags": [ { "label": "Wathunder", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_follow_withdraw", "project_link": "nonebot-plugin-follow-withdraw", "author_id": 63870437, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_ocgbot_v2", "project_link": "nonebot-plugin-ocgbot-v2", "author_id": 76525116, "tags": [ { "label": "游戏王", "color": "#2ecbed" }, { "label": "ygo", "color": "#f77117" } ], "is_official": false }, { "module_name": "nonebot_plugin_helltide", "project_link": "nonebot-plugin-helltide", "author_id": 2779686, "tags": [ { "label": "helltide", "color": "#ff0000" }, { "label": "diablo4", "color": "#ff3300" } ], "is_official": false }, { "module_name": "nonebot_plugin_userinfo", "project_link": "nonebot_plugin_userinfo", "author_id": 33149974, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_poke", "project_link": "nonebot-plugin-poke", "author_id": 70925546, "tags": [ { "label": "v11", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_friends", "project_link": "nonebot-plugin-friends", "author_id": 70925546, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_update", "project_link": "nonebot-plugin-update", "author_id": 59153990, "tags": [ { "label": "Nonebot", "color": "#ea5252" }, { "label": "tool", "color": "#527dea" } ], "is_official": false }, { "module_name": "nonebot_plugin_cp_broadcast", "project_link": "nonebot-plugin-cp-broadcast", "author_id": 103566602, "tags": [ { "label": "算法竞赛", "color": "#ea5252" }, { "label": "比赛播报", "color": "#55ea52" } ], "is_official": false }, { "module_name": "nonebot_plugin_cfassistant", "project_link": "nonebot-plugin-cfassistant", "author_id": 24607145, "tags": [ { "label": "ACM", "color": "#52EACF" }, { "label": "Codeforces", "color": "#52EACF" } ], "is_official": false }, { "module_name": "nonebot_plugin_splatoon3_schedule", "project_link": "nonebot-plugin-splatoon3-schedule", "author_id": 53274578, "tags": [ { "label": "splatoon3", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_capoo", "project_link": "nonebot-plugin-capoo", "author_id": 103566602, "tags": [ { "label": "猫猫虫咖波", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_eventexpiry", "project_link": "nonebot-plugin-eventexpiry", "author_id": 66513481, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_nobahpicture", "project_link": "nonebot-plugin-nobahpicture", "author_id": 54620759, "tags": [ { "label": "碧蓝档案", "color": "#00defe" }, { "label": "蔚蓝档案", "color": "#00defe" }, { "label": "涩图", "color": "#00defe" } ], "is_official": false }, { "module_name": "nonebot_plugin_blocker", "project_link": "nonebot-plugin-blocker", "author_id": 41883458, "tags": [ { "label": "Blocker", "color": "#39c5bb" } ], "is_official": false }, { "module_name": "nonebot_plugin_picture_api", "project_link": "nonebot-plugin-picture-api", "author_id": 57926506, "tags": [ { "label": "图片api", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_wenan", "project_link": "nonebot-plugin-wenan", "author_id": 57926506, "tags": [ { "label": "文案api", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_mongodb", "project_link": "nonebot-plugin-mongodb", "author_id": 40534114, "tags": [ { "label": "mongodb", "color": "#30a340" }, { "label": "beanie", "color": "#ff0a0a" } ], "is_official": false }, { "module_name": "nonebot_plugin_templates", "project_link": "nonebot-plugin-templates", "author_id": 69547456, "tags": [ { "label": "模板渲染", "color": "#eacd52" }, { "label": "图片生成", "color": "#adea52" } ], "is_official": false }, { "module_name": "nonebot_plugin_pokesomeone", "project_link": "nonebot-plugin-pokesomeone", "author_id": 69547456, "tags": [ { "label": "戳一戳", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_dall-e", "project_link": "nonebot-plugin-dall-e", "author_id": 102890277, "tags": [ { "label": "DALL-E", "color": "#ea5252" }, { "label": "AI画图", "color": "#52ea61" } ], "is_official": false }, { "module_name": "nonebot_plugin_tempfile", "project_link": "nonebot-plugin-tempfile", "author_id": 37037264, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_disconnect_notice", "project_link": "nonebot-plugin-disconnect-notice", "author_id": 53274578, "tags": [ { "label": "掉线通知", "color": "#ea5252" }, { "label": "邮件", "color": "#52eaa4" }, { "label": "通知", "color": "#cbea52" } ], "is_official": false }, { "module_name": "nonebot_plugin_BotMailNotice", "project_link": "nonebot-plugin-botmailnotice", "author_id": 78628186, "tags": [ { "label": "Mail", "color": "#52e5ea" } ], "is_official": false }, { "module_name": "nonebot_plugin_theworld", "project_link": "nonebot-plugin-theworld", "author_id": 66513481, "tags": [ { "label": "JOJO", "color": "#75147c" }, { "label": "DIO", "color": "#f9d849" } ], "is_official": false }, { "module_name": "nonebot_plugin_nonememe", "project_link": "nonebot-plugin-nonememe", "author_id": 59048777, "tags": [ { "label": "NoneMeme", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_aujob", "project_link": "nonebot-plugin-aujob", "author_id": 110767171, "tags": [ { "label": "among us", "color": "#48d5bf" }, { "label": "TOH", "color": "#05c4f2" } ], "is_official": false }, { "module_name": "nonebot_plugin_bind", "project_link": "nonebot-plugin-bind", "author_id": 69547456, "tags": [ { "label": "跨平台", "color": "#5289ea" }, { "label": "账户绑定", "color": "#6eb428" } ], "is_official": false }, { "module_name": "nonebot_plugin_savepic", "project_link": "nonebot-plugin-savepic", "author_id": 32036413, "tags": [ { "label": "表情包", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_lostark_wandering_trader", "project_link": "nonebot-plugin-lostark-wandering-trader", "author_id": 11713728, "tags": [ { "label": "命运方舟", "color": "#5289ea" } ], "is_official": false }, { "module_name": "nonebot_plugin_csornament", "project_link": "nonebot-plugin-csornament", "author_id": 129657153, "tags": [ { "label": "CS:GO", "color": "#047b97" } ], "is_official": false }, { "module_name": "nonebot_plugin_mcversion", "project_link": "nonebot-plugin-mcversion", "author_id": 110646806, "tags": [ { "label": "版本", "color": "#ea5252" }, { "label": "MC", "color": "#52e7ea" } ], "is_official": false }, { "module_name": "nonebot_plugin_muteme", "project_link": "nonebot-plugin-muteme", "author_id": 96647974, "tags": [ { "label": "禁言", "color": "#e45b8d" }, { "label": "muteme", "color": "#5bc2e4" } ], "is_official": false }, { "module_name": "nonebot_plugin_jx3", "project_link": "nonebot-plugin-jx3", "author_id": 61312850, "tags": [ { "label": "剑网三", "color": "#ea5252" }, { "label": "jx3", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_xinghuo_api", "project_link": "nonebot-plugin-xinghuo-api", "author_id": 16055526, "tags": [ { "label": "AI", "color": "#ea5252" }, { "label": "Chat", "color": "#ea5252" } ], "is_official": false }, { "module_name": "dicergirl", "project_link": "dicergirl", "author_id": 46275354, "tags": [ { "label": "跑团", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_uvdiviner", "project_link": "nonebot-plugin-uvdiviner", "author_id": 46275354, "tags": [ { "label": "占卜", "color": "#440e0e" } ], "is_official": false }, { "module_name": "nonebot_plugin_push", "project_link": "nonebot-plugin-push", "author_id": 44370805, "tags": [ { "label": "邮件", "color": "#91c0bd" }, { "label": "飞书", "color": "#7aaccc" }, { "label": "推送", "color": "#9e97c4" } ], "is_official": false }, { "module_name": "nonebot_plugin_sudo", "project_link": "nonebot-plugin-sudo", "author_id": 104149371, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_arxivRSS", "project_link": "nonebot-plugin-arxivRSS", "author_id": 83060644, "tags": [ { "label": "arxiv", "color": "#f30f0f" }, { "label": "RSS", "color": "#70a7d8" }, { "label": "论文", "color": "#f541dc" } ], "is_official": false }, { "module_name": "nonebot_plugin_anime_trace", "project_link": "nonebot-plugin-anime-trace", "author_id": 53679884, "tags": [ { "label": "AI", "color": "#f541dc" } ], "is_official": false }, { "module_name": "nonebot_plugin_send_message", "project_link": "nonebot-plugin-send-message", "author_id": 99388013, "tags": [ { "label": "传话", "color": "#52ea75" } ], "is_official": false }, { "module_name": "nonebot_plugin_requests", "project_link": "nonebot-plugin-requests", "author_id": 41713304, "tags": [ { "label": "requests", "color": "#80bcc2" } ], "is_official": false }, { "module_name": "nonebot_plugin_simple_httpcat", "project_link": "nonebot-plugin-simple-httpcat", "author_id": 96647974, "tags": [ { "label": "httpcat", "color": "#5dbfc8" } ], "is_official": false }, { "module_name": "nonebot_plugin_qrcode2", "project_link": "nonebot-plugin-qrcode2", "author_id": 53679884, "tags": [ { "label": "qrcode", "color": "#f541dc" } ], "is_official": false }, { "module_name": "nonebot_plugin_fakemsg", "project_link": "nonebot-plugin-fakemsg", "author_id": 106718176, "tags": [ { "label": "合并转发", "color": "#1ae32c" } ], "is_official": false }, { "module_name": "nonebot_plugin_alchelper", "project_link": "nonebot-plugin-alchelper", "author_id": 42648639, "tags": [ { "label": "Alconna", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_souti", "project_link": "nonebot-plugin-souti", "author_id": 124417044, "tags": [ { "label": "搜题", "color": "#49e3d5" } ], "is_official": false }, { "module_name": "nonebot_plugin_helper_plus", "project_link": "nonebot-plugin-helper-plus", "author_id": 83159422, "tags": [ { "label": "帮助", "color": "#eae152" }, { "label": "命令控制", "color": "#52ea5c" } ], "is_official": false }, { "module_name": "nonebot_plugin_wearskirt", "project_link": "nonebot-plugin-wearskirt", "author_id": 74490140, "tags": [ { "label": "女装", "color": "#ffc0cb" } ], "is_official": false }, { "module_name": "nonebot_plugin_skland_arksign", "project_link": "nonebot-plugin-skland-arksign", "author_id": 44727214, "tags": [ { "label": "森空岛", "color": "#c8eb21" }, { "label": "明日方舟", "color": "#111111" }, { "label": "签到", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_group_whitelist", "project_link": "nonebot-plugin-group-whitelist", "author_id": 121035454, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_ernie", "project_link": "nonebot-plugin-ernie", "author_id": 39023047, "tags": [ { "label": "文心一言", "color": "#2e317c" } ], "is_official": false }, { "module_name": "TeenStudy", "project_link": "TeenStudy", "author_id": 143619319, "tags": [ { "label": "青年大学习", "color": "#7aea52" }, { "label": "Web UI", "color": "#52eaea" }, { "label": "多地区", "color": "#dfea52" } ], "is_official": false }, { "module_name": "nonebot_plugin_video_api", "project_link": "nonebot-plugin-video-api", "author_id": 57926506, "tags": [ { "label": "视频api", "color": "#14d8de" } ], "is_official": false }, { "module_name": "nonebot_plugin_ottohzys", "project_link": "nonebot-plugin-ottohzys", "author_id": 59048777, "tags": [ { "label": "otto", "color": "#00a6ed" }, { "label": "电棍", "color": "#00a6ed" }, { "label": "活字印刷", "color": "#00a6ed" } ], "is_official": false }, { "module_name": "nonebot_plugin_ability", "project_link": "nonebot-plugin-ability", "author_id": 110453675, "tags": [ { "label": "🔋", "color": "#8bff00" } ], "is_official": false }, { "module_name": "nonebot_plugin_agent", "project_link": "nonebot-plugin-agent", "author_id": 56953648, "tags": [ { "label": "agent", "color": "#99ccff" } ], "is_official": false }, { "module_name": "nonebot_plugin_blockwords", "project_link": "nonebot-plugin-blockwords", "author_id": 50488999, "tags": [ { "label": "word", "color": "#99ccff" }, { "label": "屏蔽词", "color": "#ffcc99" }, { "label": "blockwords", "color": "#ffcc99" } ], "is_official": false }, { "module_name": "nonebot_plugin_matchreminder", "project_link": "nonebot-plugin-matchreminder", "author_id": 135496272, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_eop_ai", "project_link": "nonebot_plugin_eop_ai", "author_id": 31379266, "tags": [ { "label": "POE", "color": "#bf40bf" }, { "label": "GPT", "color": "#aaff00" }, { "label": "AI", "color": "#fa5f55" } ], "is_official": false }, { "module_name": "nonebot_plugin_playercheck", "project_link": "nonebot-plugin-playercheck", "author_id": 38505121, "tags": [ { "label": "音游", "color": "#ec623c" }, { "label": "成分", "color": "#ef5ec4" } ], "is_official": false }, { "module_name": "nonebot_plugin_op_finder", "project_link": "nonebot-plugin-op-finder", "author_id": 29861280, "tags": [ { "label": "原神", "color": "#00a0ea" } ], "is_official": false }, { "module_name": "nonebot_plugin_morep_finder", "project_link": "nonebot-plugin-morep-finder", "author_id": 29861280, "tags": [], "is_official": false }, { "module_name": "nonebot-plugin-yesman", "project_link": "nonebot-plugin-yesman", "author_id": 89695226, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_fgoavatarguess", "project_link": "nonebot-plugin-fgoavatarguess", "author_id": 114396889, "tags": [ { "label": "FGO", "color": "#66cccc" } ], "is_official": false }, { "module_name": "nonebot_plugin_vrchat", "project_link": "nonebot-plugin-vrchat", "author_id": 70925546, "tags": [ { "label": "VRChat", "color": "#7aea53" }, { "label": "Alconna", "color": "#ea5252" }, { "label": "i18n", "color": "#5452ea" } ], "is_official": false }, { "module_name": "nonebot_plugin_batitle", "project_link": "nonebot-plugin-batitle", "author_id": 41883458, "tags": [ { "label": "碧蓝档案", "color": "#00d7fb" } ], "is_official": false }, { "module_name": "nonebot_plugin_maimaidx", "project_link": "nonebot-plugin-maimaidx", "author_id": 65105396, "tags": [ { "label": "maimai", "color": "#cea6ff" } ], "is_official": false }, { "module_name": "nonebot_plugin_getbapics", "project_link": "nonebot_plugin_getbapics", "author_id": 144128876, "tags": [ { "label": "碧蓝档案", "color": "#00defe" }, { "label": "BA", "color": "#00defe" } ], "is_official": false }, { "module_name": "nonebot_plugin_make_choice", "project_link": "nonebot-plugin-make-choice", "author_id": 57581480, "tags": [ { "label": "choice", "color": "#77c5ff" } ], "is_official": false }, { "module_name": "nonebot_plugin_scheduled_broadcast", "project_link": "nonebot-plugin-scheduled-broadcast", "author_id": 33901373, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_any", "project_link": "nonebot-plugin-any", "author_id": 81250368, "tags": [ { "label": "跨平台", "color": "#36d399" }, { "label": "统一", "color": "#fa5f55" }, { "label": "工具库", "color": "#52eadd" } ], "is_official": false }, { "module_name": "nonebot_plugin_bertvits2", "project_link": "nonebot-plugin-bertvits2", "author_id": 52267304, "tags": [ { "label": "语音合成", "color": "#9bf6ff" } ], "is_official": false }, { "module_name": "nonebot_plugin_wake_on_lan", "project_link": "nonebot-plugin-wake-on-lan", "author_id": 52388789, "tags": [ { "label": "局域网唤醒", "color": "#1e90ff" }, { "label": "WOL", "color": "#4682b4" } ], "is_official": false }, { "module_name": "nonebot_plugin_bingimagecreator", "project_link": "nonebot-plugin-bingimagecreator", "author_id": 16055526, "tags": [ { "label": "AI", "color": "#ea5252" }, { "label": "绘图", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_user", "project_link": "nonebot-plugin-user", "author_id": 5219550, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_shorturl", "project_link": "nonebot-plugin-shorturl", "author_id": 14922941, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_filehost", "project_link": "nonebot-plugin-filehost", "author_id": 32300164, "tags": [], "is_official": true }, { "module_name": "nonebot_plugin_smms", "project_link": "nonebot-plugin-smms", "author_id": 44370805, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_nsfw", "project_link": "nonebot-plugin-nsfw", "author_id": 48091591, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_batarot", "project_link": "nonebot-plugin-batarot", "author_id": 143330850, "tags": [ { "label": "碧蓝档案", "color": "#15a0df" } ], "is_official": false }, { "module_name": "nonebot_plugin_longtu", "project_link": "nonebot-plugin-longtu", "author_id": 143330850, "tags": [ { "label": "龙图", "color": "#e1122d" } ], "is_official": false }, { "module_name": "nonebot_plugin_phigros", "project_link": "nonebot-plugin-phigros", "author_id": 96647974, "tags": [ { "label": "Phigros", "color": "#b0339a" } ], "is_official": false }, { "module_name": "nonebot_plugin_antimonkey", "project_link": "nonebot-plugin-antimonkey", "author_id": 131594704, "tags": [ { "label": "猴子", "color": "#ea5252" }, { "label": "群管", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_waiter", "project_link": "nonebot-plugin-waiter", "author_id": 42648639, "tags": [ { "label": "waiter", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_imagemaster", "project_link": "nonebot-plugin-imagemaster", "author_id": 131594704, "tags": [ { "label": "修图", "color": "#52d4ea" } ], "is_official": false }, { "module_name": "nonebot_plugin_nikke", "project_link": "nonebot-plugin-nikke", "author_id": 143330850, "tags": [ { "label": "nikke", "color": "#e111b7" } ], "is_official": false }, { "module_name": "nonebot_plugin_mcpic", "project_link": "nonebot-plugin-mcpic", "author_id": 77601125, "tags": [ { "label": "Minecraft", "color": "#3cb371" }, { "label": "MC", "color": "#3cb371" }, { "label": "图片", "color": "#3cb371" } ], "is_official": false }, { "module_name": "nonebot_plugin_wx4", "project_link": "nonebot-plugin-wx4", "author_id": 72342321, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_mypower", "project_link": "nonebot-plugin-mypower", "author_id": 118712549, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_bard", "project_link": "nonebot-plugin-bard", "author_id": 16055526, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_nekoimage", "project_link": "nonebot-plugin-nekoimage", "author_id": 114645197, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_finallines", "project_link": "nonebot-plugin-finallines", "author_id": 143330850, "tags": [ { "label": "最终台词", "color": "#052199" } ], "is_official": false }, { "module_name": "nonebot_plugin_gemini", "project_link": "nonebot-plugin-gemini", "author_id": 55650833, "tags": [ { "label": "Gemini", "color": "#3e8ffb" } ], "is_official": false }, { "module_name": "haruka_bot_red", "project_link": "haruka_bot_red", "author_id": 80164656, "tags": [ { "label": "bilibili", "color": "#c83f3f" } ], "is_official": false }, { "module_name": "nonebot_plugin_enatfrp", "project_link": "nonebot-plugin-enatfrp", "author_id": 61458340, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_nezha", "project_link": "nonebot-plugin-nezha", "author_id": 61458340, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_randpic", "project_link": "nonebot-plugin-randpic", "author_id": 103566602, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_BAdrawcard", "project_link": "nonebot-plugin-badrawcard", "author_id": 117559987, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_gpt", "project_link": "nonebot-plugin-gpt", "author_id": 57703506, "tags": [ { "label": "ChatGPT", "color": "#50ec9d" } ], "is_official": false }, { "module_name": "nonebot_plugin_easy_blacklist", "project_link": "nonebot-plugin-easy-blacklist", "author_id": 99388013, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_reminder", "project_link": "nonebot-plugin-reminder", "author_id": 38395332, "tags": [ { "label": "scheduler", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_chikari_yinpa", "project_link": "nonebot-plugin-chikari-yinpa", "author_id": 121878042, "tags": [ { "label": "yinpa", "color": "#ffff00" } ], "is_official": false }, { "module_name": "nonebot_plugin_splatoon3_nso", "project_link": "nonebot-plugin-splatoon3-nso", "author_id": 53274578, "tags": [ { "label": "splatoon3", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_bf1marneserverlist", "project_link": "nonebot-plugin-bf1marneserverlist", "author_id": 79032826, "tags": [ { "label": "server", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_kawaii_status", "project_link": "nonebot-plugin-kawaii-status", "author_id": 110453675, "tags": [ { "label": "简约", "color": "#54adff" }, { "label": "可爱", "color": "#ffb3cc" } ], "is_official": false }, { "module_name": "nonebot_plugin_vits_tts", "project_link": "nonebot-plugin-vits-tts", "author_id": 109732988, "tags": [ { "label": "VITS", "color": "#ea5252" }, { "label": "TTS", "color": "#52dbea" } ], "is_official": false }, { "module_name": "nonebot_plugin_chatglm_plus", "project_link": "nonebot-plugin-chatglm-plus", "author_id": 96647974, "tags": [ { "label": "ChatGLM", "color": "#73cccc" } ], "is_official": false }, { "module_name": "nonebot_plugin_fishing", "project_link": "nonebot-plugin-fishing", "author_id": 160833462, "tags": [ { "label": "钓鱼", "color": "#87cefa" } ], "is_official": false }, { "module_name": "nonebot_plugin_a2s_query", "project_link": "nonebot-plugin-a2s-query", "author_id": 81429435, "tags": [ { "label": "游戏服务器", "color": "#ea5252" }, { "label": "value", "color": "#99ea52" } ], "is_official": false }, { "module_name": "nonebot_plugin_dice_narrator", "project_link": "nonebot-plugin-dice-narrator", "author_id": 57167362, "tags": [ { "label": "GPT", "color": "#29b752" }, { "label": "掷骰姬", "color": "#c84b9d" } ], "is_official": false }, { "module_name": "nonebot_plugin_steam_info", "project_link": "nonebot-plugin-steam-info", "author_id": 55650833, "tags": [ { "label": "Steam", "color": "#14305e" } ], "is_official": false }, { "module_name": "nonebot_plugin_orangejuice", "project_link": "nonebot-plugin-orangejuice", "author_id": 49135577, "tags": [ { "label": "百橙", "color": "#ed6f00" }, { "label": "100OJ", "color": "#ed6f00" } ], "is_official": false }, { "module_name": "nonebot_plugin_md", "project_link": "nonebot_plugin_md", "author_id": 70925546, "tags": [ { "label": "muse dash", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_duel", "project_link": "nonebot-plugin-duel", "author_id": 109732988, "tags": [ { "label": "决斗", "color": "#5be7d1" } ], "is_official": false }, { "module_name": "nonebot_plugin_pallas_repeater", "project_link": "nonebot-plugin-pallas-repeater", "author_id": 109732988, "tags": [ { "label": "复读鸡", "color": "#52eae7" } ], "is_official": false }, { "module_name": "nonebot_plugin_humanaticstore", "project_link": "nonebot-plugin-humanaticstore", "author_id": 160235071, "tags": [ { "label": "config", "color": "#ea5252" }, { "label": "配置工具", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_ghtiles", "project_link": "nonebot-plugin-ghtiles", "author_id": 18106422, "tags": [ { "label": "Github", "color": "#2a2a2a" } ], "is_official": false }, { "module_name": "nonebot_plugin_diffsinger", "project_link": "nonebot-plugin-diffsinger", "author_id": 39423408, "tags": [ { "label": "diffsinger", "color": "#c24444" } ], "is_official": false }, { "module_name": "nonebot_plugin_chikari_economy", "project_link": "nonebot-plugin-chikari-economy", "author_id": 121878042, "tags": [ { "label": "经济", "color": "#adad73" } ], "is_official": false }, { "module_name": "nonebot_plugin_auto_bot_selector", "project_link": "nonebot-plugin-auto-bot-selector", "author_id": 40534114, "tags": [ { "label": "多适配器", "color": "#5280ea" }, { "label": "跨平台", "color": "#5452ea" } ], "is_official": false }, { "module_name": "nonebot_plugin_nai3", "project_link": "nonebot-plugin-nai3", "author_id": 66541860, "tags": [ { "label": "NovelAI", "color": "#35f139" } ], "is_official": false }, { "module_name": "nonebot_plugin_hx_yinying", "project_link": "nonebot-plugin-hx-yinying", "author_id": 121207415, "tags": [ { "label": "Chat", "color": "#3b53ff" }, { "label": "幻歆!", "color": "#0d7ccd" }, { "label": "银影!", "color": "#2b4dae" } ], "is_official": false }, { "module_name": "nonebot_plugin_fhl", "project_link": "nonebot-plugin-fhl", "author_id": 158065462, "tags": [ { "label": "飞花令", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_yinyu", "project_link": "nonebot-plugin-yinyu", "author_id": 136897416, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_yinying_chat", "project_link": "nonebot-plugin-yinying-chat", "author_id": 79129640, "tags": [ { "label": "Furry", "color": "#ea5252" }, { "label": "银影", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_dcqg_relay", "project_link": "nonebot-plugin-dcqg-relay", "author_id": 35159351, "tags": [ { "label": "消息互通", "color": "#52beea" } ], "is_official": false }, { "module_name": "nonebot_plugin_zsmeme", "project_link": "nonebot-plugin-zsmeme", "author_id": 136897416, "tags": [ { "label": "帕弥什", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_sanyao", "project_link": "nonebot-plugin-sanyao", "author_id": 99971730, "tags": [ { "label": "占卜", "color": "#415656" } ], "is_official": false }, { "module_name": "nonebot_plugin_cyberfurry", "project_link": "nonebot-plugin-cyberfurry", "author_id": 106443696, "tags": [ { "label": "幼龙云端", "color": "#ccc719" }, { "label": "银影", "color": "#af2af3" } ], "is_official": false }, { "module_name": "nonebot_plugin_helpwithpic", "project_link": "nonebot-plugin-helpwithpic", "author_id": 106443696, "tags": [ { "label": "帮助", "color": "#c61b1b" }, { "label": "图片生成", "color": "#c61b1b" } ], "is_official": false }, { "module_name": "nonebot_plugin_sticker_saver", "project_link": "nonebot-plugin-sticker-saver", "author_id": 6457253, "tags": [ { "label": "表情包", "color": "#f1ce15" } ], "is_official": false }, { "module_name": "nonebot_plugin_anime_downloader", "project_link": "nonebot-plugin-anime-downloader", "author_id": 55650833, "tags": [ { "label": "Anime", "color": "#ff7474" } ], "is_official": false }, { "module_name": "nonebot_plugin_with_ai_agents", "project_link": "nonebot-plugin-with-ai-agents", "author_id": 49857339, "tags": [ { "label": "AI 智能体", "color": "#5dac81" }, { "label": "多平台模型", "color": "#5dac81" } ], "is_official": false }, { "module_name": "nonebot_plugin_RanFurryPic", "project_link": "nonebot-plugin-RanFurryPic", "author_id": 97278930, "tags": [ { "label": "furry", "color": "#8cb9e3" } ], "is_official": false }, { "module_name": "nonebot_plugin_furryfusion", "project_link": "nonebot-plugin-furryfusion", "author_id": 97278930, "tags": [ { "label": "furry", "color": "#8cb9e3" } ], "is_official": false }, { "module_name": "nonebot_plugin_mysticism", "project_link": "nonebot-plugin-mysticism", "author_id": 32036413, "tags": [ { "label": "占卜", "color": "#ea5252" }, { "label": "tarot", "color": "#ea5252" }, { "label": "神秘学", "color": "#2d4168" } ], "is_official": false }, { "module_name": "nonebot_plugin_tsugu_frontend", "project_link": "nonebot-plugin-tsugu-frontend", "author_id": 55650833, "tags": [ { "label": "BanGDream", "color": "#e70050" } ], "is_official": false }, { "module_name": "nonebot_plugin_kurogames", "project_link": "nonebot-plugin-kurogames", "author_id": 36001297, "tags": [ { "label": "战双帕弥什", "color": "#ea5252" }, { "label": "鸣潮", "color": "#ea5252" }, { "label": "库洛", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_valve_server_query", "project_link": "nonebot-plugin-valve-server-query", "author_id": 98267824, "tags": [ { "label": "valve", "color": "#ea5252" }, { "label": "query", "color": "#ea5252" }, { "label": "a2s", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_sparkapi", "project_link": "nonebot-plugin-sparkapi", "author_id": 145911132, "tags": [ { "label": "AI", "color": "#00ff00" }, { "label": "星火", "color": "#0000ff" }, { "label": "Chat", "color": "#ff0000" } ], "is_official": false }, { "module_name": "nonebot_plugin_tsugu_bangdream_bot", "project_link": "nonebot-plugin-tsugu-bangdream-bot", "author_id": 68172940, "tags": [ { "label": "tsugu", "color": "#ffee88" } ], "is_official": false }, { "module_name": "nonebot_plugin_calc24", "project_link": "nonebot-plugin-calc24", "author_id": 139576615, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_nai3_bot", "project_link": "nonebot-plugin-nai3-bot", "author_id": 16055526, "tags": [ { "label": "NovelAI", "color": "#52ea52" }, { "label": "NAI3", "color": "#52ea52" } ], "is_official": false }, { "module_name": "nonebot_plugin_dg_lab_play", "project_link": "nonebot-plugin-dg-lab-play", "author_id": 63289359, "tags": [ { "label": "dg-lab", "color": "#fee99d" }, { "label": "dg-lab-v3", "color": "#fee99d" }, { "label": "t:game", "color": "#019bf1" } ], "is_official": false }, { "module_name": "nonebot_plugin_authrespond", "project_link": "nonebot-plugin-authrespond", "author_id": 106443696, "tags": [ { "label": "黑名单", "color": "#e81616" }, { "label": "cubplugins", "color": "#28a5d1" }, { "label": "权限控制", "color": "#c75d59" } ], "is_official": false }, { "module_name": "nonebot_plugin_shutdown_hook", "project_link": "nonebot-plugin-shutdown-hook", "author_id": 32418823, "tags": [ { "label": "工具", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_plus_one", "project_link": "nonebot-plugin-plus-one", "author_id": 49857339, "tags": [ { "label": "复读姬", "color": "#df3cda" }, { "label": "船新版本", "color": "#28d98e" } ], "is_official": false }, { "module_name": "nonebot_plugin_aising", "project_link": "nonebot-plugin-aising", "author_id": 149048350, "tags": [ { "label": "唱歌", "color": "#ea5252" }, { "label": "NeuCoSVC", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_qqshell", "project_link": "nonebot-plugin-qqshell", "author_id": 49857339, "tags": [ { "label": "SSH", "color": "#cd2a8f" } ], "is_official": false }, { "module_name": "nonebot_plugin_lynchpined", "project_link": "nonebot-plugin-lynchpined", "author_id": 30611816, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_gakuenImasCalculator", "project_link": "nonebot-plugin-gakuenImasCalculator", "author_id": 54504721, "tags": [ { "label": "游戏", "color": "#ea5252" }, { "label": "工具", "color": "#ea5f52" } ], "is_official": false }, { "module_name": "nonebot_plugin_beauty_rater", "project_link": "nonebot-plugin-beauty-rater", "author_id": 110453675, "tags": [ { "label": "🌐", "color": "#e7f4ff" } ], "is_official": false }, { "module_name": "nonebot_plugin_xjie_weather", "project_link": "nonebot-plugin-xjie-weather", "author_id": 139576615, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_wwgachalogs", "project_link": "nonebot-plugin-wwgachalogs", "author_id": 61410850, "tags": [ { "label": "鸣潮", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_saalc", "project_link": "nonebot-plugin-saalc", "author_id": 57004769, "tags": [ { "label": "SAA", "color": "#17a5fe" }, { "label": "Alconna", "color": "#fe9517" } ], "is_official": false }, { "module_name": "nonebot_plugin_easymarkdown", "project_link": "nonebot-plugin-easymarkdown", "author_id": 131594704, "tags": [ { "label": "code", "color": "#5284ea" } ], "is_official": false }, { "module_name": "nonebot_plugin_multigpt", "project_link": "nonebot-plugin-multigpt", "author_id": 135360021, "tags": [ { "label": "GPT", "color": "#52d7ea" }, { "label": "PPT", "color": "#ea52c7" }, { "label": "多模", "color": "#eac252" } ], "is_official": false }, { "module_name": "nonebot_plugin_mcsm", "project_link": "nonebot-plugin-mcsm", "author_id": 98267824, "tags": [ { "label": "MCSM", "color": "#ea5252" }, { "label": "应用托管", "color": "#5272ea" }, { "label": "服务器管理", "color": "#4cc275" } ], "is_official": false }, { "module_name": "nonebot_plugin_helldivers", "project_link": "nonebot-plugin-helldivers", "author_id": 57581480, "tags": [ { "label": "helldivers", "color": "#ffd700" } ], "is_official": false }, { "module_name": "nonebot_plugin_mahjong_hand_guess", "project_link": "nonebot-plugin-mahjong-hand-guess", "author_id": 56375835, "tags": [ { "label": "game", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_asmr", "project_link": "nonebot-plugin-asmr", "author_id": 149048350, "tags": [ { "label": "asmr", "color": "#ea5252" }, { "label": "音声", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_eve_tool", "project_link": "nonebot-plugin-eve-tool", "author_id": 95174933, "tags": [ { "label": "game", "color": "#ea5252" }, { "label": "eve", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_daily_task", "project_link": "nonebot-plugin-daily-task", "author_id": 45627292, "tags": [ { "label": "每日任务", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_biliforward", "project_link": "nonebot-plugin-biliforward", "author_id": 61410850, "tags": [ { "label": "bilibili", "color": "#ea5252" }, { "label": "哔哩哔哩", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_obastatus", "project_link": "nonebot-plugin-obastatus", "author_id": 63110083, "tags": [ { "label": "BMCLAPI", "color": "#5f82ba" } ], "is_official": false }, { "module_name": "nonebot_plugin_dcqq_relay", "project_link": "nonebot-plugin-dcqq-relay", "author_id": 35159351, "tags": [ { "label": "消息互通", "color": "#428fdb" } ], "is_official": false }, { "module_name": "nonebot_plugin_ncupdate", "project_link": "nonebot-plugin-ncupdate", "author_id": 118712549, "tags": [ { "label": "NapCat", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_anymate", "project_link": "nonebot-plugin-anymate", "author_id": 89532126, "tags": [ { "label": "server", "color": "#ea5252" }, { "label": "func", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_cfr2", "project_link": "nonebot-plugin-cfr2", "author_id": 25610914, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_WWwiki", "project_link": "nonebot-plugin-WWwiki", "author_id": 136897416, "tags": [ { "label": "鸣潮", "color": "#ea5252" }, { "label": "wiki", "color": "#30af29" } ], "is_official": false }, { "module_name": "nonebot_plugin_acgnshow", "project_link": "nonebot-plugin-acgnshow", "author_id": 60691961, "tags": [ { "label": "bilibili", "color": "#f21010" } ], "is_official": false }, { "module_name": "nonebot_plugin_runagain", "project_link": "nonebot-plugin-runagain", "author_id": 37037264, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_wordle_simple", "project_link": "nonebot-plugin-wordle-simple", "author_id": 59602626, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_daily_oil_price", "project_link": "nonebot-plugin-daily-oil-price", "author_id": 16321862, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_game_torrent", "project_link": "nonebot-plugin-game-torrent", "author_id": 106718176, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_weiweibot", "project_link": "nonebot-plugin-weiweibot", "author_id": 85827122, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_autopush", "project_link": "nonebot-plugin-autopush", "author_id": 104149371, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_exe_code", "project_link": "nonebot-plugin-exe-code", "author_id": 69091901, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_system_command", "project_link": "nonebot-plugin-system-command", "author_id": 107618388, "tags": [ { "label": "shell", "color": "#0078d4" }, { "label": "cmd", "color": "#24292f" } ], "is_official": false }, { "module_name": "nonebot_plugin_cogvideox", "project_link": "nonebot-plugin-cogvideox", "author_id": 16055526, "tags": [ { "label": "AI", "color": "#ea5252" }, { "label": "视频生成", "color": "#1a30b7" } ], "is_official": false }, { "module_name": "nonebot_plugin_gpt_sovits", "project_link": "nonebot-plugin-gpt-sovits", "author_id": 55650833, "tags": [ { "label": "GPT-SoVITS", "color": "#000000" } ], "is_official": false }, { "module_name": "nonebot_plugin_wakatime", "project_link": "nonebot-plugin-wakatime", "author_id": 110453675, "tags": [ { "label": "WakaTime", "color": "#000000" } ], "is_official": false }, { "module_name": "nonebot_plugin_sunoai", "project_link": "nonebot-plugin-sunoai", "author_id": 149048350, "tags": [ { "label": "SunoAi", "color": "#ea5252" }, { "label": "AI歌曲", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_web_bottle", "project_link": "nonebot_plugin_web_bottle", "author_id": 107746729, "tags": [ { "label": "漂流瓶", "color": "#689dd0" }, { "label": "审核", "color": "#e3567b" } ], "is_official": false }, { "module_name": "nonebot_plugin_deer_pipe", "project_link": "nonebot-plugin-deer-pipe", "author_id": 49141130, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_alist", "project_link": "nonebot-plugin-alist", "author_id": 52738183, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_bili_fav_watcher", "project_link": "nonebot-plugin-bili-fav-watcher", "author_id": 94740235, "tags": [ { "label": "bilibili", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_essence_message", "project_link": "nonebot-plugin-essence-message", "author_id": 57578111, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_ba_tools", "project_link": "nonebot-plugin-ba-tools", "author_id": 144128876, "tags": [ { "label": "蔚蓝档案", "color": "#00fcf8" } ], "is_official": false }, { "module_name": "nonebot_plugin_fakepic", "project_link": "nonebot-plugin-fakepic", "author_id": 170833701, "tags": [ { "label": "图片生成", "color": "#3a82ff" } ], "is_official": false }, { "module_name": "pokepoke_miss", "project_link": "pokepoke_miss", "author_id": 140364698, "tags": [ { "label": "舞萌", "color": "#f7fe0e" }, { "label": "戳一戳", "color": "#e80606" } ], "is_official": false }, { "module_name": "nonebot_plugin_ehentai_search", "project_link": "nonebot-plugin-ehentai-search", "author_id": 61854722, "tags": [ { "label": "ehentai", "color": "#ffe700" } ], "is_official": false }, { "module_name": "mai2_pcount", "project_link": "mai2_pcount", "author_id": 140364698, "tags": [ { "label": "舞萌", "color": "#eabf0b" }, { "label": "机厅", "color": "#a60a25" } ], "is_official": false }, { "module_name": "nonebot_plugin_dify", "project_link": "nonebot-plugin-dify", "author_id": 1080807, "tags": [ { "label": "LLM", "color": "#ea5252" }, { "label": "Dify", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_looklike", "project_link": "nonebot-plugin-looklike", "author_id": 107618388, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_wait_a_minute", "project_link": "nonebot-plugin-wait-a-minute", "author_id": 51957264, "tags": [ { "label": "hook", "color": "#ff5349" } ], "is_official": false }, { "module_name": "nonebot_plugin_repesix", "project_link": "nonebot-plugin-repesix", "author_id": 107618388, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_liteyukibot", "project_link": "nonebot-plugin-liteyukibot", "author_id": 79104275, "tags": [ { "label": "liteyuki", "color": "#d0e9ff" }, { "label": "轻雪", "color": "#a2d8f4" } ], "is_official": false }, { "module_name": "nonebot_plugin_mute", "project_link": "nonebot_plugin_mute", "author_id": 140364698, "tags": [ { "label": "禁言", "color": "#ff0303" }, { "label": "娱乐", "color": "#03fff2" } ], "is_official": false }, { "module_name": "nekro_agent", "project_link": "nekro-agent", "author_id": 57167362, "tags": [ { "label": "Agent", "color": "#ece349" }, { "label": "ChatAI", "color": "#3cae69" } ], "is_official": false }, { "module_name": "nonebot_plugin_lagrange", "project_link": "nonebot_plugin_lagrange", "author_id": 90964775, "tags": [ { "label": "Lagrange", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_mccheck", "project_link": "nonebot-plugin-mccheck", "author_id": 104612722, "tags": [ { "label": "Minecraft", "color": "#ea5252" }, { "label": "i18n", "color": "#39c5bb" } ], "is_official": false }, { "module_name": "nonebot_plugin_lxns_maimai", "project_link": "nonebot-plugin-lxns-maimai", "author_id": 110453675, "tags": [ { "label": "maimai", "color": "#228be6" } ], "is_official": false }, { "module_name": "nonebot_plugin_sendpic", "project_link": "nonebot-plugin-sendpic", "author_id": 162463571, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_yoyogame", "project_link": "nonebot-plugin-yoyogame", "author_id": 91937041, "tags": [ { "label": "game", "color": "#ea5252" }, { "label": "多人游戏", "color": "#73fe1f" } ], "is_official": false }, { "module_name": "nonebot_plugin_tea_silencer", "project_link": "nonebot-plugin-tea-silencer", "author_id": 99666950, "tags": [ { "label": "拦截", "color": "#fe7931" }, { "label": "屏蔽", "color": "#31c8fe" } ], "is_official": false }, { "module_name": "nonebot_plugin_avalon", "project_link": "nonebot-plugin-avalon", "author_id": 49141130, "tags": [ { "label": "game", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_werewolf", "project_link": "nonebot-plugin-werewolf", "author_id": 69091901, "tags": [ { "label": "game", "color": "#16acf3" }, { "label": "狼人杀", "color": "#f3161a" } ], "is_official": false }, { "module_name": "nonebot_plugin_tarina_lang_turbo", "project_link": "nonebot-plugin-tarina-lang-turbo", "author_id": 51957264, "tags": [ { "label": "i18n", "color": "#ea5f52" } ], "is_official": false }, { "module_name": "nonebot-plugin-ACGalaxy", "project_link": "nonebot-plugin-ACGalaxy", "author_id": 77649130, "tags": [ { "label": "bilibili", "color": "#52eaea" }, { "label": "漫展", "color": "#52ea65" } ], "is_official": false }, { "module_name": "nonebot_plugin_QRrender", "project_link": "nonebot-plugin-QRrender", "author_id": 162463571, "tags": [ { "label": "二维码", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_weather_rank", "project_link": "nonebot-plugin-weather-rank", "author_id": 144128876, "tags": [ { "label": "weather", "color": "#43f1ff" }, { "label": "天气", "color": "#43f1ff" } ], "is_official": false }, { "module_name": "nonebot_plugin_witff", "project_link": "nonebot-plugin-witff", "author_id": 131526534, "tags": [ { "label": "furry", "color": "#ea5f52" } ], "is_official": false }, { "module_name": "nonebot_plugin_logstream", "project_link": "nonebot-plugin-logstream", "author_id": 171804402, "tags": [ { "label": "log", "color": "#41fae7" }, { "label": "SSE", "color": "#1eefff" } ], "is_official": false }, { "module_name": "nonebot_plugin_uninfo", "project_link": "nonebot-plugin-uninfo", "author_id": 42648639, "tags": [ { "label": "跨平台", "color": "#5752ea" }, { "label": "用户数据", "color": "#ea52d2" }, { "label": "群组频道数据", "color": "#ea7d52" } ], "is_official": false }, { "module_name": "nonebot_plugin_inspect", "project_link": "nonebot-plugin-inspect", "author_id": 42648639, "tags": [ { "label": "多平台适配", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_batch_withdrawal", "project_link": "nonebot-plugin-batch-withdrawal", "author_id": 114509415, "tags": [ { "label": "群管", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_yareminder", "project_link": "nonebot-plugin-yareminder", "author_id": 88876404, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_calc_game", "project_link": "nonebot-plugin-calc-game", "author_id": 143202058, "tags": [ { "label": "game", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_fun_content", "project_link": "nonebot-plugin-fun-content", "author_id": 119806180, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_pjsk_helper", "project_link": "nonebot-plugin-pjsk-helper", "author_id": 140572469, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_color_see_see", "project_link": "nonebot-plugin-color-see-see", "author_id": 80870777, "tags": [ { "label": "game", "color": "#97e7e1" } ], "is_official": false }, { "module_name": "nonebot_plugin_githubmodels", "project_link": "nonebot-plugin-githubmodels", "author_id": 122811297, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_beatsaberscore", "project_link": "nonebot-plugin-beatsaberscore", "author_id": 172130062, "tags": [ { "label": "Beat Saber", "color": "#456df1" } ], "is_official": false }, { "module_name": "nonebot_plugin_SimpleToWrite", "project_link": "nonebot-plugin-SimpleToWrite", "author_id": 174641131, "tags": [ { "label": "编写简化", "color": "#104772" }, { "label": "小白向推荐", "color": "#149581" } ], "is_official": false }, { "module_name": "nonebot_plugin_nonechat", "project_link": "nonebot-plugin-nonechat", "author_id": 144128876, "tags": [ { "label": "LLM", "color": "#52eacf" } ], "is_official": false }, { "module_name": "nonebot_plugin_marshoai", "project_link": "nonebot-plugin-marshoai", "author_id": 60691961, "tags": [ { "label": "猫娘", "color": "#e6a432" }, { "label": "AI", "color": "#32c3e6" } ], "is_official": false }, { "module_name": "nonebot_plugin_osu_match_monitor", "project_link": "nonebot-plugin-osu-match-monitor", "author_id": 65720409, "tags": [ { "label": "OSU", "color": "#da6699" } ], "is_official": false }, { "module_name": "nonebot_plugin_lolinfo", "project_link": "nonebot-plugin-lolinfo", "author_id": 112923496, "tags": [ { "label": "LOL", "color": "#02ceff" }, { "label": "英雄联盟", "color": "#ff02fb" } ], "is_official": false }, { "module_name": "nonebot_plugin_bf5_grouptools", "project_link": "nonebot_plugin_bf5_grouptools", "author_id": 90964775, "tags": [ { "label": "战地五", "color": "#529aea" } ], "is_official": false }, { "module_name": "nonebot_plugin_mc_watcher", "project_link": "nonebot_plugin_mc_watcher", "author_id": 90964775, "tags": [ { "label": "Minecraft", "color": "#d79f10" } ], "is_official": false }, { "module_name": "nonebot_plugin_zxpm", "project_link": "nonebot-plugin-zxpm", "author_id": 45528451, "tags": [ { "label": "小真寻", "color": "#fbe4e4" }, { "label": "多平台适配", "color": "#ea5252" }, { "label": "插件管理", "color": "#456df1" } ], "is_official": false }, { "module_name": "nonebot_plugin_lingyi_chat", "project_link": "nonebot-plugin-lingyi-chat", "author_id": 149694986, "tags": [ { "label": "ChatGPT", "color": "#ea5252" }, { "label": "AI", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_bfvsearch", "project_link": "nonebot-plugin-bfvsearch", "author_id": 166974448, "tags": [ { "label": "战地五", "color": "#529aea" } ], "is_official": false }, { "module_name": "nonebot_plugin_safeR18", "project_link": "nonebot-plugin-safer18", "author_id": 91937041, "tags": [ { "label": "图片", "color": "#76ecfd" }, { "label": "R18", "color": "#fd0004" } ], "is_official": false }, { "module_name": "nonebot_plugin_updater", "project_link": "nonebot-plugin-updater", "author_id": 144128876, "tags": [ { "label": "插件更新", "color": "#dd2200" } ], "is_official": false }, { "module_name": "nonebot_plugin_npu", "project_link": "nonebot-plugin-npu", "author_id": 91271243, "tags": [ { "label": "西工大", "color": "#66ccff" } ], "is_official": false }, { "module_name": "nonebot_plugin_running_state", "project_link": "nonebot-plugin-running-state", "author_id": 114509415, "tags": [ { "label": "server", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_zxreport", "project_link": "nonebot-plugin-zxreport", "author_id": 45528451, "tags": [ { "label": "小真寻", "color": "#fbe4e4" }, { "label": "多平台适配", "color": "#ea5252" }, { "label": "日报", "color": "#dd628b" } ], "is_official": false }, { "module_name": "nonebot_plugin_buy", "project_link": "nonebot-plugin-buy", "author_id": 93068569, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_nailongremove", "project_link": "nonebot-plugin-nailongremove", "author_id": 218371791, "tags": [ { "label": "图像分类模型", "color": "#5269ea" } ], "is_official": false }, { "module_name": "nonebot_plugin_pmhelp", "project_link": "nonebot-plugin-pmhelp", "author_id": 110460087, "tags": [ { "label": "帮助", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_jtj", "project_link": "nonebot-plugin-jtj", "author_id": 93068569, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_BR", "project_link": "nonebot_plugin_BR", "author_id": 70925546, "tags": [ { "label": "游戏", "color": "#fa0404" } ], "is_official": false }, { "module_name": "nonebot_plugin_boom", "project_link": "nonebot-plugin-boom", "author_id": 149694986, "tags": [ { "label": "unsafe", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_picsetu", "project_link": "nonebot-plugin-picsetu", "author_id": 114509415, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_gotify", "project_link": "nonebot-plugin-gotify", "author_id": 79104275, "tags": [ { "label": "gotify", "color": "#6eddff" }, { "label": "通知推送", "color": "#6eddff" } ], "is_official": false }, { "module_name": "nonebot_plugin_voicemusic", "project_link": "nonebot-plugin-voicemusic", "author_id": 93068569, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_fishspeech_tts", "project_link": "nonebot-plugin-fishspeech-tts", "author_id": 106718176, "tags": [ { "label": "TTS", "color": "#23e907" }, { "label": "语音合成", "color": "#e9297f" } ], "is_official": false }, { "module_name": "nonebot_plugin_summary", "project_link": "nonebot-plugin-summary", "author_id": 91937041, "tags": [ { "label": "省流", "color": "#57f99a" }, { "label": "AI", "color": "#c0d048" } ], "is_official": false }, { "module_name": "nonebot_plugin_ddrace", "project_link": "nonebot-plugin-ddrace", "author_id": 60888755, "tags": [ { "label": "game", "color": "#ea5252" }, { "label": "DDNet", "color": "#f39c12" } ], "is_official": false }, { "module_name": "nonebot_plugin_mai_arcade", "project_link": "nonebot-plugin-mai-arcade", "author_id": 114254083, "tags": [ { "label": "maimai", "color": "#ea5252" }, { "label": "arcade", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_prevent_withdrawal", "project_link": "nonebot-prevent-withdrawal", "author_id": 114509415, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_bilimusic", "project_link": "nonebot_plugin_bilimusic", "author_id": 90964775, "tags": [ { "label": "bilibili", "color": "#52d5ea" }, { "label": "music", "color": "#ea52ca" } ], "is_official": false }, { "module_name": "nonebot_plugin_mmm", "project_link": "nonebot-plugin-mmm", "author_id": 61458340, "tags": [ { "label": "人机合一", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_pong", "project_link": "nonebot-plugin-pong", "author_id": 61458340, "tags": [ { "label": "ping", "color": "#ff0000" }, { "label": "pong", "color": "#0000ff" } ], "is_official": false }, { "module_name": "nonebot_plugin_partner_join", "project_link": "nonebot-plugin-partner-join", "author_id": 114254083, "tags": [ { "label": "maimai", "color": "#a1b5e8" }, { "label": "picture", "color": "#a1b5e8" } ], "is_official": false }, { "module_name": "nonebot_plugin_zxpix", "project_link": "nonebot-plugin-zxpix", "author_id": 45528451, "tags": [ { "label": "小真寻", "color": "#fbe4e4" }, { "label": "多平台适配", "color": "#ea5252" }, { "label": "xp", "color": "#e96ab5" } ], "is_official": false }, { "module_name": "nonebot_plugin_impart", "project_link": "nonebot-plugin-impart", "author_id": 114254083, "tags": [ { "label": "impart", "color": "#fa9650" } ], "is_official": false }, { "module_name": "nonebot_plugin_wife", "project_link": "nonebot-plugin-wife", "author_id": 107618388, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_comfyui", "project_link": "nonebot-plugin-comfyui", "author_id": 126318917, "tags": [ { "label": "comfyui", "color": "#ea5252" }, { "label": "AI绘图", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_omb", "project_link": "nonebot-plugin-omb", "author_id": 61458340, "tags": [ { "label": "人机合一", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_leetcodeAPI_KHASA", "project_link": "nonebot-plugin-leetcodeAPI-KHASA", "author_id": 178357384, "tags": [ { "label": "leetcode", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_pypistats", "project_link": "nonebot-plugin-pypistats", "author_id": 114509415, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_better_broadcast", "project_link": "nonebot-plugin-better-broadcast", "author_id": 98072207, "tags": [ { "label": "广播", "color": "#1fd5ec" }, { "label": "broadcast", "color": "#231fec" } ], "is_official": false }, { "module_name": "nonebot_plugin_bililivedm", "project_link": "nonebot-plugin-bililivedm", "author_id": 174641131, "tags": [ { "label": "bilibili", "color": "#ea5252" }, { "label": "全平台可用", "color": "#232a7a" } ], "is_official": false }, { "module_name": "nonebot_plugin_text_ban", "project_link": "nonebot-plugin-text-ban", "author_id": 114509415, "tags": [ { "label": "群管", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot-plugin-auto-sendlike", "project_link": "nonebot-plugin-auto-sendlike", "author_id": 33365787, "tags": [ { "label": "点赞", "color": "#ea5252" }, { "label": "自动化", "color": "#de7c7d" } ], "is_official": false }, { "module_name": "nonebot_plugin_kindness_lesson", "project_link": "nonebot-plugin-kindness-lesson", "author_id": 55650833, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_manageweb", "project_link": "nonebot-plugin-manageweb", "author_id": 110460087, "tags": [ { "label": "webui", "color": "#ea5252" }, { "label": "插件更新", "color": "#505d4d" } ], "is_official": false }, { "module_name": "nonebot_plugin_api_scheduler", "project_link": "nonebot-plugin-api-scheduler", "author_id": 68597153, "tags": [ { "label": "scheduler", "color": "#ea5252" }, { "label": "api", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_pam", "project_link": "nonebot-plugin-pam", "author_id": 32036413, "tags": [ { "label": "rule", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_zepplife", "project_link": "nonebot-plugin-zepplife", "author_id": 137581352, "tags": [ { "label": "运动", "color": "#34cb0e" } ], "is_official": false }, { "module_name": "nonebot_plugin_hyp", "project_link": "nonebot-plugin-hyp", "author_id": 87823528, "tags": [ { "label": "Hypixel", "color": "#d5a446" } ], "is_official": false }, { "module_name": "nonebot_plugin_zxwb", "project_link": "nonebot-plugin-zxwb", "author_id": 45528451, "tags": [ { "label": "小真寻", "color": "#fbe4e4" }, { "label": "多平台适配", "color": "#ea5252" }, { "label": "词条问答", "color": "#527dea" } ], "is_official": false }, { "module_name": "nonebot_plugin_resolver2", "project_link": "nonebot-plugin-resolver2", "author_id": 64878354, "tags": [ { "label": "音频", "color": "#cb324f" }, { "label": "视频", "color": "#cb324f" }, { "label": "bilibili", "color": "#ec3658" } ], "is_official": false }, { "module_name": "nonebot_plugin_water_geoup_stats", "project_link": "nonebot-plugin-water-geoup-stats", "author_id": 114509415, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_pcr_sign", "project_link": "nonebot-plugin-pcr-sign", "author_id": 80870777, "tags": [ { "label": "PCR", "color": "#ea5252" }, { "label": "签到", "color": "#aeeaa8" } ], "is_official": false }, { "module_name": "nonebot_plugin_emojilike", "project_link": "nonebot-plugin-emojilike", "author_id": 64878354, "tags": [ { "label": "赞", "color": "#e8ec2a" }, { "label": "emoji", "color": "#e5e861" } ], "is_official": false }, { "module_name": "nonebot-plugin-multimodal-gemini", "project_link": "nonebot-plugin-multimodal-gemini", "author_id": 33365787, "tags": [ { "label": "Gemini", "color": "#368bd1" }, { "label": "AI", "color": "#368bd1" }, { "label": "GPT", "color": "#368bd1" } ], "is_official": false }, { "module_name": "nonebot_plugin_csgomarket", "project_link": "nonebot-plugin-csgomarket", "author_id": 188882430, "tags": [ { "label": "CSGO", "color": "#fb9f29" } ], "is_official": false }, { "module_name": "nonebot_plugin_nailongmagic", "project_link": "nonebot-plugin-nailongmagic", "author_id": 218371791, "tags": [ { "label": "SD", "color": "#3645e3" }, { "label": "nailong", "color": "#edd81b" }, { "label": "func", "color": "#43ed1b" } ], "is_official": false }, { "module_name": "nonebot_plugin_amitabha", "project_link": "nonebot-plugin-amitabha", "author_id": 53631287, "tags": [ { "label": "念佛", "color": "#fae1a9" }, { "label": "阿弥陀佛", "color": "#fab6a9" } ], "is_official": false }, { "module_name": "nonebot_plugin_ollama", "project_link": "nonebot-plugin-ollama", "author_id": 189951286, "tags": [ { "label": "LLM", "color": "#da56f6" }, { "label": "AI", "color": "#56f65f" } ], "is_official": false }, { "module_name": "nonebot_plugin_picstatus_template_zhenxun", "project_link": "nonebot-plugin-picstatus-template-zhenxun", "author_id": 59048777, "tags": [ { "label": "PicStatus", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_arcaea_sticker", "project_link": "nonebot-plugin-arcaea-sticker", "author_id": 106399011, "tags": [ { "label": "Arcaea", "color": "#1f1e33" }, { "label": "表情包", "color": "#d560e2" } ], "is_official": false }, { "module_name": "nonebot_plugin_simple_block", "project_link": "nonebot-plugin-simple-block", "author_id": 98072207, "tags": [ { "label": "阻断", "color": "#1c999e" }, { "label": "调试", "color": "#eda509" } ], "is_official": false }, { "module_name": "nonebot_plugin_add_friends", "project_link": "nonebot-plugin-add-friends", "author_id": 48401273, "tags": [ { "label": "同意好友", "color": "#04ff48" }, { "label": "加群邀请", "color": "#0ecdf7" } ], "is_official": false }, { "module_name": "nonebot_plugin_addons_manager", "project_link": "nonebot-plugin-addons-manager", "author_id": 131855327, "tags": [ { "label": "求生之路", "color": "#ffff6e" } ], "is_official": false }, { "module_name": "nonebot_plugin_flo_luck", "project_link": "nonebot-plugin-flo-luck", "author_id": 188882430, "tags": [ { "label": "jrrp", "color": "#52eae7" } ], "is_official": false }, { "module_name": "nonebot_plugin_auto_enter_group", "project_link": "nonebot-plugin-auto-enter-group", "author_id": 99017826, "tags": [ { "label": "群管", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_prometheus", "project_link": "nonebot-plugin-prometheus", "author_id": 143583484, "tags": [ { "label": "server", "color": "#f94300" } ], "is_official": false }, { "module_name": "nonebot_plugin_pjsekaihelper", "project_link": "nonebot-plugin-pjsekaihelper", "author_id": 186144551, "tags": [ { "label": "pjsk", "color": "#ea5252" }, { "label": "世界计划", "color": "#52ea5a" }, { "label": "音游", "color": "#52c5ea" } ], "is_official": false }, { "module_name": "nonebot_plugin_quark", "project_link": "nonebot-plugin-quark", "author_id": 64878354, "tags": [ { "label": "search", "color": "#e1cfcf" }, { "label": "quark", "color": "#cfbfbf" } ], "is_official": false }, { "module_name": "nonebot_plugin_zxui", "project_link": "nonebot-plugin-zxui", "author_id": 45528451, "tags": [ { "label": "小真寻", "color": "#fbe4e4" }, { "label": "WebUi", "color": "#2bcaca" } ], "is_official": false }, { "module_name": "nonebot_plugin_whats_talk_gemini", "project_link": "nonebot-plugin-whats-talk-gemini", "author_id": 48401273, "tags": [ { "label": "群聊总结", "color": "#03f744" }, { "label": "AI", "color": "#feee06" }, { "label": "Gemini", "color": "#0609fe" } ], "is_official": false }, { "module_name": "nonebot_plugin_liarsbar", "project_link": "nonebot-plugin-liarsbar", "author_id": 101725770, "tags": [ { "label": "game", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_number_detection", "project_link": "nonebot-plugin-number-detection", "author_id": 114509415, "tags": [ { "label": "群管", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_qbittorrent_manager", "project_link": "nonebot-plugin-qbittorrent-manager", "author_id": 109544213, "tags": [ { "label": "文件下载", "color": "#4985d1" }, { "label": "跨平台", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_checkbpm", "project_link": "nonebot-plugin-checkbpm", "author_id": 186144551, "tags": [ { "label": "music", "color": "#ea5252" }, { "label": "bpm", "color": "#ea5252" }, { "label": "tool", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_apod", "project_link": "nonebot-plugin-apod", "author_id": 122811297, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_envious", "project_link": "nonebot-plugin-envious", "author_id": 64878354, "tags": [ { "label": "羡慕", "color": "#d84848" } ], "is_official": false }, { "module_name": "nonebot_plugin_emlp_Lightweight", "project_link": "nonebot-plugin-emlp-Lightweight", "author_id": 174641131, "tags": [ { "label": "恶魔轮盘", "color": "#ea5252" }, { "label": "高自由度", "color": "#3b35ac" }, { "label": "全平台", "color": "#41d7a7" } ], "is_official": false }, { "module_name": "nonebot_plugin_summary_group", "project_link": "nonebot-plugin-summary-group", "author_id": 85243954, "tags": [ { "label": "群聊总结", "color": "#b8e994" }, { "label": "AI", "color": "#1289a7" }, { "label": "分析", "color": "#0652dd" } ], "is_official": false }, { "module_name": "nonebot_plugin_palworld", "project_link": "nonebot-plugin-palworld", "author_id": 121207415, "tags": [ { "label": "palworld", "color": "#006cff" } ], "is_official": false }, { "module_name": "nonebot_plugin_llm_jade", "project_link": "nonebot-plugin-llm-jade", "author_id": 96647974, "tags": [ { "label": "玉", "color": "#1fe792" } ], "is_official": false }, { "module_name": "nonebot_plugin_pictranslator", "project_link": "nonebot-plugin-pictranslator", "author_id": 75929887, "tags": [ { "label": "翻译", "color": "#ea5252" }, { "label": "图片翻译", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_fortnite", "project_link": "nonebot-plugin-fortnite", "author_id": 64878354, "tags": [ { "label": "EPIC", "color": "#0b0b11" }, { "label": "堡垒之夜", "color": "#23a5c2" }, { "label": "Fortnite", "color": "#23a5c2" } ], "is_official": false }, { "module_name": "nonebot_plugin_neuro_draw", "project_link": "nonebot-plugin-neuro-draw", "author_id": 55650833, "tags": [ { "label": "Neuro-sama", "color": "#ed5c5c" } ], "is_official": false }, { "module_name": "nonebot_plugin_remind", "project_link": "nonebot-plugin-remind", "author_id": 137194341, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_joke", "project_link": "nonebot-plugin-joke", "author_id": 137194341, "tags": [ { "label": "joke", "color": "#0b48a9" } ], "is_official": false }, { "module_name": "nonebot_plugin_group_file_admin", "project_link": "nonebot-plugin-group-file-admin", "author_id": 114509415, "tags": [ { "label": "群管", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_deepseek", "project_link": "nonebot-plugin-deepseek", "author_id": 110453675, "tags": [ { "label": "DeepSeek", "color": "#4d6bfe" } ], "is_official": false }, { "module_name": "nonebot_plugin_bot_tap", "project_link": "nonebot-plugin-bot-tap", "author_id": 96647974, "tags": [ { "label": "Bot", "color": "#57a4ce" }, { "label": "管理", "color": "#57ce66" } ], "is_official": false }, { "module_name": "nonebot_plugin_track_anime", "project_link": "nonebot-plugin-track-anime", "author_id": 113231410, "tags": [ { "label": "追番工具", "color": "#c78787" } ], "is_official": false }, { "module_name": "nonebot_plugin_groups_aichat", "project_link": "nonebot-plugin-groups-aichat", "author_id": 51886078, "tags": [ { "label": "ChatGPT", "color": "#33cc99" }, { "label": "Gemini", "color": "#59fb51" }, { "label": "DeepSeek", "color": "#3399aa" } ], "is_official": false }, { "module_name": "nonebot_plugin_suggarchat", "project_link": "nonebot-plugin-suggarchat", "author_id": 67693593, "tags": [ { "label": "ChatBot", "color": "#ea5252" }, { "label": "OpenAI", "color": "#00ffe3" }, { "label": "聊天", "color": "#0067ff" } ], "is_official": false }, { "module_name": "nonebot_plugin_nbnhhsh_q", "project_link": "nonebot-plugin-nbnhhsh-q", "author_id": 57782574, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_llmchat", "project_link": "nonebot-plugin-llmchat", "author_id": 87348379, "tags": [ { "label": "DeepSeek", "color": "#45beff" }, { "label": "LLM", "color": "#ff6dea" }, { "label": "ChatGPT", "color": "#9eff6d" } ], "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", "author_id": 67693593, "tags": [ { "label": "运势", "color": "#ea5252" }, { "label": "占卜", "color": "#ea5252" }, { "label": "洛谷", "color": "#0950b8" } ], "is_official": false }, { "module_name": "nonebot_plugin_vocu", "project_link": "nonebot-plugin-vocu", "author_id": 64878354, "tags": [ { "label": "TTS", "color": "#4e61a5" }, { "label": "语音", "color": "#667dd0" } ], "is_official": false }, { "module_name": "nonebot_plugin_aitalk", "project_link": "nonebot-plugin-aitalk", "author_id": 98072207, "tags": [ { "label": "LLM", "color": "#ea5252" }, { "label": "OpenAI", "color": "#52dfea" }, { "label": "DeepSeek", "color": "#9686bb" } ], "is_official": false }, { "module_name": "nonebot_plugin_timed_nickname_updater", "project_link": "nonebot-plugin-timed-nickname-updater", "author_id": 85243954, "tags": [ { "label": "群昵称", "color": "#2bcbba" } ], "is_official": false }, { "module_name": "nonebot_plugin_meme_stickers", "project_link": "nonebot-plugin-meme-stickers", "author_id": 59048777, "tags": [ { "label": "PJSK", "color": "#ea5252" }, { "label": "Arcaea", "color": "#ea5252" }, { "label": "表情包", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_whois", "project_link": "nonebot-plugin-whois", "author_id": 157226607, "tags": [ { "label": "域名", "color": "#14ff22" } ], "is_official": false }, { "module_name": "nonebot_plugin_tangkiller", "project_link": "nonebot-plugin-tangkiller", "author_id": 85243954, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_binsearch", "project_link": "nonebot-plugin-binsearch", "author_id": 157226607, "tags": [ { "label": "BankCard", "color": "#37efff" } ], "is_official": false }, { "module_name": "nonebot_plugin_chatgpt_api", "project_link": "nonebot-plugin-chatgpt-api", "author_id": 66420814, "tags": [ { "label": "ChatGPT", "color": "#000000" }, { "label": "DeepSeek", "color": "#4d6bfe" }, { "label": "OpenAI", "color": "#01a47e" } ], "is_official": false }, { "module_name": "nonebot_plugin_random_reply", "project_link": "nonebot-plugin-random-reply", "author_id": 16055526, "tags": [ { "label": "AI", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_aiochatllm", "project_link": "nonebot-plugin-aiochatllm", "author_id": 176760093, "tags": [ { "label": "LLM", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_guess_song", "project_link": "nonebot-plugin-guess-song", "author_id": 116427400, "tags": [ { "label": "maimai", "color": "#17bcff" }, { "label": "音游", "color": "#ff9122" }, { "label": "猜歌", "color": "#ff2e5c" } ], "is_official": false }, { "module_name": "nonebot_plugin_paminet_nodirtymsg", "project_link": "nonebot-plugin-paminet-nodirtymsg", "author_id": 141894077, "tags": [ { "label": "群管", "color": "#ff0000" } ], "is_official": false }, { "module_name": "nonebot_plugin_suggarex_cf", "project_link": "nonebot-plugin-suggarex-cf", "author_id": 67693593, "tags": [ { "label": "Suggar", "color": "#ea5252" }, { "label": "CloudFlare", "color": "#ced23c" } ], "is_official": false }, { "module_name": "nonebot_plugin_llm_plugins_call", "project_link": "nonebot-plugin-llm-plugins-call", "author_id": 16055526, "tags": [ { "label": "AI", "color": "#ea5252" }, { "label": "工具调用", "color": "#586fd9" } ], "is_official": false }, { "module_name": "nonebot_plugin_dingzhen", "project_link": "nonebot-plugin-dingzhen", "author_id": 109138085, "tags": [ { "label": "语音", "color": "#126fdf" }, { "label": "丁真", "color": "#df2f2f" }, { "label": "语音合成", "color": "#0cdf4f" } ], "is_official": false }, { "module_name": "nonebot_plugin_gold_price", "project_link": "nonebot-plugin-gold-price", "author_id": 96228495, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_whoasked", "project_link": "nonebot-plugin-whoasked", "author_id": 163709829, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_moellmchats", "project_link": "nonebot-plugin-moellmchats", "author_id": 31814960, "tags": [ { "label": "DeepSeek", "color": "#3586ff" }, { "label": "LLM", "color": "#ea5252" }, { "label": "ChatGPT", "color": "#000000" } ], "is_official": false }, { "module_name": "nonebot_plugin_jmdownloader", "project_link": "nonebot-plugin-jmdownloader", "author_id": 74812967, "tags": [ { "label": "禁漫", "color": "#f3d667" } ], "is_official": false }, { "module_name": "nonebot_plugin_jm", "project_link": "nonebot-plugin-jm", "author_id": 85243954, "tags": [ { "label": "禁漫", "color": "#f3d667" } ], "is_official": false }, { "module_name": "nonebot_plugin_zssm", "project_link": "nonebot-plugin-zssm", "author_id": 59153990, "tags": [ { "label": "func", "color": "#961d1d" }, { "label": "ai", "color": "#5bd46c" }, { "label": "deepseek", "color": "#4b57ed" } ], "is_official": false }, { "module_name": "nonebot_plugin_gemini_vision", "project_link": "nonebot-plugin-gemini-vision", "author_id": 98764734, "tags": [ { "label": "Gemini", "color": "#ea5252" }, { "label": "vision", "color": "#ea5252" }, { "label": "ai", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_oi_helper", "project_link": "nonebot-plugin-oi-helper", "author_id": 78906287, "tags": [ { "label": "ACM", "color": "#ea5252" }, { "label": "OI", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_mclib", "project_link": "nonebot_plugin_mclib", "author_id": 67693593, "tags": [ { "label": "MC", "color": "#ea5252" }, { "label": "Minecraft", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_furryyunhei", "project_link": "nonebot-plugin-furryyunhei", "author_id": 193674838, "tags": [ { "label": "Furry", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_testhkk", "project_link": "nonebot-plugin-testhkk", "author_id": 157194530, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_error_report", "project_link": "nonebot-plugin-error-report", "author_id": 121207415, "tags": [ { "label": "跨平台", "color": "#52b2ea" }, { "label": "Alconna", "color": "#eabd52" } ], "is_official": false }, { "module_name": "nonebot_plugin_asmr100", "project_link": "nonebot-plugin-asmr100", "author_id": 95261137, "tags": [ { "label": "asmr", "color": "#52b4ea" }, { "label": "R18", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_skland", "project_link": "nonebot-plugin-skland", "author_id": 80870777, "tags": [ { "label": "森空岛", "color": "#bfec00" }, { "label": "明日方舟", "color": "#67fdfe" }, { "label": "终末地", "color": "#e3d600" } ], "is_official": false }, { "module_name": "nonebot_plugin_argot", "project_link": "nonebot-plugin-argot", "author_id": 110453675, "tags": [ { "label": "👁️", "color": "#e74f88" }, { "label": "🤫", "color": "#ffd8b8" } ], "is_official": false }, { "module_name": "nonebot_plugin_github_release_notifier", "project_link": "nonebot-plugin-github-release-notifier", "author_id": 80151962, "tags": [ { "label": "Github", "color": "#2a2a2a" } ], "is_official": false }, { "module_name": "nonebot_plugin_bfvservermap", "project_link": "nonebot-plugin-bfvservermap", "author_id": 163698589, "tags": [ { "label": "战地五", "color": "#529aea" } ], "is_official": false }, { "module_name": "nonebot_plugin_tieba_monitor", "project_link": "nonebot-plugin-tieba-monitor", "author_id": 104332832, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_anywhere_llm", "project_link": "nonebot-plugin-anywhere-llm", "author_id": 57753690, "tags": [ { "label": "DeepSeek", "color": "#5852f7" }, { "label": "LLM", "color": "#f75297" } ], "is_official": false }, { "module_name": "nonebot_plugin_latex", "project_link": "nonebot-plugin-latex", "author_id": 71250018, "tags": [ { "label": "LaTeX", "color": "#007af9" } ], "is_official": false }, { "module_name": "nonebot_plugin_mcmod", "project_link": "nonebot-plugin-mcmod", "author_id": 58966022, "tags": [ { "label": "Minecraft", "color": "#7d24d5" } ], "is_official": false }, { "module_name": "nonebot_plugin_revolver", "project_link": "nonebot-plugin-revolver", "author_id": 68112346, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_custom_face", "project_link": "nonebot-plugin-custom-face", "author_id": 109930302, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_jmcomic", "project_link": "nonebot-plugin-jmcomic", "author_id": 66541860, "tags": [ { "label": "禁漫", "color": "#f3d667" } ], "is_official": false }, { "module_name": "nonebot_plugin_taygedo_helper", "project_link": "nonebot-plugin-taygedo-helper", "author_id": 61410850, "tags": [ { "label": "幻塔", "color": "#ea5252" }, { "label": "塔吉多", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_ban_sticker", "project_link": "nonebot-plugin-ban-sticker", "author_id": 57578111, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_hitokoto_plus", "project_link": "nonebot-plugin-hitokoto-plus", "author_id": 163709829, "tags": [ { "label": "一言", "color": "#7d24d5" } ], "is_official": false }, { "module_name": "nonebot_plugin_poker", "project_link": "nonebot-plugin-poker", "author_id": 138541077, "tags": [ { "label": "小游戏", "color": "#ea5252" }, { "label": "对战", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_mhguesser", "project_link": "nonebot-plugin-mhguesser", "author_id": 53973735, "tags": [ { "label": "怪物猎人", "color": "#b128a2" } ], "is_official": false }, { "module_name": "nonebot_plugin_ciku", "project_link": "nonebot-plugin-ciku", "author_id": 174641131, "tags": [ { "label": "词库", "color": "#ea5252" }, { "label": "小白推荐", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_noadpls", "project_link": "nonebot-plugin-noadpls", "author_id": 60888755, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_sideload", "project_link": "nonebot-plugin-sideload", "author_id": 96647974, "tags": [ { "label": "Web", "color": "#52eada" }, { "label": "聊天", "color": "#85ea52" } ], "is_official": false }, { "module_name": "nonebot_plugin_paper", "project_link": "nonebot-plugin-paper", "author_id": 73932916, "tags": [ { "label": "arxiv", "color": "#1c1a17" } ], "is_official": false }, { "module_name": "nonebot_plugin_dorodoro", "project_link": "nonebot-plugin-dorodoro", "author_id": 93612024, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_qqdetail", "project_link": "nonebot-plugin-qqdetail", "author_id": 144674902, "tags": [ { "label": "qq", "color": "#52eabc" } ], "is_official": false }, { "module_name": "nonebot_plugin_multi_source_daily", "project_link": "nonebot-plugin-multi-source-daily", "author_id": 32546670, "tags": [ { "label": "日报", "color": "#ea5252" }, { "label": "news", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_fix_qq_img_ssl", "project_link": "nonebot-plugin-fix-qq-img-ssl", "author_id": 59048777, "tags": [ { "label": "SSL", "color": "#ea5252" }, { "label": "qq", "color": "#52eabc" } ], "is_official": false }, { "module_name": "nonebot_plugin_farm", "project_link": "nonebot-plugin-farm", "author_id": 51057547, "tags": [ { "label": "种地", "color": "#90ee90" } ], "is_official": false }, { "module_name": "nonebot_plugin_ehentai", "project_link": "nonebot-plugin-ehentai", "author_id": 35657483, "tags": [ { "label": "ehentai", "color": "#e3e0d1" } ], "is_official": false }, { "module_name": "nonebot_plugin_picmenu_next", "project_link": "nonebot-plugin-picmenu-next", "author_id": 59048777, "tags": [ { "label": "帮助", "color": "#ea5252" }, { "label": "菜单", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_nyaturingtest", "project_link": "nonebot-plugin-nyaturingtest", "author_id": 74781933, "tags": [ { "label": "ChatBot", "color": "#689fff" }, { "label": "QQ群", "color": "#689fff" }, { "label": "LLM", "color": "#689fff" } ], "is_official": false }, { "module_name": "nonebot_plugin_clovers", "project_link": "nonebot-plugin-clovers", "author_id": 51886078, "tags": [ { "label": "clovers", "color": "#00cc33" } ], "is_official": false }, { "module_name": "nonebot_plugin_pokemonle", "project_link": "nonebot-plugin-pokemonle", "author_id": 53973735, "tags": [ { "label": "宝可梦", "color": "#fff160" } ], "is_official": false }, { "module_name": "nonebot_plugin_bfvplayerlist", "project_link": "nonebot-plugin-bfvplayerlist", "author_id": 163698589, "tags": [ { "label": "战地五", "color": "#529aea" } ], "is_official": false }, { "module_name": "nonebot_plugin_emojilike_automonkey", "project_link": "nonebot-plugin-emojilike-automonkey", "author_id": 109930302, "tags": [ { "label": "emoji", "color": "#6677ff" } ], "is_official": false }, { "module_name": "nonebot_plugin_df_armor_repair_simulator", "project_link": "nonebot-plugin-df-armor-repair-simulator", "author_id": 109930302, "tags": [ { "label": "tool", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_hacker_news", "project_link": "nonebot-plugin-hacker-news", "author_id": 36318991, "tags": [ { "label": "broadcast", "color": "#ea5252" }, { "label": "news", "color": "#f59a10" } ], "is_official": false }, { "module_name": "nonebot_plugin_limiter", "project_link": "nonebot-plugin-limiter", "author_id": 98752512, "tags": [ { "label": "cooldown", "color": "#527aea" } ], "is_official": false }, { "module_name": "nonebot_plugin_zzzpanel", "project_link": "nonebot-plugin-zzzpanel", "author_id": 174641131, "tags": [ { "label": "米哈游", "color": "#ea5252" }, { "label": "绝区零", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_NobleDuel", "project_link": "nonebot-plugin-NobleDuel", "author_id": 105113722, "tags": [ { "label": "贵族决斗", "color": "#527dea" }, { "label": "恶魔轮盘", "color": "#ea5255" }, { "label": "养成", "color": "#82ea52" } ], "is_official": false }, { "module_name": "nonebot_plugin_sunset_reminder", "project_link": "nonebot-plugin-sunset-reminder", "author_id": 80151962, "tags": [ { "label": "火烧云", "color": "#ff9c00" } ], "is_official": false }, { "module_name": "nonebot_plugin_binance", "project_link": "nonebot-plugin-binance", "author_id": 96228495, "tags": [ { "label": "binance", "color": "#ea5252" }, { "label": "币安", "color": "#e9ea52" } ], "is_official": false }, { "module_name": "nonebot_plugin_orm", "project_link": "nonebot-plugin-orm", "author_id": 45716046, "tags": [], "is_official": true }, { "module_name": "nonebot_plugin_exdi", "project_link": "nonebot-plugin-exdi", "author_id": 122149478, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_xibao", "project_link": "nonebot-plugin-xibao", "author_id": 176733343, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_vv", "project_link": "nonebot-plugin-vv", "author_id": 85243954, "tags": [ { "label": "表情包", "color": "#b9f6ca" } ], "is_official": false }, { "module_name": "nonebot_plugin_deltaforce_simulator", "project_link": "nonebot-plugin-deltaforce-simulator", "author_id": 16055526, "tags": [ { "label": "三角洲", "color": "#457a47" } ], "is_official": false }, { "module_name": "nonebot_plugin_liteperm", "project_link": "nonebot_plugin_liteperm", "author_id": 67693593, "tags": [ { "label": "权限控制", "color": "#ea5252" }, { "label": "管理", "color": "#d7ae22" } ], "is_official": false }, { "module_name": "nonebot_plugin_lazytea", "project_link": "nonebot_plugin_lazytea", "author_id": 78413699, "tags": [ { "label": "GUI", "color": "#ea5252" }, { "label": "图形化", "color": "#52e9ea" } ], "is_official": false }, { "module_name": "nonebot_plugin_flomic", "project_link": "nonebot-plugin-flomic", "author_id": 188882430, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_fishing2", "project_link": "nonebot-plugin-fishing2", "author_id": 49135577, "tags": [ { "label": "钓鱼", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_llm_helper", "project_link": "nonebot-plugin-llm-helper", "author_id": 90964775, "tags": [ { "label": "多平台适配", "color": "#f16969" }, { "label": "帮助", "color": "#43b1ce" }, { "label": "LLM", "color": "#a2c36d" } ], "is_official": false }, { "module_name": "nonebot_plugin_guess_disease", "project_link": "nonebot-plugin-guess-disease", "author_id": 68677053, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_delta_helper", "project_link": "nonebot-plugin-delta-helper", "author_id": 61410850, "tags": [ { "label": "deltaforce", "color": "#ea5252" }, { "label": "三角洲行动", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_awsmgmt", "project_link": "nonebot-plugin-awsmgmt", "author_id": 20412597, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_lazytea_shell_extension", "project_link": "nonebot_plugin_lazytea_shell_extension", "author_id": 78413699, "tags": [ { "label": "GUI", "color": "#53f047" }, { "label": "管理", "color": "#fff29e" } ], "is_official": false }, { "module_name": "nonebot_plugin_nmcweather", "project_link": "nonebot-plugin-nmcweather", "author_id": 65002276, "tags": [ { "label": "weather", "color": "#05f73c" }, { "label": "天气", "color": "#08f9c7" } ], "is_official": false }, { "module_name": "nonebot_plugin_fuckfinalshell", "project_link": "nonebot-plugin-fuckfinalshell", "author_id": 144674902, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_value", "project_link": "nonebot-plugin-value", "author_id": 67693593, "tags": [ { "label": "value", "color": "#d10a0a" }, { "label": "货币", "color": "#3c0eff" }, { "label": "经济", "color": "#1b9f1d" } ], "is_official": false }, { "module_name": "nonebot_plugin_mcplayer_render", "project_link": "nonebot-plugin-mcplayer-render", "author_id": 49135577, "tags": [ { "label": "Minecraft", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_alisten", "project_link": "nonebot-plugin-alisten", "author_id": 5219550, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_simple_setu", "project_link": "nonebot-plugin-simple-setu", "author_id": 178264759, "tags": [ { "label": "pixiv", "color": "#00f5ed" }, { "label": "色图", "color": "#ed0823" } ], "is_official": false }, { "module_name": "nonebot-plugin-anipusher", "project_link": "nonebot-plugin-anipusher", "author_id": 32594985, "tags": [ { "label": "Emby", "color": "#0b8a31" }, { "label": "AniRss", "color": "#0b8a31" } ], "is_official": false }, { "module_name": "nonebot_plugin_huaer_bot", "project_link": "nonebot-plugin-huaer-bot", "author_id": 216365707, "tags": [ { "label": "LLM", "color": "#0583b3" } ], "is_official": false }, { "module_name": "nonebot_plugin_sell_poor", "project_link": "nonebot-plugin-sell-poor", "author_id": 96647974, "tags": [ { "label": "卖弱", "color": "#ea5252" }, { "label": "😭😭😭", "color": "#52eaba" } ], "is_official": false }, { "module_name": "nonebot_plugin_abs", "project_link": "nonebot-plugin-abs", "author_id": 64878354, "tags": [ { "label": "abstract", "color": "#edf119" } ], "is_official": false }, { "module_name": "nonebot_plugin_ImageLibrary", "project_link": "nonebot-plugin-imagelibrary", "author_id": 126797731, "tags": [ { "label": "image", "color": "#7629c4" } ], "is_official": false }, { "module_name": "nonebot_plugin_exhibitionism", "project_link": "nonebot_plugin_exhibitionism", "author_id": 78413699, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_akashgen", "project_link": "nonebot-plugin-akashgen", "author_id": 144674902, "tags": [ { "label": "绘画", "color": "#98f698" }, { "label": "Akash", "color": "#a1eecf" }, { "label": "AI", "color": "#78bbf0" } ], "is_official": false }, { "module_name": "nonebot_plugin_figurine", "project_link": "nonebot-plugin-figurine", "author_id": 99017826, "tags": [ { "label": "figurine", "color": "#ea5252" }, { "label": "手办", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_distributed_blacklist", "project_link": "nonebot-plugin-distributed-blacklist", "author_id": 65720409, "tags": [ { "label": "blacklist", "color": "#000000" } ], "is_official": false }, { "module_name": "nonebot_plugin_bafortune", "project_link": "nonebot-plugin-bafortune", "author_id": 98072207, "tags": [ { "label": "碧蓝档案", "color": "#2b8dce" }, { "label": "今日运势", "color": "#ce2b2b" }, { "label": "多平台适配", "color": "#c2ce2b" } ], "is_official": false }, { "module_name": "nonebot_plugin_repeat_checker", "project_link": "nonebot-plugin-repeat-checker", "author_id": 56631400, "tags": [ { "label": "复读", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_quark_autosave", "project_link": "nonebot-plugin-quark-autosave", "author_id": 64878354, "tags": [ { "label": "quark", "color": "#593dc3" }, { "label": "夸克", "color": "#6542eb" }, { "label": "网盘", "color": "#235c84" } ], "is_official": false }, { "module_name": "nonebot_plugin_who_is_spy", "project_link": "nonebot-plugin-who-is-spy", "author_id": 144591143, "tags": [ { "label": "game", "color": "#ea5252" }, { "label": "游戏", "color": "#ea52cd" } ], "is_official": false }, { "module_name": "nonebot_plugin_dst_qq", "project_link": "nonebot-plugin-dst-qq", "author_id": 145603392, "tags": [ { "label": "server", "color": "#52ea9d" } ], "is_official": false }, { "module_name": "nonebot_plugin_fupan", "project_link": "nonebot-plugin-fupan", "author_id": 867749, "tags": [ { "label": "股票", "color": "#ea5252" }, { "label": "打卡", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_algo", "project_link": "nonebot-plugin-algo", "author_id": 188494207, "tags": [ { "label": "算法竞赛", "color": "#52c5ea" }, { "label": "ACM", "color": "#eb0e31" } ], "is_official": false }, { "module_name": "nonebot_plugin_llm_extension", "project_link": "nonebot-plugin-llm-extension", "author_id": 110453675, "tags": [ { "label": "🇦🇮", "color": "#f0f0f0" } ], "is_official": false }, { "module_name": "nonebot_plugin_htmlkit", "project_link": "nonebot-plugin-htmlkit", "author_id": 50769752, "tags": [ { "label": "模板渲染", "color": "#a57021" }, { "label": "图片生成", "color": "#74c817" }, { "label": "HTML渲染", "color": "#199579" } ], "is_official": true }, { "module_name": "nonebot_plugin_mhcodes", "project_link": "nonebot-plugin-mhcodes", "author_id": 99017826, "tags": [ { "label": "怪物猎人", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_parser", "project_link": "nonebot-plugin-parser", "author_id": 64878354, "tags": [ { "label": "图集", "color": "#263bd5" }, { "label": "视频", "color": "#152ac0" }, { "label": "解析", "color": "#152ac0" } ], "is_official": false }, { "module_name": "nonebot_plugin_kookcardmessage", "project_link": "nonebot-plugin-kookcardmessage", "author_id": 174641131, "tags": [ { "label": "kook", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_mcnews", "project_link": "nonebot-plugin-mcnews", "author_id": 110646806, "tags": [ { "label": "MC", "color": "#52e7ea" }, { "label": "新闻", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_open_file", "project_link": "nonebot-plugin-open-file", "author_id": 131272574, "tags": [ { "label": "file", "color": "#a50a0a" } ], "is_official": false }, { "module_name": "nonebot_plugin_memory", "project_link": "nonebot-plugin-memory", "author_id": 175703143, "tags": [ { "label": "log", "color": "#336daf" }, { "label": "func", "color": "#5bd0a2" } ], "is_official": false }, { "module_name": "nonebot_plugin_pxchat", "project_link": "nonebot-plugin-pxchat", "author_id": 112293881, "tags": [ { "label": "chat", "color": "#52eab7" }, { "label": "deepseek", "color": "#52ddea" }, { "label": "mcp", "color": "#5292ea" } ], "is_official": false }, { "module_name": "nonebot_plugin_daily_bing", "project_link": "nonebot-plugin-daily-bing", "author_id": 122811297, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_ai_turtle_soup", "project_link": "nonebot-plugin-ai-turtle-soup", "author_id": 68174188, "tags": [ { "label": "群聊小游戏", "color": "#ea5252" }, { "label": "海龟汤", "color": "#ea5252" }, { "label": "AI", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_bili_helper", "project_link": "nonebot-plugin-bili-helper", "author_id": 4216470, "tags": [ { "label": "bilibili", "color": "#ea5252" }, { "label": "B站", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_markdown2img", "project_link": "nonebot-plugin-markdown2img", "author_id": 96008766, "tags": [ { "label": "markdown", "color": "#ea5252" }, { "label": "func", "color": "#0fe16e" } ], "is_official": false }, { "module_name": "nonebot_plugin_jrrp3", "project_link": "nonebot-plugin-jrrp3", "author_id": 79314033, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_image_symmetry", "project_link": "nonebot-plugin-image-symmetry", "author_id": 79314033, "tags": [ { "label": "image", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_manosaba_memes", "project_link": "nonebot-plugin-manosaba-memes", "author_id": 55650833, "tags": [ { "label": "魔法少女的魔法审判", "color": "#de7d92" } ], "is_official": false }, { "module_name": "nonebot_plugin_course_schedule", "project_link": "nonebot-plugin-course-schedule", "author_id": 49135577, "tags": [ { "label": "课表", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_ipinfo", "project_link": "nonebot-plugin-ipinfo", "author_id": 122811297, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_templates_draw", "project_link": "nonebot-plugin-templates-draw", "author_id": 99017826, "tags": [ { "label": "AI画图", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_manosoba_reply_generator", "project_link": "nonebot-plugin-manosoba-reply-generator", "author_id": 111682952, "tags": [ { "label": "func", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_anans_sketchbook", "project_link": "nonebot-plugin-anans-sketchbook", "author_id": 64982342, "tags": [ { "label": "魔法少女的魔女审判", "color": "#d07e82" }, { "label": "夏目安安", "color": "#6a5acd" }, { "label": "表情包", "color": "#ca64dc" } ], "is_official": false }, { "module_name": "nonebot_plugin_anan_say", "project_link": "nonebot-plugin-anan-say", "author_id": 122149478, "tags": [ { "label": "魔女审判", "color": "#b10ba0" } ], "is_official": false }, { "module_name": "nonebot_plugin_terralink", "project_link": "nonebot-plugin-terralink", "author_id": 96228495, "tags": [ { "label": "泰拉瑞亚", "color": "#b6e161" }, { "label": "群服互通", "color": "#669ed7" }, { "label": "Terraria", "color": "#df6262" } ], "is_official": false }, { "module_name": "nonebot_plugin_railwaytools", "project_link": "nonebot-plugin-railwaytools", "author_id": 51502183, "tags": [ { "label": "中国铁路", "color": "#5287ea" } ], "is_official": false }, { "module_name": "nonebot_plugin_omikuji", "project_link": "nonebot-plugin-omikuji", "author_id": 67693593, "tags": [ { "label": "御神签", "color": "#ea5252" }, { "label": "运势", "color": "#0bc2af" }, { "label": "占卜", "color": "#811eee" } ], "is_official": false }, { "module_name": "nonebot_plugin_rollpig", "project_link": "nonebot-plugin-rollpig", "author_id": 30120610, "tags": [ { "label": "pig", "color": "#fdd7e4" } ], "is_official": false }, { "module_name": "nonebot_plugin_instagram", "project_link": "nonebot-plugin-instagram", "author_id": 49683326, "tags": [ { "label": "instagram", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_mcserver_status_check", "project_link": "nonebot-plugin-mcserver-status-check", "author_id": 153894603, "tags": [ { "label": "Minecraft", "color": "#ea5252" }, { "label": "MC", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_perithacus", "project_link": "nonebot-plugin-pErithacus", "author_id": 21017431, "tags": [ { "label": "chat", "color": "#ea5252" }, { "label": "自动回复", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_quickreply", "project_link": "nonebot-plugin-quickreply", "author_id": 104259619, "tags": [ { "label": "工具", "color": "#4de024" } ], "is_official": false }, { "module_name": "nonebot_plugin_jimeng", "project_link": "nonebot-plugin-jimeng", "author_id": 104259619, "tags": [ { "label": "OpenAi", "color": "#1d7374" }, { "label": "绘画", "color": "#8b1bb4" } ], "is_official": false }, { "module_name": "nonebot_plugin_boardgamehelper", "project_link": "nonebot-plugin-boardgamehelper", "author_id": 60382099, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_ai_groupmate", "project_link": "nonebot-plugin-ai-groupmate", "author_id": 30517062, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_trans_progress", "project_link": "nonebot-plugin-trans-progress", "author_id": 99017826, "tags": [ { "label": "汉化", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_word_censor", "project_link": "nonebot-plugin-word-censor", "author_id": 99163726, "tags": [ { "label": "rule", "color": "#ea5252" }, { "label": "黑名单", "color": "#000000" } ], "is_official": false }, { "module_name": "nonebot_plugin_internet_outage", "project_link": "nonebot-plugin-internet-outage", "author_id": 110646806, "tags": [ { "label": "Cloudflare", "color": "#f97316" }, { "label": "网络中断监测", "color": "#dc2626" } ], "is_official": false }, { "module_name": "nonebot_plugin_group_relay", "project_link": "nonebot-plugin-group-relay", "author_id": 86188856, "tags": [ { "label": "func", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_tavily", "project_link": "nonebot-plugin-tavily", "author_id": 1080807, "tags": [ { "label": "search", "color": "#39c5bb" } ], "is_official": false }, { "module_name": "nonebot_plugin_bf6_stats", "project_link": "nonebot-plugin-bf6-stats", "author_id": 202926395, "tags": [ { "label": "bf6", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_bili2mp4", "project_link": "nonebot-plugin-bili2mp4", "author_id": 181480818, "tags": [ { "label": "bilibili", "color": "#ff6699" }, { "label": "mp4", "color": "#ff6699" }, { "label": "小程序", "color": "#ff6699" } ], "is_official": false }, { "module_name": "nonebot_plugin_qqmusic_reco", "project_link": "nonebot-plugin-qqmusic-reco", "author_id": 99163726, "tags": [ { "label": "music", "color": "#ea5252" }, { "label": "recommend", "color": "#beeb0c" } ], "is_official": false }, { "module_name": "nonebot_plugin_osugreek", "project_link": "nonebot-plugin-osugreek", "author_id": 64720173, "tags": [ { "label": "osu", "color": "#ff66aa" } ], "is_official": false }, { "module_name": "nonebot_plugin_uniconf", "project_link": "nonebot-plugin-uniconf", "author_id": 67693593, "tags": [ { "label": "config", "color": "#0cccff" }, { "label": "配置文件", "color": "#c3ff0c" }, { "label": "API", "color": "#ffff00" } ], "is_official": false }, { "module_name": "nonebot_plugin_dice_helper", "project_link": "nonebot-plugin-dice-helper", "author_id": 30589360, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_mcpclient", "project_link": "nonebot-plugin-mcpclient", "author_id": 1080807, "tags": [ { "label": "mcp", "color": "#243576" } ], "is_official": false }, { "module_name": "nonebot_plugin_BitTorrents", "project_link": "nonebot-plugin-bittorrents", "author_id": 98020024, "tags": [ { "label": "func", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_personification", "project_link": "nonebot-plugin-shiro-personification", "author_id": 114895516, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_shiro_web_console", "project_link": "nonebot-plugin-shiro-web-console", "author_id": 114895516, "tags": [ { "label": "WebUi", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_nodejsphira", "project_link": "nonebot-plugin-nodejsphira", "author_id": 75435667, "tags": [ { "label": "server", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_doroending", "project_link": "nonebot-plugin-doroending", "author_id": 127853582, "tags": [ { "label": "doro", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_auto_emojimix", "project_link": "nonebot-plugin-auto-emojimix", "author_id": 74812967, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_peek", "project_link": "nonebot-plugin-peek", "author_id": 74812967, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_rikka", "project_link": "nonebot-plugin-rikka", "author_id": 72406624, "tags": [ { "label": "maimai", "color": "#60bacf" } ], "is_official": false }, { "module_name": "nonebot_plugin_cardimg", "project_link": "nonebot-plugin-cardimg", "author_id": 99666950, "tags": [ { "label": "模板渲染", "color": "#dec7ef" }, { "label": "图片生成", "color": "#f8d8cf" }, { "label": "HTML渲染", "color": "#c2dff2" } ], "is_official": false }, { "module_name": "nonebot_plugin_mute_cat", "project_link": "nonebot-plugin-mute-cat", "author_id": 199351962, "tags": [ { "label": "工具", "color": "#ea5252" }, { "label": "群管理", "color": "#52eacf" }, { "label": "禁言", "color": "#830daa" } ], "is_official": false }, { "module_name": "nonebot-plugin-trumpwatcher", "project_link": "nonebot-plugin-trumpwatcher", "author_id": 42519315, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_mc_whitelist_controller", "project_link": "nonebot-plugin-mc-whitelist-controller", "author_id": 51502183, "tags": [ { "label": "Minecraft", "color": "#ea5252" }, { "label": "MC", "color": "#ea5252" }, { "label": "server", "color": "#ea5252" } ], "is_official": false }, { "module_name": "nonebot_plugin_nbnhhsh", "project_link": "nonebot-plugin-nbnhhsh", "author_id": 24908800, "tags": [], "is_official": false }, { "module_name": "nonebot_plugin_codex", "project_link": "nonebot-plugin-codex", "author_id": 98325911, "tags": [ { "label": "OpenAI", "color": "#ea5252" }, { "label": "Codex", "color": "#6fd8fc" }, { "label": "VibeCoding", "color": "#74fc6f" } ], "is_official": false }, ] ================================================ FILE: nonebot/__init__.py ================================================ """本模块主要定义了 NoneBot 启动所需函数,供 bot 入口文件调用。 ## 快捷导入 为方便使用,本模块从子模块导入了部分内容,以下内容可以直接通过本模块导入: - `on` => {ref}``on` ` - `on_metaevent` => {ref}``on_metaevent` ` - `on_message` => {ref}``on_message` ` - `on_notice` => {ref}``on_notice` ` - `on_request` => {ref}``on_request` ` - `on_startswith` => {ref}``on_startswith` ` - `on_endswith` => {ref}``on_endswith` ` - `on_fullmatch` => {ref}``on_fullmatch` ` - `on_keyword` => {ref}``on_keyword` ` - `on_command` => {ref}``on_command` ` - `on_shell_command` => {ref}``on_shell_command` ` - `on_regex` => {ref}``on_regex` ` - `on_type` => {ref}``on_type` ` - `CommandGroup` => {ref}``CommandGroup` ` - `Matchergroup` => {ref}``MatcherGroup` ` - `load_plugin` => {ref}``load_plugin` ` - `load_plugins` => {ref}``load_plugins` ` - `load_all_plugins` => {ref}``load_all_plugins` ` - `load_from_json` => {ref}``load_from_json` ` - `load_from_toml` => {ref}``load_from_toml` ` - `load_builtin_plugin` => {ref}``load_builtin_plugin` ` - `load_builtin_plugins` => {ref}``load_builtin_plugins` ` - `get_plugin` => {ref}``get_plugin` ` - `get_plugin_by_module_name` => {ref}``get_plugin_by_module_name` ` - `get_loaded_plugins` => {ref}``get_loaded_plugins` ` - `get_available_plugin_names` => {ref}``get_available_plugin_names` ` - `get_plugin_config` => {ref}``get_plugin_config` ` - `require` => {ref}``require` ` FrontMatter: mdx: format: md sidebar_position: 0 description: nonebot 模块 """ from importlib.metadata import version import os from typing import Any, TypeVar, overload import loguru from nonebot.adapters import Adapter, Bot from nonebot.compat import model_dump from nonebot.config import DOTENV_TYPE, Config, Env from nonebot.drivers import ASGIMixin, Driver, combine_driver from nonebot.log import logger as logger from nonebot.utils import escape_tag, resolve_dot_notation try: __version__ = version("nonebot2") except Exception: # pragma: no cover __version__ = None A = TypeVar("A", bound=Adapter) _driver: Driver | None = None def get_driver() -> Driver: """获取全局 {ref}`nonebot.drivers.Driver` 实例。 可用于在计划任务的回调等情形中获取当前 {ref}`nonebot.drivers.Driver` 实例。 返回: 全局 {ref}`nonebot.drivers.Driver` 对象 异常: ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化 ({ref}`nonebot.init ` 尚未调用) 用法: ```python driver = nonebot.get_driver() ``` """ if _driver is None: raise ValueError("NoneBot has not been initialized.") return _driver @overload def get_adapter(name: str) -> Adapter: """ 参数: name: 适配器名称 返回: 指定名称的 {ref}`nonebot.adapters.Adapter` 对象 """ @overload def get_adapter(name: type[A]) -> A: """ 参数: name: 适配器类型 返回: 指定类型的 {ref}`nonebot.adapters.Adapter` 对象 """ def get_adapter(name: str | type[Adapter]) -> Adapter: """获取已注册的 {ref}`nonebot.adapters.Adapter` 实例。 异常: ValueError: 指定的 {ref}`nonebot.adapters.Adapter` 未注册 ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化 ({ref}`nonebot.init ` 尚未调用) 用法: ```python from nonebot.adapters.console import Adapter adapter = nonebot.get_adapter(Adapter) ``` """ adapters = get_adapters() target = name if isinstance(name, str) else name.get_name() if target not in adapters: raise ValueError(f"Adapter {target} not registered.") return adapters[target] def get_adapters() -> dict[str, Adapter]: """获取所有已注册的 {ref}`nonebot.adapters.Adapter` 实例。 返回: 所有 {ref}`nonebot.adapters.Adapter` 实例字典 异常: ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化 ({ref}`nonebot.init ` 尚未调用) 用法: ```python adapters = nonebot.get_adapters() ``` """ return get_driver()._adapters.copy() def get_app() -> Any: """获取全局 {ref}`nonebot.drivers.ASGIMixin` 对应的 Server App 对象。 返回: Server App 对象 异常: AssertionError: 全局 Driver 对象不是 {ref}`nonebot.drivers.ASGIMixin` 类型 ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化 ({ref}`nonebot.init ` 尚未调用) 用法: ```python app = nonebot.get_app() ``` """ driver = get_driver() assert isinstance(driver, ASGIMixin), "app object is only available for asgi driver" return driver.server_app def get_asgi() -> Any: """获取全局 {ref}`nonebot.drivers.ASGIMixin` 对应的 [ASGI](https://asgi.readthedocs.io/) 对象。 返回: ASGI 对象 异常: AssertionError: 全局 Driver 对象不是 {ref}`nonebot.drivers.ASGIMixin` 类型 ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化 ({ref}`nonebot.init ` 尚未调用) 用法: ```python asgi = nonebot.get_asgi() ``` """ driver = get_driver() assert isinstance(driver, ASGIMixin), ( "asgi object is only available for asgi driver" ) return driver.asgi def get_bot(self_id: str | None = None) -> Bot: """获取一个连接到 NoneBot 的 {ref}`nonebot.adapters.Bot` 对象。 当提供 `self_id` 时,此函数是 `get_bots()[self_id]` 的简写; 当不提供时,返回一个 {ref}`nonebot.adapters.Bot`。 参数: self_id: 用来识别 {ref}`nonebot.adapters.Bot` 的 {ref}`nonebot.adapters.Bot.self_id` 属性 返回: {ref}`nonebot.adapters.Bot` 对象 异常: KeyError: 对应 self_id 的 Bot 不存在 ValueError: 没有传入 self_id 且没有 Bot 可用 ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化 ({ref}`nonebot.init ` 尚未调用) 用法: ```python assert nonebot.get_bot("12345") == nonebot.get_bots()["12345"] another_unspecified_bot = nonebot.get_bot() ``` """ bots = get_bots() if self_id is not None: return bots[self_id] for bot in bots.values(): return bot raise ValueError("There are no bots to get.") def get_bots() -> dict[str, Bot]: """获取所有连接到 NoneBot 的 {ref}`nonebot.adapters.Bot` 对象。 返回: 一个以 {ref}`nonebot.adapters.Bot.self_id` 为键 {ref}`nonebot.adapters.Bot` 对象为值的字典 异常: ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化 ({ref}`nonebot.init ` 尚未调用) 用法: ```python bots = nonebot.get_bots() ``` """ return get_driver().bots def _resolve_combine_expr(obj_str: str) -> type[Driver]: drivers = obj_str.split("+") DriverClass = resolve_dot_notation( drivers[0], "Driver", default_prefix="nonebot.drivers." ) if len(drivers) == 1: logger.trace(f"Detected driver {DriverClass} with no mixins.") return DriverClass mixins = [ resolve_dot_notation(mixin, "Mixin", default_prefix="nonebot.drivers.") for mixin in drivers[1:] ] logger.trace(f"Detected driver {DriverClass} with mixins {mixins}.") return combine_driver(DriverClass, *mixins) def _log_patcher(record: "loguru.Record"): """使用插件标识优化日志展示""" record["name"] = ( plugin.id_ if (module_name := record["name"]) and (plugin := get_plugin_by_module_name(module_name)) else (module_name and module_name.split(".", maxsplit=1)[0]) ) def init(*, _env_file: DOTENV_TYPE | None = None, **kwargs: Any) -> None: """初始化 NoneBot 以及 全局 {ref}`nonebot.drivers.Driver` 对象。 NoneBot 将会从 .env 文件中读取环境信息,并使用相应的 env 文件配置。 也可以传入自定义的 `_env_file` 来指定 NoneBot 从该文件读取配置。 参数: _env_file: 配置文件名,默认从 `.env.{env_name}` 中读取配置 kwargs: 任意变量,将会存储到 {ref}`nonebot.drivers.Driver.config` 对象里 用法: ```python nonebot.init(database=Database(...)) ``` """ global _driver if not _driver: logger.success("NoneBot is initializing...") env = Env() _env_file = _env_file or f".env.{env.environment}" config = Config( **kwargs, _env_file=( (".env", _env_file) if isinstance(_env_file, (str, os.PathLike)) else _env_file ), ) logger.configure( extra={"nonebot_log_level": config.log_level}, patcher=_log_patcher ) logger.opt(colors=True).info( f"Current Env: {escape_tag(env.environment)}" ) logger.opt(colors=True).debug( f"Loaded Config: {escape_tag(str(model_dump(config)))}" ) DriverClass = _resolve_combine_expr(config.driver) _driver = DriverClass(env, config) def run(*args: Any, **kwargs: Any) -> None: """启动 NoneBot,即运行全局 {ref}`nonebot.drivers.Driver` 对象。 参数: args: 传入 {ref}`nonebot.drivers.Driver.run` 的位置参数 kwargs: 传入 {ref}`nonebot.drivers.Driver.run` 的命名参数 用法: ```python nonebot.run(host="127.0.0.1", port=8080) ``` """ logger.success("Running NoneBot...") get_driver().run(*args, **kwargs) from nonebot.plugin import CommandGroup as CommandGroup from nonebot.plugin import MatcherGroup as MatcherGroup from nonebot.plugin import get_available_plugin_names as get_available_plugin_names from nonebot.plugin import get_loaded_plugins as get_loaded_plugins from nonebot.plugin import get_plugin as get_plugin from nonebot.plugin import get_plugin_by_module_name as get_plugin_by_module_name from nonebot.plugin import get_plugin_config as get_plugin_config from nonebot.plugin import load_all_plugins as load_all_plugins from nonebot.plugin import load_builtin_plugin as load_builtin_plugin from nonebot.plugin import load_builtin_plugins as load_builtin_plugins from nonebot.plugin import load_from_json as load_from_json from nonebot.plugin import load_from_toml as load_from_toml from nonebot.plugin import load_plugin as load_plugin from nonebot.plugin import load_plugins as load_plugins from nonebot.plugin import on as on from nonebot.plugin import on_command as on_command from nonebot.plugin import on_endswith as on_endswith from nonebot.plugin import on_fullmatch as on_fullmatch from nonebot.plugin import on_keyword as on_keyword from nonebot.plugin import on_message as on_message from nonebot.plugin import on_metaevent as on_metaevent from nonebot.plugin import on_notice as on_notice from nonebot.plugin import on_regex as on_regex from nonebot.plugin import on_request as on_request from nonebot.plugin import on_shell_command as on_shell_command from nonebot.plugin import on_startswith as on_startswith from nonebot.plugin import on_type as on_type from nonebot.plugin import require as require ================================================ FILE: nonebot/adapters/__init__.py ================================================ """本模块定义了协议适配基类,各协议请继承以下基类。 使用 {ref}`nonebot.drivers.Driver.register_adapter` 注册适配器。 FrontMatter: mdx: format: md sidebar_position: 0 description: nonebot.adapters 模块 """ from nonebot.internal.adapter import Adapter as Adapter from nonebot.internal.adapter import Bot as Bot from nonebot.internal.adapter import Event as Event from nonebot.internal.adapter import Message as Message from nonebot.internal.adapter import MessageSegment as MessageSegment from nonebot.internal.adapter import MessageTemplate as MessageTemplate __autodoc__ = { "Bot": True, "Event": True, "Adapter": True, "Message": True, "Message.__getitem__": True, "Message.__contains__": True, "Message._construct": True, "MessageSegment": True, "MessageSegment.__str__": True, "MessageSegment.__add__": True, "MessageTemplate": True, } ================================================ FILE: nonebot/compat.py ================================================ """本模块为 Pydantic 版本兼容层模块 为兼容 Pydantic V1 与 V2 版本,定义了一系列兼容函数与类供使用。 FrontMatter: mdx: format: md sidebar_position: 16 description: nonebot.compat 模块 """ from collections.abc import Callable, Generator from dataclasses import dataclass, is_dataclass from functools import cached_property, wraps from typing import ( TYPE_CHECKING, Annotated, Any, Generic, Literal, Protocol, TypeAlias, TypeVar, cast, get_args, get_origin, overload, ) from typing_extensions import ParamSpec, Self, is_typeddict from pydantic import VERSION, BaseModel from nonebot.typing import origin_is_annotated T = TypeVar("T") P = ParamSpec("P") PYDANTIC_V2 = int(VERSION.split(".", 1)[0]) == 2 if TYPE_CHECKING: class _CustomValidationClass(Protocol): @classmethod def __get_validators__(cls) -> Generator[Callable[..., Any], None, None]: ... 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", "ConfigDict", "FieldInfo", "LegacyUnionField", "ModelField", "PydanticUndefined", "PydanticUndefinedType", "Required", "TypeAdapter", "custom_validation", "field_validator", "model_config", "model_dump", "model_fields", "model_validator", "type_validate_json", "type_validate_python", ) __autodoc__ = { "PydanticUndefined": "Pydantic Undefined object", "PydanticUndefinedType": "Pydantic Undefined type", } if PYDANTIC_V2: # pragma: pydantic-v2 from pydantic import Field, GetCoreSchemaHandler from pydantic import TypeAdapter as TypeAdapter from pydantic import field_validator as field_validator from pydantic import model_validator as model_validator from pydantic._internal._repr import display_as_type from pydantic.fields import FieldInfo as BaseFieldInfo from pydantic_core import CoreSchema, core_schema Required = Ellipsis """Alias of Ellipsis for compatibility with pydantic v1""" # Export undefined type from pydantic_core import PydanticUndefined as PydanticUndefined from pydantic_core import PydanticUndefinedType as PydanticUndefinedType # isort: split # Export model config dict from pydantic import ConfigDict as ConfigDict DEFAULT_CONFIG = ConfigDict(extra="allow", arbitrary_types_allowed=True) """Default config for validations""" def _get_legacy_union_field(func: Callable[P, T]) -> Callable[P, T]: @wraps(func) def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: kwargs["union_mode"] = "left_to_right" return func(*args, **kwargs) return wrapper LegacyUnionField = _get_legacy_union_field(Field) LegacyUnionField.__doc__ = "Mark field to use legacy left to right union mode" class FieldInfo(BaseFieldInfo): # pyright: ignore[reportGeneralTypeIssues] """FieldInfo class with extra property for compatibility with pydantic v1""" # make default can be positional argument def __init__(self, default: Any = PydanticUndefined, **kwargs: Any) -> None: super().__init__(default=default, **kwargs) @property def extra(self) -> dict[str, Any]: """Extra data that is not part of the standard pydantic fields. For compatibility with pydantic v1. """ # extract extra data from attributes set except used slots # we need to call super in advance due to # comprehension not inlined in cpython < 3.12 # https://peps.python.org/pep-0709/ slots = super().__slots__ return {k: v for k, v in self._attributes_set.items() if k not in slots} @classmethod def _inherit_construct( cls, field_info: BaseFieldInfo | None = None, **kwargs: Any ) -> Self: init_kwargs = {} if field_info: init_kwargs.update(field_info._attributes_set) init_kwargs.update(kwargs) instance = cls(**init_kwargs) if field_info: instance.metadata = field_info.metadata return instance @dataclass class ModelField: """ModelField class for compatibility with pydantic v1""" name: str """The name of the field.""" annotation: Any """The annotation of the field.""" field_info: FieldInfo """The FieldInfo of the field.""" @classmethod def _construct(cls, name: str, annotation: Any, field_info: FieldInfo) -> Self: return cls(name, annotation, field_info) @classmethod def construct( cls, name: str, annotation: Any, field_info: FieldInfo | None = None ) -> Self: """Construct a ModelField from given infos.""" return cls._construct(name, annotation, field_info or FieldInfo()) def __hash__(self) -> int: # Each ModelField is unique for our purposes, # to allow store them in a set. return id(self) @cached_property def type_adapter(self) -> TypeAdapter: """TypeAdapter of the field. Cache the TypeAdapter to avoid creating it multiple times. Pydantic v2 uses too much cpu time to create TypeAdapter. See: https://github.com/pydantic/pydantic/issues/9834 """ return TypeAdapter( Annotated[self.annotation, self.field_info], config=None if self._annotation_has_config() else DEFAULT_CONFIG, ) def _annotation_has_config(self) -> bool: """Check if the annotation has config. TypeAdapter raise error when annotation has config and given config is not None. """ type_is_annotated = origin_is_annotated(get_origin(self.annotation)) inner_type = ( get_args(self.annotation)[0] if type_is_annotated else self.annotation ) try: return ( issubclass(inner_type, BaseModel) or is_dataclass(inner_type) or is_typeddict(inner_type) ) except TypeError: return False def get_default(self) -> Any: """Get the default value of the field.""" return self.field_info.get_default(call_default_factory=True) def _type_display(self): """Get the display of the type of the field.""" return display_as_type(self.annotation) def validate_value(self, value: Any) -> Any: """Validate the value pass to the field.""" return self.type_adapter.validate_python(value) def model_fields(model: type[BaseModel]) -> list[ModelField]: """Get field list of a model.""" return [ ModelField._construct( name=name, annotation=field_info.rebuild_annotation(), field_info=FieldInfo._inherit_construct(field_info), ) for name, field_info in model.model_fields.items() ] def model_config(model: type[BaseModel]) -> Any: """Get config of a model.""" return model.model_config def model_dump( model: BaseModel, 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( # 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, exclude_none=exclude_none, ) def type_validate_python(type_: type[T], data: Any) -> T: """Validate data with given type.""" return TypeAdapter(type_).validate_python(data) def type_validate_json(type_: type[T], data: str | bytes) -> T: """Validate JSON with given type.""" return TypeAdapter(type_).validate_json(data) def __get_pydantic_core_schema__( cls: type["_CustomValidationClass"], source_type: Any, handler: GetCoreSchemaHandler, ) -> CoreSchema: validators = list(cls.__get_validators__()) if len(validators) == 1: return core_schema.no_info_plain_validator_function(validators[0]) return core_schema.chain_schema( [core_schema.no_info_plain_validator_function(func) for func in validators] ) def custom_validation(class_: type["CVC"]) -> type["CVC"]: """Use pydantic v1 like validator generator in pydantic v2""" setattr( class_, "__get_pydantic_core_schema__", classmethod(__get_pydantic_core_schema__), ) return class_ else: # pragma: pydantic-v1 from pydantic import BaseConfig as PydanticConfig from pydantic import Extra, parse_obj_as, parse_raw_as, root_validator, validator from pydantic.fields import FieldInfo as BaseFieldInfo from pydantic.fields import ModelField as BaseModelField from pydantic.schema import get_annotation_from_field_info # isort: split from pydantic.fields import Required as Required # isort: split from pydantic.fields import Undefined as PydanticUndefined from pydantic.fields import UndefinedType as PydanticUndefinedType class ConfigDict(PydanticConfig): """Config class that allow get value with default value.""" @classmethod def get(cls, field: str, default: Any = None) -> Any: """Get a config value.""" return getattr(cls, field, default) class DEFAULT_CONFIG(ConfigDict): extra = Extra.allow arbitrary_types_allowed = True from pydantic.fields import Field as LegacyUnionField class FieldInfo(BaseFieldInfo): def __init__(self, default: Any = PydanticUndefined, **kwargs: Any): # preprocess default value to make it compatible with pydantic v2 # when default is Required, set it to PydanticUndefined if default is Required: default = PydanticUndefined super().__init__(default, **kwargs) @classmethod def _inherit_construct( cls, field_info: BaseFieldInfo | None = None, **kwargs: Any ): if field_info: init_kwargs = { s: getattr(field_info, s) for s in field_info.__slots__ if s != "extra" } init_kwargs.update(field_info.extra) else: init_kwargs = {} init_kwargs.update(kwargs) return cls(**init_kwargs) class ModelField(BaseModelField): @classmethod def _construct(cls, name: str, annotation: Any, field_info: FieldInfo) -> Self: return cls( name=name, type_=annotation, class_validators=None, model_config=DEFAULT_CONFIG, default=field_info.default, default_factory=field_info.default_factory, required=( field_info.default is PydanticUndefined and field_info.default_factory is None ), field_info=field_info, ) @classmethod def construct( cls, name: str, annotation: Any, field_info: FieldInfo | None = None ) -> Self: """Construct a ModelField from given infos. Field annotation is preprocessed with field_info. """ if field_info is not None: annotation = get_annotation_from_field_info( annotation, field_info, name ) return cls._construct(name, annotation, field_info or FieldInfo()) def validate_value(self, value: Any) -> Any: """Validate the value pass to the field.""" v, errs_ = self.validate(value, {}, loc=()) if errs_: raise ValueError(value, self) return v class TypeAdapter(Generic[T]): @overload def __init__( self, type: type[T], *, config: ConfigDict | None = ..., ) -> None: ... @overload def __init__( self, type: Any, *, config: ConfigDict | None = ..., ) -> None: ... def __init__( self, type: Any, *, config: ConfigDict | None = None, ) -> None: self.type = type self.config = config def validate_python(self, value: Any) -> T: return type_validate_python(self.type, value) def validate_json(self, value: str | bytes) -> T: return type_validate_json(self.type, value) @overload def field_validator( field: str, /, *fields: str, mode: Literal["before"], check_fields: bool | None = None, ): ... @overload def field_validator( field: str, /, *fields: str, mode: Literal["after"] = ..., check_fields: bool | None = None, ): ... def field_validator( field: str, /, *fields: str, mode: Literal["before", "after"] = "after", check_fields: bool | None = None, ): if mode == "before": return validator( field, *fields, pre=True, check_fields=check_fields or True, allow_reuse=True, ) else: return validator( field, *fields, check_fields=check_fields or True, allow_reuse=True ) def model_fields(model: type[BaseModel]) -> list[ModelField]: """Get field list of a model.""" # construct the model field without preprocess to avoid error return [ ModelField._construct( name=model_field.name, annotation=model_field.annotation, field_info=FieldInfo._inherit_construct(model_field.field_info), ) for model_field in model.__fields__.values() ] def model_config(model: type[BaseModel]) -> Any: """Get config of a model.""" return model.__config__ def model_dump( model: BaseModel, 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=cast(Any, include), exclude=cast(Any, exclude), by_alias=by_alias, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, exclude_none=exclude_none, ) @overload def model_validator(*, mode: Literal["before"]): ... @overload def model_validator(*, mode: Literal["after"]): ... def model_validator(*, mode: Literal["before", "after"]): if mode == "before": return root_validator(pre=True, allow_reuse=True) else: return root_validator(skip_on_failure=True, allow_reuse=True) def type_validate_python(type_: type[T], data: Any) -> T: """Validate data with given type.""" return parse_obj_as(type_, data) def type_validate_json(type_: type[T], data: str | bytes) -> T: """Validate JSON with given type.""" return parse_raw_as(type_, data) def custom_validation(class_: type["CVC"]) -> type["CVC"]: """Do nothing in pydantic v1""" return class_ ================================================ FILE: nonebot/config.py ================================================ """本模块定义了 NoneBot 本身运行所需的配置项。 NoneBot 使用 [`pydantic`](https://pydantic-docs.helpmanual.io/) 以及 [`python-dotenv`](https://saurabh-kumar.com/python-dotenv/) 来读取配置。 配置项需符合特殊格式或 json 序列化格式 详情见 [`pydantic Field Type`](https://pydantic-docs.helpmanual.io/usage/types/) 文档。 FrontMatter: mdx: format: md sidebar_position: 1 description: nonebot.config 模块 """ import abc from collections.abc import Mapping from datetime import timedelta from ipaddress import IPv4Address import json import os from pathlib import Path from typing import TYPE_CHECKING, Any, TypeAlias, get_args, get_origin from dotenv import dotenv_values from pydantic import BaseModel, Field from pydantic.networks import IPvAnyAddress from nonebot.compat import ( PYDANTIC_V2, ConfigDict, LegacyUnionField, ModelField, PydanticUndefined, PydanticUndefinedType, model_config, model_fields, ) from nonebot.log import logger from nonebot.typing import origin_is_union from nonebot.utils import deep_update, lenient_issubclass, type_is_complex DOTENV_TYPE: TypeAlias = Path | str | list[Path | str] | tuple[Path | str, ...] ENV_FILE_SENTINEL = Path("") class SettingsError(ValueError): ... class BaseSettingsSource(abc.ABC): def __init__(self, settings_cls: type[BaseModel]) -> None: self.settings_cls = settings_cls @property def config(self) -> "SettingsConfig": return model_config(self.settings_cls) @abc.abstractmethod def __call__(self) -> dict[str, Any]: raise NotImplementedError class InitSettingsSource(BaseSettingsSource): __slots__ = ("init_kwargs",) def __init__( self, settings_cls: type[BaseModel], init_kwargs: dict[str, Any] ) -> None: self.init_kwargs = init_kwargs super().__init__(settings_cls) def __call__(self) -> dict[str, Any]: return self.init_kwargs def __repr__(self) -> str: return f"InitSettingsSource(init_kwargs={self.init_kwargs!r})" class DotEnvSettingsSource(BaseSettingsSource): def __init__( self, settings_cls: type[BaseModel], env_file: DOTENV_TYPE | None, env_file_encoding: str, case_sensitive: bool | None = False, env_nested_delimiter: str | None = None, ) -> None: super().__init__(settings_cls) self.env_file = env_file self.env_file_encoding = env_file_encoding self.case_sensitive = case_sensitive self.env_nested_delimiter = env_nested_delimiter def _apply_case_sensitive(self, var_name: str) -> str: return var_name if self.case_sensitive else var_name.lower() def _field_is_complex(self, field: ModelField) -> tuple[bool, bool]: if type_is_complex(field.annotation): return True, False elif origin_is_union(get_origin(field.annotation)) and any( type_is_complex(arg) for arg in get_args(field.annotation) ): return True, True return False, False def _parse_env_vars( self, env_vars: Mapping[str, str | None] ) -> dict[str, str | None]: return { self._apply_case_sensitive(key): value for key, value in env_vars.items() } def _read_env_file(self, file_path: Path) -> dict[str, str | None]: file_vars = dotenv_values(file_path, encoding=self.env_file_encoding) return self._parse_env_vars(file_vars) def _read_env_files(self) -> dict[str, str | None]: env_files = self.env_file if env_files is None: return {} if isinstance(env_files, (str, os.PathLike)): env_files = [env_files] dotenv_vars: dict[str, str | None] = {} for env_file in env_files: env_path = Path(env_file).expanduser() if env_path.is_file(): dotenv_vars.update(self._read_env_file(env_path)) return dotenv_vars def _next_field(self, field: ModelField | None, key: str) -> ModelField | None: if not field or origin_is_union(get_origin(field.annotation)): return None elif field.annotation and lenient_issubclass(field.annotation, BaseModel): for field in model_fields(field.annotation): if field.name == key: return field return None def _explode_env_vars( self, field: ModelField, env_vars: dict[str, str | None], env_file_vars: dict[str, str | None], ) -> dict[str, Any]: if self.env_nested_delimiter is None: return {} prefix = f"{field.name}{self.env_nested_delimiter}" result: dict[str, Any] = {} for env_name, env_val in env_vars.items(): if not env_name.startswith(prefix): continue # delete from file vars when used env_file_vars.pop(env_name, None) _, *keys, last_key = env_name.split(self.env_nested_delimiter) env_var = result target_field: ModelField | None = field for key in keys: target_field = self._next_field(target_field, key) env_var = env_var.setdefault(key, {}) target_field = self._next_field(target_field, last_key) if target_field and env_val: is_complex, allow_parse_failure = self._field_is_complex(target_field) if is_complex: try: env_val = json.loads(env_val) except ValueError as e: if not allow_parse_failure: raise SettingsError( f'error parsing env var "{env_name}"' ) from e env_var[last_key] = env_val return result def __call__(self) -> dict[str, Any]: """从环境变量和 dotenv 配置文件中读取配置项。""" d: dict[str, Any] = {} env_vars = self._parse_env_vars(os.environ) env_file_vars = self._read_env_files() env_vars = {**env_file_vars, **env_vars} for field in model_fields(self.settings_cls): field_name = field.name env_name = self._apply_case_sensitive(field_name) alias_name = field.field_info.alias alias_env_name = ( None if alias_name is None else self._apply_case_sensitive(alias_name) ) # pydantic use alias name to validate if exist if alias_name is not None: field_name = alias_name # try get values from env vars env_val = env_vars.get(env_name, PydanticUndefined) alias_env_val = ( PydanticUndefined if alias_env_name is None else env_vars.get(alias_env_name, PydanticUndefined) ) # alias env value has higher priority env_val = ( env_val if isinstance(alias_env_val, PydanticUndefinedType) else alias_env_val ) # delete from file vars when used if env_name in env_file_vars: del env_file_vars[env_name] if alias_env_name is not None and alias_env_name in env_file_vars: del env_file_vars[alias_env_name] is_complex, allow_parse_failure = self._field_is_complex(field) if is_complex: if isinstance(env_val, PydanticUndefinedType): # field is complex but no value found so far, try explode_env_vars if env_val_built := self._explode_env_vars( field, env_vars, env_file_vars ): d[field_name] = env_val_built elif env_val is None: d[field_name] = env_val else: # field is complex and there's a value # decode that as JSON, then add explode_env_vars try: env_val = json.loads(env_val) except ValueError as e: if not allow_parse_failure: raise SettingsError( f'error parsing env var "{env_name}"' ) from e if isinstance(env_val, dict): # field value is a dict # try explode_env_vars to find more sub-values d[field_name] = deep_update( env_val, self._explode_env_vars(field, env_vars, env_file_vars), ) else: d[field_name] = env_val elif env_val is not PydanticUndefined: # simplest case, field is not complex # we only need to add the value if it was found d[field_name] = env_val # remain user custom config for env_name in env_file_vars: env_val = env_vars[env_name] if env_val and (val_striped := env_val.strip()): # there's a value, decode that as JSON try: env_val = json.loads(val_striped) except ValueError: logger.trace( "Error while parsing JSON for " f"{env_name!r}={val_striped!r}. " "Assumed as string." ) # explode value when it's a nested dict env_name, *nested_keys = env_name.split(self.env_nested_delimiter) if nested_keys and (env_name not in d or isinstance(d[env_name], dict)): result = {} *keys, last_key = nested_keys _tmp = result for key in keys: _tmp = _tmp.setdefault(key, {}) _tmp[last_key] = env_val d[env_name] = deep_update(d.get(env_name, {}), result) elif not nested_keys: d[env_name] = env_val return d if PYDANTIC_V2: # pragma: pydantic-v2 class SettingsConfig(ConfigDict, total=False): env_file: DOTENV_TYPE | None env_file_encoding: str case_sensitive: bool env_nested_delimiter: str | None else: # pragma: pydantic-v1 class SettingsConfig(ConfigDict): env_file: DOTENV_TYPE | None env_file_encoding: str case_sensitive: bool env_nested_delimiter: str | None class BaseSettings(BaseModel): if TYPE_CHECKING: # dummy getattr for pylance checking, actually not used def __getattr__(self, name: str) -> Any: # pragma: no cover return self.__dict__.get(name) if PYDANTIC_V2: # pragma: pydantic-v2 model_config = SettingsConfig( extra="allow", env_file=".env", env_file_encoding="utf-8", case_sensitive=False, env_nested_delimiter="__", ) else: # pragma: pydantic-v1 class Config(SettingsConfig): extra = "allow" # type: ignore env_file = ".env" env_file_encoding = "utf-8" case_sensitive = False env_nested_delimiter = "__" def __init__( __settings_self__, # pyright: ignore[reportSelfClsParameterName] _env_file: DOTENV_TYPE | None = ENV_FILE_SENTINEL, _env_file_encoding: str | None = None, _env_nested_delimiter: str | None = None, **values: Any, ) -> None: settings_config = model_config(__settings_self__.__class__) env_file = ( _env_file if _env_file is not ENV_FILE_SENTINEL else settings_config.get("env_file", (".env",)) ) env_file_encoding = ( _env_file_encoding if _env_file_encoding is not None else settings_config.get("env_file_encoding", "utf-8") ) env_nested_delimiter = ( _env_nested_delimiter if _env_nested_delimiter is not None else settings_config.get("env_nested_delimiter", None) ) super().__init__( **__settings_self__._settings_build_values( __settings_self__.__class__, values, env_file=env_file, env_file_encoding=env_file_encoding, env_nested_delimiter=env_nested_delimiter, ) ) __settings_self__._env_file = env_file __settings_self__._env_file_encoding = env_file_encoding __settings_self__._env_nested_delimiter = env_nested_delimiter @staticmethod def _settings_build_values( settings_cls: type[BaseModel], init_kwargs: dict[str, Any], env_file: DOTENV_TYPE | None, env_file_encoding: str, env_nested_delimiter: str | None, ) -> dict[str, Any]: init_settings = InitSettingsSource(settings_cls, init_kwargs=init_kwargs) env_settings = DotEnvSettingsSource( settings_cls, env_file=env_file, env_file_encoding=env_file_encoding, env_nested_delimiter=env_nested_delimiter, ) return deep_update(env_settings(), init_settings()) class Env(BaseSettings): """运行环境配置。大小写不敏感。 将会从 **环境变量** > **dotenv 配置文件** 的优先级读取环境信息。 """ environment: str = "prod" """当前环境名。 NoneBot 将从 `.env.{environment}` 文件中加载配置。 """ class Config(BaseSettings): """NoneBot 主要配置。大小写不敏感。 除了 NoneBot 的配置项外,还可以自行添加配置项到 `.env.{environment}` 文件中。 这些配置将会在 json 反序列化后一起带入 `Config` 类中。 配置方法参考: [配置](https://nonebot.dev/docs/appendices/config) """ if TYPE_CHECKING: _env_file: DOTENV_TYPE | None = ".env", ".env.prod" # nonebot configs driver: str = "~fastapi" """NoneBot 运行所使用的 `Driver` 。继承自 {ref}`nonebot.drivers.Driver` 。 配置格式为 `[:][+[:]]*`。 `~` 为 `nonebot.drivers.` 的缩写。 配置方法参考: [配置驱动器](https://nonebot.dev/docs/advanced/driver#%E9%85%8D%E7%BD%AE%E9%A9%B1%E5%8A%A8%E5%99%A8) """ host: IPvAnyAddress = IPv4Address("127.0.0.1") # type: ignore """NoneBot {ref}`nonebot.drivers.ReverseDriver` 服务端监听的 IP/主机名。""" port: int = Field(default=8080, ge=1, le=65535) """NoneBot {ref}`nonebot.drivers.ReverseDriver` 服务端监听的端口。""" log_level: int | str = LegacyUnionField(default="INFO") """NoneBot 日志输出等级,可以为 `int` 类型等级或等级名称。 参考 [记录日志](https://nonebot.dev/docs/appendices/log),[loguru 日志等级](https://loguru.readthedocs.io/en/stable/api/logger.html#levels)。 :::tip 提示 日志等级名称应为大写,如 `INFO`。 ::: 用法: ```conf LOG_LEVEL=25 LOG_LEVEL=INFO ``` """ # bot connection configs api_timeout: float | None = 30.0 """API 请求超时时间,单位: 秒。""" # bot runtime configs superusers: set[str] = set() """机器人超级用户。 用法: ```conf SUPERUSERS=["12345789"] ``` """ nickname: set[str] = set() """机器人昵称。""" command_start: set[str] = {"/"} """命令的起始标记,用于判断一条消息是不是命令。 参考[命令响应规则](https://nonebot.dev/docs/advanced/matcher#command)。 用法: ```conf COMMAND_START=["/", ""] ``` """ command_sep: set[str] = {"."} """命令的分隔标记,用于将文本形式的命令切分为元组(实际的命令名)。 参考[命令响应规则](https://nonebot.dev/docs/advanced/matcher#command)。 用法: ```conf COMMAND_SEP=["."] ``` """ session_expire_timeout: timedelta = timedelta(minutes=2) """等待用户回复的超时时间。 用法: ```conf SESSION_EXPIRE_TIMEOUT=[-][DD]D[,][HH:MM:]SS[.ffffff] SESSION_EXPIRE_TIMEOUT=[±]P[DD]DT[HH]H[MM]M[SS]S # ISO 8601 ``` """ # adapter configs # adapter configs are defined in adapter/config.py # custom configs # custom configs can be assigned during nonebot.init # or from env file using json loads if PYDANTIC_V2: # pragma: pydantic-v2 model_config = SettingsConfig(env_file=(".env", ".env.prod")) else: # pragma: pydantic-v1 class Config( # pyright: ignore[reportIncompatibleVariableOverride] SettingsConfig ): env_file = ".env", ".env.prod" __autodoc__ = { "SettingsError": False, "BaseSettingsSource": False, "InitSettingsSource": False, "DotEnvSettingsSource": False, "SettingsConfig": False, "BaseSettings": False, } ================================================ FILE: nonebot/consts.py ================================================ """本模块包含了 NoneBot 事件处理过程中使用到的常量。 FrontMatter: mdx: format: md sidebar_position: 9 description: nonebot.consts 模块 """ import os import sys from typing import Literal # used by Matcher RECEIVE_KEY: Literal["_receive_{id}"] = "_receive_{id}" """`receive` 存储 key""" LAST_RECEIVE_KEY: Literal["_last_receive"] = "_last_receive" """`last_receive` 存储 key""" ARG_KEY: Literal["{key}"] = "{key}" """`arg` 存储 key""" REJECT_TARGET: Literal["_current_target"] = "_current_target" """当前 `reject` 目标存储 key""" REJECT_CACHE_TARGET: Literal["_next_target"] = "_next_target" """下一个 `reject` 目标存储 key""" PAUSE_PROMPT_RESULT_KEY: Literal["_pause_result"] = "_pause_result" """`pause` prompt 发送结果存储 key""" REJECT_PROMPT_RESULT_KEY: Literal["_reject_{key}_result"] = "_reject_{key}_result" """`reject` prompt 发送结果存储 key""" # used by Rule PREFIX_KEY: Literal["_prefix"] = "_prefix" """命令前缀存储 key""" CMD_KEY: Literal["command"] = "command" """命令元组存储 key""" RAW_CMD_KEY: Literal["raw_command"] = "raw_command" """命令文本存储 key""" CMD_ARG_KEY: Literal["command_arg"] = "command_arg" """命令参数存储 key""" CMD_START_KEY: Literal["command_start"] = "command_start" """命令开头存储 key""" CMD_WHITESPACE_KEY: Literal["command_whitespace"] = "command_whitespace" """命令与参数间空白符存储 key""" SHELL_ARGS: Literal["_args"] = "_args" """shell 命令 parse 后参数字典存储 key""" SHELL_ARGV: Literal["_argv"] = "_argv" """shell 命令原始参数列表存储 key""" REGEX_MATCHED: Literal["_matched"] = "_matched" """正则匹配结果存储 key""" STARTSWITH_KEY: Literal["_startswith"] = "_startswith" """响应触发前缀 key""" ENDSWITH_KEY: Literal["_endswith"] = "_endswith" """响应触发后缀 key""" FULLMATCH_KEY: Literal["_fullmatch"] = "_fullmatch" """响应触发完整消息 key""" KEYWORD_KEY: Literal["_keyword"] = "_keyword" """响应触发关键字 key""" WINDOWS = sys.platform.startswith("win") or (sys.platform == "cli" and os.name == "nt") ================================================ FILE: nonebot/dependencies/__init__.py ================================================ """本模块模块实现了依赖注入的定义与处理。 FrontMatter: mdx: format: md sidebar_position: 0 description: nonebot.dependencies 模块 """ import abc from collections.abc import Awaitable, Callable, Iterable from dataclasses import dataclass, field from functools import partial import inspect from typing import Any, Generic, TypeVar, cast import anyio from exceptiongroup import BaseExceptionGroup, catch from nonebot.compat import FieldInfo, ModelField, PydanticUndefined from nonebot.exception import SkippedException from nonebot.log import logger from nonebot.typing import _DependentCallable from nonebot.utils import ( flatten_exception_group, is_coroutine_callable, run_coro_with_shield, run_sync, ) from .utils import check_field_type, get_typed_signature R = TypeVar("R") T = TypeVar("T", bound="Dependent") class Param(abc.ABC, FieldInfo): """依赖注入的基本单元 —— 参数。 继承自 `pydantic.fields.FieldInfo`,用于描述参数信息(不包括参数名)。 """ def __init__(self, *args, validate: bool = False, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.validate = validate @classmethod def _check_param( cls, param: inspect.Parameter, allow_types: tuple[type["Param"], ...] ) -> "Param | None": return @classmethod def _check_parameterless( cls, value: Any, allow_types: tuple[type["Param"], ...] ) -> "Param | None": return @abc.abstractmethod async def _solve(self, **kwargs: Any) -> Any: raise NotImplementedError async def _check(self, **kwargs: Any) -> None: return @dataclass(frozen=True) class Dependent(Generic[R]): """依赖注入容器 参数: call: 依赖注入的可调用对象,可以是任何 Callable 对象 pre_checkers: 依赖注入解析前的参数检查 params: 具名参数列表 parameterless: 匿名参数列表 allow_types: 允许的参数类型 """ call: _DependentCallable[R] params: tuple[ModelField, ...] = field(default_factory=tuple) parameterless: tuple[Param, ...] = field(default_factory=tuple) def __repr__(self) -> str: if inspect.isfunction(self.call) or inspect.isclass(self.call): call_str = self.call.__name__ else: call_str = repr(self.call) return ( f"Dependent(call={call_str}" + (f", parameterless={self.parameterless}" if self.parameterless else "") + ")" ) async def __call__(self, **kwargs: Any) -> R: exception: BaseExceptionGroup[SkippedException] | None = None def _handle_skipped(exc_group: BaseExceptionGroup[SkippedException]): nonlocal exception exception = exc_group # raise one of the exceptions instead excs = list(flatten_exception_group(exc_group)) logger.trace(f"{self} skipped due to {excs}") with catch({SkippedException: _handle_skipped}): # do pre-check await self.check(**kwargs) # solve param values values = await self.solve(**kwargs) # call function if is_coroutine_callable(self.call): return await cast(Callable[..., Awaitable[R]], self.call)(**values) else: return await run_sync(cast(Callable[..., R], self.call))(**values) raise exception @staticmethod def parse_params( call: _DependentCallable[R], allow_types: tuple[type[Param], ...] ) -> tuple[ModelField, ...]: fields: list[ModelField] = [] params = get_typed_signature(call).parameters.values() for param in params: if isinstance(param.default, Param): field_info = param.default else: for allow_type in allow_types: if field_info := allow_type._check_param(param, allow_types): break else: raise ValueError( f"Unknown parameter {param.name} " f"for function {call} with type {param.annotation}" ) annotation: Any = Any if param.annotation is not param.empty: annotation = param.annotation fields.append( ModelField.construct( name=param.name, annotation=annotation, field_info=field_info ) ) return tuple(fields) @staticmethod def parse_parameterless( parameterless: tuple[Any, ...], allow_types: tuple[type[Param], ...] ) -> tuple[Param, ...]: parameterless_params: list[Param] = [] for value in parameterless: for allow_type in allow_types: if param := allow_type._check_parameterless(value, allow_types): break else: raise ValueError(f"Unknown parameterless {value}") parameterless_params.append(param) return tuple(parameterless_params) @classmethod def parse( cls, *, call: _DependentCallable[R], parameterless: Iterable[Any] | None = None, allow_types: Iterable[type[Param]], ) -> "Dependent[R]": allow_types = tuple(allow_types) params = cls.parse_params(call, allow_types) parameterless_params = ( () if parameterless is None else cls.parse_parameterless(tuple(parameterless), allow_types) ) return cls(call, params, parameterless_params) async def check(self, **params: Any) -> None: if self.parameterless: async with anyio.create_task_group() as tg: for param in self.parameterless: tg.start_soon(partial(param._check, **params)) if self.params: async with anyio.create_task_group() as tg: for param in self.params: tg.start_soon( partial(cast(Param, param.field_info)._check, **params) ) async def _solve_field(self, field: ModelField, params: dict[str, Any]) -> Any: param = cast(Param, field.field_info) value = await param._solve(**params) if value is PydanticUndefined: value = field.get_default() v = check_field_type(field, value) return v if param.validate else value async def solve(self, **params: Any) -> dict[str, Any]: # solve parameterless for param in self.parameterless: await param._solve(**params) # solve param values result: dict[str, Any] = {} if not self.params: return result async def _solve_field(field: ModelField, params: dict[str, Any]) -> None: value = await self._solve_field(field, params) result[field.name] = value async with anyio.create_task_group() as tg: for field in self.params: # shield the task to prevent cancellation # when one of the tasks raises an exception # this will improve the dependency cache reusability tg.start_soon(run_coro_with_shield, _solve_field(field, params)) return result __autodoc__ = {"CustomConfig": False} ================================================ FILE: nonebot/dependencies/utils.py ================================================ """ FrontMatter: mdx: format: md sidebar_position: 1 description: nonebot.dependencies.utils 模块 """ from collections.abc import Callable import inspect from typing import Any, ForwardRef, cast from typing_extensions import TypeAliasType from loguru import logger from nonebot.compat import ModelField from nonebot.exception import TypeMisMatch from nonebot.typing import evaluate_forwardref, is_type_alias_type def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature: """获取可调用对象签名""" signature = inspect.signature(call) globalns = getattr(call, "__globals__", {}) typed_params = [ inspect.Parameter( name=param.name, kind=param.kind, default=param.default, annotation=get_typed_annotation(param, globalns), ) for param in signature.parameters.values() ] return inspect.Signature(typed_params) def get_typed_annotation(param: inspect.Parameter, globalns: dict[str, Any]) -> Any: """获取参数的类型注解""" annotation = param.annotation if isinstance(annotation, str): annotation = ForwardRef(annotation) try: annotation = evaluate_forwardref(annotation, globalns, globalns) except Exception as e: logger.opt(colors=True, exception=e).warning( f'Unknown ForwardRef["{param.annotation}"] for parameter {param.name}' ) return inspect.Parameter.empty if is_type_alias_type(annotation): # Python 3.12+ supports PEP 695 TypeAliasType annotation = cast(TypeAliasType, annotation).__value__ return annotation def check_field_type(field: ModelField, value: Any) -> Any: """检查字段类型是否匹配""" try: return field.validate_value(value) except ValueError: raise TypeMisMatch(field, value) ================================================ FILE: nonebot/drivers/__init__.py ================================================ """本模块定义了驱动适配器基类。 各驱动请继承以下基类。 FrontMatter: mdx: format: md sidebar_position: 0 description: nonebot.drivers 模块 """ from nonebot.internal.driver import URL as URL from nonebot.internal.driver import ASGIMixin as ASGIMixin from nonebot.internal.driver import Cookies as Cookies from nonebot.internal.driver import Driver as Driver from nonebot.internal.driver import ForwardDriver as ForwardDriver from nonebot.internal.driver import ForwardMixin as ForwardMixin from nonebot.internal.driver import HTTPClientMixin as HTTPClientMixin from nonebot.internal.driver import HTTPClientSession as HTTPClientSession from nonebot.internal.driver import HTTPServerSetup as HTTPServerSetup from nonebot.internal.driver import HTTPVersion as HTTPVersion from nonebot.internal.driver import Mixin as Mixin from nonebot.internal.driver import Request as Request from nonebot.internal.driver import Response as Response from nonebot.internal.driver import ReverseDriver as ReverseDriver from nonebot.internal.driver import ReverseMixin as ReverseMixin from nonebot.internal.driver import Timeout as Timeout from nonebot.internal.driver import WebSocket as WebSocket from nonebot.internal.driver import WebSocketClientMixin as WebSocketClientMixin from nonebot.internal.driver import WebSocketServerSetup as WebSocketServerSetup from nonebot.internal.driver import combine_driver as combine_driver __autodoc__ = { "URL": True, "Cookies": True, "Request": True, "Response": True, "Timeout": True, "WebSocket": True, "HTTPVersion": True, "Driver": True, "Mixin": True, "ForwardMixin": True, "ForwardDriver": True, "HTTPClientMixin": True, "WebSocketClientMixin": True, "ReverseMixin": True, "ReverseDriver": True, "ASGIMixin": True, "combine_driver": True, "HTTPServerSetup": True, "WebSocketServerSetup": True, } ================================================ FILE: nonebot/drivers/aiohttp.py ================================================ """[AIOHTTP](https://aiohttp.readthedocs.io/en/stable/) 驱动适配器。 ```bash nb driver install aiohttp # 或者 pip install nonebot2[aiohttp] ``` :::tip 提示 本驱动仅支持客户端连接 ::: FrontMatter: mdx: format: md sidebar_position: 2 description: nonebot.drivers.aiohttp 模块 """ from collections.abc import AsyncGenerator from contextlib import asynccontextmanager from typing import TYPE_CHECKING from typing_extensions import override from multidict import CIMultiDict from nonebot.drivers import ( URL, HTTPClientMixin, HTTPClientSession, HTTPVersion, Request, Response, 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.internal.driver import ( Cookies, CookieTypes, HeaderTypes, QueryTypes, Timeout, TimeoutTypes, ) try: import aiohttp except ModuleNotFoundError as e: # pragma: no cover raise ImportError( "Please install aiohttp first to use this driver. " "Install with pip: `pip install nonebot2[aiohttp]`" ) from e class Session(HTTPClientSession): @override def __init__( self, params: QueryTypes = None, headers: HeaderTypes = None, cookies: CookieTypes = None, version: str | HTTPVersion = HTTPVersion.H11, timeout: TimeoutTypes = None, proxy: str | None = None, ): self._client: aiohttp.ClientSession | None = None self._params = URL.build(query=params).query if params is not None else None self._headers = CIMultiDict(headers) if headers is not None else None self._cookies = tuple( (cookie.name, cookie.value) for cookie in Cookies(cookies) if cookie.value is not None ) version = HTTPVersion(version) if version == HTTPVersion.H10: self._version = aiohttp.HttpVersion10 elif version == HTTPVersion.H11: self._version = aiohttp.HttpVersion11 else: raise RuntimeError(f"Unsupported HTTP version: {version}") if isinstance(timeout, Timeout): self._timeout = aiohttp.ClientTimeout( total=timeout.total, connect=timeout.connect, sock_read=timeout.read, ) else: self._timeout = aiohttp.ClientTimeout(timeout) self._proxy = proxy @property def client(self) -> aiohttp.ClientSession: if self._client is None: raise RuntimeError("Session is not initialized") return self._client @override async def request(self, setup: Request) -> Response: if self._params: url = setup.url.with_query({**self._params, **setup.url.query}) else: url = setup.url data = setup.data if setup.files: data = aiohttp.FormData(data or {}, quote_fields=False) for name, file in setup.files: data.add_field(name, file[1], content_type=file[2], filename=file[0]) cookies = ( (cookie.name, cookie.value) for cookie in setup.cookies 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, data=setup.content or data, json=setup.json, cookies=cookies, headers=setup.headers, proxy=setup.proxy or self._proxy, timeout=timeout, ) as response: return Response( response.status, headers=response.headers.copy(), content=await response.read(), request=setup, ) @override async def stream_request( self, setup: Request, *, chunk_size: int = 1024, ) -> AsyncGenerator[Response, None]: if self._params: url = setup.url.with_query({**self._params, **setup.url.query}) else: url = setup.url data = setup.data if setup.files: data = aiohttp.FormData(data or {}, quote_fields=False) for name, file in setup.files: data.add_field(name, file[1], content_type=file[2], filename=file[0]) cookies = ( (cookie.name, cookie.value) for cookie in setup.cookies 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, data=setup.content or data, json=setup.json, cookies=cookies, headers=setup.headers, proxy=setup.proxy or self._proxy, timeout=timeout, ) as response: response_headers = response.headers.copy() async for chunk in response.content.iter_chunked(chunk_size): yield Response( response.status, headers=response_headers, content=chunk, request=setup, ) @override async def setup(self) -> None: if self._client is not None: raise RuntimeError("Session has already been initialized") self._client = aiohttp.ClientSession( cookies=self._cookies, headers=self._headers, version=self._version, timeout=self._timeout, trust_env=True, ) await self._client.__aenter__() @override async def close(self) -> None: try: if self._client is not None: await self._client.close() finally: self._client = None class Mixin(HTTPClientMixin, WebSocketClientMixin): """AIOHTTP Mixin""" @property @override def type(self) -> str: return "aiohttp" @override async def request(self, setup: Request) -> Response: async with self.get_session() as session: return await session.request(setup) @override async def stream_request( self, setup: Request, *, chunk_size: int = 1024, ) -> AsyncGenerator[Response, None]: async with self.get_session() as session: async for response in session.stream_request(setup, chunk_size=chunk_size): yield response @override @asynccontextmanager async def websocket(self, setup: Request) -> AsyncGenerator["WebSocket", None]: if setup.version == HTTPVersion.H10: version = aiohttp.HttpVersion10 elif setup.version == HTTPVersion.H11: version = aiohttp.HttpVersion11 else: raise RuntimeError(f"Unsupported HTTP version: {setup.version}") if isinstance(setup.timeout, Timeout): timeout = aiohttp.ClientWSTimeout( ws_receive=setup.timeout.read, # type: ignore ws_close=setup.timeout.total, # type: ignore ) 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( setup.url, method=setup.method, timeout=timeout, headers=setup.headers, proxy=setup.proxy, ) as ws: yield WebSocket(request=setup, session=session, websocket=ws) @override def get_session( self, params: QueryTypes = None, headers: HeaderTypes = None, cookies: CookieTypes = None, version: str | HTTPVersion = HTTPVersion.H11, timeout: TimeoutTypes = None, proxy: str | None = None, ) -> Session: return Session( params=params, headers=headers, cookies=cookies, version=version, timeout=timeout, proxy=proxy, ) class WebSocket(BaseWebSocket): """AIOHTTP Websocket Wrapper""" def __init__( self, *, request: Request, session: aiohttp.ClientSession, websocket: aiohttp.ClientWebSocketResponse, ): super().__init__(request=request) self.session = session self.websocket = websocket @property @override def closed(self): return self.websocket.closed @override async def accept(self): raise NotImplementedError @override async def close(self, code: int = 1000, reason: str = ""): await self.websocket.close(code=code, message=reason.encode("utf-8")) await self.session.close() async def _receive(self) -> aiohttp.WSMessage: msg = await self.websocket.receive() if msg.type in ( aiohttp.WSMsgType.CLOSE, aiohttp.WSMsgType.CLOSING, aiohttp.WSMsgType.CLOSED, ): raise WebSocketClosed(self.websocket.close_code or 1006) return msg @override async def receive(self) -> str: msg = await self._receive() if msg.type not in (aiohttp.WSMsgType.TEXT, aiohttp.WSMsgType.BINARY): raise TypeError( f"WebSocket received unexpected frame type: {msg.type}, {msg.data!r}" ) return msg.data @override async def receive_text(self) -> str: msg = await self._receive() if msg.type != aiohttp.WSMsgType.TEXT: raise TypeError( f"WebSocket received unexpected frame type: {msg.type}, {msg.data!r}" ) return msg.data @override async def receive_bytes(self) -> bytes: msg = await self._receive() if msg.type != aiohttp.WSMsgType.BINARY: raise TypeError( f"WebSocket received unexpected frame type: {msg.type}, {msg.data!r}" ) return msg.data @override async def send_text(self, data: str) -> None: await self.websocket.send_str(data) @override async def send_bytes(self, data: bytes) -> None: await self.websocket.send_bytes(data) if TYPE_CHECKING: class Driver(Mixin, NoneDriver): ... else: Driver = combine_driver(NoneDriver, Mixin) """AIOHTTP Driver""" ================================================ FILE: nonebot/drivers/fastapi.py ================================================ """[FastAPI](https://fastapi.tiangolo.com/) 驱动适配 ```bash nb driver install fastapi # 或者 pip install nonebot2[fastapi] ``` :::tip 提示 本驱动仅支持服务端连接 ::: FrontMatter: mdx: format: md sidebar_position: 1 description: nonebot.drivers.fastapi 模块 """ import contextlib from functools import wraps import logging from typing import Any from typing_extensions import override from pydantic import BaseModel from nonebot.compat import model_dump, type_validate_python from nonebot.config import Config as NoneBotConfig from nonebot.config import Env from nonebot.drivers import ASGIMixin, HTTPServerSetup, WebSocketServerSetup from nonebot.drivers import Driver as BaseDriver from nonebot.drivers import Request as BaseRequest from nonebot.drivers import WebSocket as BaseWebSocket from nonebot.exception import WebSocketClosed from nonebot.internal.driver import FileTypes try: from fastapi import FastAPI, Request, UploadFile, status from fastapi.responses import Response from starlette.websockets import WebSocket, WebSocketDisconnect, WebSocketState import uvicorn except ModuleNotFoundError as e: # pragma: no cover raise ImportError( "Please install FastAPI first to use this driver. " "Install with pip: `pip install nonebot2[fastapi]`" ) from e def catch_closed(func): @wraps(func) async def decorator(*args, **kwargs): try: return await func(*args, **kwargs) except WebSocketDisconnect as e: raise WebSocketClosed(e.code) except KeyError: raise TypeError("WebSocket received unexpected frame type") return decorator class Config(BaseModel): """FastAPI 驱动框架设置,详情参考 FastAPI 文档""" fastapi_openapi_url: str | None = None """`openapi.json` 地址,默认为 `None` 即关闭""" fastapi_docs_url: str | None = None """`swagger` 地址,默认为 `None` 即关闭""" fastapi_redoc_url: str | None = None """`redoc` 地址,默认为 `None` 即关闭""" fastapi_include_adapter_schema: bool = True """是否包含适配器路由的 schema,默认为 `True`""" fastapi_reload: bool = False """开启/关闭冷重载""" fastapi_reload_dirs: list[str] | None = None """重载监控文件夹列表,默认为 uvicorn 默认值""" fastapi_reload_delay: float = 0.25 """重载延迟,默认为 uvicorn 默认值""" fastapi_reload_includes: list[str] | None = None """要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值""" fastapi_reload_excludes: list[str] | None = None """不要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值""" fastapi_extra: dict[str, Any] = {} """传递给 `FastAPI` 的其他参数。""" class Driver(BaseDriver, ASGIMixin): """FastAPI 驱动框架。""" def __init__(self, env: Env, config: NoneBotConfig): super().__init__(env, config) self.fastapi_config: Config = type_validate_python(Config, model_dump(config)) self._server_app = FastAPI( lifespan=self._lifespan_manager, openapi_url=self.fastapi_config.fastapi_openapi_url, docs_url=self.fastapi_config.fastapi_docs_url, redoc_url=self.fastapi_config.fastapi_redoc_url, **self.fastapi_config.fastapi_extra, ) @property @override def type(self) -> str: """驱动名称: `fastapi`""" return "fastapi" @property @override def server_app(self) -> FastAPI: """`FastAPI APP` 对象""" return self._server_app @property @override def asgi(self) -> FastAPI: """`FastAPI APP` 对象""" return self._server_app @property @override def logger(self) -> logging.Logger: """fastapi 使用的 logger""" return logging.getLogger("fastapi") @override def setup_http_server(self, setup: HTTPServerSetup): async def _handle(request: Request) -> Response: return await self._handle_http(request, setup) self._server_app.add_api_route( setup.path.path, _handle, name=setup.name, methods=[setup.method], include_in_schema=self.fastapi_config.fastapi_include_adapter_schema, ) @override def setup_websocket_server(self, setup: WebSocketServerSetup) -> None: async def _handle(websocket: WebSocket) -> None: await self._handle_ws(websocket, setup) self._server_app.add_api_websocket_route( setup.path.path, _handle, name=setup.name, ) @contextlib.asynccontextmanager async def _lifespan_manager(self, app: FastAPI): await self._lifespan.startup() try: yield finally: await self._lifespan.shutdown() @override def run( self, host: str | None = None, port: int | None = None, *args, app: str | None = None, **kwargs, ): """使用 `uvicorn` 启动 FastAPI""" super().run(host, port, app=app, **kwargs) LOGGING_CONFIG = { "version": 1, "disable_existing_loggers": False, "handlers": { "default": { "class": "nonebot.log.LoguruHandler", }, }, "loggers": { "uvicorn.error": {"handlers": ["default"], "level": "INFO"}, "uvicorn.access": { "handlers": ["default"], "level": "INFO", }, }, } uvicorn.run( app or self.server_app, # type: ignore host=host or str(self.config.host), port=port or self.config.port, reload=self.fastapi_config.fastapi_reload, reload_dirs=self.fastapi_config.fastapi_reload_dirs, reload_delay=self.fastapi_config.fastapi_reload_delay, reload_includes=self.fastapi_config.fastapi_reload_includes, reload_excludes=self.fastapi_config.fastapi_reload_excludes, log_config=LOGGING_CONFIG, **kwargs, ) async def _handle_http( self, request: Request, setup: HTTPServerSetup, ) -> Response: json: Any = None with contextlib.suppress(Exception): json = await request.json() data: dict | None = None files: list[tuple[str, FileTypes]] | None = None with contextlib.suppress(Exception): form = await request.form() data = {} files = [] for key, value in form.multi_items(): if isinstance(value, UploadFile): files.append( (key, (value.filename, value.file, value.content_type)) ) else: data[key] = value http_request = BaseRequest( request.method, str(request.url), headers=request.headers.items(), cookies=request.cookies, content=await request.body(), data=data, json=json, files=files, version=request.scope["http_version"], ) response = await setup.handle_func(http_request) return Response( response.content, response.status_code, dict(response.headers.items()) ) async def _handle_ws(self, websocket: WebSocket, setup: WebSocketServerSetup): request = BaseRequest( "GET", str(websocket.url), headers=websocket.headers.items(), cookies=websocket.cookies, version=websocket.scope.get("http_version", "1.1"), ) ws = FastAPIWebSocket( request=request, websocket=websocket, ) await setup.handle_func(ws) class FastAPIWebSocket(BaseWebSocket): """FastAPI WebSocket Wrapper""" @override def __init__(self, *, request: BaseRequest, websocket: WebSocket): super().__init__(request=request) self.websocket = websocket @property @override def closed(self) -> bool: return ( self.websocket.client_state == WebSocketState.DISCONNECTED or self.websocket.application_state == WebSocketState.DISCONNECTED ) @override async def accept(self) -> None: await self.websocket.accept() @override async def close( self, code: int = status.WS_1000_NORMAL_CLOSURE, reason: str = "" ) -> None: await self.websocket.close(code, reason) @override async def receive(self) -> str | bytes: # assert self.websocket.application_state == WebSocketState.CONNECTED msg = await self.websocket.receive() if msg["type"] == "websocket.disconnect": raise WebSocketClosed(msg["code"]) return msg["text"] if "text" in msg else msg["bytes"] @override @catch_closed async def receive_text(self) -> str: return await self.websocket.receive_text() @override @catch_closed async def receive_bytes(self) -> bytes: return await self.websocket.receive_bytes() @override async def send_text(self, data: str) -> None: await self.websocket.send({"type": "websocket.send", "text": data}) @override async def send_bytes(self, data: bytes) -> None: await self.websocket.send({"type": "websocket.send", "bytes": data}) __autodoc__ = {"catch_closed": False} ================================================ FILE: nonebot/drivers/httpx.py ================================================ """[HTTPX](https://www.python-httpx.org/) 驱动适配 ```bash nb driver install httpx # 或者 pip install nonebot2[httpx] ``` :::tip 提示 本驱动仅支持客户端 HTTP 连接 ::: FrontMatter: mdx: format: md sidebar_position: 3 description: nonebot.drivers.httpx 模块 """ from collections.abc import AsyncGenerator from typing import TYPE_CHECKING from typing_extensions import override from multidict import CIMultiDict from nonebot.drivers import ( URL, HTTPClientMixin, HTTPClientSession, HTTPVersion, Request, Response, combine_driver, ) from nonebot.drivers.none import Driver as NoneDriver from nonebot.internal.driver import ( Cookies, CookieTypes, HeaderTypes, QueryTypes, Timeout, TimeoutTypes, ) try: import httpx except ModuleNotFoundError as e: # pragma: no cover raise ImportError( "Please install httpx first to use this driver. " "Install with pip: `pip install nonebot2[httpx]`" ) from e class Session(HTTPClientSession): @override def __init__( self, params: QueryTypes = None, headers: HeaderTypes = None, cookies: CookieTypes = None, version: str | HTTPVersion = HTTPVersion.H11, timeout: TimeoutTypes = None, proxy: str | None = None, ): self._client: httpx.AsyncClient | None = None self._params = ( tuple(URL.build(query=params).query.items()) if params is not None else None ) self._headers = ( tuple(CIMultiDict(headers).items()) if headers is not None else None ) self._cookies = Cookies(cookies) self._version = HTTPVersion(version) if isinstance(timeout, Timeout): self._timeout = httpx.Timeout( timeout=timeout.total, connect=timeout.connect, read=timeout.read, ) else: self._timeout = httpx.Timeout(timeout) self._proxy = proxy @property def client(self) -> httpx.AsyncClient: if self._client is None: raise RuntimeError("Session is not initialized") return self._client @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), content=setup.content, data=setup.data, files=setup.files, json=setup.json, # ensure the params priority params=setup.url.raw_query_string, headers=tuple(setup.headers.items()), cookies=setup.cookies.jar, timeout=timeout, ) return Response( response.status_code, headers=response.headers.multi_items(), content=response.content, request=setup, ) @override async def stream_request( self, setup: Request, *, 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), content=setup.content, data=setup.data, files=setup.files, json=setup.json, # ensure the params priority params=setup.url.raw_query_string, headers=tuple(setup.headers.items()), cookies=setup.cookies.jar, timeout=timeout, ) as response: response_headers = response.headers.multi_items() async for chunk in response.aiter_bytes(chunk_size=chunk_size): yield Response( response.status_code, headers=response_headers, content=chunk, request=setup, ) @override async def setup(self) -> None: if self._client is not None: raise RuntimeError("Session has already been initialized") self._client = httpx.AsyncClient( params=self._params, headers=self._headers, cookies=self._cookies.jar, http2=self._version == HTTPVersion.H2, proxy=self._proxy, follow_redirects=True, ) await self._client.__aenter__() @override async def close(self) -> None: try: if self._client is not None: await self._client.aclose() finally: self._client = None class Mixin(HTTPClientMixin): """HTTPX Mixin""" @property @override def type(self) -> str: return "httpx" @override async def request(self, setup: Request) -> Response: async with self.get_session( version=setup.version, proxy=setup.proxy ) as session: return await session.request(setup) @override async def stream_request( self, setup: Request, *, chunk_size: int = 1024, ) -> AsyncGenerator[Response, None]: async with self.get_session( version=setup.version, proxy=setup.proxy ) as session: async for response in session.stream_request(setup, chunk_size=chunk_size): yield response @override def get_session( self, params: QueryTypes = None, headers: HeaderTypes = None, cookies: CookieTypes = None, version: str | HTTPVersion = HTTPVersion.H11, timeout: TimeoutTypes = None, proxy: str | None = None, ) -> Session: return Session( params=params, headers=headers, cookies=cookies, version=version, timeout=timeout, proxy=proxy, ) if TYPE_CHECKING: class Driver(Mixin, NoneDriver): ... else: Driver = combine_driver(NoneDriver, Mixin) """HTTPX Driver""" ================================================ FILE: nonebot/drivers/none.py ================================================ """None 驱动适配 :::tip 提示 本驱动不支持任何服务器或客户端连接 ::: FrontMatter: mdx: format: md sidebar_position: 6 description: nonebot.drivers.none 模块 """ import signal from typing_extensions import override import anyio from anyio.abc import TaskGroup from exceptiongroup import BaseExceptionGroup, catch from nonebot.config import Config, Env from nonebot.consts import WINDOWS from nonebot.drivers import Driver as BaseDriver from nonebot.log import logger from nonebot.utils import flatten_exception_group HANDLED_SIGNALS = ( signal.SIGINT, # Unix signal 2. Sent by Ctrl+C. signal.SIGTERM, # Unix signal 15. Sent by `kill `. ) if WINDOWS: # pragma: py-win32 HANDLED_SIGNALS += (signal.SIGBREAK,) # Windows signal 21. Sent by Ctrl+Break. class Driver(BaseDriver): """None 驱动框架""" def __init__(self, env: Env, config: Config): super().__init__(env, config) self.should_exit: anyio.Event = anyio.Event() self.force_exit: anyio.Event = anyio.Event() @property @override def type(self) -> str: """驱动名称: `none`""" return "none" @property @override def logger(self): """none driver 使用的 logger""" return logger @override def run(self, *args, **kwargs): """启动 none driver""" super().run(*args, **kwargs) anyio.run(self._serve) async def _serve(self): async with anyio.create_task_group() as driver_tg: driver_tg.start_soon(self._handle_signals) driver_tg.start_soon(self._listen_force_exit, driver_tg) driver_tg.start_soon(self._handle_lifespan, driver_tg) async def _handle_signals(self): try: with anyio.open_signal_receiver(*HANDLED_SIGNALS) as signal_receiver: async for sig in signal_receiver: self.exit(force=self.should_exit.is_set()) except NotImplementedError: # Windows for sig in HANDLED_SIGNALS: signal.signal(sig, self._handle_legacy_signal) # backport for Windows signal handling def _handle_legacy_signal(self, sig, frame): self.exit(force=self.should_exit.is_set()) async def _handle_lifespan(self, tg: TaskGroup): try: await self._startup() if self.should_exit.is_set(): return await self._listen_exit() await self._shutdown() finally: tg.cancel_scope.cancel() async def _startup(self): def handle_exception(exc_group: BaseExceptionGroup[Exception]) -> None: self.should_exit.set() for exc in flatten_exception_group(exc_group): logger.opt(colors=True, exception=exc).error( "Error occurred while running startup hook." "" ) logger.error( "Application startup failed. Exiting." ) with catch({Exception: handle_exception}): await self._lifespan.startup() if not self.should_exit.is_set(): logger.info("Application startup completed.") async def _listen_exit(self, tg: TaskGroup | None = None): await self.should_exit.wait() if tg is not None: tg.cancel_scope.cancel() async def _shutdown(self): logger.info("Shutting down") logger.info("Waiting for application shutdown. (CTRL+C to force quit)") error_occurred: bool = False def handle_exception(exc_group: BaseExceptionGroup[Exception]) -> None: nonlocal error_occurred error_occurred = True for exc in flatten_exception_group(exc_group): logger.opt(colors=True, exception=exc).error( "Error occurred while running shutdown hook." "" ) logger.error( "Application shutdown failed. Exiting." ) with catch({Exception: handle_exception}): await self._lifespan.shutdown() if not error_occurred: logger.info("Application shutdown complete.") async def _listen_force_exit(self, tg: TaskGroup): await self.force_exit.wait() tg.cancel_scope.cancel() def exit(self, force: bool = False): """退出 none driver 参数: force: 强制退出 """ if not self.should_exit.is_set(): self.should_exit.set() if force: self.force_exit.set() ================================================ FILE: nonebot/drivers/quart.py ================================================ """[Quart](https://pgjones.gitlab.io/quart/index.html) 驱动适配 ```bash nb driver install quart # 或者 pip install nonebot2[quart] ``` :::tip 提示 本驱动仅支持服务端连接 ::: FrontMatter: mdx: format: md sidebar_position: 5 description: nonebot.drivers.quart 模块 """ import asyncio from functools import wraps from typing import Any, cast from typing_extensions import override from pydantic import BaseModel from nonebot.compat import model_dump, type_validate_python from nonebot.config import Config as NoneBotConfig from nonebot.config import Env from nonebot.drivers import ASGIMixin, HTTPServerSetup, WebSocketServerSetup from nonebot.drivers import Driver as BaseDriver from nonebot.drivers import Request as BaseRequest from nonebot.drivers import WebSocket as BaseWebSocket from nonebot.exception import WebSocketClosed from nonebot.internal.driver import FileTypes try: from quart import Quart, Request, Response from quart import Websocket as QuartWebSocket from quart import request as _request from quart.ctx import WebsocketContext from quart.datastructures import FileStorage from quart.globals import websocket_ctx import uvicorn except ModuleNotFoundError as e: # pragma: no cover raise ImportError( "Please install Quart first to use this driver. " "Install with pip: `pip install nonebot2[quart]`" ) from e def catch_closed(func): @wraps(func) async def decorator(*args, **kwargs): try: return await func(*args, **kwargs) except asyncio.CancelledError: raise WebSocketClosed(1000) return decorator class Config(BaseModel): """Quart 驱动框架设置""" quart_reload: bool = False """开启/关闭冷重载""" quart_reload_dirs: list[str] | None = None """重载监控文件夹列表,默认为 uvicorn 默认值""" quart_reload_delay: float = 0.25 """重载延迟,默认为 uvicorn 默认值""" quart_reload_includes: list[str] | None = None """要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值""" quart_reload_excludes: list[str] | None = None """不要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值""" quart_extra: dict[str, Any] = {} """传递给 `Quart` 的其他参数。""" class Driver(BaseDriver, ASGIMixin): """Quart 驱动框架""" def __init__(self, env: Env, config: NoneBotConfig): super().__init__(env, config) self.quart_config = type_validate_python(Config, model_dump(config)) self._server_app = Quart( self.__class__.__qualname__, **self.quart_config.quart_extra ) self._server_app.before_serving(self._lifespan.startup) self._server_app.after_serving(self._lifespan.shutdown) @property @override def type(self) -> str: """驱动名称: `quart`""" return "quart" @property @override def server_app(self) -> Quart: """`Quart` 对象""" return self._server_app @property @override def asgi(self): """`Quart` 对象""" return self._server_app @property @override def logger(self): """Quart 使用的 logger""" return self._server_app.logger @override def setup_http_server(self, setup: HTTPServerSetup): async def _handle() -> Response: return await self._handle_http(setup) self._server_app.add_url_rule( setup.path.path, endpoint=setup.name, methods=[setup.method], view_func=_handle, ) @override def setup_websocket_server(self, setup: WebSocketServerSetup) -> None: async def _handle() -> None: return await self._handle_ws(setup) self._server_app.add_websocket( setup.path.path, endpoint=setup.name, view_func=_handle, ) @override def run( self, host: str | None = None, port: int | None = None, *args, app: str | None = None, **kwargs, ): """使用 `uvicorn` 启动 Quart""" super().run(host, port, app, **kwargs) LOGGING_CONFIG = { "version": 1, "disable_existing_loggers": False, "handlers": { "default": { "class": "nonebot.log.LoguruHandler", }, }, "loggers": { "uvicorn.error": {"handlers": ["default"], "level": "INFO"}, "uvicorn.access": { "handlers": ["default"], "level": "INFO", }, }, } uvicorn.run( app or self.server_app, # type: ignore host=host or str(self.config.host), port=port or self.config.port, reload=self.quart_config.quart_reload, reload_dirs=self.quart_config.quart_reload_dirs, reload_delay=self.quart_config.quart_reload_delay, reload_includes=self.quart_config.quart_reload_includes, reload_excludes=self.quart_config.quart_reload_excludes, log_config=LOGGING_CONFIG, **kwargs, ) async def _handle_http(self, setup: HTTPServerSetup) -> Response: request: Request = _request json = await request.get_json() if request.is_json else None data = await request.form files_dict = await request.files files: list[tuple[str, FileTypes]] = [] key: str value: FileStorage for key, value in files_dict.items(): files.append((key, (value.filename, value.stream, value.content_type))) http_request = BaseRequest( request.method, request.url, headers=list(request.headers.items()), cookies=list(request.cookies.items()), content=await request.get_data( cache=False, as_text=False, parse_form_data=False ), data=data or None, json=json, files=files or None, version=request.http_version, ) response = await setup.handle_func(http_request) return Response( response.content or "", response.status_code or 200, headers=dict(response.headers), ) async def _handle_ws(self, setup: WebSocketServerSetup) -> None: ctx = cast(WebsocketContext, websocket_ctx.copy()) websocket = websocket_ctx.websocket http_request = BaseRequest( websocket.method, websocket.url, headers=list(websocket.headers.items()), cookies=list(websocket.cookies.items()), version=websocket.http_version, ) ws = WebSocket(request=http_request, websocket_ctx=ctx) await setup.handle_func(ws) class WebSocket(BaseWebSocket): """Quart WebSocket Wrapper""" def __init__(self, *, request: BaseRequest, websocket_ctx: WebsocketContext): super().__init__(request=request) self.websocket_ctx = websocket_ctx @property def websocket(self) -> QuartWebSocket: return self.websocket_ctx.websocket @property @override def closed(self): # FIXME return True @override async def accept(self): await self.websocket.accept() @override async def close(self, code: int = 1000, reason: str = ""): await self.websocket.close(code, reason) @override @catch_closed async def receive(self) -> str | bytes: return await self.websocket.receive() @override @catch_closed async def receive_text(self) -> str: msg = await self.websocket.receive() if isinstance(msg, bytes): raise TypeError("WebSocket received unexpected frame type: bytes") return msg @override @catch_closed async def receive_bytes(self) -> bytes: msg = await self.websocket.receive() if isinstance(msg, str): raise TypeError("WebSocket received unexpected frame type: str") return msg @override async def send_text(self, data: str): await self.websocket.send(data) @override async def send_bytes(self, data: bytes): await self.websocket.send(data) __autodoc__ = {"catch_closed": False} ================================================ FILE: nonebot/drivers/websockets.py ================================================ """[websockets](https://websockets.readthedocs.io/) 驱动适配 ```bash nb driver install websockets # 或者 pip install nonebot2[websockets] ``` :::tip 提示 本驱动仅支持客户端 WebSocket 连接 ::: FrontMatter: mdx: format: md sidebar_position: 4 description: nonebot.drivers.websockets 模块 """ from collections.abc import AsyncGenerator, Callable from contextlib import asynccontextmanager from functools import wraps import logging 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 WebSocket as BaseWebSocket from nonebot.drivers.none import Driver as NoneDriver from nonebot.exception import WebSocketClosed from nonebot.log import LoguruHandler try: from websockets import ClientConnection, ConnectionClosed, connect except ModuleNotFoundError as e: # pragma: no cover raise ImportError( "Please install websockets first to use this driver. " "Install with pip: `pip install nonebot2[websockets]`" ) from e T = TypeVar("T") P = ParamSpec("P") logger = logging.Logger("websockets.client", "INFO") logger.addHandler(LoguruHandler()) def catch_closed( func: Callable[P, "CoroutineType[Any, Any, T]"], ) -> Callable[P, "CoroutineType[Any, Any, T]"]: @wraps(func) async def decorator(*args: P.args, **kwargs: P.kwargs) -> T: try: return await func(*args, **kwargs) except ConnectionClosed as e: raise WebSocketClosed(e.code, e.reason) return decorator class Mixin(WebSocketClientMixin): """Websockets Mixin""" @property @override def type(self) -> str: return "websockets" @override @asynccontextmanager async def websocket(self, setup: Request) -> AsyncGenerator["WebSocket", None]: if isinstance(setup.timeout, Timeout): timeout = setup.timeout.total or setup.timeout.connect or setup.timeout.read else: timeout = setup.timeout 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, ) async with connection as ws: yield WebSocket(request=setup, websocket=ws) class WebSocket(BaseWebSocket): """Websockets WebSocket Wrapper""" @override def __init__(self, *, request: Request, websocket: ClientConnection): super().__init__(request=request) self.websocket = websocket @property @override def closed(self) -> bool: return self.websocket.close_code is not None @override async def accept(self): raise NotImplementedError @override async def close(self, code: int = 1000, reason: str = ""): await self.websocket.close(code, reason) @override @catch_closed async def receive(self) -> str | bytes: return await self.websocket.recv() @override @catch_closed async def receive_text(self) -> str: msg = await self.websocket.recv() if isinstance(msg, bytes): raise TypeError("WebSocket received unexpected frame type: bytes") return msg @override @catch_closed async def receive_bytes(self) -> bytes: msg = await self.websocket.recv() if isinstance(msg, str): raise TypeError("WebSocket received unexpected frame type: str") return msg @override async def send_text(self, data: str) -> None: await self.websocket.send(data) @override async def send_bytes(self, data: bytes) -> None: await self.websocket.send(data) if TYPE_CHECKING: class Driver(Mixin, NoneDriver): ... else: Driver = combine_driver(NoneDriver, Mixin) """Websockets Driver""" ================================================ FILE: nonebot/exception.py ================================================ """本模块包含了所有 NoneBot 运行时可能会抛出的异常。 这些异常并非所有需要用户处理,在 NoneBot 内部运行时被捕获,并进行对应操作。 ```bash NoneBotException ├── ParserExit ├── ProcessException | ├── IgnoredException | ├── SkippedException | | └── TypeMisMatch | ├── MockApiException | └── StopPropagation ├── MatcherException | ├── PausedException | ├── RejectedException | └── FinishedException ├── AdapterException | ├── NoLogException | ├── ApiNotAvailable | ├── NetworkError | └── ActionFailed └── DriverException └── WebSocketClosed ``` FrontMatter: mdx: format: md sidebar_position: 10 description: nonebot.exception 模块 """ from typing import Any from nonebot.compat import ModelField class NoneBotException(Exception): """所有 NoneBot 发生的异常基类。""" def __str__(self) -> str: return self.__repr__() # Rule Exception class ParserExit(NoneBotException): """{ref}`nonebot.rule.shell_command` 处理消息失败时返回的异常。""" def __init__(self, status: int = 0, message: str | None = None) -> None: self.status = status self.message = message def __repr__(self) -> str: return ( f"ParserExit(status={self.status}" + (f", message={self.message!r}" if self.message else "") + ")" ) # Processor Exception class ProcessException(NoneBotException): """事件处理过程中发生的异常基类。""" class IgnoredException(ProcessException): """指示 NoneBot 应该忽略该事件。可由 PreProcessor 抛出。 参数: reason: 忽略事件的原因 """ def __init__(self, reason: Any) -> None: self.reason: Any = reason def __repr__(self) -> str: return f"IgnoredException(reason={self.reason!r})" class SkippedException(ProcessException): """指示 NoneBot 立即结束当前 `Dependent` 的运行。 例如,可以在 `Handler` 中通过 {ref}`nonebot.matcher.Matcher.skip` 抛出。 用法: ```python def always_skip(): Matcher.skip() @matcher.handle() async def handler(dependency = Depends(always_skip)): # never run ``` """ class TypeMisMatch(SkippedException): """当前 `Handler` 的参数类型不匹配。""" def __init__(self, param: ModelField, value: Any) -> None: self.param: ModelField = param self.value: Any = value def __repr__(self) -> str: return ( f"TypeMisMatch(param={self.param.name}, " f"type={self.param._type_display()}, value={self.value!r}>" ) class MockApiException(ProcessException): """指示 NoneBot 阻止本次 API 调用或修改本次调用返回值,并返回自定义内容。 可由 api hook 抛出。 参数: result: 返回的内容 """ def __init__(self, result: Any): self.result = result def __repr__(self) -> str: return f"MockApiException(result={self.result!r})" class StopPropagation(ProcessException): """指示 NoneBot 终止事件向下层传播。 在 {ref}`nonebot.matcher.Matcher.block` 为 `True` 或使用 {ref}`nonebot.matcher.Matcher.stop_propagation` 方法时抛出。 用法: ```python matcher = on_notice(block=True) # 或者 @matcher.handle() async def handler(matcher: Matcher): matcher.stop_propagation() ``` """ # Matcher Exceptions class MatcherException(NoneBotException): """所有 Matcher 发生的异常基类。""" class PausedException(MatcherException): """指示 NoneBot 结束当前 `Handler` 并等待下一条消息后继续下一个 `Handler`。 可用于用户输入新信息。 可以在 `Handler` 中通过 {ref}`nonebot.matcher.Matcher.pause` 抛出。 用法: ```python @matcher.handle() async def handler(): await matcher.pause("some message") ``` """ class RejectedException(MatcherException): """指示 NoneBot 结束当前 `Handler` 并等待下一条消息后重新运行当前 `Handler`。 可用于用户重新输入。 可以在 `Handler` 中通过 {ref}`nonebot.matcher.Matcher.reject` 抛出。 用法: ```python @matcher.handle() async def handler(): await matcher.reject("some message") ``` """ class FinishedException(MatcherException): """指示 NoneBot 结束当前 `Handler` 且后续 `Handler` 不再被运行。可用于结束用户会话。 可以在 `Handler` 中通过 {ref}`nonebot.matcher.Matcher.finish` 抛出。 用法: ```python @matcher.handle() async def handler(): await matcher.finish("some message") ``` """ # Adapter Exceptions class AdapterException(NoneBotException): """代表 `Adapter` 抛出的异常,所有的 `Adapter` 都要在内部继承自这个 `Exception`。 参数: adapter_name: 标识 adapter """ def __init__(self, adapter_name: str, *args: object) -> None: super().__init__(*args) self.adapter_name: str = adapter_name class NoLogException(AdapterException): """指示 NoneBot 对当前 `Event` 进行处理但不显示 Log 信息。 可在 {ref}`nonebot.adapters.Event.get_log_string` 时抛出 """ class ApiNotAvailable(AdapterException): """在 API 连接不可用时抛出。""" class NetworkError(AdapterException): """在网络出现问题时抛出, 如: API 请求地址不正确, API 请求无返回或返回状态非正常等。 """ class ActionFailed(AdapterException): """API 请求成功返回数据,但 API 操作失败。""" # Driver Exceptions class DriverException(NoneBotException): """`Driver` 抛出的异常基类。""" class WebSocketClosed(DriverException): """WebSocket 连接已关闭。""" def __init__(self, code: int, reason: str | None = None) -> None: self.code = code self.reason = reason def __repr__(self) -> str: return ( f"WebSocketClosed(code={self.code}" + (f", reason={self.reason!r}" if self.reason else "") + ")" ) ================================================ FILE: nonebot/internal/__init__.py ================================================ ================================================ FILE: nonebot/internal/adapter/__init__.py ================================================ from .adapter import Adapter as Adapter from .bot import Bot as Bot from .event import Event as Event from .message import Message as Message from .message import MessageSegment as MessageSegment from .template import MessageTemplate as MessageTemplate ================================================ FILE: nonebot/internal/adapter/adapter.py ================================================ import abc from collections.abc import AsyncGenerator from contextlib import asynccontextmanager from typing import Any from nonebot.config import Config from nonebot.internal.driver import ( ASGIMixin, Driver, HTTPClientMixin, HTTPServerSetup, Request, Response, WebSocket, WebSocketClientMixin, WebSocketServerSetup, ) from nonebot.internal.driver._lifespan import LIFESPAN_FUNC from .bot import Bot class Adapter(abc.ABC): """协议适配器基类。 通常,在 Adapter 中编写协议通信相关代码,如: 建立通信连接、处理接收与发送 data 等。 参数: driver: {ref}`nonebot.drivers.Driver` 实例 kwargs: 其他由 {ref}`nonebot.drivers.Driver.register_adapter` 传入的额外参数 """ def __init__(self, driver: Driver, **kwargs: Any): self.driver: Driver = driver """{ref}`nonebot.drivers.Driver` 实例""" self.bots: dict[str, Bot] = {} """本协议适配器已建立连接的 {ref}`nonebot.adapters.Bot` 实例""" def __repr__(self) -> str: return f"Adapter(name={self.get_name()!r})" @classmethod @abc.abstractmethod def get_name(cls) -> str: """当前协议适配器的名称""" raise NotImplementedError @property def config(self) -> Config: """全局 NoneBot 配置""" return self.driver.config def bot_connect(self, bot: Bot) -> None: """告知 NoneBot 建立了一个新的 {ref}`nonebot.adapters.Bot` 连接。 当有新的 {ref}`nonebot.adapters.Bot` 实例连接建立成功时调用。 参数: bot: {ref}`nonebot.adapters.Bot` 实例 """ self.driver._bot_connect(bot) self.bots[bot.self_id] = bot def bot_disconnect(self, bot: Bot) -> None: """告知 NoneBot {ref}`nonebot.adapters.Bot` 连接已断开。 当有 {ref}`nonebot.adapters.Bot` 实例连接断开时调用。 参数: bot: {ref}`nonebot.adapters.Bot` 实例 """ if self.bots.pop(bot.self_id, None) is None: raise RuntimeError(f"{bot} not found in adapter {self.get_name()}") self.driver._bot_disconnect(bot) def setup_http_server(self, setup: HTTPServerSetup): """设置一个 HTTP 服务器路由配置""" if not isinstance(self.driver, ASGIMixin): raise TypeError("Current driver does not support http server") self.driver.setup_http_server(setup) def setup_websocket_server(self, setup: WebSocketServerSetup): """设置一个 WebSocket 服务器路由配置""" if not isinstance(self.driver, ASGIMixin): raise TypeError("Current driver does not support websocket server") self.driver.setup_websocket_server(setup) async def request(self, setup: Request) -> Response: """进行一个 HTTP 客户端请求""" if not isinstance(self.driver, HTTPClientMixin): raise TypeError("Current driver does not support http client") return await self.driver.request(setup) @asynccontextmanager async def websocket(self, setup: Request) -> AsyncGenerator[WebSocket, None]: """建立一个 WebSocket 客户端连接请求""" if not isinstance(self.driver, WebSocketClientMixin): raise TypeError("Current driver does not support websocket client") async with self.driver.websocket(setup) as ws: yield ws def on_ready(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC: return self.driver._lifespan.on_ready(func) @abc.abstractmethod async def _call_api(self, bot: Bot, api: str, **data: Any) -> Any: """`Adapter` 实际调用 api 的逻辑实现函数,实现该方法以调用 api。 参数: api: API 名称 data: API 数据 """ raise NotImplementedError __autodoc__ = {"Adapter._call_api": True} ================================================ FILE: nonebot/internal/adapter/bot.py ================================================ import abc from functools import partial from typing import TYPE_CHECKING, Any, ClassVar, Protocol import anyio from exceptiongroup import BaseExceptionGroup, catch from nonebot.config import Config from nonebot.exception import MockApiException from nonebot.log import logger from nonebot.typing import T_CalledAPIHook, T_CallingAPIHook from nonebot.utils import flatten_exception_group if TYPE_CHECKING: from .adapter import Adapter from .event import Event from .message import Message, MessageSegment class _ApiCall(Protocol): async def __call__(self, **kwargs: Any) -> Any: ... class Bot(abc.ABC): """Bot 基类。 用于处理上报消息,并提供 API 调用接口。 参数: adapter: 协议适配器实例 self_id: 机器人 ID """ _calling_api_hook: ClassVar[set[T_CallingAPIHook]] = set() """call_api 时执行的函数""" _called_api_hook: ClassVar[set[T_CalledAPIHook]] = set() """call_api 后执行的函数""" def __init__(self, adapter: "Adapter", self_id: str): self.adapter: "Adapter" = adapter """协议适配器实例""" self.self_id: str = self_id """机器人 ID""" def __repr__(self) -> str: return f"Bot(type={self.type!r}, self_id={self.self_id!r})" def __getattr__(self, name: str) -> "_ApiCall": if name.startswith("__") and name.endswith("__"): raise AttributeError( f"'{self.__class__.__name__}' object has no attribute '{name}'" ) return partial(self.call_api, name) @property def type(self) -> str: """协议适配器名称""" return self.adapter.get_name() @property def config(self) -> Config: """全局 NoneBot 配置""" return self.adapter.config async def call_api(self, api: str, **data: Any) -> Any: """调用机器人 API 接口,可以通过该函数或直接通过 bot 属性进行调用 参数: api: API 名称 data: API 数据 用法: ```python await bot.call_api("send_msg", message="hello world") await bot.send_msg(message="hello world") ``` """ result: Any = None skip_calling_api: bool = False exception: Exception | None = None if self._calling_api_hook: logger.debug("Running CallingAPI hooks...") def _handle_mock_api_exception( exc_group: BaseExceptionGroup[MockApiException], ) -> None: nonlocal skip_calling_api, result excs = [ exc for exc in flatten_exception_group(exc_group) if isinstance(exc, MockApiException) ] if not excs: return elif len(excs) > 1: logger.warning( "Multiple hooks want to mock API result. Use the first one." ) skip_calling_api = True result = excs[0].result logger.debug( f"Calling API {api} is cancelled. Return {result!r} instead." ) def _handle_exception(exc_group: BaseExceptionGroup[Exception]) -> None: for exc in flatten_exception_group(exc_group): logger.opt(colors=True, exception=exc).error( "Error when running CallingAPI hook. " "Running cancelled!" ) with catch( { MockApiException: _handle_mock_api_exception, Exception: _handle_exception, } ): async with anyio.create_task_group() as tg: for hook in self._calling_api_hook: tg.start_soon(hook, self, api, data) if not skip_calling_api: try: result = await self.adapter._call_api(self, api, **data) except Exception as e: exception = e if self._called_api_hook: logger.debug("Running CalledAPI hooks...") def _handle_mock_api_exception( exc_group: BaseExceptionGroup[MockApiException], ) -> None: nonlocal result, exception excs = [ exc for exc in flatten_exception_group(exc_group) if isinstance(exc, MockApiException) ] if not excs: return elif len(excs) > 1: logger.warning( "Multiple hooks want to mock API result. Use the first one." ) result = excs[0].result exception = None logger.debug( f"Calling API {api} result is mocked. Return {result} instead." ) def _handle_exception(exc_group: BaseExceptionGroup[Exception]) -> None: for exc in flatten_exception_group(exc_group): logger.opt(colors=True, exception=exc).error( "Error when running CalledAPI hook. " "Running cancelled!" ) with catch( { MockApiException: _handle_mock_api_exception, Exception: _handle_exception, } ): async with anyio.create_task_group() as tg: for hook in self._called_api_hook: tg.start_soon(hook, self, exception, api, data, result) if exception: raise exception return result @abc.abstractmethod async def send( self, event: "Event", message: "str | Message | MessageSegment", **kwargs: Any, ) -> Any: """调用机器人基础发送消息接口 参数: event: 上报事件 message: 要发送的消息 kwargs: 任意额外参数 """ raise NotImplementedError @classmethod def on_calling_api(cls, func: T_CallingAPIHook) -> T_CallingAPIHook: """调用 api 预处理。 钩子函数参数: - bot: 当前 bot 对象 - api: 调用的 api 名称 - data: api 调用的参数字典 """ cls._calling_api_hook.add(func) return func @classmethod def on_called_api(cls, func: T_CalledAPIHook) -> T_CalledAPIHook: """调用 api 后处理。 钩子函数参数: - bot: 当前 bot 对象 - exception: 调用 api 时发生的错误 - api: 调用的 api 名称 - data: api 调用的参数字典 - result: api 调用的返回 """ cls._called_api_hook.add(func) return func ================================================ FILE: nonebot/internal/adapter/event.py ================================================ import abc from typing import Any, TypeVar from pydantic import BaseModel from nonebot.compat import PYDANTIC_V2, ConfigDict from nonebot.utils import DataclassEncoder from .message import Message E = TypeVar("E", bound="Event") class Event(abc.ABC, BaseModel): """Event 基类。提供获取关键信息的方法,其余信息可直接获取。""" if PYDANTIC_V2: # pragma: pydantic-v2 model_config = ConfigDict(extra="allow") else: # pragma: pydantic-v1 class Config(ConfigDict): extra = "allow" # type: ignore json_encoders = {Message: DataclassEncoder} # noqa: RUF012 if not PYDANTIC_V2: # pragma: pydantic-v1 @classmethod def validate(cls: type["E"], value: Any) -> "E": if isinstance(value, Event) and not isinstance(value, cls): raise TypeError(f"{value} is incompatible with Event type {cls}") return super().validate(value) @abc.abstractmethod def get_type(self) -> str: """获取事件类型的方法,类型通常为 NoneBot 内置的四种类型。""" raise NotImplementedError @abc.abstractmethod def get_event_name(self) -> str: """获取事件名称的方法。""" raise NotImplementedError @abc.abstractmethod def get_event_description(self) -> str: """获取事件描述的方法,通常为事件具体内容。""" raise NotImplementedError def __str__(self) -> str: return f"[{self.get_event_name()}]: {self.get_event_description()}" def get_log_string(self) -> str: """获取事件日志信息的方法。 通常你不需要修改这个方法,只有当希望 NoneBot 隐藏该事件日志时, 可以抛出 `NoLogException` 异常。 异常: NoLogException: 希望 NoneBot 隐藏该事件日志 """ return f"[{self.get_event_name()}]: {self.get_event_description()}" @abc.abstractmethod def get_user_id(self) -> str: """获取事件主体 id 的方法,通常是用户 id 。""" raise NotImplementedError @abc.abstractmethod def get_session_id(self) -> str: """获取会话 id 的方法,用于判断当前事件属于哪一个会话, 通常是用户 id、群组 id 组合。 """ raise NotImplementedError @abc.abstractmethod def get_message(self) -> "Message": """获取事件消息内容的方法。""" raise NotImplementedError def get_plaintext(self) -> str: """获取消息纯文本的方法。 通常不需要修改,默认通过 `get_message().extract_plain_text` 获取。 """ return self.get_message().extract_plain_text() @abc.abstractmethod def is_tome(self) -> bool: """获取事件是否与机器人有关的方法。""" raise NotImplementedError ================================================ FILE: nonebot/internal/adapter/message.py ================================================ import abc from collections.abc import Iterable from copy import deepcopy from dataclasses import asdict, dataclass, field from typing import ( # noqa: UP035 Any, Generic, SupportsIndex, Type, TypeVar, overload, ) from typing_extensions import Self from nonebot.compat import custom_validation, type_validate_python from .template import MessageTemplate TMS = TypeVar("TMS", bound="MessageSegment") TM = TypeVar("TM", bound="Message") @custom_validation @dataclass class MessageSegment(abc.ABC, Generic[TM]): """消息段基类""" type: str """消息段类型""" data: dict[str, Any] = field(default_factory=dict) """消息段数据""" @classmethod @abc.abstractmethod def get_message_class(cls) -> Type[TM]: # noqa: UP006 """获取消息数组类型""" raise NotImplementedError @abc.abstractmethod def __str__(self) -> str: """该消息段所代表的 str,在命令匹配部分使用""" raise NotImplementedError def __len__(self) -> int: return len(str(self)) def __ne__( # pyright: ignore[reportIncompatibleMethodOverride] self, other: Self ) -> bool: return not self == other def __add__(self, other: str | Self | Iterable[Self]) -> TM: return self.get_message_class()(self) + other def __radd__(self, other: str | Self | Iterable[Self]) -> TM: return self.get_message_class()(other) + self @classmethod def __get_validators__(cls): yield cls._validate @classmethod def _validate(cls, value) -> Self: if isinstance(value, cls): return value if isinstance(value, MessageSegment): raise ValueError(f"Type {type(value)} can not be converted to {cls}") if not isinstance(value, dict): raise ValueError(f"Expected dict for MessageSegment, got {type(value)}") if "type" not in value: raise ValueError( f"Expected dict with 'type' for MessageSegment, got {value}" ) return cls(type=value["type"], data=value.get("data", {})) def get(self, key: str, default: Any = None): return asdict(self).get(key, default) def keys(self): return asdict(self).keys() def values(self): return asdict(self).values() def items(self): return asdict(self).items() def join(self, iterable: Iterable[Self | TM]) -> TM: return self.get_message_class()(self).join(iterable) def copy(self) -> Self: return deepcopy(self) @abc.abstractmethod def is_text(self) -> bool: """当前消息段是否为纯文本""" raise NotImplementedError @custom_validation class Message(list[TMS], abc.ABC): """消息序列 参数: message: 消息内容 """ def __init__( self, message: str | None | Iterable[TMS] | TMS = None, ): super().__init__() if message is None: return elif isinstance(message, str): self.extend(self._construct(message)) elif isinstance(message, MessageSegment): self.append(message) elif isinstance(message, Iterable): self.extend(message) else: self.extend(self._construct(message)) # pragma: no cover @classmethod def template(cls, format_string: str | TM) -> MessageTemplate[Self]: """创建消息模板。 用法和 `str.format` 大致相同,支持以 `Message` 对象作为消息模板并输出消息对象。 并且提供了拓展的格式化控制符, 可以通过该消息类型的 `MessageSegment` 工厂方法创建消息。 参数: format_string: 格式化模板 返回: 消息格式化器 """ return MessageTemplate(format_string, cls) @classmethod @abc.abstractmethod def get_segment_class(cls) -> type[TMS]: """获取消息段类型""" raise NotImplementedError def __str__(self) -> str: return "".join(str(seg) for seg in self) @classmethod def __get_validators__(cls): yield cls._validate @classmethod def _validate(cls, value) -> Self: if isinstance(value, cls): return value elif isinstance(value, Message): raise ValueError(f"Type {type(value)} can not be converted to {cls}") elif isinstance(value, str): pass elif isinstance(value, dict): value = type_validate_python(cls.get_segment_class(), value) elif isinstance(value, Iterable): value = [type_validate_python(cls.get_segment_class(), v) for v in value] else: raise ValueError( f"Expected str, dict or iterable for Message, got {type(value)}" ) return cls(value) @staticmethod @abc.abstractmethod def _construct(msg: str) -> Iterable[TMS]: """构造消息数组""" raise NotImplementedError def __add__( # pyright: ignore[reportIncompatibleMethodOverride] self, other: str | TMS | Iterable[TMS] ) -> Self: result = self.copy() result += other return result def __radd__(self, other: str | TMS | Iterable[TMS]) -> Self: result = self.__class__(other) return result + self def __iadd__(self, other: str | TMS | Iterable[TMS]) -> Self: if isinstance(other, str): self.extend(self._construct(other)) elif isinstance(other, MessageSegment): self.append(other) elif isinstance(other, Iterable): self.extend(other) else: raise TypeError(f"Unsupported type {type(other)!r}") return self @overload def __getitem__(self, args: str) -> Self: """获取仅包含指定消息段类型的消息 参数: args: 消息段类型 返回: 所有类型为 `args` 的消息段 """ @overload def __getitem__(self, args: tuple[str, int]) -> TMS: """索引指定类型的消息段 参数: args: 消息段类型和索引 返回: 类型为 `args[0]` 的消息段第 `args[1]` 个 """ @overload def __getitem__(self, args: tuple[str, slice]) -> Self: """切片指定类型的消息段 参数: args: 消息段类型和切片 返回: 类型为 `args[0]` 的消息段切片 `args[1]` """ @overload def __getitem__(self, args: int) -> TMS: """索引消息段 参数: args: 索引 返回: 第 `args` 个消息段 """ @overload def __getitem__(self, args: slice) -> Self: """切片消息段 参数: args: 切片 返回: 消息切片 `args` """ def __getitem__( # pyright: ignore[reportIncompatibleMethodOverride] self, args: str | tuple[str, int] | tuple[str, slice] | int | slice, ) -> TMS | Self: arg1, arg2 = args if isinstance(args, tuple) else (args, None) if isinstance(arg1, int) and arg2 is None: return super().__getitem__(arg1) elif isinstance(arg1, slice) and arg2 is None: return self.__class__(super().__getitem__(arg1)) elif isinstance(arg1, str) and arg2 is None: return self.__class__(seg for seg in self if seg.type == arg1) elif isinstance(arg1, str) and isinstance(arg2, int): return [seg for seg in self if seg.type == arg1][arg2] elif isinstance(arg1, str) and isinstance(arg2, slice): return self.__class__([seg for seg in self if seg.type == arg1][arg2]) else: raise ValueError("Incorrect arguments to slice") # pragma: no cover def __contains__( # pyright: ignore[reportIncompatibleMethodOverride] self, value: TMS | str ) -> bool: """检查消息段是否存在 参数: value: 消息段或消息段类型 返回: 消息内是否存在给定消息段或给定类型的消息段 """ if isinstance(value, str): return next((seg for seg in self if seg.type == value), None) is not None return super().__contains__(value) def has(self, value: TMS | str) -> bool: """与 {ref}``__contains__` ` 相同""" return value in self def index(self, value: TMS | str, *args: SupportsIndex) -> int: """索引消息段 参数: value: 消息段或者消息段类型 arg: start 与 end 返回: 索引 index 异常: ValueError: 消息段不存在 """ if isinstance(value, str): first_segment = next((seg for seg in self if seg.type == value), None) if first_segment is None: raise ValueError(f"Segment with type {value!r} is not in message") return super().index(first_segment, *args) return super().index(value, *args) def get(self, type_: str, count: int | None = None) -> Self: """获取指定类型的消息段 参数: type_: 消息段类型 count: 获取个数 返回: 构建的新消息 """ if count is None: return self[type_] iterator, filtered = ( (seg for seg in self if seg.type == type_), self.__class__(), ) for _ in range(count): seg = next(iterator, None) if seg is None: break filtered.append(seg) return filtered def count(self, value: TMS | str) -> int: """计算指定消息段的个数 参数: value: 消息段或消息段类型 返回: 个数 """ return len(self[value]) if isinstance(value, str) else super().count(value) def only(self, value: TMS | str) -> bool: """检查消息中是否仅包含指定消息段 参数: value: 指定消息段或消息段类型 返回: 是否仅包含指定消息段 """ if isinstance(value, str): return all(seg.type == value for seg in self) return all(seg == value for seg in self) def append( # pyright: ignore[reportIncompatibleMethodOverride] self, obj: str | TMS ) -> Self: """添加一个消息段到消息数组末尾。 参数: obj: 要添加的消息段 """ if isinstance(obj, MessageSegment): super().append(obj) elif isinstance(obj, str): self.extend(self._construct(obj)) else: raise ValueError(f"Unexpected type: {type(obj)} {obj}") # pragma: no cover return self def extend( # pyright: ignore[reportIncompatibleMethodOverride] self, obj: Self | Iterable[TMS] ) -> Self: """拼接一个消息数组或多个消息段到消息数组末尾。 参数: obj: 要添加的消息数组 """ for segment in obj: self.append(segment) return self def join(self, iterable: Iterable[TMS | Self]) -> Self: """将多个消息连接并将自身作为分割 参数: iterable: 要连接的消息 返回: 连接后的消息 """ ret = self.__class__() for index, msg in enumerate(iterable): if index != 0: ret.extend(self) if isinstance(msg, MessageSegment): ret.append(msg.copy()) else: ret.extend(msg.copy()) return ret def copy(self) -> Self: """深拷贝消息""" return deepcopy(self) def include(self, *types: str) -> Self: """过滤消息 参数: types: 包含的消息段类型 返回: 新构造的消息 """ return self.__class__(seg for seg in self if seg.type in types) def exclude(self, *types: str) -> Self: """过滤消息 参数: types: 不包含的消息段类型 返回: 新构造的消息 """ return self.__class__(seg for seg in self if seg.type not in types) def extract_plain_text(self) -> str: """提取消息内纯文本消息""" return "".join(str(seg) for seg in self if seg.is_text()) ================================================ FILE: nonebot/internal/adapter/template.py ================================================ from _string import formatter_field_name_split # type: ignore from collections.abc import Callable, Mapping, Sequence import functools from string import Formatter from typing import ( TYPE_CHECKING, Any, Generic, TypeAlias, TypeVar, cast, overload, ) if TYPE_CHECKING: from .message import Message, MessageSegment def formatter_field_name_split( field_name: str, ) -> tuple[str, list[tuple[bool, str]]]: ... TM = TypeVar("TM", bound="Message") TF = TypeVar("TF", str, "Message") FormatSpecFunc: TypeAlias = Callable[[Any], str] FormatSpecFunc_T = TypeVar("FormatSpecFunc_T", bound=FormatSpecFunc) class MessageTemplate(Formatter, Generic[TF]): """消息模板格式化实现类。 参数: template: 模板 factory: 消息类型工厂,默认为 `str` private_getattr: 是否允许在模板中访问私有属性,默认为 `False` """ @overload def __init__( self: "MessageTemplate[str]", template: str, factory: type[str] = str, private_getattr: bool = False, ) -> None: ... @overload def __init__( self: "MessageTemplate[TM]", template: str | TM, factory: type[TM], private_getattr: bool = False, ) -> None: ... def __init__( self, template: str | TM, factory: type[str] | type[TM] = str, private_getattr: bool = False, ) -> None: self.template: TF = template # type: ignore self.factory: type[TF] = factory # type: ignore self.format_specs: dict[str, FormatSpecFunc] = {} self.private_getattr = private_getattr def __repr__(self) -> str: return f"MessageTemplate({self.template!r}, factory={self.factory!r})" def add_format_spec( self, spec: FormatSpecFunc_T, name: str | None = None ) -> FormatSpecFunc_T: name = name or spec.__name__ if name in self.format_specs: raise ValueError(f"Format spec {name} already exists!") self.format_specs[name] = spec return spec def format( # pyright: ignore[reportIncompatibleMethodOverride] self, *args, **kwargs ) -> TF: """根据传入参数和模板生成消息对象""" return self._format(args, kwargs) def format_map(self, mapping: Mapping[str, Any]) -> TF: """根据传入字典和模板生成消息对象, 在传入字段名不是有效标识符时有用""" return self._format([], mapping) def _format(self, args: Sequence[Any], kwargs: Mapping[str, Any]) -> TF: full_message = self.factory() used_args, arg_index = set(), 0 if isinstance(self.template, str): msg, arg_index = self._vformat( self.template, args, kwargs, used_args, arg_index ) full_message += msg elif isinstance(self.template, self.factory): template = cast("Message[MessageSegment]", self.template) for seg in template: if not seg.is_text(): full_message += seg else: msg, arg_index = self._vformat( str(seg), args, kwargs, used_args, arg_index ) full_message += msg else: raise TypeError("template must be a string or instance of Message!") self.check_unused_args(used_args, args, kwargs) return cast(TF, full_message) def vformat( # pyright: ignore[reportIncompatibleMethodOverride] self, format_string: str, args: Sequence[Any], kwargs: Mapping[str, Any], ) -> TF: raise NotImplementedError("`vformat` has merged into `_format`") def _vformat( # pyright: ignore[reportIncompatibleMethodOverride] self, format_string: str, args: Sequence[Any], kwargs: Mapping[str, Any], used_args: set[int | str], auto_arg_index: int = 0, ) -> tuple[TF, int]: results: list[Any] = [self.factory()] for literal_text, field_name, format_spec, conversion in self.parse( format_string ): # output the literal text if literal_text: results.append(literal_text) # if there's a field, output it if field_name is not None: # this is some markup, find the object and do # the formatting # handle arg indexing when empty field_names are given. if field_name == "": if auto_arg_index is False: raise ValueError( "cannot switch from manual field specification to " "automatic field numbering" ) field_name = str(auto_arg_index) auto_arg_index += 1 elif field_name.isdigit(): if auto_arg_index: raise ValueError( "cannot switch from manual field specification to " "automatic field numbering" ) # disable auto arg incrementing, if it gets # used later on, then an exception will be raised auto_arg_index = False # given the field_name, find the object it references # and the argument it came from obj, arg_used = self.get_field(field_name, args, kwargs) used_args.add(arg_used) # do any conversion on the resulting object obj = self.convert_field(obj, conversion) if conversion else obj # format the object and append to the result formatted_text = ( self.format_field(obj, format_spec) if format_spec else obj ) results.append(formatted_text) return functools.reduce(self._add, results), auto_arg_index def get_field( self, field_name: str, args: Sequence[Any], kwargs: Mapping[str, Any] ) -> tuple[Any, int | str]: first, rest = formatter_field_name_split(field_name) obj = self.get_value(first, args, kwargs) for is_attr, value in rest: if not self.private_getattr and value.startswith("_"): raise ValueError("Cannot access private attribute") obj = getattr(obj, value) if is_attr else obj[value] return obj, first def format_field(self, value: Any, format_spec: str) -> Any: formatter: FormatSpecFunc | None = self.format_specs.get(format_spec) if formatter is None and not issubclass(self.factory, str): segment_class: type["MessageSegment"] = self.factory.get_segment_class() method = getattr(segment_class, format_spec, None) if callable(method) and not cast(str, method.__name__).startswith("_"): formatter = getattr(segment_class, format_spec) return ( super().format_field(value, format_spec) if formatter is None else formatter(value) ) def _add(self, a: Any, b: Any) -> Any: try: return a + b except TypeError: return a + str(b) ================================================ FILE: nonebot/internal/driver/__init__.py ================================================ from .abstract import ASGIMixin as ASGIMixin from .abstract import Driver as Driver from .abstract import ForwardDriver as ForwardDriver from .abstract import ForwardMixin as ForwardMixin from .abstract import HTTPClientMixin as HTTPClientMixin from .abstract import HTTPClientSession as HTTPClientSession from .abstract import Mixin as Mixin 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 URL as URL from .model import ContentTypes as ContentTypes from .model import Cookies as Cookies from .model import CookieTypes as CookieTypes from .model import DataTypes as DataTypes from .model import FileContent as FileContent from .model import FilesTypes as FilesTypes from .model import FileType as FileType from .model import FileTypes as FileTypes from .model import HeaderTypes as HeaderTypes from .model import HTTPServerSetup as HTTPServerSetup from .model import HTTPVersion as HTTPVersion from .model import QueryTypes as QueryTypes from .model import QueryVariable as QueryVariable from .model import RawURL as RawURL from .model import Request as Request from .model import Response as Response from .model import SimpleQuery as SimpleQuery from .model import Timeout as Timeout from .model import TimeoutTypes as TimeoutTypes from .model import WebSocket as WebSocket from .model import WebSocketServerSetup as WebSocketServerSetup ================================================ FILE: nonebot/internal/driver/_lifespan.py ================================================ from collections.abc import Awaitable, Callable, Iterable from types import TracebackType from typing import Any, TypeAlias, cast import anyio from anyio.abc import TaskGroup from exceptiongroup import suppress from nonebot.utils import is_coroutine_callable, run_sync SYNC_LIFESPAN_FUNC: TypeAlias = Callable[[], Any] ASYNC_LIFESPAN_FUNC: TypeAlias = Callable[[], Awaitable[Any]] LIFESPAN_FUNC: TypeAlias = SYNC_LIFESPAN_FUNC | ASYNC_LIFESPAN_FUNC class Lifespan: def __init__(self) -> None: self._task_group: TaskGroup | None = None self._startup_funcs: list[LIFESPAN_FUNC] = [] self._ready_funcs: list[LIFESPAN_FUNC] = [] self._shutdown_funcs: list[LIFESPAN_FUNC] = [] @property def task_group(self) -> TaskGroup: if self._task_group is None: raise RuntimeError("Lifespan not started") return self._task_group @task_group.setter def task_group(self, task_group: TaskGroup) -> None: if self._task_group is not None: raise RuntimeError("Lifespan already started") self._task_group = task_group def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC: self._startup_funcs.append(func) return func def on_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC: self._shutdown_funcs.append(func) return func def on_ready(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC: self._ready_funcs.append(func) return func @staticmethod async def _run_lifespan_func( funcs: Iterable[LIFESPAN_FUNC], ) -> None: for func in funcs: if is_coroutine_callable(func): await cast(ASYNC_LIFESPAN_FUNC, func)() else: await run_sync(cast(SYNC_LIFESPAN_FUNC, func))() async def startup(self) -> None: # create background task group self.task_group = anyio.create_task_group() await self.task_group.__aenter__() # run startup funcs if self._startup_funcs: await self._run_lifespan_func(self._startup_funcs) # run ready funcs if self._ready_funcs: await self._run_lifespan_func(self._ready_funcs) async def shutdown( self, *, exc_type: type[BaseException] | None = None, exc_val: BaseException | None = None, exc_tb: TracebackType | None = None, ) -> None: if self._shutdown_funcs: # reverse shutdown funcs to ensure stack order await self._run_lifespan_func(reversed(self._shutdown_funcs)) # shutdown background task group self.task_group.cancel_scope.cancel() with suppress(Exception): await self.task_group.__aexit__(exc_type, exc_val, exc_tb) self._task_group = None async def __aenter__(self) -> None: await self.startup() async def __aexit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> None: await self.shutdown(exc_type=exc_type, exc_val=exc_val, exc_tb=exc_tb) ================================================ FILE: nonebot/internal/driver/abstract.py ================================================ import abc from collections.abc import AsyncGenerator from contextlib import AsyncExitStack, asynccontextmanager from types import TracebackType from typing import TYPE_CHECKING, Any, ClassVar, TypeAlias from typing_extensions import Self from anyio import CancelScope, create_task_group from anyio.abc import TaskGroup from exceptiongroup import BaseExceptionGroup, catch from nonebot.config import Config, Env from nonebot.dependencies import Dependent from nonebot.exception import SkippedException from nonebot.internal.params import BotParam, DefaultParam, DependParam from nonebot.log import logger from nonebot.typing import ( T_BotConnectionHook, T_BotDisconnectionHook, T_DependencyCache, ) from nonebot.utils import escape_tag, flatten_exception_group, run_coro_with_catch from ._lifespan import LIFESPAN_FUNC, Lifespan from .model import ( CookieTypes, HeaderTypes, HTTPServerSetup, HTTPVersion, QueryTypes, Request, Response, TimeoutTypes, WebSocket, WebSocketServerSetup, ) if TYPE_CHECKING: from nonebot.internal.adapter import Adapter, Bot BOT_HOOK_PARAMS = [DependParam, BotParam, DefaultParam] class Driver(abc.ABC): """驱动器基类。 驱动器控制框架的启动和停止,适配器的注册,以及机器人生命周期管理。 参数: env: 包含环境信息的 Env 对象 config: 包含配置信息的 Config 对象 """ _adapters: ClassVar[dict[str, "Adapter"]] = {} """已注册的适配器列表""" _bot_connection_hook: ClassVar[set[Dependent[Any]]] = set() """Bot 连接建立时执行的函数""" _bot_disconnection_hook: ClassVar[set[Dependent[Any]]] = set() """Bot 连接断开时执行的函数""" def __init__(self, env: Env, config: Config): self.env: str = env.environment """环境名称""" self.config: Config = config """全局配置对象""" self._bots: dict[str, "Bot"] = {} self._lifespan = Lifespan() def __repr__(self) -> str: return ( f"Driver(type={self.type!r}, " f"adapters={len(self._adapters)}, bots={len(self._bots)})" ) @property def bots(self) -> dict[str, "Bot"]: """获取当前所有已连接的 Bot""" return self._bots @property def task_group(self) -> TaskGroup: return self._lifespan.task_group def register_adapter(self, adapter: type["Adapter"], **kwargs) -> None: """注册一个协议适配器 参数: adapter: 适配器类 kwargs: 其他传递给适配器的参数 """ name = adapter.get_name() if name in self._adapters: logger.opt(colors=True).debug( f'Adapter "{escape_tag(name)}" already exists' ) return self._adapters[name] = adapter(self, **kwargs) logger.opt(colors=True).debug( f'Succeeded to load adapter "{escape_tag(name)}"' ) @property @abc.abstractmethod def type(self) -> str: """驱动类型名称""" raise NotImplementedError @property @abc.abstractmethod def logger(self): """驱动专属 logger 日志记录器""" raise NotImplementedError @abc.abstractmethod def run(self, *args, **kwargs): """启动驱动框架""" logger.opt(colors=True).success( f"Loaded adapters: {escape_tag(', '.join(self._adapters))}" ) def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC: """注册一个启动时执行的函数""" return self._lifespan.on_startup(func) def on_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC: """注册一个停止时执行的函数""" return self._lifespan.on_shutdown(func) @classmethod def on_bot_connect(cls, func: T_BotConnectionHook) -> T_BotConnectionHook: """装饰一个函数使他在 bot 连接成功时执行。 钩子函数参数: - bot: 当前连接上的 Bot 对象 """ cls._bot_connection_hook.add( Dependent[Any].parse(call=func, allow_types=BOT_HOOK_PARAMS) ) return func @classmethod def on_bot_disconnect(cls, func: T_BotDisconnectionHook) -> T_BotDisconnectionHook: """装饰一个函数使他在 bot 连接断开时执行。 钩子函数参数: - bot: 当前连接上的 Bot 对象 """ cls._bot_disconnection_hook.add( Dependent[Any].parse(call=func, allow_types=BOT_HOOK_PARAMS) ) return func def _bot_connect(self, bot: "Bot") -> None: """在连接成功后,调用该函数来注册 bot 对象""" if bot.self_id in self._bots: raise RuntimeError(f"Duplicate bot connection with id {bot.self_id}") self._bots[bot.self_id] = bot if not self._bot_connection_hook: return def handle_exception(exc_group: BaseExceptionGroup) -> None: for exc in flatten_exception_group(exc_group): logger.opt(colors=True, exception=exc).error( "" "Error when running WebSocketConnection hook:" "" ) async def _run_hook(bot: "Bot") -> None: dependency_cache: T_DependencyCache = {} with CancelScope(shield=True), catch({Exception: handle_exception}): async with AsyncExitStack() as stack, create_task_group() as tg: for hook in self._bot_connection_hook: tg.start_soon( run_coro_with_catch, hook( bot=bot, stack=stack, dependency_cache=dependency_cache ), (SkippedException,), ) self.task_group.start_soon(_run_hook, bot) def _bot_disconnect(self, bot: "Bot") -> None: """在连接断开后,调用该函数来注销 bot 对象""" if bot.self_id in self._bots: del self._bots[bot.self_id] if not self._bot_disconnection_hook: return def handle_exception(exc_group: BaseExceptionGroup) -> None: for exc in flatten_exception_group(exc_group): logger.opt(colors=True, exception=exc).error( "" "Error when running WebSocketDisConnection hook:" "" ) async def _run_hook(bot: "Bot") -> None: dependency_cache: T_DependencyCache = {} # shield cancellation to ensure bot disconnect hooks are always run with CancelScope(shield=True), catch({Exception: handle_exception}): async with create_task_group() as tg, AsyncExitStack() as stack: for hook in self._bot_disconnection_hook: tg.start_soon( run_coro_with_catch, hook( bot=bot, stack=stack, dependency_cache=dependency_cache ), (SkippedException,), ) self.task_group.start_soon(_run_hook, bot) class Mixin(abc.ABC): """可与其他驱动器共用的混入基类。""" @property @abc.abstractmethod def type(self) -> str: """混入驱动类型名称""" raise NotImplementedError class ForwardMixin(Mixin): """客户端混入基类。""" class ReverseMixin(Mixin): """服务端混入基类。""" class HTTPClientSession(abc.ABC): """HTTP 客户端会话基类。""" @abc.abstractmethod def __init__( self, params: QueryTypes = None, headers: HeaderTypes = None, cookies: CookieTypes = None, version: str | HTTPVersion = HTTPVersion.H11, timeout: TimeoutTypes = None, proxy: str | None = None, ): raise NotImplementedError @abc.abstractmethod async def request(self, setup: Request) -> Response: """发送一个 HTTP 请求""" raise NotImplementedError @abc.abstractmethod async def stream_request( self, setup: Request, *, chunk_size: int = 1024, ) -> AsyncGenerator[Response, None]: """发送一个 HTTP 流式请求""" raise NotImplementedError yield # used for static type checking's generator detection @abc.abstractmethod async def setup(self) -> None: """初始化会话""" raise NotImplementedError @abc.abstractmethod async def close(self) -> None: """关闭会话""" raise NotImplementedError async def __aenter__(self) -> Self: await self.setup() return self async def __aexit__( self, exc_type: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None, ) -> None: await self.close() class HTTPClientMixin(ForwardMixin): """HTTP 客户端混入基类。""" @abc.abstractmethod async def request(self, setup: Request) -> Response: """发送一个 HTTP 请求""" raise NotImplementedError @abc.abstractmethod async def stream_request( self, setup: Request, *, chunk_size: int = 1024, ) -> AsyncGenerator[Response, None]: """发送一个 HTTP 流式请求""" raise NotImplementedError yield # used for static type checking's generator detection @abc.abstractmethod def get_session( self, params: QueryTypes = None, headers: HeaderTypes = None, cookies: CookieTypes = None, version: str | HTTPVersion = HTTPVersion.H11, timeout: TimeoutTypes = None, proxy: str | None = None, ) -> HTTPClientSession: """获取一个 HTTP 会话""" raise NotImplementedError class WebSocketClientMixin(ForwardMixin): """WebSocket 客户端混入基类。""" @abc.abstractmethod @asynccontextmanager async def websocket(self, setup: Request) -> AsyncGenerator[WebSocket, None]: """发起一个 WebSocket 连接""" raise NotImplementedError yield # used for static type checking's generator detection class ASGIMixin(ReverseMixin): """ASGI 服务端基类。 将后端框架封装,以满足适配器使用。 """ @property @abc.abstractmethod def server_app(self) -> Any: """驱动 APP 对象""" raise NotImplementedError @property @abc.abstractmethod def asgi(self) -> Any: """驱动 ASGI 对象""" raise NotImplementedError @abc.abstractmethod def setup_http_server(self, setup: "HTTPServerSetup") -> None: """设置一个 HTTP 服务器路由配置""" raise NotImplementedError @abc.abstractmethod def setup_websocket_server(self, setup: "WebSocketServerSetup") -> None: """设置一个 WebSocket 服务器路由配置""" raise NotImplementedError ForwardDriver: TypeAlias = ForwardMixin """支持客户端请求的驱动器。 **Deprecated**,请使用 {ref}`nonebot.drivers.ForwardMixin` 或其子类代替。 """ ReverseDriver: TypeAlias = ReverseMixin """支持服务端请求的驱动器。 **Deprecated**,请使用 {ref}`nonebot.drivers.ReverseMixin` 或其子类代替。 """ ================================================ FILE: nonebot/internal/driver/combine.py ================================================ from typing import TYPE_CHECKING, TypeVar, overload from .abstract import Driver, Mixin D = TypeVar("D", bound="Driver") if TYPE_CHECKING: class CombinedDriver(Driver, Mixin): ... @overload def combine_driver(driver: type[D]) -> type[D]: ... @overload def combine_driver( driver: type[D], __m: type[Mixin], /, *mixins: type[Mixin] ) -> type["CombinedDriver"]: ... def combine_driver( driver: type[D], *mixins: type[Mixin] ) -> type[D] | type["CombinedDriver"]: """将一个驱动器和多个混入类合并。""" # check first if not issubclass(driver, Driver): raise TypeError("`driver` must be subclass of Driver") if not all(issubclass(m, Mixin) for m in mixins): raise TypeError("`mixins` must be subclass of Mixin") if not mixins: return driver def type_(self: "CombinedDriver") -> str: return ( driver.type.__get__(self) # type: ignore + "+" + "+".join(x.type.__get__(self) for x in mixins) # type: ignore ) return type("CombinedDriver", (*mixins, driver), {"type": property(type_)}) # type: ignore ================================================ FILE: nonebot/internal/driver/model.py ================================================ import abc from collections.abc import Awaitable, Callable, Iterator, Mapping, MutableMapping from dataclasses import dataclass from enum import Enum from http.cookiejar import Cookie, CookieJar from typing import IO, Any, TypeAlias import urllib.request from multidict import CIMultiDict from yarl import URL as URL @dataclass class Timeout: """Request 超时配置。""" total: float | None = None connect: float | None = None read: float | None = None RawURL: TypeAlias = tuple[bytes, bytes, int | None, bytes] SimpleQuery: TypeAlias = str | int | float QueryVariable: TypeAlias = SimpleQuery | list[SimpleQuery] QueryTypes: TypeAlias = ( None | str | Mapping[str, QueryVariable] | list[tuple[str, SimpleQuery]] ) HeaderTypes: TypeAlias = ( None | CIMultiDict[str] | dict[str, str] | list[tuple[str, str]] ) CookieTypes: TypeAlias = ( "None | Cookies | CookieJar | dict[str, str] | list[tuple[str, str]]" ) ContentTypes: TypeAlias = str | bytes | None DataTypes: TypeAlias = dict | None FileContent: TypeAlias = IO[bytes] | bytes FileType: TypeAlias = tuple[str | None, FileContent, str | None] FileTypes: TypeAlias = ( FileContent # file (or bytes) | tuple[str | None, FileContent] # (filename, file (or bytes)) | FileType # (filename, file (or bytes), content_type) ) FilesTypes: TypeAlias = dict[str, FileTypes] | list[tuple[str, FileTypes]] | None TimeoutTypes: TypeAlias = float | Timeout | None class HTTPVersion(Enum): H10 = "1.0" H11 = "1.1" H2 = "2" class Request: def __init__( self, method: str | bytes, url: "URL | str | RawURL", *, params: QueryTypes = None, headers: HeaderTypes = None, cookies: CookieTypes = None, content: ContentTypes = None, data: DataTypes = None, json: Any = None, files: FilesTypes = None, version: str | HTTPVersion = HTTPVersion.H11, timeout: TimeoutTypes = None, proxy: str | None = None, ): # method self.method: str = ( method.decode("ascii").upper() if isinstance(method, bytes) else method.upper() ) # http version self.version: HTTPVersion = HTTPVersion(version) # timeout self.timeout: TimeoutTypes = timeout # proxy self.proxy: str | None = proxy # url if isinstance(url, tuple): scheme, host, port, path = url url = URL.build( scheme=scheme.decode("ascii"), host=host.decode("ascii"), port=port, path=path.decode("ascii"), ) else: url = URL(url) if params is not None: url = url.update_query(params) self.url: URL = url # headers self.headers: CIMultiDict[str] = ( CIMultiDict(headers) if headers is not None else CIMultiDict() ) # cookies self.cookies = Cookies(cookies) # body self.content: ContentTypes = content self.data: DataTypes = data self.json: Any = json self.files: list[tuple[str, FileType]] | None = None if files: self.files = [] files_ = files.items() if isinstance(files, dict) else files for name, file_info in files_: if not isinstance(file_info, tuple): self.files.append((name, (name, file_info, None))) elif len(file_info) == 2: self.files.append((name, (file_info[0], file_info[1], None))) else: self.files.append((name, file_info)) # type: ignore def __repr__(self) -> str: return f"{self.__class__.__name__}(method={self.method!r}, url='{self.url!s}')" class Response: def __init__( self, status_code: int, *, headers: HeaderTypes = None, content: ContentTypes = None, request: Request | None = None, ): # status code self.status_code: int = status_code # headers self.headers: CIMultiDict[str] = ( CIMultiDict(headers) if headers is not None else CIMultiDict() ) # body self.content: ContentTypes = content # request self.request: Request | None = request def __repr__(self) -> str: return f"{self.__class__.__name__}(status_code={self.status_code!r})" class WebSocket(abc.ABC): def __init__(self, *, request: Request): self.request: Request = request def __repr__(self) -> str: return f"{self.__class__.__name__}('{self.request.url!s}')" @property @abc.abstractmethod def closed(self) -> bool: """连接是否已经关闭""" raise NotImplementedError @abc.abstractmethod async def accept(self) -> None: """接受 WebSocket 连接请求""" raise NotImplementedError @abc.abstractmethod async def close(self, code: int = 1000, reason: str = "") -> None: """关闭 WebSocket 连接请求""" raise NotImplementedError @abc.abstractmethod async def receive(self) -> str | bytes: """接收一条 WebSocket text/bytes 信息""" raise NotImplementedError @abc.abstractmethod async def receive_text(self) -> str: """接收一条 WebSocket text 信息""" raise NotImplementedError @abc.abstractmethod async def receive_bytes(self) -> bytes: """接收一条 WebSocket binary 信息""" raise NotImplementedError async def send(self, data: str | bytes) -> None: """发送一条 WebSocket text/bytes 信息""" if isinstance(data, str): await self.send_text(data) elif isinstance(data, bytes): await self.send_bytes(data) else: raise TypeError("WebSocker send method expects str or bytes!") @abc.abstractmethod async def send_text(self, data: str) -> None: """发送一条 WebSocket text 信息""" raise NotImplementedError @abc.abstractmethod async def send_bytes(self, data: bytes) -> None: """发送一条 WebSocket binary 信息""" raise NotImplementedError class Cookies(MutableMapping): def __init__(self, cookies: CookieTypes = None) -> None: self.jar: CookieJar = cookies if isinstance(cookies, CookieJar) else CookieJar() if cookies is not None and not isinstance(cookies, CookieJar): if isinstance(cookies, dict): for key, value in cookies.items(): self.set(key, value) elif isinstance(cookies, list): for key, value in cookies: self.set(key, value) elif isinstance(cookies, Cookies): for cookie in cookies.jar: self.jar.set_cookie(cookie) else: raise TypeError(f"Cookies must be dict or list, not {type(cookies)}") def set(self, name: str, value: str, domain: str = "", path: str = "/") -> None: cookie = Cookie( version=0, name=name, value=value, port=None, port_specified=False, domain=domain, domain_specified=bool(domain), domain_initial_dot=domain.startswith("."), path=path, path_specified=bool(path), secure=False, expires=None, discard=True, comment=None, comment_url=None, rest={}, rfc2109=False, ) self.jar.set_cookie(cookie) def get( # pyright: ignore[reportIncompatibleMethodOverride] self, name: str, default: str | None = None, domain: str | None = None, path: str | None = None, ) -> str | None: value: str | None = None for cookie in self.jar: if ( cookie.name == name and (domain is None or cookie.domain == domain) and (path is None or cookie.path == path) ): if value is not None: message = f"Multiple cookies exist with name={name}" raise ValueError(message) value = cookie.value return default if value is None else value def delete( self, name: str, domain: str | None = None, path: str | None = None ) -> None: if domain is not None and path is not None: return self.jar.clear(domain, path, name) remove = [ cookie for cookie in self.jar if cookie.name == name and (domain is None or cookie.domain == domain) and (path is None or cookie.path == path) ] for cookie in remove: self.jar.clear(cookie.domain, cookie.path, cookie.name) def clear(self, domain: str | None = None, path: str | None = None) -> None: self.jar.clear(domain, path) def update( # pyright: ignore[reportIncompatibleMethodOverride] self, cookies: CookieTypes = None ) -> None: cookies = Cookies(cookies) for cookie in cookies.jar: self.jar.set_cookie(cookie) def as_header(self, request: Request) -> dict[str, str]: urllib_request = self._CookieCompatRequest(request) self.jar.add_cookie_header(urllib_request) return urllib_request.added_headers def __setitem__(self, name: str, value: str) -> None: return self.set(name, value) def __getitem__(self, name: str) -> str: value = self.get(name) if value is None: raise KeyError(name) return value def __delitem__(self, name: str) -> None: return self.delete(name) def __len__(self) -> int: return len(self.jar) def __iter__(self) -> Iterator[Cookie]: return iter(self.jar) def __repr__(self) -> str: cookies_repr = ", ".join( f"Cookie({cookie.name}={cookie.value} for {cookie.domain})" for cookie in self.jar ) return f"{self.__class__.__name__}({cookies_repr})" class _CookieCompatRequest(urllib.request.Request): def __init__(self, request: Request) -> None: super().__init__( url=str(request.url), headers=dict(request.headers), method=request.method, ) self.request = request self.added_headers: dict[str, str] = {} def add_unredirected_header( # pyright: ignore[reportIncompatibleMethodOverride] self, key: str, value: str ) -> None: super().add_unredirected_header(key, value) self.added_headers[key] = value @dataclass class HTTPServerSetup: """HTTP 服务器路由配置。""" path: URL # path should not be absolute, check it by URL.is_absolute() == False method: str name: str handle_func: Callable[[Request], Awaitable[Response]] @dataclass class WebSocketServerSetup: """WebSocket 服务器路由配置。""" path: URL # path should not be absolute, check it by URL.is_absolute() == False name: str handle_func: Callable[[WebSocket], Awaitable[Any]] ================================================ FILE: nonebot/internal/matcher/__init__.py ================================================ from .manager import MatcherManager as MatcherManager from .provider import DEFAULT_PROVIDER_CLASS as DEFAULT_PROVIDER_CLASS from .provider import MatcherProvider as MatcherProvider matchers = MatcherManager() from .matcher import Matcher as Matcher from .matcher import MatcherSource as MatcherSource from .matcher import current_bot as current_bot from .matcher import current_event as current_event from .matcher import current_handler as current_handler from .matcher import current_matcher as current_matcher ================================================ FILE: nonebot/internal/matcher/manager.py ================================================ from collections.abc import ItemsView, Iterator, KeysView, MutableMapping, ValuesView from typing import TYPE_CHECKING, TypeVar, overload from .provider import DEFAULT_PROVIDER_CLASS, MatcherProvider if TYPE_CHECKING: from .matcher import Matcher T = TypeVar("T") class MatcherManager(MutableMapping[int, list[type["Matcher"]]]): """事件响应器管理器 实现了常用字典操作,用于管理事件响应器。 """ def __init__(self): self.provider: MatcherProvider = DEFAULT_PROVIDER_CLASS({}) def __repr__(self) -> str: return f"MatcherManager(provider={self.provider!r})" def __contains__(self, o: object) -> bool: return o in self.provider def __iter__(self) -> Iterator[int]: return iter(self.provider) def __len__(self) -> int: return len(self.provider) def __getitem__(self, key: int) -> list[type["Matcher"]]: return self.provider[key] def __setitem__(self, key: int, value: list[type["Matcher"]]) -> None: self.provider[key] = value def __delitem__(self, key: int) -> None: del self.provider[key] def __eq__(self, other: object) -> bool: return isinstance(other, MatcherManager) and self.provider == other.provider def keys(self) -> KeysView[int]: return self.provider.keys() def values(self) -> ValuesView[list[type["Matcher"]]]: return self.provider.values() def items(self) -> ItemsView[int, list[type["Matcher"]]]: return self.provider.items() @overload def get(self, key: int) -> list[type["Matcher"]] | None: ... @overload def get( self, key: int, default: list[type["Matcher"]] ) -> list[type["Matcher"]]: ... @overload def get(self, key: int, default: T) -> list[type["Matcher"]] | T: ... def get( self, key: int, default: T | None = None ) -> list[type["Matcher"]] | T | None: return self.provider.get(key, default) def pop( # pyright: ignore[reportIncompatibleMethodOverride] self, key: int ) -> list[type["Matcher"]]: return self.provider.pop(key) def popitem(self) -> tuple[int, list[type["Matcher"]]]: return self.provider.popitem() def clear(self) -> None: self.provider.clear() def update( # pyright: ignore[reportIncompatibleMethodOverride] self, m: MutableMapping[int, list[type["Matcher"]]], / ) -> None: self.provider.update(m) def setdefault( self, key: int, default: list[type["Matcher"]] ) -> list[type["Matcher"]]: return self.provider.setdefault(key, default) def set_provider(self, provider_class: type[MatcherProvider]) -> None: """设置事件响应器存储器 参数: provider_class: 事件响应器存储器类 """ self.provider = provider_class(self.provider) ================================================ FILE: nonebot/internal/matcher/matcher.py ================================================ from collections.abc import Iterable from contextlib import AsyncExitStack, contextmanager from contextvars import ContextVar from dataclasses import dataclass from datetime import datetime, timedelta import inspect from pathlib import Path import sys from types import ModuleType from typing import ( # noqa: UP035 TYPE_CHECKING, Any, Callable, ClassVar, NoReturn, Type, TypeVar, overload, ) from typing_extensions import Self import warnings from exceptiongroup import BaseExceptionGroup, catch from nonebot.consts import ( ARG_KEY, LAST_RECEIVE_KEY, PAUSE_PROMPT_RESULT_KEY, RECEIVE_KEY, REJECT_CACHE_TARGET, REJECT_PROMPT_RESULT_KEY, REJECT_TARGET, ) from nonebot.dependencies import Dependent, Param from nonebot.exception import ( FinishedException, PausedException, RejectedException, SkippedException, StopPropagation, ) from nonebot.internal.adapter import ( Bot, Event, Message, MessageSegment, MessageTemplate, ) from nonebot.internal.params import ( ArgParam, BotParam, DefaultParam, DependParam, Depends, EventParam, MatcherParam, StateParam, ) from nonebot.internal.permission import Permission, User from nonebot.internal.rule import Rule from nonebot.log import logger from nonebot.typing import ( T_DependencyCache, T_Handler, T_PermissionUpdater, T_State, T_TypeUpdater, ) from nonebot.utils import classproperty, flatten_exception_group from . import matchers if TYPE_CHECKING: from nonebot.plugin import Plugin T = TypeVar("T") current_bot: ContextVar[Bot] = ContextVar("current_bot") current_event: ContextVar[Event] = ContextVar("current_event") current_matcher: ContextVar["Matcher"] = ContextVar("current_matcher") current_handler: ContextVar[Dependent[Any]] = ContextVar("current_handler") @dataclass class MatcherSource: """Matcher 源代码上下文信息""" plugin_id: str | None = None """事件响应器所在插件标识符""" module_name: str | None = None """事件响应器所在插件模块的路径名""" lineno: int | None = None """事件响应器所在行号""" @property def plugin(self) -> "Plugin | None": """事件响应器所在插件""" from nonebot.plugin import get_plugin if self.plugin_id is not None: return get_plugin(self.plugin_id) @property def plugin_name(self) -> str | None: """事件响应器所在插件名""" return self.plugin and self.plugin.name @property def module(self) -> ModuleType | None: if self.module_name is not None: return sys.modules.get(self.module_name) @property def file(self) -> Path | None: if self.module is not None and (file := inspect.getsourcefile(self.module)): return Path(file).absolute() class MatcherMeta(type): if TYPE_CHECKING: type: str _source: MatcherSource | None module_name: str | None def __repr__(self) -> str: return ( f"{self.__name__}(type={self.type!r}" + (f", module={self.module_name}" if self.module_name else "") + ( f", lineno={self._source.lineno}" if self._source and self._source.lineno is not None else "" ) + ")" ) class Matcher(metaclass=MatcherMeta): """事件响应器类""" _source: ClassVar[MatcherSource | None] = None type: ClassVar[str] = "" """事件响应器类型""" rule: ClassVar[Rule] = Rule() """事件响应器匹配规则""" permission: ClassVar[Permission] = Permission() """事件响应器触发权限""" handlers: ClassVar[list[Dependent[Any]]] = [] """事件响应器拥有的事件处理函数列表""" priority: ClassVar[int] = 1 """事件响应器优先级""" block: bool = False """事件响应器是否阻止事件传播""" temp: ClassVar[bool] = False """事件响应器是否为临时""" expire_time: ClassVar[datetime | None] = None """事件响应器过期时间点""" _default_state: ClassVar[T_State] = {} """事件响应器默认状态""" _default_type_updater: ClassVar[Dependent[str] | None] = None """事件响应器类型更新函数""" _default_permission_updater: ClassVar[Dependent[Permission] | None] = None """事件响应器权限更新函数""" HANDLER_PARAM_TYPES: ClassVar[tuple[Type[Param], ...]] = ( # noqa: UP006 DependParam, BotParam, EventParam, StateParam, ArgParam, MatcherParam, DefaultParam, ) def __init__(self): self.remain_handlers: list[Dependent[Any]] = self.handlers.copy() self.state = self._default_state.copy() def __repr__(self) -> str: return ( f"{self.__class__.__name__}(type={self.type!r}" + (f", module={self.module_name}" if self.module_name else "") + ( f", lineno={self._source.lineno}" if self._source and self._source.lineno is not None else "" ) + ")" ) @classmethod def new( cls, type_: str = "", rule: Rule | None = None, permission: Permission | None = None, handlers: list[T_Handler | Dependent[Any]] | None = None, temp: bool = False, priority: int = 1, block: bool = False, *, plugin: "Plugin | None" = None, module: ModuleType | None = None, source: MatcherSource | None = None, expire_time: datetime | timedelta | None = None, default_state: T_State | None = None, default_type_updater: T_TypeUpdater | Dependent[str] | None = None, default_permission_updater: T_PermissionUpdater | Dependent[Permission] | None = None, ) -> Type[Self]: # noqa: UP006 """ 创建一个新的事件响应器,并存储至 `matchers <#matchers>`_ 参数: type_: 事件响应器类型,与 `event.get_type()` 一致时触发,空字符串表示任意 rule: 匹配规则 permission: 权限 handlers: 事件处理函数列表 temp: 是否为临时事件响应器,即触发一次后删除 priority: 响应优先级 block: 是否阻止事件向更低优先级的响应器传播 plugin: **Deprecated.** 事件响应器所在插件 module: **Deprecated.** 事件响应器所在模块 source: 事件响应器源代码上下文信息 expire_time: 事件响应器最终有效时间点,过时即被删除 default_state: 默认状态 `state` default_type_updater: 默认事件类型更新函数 default_permission_updater: 默认会话权限更新函数 返回: Type[Matcher]: 新的事件响应器类 """ if plugin is not None: warnings.warn( ( "Pass `plugin` context info to create Matcher is deprecated. " "Use `source` instead." ), DeprecationWarning, ) if module is not None: warnings.warn( ( "Pass `module` context info to create Matcher is deprecated. " "Use `source` instead." ), DeprecationWarning, ) source = source or ( MatcherSource( plugin_id=plugin and plugin.id_, module_name=module and module.__name__, ) if plugin is not None or module is not None else None ) NewMatcher = type( cls.__name__, (cls,), { "_source": source, "type": type_, "rule": rule or Rule(), "permission": permission or Permission(), "handlers": ( [ ( handler if isinstance(handler, Dependent) else Dependent[Any].parse( call=handler, allow_types=cls.HANDLER_PARAM_TYPES ) ) for handler in handlers ] if handlers else [] ), "temp": temp, "expire_time": ( expire_time and ( expire_time if isinstance(expire_time, datetime) else datetime.now() + expire_time ) ), "priority": priority, "block": block, "_default_state": default_state or {}, "_default_type_updater": ( default_type_updater and ( default_type_updater if isinstance(default_type_updater, Dependent) else Dependent[str].parse( call=default_type_updater, allow_types=cls.HANDLER_PARAM_TYPES, ) ) ), "_default_permission_updater": ( default_permission_updater and ( default_permission_updater if isinstance(default_permission_updater, Dependent) else Dependent[Permission].parse( call=default_permission_updater, allow_types=cls.HANDLER_PARAM_TYPES, ) ) ), }, ) logger.trace(f"Define new matcher {NewMatcher}") matchers[priority].append(NewMatcher) return NewMatcher # type: ignore @classmethod def destroy(cls) -> None: """销毁当前的事件响应器""" matchers[cls.priority].remove(cls) @classproperty def plugin(cls) -> "Plugin | None": """事件响应器所在插件""" return cls._source and cls._source.plugin @classproperty def plugin_id(cls) -> str | None: """事件响应器所在插件标识符""" return cls._source and cls._source.plugin_id @classproperty def plugin_name(cls) -> str | None: """事件响应器所在插件名""" return cls._source and cls._source.plugin_name @classproperty def module(cls) -> ModuleType | None: """事件响应器所在插件模块""" return cls._source and cls._source.module @classproperty def module_name(cls) -> str | None: """事件响应器所在插件模块路径""" return cls._source and cls._source.module_name @classmethod async def check_perm( cls, bot: Bot, event: Event, stack: AsyncExitStack | None = None, dependency_cache: T_DependencyCache | None = None, ) -> bool: """检查是否满足触发权限 参数: bot: Bot 对象 event: 上报事件 stack: 异步上下文栈 dependency_cache: 依赖缓存 返回: 是否满足权限 """ event_type = event.get_type() return event_type == (cls.type or event_type) and await cls.permission( bot, event, stack, dependency_cache ) @classmethod async def check_rule( cls, bot: Bot, event: Event, state: T_State, stack: AsyncExitStack | None = None, dependency_cache: T_DependencyCache | None = None, ) -> bool: """检查是否满足匹配规则 参数: bot: Bot 对象 event: 上报事件 state: 当前状态 stack: 异步上下文栈 dependency_cache: 依赖缓存 返回: 是否满足匹配规则 """ event_type = event.get_type() return event_type == (cls.type or event_type) and await cls.rule( bot, event, state, stack, dependency_cache ) @classmethod def type_updater(cls, func: T_TypeUpdater) -> T_TypeUpdater: """装饰一个函数来更改当前事件响应器的默认响应事件类型更新函数 参数: func: 响应事件类型更新函数 """ cls._default_type_updater = Dependent[str].parse( call=func, allow_types=cls.HANDLER_PARAM_TYPES ) return func @classmethod def permission_updater(cls, func: T_PermissionUpdater) -> T_PermissionUpdater: """装饰一个函数来更改当前事件响应器的默认会话权限更新函数 参数: func: 会话权限更新函数 """ cls._default_permission_updater = Dependent[Permission].parse( call=func, allow_types=cls.HANDLER_PARAM_TYPES ) return func @classmethod def append_handler( cls, handler: T_Handler, parameterless: Iterable[Any] | None = None ) -> Dependent[Any]: handler_ = Dependent[Any].parse( call=handler, parameterless=parameterless, allow_types=cls.HANDLER_PARAM_TYPES, ) cls.handlers.append(handler_) return handler_ @classmethod def handle( cls, parameterless: Iterable[Any] | None = None ) -> Callable[[T_Handler], T_Handler]: """装饰一个函数来向事件响应器直接添加一个处理函数 参数: parameterless: 非参数类型依赖列表 """ def _decorator(func: T_Handler) -> T_Handler: cls.append_handler(func, parameterless=parameterless) return func return _decorator @classmethod def receive( cls, id: str = "", parameterless: Iterable[Any] | None = None ) -> Callable[[T_Handler], T_Handler]: """装饰一个函数来指示 NoneBot 在接收用户新的一条消息后继续运行该函数 参数: id: 消息 ID parameterless: 非参数类型依赖列表 """ async def _receive(event: Event, matcher: "Matcher") -> None: matcher.set_target(RECEIVE_KEY.format(id=id)) if matcher.get_target() == RECEIVE_KEY.format(id=id): matcher.set_receive(id, event) return if matcher.get_receive(id, ...) is not ...: return await matcher.reject() _parameterless = (Depends(_receive), *(parameterless or ())) def _decorator(func: T_Handler) -> T_Handler: if cls.handlers and cls.handlers[-1].call is func: func_handler = cls.handlers[-1] new_handler = Dependent( call=func_handler.call, params=func_handler.params, parameterless=Dependent.parse_parameterless( tuple(_parameterless), cls.HANDLER_PARAM_TYPES ) + func_handler.parameterless, ) cls.handlers[-1] = new_handler else: cls.append_handler(func, parameterless=_parameterless) return func return _decorator @classmethod def got( cls, key: str, prompt: str | Message | MessageSegment | MessageTemplate | None = None, parameterless: Iterable[Any] | None = None, ) -> Callable[[T_Handler], T_Handler]: """装饰一个函数来指示 NoneBot 获取一个参数 `key` 当要获取的 `key` 不存在时接收用户新的一条消息再运行该函数, 如果 `key` 已存在则直接继续运行 参数: key: 参数名 prompt: 在参数不存在时向用户发送的消息 parameterless: 非参数类型依赖列表 """ async def _key_getter(event: Event, matcher: "Matcher"): matcher.set_target(ARG_KEY.format(key=key)) if matcher.get_target() == ARG_KEY.format(key=key): matcher.set_arg(key, event.get_message()) return if matcher.get_arg(key, ...) is not ...: return await matcher.reject(prompt) _parameterless = (Depends(_key_getter), *(parameterless or ())) def _decorator(func: T_Handler) -> T_Handler: if cls.handlers and cls.handlers[-1].call is func: func_handler = cls.handlers[-1] new_handler = Dependent( call=func_handler.call, params=func_handler.params, parameterless=Dependent.parse_parameterless( tuple(_parameterless), cls.HANDLER_PARAM_TYPES ) + func_handler.parameterless, ) cls.handlers[-1] = new_handler else: cls.append_handler(func, parameterless=_parameterless) return func return _decorator @classmethod async def send( cls, message: str | Message | MessageSegment | MessageTemplate, **kwargs: Any, ) -> Any: """发送一条消息给当前交互用户 参数: message: 消息内容 kwargs: {ref}`nonebot.adapters.Bot.send` 的参数, 请参考对应 adapter 的 bot 对象 api """ bot = current_bot.get() event = current_event.get() if isinstance(message, MessageTemplate): state = current_matcher.get().state _message = message.format(**state) else: _message = message return await bot.send(event=event, message=_message, **kwargs) @classmethod async def finish( cls, message: str | Message | MessageSegment | MessageTemplate | None = None, **kwargs, ) -> NoReturn: """发送一条消息给当前交互用户并结束当前事件响应器 参数: message: 消息内容 kwargs: {ref}`nonebot.adapters.Bot.send` 的参数, 请参考对应 adapter 的 bot 对象 api """ if message is not None: await cls.send(message, **kwargs) raise FinishedException @classmethod async def pause( cls, prompt: str | Message | MessageSegment | MessageTemplate | None = None, **kwargs, ) -> NoReturn: """发送一条消息给当前交互用户并暂停事件响应器,在接收用户新的一条消息后继续下一个处理函数 参数: prompt: 消息内容 kwargs: {ref}`nonebot.adapters.Bot.send` 的参数, 请参考对应 adapter 的 bot 对象 api """ try: matcher = current_matcher.get() except Exception: matcher = None if prompt is not None: result = await cls.send(prompt, **kwargs) if matcher is not None: matcher.state[PAUSE_PROMPT_RESULT_KEY] = result raise PausedException @classmethod async def reject( cls, prompt: str | Message | MessageSegment | MessageTemplate | None = None, **kwargs, ) -> NoReturn: """最近使用 `got` / `receive` 接收的消息不符合预期, 发送一条消息给当前交互用户并将当前事件处理流程中断在当前位置,在接收用户新的一个事件后从头开始执行当前处理函数 参数: prompt: 消息内容 kwargs: {ref}`nonebot.adapters.Bot.send` 的参数, 请参考对应 adapter 的 bot 对象 api """ try: matcher = current_matcher.get() key = matcher.get_target() except Exception: matcher = None key = None key = REJECT_PROMPT_RESULT_KEY.format(key=key) if key is not None else None if prompt is not None: result = await cls.send(prompt, **kwargs) if key is not None and matcher: matcher.state[key] = result raise RejectedException @classmethod async def reject_arg( cls, key: str, prompt: str | Message | MessageSegment | MessageTemplate | None = None, **kwargs, ) -> NoReturn: """最近使用 `got` 接收的消息不符合预期, 发送一条消息给当前交互用户并将当前事件处理流程中断在当前位置,在接收用户新的一条消息后从头开始执行当前处理函数 参数: key: 参数名 prompt: 消息内容 kwargs: {ref}`nonebot.adapters.Bot.send` 的参数, 请参考对应 adapter 的 bot 对象 api """ matcher = current_matcher.get() arg_key = ARG_KEY.format(key=key) matcher.set_target(arg_key) if prompt is not None: result = await cls.send(prompt, **kwargs) matcher.state[REJECT_PROMPT_RESULT_KEY.format(key=arg_key)] = result raise RejectedException @classmethod async def reject_receive( cls, id: str = "", prompt: str | Message | MessageSegment | MessageTemplate | None = None, **kwargs, ) -> NoReturn: """最近使用 `receive` 接收的消息不符合预期, 发送一条消息给当前交互用户并将当前事件处理流程中断在当前位置,在接收用户新的一个事件后从头开始执行当前处理函数 参数: id: 消息 id prompt: 消息内容 kwargs: {ref}`nonebot.adapters.Bot.send` 的参数, 请参考对应 adapter 的 bot 对象 api """ matcher = current_matcher.get() receive_key = RECEIVE_KEY.format(id=id) matcher.set_target(receive_key) if prompt is not None: result = await cls.send(prompt, **kwargs) matcher.state[REJECT_PROMPT_RESULT_KEY.format(key=receive_key)] = result raise RejectedException @classmethod def skip(cls) -> NoReturn: """跳过当前事件处理函数,继续下一个处理函数 通常在事件处理函数的依赖中使用。 """ raise SkippedException @overload def get_receive(self, id: str) -> Event | None: ... @overload def get_receive(self, id: str, default: T) -> Event | T: ... def get_receive(self, id: str, default: T | None = None) -> Event | T | None: """获取一个 `receive` 事件 如果没有找到对应的事件,返回 `default` 值 """ return self.state.get(RECEIVE_KEY.format(id=id), default) def set_receive(self, id: str, event: Event) -> None: """设置一个 `receive` 事件""" self.state[RECEIVE_KEY.format(id=id)] = event self.state[LAST_RECEIVE_KEY] = event @overload def get_last_receive(self) -> Event | None: ... @overload def get_last_receive(self, default: T) -> Event | T: ... def get_last_receive(self, default: T | None = None) -> Event | T | None: """获取最近一次 `receive` 事件 如果没有事件,返回 `default` 值 """ return self.state.get(LAST_RECEIVE_KEY, default) @overload def get_arg(self, key: str) -> Message | None: ... @overload def get_arg(self, key: str, default: T) -> Message | T: ... def get_arg(self, key: str, default: T | None = None) -> Message | T | None: """获取一个 `got` 消息 如果没有找到对应的消息,返回 `default` 值 """ return self.state.get(ARG_KEY.format(key=key), default) def set_arg(self, key: str, message: Message) -> None: """设置一个 `got` 消息""" self.state[ARG_KEY.format(key=key)] = message def set_target(self, target: str, cache: bool = True) -> None: if cache: self.state[REJECT_CACHE_TARGET] = target else: self.state[REJECT_TARGET] = target @overload def get_target(self) -> str | None: ... @overload def get_target(self, default: T) -> str | T: ... def get_target(self, default: T | None = None) -> str | T | None: return self.state.get(REJECT_TARGET, default) def stop_propagation(self): """阻止事件传播""" self.block = True async def update_type( self, bot: Bot, event: Event, stack: AsyncExitStack | None = None, dependency_cache: T_DependencyCache | None = None, ) -> str: updater = self.__class__._default_type_updater return ( await updater( bot=bot, event=event, state=self.state, matcher=self, stack=stack, dependency_cache=dependency_cache, ) if updater else "message" ) async def update_permission( self, bot: Bot, event: Event, stack: AsyncExitStack | None = None, dependency_cache: T_DependencyCache | None = None, ) -> Permission: if updater := self.__class__._default_permission_updater: return await updater( bot=bot, event=event, state=self.state, matcher=self, stack=stack, dependency_cache=dependency_cache, ) return Permission(User.from_event(event, perm=self.permission)) async def resolve_reject(self): handler = current_handler.get() self.remain_handlers.insert(0, handler) if REJECT_CACHE_TARGET in self.state: self.state[REJECT_TARGET] = self.state[REJECT_CACHE_TARGET] @contextmanager def ensure_context(self, bot: Bot, event: Event): b_t = current_bot.set(bot) e_t = current_event.set(event) m_t = current_matcher.set(self) try: yield finally: current_bot.reset(b_t) current_event.reset(e_t) current_matcher.reset(m_t) async def simple_run( self, bot: Bot, event: Event, state: T_State, stack: AsyncExitStack | None = None, dependency_cache: T_DependencyCache | None = None, ): logger.trace( f"{self} run with incoming args: " f"bot={bot}, event={event!r}, state={state!r}" ) def _handle_stop_propagation(exc_group: BaseExceptionGroup[StopPropagation]): self.block = True with self.ensure_context(bot, event): try: with catch({StopPropagation: _handle_stop_propagation}): # Refresh preprocess state self.state.update(state) while self.remain_handlers: handler = self.remain_handlers.pop(0) current_handler.set(handler) logger.debug(f"Running handler {handler}") def _handle_skipped( exc_group: BaseExceptionGroup[SkippedException], ): logger.debug(f"Handler {handler} skipped") with catch({SkippedException: _handle_skipped}): await handler( matcher=self, bot=bot, event=event, state=self.state, stack=stack, dependency_cache=dependency_cache, ) finally: logger.info(f"{self} running complete") # 运行handlers async def run( self, bot: Bot, event: Event, state: T_State, stack: AsyncExitStack | None = None, dependency_cache: T_DependencyCache | None = None, ): exc: FinishedException | RejectedException | PausedException | None = None def _handle_special_exception( exc_group: BaseExceptionGroup[ FinishedException | RejectedException | PausedException ], ): nonlocal exc excs = list(flatten_exception_group(exc_group)) if len(excs) > 1: logger.warning( "Multiple session control exceptions occurred. " "NoneBot will choose the proper one." ) finished_exc = next( (e for e in excs if isinstance(e, FinishedException)), None, ) rejected_exc = next( (e for e in excs if isinstance(e, RejectedException)), None, ) paused_exc = next( (e for e in excs if isinstance(e, PausedException)), None, ) exc = finished_exc or rejected_exc or paused_exc elif isinstance( excs[0], (FinishedException, RejectedException, PausedException) ): exc = excs[0] with catch( { ( FinishedException, RejectedException, PausedException, ): _handle_special_exception } ): await self.simple_run(bot, event, state, stack, dependency_cache) if isinstance(exc, FinishedException): pass elif isinstance(exc, RejectedException): await self.resolve_reject() type_ = await self.update_type(bot, event, stack, dependency_cache) permission = await self.update_permission( bot, event, stack, dependency_cache ) self.new( type_, Rule(), permission, self.remain_handlers, temp=True, priority=0, block=True, source=self.__class__._source, expire_time=bot.config.session_expire_timeout, default_state=self.state, default_type_updater=self.__class__._default_type_updater, default_permission_updater=self.__class__._default_permission_updater, ) elif isinstance(exc, PausedException): type_ = await self.update_type(bot, event, stack, dependency_cache) permission = await self.update_permission( bot, event, stack, dependency_cache ) self.new( type_, Rule(), permission, self.remain_handlers, temp=True, priority=0, block=True, source=self.__class__._source, expire_time=bot.config.session_expire_timeout, default_state=self.state, default_type_updater=self.__class__._default_type_updater, default_permission_updater=self.__class__._default_permission_updater, ) ================================================ FILE: nonebot/internal/matcher/provider.py ================================================ import abc from collections import defaultdict from collections.abc import Mapping, MutableMapping from typing import TYPE_CHECKING if TYPE_CHECKING: from .matcher import Matcher class MatcherProvider(abc.ABC, MutableMapping[int, list[type["Matcher"]]]): """事件响应器存储器基类 参数: matchers: 当前存储器中已有的事件响应器 """ @abc.abstractmethod def __init__(self, matchers: Mapping[int, list[type["Matcher"]]]): raise NotImplementedError class _DictProvider(defaultdict[int, list[type["Matcher"]]], MatcherProvider): # type: ignore def __init__(self, matchers: Mapping[int, list[type["Matcher"]]]): super().__init__(list, matchers) DEFAULT_PROVIDER_CLASS = _DictProvider """默认存储器类型""" ================================================ FILE: nonebot/internal/params.py ================================================ from collections.abc import Callable from contextlib import AsyncExitStack, asynccontextmanager, contextmanager from enum import Enum import inspect from typing import ( TYPE_CHECKING, Annotated, Any, Literal, cast, get_args, get_origin, ) from typing_extensions import Self, override import anyio from exceptiongroup import BaseExceptionGroup, catch from pydantic.fields import FieldInfo as PydanticFieldInfo from nonebot.compat import FieldInfo, ModelField, PydanticUndefined from nonebot.consts import ARG_KEY, REJECT_PROMPT_RESULT_KEY from nonebot.dependencies import Dependent, Param from nonebot.dependencies.utils import check_field_type from nonebot.exception import SkippedException from nonebot.typing import ( _STATE_FLAG, T_DependencyCache, T_Handler, T_State, origin_is_annotated, ) from nonebot.utils import ( generic_check_issubclass, get_name, is_async_gen_callable, is_coroutine_callable, is_gen_callable, run_sync, run_sync_ctx_manager, ) if TYPE_CHECKING: from nonebot.adapters import Bot, Event, Message from nonebot.matcher import Matcher class DependsInner: def __init__( self, dependency: T_Handler | None = None, *, use_cache: bool = True, validate: bool | PydanticFieldInfo = False, ) -> None: self.dependency = dependency self.use_cache = use_cache self.validate = validate def __repr__(self) -> str: dep = get_name(self.dependency) cache = "" if self.use_cache else ", use_cache=False" validate = f", validate={self.validate}" if self.validate else "" return f"DependsInner({dep}{cache}{validate})" def Depends( dependency: T_Handler | None = None, *, use_cache: bool = True, validate: bool | PydanticFieldInfo = False, ) -> Any: """子依赖装饰器 参数: dependency: 依赖函数。默认为参数的类型注释。 use_cache: 是否使用缓存。默认为 `True`。 validate: 是否使用 Pydantic 类型校验。默认为 `False`。 用法: ```python def depend_func() -> Any: return ... def depend_gen_func(): try: yield ... finally: ... async def handler( param_name: Any = Depends(depend_func), gen: Any = Depends(depend_gen_func), ): ... ``` """ return DependsInner(dependency, use_cache=use_cache, validate=validate) class CacheState(str, Enum): """子依赖缓存状态""" PENDING = "PENDING" FINISHED = "FINISHED" class DependencyCache: """子依赖结果缓存。 用于缓存子依赖的结果,以避免重复计算。 """ def __init__(self): self._state = CacheState.PENDING self._result: Any = None self._exception: BaseException | None = None self._waiter = anyio.Event() def done(self) -> bool: return self._state == CacheState.FINISHED def result(self) -> Any: """获取子依赖结果""" if self._state != CacheState.FINISHED: raise RuntimeError("Result is not ready") if self._exception is not None: raise self._exception return self._result def exception(self) -> BaseException | None: """获取子依赖异常""" if self._state != CacheState.FINISHED: raise RuntimeError("Result is not ready") return self._exception def set_result(self, result: Any) -> None: """设置子依赖结果""" if self._state != CacheState.PENDING: raise RuntimeError(f"Cache state invalid: {self._state}") self._result = result self._state = CacheState.FINISHED self._waiter.set() def set_exception(self, exception: BaseException) -> None: """设置子依赖异常""" if self._state != CacheState.PENDING: raise RuntimeError(f"Cache state invalid: {self._state}") self._exception = exception self._state = CacheState.FINISHED self._waiter.set() async def wait(self): """等待子依赖结果""" await self._waiter.wait() if self._state != CacheState.FINISHED: raise RuntimeError("Invalid cache state") if self._exception is not None: raise self._exception return self._result class DependParam(Param): """子依赖注入参数。 本注入解析所有子依赖注入,然后将它们的返回值作为参数值传递给父依赖。 本注入应该具有最高优先级,因此应该在其他参数之前检查。 """ def __init__( self, *args, dependent: Dependent[Any], use_cache: bool, **kwargs: Any ) -> None: super().__init__(*args, **kwargs) self.dependent = dependent self.use_cache = use_cache def __repr__(self) -> str: return f"Depends({self.dependent}, use_cache={self.use_cache})" @classmethod def _from_field( cls, sub_dependent: Dependent[Any], use_cache: bool, validate: bool | PydanticFieldInfo, ) -> Self: return cls._inherit_construct( validate if isinstance(validate, PydanticFieldInfo) else None, dependent=sub_dependent, use_cache=use_cache, validate=bool(validate), ) @classmethod @override def _check_param( cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...] ) -> Self | None: type_annotation, depends_inner = param.annotation, None # extract type annotation and dependency from Annotated if get_origin(param.annotation) is Annotated: type_annotation, *extra_args = get_args(param.annotation) depends_inner = next( (x for x in reversed(extra_args) if isinstance(x, DependsInner)), None ) # param default value takes higher priority depends_inner = ( param.default if isinstance(param.default, DependsInner) else depends_inner ) # not a dependent if depends_inner is None: return dependency: T_Handler # sub dependency is not specified, use type annotation if depends_inner.dependency is None: assert type_annotation is not inspect.Signature.empty, ( "Dependency cannot be empty" ) dependency = type_annotation else: dependency = depends_inner.dependency # parse sub dependency sub_dependent = Dependent[Any].parse( call=dependency, allow_types=allow_types, ) return cls._from_field( sub_dependent, depends_inner.use_cache, depends_inner.validate ) @classmethod @override def _check_parameterless( cls, value: Any, allow_types: tuple[type[Param], ...] ) -> "Param | None": if isinstance(value, DependsInner): assert value.dependency, "Dependency cannot be empty" dependent = Dependent[Any].parse( call=value.dependency, allow_types=allow_types ) return cls._from_field(dependent, value.use_cache, value.validate) @override async def _solve( self, stack: AsyncExitStack | None = None, dependency_cache: T_DependencyCache | None = None, **kwargs: Any, ) -> Any: use_cache: bool = self.use_cache dependency_cache = {} if dependency_cache is None else dependency_cache sub_dependent = self.dependent call = cast(Callable[..., Any], sub_dependent.call) # solve sub dependency with current cache exc: BaseExceptionGroup[SkippedException] | None = None def _handle_skipped(exc_group: BaseExceptionGroup[SkippedException]): nonlocal exc exc = exc_group with catch({SkippedException: _handle_skipped}): sub_values = await sub_dependent.solve( stack=stack, dependency_cache=dependency_cache, **kwargs, ) if exc is not None: raise exc # run dependency function if use_cache and call in dependency_cache: return await dependency_cache[call].wait() if is_gen_callable(call) or is_async_gen_callable(call): assert isinstance(stack, AsyncExitStack), ( "Generator dependency should be called in context" ) if is_gen_callable(call): cm = run_sync_ctx_manager(contextmanager(call)(**sub_values)) else: cm = asynccontextmanager(call)(**sub_values) target = stack.enter_async_context(cm) elif is_coroutine_callable(call): target = call(**sub_values) else: target = run_sync(call)(**sub_values) dependency_cache[call] = cache = DependencyCache() try: result = await target except Exception as e: cache.set_exception(e) raise except BaseException as e: cache.set_exception(e) # remove cache when base exception occurs # e.g. CancelledError dependency_cache.pop(call, None) raise else: cache.set_result(result) return result @override async def _check(self, **kwargs: Any) -> None: # run sub dependent pre-checkers await self.dependent.check(**kwargs) class BotParam(Param): """{ref}`nonebot.adapters.Bot` 注入参数。 本注入解析所有类型为且仅为 {ref}`nonebot.adapters.Bot` 及其子类或 `None` 的参数。 为保证兼容性,本注入还会解析名为 `bot` 且没有类型注解的参数。 """ def __init__(self, *args, checker: ModelField | None = None, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.checker = checker def __repr__(self) -> str: return ( "BotParam(" + (repr(self.checker.annotation) if self.checker is not None else "") + ")" ) @classmethod @override def _check_param( cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...] ) -> Self | None: from nonebot.adapters import Bot # param type is Bot(s) or subclass(es) of Bot or None if generic_check_issubclass(param.annotation, Bot): checker: ModelField | None = None if param.annotation is not Bot: checker = ModelField.construct( name=param.name, annotation=param.annotation, field_info=FieldInfo() ) return cls(checker=checker) # legacy: param is named "bot" and has no type annotation elif param.annotation == param.empty and param.name == "bot": return cls() @override async def _solve( # pyright: ignore[reportIncompatibleMethodOverride] self, bot: "Bot", **kwargs: Any ) -> Any: return bot @override async def _check( # pyright: ignore[reportIncompatibleMethodOverride] self, bot: "Bot", **kwargs: Any ) -> None: if self.checker is not None: check_field_type(self.checker, bot) class EventParam(Param): """{ref}`nonebot.adapters.Event` 注入参数 本注入解析所有类型为且仅为 {ref}`nonebot.adapters.Event` 及其子类或 `None` 的参数。 为保证兼容性,本注入还会解析名为 `event` 且没有类型注解的参数。 """ def __init__(self, *args, checker: ModelField | None = None, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.checker = checker def __repr__(self) -> str: return ( "EventParam(" + (repr(self.checker.annotation) if self.checker is not None else "") + ")" ) @classmethod @override def _check_param( cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...] ) -> Self | None: from nonebot.adapters import Event # param type is Event(s) or subclass(es) of Event or None if generic_check_issubclass(param.annotation, Event): checker: ModelField | None = None if param.annotation is not Event: checker = ModelField.construct( name=param.name, annotation=param.annotation, field_info=FieldInfo() ) return cls(checker=checker) # legacy: param is named "event" and has no type annotation elif param.annotation == param.empty and param.name == "event": return cls() @override async def _solve( # pyright: ignore[reportIncompatibleMethodOverride] self, event: "Event", **kwargs: Any ) -> Any: return event @override async def _check( # pyright: ignore[reportIncompatibleMethodOverride] self, event: "Event", **kwargs: Any ) -> Any: if self.checker is not None: check_field_type(self.checker, event) class StateParam(Param): """事件处理状态注入参数 本注入解析所有类型为 `T_State` 的参数。 为保证兼容性,本注入还会解析名为 `state` 且没有类型注解的参数。 """ def __repr__(self) -> str: return "StateParam()" @classmethod @override def _check_param( cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...] ) -> Self | None: # param type is T_State if origin_is_annotated( get_origin(param.annotation) ) and _STATE_FLAG in get_args(param.annotation): return cls() # legacy: param is named "state" and has no type annotation elif param.annotation == param.empty and param.name == "state": return cls() @override async def _solve( # pyright: ignore[reportIncompatibleMethodOverride] self, state: T_State, **kwargs: Any ) -> Any: return state class MatcherParam(Param): """事件响应器实例注入参数 本注入解析所有类型为且仅为 {ref}`nonebot.matcher.Matcher` 及其子类或 `None` 的参数。 为保证兼容性,本注入还会解析名为 `matcher` 且没有类型注解的参数。 """ def __init__(self, *args, checker: ModelField | None = None, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.checker = checker def __repr__(self) -> str: return ( "MatcherParam(" + (repr(self.checker.annotation) if self.checker is not None else "") + ")" ) @classmethod @override def _check_param( cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...] ) -> Self | None: from nonebot.matcher import Matcher # param type is Matcher(s) or subclass(es) of Matcher or None if generic_check_issubclass(param.annotation, Matcher): checker: ModelField | None = None if param.annotation is not Matcher: checker = ModelField.construct( name=param.name, annotation=param.annotation, field_info=FieldInfo() ) return cls(checker=checker) # legacy: param is named "matcher" and has no type annotation elif param.annotation == param.empty and param.name == "matcher": return cls() @override async def _solve( # pyright: ignore[reportIncompatibleMethodOverride] self, matcher: "Matcher", **kwargs: Any ) -> Any: return matcher @override async def _check( # pyright: ignore[reportIncompatibleMethodOverride] self, matcher: "Matcher", **kwargs: Any ) -> Any: if self.checker is not None: check_field_type(self.checker, matcher) class ArgInner: def __init__( self, key: str | None, type: Literal["message", "str", "plaintext", "prompt"] ) -> None: self.key: str | None = key self.type: Literal["message", "str", "plaintext", "prompt"] = type def __repr__(self) -> str: return f"ArgInner(key={self.key!r}, type={self.type!r})" def Arg(key: str | None = None) -> Any: """Arg 参数消息""" return ArgInner(key, "message") def ArgStr(key: str | None = None) -> str: """Arg 参数消息文本""" return ArgInner(key, "str") # type: ignore def ArgPlainText(key: str | None = None) -> str: """Arg 参数消息纯文本""" return ArgInner(key, "plaintext") # type: ignore def ArgPromptResult(key: str | None = None) -> Any: """`arg` prompt 发送结果""" return ArgInner(key, "prompt") class ArgParam(Param): """Arg 注入参数 本注入解析事件响应器操作 `got` 所获取的参数。 可以通过 `Arg`、`ArgStr`、`ArgPlainText` 等函数参数 `key` 指定获取的参数, 留空则会根据参数名称获取。 """ def __init__( self, *args, key: str, type: Literal["message", "str", "plaintext", "prompt"], **kwargs: Any, ) -> None: super().__init__(*args, **kwargs) self.key = key self.type = type def __repr__(self) -> str: return f"ArgParam(key={self.key!r}, type={self.type!r})" @classmethod @override def _check_param( cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...] ) -> Self | None: if isinstance(param.default, ArgInner): return cls(key=param.default.key or param.name, type=param.default.type) elif get_origin(param.annotation) is Annotated: for arg in get_args(param.annotation)[:0:-1]: if isinstance(arg, ArgInner): return cls(key=arg.key or param.name, type=arg.type) async def _solve( # pyright: ignore[reportIncompatibleMethodOverride] self, matcher: "Matcher", **kwargs: Any ) -> Any: if self.type == "message": return self._solve_message(matcher) elif self.type == "str": return self._solve_str(matcher) elif self.type == "plaintext": return self._solve_plaintext(matcher) elif self.type == "prompt": return self._solve_prompt(matcher) else: raise ValueError(f"Unknown Arg type: {self.type}") def _solve_message(self, matcher: "Matcher") -> "Message | None": return matcher.get_arg(self.key) def _solve_str(self, matcher: "Matcher") -> str | None: message = matcher.get_arg(self.key) return str(message) if message is not None else None def _solve_plaintext(self, matcher: "Matcher") -> str | None: message = matcher.get_arg(self.key) return message.extract_plain_text() if message is not None else None def _solve_prompt(self, matcher: "Matcher") -> Any | None: return matcher.state.get( REJECT_PROMPT_RESULT_KEY.format(key=ARG_KEY.format(key=self.key)) ) class ExceptionParam(Param): """{ref}`nonebot.message.run_postprocessor` 的异常注入参数 本注入解析所有类型为 `Exception` 或 `None` 的参数。 为保证兼容性,本注入还会解析名为 `exception` 且没有类型注解的参数。 """ def __repr__(self) -> str: return "ExceptionParam()" @classmethod @override def _check_param( cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...] ) -> Self | None: # param type is Exception(s) or subclass(es) of Exception or None if generic_check_issubclass(param.annotation, Exception): return cls() # legacy: param is named "exception" and has no type annotation elif param.annotation == param.empty and param.name == "exception": return cls() @override async def _solve(self, exception: Exception | None = None, **kwargs: Any) -> Any: return exception class DefaultParam(Param): """默认值注入参数 本注入解析所有剩余未能解析且具有默认值的参数。 本注入参数应该具有最低优先级,因此应该在所有其他注入参数之后使用。 """ def __repr__(self) -> str: return f"DefaultParam(default={self.default!r})" @classmethod @override def _check_param( cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...] ) -> Self | None: if param.default != param.empty: return cls(default=param.default) @override async def _solve(self, **kwargs: Any) -> Any: return PydanticUndefined __autodoc__ = { "DependsInner": False, "StateInner": False, "ArgInner": False, } ================================================ FILE: nonebot/internal/permission.py ================================================ from contextlib import AsyncExitStack from typing import ClassVar, NoReturn from typing_extensions import Self import anyio from nonebot.dependencies import Dependent from nonebot.exception import SkippedException from nonebot.typing import T_DependencyCache, T_PermissionChecker from nonebot.utils import run_coro_with_catch from .adapter import Bot, Event from .params import BotParam, DefaultParam, DependParam, EventParam, Param class Permission: """{ref}`nonebot.matcher.Matcher` 权限类。 当事件传递时,在 {ref}`nonebot.matcher.Matcher` 运行前进行检查。 参数: checkers: PermissionChecker 用法: ```python Permission(async_function) | sync_function # 等价于 Permission(async_function, sync_function) ``` """ __slots__ = ("checkers",) HANDLER_PARAM_TYPES: ClassVar[list[type[Param]]] = [ DependParam, BotParam, EventParam, DefaultParam, ] def __init__(self, *checkers: T_PermissionChecker | Dependent[bool]) -> None: self.checkers: set[Dependent[bool]] = { ( checker if isinstance(checker, Dependent) else Dependent[bool].parse( call=checker, allow_types=self.HANDLER_PARAM_TYPES ) ) for checker in checkers } """存储 `PermissionChecker`""" def __repr__(self) -> str: return f"Permission({', '.join(repr(checker) for checker in self.checkers)})" async def __call__( self, bot: Bot, event: Event, stack: AsyncExitStack | None = None, dependency_cache: T_DependencyCache | None = None, ) -> bool: """检查是否满足某个权限。 参数: bot: Bot 对象 event: Event 对象 stack: 异步上下文栈 dependency_cache: 依赖缓存 """ if not self.checkers: return True result = False async def _run_checker(checker: Dependent[bool]) -> None: nonlocal result # calculate the result first to avoid data racing is_passed = await run_coro_with_catch( checker( bot=bot, event=event, stack=stack, dependency_cache=dependency_cache ), (SkippedException,), False, ) result |= is_passed async with anyio.create_task_group() as tg: for checker in self.checkers: tg.start_soon(_run_checker, checker) return result def __and__(self, other: object) -> NoReturn: raise RuntimeError("And operation between Permissions is not allowed.") def __or__(self, other: "Permission | T_PermissionChecker | None") -> "Permission": if other is None: return self elif isinstance(other, Permission): return Permission(*self.checkers, *other.checkers) else: return Permission(*self.checkers, other) def __ror__(self, other: "Permission | T_PermissionChecker | None") -> "Permission": if other is None: return self elif isinstance(other, Permission): return Permission(*other.checkers, *self.checkers) else: return Permission(other, *self.checkers) class User: """检查当前事件是否属于指定会话。 参数: users: 会话 ID 元组 perm: 需同时满足的权限 """ __slots__ = ("perm", "users") def __init__(self, users: tuple[str, ...], perm: Permission | None = None) -> None: self.users = users self.perm = perm def __repr__(self) -> str: return ( f"User(users={self.users}" + (f", permission={self.perm})" if self.perm else "") + ")" ) async def __call__(self, bot: Bot, event: Event) -> bool: try: session = event.get_session_id() except Exception: return False return bool( session in self.users and (self.perm is None or await self.perm(bot, event)) ) @classmethod def _clean_permission(cls, perm: Permission) -> Permission | None: if len(perm.checkers) == 1 and isinstance( user_perm := next(iter(perm.checkers)).call, cls ): return user_perm.perm return perm @classmethod def from_event(cls, event: Event, perm: Permission | None = None) -> Self: """从事件中获取会话 ID。 如果 `perm` 中仅有 `User` 类型的权限检查函数,则会去除原有的会话 ID 限制。 参数: event: Event 对象 perm: 需同时满足的权限 """ return cls((event.get_session_id(),), perm=perm and cls._clean_permission(perm)) @classmethod def from_permission(cls, *users: str, perm: Permission | None = None) -> Self: """指定会话与权限。 如果 `perm` 中仅有 `User` 类型的权限检查函数,则会去除原有的会话 ID 限制。 参数: users: 会话白名单 perm: 需同时满足的权限 """ return cls(users, perm=perm and cls._clean_permission(perm)) def USER(*users: str, perm: Permission | None = None): """匹配当前事件属于指定会话。 如果 `perm` 中仅有 `User` 类型的权限检查函数,则会去除原有检查函数的会话 ID 限制。 参数: user: 会话白名单 perm: 需要同时满足的权限 """ return Permission(User.from_permission(*users, perm=perm)) ================================================ FILE: nonebot/internal/rule.py ================================================ from contextlib import AsyncExitStack from typing import ClassVar, NoReturn import anyio from exceptiongroup import BaseExceptionGroup, catch from nonebot.dependencies import Dependent from nonebot.exception import SkippedException from nonebot.typing import T_DependencyCache, T_RuleChecker, T_State from .adapter import Bot, Event from .params import BotParam, DefaultParam, DependParam, EventParam, Param, StateParam class Rule: """{ref}`nonebot.matcher.Matcher` 规则类。 当事件传递时,在 {ref}`nonebot.matcher.Matcher` 运行前进行检查。 参数: *checkers: RuleChecker 用法: ```python Rule(async_function) & sync_function # 等价于 Rule(async_function, sync_function) ``` """ __slots__ = ("checkers",) HANDLER_PARAM_TYPES: ClassVar[list[type[Param]]] = [ DependParam, BotParam, EventParam, StateParam, DefaultParam, ] def __init__(self, *checkers: T_RuleChecker | Dependent[bool]) -> None: self.checkers: set[Dependent[bool]] = { ( checker if isinstance(checker, Dependent) else Dependent[bool].parse( call=checker, allow_types=self.HANDLER_PARAM_TYPES ) ) for checker in checkers } """存储 `RuleChecker`""" def __repr__(self) -> str: return f"Rule({', '.join(repr(checker) for checker in self.checkers)})" async def __call__( self, bot: Bot, event: Event, state: T_State, stack: AsyncExitStack | None = None, dependency_cache: T_DependencyCache | None = None, ) -> bool: """检查是否符合所有规则 参数: bot: Bot 对象 event: Event 对象 state: 当前 State stack: 异步上下文栈 dependency_cache: 依赖缓存 """ if not self.checkers: return True result = True def _handle_skipped_exception( exc_group: BaseExceptionGroup[SkippedException], ) -> None: nonlocal result result = False async def _run_checker(checker: Dependent[bool]) -> None: nonlocal result # calculate the result first to avoid data racing is_passed = await checker( bot=bot, event=event, state=state, stack=stack, dependency_cache=dependency_cache, ) result &= is_passed with catch({SkippedException: _handle_skipped_exception}): async with anyio.create_task_group() as tg: for checker in self.checkers: tg.start_soon(_run_checker, checker) return result def __and__(self, other: "Rule | T_RuleChecker | None") -> "Rule": if other is None: return self elif isinstance(other, Rule): return Rule(*self.checkers, *other.checkers) else: return Rule(*self.checkers, other) def __rand__(self, other: "Rule | T_RuleChecker | None") -> "Rule": if other is None: return self elif isinstance(other, Rule): return Rule(*other.checkers, *self.checkers) else: return Rule(other, *self.checkers) def __or__(self, other: object) -> NoReturn: raise RuntimeError("Or operation between rules is not allowed.") ================================================ FILE: nonebot/log.py ================================================ """本模块定义了 NoneBot 的日志记录 Logger。 NoneBot 使用 [`loguru`][loguru] 来记录日志信息。 自定义 logger 请参考 [自定义日志](https://nonebot.dev/docs/appendices/log) 以及 [`loguru`][loguru] 文档。 [loguru]: https://github.com/Delgan/loguru FrontMatter: mdx: format: md sidebar_position: 7 description: nonebot.log 模块 """ import inspect import logging import sys from typing import TYPE_CHECKING import loguru if TYPE_CHECKING: # avoid sphinx autodoc resolve annotation failed # because loguru module do not have `Logger` class actually from loguru import Logger, Record # logger = logging.getLogger("nonebot") logger: "Logger" = loguru.logger """NoneBot 日志记录器对象。 默认信息: - 格式: `[%(asctime)s %(name)s] %(levelname)s: %(message)s` - 等级: `INFO` ,根据 `config.log_level` 配置改变 - 输出: 输出至 stdout 用法: ```python from nonebot.log import logger ``` """ # default_handler = logging.StreamHandler(sys.stdout) # default_handler.setFormatter( # logging.Formatter("[%(asctime)s %(name)s] %(levelname)s: %(message)s")) # logger.addHandler(default_handler) # https://loguru.readthedocs.io/en/stable/overview.html#entirely-compatible-with-standard-logging class LoguruHandler(logging.Handler): # pragma: no cover """logging 与 loguru 之间的桥梁,将 logging 的日志转发到 loguru。""" def emit(self, record: logging.LogRecord): try: level = logger.level(record.levelname).name except ValueError: level = record.levelno frame, depth = inspect.currentframe(), 0 while frame and (depth == 0 or frame.f_code.co_filename == logging.__file__): frame = frame.f_back depth += 1 logger.opt(depth=depth, exception=record.exc_info).log( level, record.getMessage() ) def default_filter(record: "Record"): """默认的日志过滤器,根据 `config.log_level` 配置改变日志等级。""" log_level = record["extra"].get("nonebot_log_level", "INFO") levelno = logger.level(log_level).no if isinstance(log_level, str) else log_level return record["level"].no >= levelno default_format: str = ( "{time:MM-DD HH:mm:ss} " "[{level}] " "{name} | " # "{function}:{line}| " "{message}" ) """默认日志格式""" logger.remove() logger_id = logger.add( sys.stdout, level=0, diagnose=False, filter=default_filter, format=default_format, ) """默认日志处理器 id""" __autodoc__ = {"logger_id": False} ================================================ FILE: nonebot/matcher.py ================================================ """本模块实现事件响应器的创建与运行,并提供一些快捷方法来帮助用户更好的与机器人进行对话。 FrontMatter: mdx: format: md sidebar_position: 3 description: nonebot.matcher 模块 """ from nonebot.internal.matcher import DEFAULT_PROVIDER_CLASS as DEFAULT_PROVIDER_CLASS from nonebot.internal.matcher import Matcher as Matcher from nonebot.internal.matcher import MatcherManager as MatcherManager from nonebot.internal.matcher import MatcherProvider as MatcherProvider from nonebot.internal.matcher import MatcherSource as MatcherSource from nonebot.internal.matcher import current_bot as current_bot from nonebot.internal.matcher import current_event as current_event from nonebot.internal.matcher import current_handler as current_handler from nonebot.internal.matcher import current_matcher as current_matcher from nonebot.internal.matcher import matchers as matchers __autodoc__ = { "Matcher": True, "matchers": True, "MatcherManager": True, "MatcherProvider": True, "DEFAULT_PROVIDER_CLASS": True, } ================================================ FILE: nonebot/message.py ================================================ """本模块定义了事件处理主要流程。 NoneBot 内部处理并按优先级分发事件给所有事件响应器,提供了多个插槽以进行事件的预处理等。 FrontMatter: mdx: format: md sidebar_position: 2 description: nonebot.message 模块 """ from collections.abc import Callable import contextlib from contextlib import AsyncExitStack from datetime import datetime from typing import TYPE_CHECKING, Any import anyio from exceptiongroup import BaseExceptionGroup, catch from nonebot.dependencies import Dependent from nonebot.exception import ( IgnoredException, NoLogException, SkippedException, StopPropagation, ) from nonebot.internal.params import ( ArgParam, BotParam, DefaultParam, DependParam, EventParam, ExceptionParam, MatcherParam, StateParam, ) from nonebot.log import logger from nonebot.matcher import Matcher, matchers from nonebot.rule import TrieRule from nonebot.typing import ( T_DependencyCache, T_EventPostProcessor, T_EventPreProcessor, T_RunPostProcessor, T_RunPreProcessor, T_State, ) from nonebot.utils import ( escape_tag, flatten_exception_group, run_coro_with_catch, run_coro_with_shield, ) if TYPE_CHECKING: from nonebot.adapters import Bot, Event _event_preprocessors: set[Dependent[Any]] = set() _event_postprocessors: set[Dependent[Any]] = set() _run_preprocessors: set[Dependent[Any]] = set() _run_postprocessors: set[Dependent[Any]] = set() EVENT_PCS_PARAMS = ( DependParam, BotParam, EventParam, StateParam, DefaultParam, ) RUN_PREPCS_PARAMS = ( DependParam, BotParam, EventParam, StateParam, ArgParam, MatcherParam, DefaultParam, ) RUN_POSTPCS_PARAMS = ( DependParam, ExceptionParam, BotParam, EventParam, StateParam, ArgParam, MatcherParam, DefaultParam, ) def event_preprocessor(func: T_EventPreProcessor) -> T_EventPreProcessor: """事件预处理。 装饰一个函数,使它在每次接收到事件并分发给各响应器之前执行。 """ _event_preprocessors.add( Dependent[Any].parse(call=func, allow_types=EVENT_PCS_PARAMS) ) return func def event_postprocessor(func: T_EventPostProcessor) -> T_EventPostProcessor: """事件后处理。 装饰一个函数,使它在每次接收到事件并分发给各响应器之后执行。 """ _event_postprocessors.add( Dependent[Any].parse(call=func, allow_types=EVENT_PCS_PARAMS) ) return func def run_preprocessor(func: T_RunPreProcessor) -> T_RunPreProcessor: """运行预处理。 装饰一个函数,使它在每次事件响应器运行前执行。 """ _run_preprocessors.add( Dependent[Any].parse(call=func, allow_types=RUN_PREPCS_PARAMS) ) return func def run_postprocessor(func: T_RunPostProcessor) -> T_RunPostProcessor: """运行后处理。 装饰一个函数,使它在每次事件响应器运行后执行。 """ _run_postprocessors.add( Dependent[Any].parse(call=func, allow_types=RUN_POSTPCS_PARAMS) ) return func def _handle_ignored_exception( msg: str, ) -> Callable[[BaseExceptionGroup[IgnoredException]], None]: def _handle(exc_group: BaseExceptionGroup[IgnoredException]) -> None: logger.opt(colors=True).info(msg) return _handle def _handle_exception(msg: str) -> Callable[[BaseExceptionGroup[Exception]], None]: def _handle(exc_group: BaseExceptionGroup[Exception]) -> None: for exc in flatten_exception_group(exc_group): logger.opt(colors=True, exception=exc).error(msg) return _handle async def _apply_event_preprocessors( bot: "Bot", event: "Event", state: T_State, stack: AsyncExitStack | None = None, dependency_cache: T_DependencyCache | None = None, show_log: bool = True, ) -> bool: """运行事件预处理。 参数: bot: Bot 对象 event: Event 对象 state: 会话状态 stack: 异步上下文栈 dependency_cache: 依赖缓存 show_log: 是否显示日志 返回: 是否继续处理事件 """ if not _event_preprocessors: return True if show_log: logger.debug("Running PreProcessors...") with catch( { IgnoredException: _handle_ignored_exception( f"Event {escape_tag(event.get_event_name())} is ignored" ), Exception: _handle_exception( "Error when running EventPreProcessors. " "Event ignored!" ), } ): async with anyio.create_task_group() as tg: for proc in _event_preprocessors: tg.start_soon( run_coro_with_catch, proc( bot=bot, event=event, state=state, stack=stack, dependency_cache=dependency_cache, ), (SkippedException,), ) return True return False async def _apply_event_postprocessors( bot: "Bot", event: "Event", state: T_State, stack: AsyncExitStack | None = None, dependency_cache: T_DependencyCache | None = None, show_log: bool = True, ) -> None: """运行事件后处理。 参数: bot: Bot 对象 event: Event 对象 state: 会话状态 stack: 异步上下文栈 dependency_cache: 依赖缓存 show_log: 是否显示日志 """ if not _event_postprocessors: return if show_log: logger.debug("Running PostProcessors...") with catch( { Exception: _handle_exception( "Error when running EventPostProcessors" ) } ): async with anyio.create_task_group() as tg: for proc in _event_postprocessors: tg.start_soon( run_coro_with_catch, proc( bot=bot, event=event, state=state, stack=stack, dependency_cache=dependency_cache, ), (SkippedException,), ) async def _apply_run_preprocessors( bot: "Bot", event: "Event", state: T_State, matcher: Matcher, stack: AsyncExitStack | None = None, dependency_cache: T_DependencyCache | None = None, ) -> bool: """运行事件响应器运行前处理。 参数: bot: Bot 对象 event: Event 对象 state: 会话状态 matcher: 事件响应器 stack: 异步上下文栈 dependency_cache: 依赖缓存 返回: 是否继续处理事件 """ if not _run_preprocessors: return True # ensure matcher function can be correctly called with ( matcher.ensure_context(bot, event), catch( { IgnoredException: _handle_ignored_exception( f"{matcher} running is cancelled" ), Exception: _handle_exception( "Error when running RunPreProcessors. " "Running cancelled!" ), } ), ): async with anyio.create_task_group() as tg: for proc in _run_preprocessors: tg.start_soon( run_coro_with_catch, proc( matcher=matcher, bot=bot, event=event, state=state, stack=stack, dependency_cache=dependency_cache, ), (SkippedException,), ) return True return False async def _apply_run_postprocessors( bot: "Bot", event: "Event", matcher: Matcher, exception: Exception | None = None, stack: AsyncExitStack | None = None, dependency_cache: T_DependencyCache | None = None, ) -> None: """运行事件响应器运行后处理。 参数: bot: Bot 对象 event: Event 对象 matcher: 事件响应器 exception: 事件响应器运行异常 stack: 异步上下文栈 dependency_cache: 依赖缓存 """ if not _run_postprocessors: return with ( matcher.ensure_context(bot, event), catch( { Exception: _handle_exception( "Error when running RunPostProcessors" "" ) } ), ): async with anyio.create_task_group() as tg: for proc in _run_postprocessors: tg.start_soon( run_coro_with_catch, proc( matcher=matcher, exception=exception, bot=bot, event=event, state=matcher.state, stack=stack, dependency_cache=dependency_cache, ), (SkippedException,), ) async def _check_matcher( Matcher: type[Matcher], bot: "Bot", event: "Event", state: T_State, stack: AsyncExitStack | None = None, dependency_cache: T_DependencyCache | None = None, ) -> bool: """检查事件响应器是否符合运行条件。 请注意,过时的事件响应器将被**销毁**。对于未过时的事件响应器,将会一次检查其响应类型、权限和规则。 参数: Matcher: 要检查的事件响应器 bot: Bot 对象 event: Event 对象 state: 会话状态 stack: 异步上下文栈 dependency_cache: 依赖缓存 返回: bool: 是否符合运行条件 """ if Matcher.expire_time and datetime.now() > Matcher.expire_time: with contextlib.suppress(Exception): Matcher.destroy() return False try: if not await Matcher.check_perm(bot, event, stack, dependency_cache): logger.trace(f"Permission conditions not met for {Matcher}") return False except Exception as e: logger.opt(colors=True, exception=e).error( f"Permission check failed for {Matcher}." ) return False try: if not await Matcher.check_rule(bot, event, state, stack, dependency_cache): logger.trace(f"Rule conditions not met for {Matcher}") return False except Exception as e: logger.opt(colors=True, exception=e).error( f"Rule check failed for {Matcher}." ) return False return True async def _run_matcher( Matcher: type[Matcher], bot: "Bot", event: "Event", state: T_State, stack: AsyncExitStack | None = None, dependency_cache: T_DependencyCache | None = None, ) -> None: """运行事件响应器。 临时事件响应器将在运行前被**销毁**。 参数: Matcher: 事件响应器 bot: Bot 对象 event: Event 对象 state: 会话状态 stack: 异步上下文栈 dependency_cache: 依赖缓存 异常: StopPropagation: 阻止事件继续传播 """ logger.info(f"Event will be handled by {Matcher}") if Matcher.temp: with contextlib.suppress(Exception): Matcher.destroy() matcher = Matcher() if not await _apply_run_preprocessors( bot=bot, event=event, state=state, matcher=matcher, stack=stack, dependency_cache=dependency_cache, ): return exception = None logger.debug(f"Running {matcher}") try: await matcher.run(bot, event, state, stack, dependency_cache) except Exception as e: logger.opt(colors=True, exception=e).error( f"Running {matcher} failed." ) exception = e await _apply_run_postprocessors( bot=bot, event=event, matcher=matcher, exception=exception, stack=stack, dependency_cache=dependency_cache, ) if matcher.block: raise StopPropagation async def check_and_run_matcher( Matcher: type[Matcher], bot: "Bot", event: "Event", state: T_State, stack: AsyncExitStack | None = None, dependency_cache: T_DependencyCache | None = None, ) -> None: """检查并运行事件响应器。 参数: Matcher: 事件响应器 bot: Bot 对象 event: Event 对象 state: 会话状态 stack: 异步上下文栈 dependency_cache: 依赖缓存 """ if not await _check_matcher( Matcher=Matcher, bot=bot, event=event, state=state, stack=stack, dependency_cache=dependency_cache, ): return await _run_matcher( Matcher=Matcher, bot=bot, event=event, state=state, stack=stack, dependency_cache=dependency_cache, ) async def handle_event(bot: "Bot", event: "Event") -> None: """处理一个事件。调用该函数以实现分发事件。 参数: bot: Bot 对象 event: Event 对象 用法: ```python driver.task_group.start_soon(handle_event, bot, event) ``` """ show_log = True log_msg = f"{escape_tag(bot.type)} {escape_tag(bot.self_id)} | " try: log_msg += event.get_log_string() except NoLogException: show_log = False if show_log: logger.opt(colors=True).success(log_msg) state: dict[Any, Any] = {} dependency_cache: T_DependencyCache = {} # create event scope context async with AsyncExitStack() as stack: if not await _apply_event_preprocessors( bot=bot, event=event, state=state, stack=stack, dependency_cache=dependency_cache, ): return # Trie Match try: TrieRule.get_value(bot, event, state) except Exception as e: logger.opt(colors=True, exception=e).warning( "Error while parsing command for event" ) break_flag = False def _handle_stop_propagation(exc_group: BaseExceptionGroup) -> None: nonlocal break_flag break_flag = True logger.debug("Stop event propagation") # iterate through all priority until stop propagation for priority in sorted(matchers.keys()): if break_flag: break if show_log: logger.debug(f"Checking for matchers in priority {priority}...") if not (priority_matchers := matchers[priority]): continue with catch( { StopPropagation: _handle_stop_propagation, Exception: _handle_exception( "Error when checking Matcher." ), } ): async with anyio.create_task_group() as tg: for matcher in priority_matchers: tg.start_soon( run_coro_with_shield, check_and_run_matcher( matcher, bot, event, state.copy(), stack, dependency_cache, ), ) if show_log: logger.debug("Checking for matchers completed") await _apply_event_postprocessors(bot, event, state, stack, dependency_cache) ================================================ FILE: nonebot/params.py ================================================ """本模块定义了依赖注入的各类参数。 FrontMatter: mdx: format: md sidebar_position: 4 description: nonebot.params 模块 """ from collections.abc import Callable from re import Match from typing import Any, Literal, overload from nonebot.adapters import Event, Message, MessageSegment from nonebot.consts import ( CMD_ARG_KEY, CMD_KEY, CMD_START_KEY, CMD_WHITESPACE_KEY, ENDSWITH_KEY, FULLMATCH_KEY, KEYWORD_KEY, PAUSE_PROMPT_RESULT_KEY, PREFIX_KEY, RAW_CMD_KEY, RECEIVE_KEY, REGEX_MATCHED, REJECT_PROMPT_RESULT_KEY, SHELL_ARGS, SHELL_ARGV, STARTSWITH_KEY, ) from nonebot.internal.params import Arg as Arg from nonebot.internal.params import ArgParam as ArgParam from nonebot.internal.params import ArgPlainText as ArgPlainText from nonebot.internal.params import ArgPromptResult as ArgPromptResult from nonebot.internal.params import ArgStr as ArgStr from nonebot.internal.params import BotParam as BotParam from nonebot.internal.params import DefaultParam as DefaultParam from nonebot.internal.params import DependParam as DependParam from nonebot.internal.params import Depends as Depends from nonebot.internal.params import EventParam as EventParam from nonebot.internal.params import ExceptionParam as ExceptionParam from nonebot.internal.params import MatcherParam as MatcherParam from nonebot.internal.params import StateParam as StateParam from nonebot.matcher import Matcher from nonebot.typing import T_State async def _event_type(event: Event) -> str: return event.get_type() def EventType() -> str: """{ref}`nonebot.adapters.Event` 类型参数""" return Depends(_event_type) async def _event_message(event: Event) -> Message: return event.get_message() def EventMessage() -> Any: """{ref}`nonebot.adapters.Event` 消息参数""" return Depends(_event_message) async def _event_plain_text(event: Event) -> str: return event.get_plaintext() def EventPlainText() -> str: """{ref}`nonebot.adapters.Event` 纯文本消息参数""" return Depends(_event_plain_text) async def _event_to_me(event: Event) -> bool: return event.is_tome() def EventToMe() -> bool: """{ref}`nonebot.adapters.Event` `to_me` 参数""" return Depends(_event_to_me) def _command(state: T_State) -> Message: return state[PREFIX_KEY][CMD_KEY] def Command() -> tuple[str, ...]: """消息命令元组""" return Depends(_command) def _raw_command(state: T_State) -> Message: return state[PREFIX_KEY][RAW_CMD_KEY] def RawCommand() -> str: """消息命令文本""" return Depends(_raw_command) def _command_arg(state: T_State) -> Message: return state[PREFIX_KEY][CMD_ARG_KEY] def CommandArg() -> Any: """消息命令参数""" return Depends(_command_arg) def _command_start(state: T_State) -> str: return state[PREFIX_KEY][CMD_START_KEY] def CommandStart() -> str: """消息命令开头""" return Depends(_command_start) def _command_whitespace(state: T_State) -> str: return state[PREFIX_KEY][CMD_WHITESPACE_KEY] def CommandWhitespace() -> str: """消息命令与参数之间的空白""" return Depends(_command_whitespace) def _shell_command_args(state: T_State) -> Any: return state[SHELL_ARGS] # Namespace or ParserExit def ShellCommandArgs() -> Any: """shell 命令解析后的参数字典""" return Depends(_shell_command_args, use_cache=False) def _shell_command_argv(state: T_State) -> list[str | MessageSegment]: return state[SHELL_ARGV] def ShellCommandArgv() -> Any: """shell 命令原始参数列表""" return Depends(_shell_command_argv, use_cache=False) def _regex_matched(state: T_State) -> Match[str]: return state[REGEX_MATCHED] def RegexMatched() -> Match[str]: """正则匹配结果""" return Depends(_regex_matched, use_cache=False) def _regex_str( groups: tuple[str | int, ...], ) -> Callable[[T_State], str | tuple[str | Any, ...] | Any]: def _regex_str_dependency( state: T_State, ) -> str | tuple[str | Any, ...] | Any: return _regex_matched(state).group(*groups) return _regex_str_dependency @overload def RegexStr(group: Literal[0] = 0, /) -> str: ... @overload def RegexStr(group: str | int, /) -> str | Any: ... @overload def RegexStr( group1: str | int, group2: str | int, /, *groups: str | int ) -> tuple[str | Any, ...]: ... def RegexStr(*groups: str | int) -> str | tuple[str | Any, ...] | Any: """正则匹配结果文本""" return Depends(_regex_str(groups), use_cache=False) def _regex_group(state: T_State) -> tuple[Any, ...]: return _regex_matched(state).groups() def RegexGroup() -> tuple[Any, ...]: """正则匹配结果 group 元组""" return Depends(_regex_group, use_cache=False) def _regex_dict(state: T_State) -> dict[str, Any]: return _regex_matched(state).groupdict() def RegexDict() -> dict[str, Any]: """正则匹配结果 group 字典""" return Depends(_regex_dict, use_cache=False) def _startswith(state: T_State) -> str: return state[STARTSWITH_KEY] def Startswith() -> str: """响应触发前缀""" return Depends(_startswith, use_cache=False) def _endswith(state: T_State) -> str: return state[ENDSWITH_KEY] def Endswith() -> str: """响应触发后缀""" return Depends(_endswith, use_cache=False) def _fullmatch(state: T_State) -> str: return state[FULLMATCH_KEY] def Fullmatch() -> str: """响应触发完整消息""" return Depends(_fullmatch, use_cache=False) def _keyword(state: T_State) -> str: return state[KEYWORD_KEY] def Keyword() -> str: """响应触发关键字""" return Depends(_keyword, use_cache=False) def Received(id: str | None = None, default: Any = None) -> Any: """`receive` 事件参数""" def _received(matcher: "Matcher") -> Any: return matcher.get_receive(id or "", default) return Depends(_received, use_cache=False) def LastReceived(default: Any = None) -> Any: """`last_receive` 事件参数""" def _last_received(matcher: "Matcher") -> Any: return matcher.get_last_receive(default) return Depends(_last_received, use_cache=False) def ReceivePromptResult(id: str | None = None) -> Any: """`receive` prompt 发送结果""" def _receive_prompt_result(matcher: "Matcher") -> Any: return matcher.state.get( REJECT_PROMPT_RESULT_KEY.format(key=RECEIVE_KEY.format(id=id)) ) return Depends(_receive_prompt_result, use_cache=False) def PausePromptResult() -> Any: """`pause` prompt 发送结果""" def _pause_prompt_result(matcher: "Matcher") -> Any: return matcher.state.get(PAUSE_PROMPT_RESULT_KEY) return Depends(_pause_prompt_result, use_cache=False) __autodoc__ = { "Arg": True, "ArgStr": True, "Depends": True, "ArgParam": True, "BotParam": True, "EventParam": True, "StateParam": True, "DependParam": True, "ArgPlainText": True, "DefaultParam": True, "MatcherParam": True, "ExceptionParam": True, "ArgPromptResult": True, } ================================================ FILE: nonebot/permission.py ================================================ """本模块是 {ref}`nonebot.matcher.Matcher.permission` 的类型定义。 每个{ref}`事件响应器 ` 拥有一个 {ref}`nonebot.permission.Permission`,其中是 `PermissionChecker` 的集合。 只要有一个 `PermissionChecker` 检查结果为 `True` 时就会继续运行。 FrontMatter: mdx: format: md sidebar_position: 6 description: nonebot.permission 模块 """ from nonebot.adapters import Bot, Event from nonebot.internal.permission import USER as USER from nonebot.internal.permission import Permission as Permission from nonebot.internal.permission import User as User from nonebot.params import EventType class Message: """检查是否为消息事件""" __slots__ = () def __repr__(self) -> str: return "Message()" async def __call__(self, type: str = EventType()) -> bool: return type == "message" class Notice: """检查是否为通知事件""" __slots__ = () def __repr__(self) -> str: return "Notice()" async def __call__(self, type: str = EventType()) -> bool: return type == "notice" class Request: """检查是否为请求事件""" __slots__ = () def __repr__(self) -> str: return "Request()" async def __call__(self, type: str = EventType()) -> bool: return type == "request" class MetaEvent: """检查是否为元事件""" __slots__ = () def __repr__(self) -> str: return "MetaEvent()" async def __call__(self, type: str = EventType()) -> bool: return type == "meta_event" MESSAGE: Permission = Permission(Message()) """匹配任意 `message` 类型事件 仅在需要同时捕获不同类型事件时使用,优先使用 message type 的 Matcher。 """ NOTICE: Permission = Permission(Notice()) """匹配任意 `notice` 类型事件 仅在需要同时捕获不同类型事件时使用,优先使用 notice type 的 Matcher。 """ REQUEST: Permission = Permission(Request()) """匹配任意 `request` 类型事件 仅在需要同时捕获不同类型事件时使用,优先使用 request type 的 Matcher。 """ METAEVENT: Permission = Permission(MetaEvent()) """匹配任意 `meta_event` 类型事件 仅在需要同时捕获不同类型事件时使用,优先使用 meta_event type 的 Matcher。 """ class SuperUser: """检查当前事件是否是消息事件且属于超级管理员""" __slots__ = () def __repr__(self) -> str: return "Superuser()" async def __call__(self, bot: Bot, event: Event) -> bool: try: user_id = event.get_user_id() except Exception: return False return ( f"{bot.adapter.get_name().split(maxsplit=1)[0].lower()}:{user_id}" in bot.config.superusers or user_id in bot.config.superusers # 兼容旧配置 ) SUPERUSER: Permission = Permission(SuperUser()) """匹配任意超级用户事件""" __autodoc__ = { "Permission": True, "Permission.__call__": True, "User": True, "USER": True, } ================================================ FILE: nonebot/plugin/__init__.py ================================================ """本模块为 NoneBot 插件开发提供便携的定义函数。 ## 快捷导入 为方便使用,本模块从子模块导入了部分内容,以下内容可以直接通过本模块导入: - `on` => {ref}``on` ` - `on_metaevent` => {ref}``on_metaevent` ` - `on_message` => {ref}``on_message` ` - `on_notice` => {ref}``on_notice` ` - `on_request` => {ref}``on_request` ` - `on_startswith` => {ref}``on_startswith` ` - `on_endswith` => {ref}``on_endswith` ` - `on_fullmatch` => {ref}``on_fullmatch` ` - `on_keyword` => {ref}``on_keyword` ` - `on_command` => {ref}``on_command` ` - `on_shell_command` => {ref}``on_shell_command` ` - `on_regex` => {ref}``on_regex` ` - `on_type` => {ref}``on_type` ` - `CommandGroup` => {ref}``CommandGroup` ` - `Matchergroup` => {ref}``MatcherGroup` ` - `load_plugin` => {ref}``load_plugin` ` - `load_plugins` => {ref}``load_plugins` ` - `load_all_plugins` => {ref}``load_all_plugins` ` - `load_from_json` => {ref}``load_from_json` ` - `load_from_toml` => {ref}``load_from_toml` ` - `load_builtin_plugin` => {ref}``load_builtin_plugin` ` - `load_builtin_plugins` => {ref}``load_builtin_plugins` ` - `require` => {ref}``require` ` - `PluginMetadata` => {ref}``PluginMetadata` ` FrontMatter: mdx: format: md sidebar_position: 0 description: nonebot.plugin 模块 """ from contextvars import ContextVar from itertools import chain from types import ModuleType from typing import TypeVar from pydantic import BaseModel from nonebot import get_driver from nonebot.compat import model_dump, type_validate_python from nonebot.config import BaseSettings C = TypeVar("C", bound=BaseModel) _plugins: dict[str, "Plugin"] = {} _managers: list["PluginManager"] = [] _current_plugin: ContextVar["Plugin | None"] = ContextVar( "_current_plugin", default=None ) def _module_name_to_plugin_name(module_name: str) -> str: return module_name.rsplit(".", 1)[-1] def _controlled_modules() -> dict[str, str]: return { plugin_id: module_name for manager in _managers for plugin_id, module_name in manager.controlled_modules.items() } def _find_parent_plugin_id( module_name: str, controlled_modules: dict[str, str] | None = None ) -> str | None: if controlled_modules is None: controlled_modules = _controlled_modules() available = { module_name: plugin_id for plugin_id, module_name in controlled_modules.items() } while "." in module_name: module_name, _ = module_name.rsplit(".", 1) if module_name in available: return available[module_name] def _module_name_to_plugin_id( module_name: str, controlled_modules: dict[str, str] | None = None ) -> str: plugin_name = _module_name_to_plugin_name(module_name) if parent_plugin_id := _find_parent_plugin_id(module_name, controlled_modules): return f"{parent_plugin_id}:{plugin_name}" return plugin_name def _new_plugin( module_name: str, module: ModuleType, manager: "PluginManager" ) -> "Plugin": plugin_id = _module_name_to_plugin_id(module_name) if plugin_id in _plugins: raise RuntimeError( f"Plugin {plugin_id} already exists! Check your plugin name." ) parent_plugin_id = _find_parent_plugin_id(module_name) if parent_plugin_id is not None and parent_plugin_id not in _plugins: raise RuntimeError( f"Parent plugin {parent_plugin_id} must " f"be loaded before loading {plugin_id}." ) parent_plugin = _plugins[parent_plugin_id] if parent_plugin_id is not None else None plugin = Plugin( name=_module_name_to_plugin_name(module_name), module=module, module_name=module_name, manager=manager, parent_plugin=parent_plugin, ) if parent_plugin: parent_plugin.sub_plugins.add(plugin) _plugins[plugin_id] = plugin return plugin def _revert_plugin(plugin: "Plugin") -> None: if plugin.id_ not in _plugins: raise RuntimeError("Plugin not found!") del _plugins[plugin.id_] if parent_plugin := plugin.parent_plugin: parent_plugin.sub_plugins.discard(plugin) def get_plugin(plugin_id: str) -> "Plugin | None": """获取已经导入的某个插件。 如果为 `load_plugins` 文件夹导入的插件,则为文件(夹)名。 如果为嵌套的子插件,标识符为 `父插件标识符:子插件文件(夹)名`。 参数: plugin_id: 插件标识符,即 {ref}`nonebot.plugin.model.Plugin.id_`。 """ return _plugins.get(plugin_id) def get_plugin_by_module_name(module_name: str) -> "Plugin | None": """通过模块名获取已经导入的某个插件。 如果提供的模块名为某个插件的子模块,同样会返回该插件。 参数: module_name: 模块名,即 {ref}`nonebot.plugin.model.Plugin.module_name`。 """ loaded = {plugin.module_name: plugin for plugin in _plugins.values()} has_parent = True while has_parent: if module_name in loaded: return loaded[module_name] module_name, *has_parent = module_name.rsplit(".", 1) def get_loaded_plugins() -> set["Plugin"]: """获取当前已导入的所有插件。""" return set(_plugins.values()) def get_available_plugin_names() -> set[str]: """获取当前所有可用的插件标识符(包含尚未加载的插件)。""" return {*chain.from_iterable(manager.available_plugins for manager in _managers)} def get_plugin_config(config: type[C]) -> C: """从全局配置获取当前插件需要的配置项。""" global_config = get_driver().config return type_validate_python( config, BaseSettings._settings_build_values( config, model_dump(global_config), env_file=global_config._env_file, env_file_encoding=global_config._env_file_encoding, env_nested_delimiter=global_config._env_nested_delimiter, ), ) from .load import inherit_supported_adapters as inherit_supported_adapters from .load import load_all_plugins as load_all_plugins from .load import load_builtin_plugin as load_builtin_plugin from .load import load_builtin_plugins as load_builtin_plugins from .load import load_from_json as load_from_json from .load import load_from_toml as load_from_toml from .load import load_plugin as load_plugin from .load import load_plugins as load_plugins from .load import require as require from .manager import PluginManager from .model import Plugin as Plugin from .model import PluginMetadata as PluginMetadata from .on import CommandGroup as CommandGroup from .on import MatcherGroup as MatcherGroup from .on import on as on from .on import on_command as on_command from .on import on_endswith as on_endswith from .on import on_fullmatch as on_fullmatch from .on import on_keyword as on_keyword from .on import on_message as on_message from .on import on_metaevent as on_metaevent from .on import on_notice as on_notice from .on import on_regex as on_regex from .on import on_request as on_request from .on import on_shell_command as on_shell_command from .on import on_startswith as on_startswith from .on import on_type as on_type ================================================ FILE: nonebot/plugin/load.py ================================================ """本模块定义插件加载接口。 FrontMatter: mdx: format: md sidebar_position: 1 description: nonebot.plugin.load 模块 """ from collections.abc import Iterable from itertools import chain import json from pathlib import Path from types import ModuleType from nonebot.log import logger from nonebot.utils import path_to_module_name from . import _managers, _module_name_to_plugin_id, get_plugin from .manager import PluginManager from .model import Plugin try: # pragma: py-gte-311 import tomllib # pyright: ignore[reportMissingImports] except ModuleNotFoundError: # pragma: py-lt-311 import tomli as tomllib # pyright: ignore[reportMissingImports] def load_plugin(module_path: str | Path) -> Plugin | None: """加载单个插件,可以是本地插件或是通过 `pip` 安装的插件。 参数: module_path: 插件名称 `path.to.your.plugin` 或插件路径 `pathlib.Path(path/to/your/plugin)` """ module_path = ( path_to_module_name(module_path) if isinstance(module_path, Path) else module_path ) manager = PluginManager([module_path]) _managers.append(manager) return manager.load_plugin(module_path) def load_plugins(*plugin_dir: str) -> set[Plugin]: """导入文件夹下多个插件,以 `_` 开头的插件不会被导入! 参数: plugin_dir: 文件夹路径 """ manager = PluginManager(search_path=plugin_dir) _managers.append(manager) return manager.load_all_plugins() def load_all_plugins( module_path: Iterable[str], plugin_dir: Iterable[str] ) -> set[Plugin]: """导入指定列表中的插件以及指定目录下多个插件,以 `_` 开头的插件不会被导入! 参数: module_path: 指定插件集合 plugin_dir: 指定文件夹路径集合 """ manager = PluginManager(module_path, plugin_dir) _managers.append(manager) return manager.load_all_plugins() def load_from_json(file_path: str, encoding: str = "utf-8") -> set[Plugin]: """导入指定 json 文件中的 `plugins` 以及 `plugin_dirs` 下多个插件。 以 `_` 开头的插件不会被导入! 参数: file_path: 指定 json 文件路径 encoding: 指定 json 文件编码 用法: ```json title=plugins.json { "plugins": ["some_plugin"], "plugin_dirs": ["some_dir"] } ``` ```python nonebot.load_from_json("plugins.json") ``` """ with open(file_path, encoding=encoding) as f: data = json.load(f) if not isinstance(data, dict): raise TypeError("json file must contains a dict!") plugins = data.get("plugins") plugin_dirs = data.get("plugin_dirs") assert isinstance(plugins, list), "plugins must be a list of plugin name" assert isinstance(plugin_dirs, list), "plugin_dirs must be a list of directories" return load_all_plugins(set(plugins), set(plugin_dirs)) def load_from_toml(file_path: str, encoding: str = "utf-8") -> set[Plugin]: """导入指定 toml 文件 `[tool.nonebot]` 中的 `plugins` 以及 `plugin_dirs` 下多个插件。 以 `_` 开头的插件不会被导入! 参数: file_path: 指定 toml 文件路径 encoding: 指定 toml 文件编码 用法: 新格式: ```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"] plugin_dirs = ["some_dir"] ``` ```python nonebot.load_from_toml("pyproject.toml") ``` """ with open(file_path, encoding=encoding) as f: data = tomllib.loads(f.read()) nonebot_data = data.get("tool", {}).get("nonebot") if nonebot_data is None: raise ValueError("Cannot find '[tool.nonebot]' in given toml file!") if not isinstance(nonebot_data, dict): raise TypeError("'[tool.nonebot]' must be a Table!") plugins = nonebot_data.get("plugins", {}) plugin_dirs = nonebot_data.get("plugin_dirs", []) assert isinstance(plugins, (list, dict)), ( "plugins must be a list or a dict of plugin name" ) assert isinstance(plugin_dirs, list), "plugin_dirs must be a list of directories" if isinstance(plugins, list): logger.warning("Legacy project format found! Upgrade with `nb upgrade-format`.") return load_all_plugins( set( chain.from_iterable(plugins.values()) if isinstance(plugins, dict) else plugins ), plugin_dirs, ) def load_builtin_plugin(name: str) -> Plugin | None: """导入 NoneBot 内置插件。 参数: name: 插件名称 """ return load_plugin(f"nonebot.plugins.{name}") def load_builtin_plugins(*plugins: str) -> set[Plugin]: """导入多个 NoneBot 内置插件。 参数: plugins: 插件名称列表 """ return load_all_plugins([f"nonebot.plugins.{p}" for p in plugins], []) def _find_manager_by_name(name: str) -> PluginManager | None: for manager in reversed(_managers): if ( name in manager.controlled_modules or name in manager.controlled_modules.values() ): return manager def require(name: str) -> ModuleType: """声明依赖插件。 参数: name: 插件模块名或插件标识符,仅在已声明插件的情况下可使用标识符。 异常: RuntimeError: 插件无法加载 """ if "." in name: # name is a module name plugin = get_plugin(_module_name_to_plugin_id(name)) else: # name is a plugin id or simple module name (equals to plugin id) plugin = get_plugin(name) # if plugin not loaded if plugin is None: # plugin already declared, module name / plugin id if manager := _find_manager_by_name(name): plugin = manager.load_plugin(name) # plugin not declared, try to declare and load it else: plugin = load_plugin(name) if plugin is None: raise RuntimeError(f'Cannot load plugin "{name}"!') return plugin.module def inherit_supported_adapters(*names: str) -> set[str] | None: """获取已加载插件的适配器支持状态集合。 如果传入了多个插件名称,返回值会自动取交集。 参数: names: 插件名称列表。 异常: RuntimeError: 插件未加载 ValueError: 插件缺少元数据 """ final_supported: set[str] | None = None for name in names: plugin = get_plugin(_module_name_to_plugin_id(name)) if plugin is None: raise RuntimeError( f'Plugin "{name}" is not loaded! You should require it first.' ) meta = plugin.metadata if meta is None: raise ValueError(f'Plugin "{name}" has no metadata!') if (raw := meta.supported_adapters) is None: continue support = { f"nonebot.adapters.{adapter[1:]}" if adapter.startswith("~") else adapter for adapter in raw } final_supported = ( support if final_supported is None else (final_supported & support) ) return final_supported ================================================ FILE: nonebot/plugin/manager.py ================================================ """本模块实现插件加载流程。 参考: [import hooks](https://docs.python.org/3/reference/import.html#import-hooks), [PEP302](https://www.python.org/dev/peps/pep-0302/) FrontMatter: mdx: format: md sidebar_position: 5 description: nonebot.plugin.manager 模块 """ from collections.abc import Iterable, Sequence import importlib from importlib.abc import MetaPathFinder from importlib.machinery import PathFinder, SourceFileLoader from itertools import chain from pathlib import Path import pkgutil import sys from types import ModuleType from nonebot.log import logger from nonebot.utils import escape_tag, path_to_module_name from . import ( _current_plugin, _managers, _module_name_to_plugin_id, _new_plugin, _revert_plugin, ) from .model import Plugin, PluginMetadata class PluginManager: """插件管理器。 参数: plugins: 独立插件模块名集合。 search_path: 插件搜索路径(文件夹),相对于当前工作目录。 """ def __init__( self, plugins: Iterable[str] | None = None, search_path: Iterable[str] | None = None, ): # simple plugin not in search path self.plugins: set[str] = set(plugins or []) self.search_path: set[str] = set(search_path or []) # cache plugins self._third_party_plugin_ids: dict[str, str] = {} self._searched_plugin_ids: dict[str, str] = {} self._prepare_plugins() def __repr__(self) -> str: return f"PluginManager(available_plugins={self.controlled_modules})" @property def third_party_plugins(self) -> set[str]: """返回所有独立插件标识符。""" return set(self._third_party_plugin_ids.keys()) @property def searched_plugins(self) -> set[str]: """返回已搜索到的插件标识符。""" return set(self._searched_plugin_ids.keys()) @property def available_plugins(self) -> set[str]: """返回当前插件管理器中可用的插件标识符。""" return self.third_party_plugins | self.searched_plugins @property def controlled_modules(self) -> dict[str, str]: """返回当前插件管理器中控制的插件标识符与模块路径映射字典。""" return dict( chain( self._third_party_plugin_ids.items(), self._searched_plugin_ids.items() ) ) def _previous_controlled_modules(self) -> dict[str, str]: _pre_managers: list[PluginManager] if self in _managers: _pre_managers = _managers[: _managers.index(self)] else: _pre_managers = _managers[:] return { plugin_id: module_name for manager in _pre_managers for plugin_id, module_name in manager.controlled_modules.items() } def _prepare_plugins(self) -> set[str]: """搜索插件并缓存插件名称。""" # get all previous ready to load plugins previous_plugin_ids = self._previous_controlled_modules() # if self not in global managers, merge self's controlled modules def get_controlled_modules(): return ( previous_plugin_ids if self in _managers else {**previous_plugin_ids, **self.controlled_modules} ) # check third party plugins for plugin in self.plugins: plugin_id = _module_name_to_plugin_id(plugin, get_controlled_modules()) if ( plugin_id in self._third_party_plugin_ids or plugin_id in previous_plugin_ids ): raise RuntimeError( f"Plugin already exists: {plugin_id}! Check your plugin name" ) self._third_party_plugin_ids[plugin_id] = plugin # check plugins in search path for module_info in pkgutil.iter_modules(self.search_path): # ignore if startswith "_" if module_info.name.startswith("_"): continue if not ( module_spec := module_info.module_finder.find_spec( module_info.name, None ) ): continue if not module_spec.origin: continue # get module name from path, pkgutil does not return the actual module name module_path = Path(module_spec.origin).resolve() module_name = path_to_module_name(module_path) plugin_id = _module_name_to_plugin_id(module_name, get_controlled_modules()) if ( plugin_id in previous_plugin_ids or plugin_id in self._third_party_plugin_ids or plugin_id in self._searched_plugin_ids ): raise RuntimeError( f"Plugin already exists: {plugin_id}! Check your plugin name" ) self._searched_plugin_ids[plugin_id] = module_name return self.available_plugins def load_plugin(self, name: str) -> Plugin | None: """加载指定插件。 可以使用完整插件模块名或者插件标识符加载。 参数: name: 插件名称或插件标识符。 """ try: # load using plugin id if name in self._third_party_plugin_ids: module = importlib.import_module(self._third_party_plugin_ids[name]) elif name in self._searched_plugin_ids: module = importlib.import_module(self._searched_plugin_ids[name]) # load using module name elif ( name in self._third_party_plugin_ids.values() or name in self._searched_plugin_ids.values() ): module = importlib.import_module(name) else: raise RuntimeError(f"Plugin not found: {name}! Check your plugin name") if ( plugin := getattr(module, "__plugin__", None) ) is None or not isinstance(plugin, Plugin): raise RuntimeError( f"Module {module.__name__} is not loaded as a plugin! " f"Make sure not to import it before loading." ) logger.opt(colors=True).success( f'Succeeded to load plugin "{escape_tag(plugin.id_)}"' + ( f' from "{escape_tag(plugin.module_name)}"' if plugin.module_name != plugin.id_ else "" ) ) return plugin except Exception as e: logger.opt(colors=True, exception=e).error( f'Failed to import "{escape_tag(name)}"' ) def load_all_plugins(self) -> set[Plugin]: """加载所有可用插件。""" return set( filter(None, (self.load_plugin(name) for name in self.available_plugins)) ) class PluginFinder(MetaPathFinder): def find_spec( self, fullname: str, path: Sequence[str] | None, target: ModuleType | None = None, ): if _managers: module_spec = PathFinder.find_spec(fullname, path, target) if not module_spec: return module_origin = module_spec.origin if not module_origin: return for manager in reversed(_managers): if fullname in manager.controlled_modules.values(): module_spec.loader = PluginLoader(manager, fullname, module_origin) return module_spec return class PluginLoader(SourceFileLoader): def __init__(self, manager: PluginManager, fullname: str, path: str) -> None: self.manager = manager self.loaded = False super().__init__(fullname, path) def create_module(self, spec) -> ModuleType | None: if self.name in sys.modules: self.loaded = True return sys.modules[self.name] # return None to use default module creation return super().create_module(spec) def exec_module(self, module: ModuleType) -> None: if self.loaded: return # create plugin before executing plugin = _new_plugin(self.name, module, self.manager) setattr(module, "__plugin__", plugin) # enter plugin context _plugin_token = _current_plugin.set(plugin) try: super().exec_module(module) except Exception: _revert_plugin(plugin) raise finally: # leave plugin context _current_plugin.reset(_plugin_token) # get plugin metadata metadata: PluginMetadata | None = getattr(module, "__plugin_meta__", None) plugin.metadata = metadata return sys.meta_path.insert(0, PluginFinder()) ================================================ FILE: nonebot/plugin/model.py ================================================ """本模块定义插件相关信息。 FrontMatter: mdx: format: md sidebar_position: 3 description: nonebot.plugin.model 模块 """ import contextlib from dataclasses import dataclass, field from types import ModuleType from typing import TYPE_CHECKING, Any, Type # noqa: UP035 from pydantic import BaseModel from nonebot.matcher import Matcher from nonebot.utils import resolve_dot_notation if TYPE_CHECKING: from nonebot.adapters import Adapter from .manager import PluginManager @dataclass(eq=False) class PluginMetadata: """插件元信息,由插件编写者提供""" name: str """插件名称""" description: str """插件功能介绍""" usage: str """插件使用方法""" type: str | None = None """插件类型,用于商店分类""" homepage: str | None = None """插件主页""" config: Type[BaseModel] | None = None # noqa: UP006 """插件配置项""" supported_adapters: set[str] | None = None """插件支持的适配器模块路径 格式为 `[:]`,`~` 为 `nonebot.adapters.` 的缩写。 `None` 表示支持**所有适配器**。 """ extra: dict[Any, Any] = field(default_factory=dict) """插件额外信息,可由插件编写者自由扩展定义""" def get_supported_adapters(self) -> set[Type["Adapter"]] | None: # noqa: UP006 """获取当前已安装的插件支持适配器类列表""" if self.supported_adapters is None: return None adapters = set() for adapter in self.supported_adapters: with contextlib.suppress(ModuleNotFoundError, AttributeError): adapters.add( resolve_dot_notation(adapter, "Adapter", "nonebot.adapters.") ) return adapters @dataclass(eq=False) class Plugin: """存储插件信息""" name: str """插件名称,NoneBot 使用 文件/文件夹 名称作为插件名称""" module: ModuleType """插件模块对象""" module_name: str """点分割模块路径""" manager: "PluginManager" """导入该插件的插件管理器""" matcher: set[type[Matcher]] = field(default_factory=set) """插件加载时定义的 `Matcher`""" parent_plugin: "Plugin | None" = None """父插件""" sub_plugins: set["Plugin"] = field(default_factory=set) """子插件集合""" metadata: PluginMetadata | None = None """插件元信息""" @property def id_(self) -> str: """插件索引标识""" return ( f"{self.parent_plugin.id_}:{self.name}" if self.parent_plugin else self.name ) ================================================ FILE: nonebot/plugin/on.py ================================================ """本模块定义事件响应器便携定义函数。 FrontMatter: mdx: format: md sidebar_position: 2 description: nonebot.plugin.on 模块 """ from datetime import datetime, timedelta import inspect import re from types import ModuleType from typing import Any import warnings from nonebot.adapters import Event from nonebot.dependencies import Dependent from nonebot.matcher import Matcher, MatcherSource from nonebot.permission import Permission from nonebot.rule import ( ArgumentParser, Rule, command, endswith, fullmatch, is_type, keyword, regex, shell_command, startswith, ) from nonebot.typing import T_Handler, T_PermissionChecker, T_RuleChecker, T_State from . import get_plugin_by_module_name from .manager import _current_plugin from .model import Plugin def store_matcher(matcher: type[Matcher]) -> None: """存储一个事件响应器到插件。 参数: matcher: 事件响应器 """ # only store the matcher defined when plugin loading if plugin := _current_plugin.get(): plugin.matcher.add(matcher) def get_matcher_plugin(depth: int = 1) -> Plugin | None: # pragma: no cover """获取事件响应器定义所在插件。 **Deprecated**, 请使用 {ref}`nonebot.plugin.on.get_matcher_source` 获取信息。 参数: depth: 调用栈深度 """ warnings.warn( "`get_matcher_plugin` is deprecated, please use `get_matcher_source` instead", DeprecationWarning, ) return (source := get_matcher_source(depth + 1)) and source.plugin def get_matcher_module(depth: int = 1) -> ModuleType | None: # pragma: no cover """获取事件响应器定义所在模块。 **Deprecated**, 请使用 {ref}`nonebot.plugin.on.get_matcher_source` 获取信息。 参数: depth: 调用栈深度 """ warnings.warn( "`get_matcher_module` is deprecated, please use `get_matcher_source` instead", DeprecationWarning, ) return (source := get_matcher_source(depth + 1)) and source.module def get_matcher_source(depth: int = 0) -> MatcherSource | None: """获取事件响应器定义所在源码信息。 参数: depth: 调用栈深度 """ current_frame = inspect.currentframe() if current_frame is None: return None frame = current_frame d = depth + 1 while d > 0: frame = frame.f_back if frame is None: raise ValueError("Depth out of range") d -= 1 module_name = (module := inspect.getmodule(frame)) and module.__name__ # matcher defined when plugin loading plugin: Plugin | None = _current_plugin.get() # matcher defined when plugin running if plugin is None and module_name: plugin = get_plugin_by_module_name(module_name) return MatcherSource( plugin_id=plugin and plugin.id_, module_name=module_name, lineno=frame.f_lineno, ) def on( type: str = "", rule: Rule | T_RuleChecker | None = None, permission: Permission | T_PermissionChecker | None = None, *, handlers: list[T_Handler | Dependent[Any]] | None = None, temp: bool = False, expire_time: datetime | timedelta | None = None, priority: int = 1, block: bool = False, state: T_State | None = None, _depth: int = 0, ) -> type[Matcher]: """注册一个基础事件响应器,可自定义类型。 参数: type: 事件响应器类型 rule: 事件响应规则 permission: 事件响应权限 handlers: 事件处理函数列表 temp: 是否为临时事件响应器(仅执行一次) expire_time: 事件响应器最终有效时间点,过时即被删除 priority: 事件响应器优先级 block: 是否阻止事件向更低优先级传递 state: 默认 state """ matcher = Matcher.new( type, Rule() & rule, Permission() | permission, temp=temp, expire_time=expire_time, priority=priority, block=block, handlers=handlers, source=get_matcher_source(_depth + 1), default_state=state, ) store_matcher(matcher) return matcher def on_metaevent(*args, _depth: int = 0, **kwargs) -> type[Matcher]: """注册一个元事件响应器。 参数: rule: 事件响应规则 permission: 事件响应权限 handlers: 事件处理函数列表 temp: 是否为临时事件响应器(仅执行一次) expire_time: 事件响应器最终有效时间点,过时即被删除 priority: 事件响应器优先级 block: 是否阻止事件向更低优先级传递 state: 默认 state """ return on("meta_event", *args, **kwargs, _depth=_depth + 1) def on_message(*args, _depth: int = 0, **kwargs) -> type[Matcher]: """注册一个消息事件响应器。 参数: rule: 事件响应规则 permission: 事件响应权限 handlers: 事件处理函数列表 temp: 是否为临时事件响应器(仅执行一次) expire_time: 事件响应器最终有效时间点,过时即被删除 priority: 事件响应器优先级 block: 是否阻止事件向更低优先级传递 state: 默认 state """ kwargs.setdefault("block", True) return on("message", *args, **kwargs, _depth=_depth + 1) def on_notice(*args, _depth: int = 0, **kwargs) -> type[Matcher]: """注册一个通知事件响应器。 参数: rule: 事件响应规则 permission: 事件响应权限 handlers: 事件处理函数列表 temp: 是否为临时事件响应器(仅执行一次) expire_time: 事件响应器最终有效时间点,过时即被删除 priority: 事件响应器优先级 block: 是否阻止事件向更低优先级传递 state: 默认 state """ return on("notice", *args, **kwargs, _depth=_depth + 1) def on_request(*args, _depth: int = 0, **kwargs) -> type[Matcher]: """注册一个请求事件响应器。 参数: rule: 事件响应规则 permission: 事件响应权限 handlers: 事件处理函数列表 temp: 是否为临时事件响应器(仅执行一次) expire_time: 事件响应器最终有效时间点,过时即被删除 priority: 事件响应器优先级 block: 是否阻止事件向更低优先级传递 state: 默认 state """ return on("request", *args, **kwargs, _depth=_depth + 1) def on_startswith( msg: str | tuple[str, ...], rule: Rule | T_RuleChecker | None = None, ignorecase: bool = False, _depth: int = 0, **kwargs, ) -> type[Matcher]: """注册一个消息事件响应器,并且当消息的**文本部分**以指定内容开头时响应。 参数: msg: 指定消息开头内容 rule: 事件响应规则 ignorecase: 是否忽略大小写 permission: 事件响应权限 handlers: 事件处理函数列表 temp: 是否为临时事件响应器(仅执行一次) expire_time: 事件响应器最终有效时间点,过时即被删除 priority: 事件响应器优先级 block: 是否阻止事件向更低优先级传递 state: 默认 state """ return on_message(startswith(msg, ignorecase) & rule, **kwargs, _depth=_depth + 1) def on_endswith( msg: str | tuple[str, ...], rule: Rule | T_RuleChecker | None = None, ignorecase: bool = False, _depth: int = 0, **kwargs, ) -> type[Matcher]: """注册一个消息事件响应器,并且当消息的**文本部分**以指定内容结尾时响应。 参数: msg: 指定消息结尾内容 rule: 事件响应规则 ignorecase: 是否忽略大小写 permission: 事件响应权限 handlers: 事件处理函数列表 temp: 是否为临时事件响应器(仅执行一次) expire_time: 事件响应器最终有效时间点,过时即被删除 priority: 事件响应器优先级 block: 是否阻止事件向更低优先级传递 state: 默认 state """ return on_message(endswith(msg, ignorecase) & rule, **kwargs, _depth=_depth + 1) def on_fullmatch( msg: str | tuple[str, ...], rule: Rule | T_RuleChecker | None = None, ignorecase: bool = False, _depth: int = 0, **kwargs, ) -> type[Matcher]: """注册一个消息事件响应器,并且当消息的**文本部分**与指定内容完全一致时响应。 参数: msg: 指定消息全匹配内容 rule: 事件响应规则 ignorecase: 是否忽略大小写 permission: 事件响应权限 handlers: 事件处理函数列表 temp: 是否为临时事件响应器(仅执行一次) expire_time: 事件响应器最终有效时间点,过时即被删除 priority: 事件响应器优先级 block: 是否阻止事件向更低优先级传递 state: 默认 state """ return on_message(fullmatch(msg, ignorecase) & rule, **kwargs, _depth=_depth + 1) def on_keyword( keywords: set[str], rule: Rule | T_RuleChecker | None = None, _depth: int = 0, **kwargs, ) -> type[Matcher]: """注册一个消息事件响应器,并且当消息纯文本部分包含关键词时响应。 参数: keywords: 关键词列表 rule: 事件响应规则 permission: 事件响应权限 handlers: 事件处理函数列表 temp: 是否为临时事件响应器(仅执行一次) expire_time: 事件响应器最终有效时间点,过时即被删除 priority: 事件响应器优先级 block: 是否阻止事件向更低优先级传递 state: 默认 state """ return on_message(keyword(*keywords) & rule, **kwargs, _depth=_depth + 1) def on_command( cmd: str | tuple[str, ...], rule: Rule | T_RuleChecker | None = None, aliases: set[str | tuple[str, ...]] | None = None, force_whitespace: str | bool | None = None, _depth: int = 0, **kwargs, ) -> type[Matcher]: """注册一个消息事件响应器,并且当消息以指定命令开头时响应。 命令匹配规则参考: `命令形式匹配 `_ 参数: cmd: 指定命令内容 rule: 事件响应规则 aliases: 命令别名 force_whitespace: 是否强制命令后必须有指定空白符 permission: 事件响应权限 handlers: 事件处理函数列表 temp: 是否为临时事件响应器(仅执行一次) expire_time: 事件响应器最终有效时间点,过时即被删除 priority: 事件响应器优先级 block: 是否阻止事件向更低优先级传递 state: 默认 state """ commands = {cmd} | (aliases or set()) kwargs.setdefault("block", False) return on_message( command(*commands, force_whitespace=force_whitespace) & rule, **kwargs, _depth=_depth + 1, ) def on_shell_command( cmd: str | tuple[str, ...], rule: Rule | T_RuleChecker | None = None, aliases: set[str | tuple[str, ...]] | None = None, parser: ArgumentParser | None = None, _depth: int = 0, **kwargs, ) -> type[Matcher]: """注册一个支持 `shell_like` 解析参数的命令消息事件响应器。 与普通的 `on_command` 不同的是,在添加 `parser` 参数时, 响应器会自动处理消息。 可以通过 {ref}`nonebot.params.ShellCommandArgv` 获取原始参数列表, 通过 {ref}`nonebot.params.ShellCommandArgs` 获取解析后的参数字典。 参数: cmd: 指定命令内容 rule: 事件响应规则 aliases: 命令别名 parser: `nonebot.rule.ArgumentParser` 对象 permission: 事件响应权限 handlers: 事件处理函数列表 temp: 是否为临时事件响应器(仅执行一次) expire_time: 事件响应器最终有效时间点,过时即被删除 priority: 事件响应器优先级 block: 是否阻止事件向更低优先级传递 state: 默认 state """ commands = {cmd} | (aliases or set()) return on_message( shell_command(*commands, parser=parser) & rule, **kwargs, _depth=_depth + 1, ) def on_regex( pattern: str, flags: int | re.RegexFlag = 0, rule: Rule | T_RuleChecker | None = None, _depth: int = 0, **kwargs, ) -> type[Matcher]: """注册一个消息事件响应器,并且当消息匹配正则表达式时响应。 命令匹配规则参考: `正则匹配 `_ 参数: pattern: 正则表达式 flags: 正则匹配标志 rule: 事件响应规则 permission: 事件响应权限 handlers: 事件处理函数列表 temp: 是否为临时事件响应器(仅执行一次) expire_time: 事件响应器最终有效时间点,过时即被删除 priority: 事件响应器优先级 block: 是否阻止事件向更低优先级传递 state: 默认 state """ return on_message(regex(pattern, flags) & rule, **kwargs, _depth=_depth + 1) def on_type( types: type[Event] | tuple[type[Event], ...], rule: Rule | T_RuleChecker | None = None, *, _depth: int = 0, **kwargs, ) -> type[Matcher]: """注册一个事件响应器,并且当事件为指定类型时响应。 参数: types: 事件类型 rule: 事件响应规则 permission: 事件响应权限 handlers: 事件处理函数列表 temp: 是否为临时事件响应器(仅执行一次) expire_time: 事件响应器最终有效时间点,过时即被删除 priority: 事件响应器优先级 block: 是否阻止事件向更低优先级传递 state: 默认 state """ event_types = types if isinstance(types, tuple) else (types,) return on(rule=is_type(*event_types) & rule, **kwargs, _depth=_depth + 1) class _Group: def __init__(self, **kwargs): """创建一个事件响应器组合,参数为默认值,与 `on` 一致""" self.matchers: list[type[Matcher]] = [] """组内事件响应器列表""" self.base_kwargs: dict[str, Any] = kwargs """其他传递给 `on` 的参数默认值""" def _get_final_kwargs( self, update: dict[str, Any], *, exclude: set[str] | None = None ) -> dict[str, Any]: """获取最终传递给 `on` 的参数 参数: update: 更新的关键字参数 exclude: 需要排除的参数 """ final_kwargs = self.base_kwargs.copy() final_kwargs.update(update) if exclude: for key in exclude: final_kwargs.pop(key, None) final_kwargs["_depth"] = 1 return final_kwargs class CommandGroup(_Group): """命令组,用于声明一组有相同名称前缀的命令。 参数: cmd: 指定命令内容 prefix_aliases: 是否影响命令别名,给命令别名加前缀 rule: 事件响应规则 permission: 事件响应权限 handlers: 事件处理函数列表 temp: 是否为临时事件响应器(仅执行一次) expire_time: 事件响应器最终有效时间点,过时即被删除 priority: 事件响应器优先级 block: 是否阻止事件向更低优先级传递 state: 默认 state """ def __init__( self, cmd: str | tuple[str, ...], prefix_aliases: bool = False, **kwargs ): """命令前缀""" super().__init__(**kwargs) self.basecmd: tuple[str, ...] = (cmd,) if isinstance(cmd, str) else cmd self.base_kwargs.pop("aliases", None) self.prefix_aliases = prefix_aliases def __repr__(self) -> str: return f"CommandGroup(cmd={self.basecmd}, matchers={len(self.matchers)})" def command(self, cmd: str | tuple[str, ...], **kwargs) -> type[Matcher]: """注册一个新的命令。新参数将会覆盖命令组默认值 参数: cmd: 指定命令内容 aliases: 命令别名 force_whitespace: 是否强制命令后必须有指定空白符 rule: 事件响应规则 permission: 事件响应权限 handlers: 事件处理函数列表 temp: 是否为临时事件响应器(仅执行一次) expire_time: 事件响应器最终有效时间点,过时即被删除 priority: 事件响应器优先级 block: 是否阻止事件向更低优先级传递 state: 默认 state """ sub_cmd = (cmd,) if isinstance(cmd, str) else cmd cmd = self.basecmd + sub_cmd if self.prefix_aliases and (aliases := kwargs.get("aliases")): kwargs["aliases"] = { self.basecmd + ((alias,) if isinstance(alias, str) else alias) for alias in aliases } matcher = on_command(cmd, **self._get_final_kwargs(kwargs)) self.matchers.append(matcher) return matcher def shell_command(self, cmd: str | tuple[str, ...], **kwargs) -> type[Matcher]: """注册一个新的 `shell_like` 命令。新参数将会覆盖命令组默认值 参数: cmd: 指定命令内容 rule: 事件响应规则 aliases: 命令别名 parser: `nonebot.rule.ArgumentParser` 对象 permission: 事件响应权限 handlers: 事件处理函数列表 temp: 是否为临时事件响应器(仅执行一次) expire_time: 事件响应器最终有效时间点,过时即被删除 priority: 事件响应器优先级 block: 是否阻止事件向更低优先级传递 state: 默认 state """ sub_cmd = (cmd,) if isinstance(cmd, str) else cmd cmd = self.basecmd + sub_cmd if self.prefix_aliases and (aliases := kwargs.get("aliases")): kwargs["aliases"] = { self.basecmd + ((alias,) if isinstance(alias, str) else alias) for alias in aliases } matcher = on_shell_command(cmd, **self._get_final_kwargs(kwargs)) self.matchers.append(matcher) return matcher class MatcherGroup(_Group): """事件响应器组合,统一管理。为 `Matcher` 创建提供默认属性。""" def __repr__(self) -> str: return f"MatcherGroup(matchers={len(self.matchers)})" def on(self, **kwargs) -> type[Matcher]: """注册一个基础事件响应器,可自定义类型。 参数: type: 事件响应器类型 rule: 事件响应规则 permission: 事件响应权限 handlers: 事件处理函数列表 temp: 是否为临时事件响应器(仅执行一次) expire_time: 事件响应器最终有效时间点,过时即被删除 priority: 事件响应器优先级 block: 是否阻止事件向更低优先级传递 state: 默认 state """ matcher = on(**self._get_final_kwargs(kwargs)) self.matchers.append(matcher) return matcher def on_metaevent(self, **kwargs) -> type[Matcher]: """注册一个元事件响应器。 参数: rule: 事件响应规则 permission: 事件响应权限 handlers: 事件处理函数列表 temp: 是否为临时事件响应器(仅执行一次) expire_time: 事件响应器最终有效时间点,过时即被删除 priority: 事件响应器优先级 block: 是否阻止事件向更低优先级传递 state: 默认 state """ final_kwargs = self._get_final_kwargs(kwargs, exclude={"type", "permission"}) matcher = on_metaevent(**final_kwargs) self.matchers.append(matcher) return matcher def on_message(self, **kwargs) -> type[Matcher]: """注册一个消息事件响应器。 参数: rule: 事件响应规则 permission: 事件响应权限 handlers: 事件处理函数列表 temp: 是否为临时事件响应器(仅执行一次) expire_time: 事件响应器最终有效时间点,过时即被删除 priority: 事件响应器优先级 block: 是否阻止事件向更低优先级传递 state: 默认 state """ final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"}) matcher = on_message(**final_kwargs) self.matchers.append(matcher) return matcher def on_notice(self, **kwargs) -> type[Matcher]: """注册一个通知事件响应器。 参数: rule: 事件响应规则 permission: 事件响应权限 handlers: 事件处理函数列表 temp: 是否为临时事件响应器(仅执行一次) expire_time: 事件响应器最终有效时间点,过时即被删除 priority: 事件响应器优先级 block: 是否阻止事件向更低优先级传递 state: 默认 state """ final_kwargs = self._get_final_kwargs(kwargs, exclude={"type", "permission"}) matcher = on_notice(**final_kwargs) self.matchers.append(matcher) return matcher def on_request(self, **kwargs) -> type[Matcher]: """注册一个请求事件响应器。 参数: rule: 事件响应规则 permission: 事件响应权限 handlers: 事件处理函数列表 temp: 是否为临时事件响应器(仅执行一次) expire_time: 事件响应器最终有效时间点,过时即被删除 priority: 事件响应器优先级 block: 是否阻止事件向更低优先级传递 state: 默认 state """ final_kwargs = self._get_final_kwargs(kwargs, exclude={"type", "permission"}) matcher = on_request(**final_kwargs) self.matchers.append(matcher) return matcher def on_startswith(self, msg: str | tuple[str, ...], **kwargs) -> type[Matcher]: """注册一个消息事件响应器,并且当消息的**文本部分**以指定内容开头时响应。 参数: msg: 指定消息开头内容 ignorecase: 是否忽略大小写 rule: 事件响应规则 permission: 事件响应权限 handlers: 事件处理函数列表 temp: 是否为临时事件响应器(仅执行一次) expire_time: 事件响应器最终有效时间点,过时即被删除 priority: 事件响应器优先级 block: 是否阻止事件向更低优先级传递 state: 默认 state """ final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"}) matcher = on_startswith(msg, **final_kwargs) self.matchers.append(matcher) return matcher def on_endswith(self, msg: str | tuple[str, ...], **kwargs) -> type[Matcher]: """注册一个消息事件响应器,并且当消息的**文本部分**以指定内容结尾时响应。 参数: msg: 指定消息结尾内容 ignorecase: 是否忽略大小写 rule: 事件响应规则 permission: 事件响应权限 handlers: 事件处理函数列表 temp: 是否为临时事件响应器(仅执行一次) expire_time: 事件响应器最终有效时间点,过时即被删除 priority: 事件响应器优先级 block: 是否阻止事件向更低优先级传递 state: 默认 state """ final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"}) matcher = on_endswith(msg, **final_kwargs) self.matchers.append(matcher) return matcher def on_fullmatch(self, msg: str | tuple[str, ...], **kwargs) -> type[Matcher]: """注册一个消息事件响应器,并且当消息的**文本部分**与指定内容完全一致时响应。 参数: msg: 指定消息全匹配内容 rule: 事件响应规则 ignorecase: 是否忽略大小写 permission: 事件响应权限 handlers: 事件处理函数列表 temp: 是否为临时事件响应器(仅执行一次) expire_time: 事件响应器最终有效时间点,过时即被删除 priority: 事件响应器优先级 block: 是否阻止事件向更低优先级传递 state: 默认 state """ final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"}) matcher = on_fullmatch(msg, **final_kwargs) self.matchers.append(matcher) return matcher def on_keyword(self, keywords: set[str], **kwargs) -> type[Matcher]: """注册一个消息事件响应器,并且当消息纯文本部分包含关键词时响应。 参数: keywords: 关键词列表 rule: 事件响应规则 permission: 事件响应权限 handlers: 事件处理函数列表 temp: 是否为临时事件响应器(仅执行一次) expire_time: 事件响应器最终有效时间点,过时即被删除 priority: 事件响应器优先级 block: 是否阻止事件向更低优先级传递 state: 默认 state """ final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"}) matcher = on_keyword(keywords, **final_kwargs) self.matchers.append(matcher) return matcher def on_command( self, cmd: str | tuple[str, ...], aliases: set[str | tuple[str, ...]] | None = None, force_whitespace: str | bool | None = None, **kwargs, ) -> type[Matcher]: """注册一个消息事件响应器,并且当消息以指定命令开头时响应。 命令匹配规则参考: `命令形式匹配 `_ 参数: cmd: 指定命令内容 aliases: 命令别名 force_whitespace: 是否强制命令后必须有指定空白符 rule: 事件响应规则 permission: 事件响应权限 handlers: 事件处理函数列表 temp: 是否为临时事件响应器(仅执行一次) expire_time: 事件响应器最终有效时间点,过时即被删除 priority: 事件响应器优先级 block: 是否阻止事件向更低优先级传递 state: 默认 state """ final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"}) matcher = on_command( cmd, aliases=aliases, force_whitespace=force_whitespace, **final_kwargs ) self.matchers.append(matcher) return matcher def on_shell_command( self, cmd: str | tuple[str, ...], aliases: set[str | tuple[str, ...]] | None = None, parser: ArgumentParser | None = None, **kwargs, ) -> type[Matcher]: """注册一个支持 `shell_like` 解析参数的命令消息事件响应器。 与普通的 `on_command` 不同的是,在添加 `parser` 参数时, 响应器会自动处理消息。 可以通过 {ref}`nonebot.params.ShellCommandArgv` 获取原始参数列表, 通过 {ref}`nonebot.params.ShellCommandArgs` 获取解析后的参数字典。 参数: cmd: 指定命令内容 aliases: 命令别名 parser: `nonebot.rule.ArgumentParser` 对象 rule: 事件响应规则 permission: 事件响应权限 handlers: 事件处理函数列表 temp: 是否为临时事件响应器(仅执行一次) expire_time: 事件响应器最终有效时间点,过时即被删除 priority: 事件响应器优先级 block: 是否阻止事件向更低优先级传递 state: 默认 state """ final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"}) matcher = on_shell_command(cmd, aliases=aliases, parser=parser, **final_kwargs) self.matchers.append(matcher) return matcher def on_regex( self, pattern: str, flags: int | re.RegexFlag = 0, **kwargs ) -> type[Matcher]: """注册一个消息事件响应器,并且当消息匹配正则表达式时响应。 命令匹配规则参考: `正则匹配 `_ 参数: pattern: 正则表达式 flags: 正则匹配标志 rule: 事件响应规则 permission: 事件响应权限 handlers: 事件处理函数列表 temp: 是否为临时事件响应器(仅执行一次) expire_time: 事件响应器最终有效时间点,过时即被删除 priority: 事件响应器优先级 block: 是否阻止事件向更低优先级传递 state: 默认 state """ final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"}) matcher = on_regex(pattern, flags=flags, **final_kwargs) self.matchers.append(matcher) return matcher def on_type( self, types: type[Event] | tuple[type[Event]], **kwargs ) -> type[Matcher]: """注册一个事件响应器,并且当事件为指定类型时响应。 参数: types: 事件类型 rule: 事件响应规则 permission: 事件响应权限 handlers: 事件处理函数列表 temp: 是否为临时事件响应器(仅执行一次) expire_time: 事件响应器最终有效时间点,过时即被删除 priority: 事件响应器优先级 block: 是否阻止事件向更低优先级传递 state: 默认 state """ final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"}) matcher = on_type(types, **final_kwargs) self.matchers.append(matcher) return matcher ================================================ FILE: nonebot/plugin/on.pyi ================================================ from datetime import datetime, timedelta import re from types import ModuleType from typing import Any from nonebot.adapters import Event from nonebot.dependencies import Dependent from nonebot.matcher import Matcher, MatcherSource from nonebot.permission import Permission from nonebot.rule import ArgumentParser, Rule from nonebot.typing import T_Handler, T_PermissionChecker, T_RuleChecker, T_State from .model import Plugin def store_matcher(matcher: type[Matcher]) -> None: ... def get_matcher_plugin(depth: int = ...) -> Plugin | None: ... def get_matcher_module(depth: int = ...) -> ModuleType | None: ... def get_matcher_source(depth: int = ...) -> MatcherSource | None: ... def on( type: str = "", rule: Rule | T_RuleChecker | None = ..., permission: Permission | T_PermissionChecker | None = ..., *, handlers: list[T_Handler | Dependent[Any]] | None = ..., temp: bool = ..., expire_time: datetime | timedelta | None = ..., priority: int = ..., block: bool = ..., state: T_State | None = ..., ) -> type[Matcher]: ... def on_metaevent( rule: Rule | T_RuleChecker | None = ..., permission: Permission | T_PermissionChecker | None = ..., *, handlers: list[T_Handler | Dependent[Any]] | None = ..., temp: bool = ..., expire_time: datetime | timedelta | None = ..., priority: int = ..., block: bool = ..., state: T_State | None = ..., ) -> type[Matcher]: ... def on_message( rule: Rule | T_RuleChecker | None = ..., permission: Permission | T_PermissionChecker | None = ..., *, handlers: list[T_Handler | Dependent[Any]] | None = ..., temp: bool = ..., expire_time: datetime | timedelta | None = ..., priority: int = ..., block: bool = ..., state: T_State | None = ..., ) -> type[Matcher]: ... def on_notice( rule: Rule | T_RuleChecker | None = ..., permission: Permission | T_PermissionChecker | None = ..., *, handlers: list[T_Handler | Dependent[Any]] | None = ..., temp: bool = ..., expire_time: datetime | timedelta | None = ..., priority: int = ..., block: bool = ..., state: T_State | None = ..., ) -> type[Matcher]: ... def on_request( rule: Rule | T_RuleChecker | None = ..., permission: Permission | T_PermissionChecker | None = ..., *, handlers: list[T_Handler | Dependent[Any]] | None = ..., temp: bool = ..., expire_time: datetime | timedelta | None = ..., priority: int = ..., block: bool = ..., state: T_State | None = ..., ) -> type[Matcher]: ... def on_startswith( msg: str | tuple[str, ...], rule: Rule | T_RuleChecker | None = ..., ignorecase: bool = ..., *, permission: Permission | T_PermissionChecker | None = ..., handlers: list[T_Handler | Dependent[Any]] | None = ..., temp: bool = ..., expire_time: datetime | timedelta | None = ..., priority: int = ..., block: bool = ..., state: T_State | None = ..., ) -> type[Matcher]: ... def on_endswith( msg: str | tuple[str, ...], rule: Rule | T_RuleChecker | None = ..., ignorecase: bool = ..., *, permission: Permission | T_PermissionChecker | None = ..., handlers: list[T_Handler | Dependent[Any]] | None = ..., temp: bool = ..., expire_time: datetime | timedelta | None = ..., priority: int = ..., block: bool = ..., state: T_State | None = ..., ) -> type[Matcher]: ... def on_fullmatch( msg: str | tuple[str, ...], rule: Rule | T_RuleChecker | None = ..., ignorecase: bool = ..., *, permission: Permission | T_PermissionChecker | None = ..., handlers: list[T_Handler | Dependent[Any]] | None = ..., temp: bool = ..., expire_time: datetime | timedelta | None = ..., priority: int = ..., block: bool = ..., state: T_State | None = ..., ) -> type[Matcher]: ... def on_keyword( keywords: set[str], rule: Rule | T_RuleChecker | None = ..., *, permission: Permission | T_PermissionChecker | None = ..., handlers: list[T_Handler | Dependent[Any]] | None = ..., temp: bool = ..., expire_time: datetime | timedelta | None = ..., priority: int = ..., block: bool = ..., state: T_State | None = ..., ) -> type[Matcher]: ... def on_command( cmd: str | tuple[str, ...], rule: Rule | T_RuleChecker | None = ..., aliases: set[str | tuple[str, ...]] | None = ..., force_whitespace: str | bool | None = ..., *, permission: Permission | T_PermissionChecker | None = ..., handlers: list[T_Handler | Dependent[Any]] | None = ..., temp: bool = ..., expire_time: datetime | timedelta | None = ..., priority: int = ..., block: bool = ..., state: T_State | None = ..., ) -> type[Matcher]: ... def on_shell_command( cmd: str | tuple[str, ...], rule: Rule | T_RuleChecker | None = ..., aliases: set[str | tuple[str, ...]] | None = ..., parser: ArgumentParser | None = ..., *, permission: Permission | T_PermissionChecker | None = ..., handlers: list[T_Handler | Dependent[Any]] | None = ..., temp: bool = ..., expire_time: datetime | timedelta | None = ..., priority: int = ..., block: bool = ..., state: T_State | None = ..., ) -> type[Matcher]: ... def on_regex( pattern: str, flags: int | re.RegexFlag = ..., rule: Rule | T_RuleChecker | None = ..., *, permission: Permission | T_PermissionChecker | None = ..., handlers: list[T_Handler | Dependent[Any]] | None = ..., temp: bool = ..., expire_time: datetime | timedelta | None = ..., priority: int = ..., block: bool = ..., state: T_State | None = ..., ) -> type[Matcher]: ... def on_type( types: type[Event] | tuple[type[Event], ...], rule: Rule | T_RuleChecker | None = ..., *, permission: Permission | T_PermissionChecker | None = ..., handlers: list[T_Handler | Dependent[Any]] | None = ..., temp: bool = ..., expire_time: datetime | timedelta | None = ..., priority: int = ..., block: bool = ..., state: T_State | None = ..., ) -> type[Matcher]: ... class _Group: matchers: list[type[Matcher]] = ... base_kwargs: dict[str, Any] = ... def _get_final_kwargs( self, update: dict[str, Any], *, exclude: set[str] | None = None ) -> dict[str, Any]: ... class CommandGroup(_Group): basecmd: tuple[str, ...] = ... prefix_aliases: bool = ... def __init__( self, cmd: str | tuple[str, ...], prefix_aliases: bool = ..., *, rule: Rule | T_RuleChecker | None = ..., permission: Permission | T_PermissionChecker | None = ..., handlers: list[T_Handler | Dependent[Any]] | None = ..., temp: bool = ..., expire_time: datetime | timedelta | None = ..., priority: int = ..., block: bool = ..., state: T_State | None = ..., ): ... def command( self, cmd: str | tuple[str, ...], *, rule: Rule | T_RuleChecker | None = ..., aliases: set[str | tuple[str, ...]] | None = ..., force_whitespace: str | bool | None = ..., permission: Permission | T_PermissionChecker | None = ..., handlers: list[T_Handler | Dependent[Any]] | None = ..., temp: bool = ..., expire_time: datetime | timedelta | None = ..., priority: int = ..., block: bool = ..., state: T_State | None = ..., ) -> type[Matcher]: ... def shell_command( self, cmd: str | tuple[str, ...], *, rule: Rule | T_RuleChecker | None = ..., aliases: set[str | tuple[str, ...]] | None = ..., parser: ArgumentParser | None = ..., permission: Permission | T_PermissionChecker | None = ..., handlers: list[T_Handler | Dependent[Any]] | None = ..., temp: bool = ..., expire_time: datetime | timedelta | None = ..., priority: int = ..., block: bool = ..., state: T_State | None = ..., ) -> type[Matcher]: ... class MatcherGroup(_Group): def __init__( self, *, type: str = ..., rule: Rule | T_RuleChecker | None = ..., permission: Permission | T_PermissionChecker | None = ..., handlers: list[T_Handler | Dependent[Any]] | None = ..., temp: bool = ..., expire_time: datetime | timedelta | None = ..., priority: int = ..., block: bool = ..., state: T_State | None = ..., ): ... def on( self, *, type: str = ..., rule: Rule | T_RuleChecker | None = ..., permission: Permission | T_PermissionChecker | None = ..., handlers: list[T_Handler | Dependent[Any]] | None = ..., temp: bool = ..., expire_time: datetime | timedelta | None = ..., priority: int = ..., block: bool = ..., state: T_State | None = ..., ) -> type[Matcher]: ... def on_metaevent( self, *, rule: Rule | T_RuleChecker | None = ..., permission: Permission | T_PermissionChecker | None = ..., handlers: list[T_Handler | Dependent[Any]] | None = ..., temp: bool = ..., expire_time: datetime | timedelta | None = ..., priority: int = ..., block: bool = ..., state: T_State | None = ..., ) -> type[Matcher]: ... def on_message( self, *, rule: Rule | T_RuleChecker | None = ..., permission: Permission | T_PermissionChecker | None = ..., handlers: list[T_Handler | Dependent[Any]] | None = ..., temp: bool = ..., expire_time: datetime | timedelta | None = ..., priority: int = ..., block: bool = ..., state: T_State | None = ..., ) -> type[Matcher]: ... def on_notice( self, *, rule: Rule | T_RuleChecker | None = ..., permission: Permission | T_PermissionChecker | None = ..., handlers: list[T_Handler | Dependent[Any]] | None = ..., temp: bool = ..., expire_time: datetime | timedelta | None = ..., priority: int = ..., block: bool = ..., state: T_State | None = ..., ) -> type[Matcher]: ... def on_request( self, *, rule: Rule | T_RuleChecker | None = ..., permission: Permission | T_PermissionChecker | None = ..., handlers: list[T_Handler | Dependent[Any]] | None = ..., temp: bool = ..., expire_time: datetime | timedelta | None = ..., priority: int = ..., block: bool = ..., state: T_State | None = ..., ) -> type[Matcher]: ... def on_startswith( self, msg: str | tuple[str, ...], *, ignorecase: bool = ..., rule: Rule | T_RuleChecker | None = ..., permission: Permission | T_PermissionChecker | None = ..., handlers: list[T_Handler | Dependent[Any]] | None = ..., temp: bool = ..., expire_time: datetime | timedelta | None = ..., priority: int = ..., block: bool = ..., state: T_State | None = ..., ) -> type[Matcher]: ... def on_endswith( self, msg: str | tuple[str, ...], *, ignorecase: bool = ..., rule: Rule | T_RuleChecker | None = ..., permission: Permission | T_PermissionChecker | None = ..., handlers: list[T_Handler | Dependent[Any]] | None = ..., temp: bool = ..., expire_time: datetime | timedelta | None = ..., priority: int = ..., block: bool = ..., state: T_State | None = ..., ) -> type[Matcher]: ... def on_fullmatch( self, msg: str | tuple[str, ...], *, ignorecase: bool = ..., rule: Rule | T_RuleChecker | None = ..., permission: Permission | T_PermissionChecker | None = ..., handlers: list[T_Handler | Dependent[Any]] | None = ..., temp: bool = ..., expire_time: datetime | timedelta | None = ..., priority: int = ..., block: bool = ..., state: T_State | None = ..., ) -> type[Matcher]: ... def on_keyword( self, keywords: set[str], *, rule: Rule | T_RuleChecker | None = ..., permission: Permission | T_PermissionChecker | None = ..., handlers: list[T_Handler | Dependent[Any]] | None = ..., temp: bool = ..., expire_time: datetime | timedelta | None = ..., priority: int = ..., block: bool = ..., state: T_State | None = ..., ) -> type[Matcher]: ... def on_command( self, cmd: str | tuple[str, ...], aliases: set[str | tuple[str, ...]] | None = ..., force_whitespace: str | bool | None = ..., *, rule: Rule | T_RuleChecker | None = ..., permission: Permission | T_PermissionChecker | None = ..., handlers: list[T_Handler | Dependent[Any]] | None = ..., temp: bool = ..., expire_time: datetime | timedelta | None = ..., priority: int = ..., block: bool = ..., state: T_State | None = ..., ) -> type[Matcher]: ... def on_shell_command( self, cmd: str | tuple[str, ...], aliases: set[str | tuple[str, ...]] | None = ..., parser: ArgumentParser | None = ..., *, rule: Rule | T_RuleChecker | None = ..., permission: Permission | T_PermissionChecker | None = ..., handlers: list[T_Handler | Dependent[Any]] | None = ..., temp: bool = ..., expire_time: datetime | timedelta | None = ..., priority: int = ..., block: bool = ..., state: T_State | None = ..., ) -> type[Matcher]: ... def on_regex( self, pattern: str, flags: int | re.RegexFlag = ..., *, rule: Rule | T_RuleChecker | None = ..., permission: Permission | T_PermissionChecker | None = ..., handlers: list[T_Handler | Dependent[Any]] | None = ..., temp: bool = ..., expire_time: datetime | timedelta | None = ..., priority: int = ..., block: bool = ..., state: T_State | None = ..., ) -> type[Matcher]: ... def on_type( self, types: type[Event] | tuple[type[Event]], *, rule: Rule | T_RuleChecker | None = ..., permission: Permission | T_PermissionChecker | None = ..., handlers: list[T_Handler | Dependent[Any]] | None = ..., temp: bool = ..., expire_time: datetime | timedelta | None = ..., priority: int = ..., block: bool = ..., state: T_State | None = ..., ) -> type[Matcher]: ... ================================================ FILE: nonebot/plugins/echo.py ================================================ from nonebot import on_command from nonebot.adapters import Message from nonebot.params import CommandArg from nonebot.plugin import PluginMetadata from nonebot.rule import to_me __plugin_meta__ = PluginMetadata( name="echo", description="重复你说的话", usage="/echo [text]", type="application", homepage="https://github.com/nonebot/nonebot2/blob/master/nonebot/plugins/echo.py", config=None, supported_adapters=None, ) echo = on_command("echo", to_me()) @echo.handle() async def handle_echo(message: Message = CommandArg()): if any((not seg.is_text()) or str(seg) for seg in message): await echo.send(message=message) ================================================ FILE: nonebot/plugins/single_session.py ================================================ from collections.abc import AsyncGenerator from nonebot.adapters import Event from nonebot.message import IgnoredException, event_preprocessor from nonebot.params import Depends from nonebot.plugin import PluginMetadata __plugin_meta__ = PluginMetadata( name="唯一会话", description="限制同一会话内同时只能运行一个响应器", usage="加载插件后自动生效", type="application", homepage="https://github.com/nonebot/nonebot2/blob/master/nonebot/plugins/single_session.py", config=None, supported_adapters=None, ) _running_matcher: dict[str, int] = {} async def matcher_mutex(event: Event) -> AsyncGenerator[bool, None]: result = False try: session_id = event.get_session_id() except Exception: yield result else: current_event_id = id(event) if event_id := _running_matcher.get(session_id): result = event_id != current_event_id else: _running_matcher[session_id] = current_event_id yield result if not result: del _running_matcher[session_id] @event_preprocessor async def preprocess(mutex: bool = Depends(matcher_mutex)): if mutex: raise IgnoredException("Another matcher running") ================================================ FILE: nonebot/py.typed ================================================ ================================================ FILE: nonebot/rule.py ================================================ """本模块是 {ref}`nonebot.matcher.Matcher.rule` 的类型定义。 每个{ref}`事件响应器 `拥有一个 {ref}`nonebot.rule.Rule`,其中是 `RuleChecker` 的集合。 只有当所有 `RuleChecker` 检查结果为 `True` 时继续运行。 FrontMatter: mdx: format: md sidebar_position: 5 description: nonebot.rule 模块 """ from argparse import Action, ArgumentError from argparse import ArgumentParser as ArgParser from argparse import Namespace as Namespace from collections.abc import Sequence from contextvars import ContextVar from gettext import gettext from itertools import chain, product import re import shlex from typing import ( IO, TYPE_CHECKING, NamedTuple, TypedDict, TypeVar, cast, overload, ) from pygtrie import CharTrie from nonebot import get_driver from nonebot.adapters import Bot, Event, Message, MessageSegment from nonebot.consts import ( CMD_ARG_KEY, CMD_KEY, CMD_START_KEY, CMD_WHITESPACE_KEY, ENDSWITH_KEY, FULLMATCH_KEY, KEYWORD_KEY, PREFIX_KEY, RAW_CMD_KEY, REGEX_MATCHED, SHELL_ARGS, SHELL_ARGV, STARTSWITH_KEY, ) from nonebot.exception import ParserExit from nonebot.internal.rule import Rule as Rule from nonebot.log import logger from nonebot.params import Command, CommandArg, CommandWhitespace, EventToMe from nonebot.typing import T_State T = TypeVar("T") class CMD_RESULT(TypedDict): command: tuple[str, ...] | None raw_command: str | None command_arg: Message | None command_start: str | None command_whitespace: str | None class TRIE_VALUE(NamedTuple): command_start: str command: tuple[str, ...] parser_message: ContextVar[str] = ContextVar("parser_message") class TrieRule: prefix: CharTrie = CharTrie() @classmethod def add_prefix(cls, prefix: str, value: TRIE_VALUE) -> None: if prefix in cls.prefix: logger.warning(f'Duplicated prefix rule "{prefix}"') return cls.prefix[prefix] = value @classmethod def get_value(cls, bot: Bot, event: Event, state: T_State) -> CMD_RESULT: prefix = CMD_RESULT( command=None, raw_command=None, command_arg=None, command_start=None, command_whitespace=None, ) state[PREFIX_KEY] = prefix if event.get_type() != "message": return prefix message = event.get_message() message_seg: MessageSegment = message[0] if message_seg.is_text(): segment_text = str(message_seg).lstrip() if pf := cls.prefix.longest_prefix(segment_text): value: TRIE_VALUE = pf.value prefix[RAW_CMD_KEY] = pf.key prefix[CMD_START_KEY] = value.command_start prefix[CMD_KEY] = value.command msg = message.copy() msg.pop(0) # check whitespace arg_str = segment_text[len(pf.key) :] arg_str_stripped = arg_str.lstrip() # check next segment until arg detected or no text remain while not arg_str_stripped and msg and msg[0].is_text(): arg_str += str(msg.pop(0)) arg_str_stripped = arg_str.lstrip() has_arg = arg_str_stripped or msg if ( has_arg and (stripped_len := len(arg_str) - len(arg_str_stripped)) > 0 ): prefix[CMD_WHITESPACE_KEY] = arg_str[:stripped_len] # construct command arg if arg_str_stripped: new_message = msg.__class__(arg_str_stripped) for new_segment in reversed(new_message): msg.insert(0, new_segment) prefix[CMD_ARG_KEY] = msg return prefix class StartswithRule: """检查消息纯文本是否以指定字符串开头。 参数: msg: 指定消息开头字符串元组 ignorecase: 是否忽略大小写 """ __slots__ = ("ignorecase", "msg") def __init__(self, msg: tuple[str, ...], ignorecase: bool = False): self.msg = msg self.ignorecase = ignorecase def __repr__(self) -> str: return f"Startswith(msg={self.msg}, ignorecase={self.ignorecase})" def __eq__(self, other: object) -> bool: return ( isinstance(other, StartswithRule) and frozenset(self.msg) == frozenset(other.msg) and self.ignorecase == other.ignorecase ) def __hash__(self) -> int: return hash((frozenset(self.msg), self.ignorecase)) async def __call__(self, event: Event, state: T_State) -> bool: try: text = event.get_plaintext() except Exception: return False if match := re.match( f"^(?:{'|'.join(re.escape(prefix) for prefix in self.msg)})", text, re.IGNORECASE if self.ignorecase else 0, ): state[STARTSWITH_KEY] = match.group() return True return False def startswith(msg: str | tuple[str, ...], ignorecase: bool = False) -> Rule: """匹配消息纯文本开头。 参数: msg: 指定消息开头字符串元组 ignorecase: 是否忽略大小写 """ if isinstance(msg, str): msg = (msg,) return Rule(StartswithRule(msg, ignorecase)) class EndswithRule: """检查消息纯文本是否以指定字符串结尾。 参数: msg: 指定消息结尾字符串元组 ignorecase: 是否忽略大小写 """ __slots__ = ("ignorecase", "msg") def __init__(self, msg: tuple[str, ...], ignorecase: bool = False): self.msg = msg self.ignorecase = ignorecase def __repr__(self) -> str: return f"Endswith(msg={self.msg}, ignorecase={self.ignorecase})" def __eq__(self, other: object) -> bool: return ( isinstance(other, EndswithRule) and frozenset(self.msg) == frozenset(other.msg) and self.ignorecase == other.ignorecase ) def __hash__(self) -> int: return hash((frozenset(self.msg), self.ignorecase)) async def __call__(self, event: Event, state: T_State) -> bool: try: text = event.get_plaintext() except Exception: return False if match := re.search( f"(?:{'|'.join(re.escape(suffix) for suffix in self.msg)})$", text, re.IGNORECASE if self.ignorecase else 0, ): state[ENDSWITH_KEY] = match.group() return True return False def endswith(msg: str | tuple[str, ...], ignorecase: bool = False) -> Rule: """匹配消息纯文本结尾。 参数: msg: 指定消息开头字符串元组 ignorecase: 是否忽略大小写 """ if isinstance(msg, str): msg = (msg,) return Rule(EndswithRule(msg, ignorecase)) class FullmatchRule: """检查消息纯文本是否与指定字符串全匹配。 参数: msg: 指定消息全匹配字符串元组 ignorecase: 是否忽略大小写 """ __slots__ = ("ignorecase", "msg") def __init__(self, msg: tuple[str, ...], ignorecase: bool = False): self.msg = tuple(map(str.casefold, msg) if ignorecase else msg) self.ignorecase = ignorecase def __repr__(self) -> str: return f"Fullmatch(msg={self.msg}, ignorecase={self.ignorecase})" def __eq__(self, other: object) -> bool: return ( isinstance(other, FullmatchRule) and frozenset(self.msg) == frozenset(other.msg) and self.ignorecase == other.ignorecase ) def __hash__(self) -> int: return hash((frozenset(self.msg), self.ignorecase)) async def __call__(self, event: Event, state: T_State) -> bool: try: text = event.get_plaintext() except Exception: return False if not text: return False text = text.casefold() if self.ignorecase else text if text in self.msg: state[FULLMATCH_KEY] = text return True return False def fullmatch(msg: str | tuple[str, ...], ignorecase: bool = False) -> Rule: """完全匹配消息。 参数: msg: 指定消息全匹配字符串元组 ignorecase: 是否忽略大小写 """ if isinstance(msg, str): msg = (msg,) return Rule(FullmatchRule(msg, ignorecase)) class KeywordsRule: """检查消息纯文本是否包含指定关键字。 参数: keywords: 指定关键字元组 """ __slots__ = ("keywords",) def __init__(self, *keywords: str): self.keywords = keywords def __repr__(self) -> str: return f"Keywords(keywords={self.keywords})" def __eq__(self, other: object) -> bool: return isinstance(other, KeywordsRule) and frozenset( self.keywords ) == frozenset(other.keywords) def __hash__(self) -> int: return hash(frozenset(self.keywords)) async def __call__(self, event: Event, state: T_State) -> bool: try: text = event.get_plaintext() except Exception: return False if not text: return False if key := next((k for k in self.keywords if k in text), None): state[KEYWORD_KEY] = key return True return False def keyword(*keywords: str) -> Rule: """匹配消息纯文本关键词。 参数: keywords: 指定关键字元组 """ return Rule(KeywordsRule(*keywords)) class CommandRule: """检查消息是否为指定命令。 参数: cmds: 指定命令元组列表 force_whitespace: 是否强制命令后必须有指定空白符 """ __slots__ = ("cmds", "force_whitespace") def __init__( self, cmds: list[tuple[str, ...]], force_whitespace: str | bool | None = None, ): self.cmds = tuple(cmds) self.force_whitespace = force_whitespace def __repr__(self) -> str: return f"Command(cmds={self.cmds})" def __eq__(self, other: object) -> bool: return isinstance(other, CommandRule) and frozenset(self.cmds) == frozenset( other.cmds ) def __hash__(self) -> int: return hash((frozenset(self.cmds),)) async def __call__( self, cmd: tuple[str, ...] | None = Command(), cmd_arg: Message | None = CommandArg(), cmd_whitespace: str | None = CommandWhitespace(), ) -> bool: if cmd not in self.cmds: return False if self.force_whitespace is None or not cmd_arg: return True if isinstance(self.force_whitespace, str): return self.force_whitespace == cmd_whitespace return self.force_whitespace == (cmd_whitespace is not None) def command( *cmds: str | tuple[str, ...], force_whitespace: str | bool | None = None, ) -> Rule: """匹配消息命令。 根据配置里提供的 {ref}``command_start` `, {ref}``command_sep` ` 判断消息是否为命令。 可以通过 {ref}`nonebot.params.Command` 获取匹配成功的命令(例: `("test",)`), 通过 {ref}`nonebot.params.RawCommand` 获取匹配成功的原始命令文本(例: `"/test"`), 通过 {ref}`nonebot.params.CommandArg` 获取匹配成功的命令参数。 参数: cmds: 命令文本或命令元组 force_whitespace: 是否强制命令后必须有指定空白符 用法: 使用默认 `command_start`, `command_sep` 配置情况下: 命令 `("test",)` 可以匹配: `/test` 开头的消息 命令 `("test", "sub")` 可以匹配: `/test.sub` 开头的消息 :::tip 提示 命令内容与后续消息间无需空格! ::: """ config = get_driver().config command_start = config.command_start command_sep = config.command_sep commands: list[tuple[str, ...]] = [] for command in cmds: if isinstance(command, str): command = (command,) commands.append(command) if len(command) == 1: for start in command_start: TrieRule.add_prefix(f"{start}{command[0]}", TRIE_VALUE(start, command)) else: for start, sep in product(command_start, command_sep): TrieRule.add_prefix( f"{start}{sep.join(command)}", TRIE_VALUE(start, command) ) return Rule(CommandRule(commands, force_whitespace)) class ArgumentParser(ArgParser): """`shell_like` 命令参数解析器,解析出错时不会退出程序。 支持 {ref}`nonebot.adapters.Message` 富文本解析。 用法: 用法与 `argparse.ArgumentParser` 相同, 参考文档: [argparse](https://docs.python.org/3/library/argparse.html) """ if TYPE_CHECKING: @overload def parse_known_args( self, args: Sequence[str | MessageSegment] | None = None, namespace: None = None, ) -> tuple[Namespace, list[str | MessageSegment]]: ... @overload def parse_known_args( self, args: Sequence[str | MessageSegment] | None, namespace: T ) -> tuple[T, list[str | MessageSegment]]: ... @overload def parse_known_args( self, *, namespace: T ) -> tuple[T, list[str | MessageSegment]]: ... def parse_known_args( # pyright: ignore[reportIncompatibleMethodOverride] self, args: Sequence[str | MessageSegment] | None = None, namespace: T | None = None, ) -> tuple[Namespace | T, list[str | MessageSegment]]: ... @overload def parse_args( self, args: Sequence[str | MessageSegment] | None = None, namespace: None = None, ) -> Namespace: ... @overload def parse_args( self, args: Sequence[str | MessageSegment] | None, namespace: T ) -> T: ... @overload def parse_args(self, *, namespace: T) -> T: ... def parse_args( self, args: Sequence[str | MessageSegment] | None = None, namespace: T | None = None, ) -> Namespace | T: result, argv = self.parse_known_args(args, namespace) if argv: msg = gettext("unrecognized arguments: %s") self.error(msg % " ".join(map(str, argv))) return cast(Namespace | T, result) def _parse_optional( self, arg_string: str | MessageSegment ) -> tuple[Action | None, str, str | None] | None: return ( super()._parse_optional(arg_string) if isinstance(arg_string, str) else None ) def _print_message(self, message: str, file: IO[str] | None = None): # type: ignore if (msg := parser_message.get(None)) is not None: parser_message.set(msg + message) else: super()._print_message(message, file) def exit(self, status: int = 0, message: str | None = None): if message: self._print_message(message) raise ParserExit(status=status, message=parser_message.get(None)) class ShellCommandRule: """检查消息是否为指定 shell 命令。 参数: cmds: 指定命令元组列表 parser: 可选参数解析器 """ __slots__ = ("cmds", "parser") def __init__(self, cmds: list[tuple[str, ...]], parser: ArgumentParser | None): self.cmds = tuple(cmds) self.parser = parser def __repr__(self) -> str: return f"ShellCommand(cmds={self.cmds}, parser={self.parser})" def __eq__(self, other: object) -> bool: return ( isinstance(other, ShellCommandRule) and frozenset(self.cmds) == frozenset(other.cmds) and self.parser is other.parser ) def __hash__(self) -> int: return hash((frozenset(self.cmds), self.parser)) async def __call__( self, state: T_State, cmd: tuple[str, ...] | None = Command(), msg: Message | None = CommandArg(), ) -> bool: if cmd not in self.cmds or msg is None: return False try: state[SHELL_ARGV] = list( chain.from_iterable( shlex.split(str(seg)) if cast(MessageSegment, seg).is_text() else (seg,) for seg in msg ) ) except Exception as e: # set SHELL_ARGV to none indicating shlex error state[SHELL_ARGV] = None # ensure SHELL_ARGS is set to ParserExit if parser is provided if self.parser: state[SHELL_ARGS] = ParserExit(status=2, message=str(e)) return True if self.parser: t = parser_message.set("") try: args = self.parser.parse_args(state[SHELL_ARGV]) state[SHELL_ARGS] = args except ArgumentError as e: state[SHELL_ARGS] = ParserExit(status=2, message=str(e)) except ParserExit as e: state[SHELL_ARGS] = e finally: parser_message.reset(t) return True def shell_command( *cmds: str | tuple[str, ...], parser: ArgumentParser | None = None ) -> Rule: """匹配 `shell_like` 形式的消息命令。 根据配置里提供的 {ref}``command_start` `, {ref}``command_sep` ` 判断消息是否为命令。 可以通过 {ref}`nonebot.params.Command` 获取匹配成功的命令 (例: `("test",)`), 通过 {ref}`nonebot.params.RawCommand` 获取匹配成功的原始命令文本 (例: `"/test"`), 通过 {ref}`nonebot.params.ShellCommandArgv` 获取解析前的参数列表 (例: `["arg", "-h"]`), 通过 {ref}`nonebot.params.ShellCommandArgs` 获取解析后的参数字典 (例: `{"arg": "arg", "h": True}`)。 :::caution 警告 如果参数解析失败,则通过 {ref}`nonebot.params.ShellCommandArgs` 获取的将是 {ref}`nonebot.exception.ParserExit` 异常。 ::: 参数: cmds: 命令文本或命令元组 parser: {ref}`nonebot.rule.ArgumentParser` 对象 用法: 使用默认 `command_start`, `command_sep` 配置,更多示例参考 [argparse](https://docs.python.org/3/library/argparse.html) 标准库文档。 ```python from nonebot.rule import ArgumentParser parser = ArgumentParser() parser.add_argument("-a", action="store_true") rule = shell_command("ls", parser=parser) ``` :::tip 提示 命令内容与后续消息间无需空格! ::: """ if parser is not None and not isinstance(parser, ArgumentParser): raise TypeError("`parser` must be an instance of nonebot.rule.ArgumentParser") config = get_driver().config command_start = config.command_start command_sep = config.command_sep commands: list[tuple[str, ...]] = [] for command in cmds: if isinstance(command, str): command = (command,) commands.append(command) if len(command) == 1: for start in command_start: TrieRule.add_prefix(f"{start}{command[0]}", TRIE_VALUE(start, command)) else: for start, sep in product(command_start, command_sep): TrieRule.add_prefix( f"{start}{sep.join(command)}", TRIE_VALUE(start, command) ) return Rule(ShellCommandRule(commands, parser)) class RegexRule: """检查消息字符串是否符合指定正则表达式。 参数: regex: 正则表达式 flags: 正则表达式标记 """ __slots__ = ("flags", "regex") def __init__(self, regex: str, flags: int = 0): self.regex = regex self.flags = flags def __repr__(self) -> str: return f"Regex(regex={self.regex!r}, flags={self.flags})" def __eq__(self, other: object) -> bool: return ( isinstance(other, RegexRule) and self.regex == other.regex and self.flags == other.flags ) def __hash__(self) -> int: return hash((self.regex, self.flags)) async def __call__(self, event: Event, state: T_State) -> bool: try: msg = event.get_message() except Exception: return False if matched := re.search(self.regex, str(msg), self.flags): state[REGEX_MATCHED] = matched return True else: return False def regex(regex: str, flags: int | re.RegexFlag = 0) -> Rule: """匹配符合正则表达式的消息字符串。 可以通过 {ref}`nonebot.params.RegexStr` 获取匹配成功的字符串, 通过 {ref}`nonebot.params.RegexGroup` 获取匹配成功的 group 元组, 通过 {ref}`nonebot.params.RegexDict` 获取匹配成功的 group 字典。 参数: regex: 正则表达式 flags: 正则表达式标记 :::tip 提示 正则表达式匹配使用 search 而非 match,如需从头匹配请使用 `r"^xxx"` 来确保匹配开头 ::: :::tip 提示 正则表达式匹配使用 `EventMessage` 的 `str` 字符串, 而非 `EventMessage` 的 `PlainText` 纯文本字符串 ::: """ return Rule(RegexRule(regex, flags)) class ToMeRule: """检查事件是否与机器人有关。""" __slots__ = () def __repr__(self) -> str: return "ToMe()" def __eq__(self, other: object) -> bool: return isinstance(other, ToMeRule) def __hash__(self) -> int: return hash((self.__class__,)) async def __call__(self, to_me: bool = EventToMe()) -> bool: return to_me def to_me() -> Rule: """匹配与机器人有关的事件。""" return Rule(ToMeRule()) class IsTypeRule: """检查事件类型是否为指定类型。""" __slots__ = ("types",) def __init__(self, *types: type[Event]): self.types = types def __repr__(self) -> str: return f"IsType(types={tuple(type.__name__ for type in self.types)})" def __eq__(self, other: object) -> bool: return isinstance(other, IsTypeRule) and self.types == other.types def __hash__(self) -> int: return hash((self.types,)) async def __call__(self, event: Event) -> bool: return isinstance(event, self.types) def is_type(*types: type[Event]) -> Rule: """匹配事件类型。 参数: types: 事件类型 """ return Rule(IsTypeRule(*types)) __autodoc__ = { "Rule": True, "Rule.__call__": True, "TrieRule": False, "ArgumentParser.exit": False, "ArgumentParser.parse_args": False, } ================================================ FILE: nonebot/typing.py ================================================ """本模块定义了 NoneBot 模块中共享的一些类型。 使用 Python 的 Type Hint 语法, 参考 [`PEP 484`](https://www.python.org/dev/peps/pep-0484/), [`PEP 526`](https://www.python.org/dev/peps/pep-0526/) 和 [`typing`](https://docs.python.org/3/library/typing.html)。 FrontMatter: mdx: format: md sidebar_position: 11 description: nonebot.typing 模块 """ import sys import types import typing as t from typing import TYPE_CHECKING, TypeAlias, TypeVar, get_args, get_origin import typing_extensions as t_ext from typing_extensions import ParamSpec, override import warnings if TYPE_CHECKING: from nonebot.adapters import Bot from nonebot.internal.params import DependencyCache from nonebot.permission import Permission T = TypeVar("T") P = ParamSpec("P") T_Wrapped: TypeAlias = t.Callable[P, T] def overrides(InterfaceClass: object): """标记一个方法为父类 interface 的 implement""" warnings.warn( "overrides is deprecated and will be removed in a future version, " "use @typing_extensions.override instead. " "See [PEP 698](https://peps.python.org/pep-0698/) for more details.", DeprecationWarning, ) return override def type_has_args(type_: type[t.Any]) -> bool: return isinstance(type_, (t._GenericAlias, types.GenericAlias, types.UnionType)) # type: ignore def origin_is_union(origin: type[t.Any] | None) -> bool: return origin is t.Union or origin is types.UnionType def origin_is_literal(origin: type[t.Any] | None) -> bool: """判断是否是 Literal 类型""" return origin is t.Literal or origin is t_ext.Literal def _literal_values(type_: type[t.Any]) -> tuple[t.Any, ...]: return get_args(type_) def all_literal_values(type_: type[t.Any]) -> list[t.Any]: """获取 Literal 类型包含的所有值""" if not origin_is_literal(get_origin(type_)): return [type_] return [x for value in _literal_values(type_) for x in all_literal_values(value)] def origin_is_annotated(origin: type[t.Any] | None) -> bool: """判断是否是 Annotated 类型""" return origin is t_ext.Annotated NONE_TYPES = {None, type(None), t.Literal[None], t_ext.Literal[None], types.NoneType} # noqa: PYI061 def is_none_type(type_: type[t.Any]) -> bool: """判断是否是 None 类型""" return type_ in NONE_TYPES if sys.version_info < (3, 12): def is_type_alias_type(type_: type[t.Any]) -> bool: """判断是否是 TypeAliasType 类型""" return isinstance(type_, t_ext.TypeAliasType) else: def is_type_alias_type(type_: type[t.Any]) -> bool: return isinstance(type_, (t.TypeAliasType, t_ext.TypeAliasType)) def evaluate_forwardref( ref: t.ForwardRef, globalns: dict[str, t.Any], localns: dict[str, t.Any] ) -> t.Any: # Python 3.13/3.12.4+ made `recursive_guard` a kwarg, # so name it explicitly to avoid: # TypeError: ForwardRef._evaluate() # missing 1 required keyword-only argument: 'recursive_guard' return ref._evaluate(globalns, localns, recursive_guard=frozenset()) # state # use annotated flag to avoid ForwardRef recreate generic type (py >= 3.11) class StateFlag: def __repr__(self) -> str: return "StateFlag()" _STATE_FLAG = StateFlag() T_State: TypeAlias = t.Annotated[dict[t.Any, t.Any], _STATE_FLAG] """事件处理状态 State 类型""" _DependentCallable: TypeAlias = t.Callable[..., T] | t.Callable[..., t.Awaitable[T]] # driver hooks T_BotConnectionHook: TypeAlias = _DependentCallable[t.Any] """Bot 连接建立时钩子函数 依赖参数: - DependParam: 子依赖参数 - BotParam: Bot 对象 - DefaultParam: 带有默认值的参数 """ T_BotDisconnectionHook: TypeAlias = _DependentCallable[t.Any] """Bot 连接断开时钩子函数 依赖参数: - DependParam: 子依赖参数 - BotParam: Bot 对象 - DefaultParam: 带有默认值的参数 """ # api hooks T_CallingAPIHook: TypeAlias = t.Callable[ ["Bot", str, dict[str, t.Any]], t.Awaitable[t.Any] ] """`bot.call_api` 钩子函数""" T_CalledAPIHook: TypeAlias = t.Callable[ ["Bot", Exception | None, str, dict[str, t.Any], t.Any], t.Awaitable[t.Any] ] """`bot.call_api` 后执行的函数,参数分别为 bot, exception, api, data, result""" # event hooks T_EventPreProcessor: TypeAlias = _DependentCallable[t.Any] """事件预处理函数 EventPreProcessor 类型 依赖参数: - DependParam: 子依赖参数 - BotParam: Bot 对象 - EventParam: Event 对象 - StateParam: State 对象 - DefaultParam: 带有默认值的参数 """ T_EventPostProcessor: TypeAlias = _DependentCallable[t.Any] """事件后处理函数 EventPostProcessor 类型 依赖参数: - DependParam: 子依赖参数 - BotParam: Bot 对象 - EventParam: Event 对象 - StateParam: State 对象 - DefaultParam: 带有默认值的参数 """ # matcher run hooks T_RunPreProcessor: TypeAlias = _DependentCallable[t.Any] """事件响应器运行前预处理函数 RunPreProcessor 类型 依赖参数: - DependParam: 子依赖参数 - BotParam: Bot 对象 - EventParam: Event 对象 - StateParam: State 对象 - MatcherParam: Matcher 对象 - DefaultParam: 带有默认值的参数 """ T_RunPostProcessor: TypeAlias = _DependentCallable[t.Any] """事件响应器运行后后处理函数 RunPostProcessor 类型 依赖参数: - DependParam: 子依赖参数 - BotParam: Bot 对象 - EventParam: Event 对象 - StateParam: State 对象 - MatcherParam: Matcher 对象 - ExceptionParam: 异常对象(可能为 None) - DefaultParam: 带有默认值的参数 """ # rule, permission T_RuleChecker: TypeAlias = _DependentCallable[bool] """RuleChecker 即判断是否响应事件的处理函数。 依赖参数: - DependParam: 子依赖参数 - BotParam: Bot 对象 - EventParam: Event 对象 - StateParam: State 对象 - DefaultParam: 带有默认值的参数 """ T_PermissionChecker: TypeAlias = _DependentCallable[bool] """PermissionChecker 即判断事件是否满足权限的处理函数。 依赖参数: - DependParam: 子依赖参数 - BotParam: Bot 对象 - EventParam: Event 对象 - DefaultParam: 带有默认值的参数 """ T_Handler: TypeAlias = _DependentCallable[t.Any] """Handler 处理函数。""" T_TypeUpdater: TypeAlias = _DependentCallable[str] """TypeUpdater 在 Matcher.pause, Matcher.reject 时被运行,用于更新响应的事件类型。 默认会更新为 `message`。 依赖参数: - DependParam: 子依赖参数 - BotParam: Bot 对象 - EventParam: Event 对象 - StateParam: State 对象 - MatcherParam: Matcher 对象 - DefaultParam: 带有默认值的参数 """ T_PermissionUpdater: TypeAlias = _DependentCallable["Permission"] """PermissionUpdater 在 Matcher.pause, Matcher.reject 时被运行,用于更新会话对象权限。 默认会更新为当前事件的触发对象。 依赖参数: - DependParam: 子依赖参数 - BotParam: Bot 对象 - EventParam: Event 对象 - StateParam: State 对象 - MatcherParam: Matcher 对象 - DefaultParam: 带有默认值的参数 """ T_DependencyCache: TypeAlias = dict[_DependentCallable[t.Any], "DependencyCache"] """依赖缓存, 用于存储依赖函数的返回值""" ================================================ FILE: nonebot/utils.py ================================================ """本模块包含了 NoneBot 的一些工具函数 FrontMatter: mdx: format: md sidebar_position: 8 description: nonebot.utils 模块 """ from collections import deque from collections.abc import ( AsyncGenerator, Callable, Coroutine, Generator, Mapping, Sequence, ) import contextlib from contextlib import AbstractContextManager, asynccontextmanager import dataclasses from functools import partial, wraps import importlib import inspect import json from pathlib import Path import re from typing import ( Any, Generic, TypeVar, get_args, get_origin, overload, ) from typing_extensions import ParamSpec, override import anyio import anyio.to_thread from exceptiongroup import BaseExceptionGroup, catch from pydantic import BaseModel from nonebot.log import logger from nonebot.typing import ( all_literal_values, is_none_type, origin_is_literal, origin_is_union, type_has_args, ) P = ParamSpec("P") R = TypeVar("R") T = TypeVar("T") K = TypeVar("K") V = TypeVar("V") E = TypeVar("E", bound=BaseException) def escape_tag(s: str) -> str: """用于记录带颜色日志时转义 `` 类型特殊标签 参考: [loguru color 标签](https://loguru.readthedocs.io/en/stable/api/logger.html#color) 参数: s: 需要转义的字符串 """ return re.sub(r"\s]*)>", r"\\\g<0>", s) def deep_update( mapping: dict[K, Any], *updating_mappings: dict[K, Any] ) -> dict[K, Any]: """深度更新合并字典""" updated_mapping = mapping.copy() for updating_mapping in updating_mappings: for k, v in updating_mapping.items(): if ( k in updated_mapping and isinstance(updated_mapping[k], dict) and isinstance(v, dict) ): updated_mapping[k] = deep_update(updated_mapping[k], v) else: updated_mapping[k] = v return updated_mapping def lenient_issubclass( cls: Any, class_or_tuple: type[Any] | tuple[type[Any], ...] ) -> bool: """检查 cls 是否是 class_or_tuple 中的一个类型子类并忽略类型错误。""" try: return isinstance(cls, type) and issubclass(cls, class_or_tuple) except TypeError: return False def generic_check_issubclass( cls: Any, class_or_tuple: type[Any] | tuple[type[Any], ...] ) -> bool: """检查 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.List`、`typing.Dict` 等泛型类型, 则会检查其原始类型是否是 class_or_tuple 中一个类型的子类。 """ # if the target is a TypeVar, we check it first if isinstance(cls, TypeVar): if cls.__constraints__: return all( is_none_type(type_) or generic_check_issubclass(type_, class_or_tuple) for type_ in cls.__constraints__ ) elif cls.__bound__: return generic_check_issubclass(cls.__bound__, class_or_tuple) return False # elif the target is not a generic type, we check it directly elif not type_has_args(cls): with contextlib.suppress(TypeError): return issubclass(cls, class_or_tuple) origin = get_origin(cls) if origin_is_union(origin): return all( is_none_type(type_) or generic_check_issubclass(type_, class_or_tuple) for type_ in get_args(cls) ) elif origin_is_literal(origin): return all( is_none_type(value) or isinstance(value, class_or_tuple) for value in all_literal_values(cls) ) # ensure generic List, Dict can be checked elif origin: # avoid class check error (typing.Final, typing.ClassVar, etc...) try: return issubclass(origin, class_or_tuple) except TypeError: return False return False def type_is_complex(type_: type[Any]) -> bool: """检查 type_ 是否是复杂类型""" origin = get_origin(type_) return _type_is_complex_inner(type_) or _type_is_complex_inner(origin) def _type_is_complex_inner(type_: type[Any] | None) -> bool: if lenient_issubclass(type_, (str, bytes)): return False return lenient_issubclass( type_, (BaseModel, Mapping, Sequence, tuple, set, frozenset, deque) ) or dataclasses.is_dataclass(type_) def is_coroutine_callable(call: Callable[..., Any]) -> bool: """检查 call 是否是一个 callable 协程函数""" if inspect.isroutine(call): return inspect.iscoroutinefunction(call) if inspect.isclass(call): return False func_ = getattr(call, "__call__", None) return inspect.iscoroutinefunction(func_) def is_gen_callable(call: Callable[..., Any]) -> bool: """检查 call 是否是一个生成器函数""" if inspect.isgeneratorfunction(call): return True func_ = getattr(call, "__call__", None) return inspect.isgeneratorfunction(func_) def is_async_gen_callable(call: Callable[..., Any]) -> bool: """检查 call 是否是一个异步生成器函数""" if inspect.isasyncgenfunction(call): return True func_ = getattr(call, "__call__", None) return inspect.isasyncgenfunction(func_) def run_sync(call: Callable[P, R]) -> Callable[P, Coroutine[None, None, R]]: """一个用于包装 sync function 为 async function 的装饰器 参数: call: 被装饰的同步函数 """ @wraps(call) async def _wrapper(*args: P.args, **kwargs: P.kwargs) -> R: return await anyio.to_thread.run_sync( partial(call, *args, **kwargs), abandon_on_cancel=True ) return _wrapper @asynccontextmanager async def run_sync_ctx_manager( cm: AbstractContextManager[T], ) -> AsyncGenerator[T, None]: """一个用于包装 sync context manager 为 async context manager 的执行函数""" try: yield await run_sync(cm.__enter__)() except Exception as e: ok = await run_sync(cm.__exit__)(type(e), e, None) if not ok: raise e else: await run_sync(cm.__exit__)(None, None, None) @overload async def run_coro_with_catch( coro: Coroutine[Any, Any, T], exc: tuple[type[Exception], ...], return_on_err: None = None, ) -> T | None: ... @overload async def run_coro_with_catch( coro: Coroutine[Any, Any, T], exc: tuple[type[Exception], ...], return_on_err: R, ) -> T | R: ... async def run_coro_with_catch( coro: Coroutine[Any, Any, T], exc: tuple[type[Exception], ...], return_on_err: R | None = None, ) -> T | R | None: """运行协程并当遇到指定异常时返回指定值。 参数: coro: 要运行的协程 exc: 要捕获的异常 return_on_err: 当发生异常时返回的值 返回: 协程的返回值或发生异常时的指定值 """ with catch({exc: lambda exc_group: None}): return await coro return return_on_err async def run_coro_with_shield(coro: Coroutine[Any, Any, T]) -> T: """运行协程并在取消时屏蔽取消异常。 参数: coro: 要运行的协程 返回: 协程的返回值 """ with anyio.CancelScope(shield=True): return await coro raise RuntimeError("This should not happen") def flatten_exception_group( exc_group: BaseExceptionGroup[E], ) -> Generator[E, None, None]: for exc in exc_group.exceptions: if isinstance(exc, BaseExceptionGroup): yield from flatten_exception_group(exc) else: yield exc def get_name(obj: Any) -> str: """获取对象的名称""" if inspect.isfunction(obj) or inspect.isclass(obj): return obj.__name__ return obj.__class__.__name__ def path_to_module_name(path: Path) -> str: """转换路径为模块名""" rel_path = path.resolve().relative_to(Path.cwd().resolve()) if rel_path.stem == "__init__": return ".".join(rel_path.parts[:-1]) else: return ".".join((*rel_path.parts[:-1], rel_path.stem)) def resolve_dot_notation( obj_str: str, default_attr: str, default_prefix: str | None = None ) -> Any: """解析并导入点分表示法的对象""" modulename, _, cls = obj_str.partition(":") if default_prefix is not None and modulename.startswith("~"): modulename = default_prefix + modulename[1:] module = importlib.import_module(modulename) if not cls: return getattr(module, default_attr) instance = module for attr_str in cls.split("."): instance = getattr(instance, attr_str) return instance class classproperty(Generic[T]): """类属性装饰器""" def __init__(self, func: Callable[[Any], T]) -> None: self.func = func def __get__(self, instance: Any, owner: type[Any] | None = None) -> T: return self.func(type(instance) if owner is None else owner) class DataclassEncoder(json.JSONEncoder): """可以序列化 {ref}`nonebot.adapters.Message`(List[Dataclass]) 的 `JSONEncoder`""" @override def default(self, o): if dataclasses.is_dataclass(o): return {f.name: getattr(o, f.name) for f in dataclasses.fields(o)} return super().default(o) def logger_wrapper(logger_name: str): """用于打印 adapter 的日志。 参数: logger_name: adapter 的名称 返回: 日志记录函数 日志记录函数的参数: - level: 日志等级 - message: 日志信息 - exception: 异常信息 """ def log(level: str, message: str, exception: Exception | None = None): logger.opt(colors=True, exception=exception).log( level, f"{escape_tag(logger_name)} | {message}" ) return log ================================================ FILE: package.json ================================================ { "name": "root", "private": true, "packageManager": "yarn@1.22.22", "workspaces": [ "website" ], "scripts": { "archive": "yarn workspace nonebot docusaurus docs:version", "build": "yarn workspace nonebot build", "build:plugin": "cross-env BASE_URL='/website/' yarn workspace nonebot build", "start": "yarn workspace nonebot start", "serve": "yarn workspace nonebot serve", "clear": "yarn workspace nonebot clear", "prettier": "prettier --config ./.prettierrc --write \"./website/\"", "lint": "yarn lint:js && yarn lint:style", "lint:js": "eslint --cache --report-unused-disable-directives \"**/*.{js,jsx,ts,tsx,mjs}\"", "lint:js:fix": "eslint --cache --report-unused-disable-directives --fix \"**/*.{js,jsx,ts,tsx,mjs}\"", "lint:style": "stylelint \"**/*.css\"", "lint:style:fix": "stylelint --fix \"**/*.css\"", "pyright": "pyright" }, "devDependencies": { "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", "cross-env": "^7.0.3", "eslint": "^8.48.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-prettier": "^8.8.0", "eslint-plugin-import": "^2.27.5", "eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-react": "^7.32.2", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-regexp": "^1.15.0", "prettier": "^3.0.3", "pyright": "1.1.393", "stylelint": "^15.10.3", "stylelint-config-standard": "^34.0.0", "stylelint-prettier": "^4.0.2" } } ================================================ FILE: packages/nonebot-plugin-docs/README.md ================================================

nonebot

# nonebot-plugin-docs _✨ NoneBot 本地文档插件 ✨_

license pypi python

## 使用方式 加载插件并启动 Bot ,在浏览器内打开 `http://host:port/website/`。 具体网址会在控制台内输出。 ================================================ FILE: packages/nonebot-plugin-docs/nonebot_plugin_docs/__init__.py ================================================ import importlib import nonebot from nonebot.log import logger from nonebot.plugin import PluginMetadata __plugin_meta__ = PluginMetadata( name="NoneBot 离线文档", description="在本地查看 NoneBot 文档", usage="启动机器人后访问 http://localhost:port/website/ 查看文档", type="application", homepage="https://github.com/nonebot/nonebot2/blob/master/packages/nonebot-plugin-docs", config=None, supported_adapters=None, ) def init(): driver = nonebot.get_driver() try: _module = importlib.import_module( f"nonebot_plugin_docs.drivers.{driver.type.split('+')[0]}" ) except ImportError: logger.warning(f"Driver {driver.type} not supported") return register_route = getattr(_module, "register_route") register_route(driver) host = str(driver.config.host) port = driver.config.port if host in {"0.0.0.0", "127.0.0.1"}: host = "localhost" logger.opt(colors=True).info( f"Nonebot docs will be running at: http://{host}:{port}/website/" ) init() ================================================ FILE: packages/nonebot-plugin-docs/nonebot_plugin_docs/drivers/fastapi.py ================================================ from pathlib import Path from fastapi.staticfiles import StaticFiles from nonebot.drivers.fastapi import Driver def register_route(driver: Driver): app = driver.server_app static_path = str((Path(__file__).parent / ".." / "dist").resolve()) app.mount("/website", StaticFiles(directory=static_path, html=True), name="docs") ================================================ FILE: packages/nonebot-plugin-docs/pyproject.toml ================================================ [project] name = "nonebot-plugin-docs" version = "2.0.0" description = "View NoneBot2 Docs Locally" authors = [{ name = "yanyongyu", email = "yyy@nonebot.dev" }] readme = "README.md" license = "MIT" keywords = ["nonebot", "nonebot2", "docs"] requires-python = ">=3.9, <4.0" dependencies = ["nonebot2 >=2.0.0, <3.0.0"] [project.urls] Homepage = "https://github.com/nonebot/nonebot2/blob/master/packages/nonebot-plugin-docs" Repository = "https://github.com/nonebot/nonebot2" [dependency-groups] dev = [] [tool.uv.build-backend] module-root = "" [build-system] requires = ["uv_build >=0.8.3, <0.9.0"] build-backend = "uv_build" ================================================ FILE: pyproject.toml ================================================ [project] name = "nonebot2" version = "2.4.4" description = "An asynchronous python bot framework." authors = [{ name = "yanyongyu", email = "yyy@nonebot.dev" }] license = "MIT" readme = "README.md" keywords = ["bot", "qq", "qqbot", "mirai", "coolq"] classifiers = [ "Development Status :: 5 - Production/Stable", "Framework :: Robot Framework", "Framework :: Robot Framework :: Library", "Operating System :: OS Independent", "Programming Language :: Python :: 3", ] requires-python = ">=3.10, <4.0" dependencies = [ "yarl >=1.7.2, <2.0.0", "anyio >=4.4.0, <5.0.0", "loguru >=0.6.0, <1.0.0", "pygtrie >=2.4.1, <3.0.0", "exceptiongroup >=1.2.2, <2.0.0", "python-dotenv >=0.21.0, <2.0.0", "typing-extensions >=4.6.0, <5.0.0", "tomli >=2.0.1, <3.0.0; python_version < '3.11'", "pydantic >=1.10.0, <3.0.0, !=2.5.0, !=2.5.1, !=2.10.0, !=2.10.1", ] [project.optional-dependencies] websockets = ["websockets >=15.0"] httpx = ["httpx[http2] >=0.26.0, <1.0.0"] aiohttp = ["aiohttp[speedups] >=3.11.0, <4.0.0"] quart = ["Quart >=0.18.0, <1.0.0", "uvicorn[standard] >=0.20.0, <1.0.0"] fastapi = ["fastapi >=0.93.0, <1.0.0", "uvicorn[standard] >=0.20.0, <1.0.0"] all = [ "websockets >=15.0", "fastapi >=0.93.0, <1.0.0", "httpx[http2] >=0.26.0, <1.0.0", "aiohttp[speedups] >=3.11.0, <4.0.0", "uvicorn[standard] >=0.20.0, <1.0.0", ] [dependency-groups] dev = [ { include-group = "test" }, { include-group = "docs" }, "ruff >=0.14.0, <0.15.0", "nonemoji >=0.1.2, <0.2.0", "pre-commit >=4.0.0, <5.0.0", ] test = [ "trio >=0.27.0", "nonebug >=0.4.1, <0.5.0", "wsproto >=1.2.0, <2.0.0", "werkzeug >=2.3.6, <4.0.0", "pytest-cov >=7.0.0, <8.0.0", "pytest-xdist >=3.0.2, <4.0.0", "coverage-conditional-plugin >=0.9.0, <0.10.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"] [project.urls] Homepage = "https://nonebot.dev/" Repository = "https://github.com/nonebot/nonebot2" Documentation = "https://nonebot.dev/" "Bug Tracker" = "https://github.com/nonebot/nonebot2/issues" Changelog = "https://nonebot.dev/changelog" Funding = "https://afdian.com/@nonebot" [tool.uv] required-version = ">=0.8.0" conflicts = [[{ group = "pydantic-v1" }, { group = "pydantic-v2" }]] [tool.uv.build-backend] module-name = "nonebot" module-root = "" [tool.pytest.ini_options] addopts = "--cov=nonebot --cov-report=term-missing" filterwarnings = ["error", "ignore::DeprecationWarning"] [tool.ruff] line-length = 88 [tool.ruff.format] line-ending = "lf" [tool.ruff.lint] select = [ "F", # Pyflakes "W", # pycodestyle warnings "E", # pycodestyle errors "I", # isort "UP", # pyupgrade "ASYNC", # flake8-async "C4", # flake8-comprehensions "T10", # flake8-debugger "T20", # flake8-print "PYI", # flake8-pyi "PT", # flake8-pytest-style "Q", # flake8-quotes "TID", # flake8-tidy-imports "RUF", # Ruff-specific rules ] ignore = [ "E402", # module-import-not-at-top-of-file "UP037", # quoted-annotation "RUF001", # ambiguous-unicode-character-string "RUF002", # ambiguous-unicode-character-docstring "RUF003", # ambiguous-unicode-character-comment ] [tool.ruff.lint.isort] force-sort-within-sections = true known-first-party = ["nonebot", "tests/*"] extra-standard-library = ["typing_extensions"] [tool.ruff.lint.flake8-pytest-style] fixture-parentheses = false mark-parentheses = false [tool.ruff.lint.pyupgrade] keep-runtime-typing = true [tool.pyright] pythonVersion = "3.10" pythonPlatform = "All" defineConstant = { PYDANTIC_V2 = true } executionEnvironments = [ { root = "./tests/python_3_12", pythonVersion = "3.12", extraPaths = [ "./", ] }, { root = "./tests", extraPaths = [ "./", ] }, { root = "./" }, ] typeCheckingMode = "standard" reportShadowedImports = false disableBytesTypePromotions = true [build-system] requires = ["uv_build >=0.8.3, <0.10.0"] build-backend = "uv_build" ================================================ FILE: scripts/build-api-docs.sh ================================================ #!/usr/bin/env bash set -e # cd to the root of the project cd "$(dirname "$0")/.." nb-autodoc nonebot \ -s nonebot.plugins \ -u nonebot.internal \ -u nonebot.internal.* cp -r ./build/nonebot/* ./website/docs/api/ yarn prettier ================================================ FILE: scripts/run-tests.sh ================================================ #!/usr/bin/env bash # cd to the root of the tests cd "$(dirname "$0")/../tests" # Run the tests pytest -n auto --cov-append --cov-report xml --junitxml=./junit.xml $@ ================================================ FILE: scripts/setup-envs.sh ================================================ #!/usr/bin/env bash echo "Setting up dev environment" uv sync --all-extras && uv run pre-commit install && yarn install ================================================ FILE: tests/.coveragerc ================================================ [run] plugins = coverage_conditional_plugin [report] exclude_lines = pragma: no cover def __repr__ def __str__ @(typing\.)?overload if (typing\.)?TYPE_CHECKING( is True)?: @(abc\.)?abstractmethod raise NotImplementedError warnings\.warn ^\.\.\.$ pass if __name__ == .__main__.: [coverage_conditional_plugin] rules = "sys_platform != 'win32'": py-win32 "sys_platform != 'linux'": py-linux "sys_platform != 'darwin'": py-darwin "sys_version_info < (3, 11)": py-gte-311 "sys_version_info >= (3, 11)": py-lt-311 "package_version('pydantic') < (2,)": pydantic-v2 "package_version('pydantic') >= (2,)": pydantic-v1 ================================================ FILE: tests/bad_plugins/bad_plugin.py ================================================ import nonebot plugin = nonebot.get_plugin("bad_plugin") assert plugin x = 1 / 0 ================================================ FILE: tests/conftest.py ================================================ from collections.abc import Callable, Generator from functools import wraps import os from pathlib import Path import sys import threading from types import EllipsisType from typing import TYPE_CHECKING, TypeVar from typing_extensions import ParamSpec from nonebug import NONEBOT_INIT_KWARGS import pytest from werkzeug.serving import BaseWSGIServer, make_server from fake_server import request_handler import nonebot from nonebot import _resolve_combine_expr from nonebot.config import Env from nonebot.drivers import URL, Driver os.environ["CONFIG_FROM_ENV"] = '{"test": "test"}' os.environ["CONFIG_OVERRIDE"] = "new" if TYPE_CHECKING: from nonebot.plugin import Plugin P = ParamSpec("P") R = TypeVar("R") collect_ignore = ["plugins/", "dynamic/", "bad_plugins/"] def pytest_configure(config: pytest.Config) -> None: config.stash[NONEBOT_INIT_KWARGS] = {"config_from_init": "init"} @pytest.fixture(name="driver") def load_driver(request: pytest.FixtureRequest) -> Driver: driver_name = getattr(request, "param", None) global_driver = nonebot.get_driver() if driver_name is None: return global_driver DriverClass = _resolve_combine_expr(driver_name) return DriverClass(Env(environment=global_driver.env), global_driver.config) @pytest.fixture(scope="session", params=[pytest.param("asyncio"), pytest.param("trio")]) def anyio_backend(request: pytest.FixtureRequest): return request.param def run_once(func: Callable[P, R]) -> Callable[P, R]: result: R | EllipsisType = ... @wraps(func) def _wrapper(*args: P.args, **kwargs: P.kwargs) -> R: nonlocal result if result is not ...: return result result = func(*args, **kwargs) return result return _wrapper @pytest.fixture(scope="session", autouse=True) @run_once def load_plugin(anyio_backend, nonebug_init: None) -> set["Plugin"]: # preload global plugins plugins: set["Plugin"] = set() plugins |= nonebot.load_plugins(str(Path(__file__).parent / "plugins")) if sys.version_info >= (3, 12): # preload python 3.12 plugins plugins |= nonebot.load_plugins( str(Path(__file__).parent / "python_3_12" / "plugins") ) return plugins @pytest.fixture(scope="session", autouse=True) @run_once def load_builtin_plugin(anyio_backend, nonebug_init: None) -> set["Plugin"]: # preload builtin plugins return nonebot.load_builtin_plugins("echo", "single_session") @pytest.fixture(scope="session", autouse=True) def server() -> Generator[BaseWSGIServer, None, None]: server = make_server("127.0.0.1", 0, app=request_handler) thread = threading.Thread(target=server.serve_forever) thread.start() try: yield server finally: server.shutdown() thread.join() @pytest.fixture(scope="session") def server_url(server: BaseWSGIServer) -> URL: return URL(f"http://{server.host}:{server.port}") ================================================ FILE: tests/dynamic/manager.py ================================================ ================================================ FILE: tests/dynamic/path.py ================================================ ================================================ FILE: tests/dynamic/require_not_declared.py ================================================ ================================================ FILE: tests/dynamic/require_not_loaded/__init__.py ================================================ ================================================ FILE: tests/dynamic/require_not_loaded/subplugin1.py ================================================ ================================================ FILE: tests/dynamic/require_not_loaded/subplugin2.py ================================================ ================================================ FILE: tests/dynamic/simple.py ================================================ ================================================ FILE: tests/fake_server.py ================================================ import base64 import json import socket from typing import TypeVar from werkzeug import Request, Response from werkzeug.datastructures import MultiDict from wsproto import ConnectionType, WSConnection from wsproto.events import ( AcceptConnection, BytesMessage, CloseConnection, Ping, TextMessage, ) from wsproto.events import Request as WSRequest from wsproto.frame_protocol import CloseReason K = TypeVar("K") V = TypeVar("V") def json_safe(string, content_type="application/octet-stream") -> str: try: string = string.decode("utf-8") json.dumps(string) return string except (ValueError, TypeError): return b"".join( [ b"data:", content_type.encode("utf-8"), b";base64,", base64.b64encode(string), ] ).decode("utf-8") def flattern(d: "MultiDict[K, V]") -> dict[K, V | list[V]]: return {k: v[0] if len(v) == 1 else v for k, v in d.to_dict(flat=False).items()} def http_echo(request: Request) -> Response: try: _json = json.loads(request.data.decode("utf-8")) except (ValueError, TypeError): _json = None return Response( json.dumps( { "url": request.url, "method": request.method, "origin": request.headers.get("X-Forwarded-For", request.remote_addr), "headers": flattern( MultiDict((k, v) for k, v in request.headers.items()) ), "args": flattern(request.args), "form": flattern(request.form), "data": json_safe(request.data), "json": _json, "files": flattern( MultiDict( ( k, json_safe( v.read(), request.files[k].content_type or "application/octet-stream", ), ) for k, v in request.files.items() ) ), } ), status=200, content_type="application/json", ) def websocket_echo(request: Request) -> Response: stream = request.environ["werkzeug.socket"] ws = WSConnection(ConnectionType.SERVER) in_data = b"GET %s HTTP/1.1\r\n" % request.path.encode("utf-8") for header, value in request.headers.items(): in_data += f"{header}: {value}\r\n".encode() in_data += b"\r\n" ws.receive_data(in_data) running: bool = True while True: out_data = b"" for event in ws.events(): if isinstance(event, WSRequest): out_data += ws.send(AcceptConnection()) elif isinstance(event, CloseConnection): out_data += ws.send(event.response()) running = False elif isinstance(event, Ping): out_data += ws.send(event.response()) elif isinstance(event, TextMessage): if event.data == "quit": out_data += ws.send( CloseConnection(CloseReason.NORMAL_CLOSURE, "bye") ) running = False else: out_data += ws.send(TextMessage(data=event.data)) elif isinstance(event, BytesMessage): if event.data == b"quit": out_data += ws.send( CloseConnection(CloseReason.NORMAL_CLOSURE, "bye") ) running = False else: out_data += ws.send(BytesMessage(data=event.data)) if out_data: stream.send(out_data) if not running: break in_data = stream.recv(4096) ws.receive_data(in_data) stream.shutdown(socket.SHUT_RDWR) return Response("", status=204) @Request.application def request_handler(request: Request) -> Response: if request.headers.get("Connection") == "Upgrade": return websocket_echo(request) else: return http_echo(request) ================================================ FILE: tests/plugins/_hidden.py ================================================ import pytest pytest.fail("should not be imported") ================================================ FILE: tests/plugins/export.py ================================================ def test(): return "export" ================================================ FILE: tests/plugins/matcher/__init__.py ================================================ from pathlib import Path from nonebot import load_plugins _sub_plugins = set() _sub_plugins |= load_plugins(str(Path(__file__).parent)) ================================================ FILE: tests/plugins/matcher/matcher_expire.py ================================================ from datetime import datetime, timedelta from nonebot.matcher import Matcher test_temp_matcher = Matcher.new("test", temp=True) test_datetime_matcher = Matcher.new( "test", expire_time=datetime.now() - timedelta(seconds=1) ) test_timedelta_matcher = Matcher.new("test", expire_time=timedelta(seconds=-1)) ================================================ FILE: tests/plugins/matcher/matcher_info.py ================================================ from nonebot import on matcher = on("message", temp=False, expire_time=None, priority=1, block=True) ================================================ FILE: tests/plugins/matcher/matcher_permission.py ================================================ from nonebot.matcher import Matcher from nonebot.permission import USER, Permission default_permission = Permission() new_permission = Permission() test_permission_updater = Matcher.new(permission=default_permission) test_user_permission_updater = Matcher.new( permission=USER("test", perm=default_permission) ) test_custom_updater = Matcher.new(permission=default_permission) @test_custom_updater.permission_updater async def _() -> Permission: return new_permission ================================================ FILE: tests/plugins/matcher/matcher_process.py ================================================ from nonebot import on_message from nonebot.adapters import Event, Message from nonebot.matcher import Matcher from nonebot.params import ArgStr, EventMessage, LastReceived, Received test_handle = on_message() @test_handle.handle() async def handle(): await test_handle.finish("send", at_sender=True) test_got = on_message() @test_got.got("key1", "prompt key1") @test_got.got("key2", "prompt key2") async def got(key1: str = ArgStr(), key2: str = ArgStr()): if key2 == "text": await test_got.reject("reject", at_sender=True) assert key1 == "text" assert key2 == "text_next" test_receive = on_message() @test_receive.receive() @test_receive.receive("receive") async def receive( x: Event = Received("receive"), y: Event = LastReceived(), z: Event = Received() ): assert str(x.get_message()) == "text" assert str(z.get_message()) == "text" assert x is y await test_receive.pause("pause", at_sender=True) test_combine = on_message() @test_combine.got("a") @test_combine.receive() @test_combine.got("b") async def combine(a: str = ArgStr(), b: str = ArgStr(), r: Event = Received()): if a == "text": await test_combine.reject_arg("a") elif b == "text": await test_combine.reject_arg("b") elif str(r.get_message()) == "text": await test_combine.reject_receive() assert a == "text_next" assert b == "text_next" assert str(r.get_message()) == "text_next" test_preset = on_message() @test_preset.handle() async def preset(matcher: Matcher, message: Message = EventMessage()): matcher.set_arg("a", message) @test_preset.got("a") @test_preset.got("b") async def reject_preset(a: str = ArgStr(), b: str = ArgStr()): if a == "text": await test_preset.reject_arg("a") assert a == "text_next" assert b == "text" test_overload = on_message() class FakeEvent(Event): ... @test_overload.got("a") async def overload(event: FakeEvent): await test_overload.reject_arg("a") @test_overload.handle() async def finish(): await test_overload.finish() test_destroy = on_message() ================================================ FILE: tests/plugins/matcher/matcher_type.py ================================================ from nonebot.matcher import Matcher test_type_updater = Matcher.new(type_="test") test_custom_updater = Matcher.new(type_="test") @test_custom_updater.type_updater async def _() -> str: return "custom" ================================================ FILE: tests/plugins/metadata.py ================================================ from pydantic import BaseModel from nonebot.adapters import Adapter from nonebot.plugin import PluginMetadata class Config(BaseModel): custom: str = "" class FakeAdapter(Adapter): ... __plugin_meta__ = PluginMetadata( name="测试插件", description="测试插件元信息", usage="无法使用", type="application", homepage="https://nonebot.dev", config=Config, supported_adapters={"~onebot.v11", "plugins.metadata:FakeAdapter"}, extra={"author": "NoneBot"}, ) ================================================ FILE: tests/plugins/metadata_2.py ================================================ from nonebot.plugin import PluginMetadata __plugin_meta__ = PluginMetadata( name="测试插件2", description="测试继承适配器", usage="无法使用", type="application", homepage="https://nonebot.dev", supported_adapters={"~onebot.v11", "~onebot.v12"}, extra={"author": "NoneBot"}, ) ================================================ FILE: tests/plugins/metadata_3.py ================================================ from nonebot.plugin import PluginMetadata __plugin_meta__ = PluginMetadata( name="测试插件3", description="测试继承适配器, 使用内置适配器全名", usage="无法使用", type="application", homepage="https://nonebot.dev", supported_adapters={ "nonebot.adapters.onebot.v11", "nonebot.adapters.onebot.v12", "~qq", }, extra={"author": "NoneBot"}, ) ================================================ FILE: tests/plugins/nested/__init__.py ================================================ from pathlib import Path from nonebot.plugin import PluginManager, _managers manager = PluginManager( search_path=[str((Path(__file__).parent / "plugins").resolve())] ) _managers.append(manager) # test load nested plugin with require manager.load_plugin("plugins.nested.plugins.nested_subplugin") manager.load_plugin("nested:nested_subplugin2") ================================================ FILE: tests/plugins/nested/plugins/nested_subplugin.py ================================================ from .nested_subplugin2 import a # noqa: F401 ================================================ FILE: tests/plugins/nested/plugins/nested_subplugin2.py ================================================ a = "required by another subplugin" ================================================ FILE: tests/plugins/param/__init__.py ================================================ from pathlib import Path from nonebot import load_plugins _sub_plugins = set() _sub_plugins |= load_plugins(str(Path(__file__).parent)) ================================================ FILE: tests/plugins/param/param_arg.py ================================================ from typing import Annotated, Any from nonebot.adapters import Message from nonebot.params import Arg, ArgPlainText, ArgPromptResult, ArgStr async def arg(key: Message = Arg()) -> Message: return key async def arg_str(key: str = ArgStr()) -> str: return key async def arg_plain_text(key: str = ArgPlainText()) -> str: return key async def annotated_arg(key: Annotated[Message, Arg()]) -> Message: return key async def annotated_arg_str(key: Annotated[str, ArgStr()]) -> str: return key async def annotated_arg_plain_text(key: Annotated[str, ArgPlainText()]) -> str: return key async def annotated_arg_prompt_result(key: Annotated[Any, ArgPromptResult()]) -> Any: return key # test dependency priority async def annotated_prior_arg(key: Annotated[str, ArgStr("foo")] = ArgPlainText()): return key async def annotated_multi_arg( key: Annotated[Annotated[str, ArgStr("foo")], ArgPlainText()], ): return key ================================================ FILE: tests/plugins/param/param_bot.py ================================================ from typing import TypeVar from nonebot.adapters import Bot async def get_bot(b: Bot) -> Bot: return b async def postpone_bot(b: "Bot") -> Bot: return b async def legacy_bot(bot): return bot async def not_legacy_bot(bot: int): ... class FooBot(Bot): ... async def sub_bot(b: FooBot) -> FooBot: return b class BarBot(Bot): ... async def union_bot(b: FooBot | BarBot) -> FooBot | BarBot: return b B = TypeVar("B", bound=Bot) async def generic_bot(b: B) -> B: return b CB = TypeVar("CB", Bot, None) async def generic_bot_none(b: CB) -> CB: return b async def not_bot(b: int | Bot): ... ================================================ FILE: tests/plugins/param/param_default.py ================================================ async def default(value: int = 1) -> int: return value ================================================ FILE: tests/plugins/param/param_depend.py ================================================ from dataclasses import dataclass from typing import Annotated import anyio from pydantic import Field from nonebot import on_message from nonebot.adapters import Bot from nonebot.params import Depends test_depends = on_message() runned = [] def dependency(): runned.append(1) return 1 def parameterless(): assert len(runned) == 0 runned.append(1) def gen_sync(): yield 1 async def gen_async(): yield 2 @dataclass class ClassDependency: x: int = Depends(gen_sync) y: int = Depends(gen_async) class FooBot(Bot): ... async def sub_bot(b: FooBot) -> FooBot: return b # test parameterless @test_depends.handle(parameterless=[Depends(parameterless)]) async def depends(x: int = Depends(dependency)): # test dependency return x @test_depends.handle() async def depends_cache(y: int = Depends(dependency, use_cache=True)): # test cache return y # test class dependency async def class_depend(c: ClassDependency = Depends()): return c # test annotated dependency async def annotated_depend(x: Annotated[int, Depends(dependency)]): return x # test annotated class dependency async def annotated_class_depend(c: Annotated[ClassDependency, Depends()]): return c # test dependency priority async def annotated_prior_depend( x: Annotated[int, Depends(lambda: 2)] = Depends(dependency), ): return x async def annotated_multi_depend( x: Annotated[Annotated[int, Depends(lambda: 2)], Depends(dependency)], ): return x # test sub dependency type mismatch async def sub_type_mismatch(b: FooBot = Depends(sub_bot)): return b # test type validate async def validate(x: int = Depends(lambda: "1", validate=True)): return x async def validate_fail(x: int = Depends(lambda: "not_number", validate=True)): return x # test FieldInfo validate async def validate_field(x: int = Depends(lambda: "1", validate=Field(gt=0))): return x async def validate_field_fail(x: int = Depends(lambda: "0", validate=Field(gt=0))): return x async def _dep(): await anyio.sleep(1) return 1 def _dep_mismatch(): return 1 async def cache_exception_func1( dep: int = Depends(_dep), mismatch: dict = Depends(_dep_mismatch), ): raise RuntimeError("Never reach here") async def cache_exception_func2( dep: int = Depends(_dep), match: int = Depends(_dep_mismatch), ): return dep ================================================ FILE: tests/plugins/param/param_event.py ================================================ from typing import TypeVar from nonebot.adapters import Event, Message from nonebot.params import EventMessage, EventPlainText, EventToMe, EventType async def event(e: Event) -> Event: return e async def postpone_event(e: "Event") -> Event: return e async def legacy_event(event): return event async def not_legacy_event(event: int): ... class FooEvent(Event): ... async def sub_event(e: FooEvent) -> FooEvent: return e class BarEvent(Event): ... async def union_event(e: FooEvent | BarEvent) -> FooEvent | BarEvent: return e E = TypeVar("E", bound=Event) async def generic_event(e: E) -> E: return e CE = TypeVar("CE", Event, None) async def generic_event_none(e: CE) -> CE: return e async def not_event(e: int | Event): ... async def event_type(t: str = EventType()) -> str: return t async def event_message(msg: Message = EventMessage()) -> Message: return msg async def event_plain_text(text: str = EventPlainText()) -> str: return text async def event_to_me(to_me: bool = EventToMe()) -> bool: return to_me ================================================ FILE: tests/plugins/param/param_exception.py ================================================ async def exc(e: Exception, x: ValueError | TypeError) -> Exception: assert e == x return e async def legacy_exc(exception) -> Exception: return exception ================================================ FILE: tests/plugins/param/param_matcher.py ================================================ from typing import Any, TypeVar from nonebot.adapters import Event from nonebot.matcher import Matcher from nonebot.params import ( LastReceived, PausePromptResult, Received, ReceivePromptResult, ) async def matcher(m: Matcher) -> Matcher: return m async def postpone_matcher(m: "Matcher") -> Matcher: return m async def legacy_matcher(matcher): return matcher async def not_legacy_matcher(matcher: int): ... class FooMatcher(Matcher): ... async def sub_matcher(m: FooMatcher) -> FooMatcher: return m class BarMatcher(Matcher): ... async def union_matcher( m: FooMatcher | BarMatcher, ) -> FooMatcher | BarMatcher: return m M = TypeVar("M", bound=Matcher) async def generic_matcher(m: M) -> M: return m CM = TypeVar("CM", Matcher, None) async def generic_matcher_none(m: CM) -> CM: return m async def not_matcher(m: int | Matcher): ... async def receive(e: Event = Received("test")) -> Event: return e async def last_receive(e: Event = LastReceived()) -> Event: return e async def receive_prompt_result(result: Any = ReceivePromptResult("test")) -> Any: return result async def pause_prompt_result(result: Any = PausePromptResult()) -> Any: return result ================================================ FILE: tests/plugins/param/param_state.py ================================================ from re import Match from nonebot.adapters import Message from nonebot.params import ( Command, CommandArg, CommandStart, CommandWhitespace, Endswith, Fullmatch, Keyword, RawCommand, RegexDict, RegexGroup, RegexMatched, RegexStr, ShellCommandArgs, ShellCommandArgv, Startswith, ) from nonebot.typing import T_State async def state(x: T_State) -> T_State: return x async def postpone_state(x: "T_State") -> T_State: return x async def legacy_state(state): return state async def not_legacy_state(state: int): ... async def command(cmd: tuple[str, ...] = Command()) -> tuple[str, ...]: return cmd async def raw_command(raw_cmd: str = RawCommand()) -> str: return raw_cmd async def command_arg(cmd_arg: Message = CommandArg()) -> Message: return cmd_arg async def command_start(start: str = CommandStart()) -> str: return start async def command_whitespace(whitespace: str = CommandWhitespace()) -> str: return whitespace async def shell_command_args( shell_command_args: dict = ShellCommandArgs(), ) -> dict: return shell_command_args async def shell_command_argv( shell_command_argv: list[str] = ShellCommandArgv(), ) -> list[str]: return shell_command_argv async def regex_dict(regex_dict: dict = RegexDict()) -> dict: return regex_dict async def regex_group(regex_group: tuple = RegexGroup()) -> tuple: return regex_group async def regex_matched(regex_matched: Match[str] = RegexMatched()) -> Match[str]: return regex_matched async def regex_str( entire: str = RegexStr(), type_: str = RegexStr("type"), second: str = RegexStr(2), groups: tuple[str, ...] = RegexStr(1, "arg"), ) -> tuple[str, str, str, tuple[str, ...]]: return entire, type_, second, groups async def startswith(startswith: str = Startswith()) -> str: return startswith async def endswith(endswith: str = Endswith()) -> str: return endswith async def fullmatch(fullmatch: str = Fullmatch()) -> str: return fullmatch async def keyword(keyword: str = Keyword()) -> str: return keyword ================================================ FILE: tests/plugins/param/priority.py ================================================ from nonebot.adapters import Bot, Event, Message from nonebot.matcher import Matcher from nonebot.params import Arg, Depends from nonebot.typing import T_State def dependency(): return 1 async def complex_priority( sub: int = Depends(dependency), bot: Bot | None = None, event: Event | None = None, state: T_State = {}, matcher: Matcher | None = None, arg: Message = Arg(), exception: Exception | None = None, default: int = 1, ): ... ================================================ FILE: tests/plugins/plugin/__init__.py ================================================ from . import matchers as matchers ================================================ FILE: tests/plugins/plugin/matchers.py ================================================ from datetime import datetime, timezone from nonebot import ( CommandGroup, MatcherGroup, on, on_command, on_endswith, on_fullmatch, on_keyword, on_message, on_metaevent, on_notice, on_regex, on_request, on_shell_command, on_startswith, on_type, ) from nonebot.adapters import Event from nonebot.matcher import Matcher async def rule() -> bool: return True async def permission() -> bool: return True async def handler(): return expire_time = datetime.now(timezone.utc) priority = 100 state = {"test": "test"} matcher_on = on( "test", rule=rule, permission=permission, handlers=[handler], temp=True, expire_time=expire_time, priority=priority, block=True, state=state, ) def matcher_on_factory() -> type[Matcher]: return on( "test", rule=rule, permission=permission, handlers=[handler], temp=True, expire_time=expire_time, priority=priority, block=True, state=state, ) matcher_on_metaevent = on_metaevent( rule=rule, handlers=[handler], temp=True, expire_time=expire_time, priority=priority, block=True, state=state, ) matcher_on_message = on_message( rule=rule, permission=permission, handlers=[handler], temp=True, expire_time=expire_time, priority=priority, block=True, state=state, ) matcher_on_notice = on_notice( rule=rule, handlers=[handler], temp=True, expire_time=expire_time, priority=priority, block=True, state=state, ) matcher_on_request = on_request( rule=rule, handlers=[handler], temp=True, expire_time=expire_time, priority=priority, block=True, state=state, ) matcher_on_startswith = on_startswith( "test", rule=rule, permission=permission, handlers=[handler], temp=True, expire_time=expire_time, priority=priority, block=True, state=state, ) matcher_on_endswith = on_endswith( "test", rule=rule, permission=permission, handlers=[handler], temp=True, expire_time=expire_time, priority=priority, block=True, state=state, ) matcher_on_fullmatch = on_fullmatch( "test", rule=rule, permission=permission, handlers=[handler], temp=True, expire_time=expire_time, priority=priority, block=True, state=state, ) matcher_on_keyword = on_keyword( {"test"}, rule=rule, permission=permission, handlers=[handler], temp=True, expire_time=expire_time, priority=priority, block=True, state=state, ) matcher_on_command = on_command( "test", rule=rule, permission=permission, handlers=[handler], temp=True, expire_time=expire_time, priority=priority, block=True, state=state, ) matcher_on_shell_command = on_shell_command( "test", rule=rule, permission=permission, handlers=[handler], temp=True, expire_time=expire_time, priority=priority, block=True, state=state, ) matcher_on_regex = on_regex( "test", rule=rule, permission=permission, handlers=[handler], temp=True, expire_time=expire_time, priority=priority, block=True, state=state, ) class TestEvent(Event): ... matcher_on_type = on_type( TestEvent, rule=rule, permission=permission, handlers=[handler], temp=True, expire_time=expire_time, priority=priority, block=True, state=state, ) cmd_group = CommandGroup( "prefix", rule=rule, permission=permission, handlers=[handler], temp=True, expire_time=expire_time, priority=priority, block=True, state=state, ) matcher_prefix_cmd = cmd_group.command("sub", aliases={"help", ("help", "foo")}) matcher_prefix_shell_cmd = cmd_group.shell_command( "sub", aliases={"help", ("help", "foo")} ) cmd_group_prefix_aliases = CommandGroup( "prefix", prefix_aliases=True, rule=rule, permission=permission, handlers=[handler], temp=True, expire_time=expire_time, priority=priority, block=True, state=state, ) matcher_prefix_aliases_cmd = cmd_group_prefix_aliases.command( "sub", aliases={"help", ("help", "foo")} ) matcher_prefix_aliases_shell_cmd = cmd_group_prefix_aliases.shell_command( "sub", aliases={"help", ("help", "foo")} ) matcher_group = MatcherGroup( rule=rule, permission=permission, handlers=[handler], temp=True, expire_time=expire_time, priority=priority, block=True, state=state, ) matcher_group_on = matcher_group.on(type="test") matcher_group_on_metaevent = matcher_group.on_metaevent() matcher_group_on_message = matcher_group.on_message() matcher_group_on_notice = matcher_group.on_notice() matcher_group_on_request = matcher_group.on_request() matcher_group_on_startswith = matcher_group.on_startswith("test") matcher_group_on_endswith = matcher_group.on_endswith("test") matcher_group_on_fullmatch = matcher_group.on_fullmatch("test") matcher_group_on_keyword = matcher_group.on_keyword({"test"}) matcher_group_on_command = matcher_group.on_command("test") matcher_group_on_shell_command = matcher_group.on_shell_command("test") matcher_group_on_regex = matcher_group.on_regex("test") matcher_group_on_type = matcher_group.on_type(TestEvent) ================================================ FILE: tests/plugins/require.py ================================================ from nonebot import require test_require = require("export").test from plugins.export import test assert test is test_require, "Export Require Error" assert test() == "export", "Export Require Error" ================================================ FILE: tests/plugins.empty.toml ================================================ ================================================ FILE: tests/plugins.invalid.json ================================================ [] ================================================ FILE: tests/plugins.invalid.toml ================================================ [tool] nonebot = [] ================================================ FILE: tests/plugins.json ================================================ { "plugins": [], "plugin_dirs": [] } ================================================ FILE: tests/plugins.legacy.toml ================================================ [tool.nonebot] plugins = [] plugin_dirs = [] ================================================ FILE: tests/plugins.toml ================================================ [tool.nonebot] plugin_dirs = [] [tool.nonebot.plugins] "@local" = [] ================================================ FILE: tests/pyproject.toml ================================================ [tool.ruff] extend = "../pyproject.toml" [tool.ruff.lint.isort] known-first-party = ["nonebot", "fake_server", "utils"] ================================================ FILE: tests/python_3_12/plugins/aliased_param/__init__.py ================================================ from pathlib import Path from nonebot import load_plugins _sub_plugins = set() _sub_plugins |= load_plugins(str(Path(__file__).parent)) ================================================ FILE: tests/python_3_12/plugins/aliased_param/param_arg.py ================================================ from typing import Annotated from nonebot.adapters import Message from nonebot.params import Arg type AliasedArg = Annotated[Message, Arg()] async def aliased_arg(key: AliasedArg) -> Message: return key ================================================ FILE: tests/python_3_12/plugins/aliased_param/param_bot.py ================================================ from nonebot.adapters import Bot type AliasedBot = Bot async def get_aliased_bot(b: AliasedBot) -> Bot: return b ================================================ FILE: tests/python_3_12/plugins/aliased_param/param_depend.py ================================================ from typing import Annotated from nonebot import on_message from nonebot.params import Depends test_depends = on_message() runned = [] def dependency(): runned.append(1) return 1 type AliasedDepends = Annotated[int, Depends(dependency)] @test_depends.handle() async def aliased_depends(x: AliasedDepends): return x ================================================ FILE: tests/python_3_12/plugins/aliased_param/param_event.py ================================================ from nonebot.adapters import Event type AliasedEvent = Event async def aliased_event(e: AliasedEvent) -> Event: return e ================================================ FILE: tests/python_3_12/plugins/aliased_param/param_exception.py ================================================ type AliasedException = Exception async def aliased_exc(e: AliasedException) -> Exception: return e ================================================ FILE: tests/python_3_12/plugins/aliased_param/param_matcher.py ================================================ from nonebot.matcher import Matcher type AliasedMatcher = Matcher async def aliased_matcher(m: AliasedMatcher) -> Matcher: return m ================================================ FILE: tests/python_3_12/plugins/aliased_param/param_state.py ================================================ from nonebot.typing import T_State type AliasedState = T_State async def aliased_state(x: AliasedState) -> T_State: return x ================================================ FILE: tests/python_3_12/pyproject.toml ================================================ [tool.ruff] extend = "../pyproject.toml" target-version = "py312" ================================================ FILE: tests/test_adapters/test_adapter.py ================================================ from contextlib import asynccontextmanager from nonebug import App import pytest from nonebot.adapters import Bot from nonebot.drivers import ( URL, Driver, HTTPServerSetup, Request, Response, WebSocket, WebSocketServerSetup, ) from utils import FakeAdapter @pytest.mark.anyio async def test_adapter_connect(app: App, driver: Driver): last_connect_bot: Bot | None = None last_disconnect_bot: Bot | None = None def _fake_bot_connect(bot: Bot): nonlocal last_connect_bot last_connect_bot = bot def _fake_bot_disconnect(bot: Bot): nonlocal last_disconnect_bot last_disconnect_bot = bot with pytest.MonkeyPatch.context() as m: m.setattr(driver, "_bot_connect", _fake_bot_connect) m.setattr(driver, "_bot_disconnect", _fake_bot_disconnect) adapter = FakeAdapter(driver) async with app.test_api() as ctx: bot = ctx.create_bot(adapter=adapter) assert last_connect_bot is bot assert adapter.bots[bot.self_id] is bot assert last_disconnect_bot is bot assert bot.self_id not in adapter.bots @pytest.mark.parametrize( "driver", [ pytest.param("nonebot.drivers.fastapi:Driver", id="fastapi"), pytest.param("nonebot.drivers.quart:Driver", id="quart"), pytest.param( "nonebot.drivers.httpx:Driver", id="httpx", marks=pytest.mark.xfail( reason="not a server", raises=TypeError, strict=True ), ), pytest.param( "nonebot.drivers.websockets:Driver", id="websockets", marks=pytest.mark.xfail( reason="not a server", raises=TypeError, strict=True ), ), pytest.param( "nonebot.drivers.aiohttp:Driver", id="aiohttp", marks=pytest.mark.xfail( reason="not a server", raises=TypeError, strict=True ), ), ], indirect=True, ) def test_adapter_server(driver: Driver): last_http_setup: HTTPServerSetup | None = None last_ws_setup: WebSocketServerSetup | None = None def _fake_setup_http_server(setup: HTTPServerSetup): nonlocal last_http_setup last_http_setup = setup def _fake_setup_websocket_server(setup: WebSocketServerSetup): nonlocal last_ws_setup last_ws_setup = setup with pytest.MonkeyPatch.context() as m: m.setattr(driver, "setup_http_server", _fake_setup_http_server, raising=False) m.setattr( driver, "setup_websocket_server", _fake_setup_websocket_server, raising=False, ) async def handle_http(request: Request): return Response(200, content="test") async def handle_ws(ws: WebSocket): ... adapter = FakeAdapter(driver) setup = HTTPServerSetup(URL("/test"), "GET", "test", handle_http) adapter.setup_http_server(setup) assert last_http_setup is setup setup = WebSocketServerSetup(URL("/test"), "test", handle_ws) adapter.setup_websocket_server(setup) assert last_ws_setup is setup @pytest.mark.anyio @pytest.mark.parametrize( "driver", [ pytest.param( "nonebot.drivers.fastapi:Driver", id="fastapi", marks=pytest.mark.xfail( reason="not a http client", raises=TypeError, strict=True ), ), pytest.param( "nonebot.drivers.quart:Driver", id="quart", marks=pytest.mark.xfail( reason="not a http client", raises=TypeError, strict=True ), ), pytest.param("nonebot.drivers.httpx:Driver", id="httpx"), pytest.param( "nonebot.drivers.websockets:Driver", id="websockets", marks=pytest.mark.xfail( reason="not a http client", raises=TypeError, strict=True ), ), pytest.param("nonebot.drivers.aiohttp:Driver", id="aiohttp"), ], indirect=True, ) async def test_adapter_http_client(driver: Driver): last_request: Request | None = None async def _fake_request(request: Request): nonlocal last_request last_request = request with pytest.MonkeyPatch.context() as m: m.setattr(driver, "request", _fake_request, raising=False) adapter = FakeAdapter(driver) request = Request("GET", URL("/test")) await adapter.request(request) assert last_request is request @pytest.mark.anyio @pytest.mark.parametrize( "driver", [ pytest.param( "nonebot.drivers.fastapi:Driver", id="fastapi", marks=pytest.mark.xfail( reason="not a websocket client", raises=TypeError, strict=True ), ), pytest.param( "nonebot.drivers.quart:Driver", id="quart", marks=pytest.mark.xfail( reason="not a websocket client", raises=TypeError, strict=True ), ), pytest.param( "nonebot.drivers.httpx:Driver", id="httpx", marks=pytest.mark.xfail( reason="not a websocket client", raises=TypeError, strict=True ), ), pytest.param("nonebot.drivers.websockets:Driver", id="websockets"), pytest.param("nonebot.drivers.aiohttp:Driver", id="aiohttp"), ], indirect=True, ) async def test_adapter_websocket_client(driver: Driver): _fake_ws = object() _last_request: Request | None = None @asynccontextmanager async def _fake_websocket(setup: Request): nonlocal _last_request _last_request = setup yield _fake_ws with pytest.MonkeyPatch.context() as m: m.setattr(driver, "websocket", _fake_websocket, raising=False) adapter = FakeAdapter(driver) request = Request("GET", URL("/test")) async with adapter.websocket(request) as ws: assert _last_request is request assert ws is _fake_ws ================================================ FILE: tests/test_adapters/test_bot.py ================================================ from typing import Any import anyio from nonebug import App import pytest from nonebot.adapters import Bot from nonebot.exception import MockApiException @pytest.mark.anyio async def test_bot_call_api(app: App): async with app.test_api() as ctx: bot = ctx.create_bot() ctx.should_call_api("test", {}, True) result = await bot.call_api("test") assert result is True async with app.test_api() as ctx: bot = ctx.create_bot() ctx.should_call_api("test", {}, exception=RuntimeError("test")) with pytest.raises(RuntimeError, match="test"): await bot.call_api("test") @pytest.mark.anyio async def test_bot_calling_api_hook_simple(app: App): runned: bool = False async def calling_api_hook(bot: Bot, api: str, data: dict[str, Any]): nonlocal runned runned = True hooks = set() with pytest.MonkeyPatch.context() as m: m.setattr(Bot, "_calling_api_hook", hooks) Bot.on_calling_api(calling_api_hook) assert hooks == {calling_api_hook} async with app.test_api() as ctx: bot = ctx.create_bot() ctx.should_call_api("test", {}, True) result = await bot.call_api("test") assert runned is True assert result is True @pytest.mark.anyio async def test_bot_calling_api_hook_mock(app: App): runned: bool = False async def calling_api_hook(bot: Bot, api: str, data: dict[str, Any]): nonlocal runned runned = True raise MockApiException(False) hooks = set() with pytest.MonkeyPatch.context() as m: m.setattr(Bot, "_calling_api_hook", hooks) Bot.on_calling_api(calling_api_hook) assert hooks == {calling_api_hook} async with app.test_api() as ctx: bot = ctx.create_bot() result = await bot.call_api("test") assert runned is True assert result is False @pytest.mark.anyio async def test_bot_calling_api_hook_multi_mock(app: App): runned1: bool = False runned2: bool = False event = anyio.Event() async def calling_api_hook1(bot: Bot, api: str, data: dict[str, Any]): nonlocal runned1 runned1 = True event.set() raise MockApiException(1) async def calling_api_hook2(bot: Bot, api: str, data: dict[str, Any]): nonlocal runned2 runned2 = True with anyio.fail_after(1): await event.wait() raise MockApiException(2) hooks = set() with pytest.MonkeyPatch.context() as m: m.setattr(Bot, "_calling_api_hook", hooks) Bot.on_calling_api(calling_api_hook1) Bot.on_calling_api(calling_api_hook2) assert hooks == {calling_api_hook1, calling_api_hook2} async with app.test_api() as ctx: bot = ctx.create_bot() result = await bot.call_api("test") assert runned1 is True assert runned2 is True assert result == 1 @pytest.mark.anyio async def test_bot_called_api_hook_simple(app: App): runned: bool = False async def called_api_hook( bot: Bot, exception: Exception | None, api: str, data: dict[str, Any], result: Any, ): nonlocal runned runned = True hooks = set() with pytest.MonkeyPatch.context() as m: m.setattr(Bot, "_called_api_hook", hooks) Bot.on_called_api(called_api_hook) assert hooks == {called_api_hook} async with app.test_api() as ctx: bot = ctx.create_bot() ctx.should_call_api("test", {}, True) result = await bot.call_api("test") assert runned is True assert result is True @pytest.mark.anyio async def test_bot_called_api_hook_mock(app: App): runned: bool = False async def called_api_hook( bot: Bot, exception: Exception | None, api: str, data: dict[str, Any], result: Any, ): nonlocal runned runned = True raise MockApiException(False) hooks = set() with pytest.MonkeyPatch.context() as m: m.setattr(Bot, "_called_api_hook", hooks) Bot.on_called_api(called_api_hook) assert hooks == {called_api_hook} async with app.test_api() as ctx: bot = ctx.create_bot() ctx.should_call_api("test", {}, True) result = await bot.call_api("test") assert runned is True assert result is False runned = False async with app.test_api() as ctx: bot = ctx.create_bot() ctx.should_call_api("test", {}, exception=RuntimeError("test")) result = await bot.call_api("test") assert runned is True assert result is False @pytest.mark.anyio async def test_bot_called_api_hook_multi_mock(app: App): runned1: bool = False runned2: bool = False event = anyio.Event() async def called_api_hook1( bot: Bot, exception: Exception | None, api: str, data: dict[str, Any], result: Any, ): nonlocal runned1 runned1 = True event.set() raise MockApiException(1) async def called_api_hook2( bot: Bot, exception: Exception | None, api: str, data: dict[str, Any], result: Any, ): nonlocal runned2 runned2 = True with anyio.fail_after(1): await event.wait() raise MockApiException(2) hooks = set() with pytest.MonkeyPatch.context() as m: m.setattr(Bot, "_called_api_hook", hooks) Bot.on_called_api(called_api_hook1) Bot.on_called_api(called_api_hook2) assert hooks == {called_api_hook1, called_api_hook2} async with app.test_api() as ctx: bot = ctx.create_bot() ctx.should_call_api("test", {}, True) result = await bot.call_api("test") assert runned1 is True assert runned2 is True assert result == 1 ================================================ FILE: tests/test_adapters/test_message.py ================================================ from pydantic import ValidationError import pytest from nonebot.adapters import Message, MessageSegment from nonebot.compat import type_validate_python from utils import FakeMessage, FakeMessageSegment def test_segment_data(): assert len(FakeMessageSegment.text("text")) == 4 assert FakeMessageSegment.text("text").get("data") == {"text": "text"} assert list(FakeMessageSegment.text("text").keys()) == ["type", "data"] assert list(FakeMessageSegment.text("text").values()) == ["text", {"text": "text"}] assert list(FakeMessageSegment.text("text").items()) == [ ("type", "text"), ("data", {"text": "text"}), ] def test_segment_equal(): assert FakeMessageSegment("text", {"text": "text"}) == FakeMessageSegment( "text", {"text": "text"} ) assert FakeMessageSegment("text", {"text": "text"}) != FakeMessageSegment( "text", {"text": "other"} ) assert FakeMessageSegment("text", {"text": "text"}) != FakeMessageSegment( "other", {"text": "text"} ) def test_segment_add(): assert FakeMessageSegment.text("text") + FakeMessageSegment.text( "text" ) == FakeMessage([FakeMessageSegment.text("text"), FakeMessageSegment.text("text")]) assert FakeMessageSegment.text("text") + "text" == FakeMessage( [FakeMessageSegment.text("text"), FakeMessageSegment.text("text")] ) assert ( FakeMessageSegment.text("text") + FakeMessage([FakeMessageSegment.text("text")]) ) == FakeMessage([FakeMessageSegment.text("text"), FakeMessageSegment.text("text")]) assert "text" + FakeMessageSegment.text("text") == FakeMessage( [FakeMessageSegment.text("text"), FakeMessageSegment.text("text")] ) def test_segment_validate(): assert type_validate_python( FakeMessageSegment, {"type": "text", "data": {"text": "text"}, "extra": "should be ignored"}, ) == FakeMessageSegment.text("text") with pytest.raises(ValidationError): type_validate_python( type("FakeMessageSegment2", (MessageSegment,), {}), FakeMessageSegment.text("text"), ) with pytest.raises(ValidationError): type_validate_python(FakeMessageSegment, "some str") with pytest.raises(ValidationError): type_validate_python(FakeMessageSegment, {"data": {}}) def test_segment_join(): seg = FakeMessageSegment.text("test") iterable = [ FakeMessageSegment.text("first"), FakeMessage( [FakeMessageSegment.text("second"), FakeMessageSegment.text("third")] ), ] assert seg.join(iterable) == FakeMessage( [ FakeMessageSegment.text("first"), FakeMessageSegment.text("test"), FakeMessageSegment.text("second"), FakeMessageSegment.text("third"), ] ) def test_segment_copy(): origin = FakeMessageSegment.text("text") copy = origin.copy() assert origin is not copy assert origin == copy def test_message_add(): assert ( FakeMessage([FakeMessageSegment.text("text")]) + FakeMessageSegment.text("text") ) == FakeMessage([FakeMessageSegment.text("text"), FakeMessageSegment.text("text")]) assert FakeMessage([FakeMessageSegment.text("text")]) + "text" == FakeMessage( [FakeMessageSegment.text("text"), FakeMessageSegment.text("text")] ) assert ( FakeMessage([FakeMessageSegment.text("text")]) + FakeMessage([FakeMessageSegment.text("text")]) ) == FakeMessage([FakeMessageSegment.text("text"), FakeMessageSegment.text("text")]) assert "text" + FakeMessage([FakeMessageSegment.text("text")]) == FakeMessage( [FakeMessageSegment.text("text"), FakeMessageSegment.text("text")] ) msg = FakeMessage([FakeMessageSegment.text("text")]) msg += FakeMessageSegment.text("text") assert msg == FakeMessage( [FakeMessageSegment.text("text"), FakeMessageSegment.text("text")] ) def test_message_getitem(): message = FakeMessage( [ FakeMessageSegment.text("test"), FakeMessageSegment.image("test2"), FakeMessageSegment.image("test3"), FakeMessageSegment.text("test4"), ] ) assert message[0] == FakeMessageSegment.text("test") assert message[:2] == FakeMessage( [FakeMessageSegment.text("test"), FakeMessageSegment.image("test2")] ) assert message["image"] == FakeMessage( [FakeMessageSegment.image("test2"), FakeMessageSegment.image("test3")] ) assert message["image", 0] == FakeMessageSegment.image("test2") assert message["image", 0:2] == message["image"] assert message.index(message[0]) == 0 assert message.index("image") == 1 assert message.get("image") == message["image"] assert message.get("image", 114514) == message["image"] assert message.get("image", 1) == FakeMessage([message["image", 0]]) assert message.count("image") == 2 def test_message_validate(): assert type_validate_python(FakeMessage, FakeMessage([])) == FakeMessage([]) with pytest.raises(ValidationError): type_validate_python(type("FakeMessage2", (Message,), {}), FakeMessage([])) assert type_validate_python(FakeMessage, "text") == FakeMessage( [FakeMessageSegment.text("text")] ) assert type_validate_python( FakeMessage, {"type": "text", "data": {"text": "text"}} ) == FakeMessage([FakeMessageSegment.text("text")]) assert type_validate_python( FakeMessage, [FakeMessageSegment.text("text"), {"type": "text", "data": {"text": "text"}}], ) == FakeMessage([FakeMessageSegment.text("text"), FakeMessageSegment.text("text")]) with pytest.raises(ValidationError): type_validate_python(FakeMessage, object()) def test_message_contains(): message = FakeMessage( [ FakeMessageSegment.text("test"), FakeMessageSegment.image("test2"), FakeMessageSegment.image("test3"), FakeMessageSegment.text("test4"), ] ) assert message.has(FakeMessageSegment.text("test")) is True assert FakeMessageSegment.text("test") in message assert message.has("image") is True assert "image" in message assert message.has(FakeMessageSegment.text("foo")) is False assert FakeMessageSegment.text("foo") not in message assert message.has("foo") is False assert "foo" not in message assert not bool(FakeMessageSegment.text("")) msg_with_empty_seg = FakeMessage([FakeMessageSegment.text("")]) assert msg_with_empty_seg.has("text") is True assert "text" in msg_with_empty_seg def test_message_only(): message = FakeMessage( [ FakeMessageSegment.text("test"), FakeMessageSegment.text("test2"), ] ) assert message.only("text") is True assert message.only(FakeMessageSegment.text("test")) is False message = FakeMessage( [ FakeMessageSegment.text("test"), FakeMessageSegment.image("test2"), FakeMessageSegment.image("test3"), FakeMessageSegment.text("test4"), ] ) assert message.only("text") is False message = FakeMessage( [ FakeMessageSegment.text("test"), FakeMessageSegment.text("test"), ] ) assert message.only(FakeMessageSegment.text("test")) is True def test_message_join(): msg = FakeMessage([FakeMessageSegment.text("test")]) iterable = [ FakeMessageSegment.text("first"), FakeMessage( [FakeMessageSegment.text("second"), FakeMessageSegment.text("third")] ), ] assert msg.join(iterable) == FakeMessage( [ FakeMessageSegment.text("first"), FakeMessageSegment.text("test"), FakeMessageSegment.text("second"), FakeMessageSegment.text("third"), ] ) def test_message_include(): message = FakeMessage( [ FakeMessageSegment.text("test"), FakeMessageSegment.image("test2"), FakeMessageSegment.image("test3"), FakeMessageSegment.text("test4"), ] ) assert message.include("text") == FakeMessage( [ FakeMessageSegment.text("test"), FakeMessageSegment.text("test4"), ] ) def test_message_exclude(): message = FakeMessage( [ FakeMessageSegment.text("test"), FakeMessageSegment.image("test2"), FakeMessageSegment.image("test3"), FakeMessageSegment.text("test4"), ] ) assert message.exclude("image") == FakeMessage( [ FakeMessageSegment.text("test"), FakeMessageSegment.text("test4"), ] ) ================================================ FILE: tests/test_adapters/test_template.py ================================================ import pytest from nonebot.adapters import MessageTemplate from utils import FakeMessage, FakeMessageSegment, escape_text def test_template_basis(): template = MessageTemplate("{key:.3%}") formatted = template.format(key=0.123456789) assert formatted == "12.346%" def test_template_message(): template = FakeMessage.template("{a:custom}{b:text}{c:image}/{d}") @template.add_format_spec def custom(input: str) -> str: return f"{input}-custom!" with pytest.raises(ValueError, match="already exists"): template.add_format_spec(custom) format_args = { "a": "custom", "b": "text", "c": "https://example.com/test", "d": 114, } formatted = template.format(**format_args) assert template.format_map(format_args) == formatted assert formatted.extract_plain_text() == "custom-custom!text/114" assert str(formatted) == "custom-custom!text[fake:image]/114" def test_rich_template_message(): pic1, pic2, pic3 = ( FakeMessageSegment.image("file:///pic1.jpg"), FakeMessageSegment.image("file:///pic2.jpg"), FakeMessageSegment.image("file:///pic3.jpg"), ) template = FakeMessage.template("{}{}" + pic2 + "{}") result = template.format(pic1, "[fake:image]", pic3) assert result["image"] == FakeMessage([pic1, pic2, pic3]) assert str(result) == ( "[fake:image]" + escape_text("[fake:image]") + "[fake:image]" + "[fake:image]" ) def test_message_injection(): template = FakeMessage.template("{name}Is Bad") message = template.format(name="[fake:image]") assert message.extract_plain_text() == escape_text("[fake:image]Is Bad") def test_malformed_template(): positive_template = FakeMessage.template("{a}{b}") message = positive_template.format(a="a", b="b") assert message.extract_plain_text() == "ab" malformed_template = FakeMessage.template("{a.__init__}") with pytest.raises(ValueError, match="private attribute"): message = malformed_template.format(a="a") malformed_template = FakeMessage.template("{a[__builtins__]}") with pytest.raises(ValueError, match="private attribute"): message = malformed_template.format(a=globals()) malformed_template = MessageTemplate( "{a[__builtins__][__import__]}{b.__init__}", private_getattr=True ) message = malformed_template.format(a=globals(), b="b") ================================================ FILE: tests/test_broadcast.py ================================================ import sys from nonebug import App import pytest from nonebot import on_message from nonebot.adapters import Bot, Event from nonebot.exception import IgnoredException from nonebot.log import default_filter, default_format, logger from nonebot.matcher import Matcher import nonebot.message as message from nonebot.message import ( event_postprocessor, event_preprocessor, run_postprocessor, run_preprocessor, ) from nonebot.params import Depends from nonebot.typing import T_State from utils import make_fake_event async def _dependency() -> int: return 1 @pytest.mark.anyio async def test_event_preprocessor(app: App, monkeypatch: pytest.MonkeyPatch): with monkeypatch.context() as m: m.setattr(message, "_event_preprocessors", set()) runned = False @event_preprocessor async def test_preprocessor( bot: Bot, event: Event, state: T_State, sub: int = Depends(_dependency), default: int = 1, ): nonlocal runned runned = True assert test_preprocessor in { dependent.call for dependent in message._event_preprocessors } with app.provider.context({}): matcher = on_message() async with app.test_matcher(matcher) as ctx: bot = ctx.create_bot() event = make_fake_event()() ctx.receive_event(bot, event) assert runned, "event_preprocessor should runned" @pytest.mark.anyio async def test_event_preprocessor_ignore(app: App, monkeypatch: pytest.MonkeyPatch): with monkeypatch.context() as m: m.setattr(message, "_event_preprocessors", set()) @event_preprocessor async def test_preprocessor(): raise IgnoredException("pass") assert test_preprocessor in { dependent.call for dependent in message._event_preprocessors } runned = False async def handler(): nonlocal runned runned = True with app.provider.context({}): matcher = on_message(handlers=[handler]) async with app.test_matcher(matcher) as ctx: bot = ctx.create_bot() event = make_fake_event()() ctx.receive_event(bot, event) assert not runned, "matcher should not runned" @pytest.mark.anyio async def test_event_preprocessor_exception( app: App, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ): with monkeypatch.context() as m: m.setattr(message, "_event_preprocessors", set()) @event_preprocessor async def test_preprocessor(): raise RuntimeError("test") assert test_preprocessor in { dependent.call for dependent in message._event_preprocessors } runned = False async def handler(): nonlocal runned runned = True handler_id = logger.add( sys.stdout, level=0, diagnose=False, filter=default_filter, format=default_format, ) try: with app.provider.context({}): matcher = on_message(handlers=[handler]) async with app.test_matcher(matcher) as ctx: bot = ctx.create_bot() event = make_fake_event()() ctx.receive_event(bot, event) finally: logger.remove(handler_id) assert not runned, "matcher should not runned" assert "RuntimeError: test" in capsys.readouterr().out @pytest.mark.anyio async def test_event_postprocessor(app: App, monkeypatch: pytest.MonkeyPatch): with monkeypatch.context() as m: m.setattr(message, "_event_postprocessors", set()) runned = False @event_postprocessor async def test_postprocessor( bot: Bot, event: Event, state: T_State, sub: int = Depends(_dependency), default: int = 1, ): nonlocal runned runned = True assert test_postprocessor in { dependent.call for dependent in message._event_postprocessors } with app.provider.context({}): matcher = on_message() async with app.test_matcher(matcher) as ctx: bot = ctx.create_bot() event = make_fake_event()() ctx.receive_event(bot, event) assert runned, "event_postprocessor should runned" @pytest.mark.anyio async def test_event_postprocessor_exception( app: App, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ): with monkeypatch.context() as m: m.setattr(message, "_event_postprocessors", set()) @event_postprocessor async def test_postprocessor(): raise RuntimeError("test") assert test_postprocessor in { dependent.call for dependent in message._event_postprocessors } handler_id = logger.add( sys.stdout, level=0, diagnose=False, filter=default_filter, format=default_format, ) try: with app.provider.context({}): matcher = on_message() async with app.test_matcher(matcher) as ctx: bot = ctx.create_bot() event = make_fake_event()() ctx.receive_event(bot, event) finally: logger.remove(handler_id) assert "RuntimeError: test" in capsys.readouterr().out @pytest.mark.anyio async def test_run_preprocessor(app: App, monkeypatch: pytest.MonkeyPatch): with monkeypatch.context() as m: m.setattr(message, "_run_preprocessors", set()) runned = False @run_preprocessor async def test_preprocessor( bot: Bot, event: Event, state: T_State, matcher: Matcher, sub: int = Depends(_dependency), default: int = 1, ): nonlocal runned runned = True await matcher.send("test") assert test_preprocessor in { dependent.call for dependent in message._run_preprocessors } with app.provider.context({}): matcher = on_message() async with app.test_matcher(matcher) as ctx: bot = ctx.create_bot() event = make_fake_event()() ctx.receive_event(bot, event) ctx.should_call_send(event, "test", True, bot=bot) assert runned, "run_preprocessor should runned" @pytest.mark.anyio async def test_run_preprocessor_ignore(app: App, monkeypatch: pytest.MonkeyPatch): with monkeypatch.context() as m: m.setattr(message, "_run_preprocessors", set()) @run_preprocessor async def test_preprocessor(): raise IgnoredException("pass") assert test_preprocessor in { dependent.call for dependent in message._run_preprocessors } runned = False async def handler(): nonlocal runned runned = True with app.provider.context({}): matcher = on_message(handlers=[handler]) async with app.test_matcher(matcher) as ctx: bot = ctx.create_bot() event = make_fake_event()() ctx.receive_event(bot, event) assert not runned, "matcher should not runned" @pytest.mark.anyio async def test_run_preprocessor_exception( app: App, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ): with monkeypatch.context() as m: m.setattr(message, "_run_preprocessors", set()) @run_preprocessor async def test_preprocessor(): raise RuntimeError("test") assert test_preprocessor in { dependent.call for dependent in message._run_preprocessors } runned = False async def handler(): nonlocal runned runned = True handler_id = logger.add( sys.stdout, level=0, diagnose=False, filter=default_filter, format=default_format, ) try: with app.provider.context({}): matcher = on_message(handlers=[handler]) async with app.test_matcher(matcher) as ctx: bot = ctx.create_bot() event = make_fake_event()() ctx.receive_event(bot, event) finally: logger.remove(handler_id) assert not runned, "matcher should not runned" assert "RuntimeError: test" in capsys.readouterr().out @pytest.mark.anyio async def test_run_postprocessor(app: App, monkeypatch: pytest.MonkeyPatch): with monkeypatch.context() as m: m.setattr(message, "_run_postprocessors", set()) runned = False @run_postprocessor async def test_postprocessor( bot: Bot, event: Event, state: T_State, matcher: Matcher, exception: Exception | None, sub: int = Depends(_dependency), default: int = 1, ): nonlocal runned runned = True await matcher.send("test") assert test_postprocessor in { dependent.call for dependent in message._run_postprocessors } with app.provider.context({}): matcher = on_message() async with app.test_matcher(matcher) as ctx: bot = ctx.create_bot() event = make_fake_event()() ctx.receive_event(bot, event) ctx.should_call_send(event, "test", True, bot=bot) assert runned, "run_postprocessor should runned" @pytest.mark.anyio async def test_run_postprocessor_exception( app: App, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ): with monkeypatch.context() as m: m.setattr(message, "_run_postprocessors", set()) @run_postprocessor async def test_postprocessor(): raise RuntimeError("test") assert test_postprocessor in { dependent.call for dependent in message._run_postprocessors } handler_id = logger.add( sys.stdout, level=0, diagnose=False, filter=default_filter, format=default_format, ) try: with app.provider.context({}): matcher = on_message() async with app.test_matcher(matcher) as ctx: bot = ctx.create_bot() event = make_fake_event()() ctx.receive_event(bot, event) finally: logger.remove(handler_id) assert "RuntimeError: test" in capsys.readouterr().out ================================================ FILE: tests/test_compat.py ================================================ from dataclasses import dataclass from typing import Annotated, Any from pydantic import BaseModel, ValidationError import pytest from nonebot.compat import ( DEFAULT_CONFIG, FieldInfo, PydanticUndefined, Required, TypeAdapter, custom_validation, field_validator, model_dump, model_validator, type_validate_json, type_validate_python, ) def test_default_config(): assert DEFAULT_CONFIG.get("extra") == "allow" assert DEFAULT_CONFIG.get("arbitrary_types_allowed") is True def test_field_info(): # required should be convert to PydanticUndefined assert FieldInfo(Required).default is PydanticUndefined # field info should allow extra attributes assert FieldInfo(test="test").extra["test"] == "test" def test_field_validator(): class TestModel(BaseModel): foo: int bar: str @field_validator("foo") @classmethod def test_validator(cls, v: Any) -> Any: if v > 0: return v raise ValueError("test must be greater than 0") @field_validator("bar", mode="before") @classmethod def test_validator_before(cls, v: Any) -> Any: if not isinstance(v, str): v = str(v) return v assert type_validate_python(TestModel, {"foo": 1, "bar": "test"}).foo == 1 assert type_validate_python(TestModel, {"foo": 1, "bar": 123}).bar == "123" with pytest.raises(ValidationError): TestModel(foo=0, bar="test") def test_type_adapter(): t = TypeAdapter(Annotated[int, FieldInfo(ge=1)]) assert t.validate_python(2) == 2 with pytest.raises(ValidationError): t.validate_python(0) assert t.validate_json("2") == 2 with pytest.raises(ValidationError): t.validate_json("0") def test_model_dump(): class NestedModel(BaseModel): hidden: int shown: int class TestModel(BaseModel): test1: int test2: int nested: NestedModel items: list[NestedModel] 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(): class TestModel(BaseModel): foo: int bar: str @model_validator(mode="before") @classmethod def test_validator_before(cls, data: Any) -> Any: if isinstance(data, dict): if "foo" not in data: data["foo"] = 1 return data @model_validator(mode="after") @classmethod def test_validator_after(cls, data: Any) -> Any: if isinstance(data, dict): if data["bar"] == "test": raise ValueError("bar should not be test") elif data.bar == "test": raise ValueError("bar should not be test") return data assert type_validate_python(TestModel, {"bar": "aaa"}).foo == 1 with pytest.raises(ValidationError): type_validate_python(TestModel, {"foo": 1, "bar": "test"}) def test_custom_validation(): called = [] @custom_validation @dataclass class TestModel: test: int @classmethod def __get_validators__(cls): yield cls._validate_1 yield cls._validate_2 @classmethod def _validate_1(cls, v: Any) -> Any: called.append(1) return v @classmethod def _validate_2(cls, v: Any) -> Any: called.append(2) return cls(test=v["test"]) assert type_validate_python(TestModel, {"test": 1}) == TestModel(test=1) assert called == [1, 2] def test_validate_json(): class TestModel(BaseModel): test1: int test2: str test3: bool test4: dict test5: list test6: int | None assert type_validate_json( TestModel, "{" ' "test1": 1,' ' "test2": "2",' ' "test3": true,' ' "test4": {},' ' "test5": [],' ' "test6": null' "}", ) == TestModel(test1=1, test2="2", test3=True, test4={}, test5=[], test6=None) ================================================ FILE: tests/test_config.py ================================================ from typing import TYPE_CHECKING from pydantic import BaseModel, Field import pytest from nonebot.compat import PYDANTIC_V2, LegacyUnionField from nonebot.config import DOTENV_TYPE, BaseSettings, SettingsConfig, SettingsError class Simple(BaseModel): a: int = 0 b: int = 0 c: dict = {} complex: list = [] class Example(BaseSettings): if TYPE_CHECKING: _env_file: DOTENV_TYPE | None = ".env", ".env.example" _env_nested_delimiter: str | None = "__" if PYDANTIC_V2: model_config = SettingsConfig( env_file=(".env", ".env.example"), env_nested_delimiter="__" ) else: class Config( # pyright: ignore[reportIncompatibleVariableOverride] SettingsConfig ): env_file = ".env", ".env.example" env_nested_delimiter = "__" simple: str = "" int_str: int | str = LegacyUnionField(default="") complex: list[int] = Field(default=[1]) complex_none: list[int] | None = None complex_union: int | list[int] = 1 nested: Simple = Simple() nested_inner: Simple = Simple() aliased_simple: str = Field(default="", alias="alias_simple") class ExampleWithoutDelimiter(Example): if PYDANTIC_V2: model_config = SettingsConfig(env_nested_delimiter=None) else: class Config( # pyright: ignore[reportIncompatibleVariableOverride] SettingsConfig ): env_nested_delimiter = None def test_config_no_env(): config = Example(_env_file=None) assert config.simple == "" with pytest.raises(AttributeError): config.common_config def test_config_with_env(): config = Example(_env_file=(".env", ".env.example")) assert config.simple == "simple" assert config.int_str == 123 assert config.complex == [1, 2, 3] assert config.complex_none is None assert config.complex_union == [1, 2, 3] assert config.nested.a == 1 assert config.nested.b == 2 assert config.nested.c == {"c": "3"} assert config.nested.complex == [1, 2, 3] with pytest.raises(AttributeError): config.nested__b with pytest.raises(AttributeError): config.nested__c__c with pytest.raises(AttributeError): config.nested__complex assert config.nested_inner.a == 1 assert config.nested_inner.b == 2 with pytest.raises(AttributeError): config.nested_inner__a with pytest.raises(AttributeError): config.nested_inner__b assert config.aliased_simple == "aliased_simple" assert config.common_config == "common" assert config.other_simple == "simple" assert config.other_nested == {"a": 1, "b": 2} with pytest.raises(AttributeError): config.other_nested__b assert config.other_nested_inner == {"a": 1, "b": 2} with pytest.raises(AttributeError): config.other_nested_inner__a with pytest.raises(AttributeError): config.other_nested_inner__b def test_config_error_env(): with pytest.MonkeyPatch().context() as m: m.setenv("COMPLEX", "not json") with pytest.raises(SettingsError): Example(_env_file=(".env", ".env.example")) def test_config_without_delimiter(): config = ExampleWithoutDelimiter() assert config.nested.a == 1 assert config.nested.b == 0 assert config.nested__b == 2 assert config.nested.c == {} assert config.nested__c__c == 3 assert config.nested.complex == [] assert config.nested__complex == [1, 2, 3] assert config.nested_inner.a == 0 assert config.nested_inner.b == 0 assert config.other_nested == {"a": 1} assert config.other_nested__b == 2 with pytest.raises(AttributeError): config.other_nested_inner assert config.other_nested_inner__a == 1 assert config.other_nested_inner__b == 2 ================================================ FILE: tests/test_driver.py ================================================ from http.cookies import SimpleCookie import json from typing import Any from aiohttp import ClientSession, ClientWebSocketResponse, WSMessage, WSMsgType import anyio from nonebug import App import pytest from nonebot.adapters import Bot from nonebot.dependencies import Dependent from nonebot.drivers import ( URL, ASGIMixin, Driver, HTTPClientMixin, HTTPServerSetup, Request, Response, Timeout, WebSocket, WebSocketClientMixin, WebSocketServerSetup, ) from nonebot.drivers.aiohttp import WebSocket as AiohttpWebSocket from nonebot.exception import WebSocketClosed from nonebot.params import Depends from utils import FakeAdapter @pytest.mark.anyio @pytest.mark.parametrize( "driver", [pytest.param("nonebot.drivers.none:Driver", id="none")], indirect=True ) async def test_lifespan(driver: Driver): adapter = FakeAdapter(driver) start_log = [] ready_log = [] shutdown_log = [] @driver.on_startup async def _startup1(): assert start_log == [] start_log.append(1) @driver.on_startup async def _startup2(): assert start_log == [1] start_log.append(2) @adapter.on_ready def _ready1(): assert start_log == [1, 2] assert ready_log == [] ready_log.append(1) @adapter.on_ready def _ready2(): assert ready_log == [1] ready_log.append(2) @driver.on_shutdown async def _shutdown1(): assert shutdown_log == [2] shutdown_log.append(1) @driver.on_shutdown async def _shutdown2(): assert shutdown_log == [] shutdown_log.append(2) async with driver._lifespan: assert start_log == [1, 2] assert ready_log == [1, 2] assert shutdown_log == [2, 1] @pytest.mark.anyio @pytest.mark.parametrize( "driver", [ pytest.param("nonebot.drivers.fastapi:Driver", id="fastapi"), pytest.param("nonebot.drivers.quart:Driver", id="quart"), ], indirect=True, ) async def test_http_server(app: App, driver: Driver): assert isinstance(driver, ASGIMixin) async def _handle_http(request: Request) -> Response: assert request.content in (b"test", "test") return Response(200, content="test") http_setup = HTTPServerSetup(URL("/http_test"), "POST", "http_test", _handle_http) driver.setup_http_server(http_setup) async with app.test_server(driver.asgi) as ctx: client = ctx.get_client() response = await client.post("/http_test", data="test") assert response.status_code == 200 assert response.text == "test" await anyio.sleep(1) @pytest.mark.anyio @pytest.mark.parametrize( "driver", [ pytest.param("nonebot.drivers.fastapi:Driver", id="fastapi"), pytest.param("nonebot.drivers.quart:Driver", id="quart"), ], indirect=True, ) async def test_websocket_server(app: App, driver: Driver): assert isinstance(driver, ASGIMixin) async def _handle_ws(ws: WebSocket) -> None: await ws.accept() data = await ws.receive() assert data == "ping" await ws.send("pong") data = await ws.receive() assert data == b"ping" await ws.send(b"pong") data = await ws.receive_text() assert data == "ping" await ws.send("pong") data = await ws.receive_bytes() assert data == b"ping" await ws.send(b"pong") with pytest.raises(WebSocketClosed, match=r"code=1000"): await ws.receive() ws_setup = WebSocketServerSetup(URL("/ws_test"), "ws_test", _handle_ws) driver.setup_websocket_server(ws_setup) async with app.test_server(driver.asgi) as ctx: client = ctx.get_client() async with client.websocket_connect("/ws_test") as ws: await ws.send_text("ping") assert await ws.receive_text() == "pong" await ws.send_bytes(b"ping") assert await ws.receive_bytes() == b"pong" await ws.send_text("ping") assert await ws.receive_text() == "pong" await ws.send_bytes(b"ping") assert await ws.receive_bytes() == b"pong" await ws.close(code=1000) await anyio.sleep(1) @pytest.mark.anyio @pytest.mark.parametrize( "driver", [ pytest.param("nonebot.drivers.fastapi:Driver", id="fastapi"), pytest.param("nonebot.drivers.quart:Driver", id="quart"), ], indirect=True, ) async def test_cross_context(app: App, driver: Driver): assert isinstance(driver, ASGIMixin) ws: WebSocket | None = None ws_ready = anyio.Event() ws_should_close = anyio.Event() # create a background task before the ws connection established async def background_task(): try: await ws_ready.wait() assert ws is not None await ws.send("ping") data = await ws.receive() assert data == "pong" finally: ws_should_close.set() async def _handle_ws(websocket: WebSocket) -> None: nonlocal ws await websocket.accept() ws = websocket ws_ready.set() await ws_should_close.wait() await websocket.close() ws_setup = WebSocketServerSetup(URL("/ws_test"), "ws_test", _handle_ws) driver.setup_websocket_server(ws_setup) async with anyio.create_task_group() as tg, app.test_server(driver.asgi) as ctx: tg.start_soon(background_task) client = ctx.get_client() async with client.websocket_connect("/ws_test") as websocket: try: data = await websocket.receive_text() assert data == "ping" await websocket.send_text("pong") except Exception as e: if not e.args or "websocket.close" not in str(e.args[0]): raise await anyio.sleep(1) @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(driver: Driver, server_url: URL): assert isinstance(driver, HTTPClientMixin) # simple post with query, headers, cookies and content request = Request( "POST", server_url, params={"param": "test"}, headers={"X-Test": "test"}, cookies={"session": "test"}, content="test", timeout=Timeout(total=4, connect=2, read=2), ) response = await driver.request(request) assert server_url.host is not None request_raw_url = Request( "POST", ( server_url.scheme.encode("ascii"), server_url.host.encode("ascii"), server_url.port, server_url.path.encode("ascii"), ), params={"param": "test"}, headers={"X-Test": "test"}, cookies={"session": "test"}, content="test", timeout=Timeout(total=4, connect=2, read=2), ) assert request.url == request_raw_url.url, ( "request.url should be equal to request_raw_url.url" ) assert response.status_code == 200 assert response.content data = json.loads(response.content) assert data["method"] == "POST" assert data["args"] == {"param": "test"} assert data["headers"].get("X-Test") == "test" assert data["headers"].get("Cookie") == "session=test" assert data["data"] == "test" # post with data body request = Request("POST", server_url, data={"form": "test"}) response = await driver.request(request) assert response.status_code == 200 assert response.content data = json.loads(response.content) assert data["method"] == "POST" assert data["form"] == {"form": "test"} # post with json body request = Request("POST", server_url, json={"json": "test"}) response = await driver.request(request) assert response.status_code == 200 assert response.content data = json.loads(response.content) assert data["method"] == "POST" assert data["json"] == {"json": "test"} # post with files and form data request = Request( "POST", server_url, data={"form": "test"}, files=[ ("test1", b"test"), ("test2", ("test.txt", b"test")), ("test3", ("test.txt", b"test", "text/plain")), ], ) response = await driver.request(request) assert response.status_code == 200 assert response.content data = json.loads(response.content) assert data["method"] == "POST" assert data["form"] == {"form": "test"} assert data["files"] == { "test1": "test", "test2": "test", "test3": "test", }, "file parsing error" # post stream request with query, headers, cookies and content request = Request( "POST", server_url, params={"param": "stream"}, headers={"X-Test": "stream"}, cookies={"session": "stream"}, content="stream_test" * 1024, timeout=Timeout(total=4, connect=2, read=2), ) chunks = [] async for resp in driver.stream_request(request, chunk_size=4): assert resp.status_code == 200 assert resp.content chunks.append(resp.content) assert all(len(chunk) == 4 for chunk in chunks[:-1]) data = json.loads(b"".join(chunks)) assert data["method"] == "POST" assert data["args"] == {"param": "stream"} assert data["headers"].get("X-Test") == "stream" assert data["headers"].get("Cookie") == "session=stream" assert data["data"] == "stream_test" * 1024 # post stream request with data body request = Request("POST", server_url, data={"form": "test"}) chunks = [] async for resp in driver.stream_request(request, chunk_size=4): assert resp.status_code == 200 assert resp.content chunks.append(resp.content) assert all(len(chunk) == 4 for chunk in chunks[:-1]) data = json.loads(b"".join(chunks)) assert data["method"] == "POST" assert data["form"] == {"form": "test"} # post stream request with json body request = Request("POST", server_url, json={"json": "test"}) chunks = [] async for resp in driver.stream_request(request, chunk_size=4): assert resp.status_code == 200 assert resp.content chunks.append(resp.content) assert all(len(chunk) == 4 for chunk in chunks[:-1]) data = json.loads(b"".join(chunks)) assert data["method"] == "POST" assert data["json"] == {"json": "test"} # post stream request with files and form data request = Request( "POST", server_url, data={"form": "test"}, files=[ ("test1", b"test"), ("test2", ("test.txt", b"test")), ("test3", ("test.txt", b"test", "text/plain")), ], ) chunks = [] async for resp in driver.stream_request(request, chunk_size=4): assert response.status_code == 200 assert response.content chunks.append(resp.content) assert all(len(chunk) == 4 for chunk in chunks[:-1]) data = json.loads(b"".join(chunks)) assert data["method"] == "POST" assert data["form"] == {"form": "test"} assert data["files"] == { "test1": "test", "test2": "test", "test3": "test", }, "file parsing error" await anyio.sleep(1) @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_session(driver: Driver, server_url: URL): assert isinstance(driver, HTTPClientMixin) session = driver.get_session( params={"session": "test"}, headers={"X-Session": "test"}, cookies={"session": "test"}, ) request = Request("GET", server_url) with pytest.raises(RuntimeError): await session.request(request) with pytest.raises(RuntimeError): # noqa: PT012 async with session: async with session: ... async with session as session: # simple post with query, headers, cookies and content request = Request( "POST", server_url, params={"param": "test"}, headers={"X-Test": "test"}, cookies={"cookie": "test"}, content="test", timeout=Timeout(total=4, connect=2, read=2), ) response = await session.request(request) assert response.status_code == 200 assert response.content data = json.loads(response.content) assert data["method"] == "POST" assert data["args"] == {"session": "test", "param": "test"} assert data["headers"].get("X-Session") == "test" assert data["headers"].get("X-Test") == "test" assert { key: cookie.value for key, cookie in SimpleCookie(data["headers"].get("Cookie")).items() } == { "session": "test", "cookie": "test", } assert data["data"] == "test" # post with data body request = Request("POST", server_url, data={"form": "test"}) response = await session.request(request) assert response.status_code == 200 assert response.content data = json.loads(response.content) assert data["method"] == "POST" assert data["args"] == {"session": "test"} assert data["headers"].get("X-Session") == "test" assert { key: cookie.value for key, cookie in SimpleCookie(data["headers"].get("Cookie")).items() } == {"session": "test"} assert data["form"] == {"form": "test"} # post with json body request = Request("POST", server_url, json={"json": "test"}) response = await session.request(request) assert response.status_code == 200 assert response.content data = json.loads(response.content) assert data["method"] == "POST" assert data["args"] == {"session": "test"} assert data["headers"].get("X-Session") == "test" assert { key: cookie.value for key, cookie in SimpleCookie(data["headers"].get("Cookie")).items() } == {"session": "test"} assert data["json"] == {"json": "test"} # post with files and form data request = Request( "POST", server_url, data={"form": "test"}, files=[ ("test1", b"test"), ("test2", ("test.txt", b"test")), ("test3", ("test.txt", b"test", "text/plain")), ], ) response = await session.request(request) assert response.status_code == 200 assert response.content data = json.loads(response.content) assert data["method"] == "POST" assert data["args"] == {"session": "test"} assert data["headers"].get("X-Session") == "test" assert { key: cookie.value for key, cookie in SimpleCookie(data["headers"].get("Cookie")).items() } == {"session": "test"} assert data["form"] == {"form": "test"} assert data["files"] == { "test1": "test", "test2": "test", "test3": "test", }, "file parsing error" # post stream request with query, headers, cookies and content request = Request( "POST", server_url, params={"param": "stream"}, headers={"X-Test": "stream"}, cookies={"cookie": "stream"}, content="stream_test" * 1024, timeout=Timeout(total=4, connect=2, read=2), ) chunks = [] async for resp in session.stream_request(request, chunk_size=4): assert resp.status_code == 200 assert resp.content chunks.append(resp.content) assert all(len(chunk) == 4 for chunk in chunks[:-1]) data = json.loads(b"".join(chunks)) assert data["method"] == "POST" assert data["args"] == {"session": "test", "param": "stream"} assert data["headers"].get("X-Session") == "test" assert data["headers"].get("X-Test") == "stream" assert { key: cookie.value for key, cookie in SimpleCookie(data["headers"].get("Cookie")).items() } == {"session": "test", "cookie": "stream"} assert data["data"] == "stream_test" * 1024 # post stream request with data body request = Request("POST", server_url, data={"form": "test"}) chunks = [] async for resp in session.stream_request(request, chunk_size=4): assert resp.status_code == 200 assert resp.content chunks.append(resp.content) assert all(len(chunk) == 4 for chunk in chunks[:-1]) data = json.loads(b"".join(chunks)) assert data["method"] == "POST" assert data["args"] == {"session": "test"} assert data["headers"].get("X-Session") == "test" assert { key: cookie.value for key, cookie in SimpleCookie(data["headers"].get("Cookie")).items() } == {"session": "test"} assert data["form"] == {"form": "test"} # post stream request with json body request = Request("POST", server_url, json={"json": "test"}) chunks = [] async for resp in session.stream_request(request, chunk_size=4): assert resp.status_code == 200 assert resp.content chunks.append(resp.content) assert all(len(chunk) == 4 for chunk in chunks[:-1]) data = json.loads(b"".join(chunks)) assert data["method"] == "POST" assert data["args"] == {"session": "test"} assert data["headers"].get("X-Session") == "test" assert { key: cookie.value for key, cookie in SimpleCookie(data["headers"].get("Cookie")).items() } == {"session": "test"} assert data["json"] == {"json": "test"} # post stream request with files and form data request = Request( "POST", server_url, data={"form": "test"}, files=[ ("test1", b"test"), ("test2", ("test.txt", b"test")), ("test3", ("test.txt", b"test", "text/plain")), ], ) chunks = [] async for resp in session.stream_request(request, chunk_size=4): assert resp.status_code == 200 assert resp.content chunks.append(resp.content) assert all(len(chunk) == 4 for chunk in chunks[:-1]) data = json.loads(b"".join(chunks)) assert data["method"] == "POST" assert data["args"] == {"session": "test"} assert data["headers"].get("X-Session") == "test" assert { key: cookie.value for key, cookie in SimpleCookie(data["headers"].get("Cookie")).items() } == {"session": "test"} assert data["form"] == {"form": "test"} assert data["files"] == { "test1": "test", "test2": "test", "test3": "test", }, "file parsing error" 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(driver: Driver, server_url: URL): assert isinstance(driver, WebSocketClientMixin) request = Request("GET", server_url.with_scheme("ws")) async with driver.websocket(request) as ws: await ws.send("test") assert await ws.receive() == "test" await ws.send(b"test") assert await ws.receive() == b"test" await ws.send_text("test") assert await ws.receive_text() == "test" await ws.send_bytes(b"test") assert await ws.receive_bytes() == b"test" await ws.send("quit") with pytest.raises(WebSocketClosed, match=r"code=1000"): await ws.receive() await anyio.sleep(1) @pytest.mark.anyio @pytest.mark.parametrize( ("msg_type"), [ pytest.param("CLOSE", id="aiohttp-close"), pytest.param("CLOSING", id="aiohttp-closing"), pytest.param("CLOSED", id="aiohttp-closed"), ], ) async def test_aiohttp_websocket_close_frame(msg_type: str) -> None: class DummyWS(ClientWebSocketResponse): def __init__(self) -> None: pass @property def close_code(self) -> None: return None @property def closed(self) -> bool: return True async def receive(self, timeout: float | None = None) -> WSMessage: # noqa: ASYNC109 return WSMessage(type=WSMsgType[msg_type], data=None, extra=None) async with ClientSession() as session: ws = AiohttpWebSocket( request=Request("GET", "ws://example.com"), session=session, websocket=DummyWS(), ) with pytest.raises(WebSocketClosed, match=r"code=1006"): await ws.receive() @pytest.mark.parametrize( ("driver", "driver_type"), [ pytest.param( "nonebot.drivers.fastapi:Driver+nonebot.drivers.aiohttp:Mixin", "fastapi+aiohttp", id="fastapi+aiohttp", ), pytest.param( "~httpx:Driver+~websockets", "none+httpx+websockets", id="httpx+websockets", ), ], indirect=["driver"], ) def test_combine_driver(driver: Driver, driver_type: str): assert driver.type == driver_type @pytest.mark.anyio async def test_bot_connect_hook(app: App, driver: Driver): with pytest.MonkeyPatch.context() as m: conn_hooks: set[Dependent[Any]] = set() disconn_hooks: set[Dependent[Any]] = set() m.setattr(Driver, "_bot_connection_hook", conn_hooks) m.setattr(Driver, "_bot_disconnection_hook", disconn_hooks) conn_should_be_called = False disconn_should_be_called = False dependency_should_be_run = False dependency_should_be_cleaned = False async def dependency(): nonlocal dependency_should_be_run, dependency_should_be_cleaned dependency_should_be_run = True try: yield 1 finally: dependency_should_be_cleaned = True @driver.on_bot_connect async def conn_hook(foo: Bot, dep: int = Depends(dependency), default: int = 1): nonlocal conn_should_be_called if foo is not bot: pytest.fail("on_bot_connect hook called with wrong bot") if dep != 1: pytest.fail("on_bot_connect hook called with wrong dependency") if default != 1: pytest.fail("on_bot_connect hook called with wrong default value") conn_should_be_called = True @driver.on_bot_disconnect async def disconn_hook( foo: Bot, dep: int = Depends(dependency), default: int = 1 ): nonlocal disconn_should_be_called if foo is not bot: pytest.fail("on_bot_disconnect hook called with wrong bot") if dep != 1: pytest.fail("on_bot_connect hook called with wrong dependency") if default != 1: pytest.fail("on_bot_connect hook called with wrong default value") disconn_should_be_called = True if conn_hook not in {hook.call for hook in conn_hooks}: # type: ignore pytest.fail("on_bot_connect hook not registered") if disconn_hook not in {hook.call for hook in disconn_hooks}: # type: ignore pytest.fail("on_bot_disconnect hook not registered") async with app.test_api() as ctx: bot = ctx.create_bot() await anyio.sleep(1) if not conn_should_be_called: pytest.fail("on_bot_connect hook not called") if not disconn_should_be_called: pytest.fail("on_bot_disconnect hook not called") if not dependency_should_be_run: pytest.fail("dependency not run") if not dependency_should_be_cleaned: pytest.fail("dependency not cleaned") ================================================ FILE: tests/test_echo.py ================================================ from nonebug import App import pytest from utils import FakeMessage, FakeMessageSegment, make_fake_event @pytest.mark.anyio async def test_echo(app: App): from nonebot.plugins.echo import echo async with app.test_matcher(echo) as ctx: bot = ctx.create_bot() message = FakeMessage("/echo 123") event = make_fake_event(_message=message)() ctx.receive_event(bot, event) ctx.should_call_send(event, FakeMessage("123"), True, bot=bot) message = FakeMessageSegment.text("/echo 123") + FakeMessageSegment.image( "test" ) event = make_fake_event(_message=message)() ctx.receive_event(bot, event) ctx.should_call_send( event, FakeMessageSegment.text("123") + FakeMessageSegment.image("test"), True, bot=bot, ) message = FakeMessage("/echo") event = make_fake_event(_message=message)() ctx.receive_event(bot, event) ================================================ FILE: tests/test_init.py ================================================ from nonebug import App import pytest import nonebot from nonebot import ( get_adapter, get_adapters, get_app, get_asgi, get_bot, get_bots, get_driver, ) from nonebot.drivers import ASGIMixin, Driver, ReverseDriver def test_init(): env = nonebot.get_driver().env assert env == "test" config = nonebot.get_driver().config assert config.nickname == {"test"} assert config.superusers == {"test", "fake:faketest"} assert config.api_timeout is None assert config.simple_none is None assert config.config_from_env == {"test": "test"} assert config.config_override == "new" assert config.config_from_init == "init" assert config.common_config == "common" assert config.common_override == "new" assert config.nested_dict == {"a": 1, "b": 2, "c": {"d": 3}} assert config.nested_missing_dict == {"a": 1, "b": {"c": 2}} assert config.not_nested == "some string" def test_get_driver(monkeypatch: pytest.MonkeyPatch): with monkeypatch.context() as m: m.setattr(nonebot, "_driver", None) with pytest.raises(ValueError, match="initialized"): get_driver() def test_get_asgi(): driver = get_driver() assert isinstance(driver, ReverseDriver) assert isinstance(driver, ASGIMixin) assert get_asgi() == driver.asgi def test_get_app(): driver = get_driver() assert isinstance(driver, ReverseDriver) assert isinstance(driver, ASGIMixin) assert get_app() == driver.server_app @pytest.mark.anyio async def test_get_adapter(app: App, monkeypatch: pytest.MonkeyPatch): async with app.test_api() as ctx: adapter = ctx.create_adapter() adapter_name = adapter.get_name() with monkeypatch.context() as m: m.setattr(Driver, "_adapters", {adapter_name: adapter}) assert get_adapters() == {adapter_name: adapter} assert get_adapter(adapter_name) is adapter assert get_adapter(adapter.__class__) is adapter with pytest.raises(ValueError, match="registered"): get_adapter("not exist") def test_run(monkeypatch: pytest.MonkeyPatch): runned = False def mock_run(*args, **kwargs): nonlocal runned runned = True assert args == ("arg",) assert kwargs == {"kwarg": "kwarg"} driver = get_driver() with monkeypatch.context() as m: m.setattr(driver, "run", mock_run) nonebot.run("arg", kwarg="kwarg") assert runned def test_get_bot(app: App, monkeypatch: pytest.MonkeyPatch): driver = get_driver() with pytest.raises(ValueError, match="no bots"): get_bot() with monkeypatch.context() as m: m.setattr(driver, "_bots", {"test": "test"}) assert get_bot() == "test" assert get_bot("test") == "test" assert get_bots() == {"test": "test"} ================================================ FILE: tests/test_matcher/test_matcher.py ================================================ from pathlib import Path import sys from nonebug import App import pytest from nonebot import get_plugin from nonebot.matcher import Matcher, matchers from nonebot.message import _check_matcher, check_and_run_matcher from nonebot.permission import Permission, User from nonebot.rule import Rule from utils import FakeMessage, make_fake_event def test_matcher_info(app: App): from plugins.matcher.matcher_info import matcher assert issubclass(matcher, Matcher) assert matcher.type == "message" assert matcher.priority == 1 assert matcher.temp is False assert matcher.expire_time is None assert matcher.block is True assert matcher._source assert matcher._source.module_name == "plugins.matcher.matcher_info" assert matcher.module is sys.modules["plugins.matcher.matcher_info"] assert matcher.module_name == "plugins.matcher.matcher_info" assert matcher._source.plugin_id == "matcher:matcher_info" assert matcher._source.plugin_name == "matcher_info" assert matcher.plugin is get_plugin("matcher:matcher_info") assert matcher.plugin_id == "matcher:matcher_info" assert matcher.plugin_name == "matcher_info" assert ( matcher._source.file == (Path(__file__).parent.parent / "plugins/matcher/matcher_info.py").absolute() ) assert matcher._source.lineno == 3 @pytest.mark.anyio async def test_matcher_check(app: App): async def falsy(): return False async def truthy(): return True async def error(): raise RuntimeError event = make_fake_event(_type="test")() with app.provider.context({}): test_perm_falsy = Matcher.new(permission=Permission(falsy)) async with app.test_api() as ctx: bot = ctx.create_bot() assert await _check_matcher(test_perm_falsy, bot, event, {}) is False test_perm_truthy = Matcher.new(permission=Permission(truthy)) async with app.test_api() as ctx: bot = ctx.create_bot() assert await _check_matcher(test_perm_truthy, bot, event, {}) is True test_perm_error = Matcher.new(permission=Permission(error)) async with app.test_api() as ctx: bot = ctx.create_bot() assert await _check_matcher(test_perm_error, bot, event, {}) is False test_rule_falsy = Matcher.new(rule=Rule(falsy)) async with app.test_api() as ctx: bot = ctx.create_bot() assert await _check_matcher(test_rule_falsy, bot, event, {}) is False test_rule_truthy = Matcher.new(rule=Rule(truthy)) async with app.test_api() as ctx: bot = ctx.create_bot() assert await _check_matcher(test_rule_truthy, bot, event, {}) is True test_rule_error = Matcher.new(rule=Rule(error)) async with app.test_api() as ctx: bot = ctx.create_bot() assert await _check_matcher(test_rule_error, bot, event, {}) is False @pytest.mark.anyio async def test_matcher_handle(app: App): from plugins.matcher.matcher_process import test_handle message = FakeMessage("text") event = make_fake_event(_message=message)() assert len(test_handle.handlers) == 1 async with app.test_matcher(test_handle) as ctx: bot = ctx.create_bot() ctx.receive_event(bot, event) ctx.should_call_send(event, "send", "result", at_sender=True) ctx.should_finished() @pytest.mark.anyio async def test_matcher_got(app: App): from plugins.matcher.matcher_process import test_got message = FakeMessage("text") event = make_fake_event(_message=message)() message_next = FakeMessage("text_next") event_next = make_fake_event(_message=message_next)() assert len(test_got.handlers) == 1 async with app.test_matcher(test_got) as ctx: bot = ctx.create_bot() ctx.receive_event(bot, event) ctx.should_call_send(event, "prompt key1", "result1") ctx.receive_event(bot, event) ctx.should_call_send(event, "prompt key2", "result2") ctx.receive_event(bot, event) ctx.should_call_send(event, "reject", "result3", at_sender=True) ctx.should_rejected() ctx.receive_event(bot, event_next) @pytest.mark.anyio async def test_matcher_receive(app: App): from plugins.matcher.matcher_process import test_receive message = FakeMessage("text") event = make_fake_event(_message=message)() assert len(test_receive.handlers) == 1 async with app.test_matcher(test_receive) as ctx: bot = ctx.create_bot() ctx.receive_event(bot, event) ctx.receive_event(bot, event) ctx.receive_event(bot, event) ctx.should_call_send(event, "pause", "result", at_sender=True) ctx.should_paused() @pytest.mark.anyio async def test_matcher_combine(app: App): from plugins.matcher.matcher_process import test_combine message = FakeMessage("text") event = make_fake_event(_message=message)() message_next = FakeMessage("text_next") event_next = make_fake_event(_message=message_next)() assert len(test_combine.handlers) == 1 async with app.test_matcher(test_combine) as ctx: bot = ctx.create_bot() ctx.receive_event(bot, event) ctx.receive_event(bot, event) ctx.receive_event(bot, event) ctx.should_rejected() ctx.receive_event(bot, event_next) ctx.should_rejected() ctx.receive_event(bot, event_next) ctx.should_rejected() ctx.receive_event(bot, event_next) @pytest.mark.anyio async def test_matcher_preset(app: App): from plugins.matcher.matcher_process import test_preset message = FakeMessage("text") event = make_fake_event(_message=message)() message_next = FakeMessage("text_next") event_next = make_fake_event(_message=message_next)() assert len(test_preset.handlers) == 2 async with app.test_matcher(test_preset) as ctx: bot = ctx.create_bot() ctx.receive_event(bot, event) ctx.receive_event(bot, event) ctx.should_rejected() ctx.receive_event(bot, event_next) @pytest.mark.anyio async def test_matcher_overload(app: App): from plugins.matcher.matcher_process import test_overload message = FakeMessage("text") event = make_fake_event(_message=message)() assert len(test_overload.handlers) == 2 async with app.test_matcher(test_overload) as ctx: bot = ctx.create_bot() ctx.receive_event(bot, event) ctx.should_finished() @pytest.mark.anyio async def test_matcher_destroy(app: App): from plugins.matcher.matcher_process import test_destroy async with app.test_matcher(test_destroy): assert len(matchers) == 1 assert len(matchers[test_destroy.priority]) == 1 assert matchers[test_destroy.priority][0] is test_destroy test_destroy.destroy() assert len(matchers[test_destroy.priority]) == 0 @pytest.mark.anyio async def test_type_updater(app: App): from plugins.matcher.matcher_type import test_custom_updater, test_type_updater event = make_fake_event()() assert test_type_updater.type == "test" async with app.test_api() as ctx: bot = ctx.create_bot() matcher = test_type_updater() new_type = await matcher.update_type(bot, event) assert new_type == "message" assert test_custom_updater.type == "test" async with app.test_api() as ctx: bot = ctx.create_bot() matcher = test_custom_updater() new_type = await matcher.update_type(bot, event) assert new_type == "custom" @pytest.mark.anyio async def test_default_permission_updater(app: App): from plugins.matcher.matcher_permission import ( default_permission, test_permission_updater, ) event = make_fake_event(_session_id="test")() assert test_permission_updater.permission is default_permission async with app.test_api() as ctx: bot = ctx.create_bot() matcher = test_permission_updater() new_perm = await matcher.update_permission(bot, event) assert len(new_perm.checkers) == 1 checker = next(iter(new_perm.checkers)).call assert isinstance(checker, User) assert checker.users == ("test",) assert checker.perm is default_permission @pytest.mark.anyio async def test_user_permission_updater(app: App): from plugins.matcher.matcher_permission import ( default_permission, test_user_permission_updater, ) event = make_fake_event(_session_id="test")() user_permission = next(iter(test_user_permission_updater.permission.checkers)).call assert isinstance(user_permission, User) assert user_permission.perm is default_permission async with app.test_api() as ctx: bot = ctx.create_bot() matcher = test_user_permission_updater() new_perm = await matcher.update_permission(bot, event) assert len(new_perm.checkers) == 1 checker = next(iter(new_perm.checkers)).call assert isinstance(checker, User) assert checker.users == ("test",) assert checker.perm is default_permission @pytest.mark.anyio async def test_custom_permission_updater(app: App): from plugins.matcher.matcher_permission import ( default_permission, new_permission, test_custom_updater, ) event = make_fake_event(_session_id="test")() assert test_custom_updater.permission is default_permission async with app.test_api() as ctx: bot = ctx.create_bot() matcher = test_custom_updater() new_perm = await matcher.update_permission(bot, event) assert new_perm is new_permission @pytest.mark.anyio async def test_run(app: App): with app.provider.context({}): assert not matchers event = make_fake_event()() async def reject(): await Matcher.reject() test_reject = Matcher.new(handlers=[reject]) async with app.test_api() as ctx: bot = ctx.create_bot() await test_reject().run(bot, event, {}) assert len(matchers[0]) == 1 assert len(matchers[0][0].handlers) == 1 del matchers[0] async def pause(): await Matcher.pause() test_pause = Matcher.new(handlers=[pause]) async with app.test_api() as ctx: bot = ctx.create_bot() await test_pause().run(bot, event, {}) assert len(matchers[0]) == 1 assert len(matchers[0][0].handlers) == 0 @pytest.mark.anyio async def test_temp(app: App): from plugins.matcher.matcher_expire import test_temp_matcher event = make_fake_event(_type="test")() with app.provider.context({test_temp_matcher.priority: [test_temp_matcher]}): async with app.test_api() as ctx: bot = ctx.create_bot() assert test_temp_matcher in matchers[test_temp_matcher.priority] await check_and_run_matcher(test_temp_matcher, bot, event, {}) assert test_temp_matcher not in matchers[test_temp_matcher.priority] @pytest.mark.anyio async def test_datetime_expire(app: App): from plugins.matcher.matcher_expire import test_datetime_matcher event = make_fake_event()() with app.provider.context( {test_datetime_matcher.priority: [test_datetime_matcher]} ): async with app.test_matcher(test_datetime_matcher) as ctx: bot = ctx.create_bot() assert test_datetime_matcher in matchers[test_datetime_matcher.priority] await check_and_run_matcher(test_datetime_matcher, bot, event, {}) assert test_datetime_matcher not in matchers[test_datetime_matcher.priority] @pytest.mark.anyio async def test_timedelta_expire(app: App): from plugins.matcher.matcher_expire import test_timedelta_matcher event = make_fake_event()() with app.provider.context( {test_timedelta_matcher.priority: [test_timedelta_matcher]} ): async with app.test_api() as ctx: bot = ctx.create_bot() assert test_timedelta_matcher in matchers[test_timedelta_matcher.priority] await check_and_run_matcher(test_timedelta_matcher, bot, event, {}) assert ( test_timedelta_matcher not in matchers[test_timedelta_matcher.priority] ) ================================================ FILE: tests/test_matcher/test_provider.py ================================================ from nonebug import App from nonebot.matcher import DEFAULT_PROVIDER_CLASS, matchers def test_manager(app: App): try: default_provider = matchers.provider matchers.set_provider(DEFAULT_PROVIDER_CLASS) assert default_provider == matchers.provider finally: matchers.provider = app.provider ================================================ FILE: tests/test_param.py ================================================ from contextlib import suppress import re import sys from exceptiongroup import BaseExceptionGroup from nonebug import App import pytest from nonebot.consts import ( ARG_KEY, CMD_ARG_KEY, CMD_KEY, CMD_START_KEY, CMD_WHITESPACE_KEY, ENDSWITH_KEY, FULLMATCH_KEY, KEYWORD_KEY, PREFIX_KEY, RAW_CMD_KEY, RECEIVE_KEY, REGEX_MATCHED, SHELL_ARGS, SHELL_ARGV, STARTSWITH_KEY, ) from nonebot.dependencies import Dependent from nonebot.exception import PausedException, RejectedException, TypeMisMatch from nonebot.matcher import Matcher from nonebot.params import ( ArgParam, BotParam, DefaultParam, DependParam, EventParam, ExceptionParam, MatcherParam, StateParam, ) from utils import FakeMessage, make_fake_event UNKNOWN_PARAM = "Unknown parameter" @pytest.mark.anyio @pytest.mark.xfail( ((3, 13) <= sys.version_info < (3, 13, 8)) or ((3, 14) <= sys.version_info < (3, 14, 1)), reason="CPython Bug, see python/cpython#137317, python/cpython#137862", ) async def test_depend(app: App): from plugins.param.param_depend import ( ClassDependency, annotated_class_depend, annotated_depend, annotated_multi_depend, annotated_prior_depend, cache_exception_func1, cache_exception_func2, class_depend, depends, runned, sub_type_mismatch, test_depends, validate, validate_fail, validate_field, validate_field_fail, ) async with app.test_dependent(depends, allow_types=[DependParam]) as ctx: ctx.should_return(1) assert len(runned) == 1 assert runned[0] == 1 runned.clear() async with app.test_matcher(test_depends) as ctx: bot = ctx.create_bot() event_next = make_fake_event()() ctx.receive_event(bot, event_next) assert runned == [1, 1] runned.clear() async with app.test_dependent(class_depend, allow_types=[DependParam]) as ctx: ctx.should_return(ClassDependency(x=1, y=2)) async with app.test_dependent(annotated_depend, allow_types=[DependParam]) as ctx: ctx.should_return(1) async with app.test_dependent( annotated_prior_depend, allow_types=[DependParam] ) as ctx: ctx.should_return(1) async with app.test_dependent( annotated_multi_depend, allow_types=[DependParam] ) as ctx: ctx.should_return(1) assert runned == [1, 1, 1] runned.clear() async with app.test_dependent( annotated_class_depend, allow_types=[DependParam] ) as ctx: ctx.should_return(ClassDependency(x=1, y=2)) with pytest.raises((TypeMisMatch, BaseExceptionGroup)) as exc_info: # noqa: PT012 async with app.test_dependent( sub_type_mismatch, allow_types=[DependParam, BotParam] ) as ctx: bot = ctx.create_bot() ctx.pass_params(bot=bot) if isinstance(exc_info.value, BaseExceptionGroup): assert exc_info.group_contains(TypeMisMatch) async with app.test_dependent(validate, allow_types=[DependParam]) as ctx: ctx.should_return(1) with pytest.raises((TypeMisMatch, BaseExceptionGroup)) as exc_info: async with app.test_dependent(validate_fail, allow_types=[DependParam]) as ctx: ... if isinstance(exc_info.value, BaseExceptionGroup): assert exc_info.group_contains(TypeMisMatch) async with app.test_dependent(validate_field, allow_types=[DependParam]) as ctx: ctx.should_return(1) with pytest.raises((TypeMisMatch, BaseExceptionGroup)) as exc_info: async with app.test_dependent( validate_field_fail, allow_types=[DependParam] ) as ctx: ... if isinstance(exc_info.value, BaseExceptionGroup): assert exc_info.group_contains(TypeMisMatch) # test cache reuse when exception raised dependency_cache = {} with pytest.raises((TypeMisMatch, BaseExceptionGroup)) as exc_info: async with app.test_dependent( cache_exception_func1, allow_types=[DependParam] ) as ctx: ctx.pass_params(dependency_cache=dependency_cache) if isinstance(exc_info.value, BaseExceptionGroup): assert exc_info.group_contains(TypeMisMatch) # dependency solve tasks should be shielded even if one of them raises an exception assert len(dependency_cache) == 2 async with app.test_dependent( cache_exception_func2, allow_types=[DependParam] ) as ctx: ctx.pass_params(dependency_cache=dependency_cache) ctx.should_return(1) @pytest.mark.anyio @pytest.mark.skipif( sys.version_info < (3, 12), reason="TypeAlias requires Python 3.12 or higher" ) async def test_aliased_depend(app: App): from python_3_12.plugins.aliased_param.param_depend import aliased_depends, runned async with app.test_dependent(aliased_depends, allow_types=[DependParam]) as ctx: ctx.should_return(1) assert len(runned) == 1 assert runned[0] == 1 runned.clear() @pytest.mark.anyio async def test_bot(app: App): from plugins.param.param_bot import ( FooBot, generic_bot, generic_bot_none, get_bot, legacy_bot, not_bot, not_legacy_bot, postpone_bot, sub_bot, union_bot, ) async with app.test_dependent(get_bot, allow_types=[BotParam]) as ctx: bot = ctx.create_bot() ctx.pass_params(bot=bot) ctx.should_return(bot) async with app.test_dependent(postpone_bot, allow_types=[BotParam]) as ctx: bot = ctx.create_bot() ctx.pass_params(bot=bot) ctx.should_return(bot) async with app.test_dependent(legacy_bot, allow_types=[BotParam]) as ctx: bot = ctx.create_bot() ctx.pass_params(bot=bot) ctx.should_return(bot) with pytest.raises(ValueError, match=UNKNOWN_PARAM): app.test_dependent(not_legacy_bot, allow_types=[BotParam]) async with app.test_dependent(sub_bot, allow_types=[BotParam]) as ctx: bot = ctx.create_bot(base=FooBot) ctx.pass_params(bot=bot) ctx.should_return(bot) with pytest.raises((TypeMisMatch, BaseExceptionGroup)) as exc_info: # noqa: PT012 async with app.test_dependent(sub_bot, allow_types=[BotParam]) as ctx: bot = ctx.create_bot() ctx.pass_params(bot=bot) if isinstance(exc_info.value, BaseExceptionGroup): assert exc_info.group_contains(TypeMisMatch) async with app.test_dependent(union_bot, allow_types=[BotParam]) as ctx: bot = ctx.create_bot(base=FooBot) ctx.pass_params(bot=bot) ctx.should_return(bot) async with app.test_dependent(generic_bot, allow_types=[BotParam]) as ctx: bot = ctx.create_bot() ctx.pass_params(bot=bot) ctx.should_return(bot) async with app.test_dependent(generic_bot_none, allow_types=[BotParam]) as ctx: bot = ctx.create_bot() ctx.pass_params(bot=bot) ctx.should_return(bot) with pytest.raises(ValueError, match=UNKNOWN_PARAM): app.test_dependent(not_bot, allow_types=[BotParam]) @pytest.mark.anyio @pytest.mark.skipif( sys.version_info < (3, 12), reason="TypeAlias requires Python 3.12 or higher" ) async def test_aliased_bot(app: App): from python_3_12.plugins.aliased_param.param_bot import get_aliased_bot async with app.test_dependent(get_aliased_bot, allow_types=[BotParam]) as ctx: bot = ctx.create_bot() ctx.pass_params(bot=bot) ctx.should_return(bot) @pytest.mark.anyio async def test_event(app: App): from plugins.param.param_event import ( FooEvent, event, event_message, event_plain_text, event_to_me, event_type, generic_event, generic_event_none, legacy_event, not_event, not_legacy_event, postpone_event, sub_event, union_event, ) fake_message = FakeMessage("text") fake_event = make_fake_event(_message=fake_message)() fake_fooevent = make_fake_event(_base=FooEvent)() async with app.test_dependent(event, allow_types=[EventParam]) as ctx: ctx.pass_params(event=fake_event) ctx.should_return(fake_event) async with app.test_dependent(postpone_event, allow_types=[EventParam]) as ctx: ctx.pass_params(event=fake_event) ctx.should_return(fake_event) async with app.test_dependent(legacy_event, allow_types=[EventParam]) as ctx: ctx.pass_params(event=fake_event) ctx.should_return(fake_event) with pytest.raises(ValueError, match=UNKNOWN_PARAM): app.test_dependent(not_legacy_event, allow_types=[EventParam]) async with app.test_dependent(sub_event, allow_types=[EventParam]) as ctx: ctx.pass_params(event=fake_fooevent) ctx.should_return(fake_fooevent) with pytest.raises((TypeMisMatch, BaseExceptionGroup)) as exc_info: async with app.test_dependent(sub_event, allow_types=[EventParam]) as ctx: ctx.pass_params(event=fake_event) if isinstance(exc_info.value, BaseExceptionGroup): assert exc_info.group_contains(TypeMisMatch) async with app.test_dependent(union_event, allow_types=[EventParam]) as ctx: ctx.pass_params(event=fake_fooevent) ctx.should_return(fake_fooevent) async with app.test_dependent(generic_event, allow_types=[EventParam]) as ctx: ctx.pass_params(event=fake_event) ctx.should_return(fake_event) async with app.test_dependent(generic_event_none, allow_types=[EventParam]) as ctx: ctx.pass_params(event=fake_event) ctx.should_return(fake_event) with pytest.raises(ValueError, match=UNKNOWN_PARAM): app.test_dependent(not_event, allow_types=[EventParam]) async with app.test_dependent( event_type, allow_types=[EventParam, DependParam] ) as ctx: ctx.pass_params(event=fake_event) ctx.should_return(fake_event.get_type()) async with app.test_dependent( event_message, allow_types=[EventParam, DependParam] ) as ctx: ctx.pass_params(event=fake_event) ctx.should_return(fake_event.get_message()) async with app.test_dependent( event_plain_text, allow_types=[EventParam, DependParam] ) as ctx: ctx.pass_params(event=fake_event) ctx.should_return(fake_event.get_plaintext()) async with app.test_dependent( event_to_me, allow_types=[EventParam, DependParam] ) as ctx: ctx.pass_params(event=fake_event) ctx.should_return(fake_event.is_tome()) @pytest.mark.anyio @pytest.mark.skipif( sys.version_info < (3, 12), reason="TypeAlias requires Python 3.12 or higher" ) async def test_aliased_event(app: App): from python_3_12.plugins.aliased_param.param_event import aliased_event fake_message = FakeMessage("text") fake_event = make_fake_event(_message=fake_message)() async with app.test_dependent(aliased_event, allow_types=[EventParam]) as ctx: ctx.pass_params(event=fake_event) ctx.should_return(fake_event) @pytest.mark.anyio async def test_state(app: App): from plugins.param.param_state import ( command, command_arg, command_start, command_whitespace, endswith, fullmatch, keyword, legacy_state, not_legacy_state, postpone_state, raw_command, regex_dict, regex_group, regex_matched, regex_str, shell_command_args, shell_command_argv, startswith, state, ) fake_message = FakeMessage("text") fake_matched = re.match(r"\[cq:(?P.*?),(?P.*?)\]", "[cq:test,arg=value]") fake_state = { PREFIX_KEY: { CMD_KEY: ("cmd",), RAW_CMD_KEY: "/cmd", CMD_START_KEY: "/", CMD_ARG_KEY: fake_message, CMD_WHITESPACE_KEY: " ", }, SHELL_ARGV: ["-h"], SHELL_ARGS: {"help": True}, REGEX_MATCHED: fake_matched, STARTSWITH_KEY: "startswith", ENDSWITH_KEY: "endswith", FULLMATCH_KEY: "fullmatch", KEYWORD_KEY: "keyword", } async with app.test_dependent(state, allow_types=[StateParam]) as ctx: ctx.pass_params(state=fake_state) ctx.should_return(fake_state) async with app.test_dependent(postpone_state, allow_types=[StateParam]) as ctx: ctx.pass_params(state=fake_state) ctx.should_return(fake_state) async with app.test_dependent(legacy_state, allow_types=[StateParam]) as ctx: ctx.pass_params(state=fake_state) ctx.should_return(fake_state) with pytest.raises(ValueError, match=UNKNOWN_PARAM): app.test_dependent(not_legacy_state, allow_types=[StateParam]) async with app.test_dependent( command, allow_types=[StateParam, DependParam] ) as ctx: ctx.pass_params(state=fake_state) ctx.should_return(fake_state[PREFIX_KEY][CMD_KEY]) async with app.test_dependent( raw_command, allow_types=[StateParam, DependParam] ) as ctx: ctx.pass_params(state=fake_state) ctx.should_return(fake_state[PREFIX_KEY][RAW_CMD_KEY]) async with app.test_dependent( command_arg, allow_types=[StateParam, DependParam] ) as ctx: ctx.pass_params(state=fake_state) ctx.should_return(fake_state[PREFIX_KEY][CMD_ARG_KEY]) async with app.test_dependent( command_start, allow_types=[StateParam, DependParam] ) as ctx: ctx.pass_params(state=fake_state) ctx.should_return(fake_state[PREFIX_KEY][CMD_START_KEY]) async with app.test_dependent( command_whitespace, allow_types=[StateParam, DependParam] ) as ctx: ctx.pass_params(state=fake_state) ctx.should_return(fake_state[PREFIX_KEY][CMD_WHITESPACE_KEY]) async with app.test_dependent( shell_command_argv, allow_types=[StateParam, DependParam] ) as ctx: ctx.pass_params(state=fake_state) ctx.should_return(fake_state[SHELL_ARGV]) async with app.test_dependent( shell_command_args, allow_types=[StateParam, DependParam] ) as ctx: ctx.pass_params(state=fake_state) ctx.should_return(fake_state[SHELL_ARGS]) async with app.test_dependent( regex_matched, allow_types=[StateParam, DependParam] ) as ctx: ctx.pass_params(state=fake_state) ctx.should_return(fake_state[REGEX_MATCHED]) async with app.test_dependent( regex_str, allow_types=[StateParam, DependParam] ) as ctx: ctx.pass_params(state=fake_state) ctx.should_return( ("[cq:test,arg=value]", "test", "arg=value", ("test", "arg=value")) ) async with app.test_dependent( regex_group, allow_types=[StateParam, DependParam] ) as ctx: ctx.pass_params(state=fake_state) ctx.should_return(("test", "arg=value")) async with app.test_dependent( regex_dict, allow_types=[StateParam, DependParam] ) as ctx: ctx.pass_params(state=fake_state) ctx.should_return({"type": "test", "arg": "arg=value"}) async with app.test_dependent( startswith, allow_types=[StateParam, DependParam] ) as ctx: ctx.pass_params(state=fake_state) ctx.should_return(fake_state[STARTSWITH_KEY]) async with app.test_dependent( endswith, allow_types=[StateParam, DependParam] ) as ctx: ctx.pass_params(state=fake_state) ctx.should_return(fake_state[ENDSWITH_KEY]) async with app.test_dependent( fullmatch, allow_types=[StateParam, DependParam] ) as ctx: ctx.pass_params(state=fake_state) ctx.should_return(fake_state[FULLMATCH_KEY]) async with app.test_dependent( keyword, allow_types=[StateParam, DependParam] ) as ctx: ctx.pass_params(state=fake_state) ctx.should_return(fake_state[KEYWORD_KEY]) @pytest.mark.anyio @pytest.mark.skipif( sys.version_info < (3, 12), reason="TypeAlias requires Python 3.12 or higher" ) async def test_aliased_state(app: App): from python_3_12.plugins.aliased_param.param_state import aliased_state fake_message = FakeMessage("text") fake_matched = re.match(r"\[cq:(?P.*?),(?P.*?)\]", "[cq:test,arg=value]") fake_state = { PREFIX_KEY: { CMD_KEY: ("cmd",), RAW_CMD_KEY: "/cmd", CMD_START_KEY: "/", CMD_ARG_KEY: fake_message, CMD_WHITESPACE_KEY: " ", }, SHELL_ARGV: ["-h"], SHELL_ARGS: {"help": True}, REGEX_MATCHED: fake_matched, STARTSWITH_KEY: "startswith", ENDSWITH_KEY: "endswith", FULLMATCH_KEY: "fullmatch", KEYWORD_KEY: "keyword", } async with app.test_dependent(aliased_state, allow_types=[StateParam]) as ctx: ctx.pass_params(state=fake_state) ctx.should_return(fake_state) @pytest.mark.anyio async def test_matcher(app: App): from plugins.param.param_matcher import ( FooMatcher, generic_matcher, generic_matcher_none, last_receive, legacy_matcher, matcher, not_legacy_matcher, not_matcher, pause_prompt_result, postpone_matcher, receive, receive_prompt_result, sub_matcher, union_matcher, ) fake_matcher = Matcher() foo_matcher = FooMatcher() async with app.test_dependent(matcher, allow_types=[MatcherParam]) as ctx: ctx.pass_params(matcher=fake_matcher) ctx.should_return(fake_matcher) async with app.test_dependent(postpone_matcher, allow_types=[MatcherParam]) as ctx: ctx.pass_params(matcher=fake_matcher) ctx.should_return(fake_matcher) async with app.test_dependent(legacy_matcher, allow_types=[MatcherParam]) as ctx: ctx.pass_params(matcher=fake_matcher) ctx.should_return(fake_matcher) with pytest.raises(ValueError, match=UNKNOWN_PARAM): app.test_dependent(not_legacy_matcher, allow_types=[MatcherParam]) async with app.test_dependent(sub_matcher, allow_types=[MatcherParam]) as ctx: ctx.pass_params(matcher=foo_matcher) ctx.should_return(foo_matcher) with pytest.raises((TypeMisMatch, BaseExceptionGroup)) as exc_info: async with app.test_dependent(sub_matcher, allow_types=[MatcherParam]) as ctx: ctx.pass_params(matcher=fake_matcher) if isinstance(exc_info.value, BaseExceptionGroup): assert exc_info.group_contains(TypeMisMatch) async with app.test_dependent(union_matcher, allow_types=[MatcherParam]) as ctx: ctx.pass_params(matcher=foo_matcher) ctx.should_return(foo_matcher) async with app.test_dependent(generic_matcher, allow_types=[MatcherParam]) as ctx: ctx.pass_params(matcher=fake_matcher) ctx.should_return(fake_matcher) async with app.test_dependent( generic_matcher_none, allow_types=[MatcherParam] ) as ctx: ctx.pass_params(matcher=fake_matcher) ctx.should_return(fake_matcher) with pytest.raises(ValueError, match=UNKNOWN_PARAM): app.test_dependent(not_matcher, allow_types=[MatcherParam]) event = make_fake_event()() fake_matcher.set_receive("test", event) event_next = make_fake_event()() fake_matcher.set_receive("", event_next) async with app.test_dependent( receive, allow_types=[MatcherParam, DependParam] ) as ctx: ctx.pass_params(matcher=fake_matcher) ctx.should_return(event) async with app.test_dependent( last_receive, allow_types=[MatcherParam, DependParam] ) as ctx: ctx.pass_params(matcher=fake_matcher) ctx.should_return(event_next) fake_matcher.set_target(RECEIVE_KEY.format(id="test"), cache=False) async with app.test_api() as ctx: bot = ctx.create_bot() ctx.should_call_send(event, "test", result=True, bot=bot) with fake_matcher.ensure_context(bot, event): with suppress(RejectedException): await fake_matcher.reject("test") async with app.test_dependent( receive_prompt_result, allow_types=[MatcherParam, DependParam] ) as ctx: ctx.pass_params(matcher=fake_matcher) ctx.should_return(True) async with app.test_api() as ctx: bot = ctx.create_bot() ctx.should_call_send(event, "test", result=False, bot=bot) with fake_matcher.ensure_context(bot, event): fake_matcher.set_target("test") with suppress(PausedException): await fake_matcher.pause("test") async with app.test_dependent( pause_prompt_result, allow_types=[MatcherParam, DependParam] ) as ctx: ctx.pass_params(matcher=fake_matcher) ctx.should_return(False) @pytest.mark.anyio @pytest.mark.skipif( sys.version_info < (3, 12), reason="TypeAlias requires Python 3.12 or higher" ) async def test_aliased_matcher(app: App): from python_3_12.plugins.aliased_param.param_matcher import aliased_matcher fake_matcher = Matcher() async with app.test_dependent(aliased_matcher, allow_types=[MatcherParam]) as ctx: ctx.pass_params(matcher=fake_matcher) ctx.should_return(fake_matcher) @pytest.mark.anyio async def test_arg(app: App): from plugins.param.param_arg import ( annotated_arg, annotated_arg_plain_text, annotated_arg_prompt_result, annotated_arg_str, annotated_multi_arg, annotated_prior_arg, arg, arg_plain_text, arg_str, ) matcher = Matcher() event = make_fake_event()() message = FakeMessage("text") matcher.set_arg("key", message) async with app.test_dependent(arg, allow_types=[ArgParam]) as ctx: ctx.pass_params(matcher=matcher) ctx.should_return(message) async with app.test_dependent(arg_str, allow_types=[ArgParam]) as ctx: ctx.pass_params(matcher=matcher) ctx.should_return(str(message)) async with app.test_dependent(arg_plain_text, allow_types=[ArgParam]) as ctx: ctx.pass_params(matcher=matcher) ctx.should_return(message.extract_plain_text()) async with app.test_dependent(annotated_arg, allow_types=[ArgParam]) as ctx: ctx.pass_params(matcher=matcher) ctx.should_return(message) async with app.test_dependent(annotated_arg_str, allow_types=[ArgParam]) as ctx: ctx.pass_params(matcher=matcher) ctx.should_return(str(message)) async with app.test_dependent( annotated_arg_plain_text, allow_types=[ArgParam] ) as ctx: ctx.pass_params(matcher=matcher) ctx.should_return(message.extract_plain_text()) matcher.set_target(ARG_KEY.format(key="key"), cache=False) async with app.test_api() as ctx: bot = ctx.create_bot() ctx.should_call_send(event, "test", result="arg", bot=bot) with matcher.ensure_context(bot, event): with suppress(RejectedException): await matcher.reject("test") async with app.test_dependent( annotated_arg_prompt_result, allow_types=[ArgParam] ) as ctx: ctx.pass_params(matcher=matcher) ctx.should_return("arg") async with app.test_dependent(annotated_multi_arg, allow_types=[ArgParam]) as ctx: ctx.pass_params(matcher=matcher) ctx.should_return(message.extract_plain_text()) async with app.test_dependent(annotated_prior_arg, allow_types=[ArgParam]) as ctx: ctx.pass_params(matcher=matcher) ctx.should_return(message.extract_plain_text()) @pytest.mark.anyio @pytest.mark.skipif( sys.version_info < (3, 12), reason="TypeAlias requires Python 3.12 or higher" ) async def test_aliased_arg(app: App): from python_3_12.plugins.aliased_param.param_arg import aliased_arg matcher = Matcher() message = FakeMessage("text") matcher.set_arg("key", message) async with app.test_dependent(aliased_arg, allow_types=[ArgParam]) as ctx: ctx.pass_params(matcher=matcher) ctx.should_return(message) @pytest.mark.anyio async def test_exception(app: App): from plugins.param.param_exception import exc, legacy_exc exception = ValueError("test") async with app.test_dependent(exc, allow_types=[ExceptionParam]) as ctx: ctx.pass_params(exception=exception) ctx.should_return(exception) async with app.test_dependent(legacy_exc, allow_types=[ExceptionParam]) as ctx: ctx.pass_params(exception=exception) ctx.should_return(exception) @pytest.mark.anyio @pytest.mark.skipif( sys.version_info < (3, 12), reason="TypeAlias requires Python 3.12 or higher" ) async def test_aliased_exception(app: App): from python_3_12.plugins.aliased_param.param_exception import aliased_exc exception = ValueError("test") async with app.test_dependent(aliased_exc, allow_types=[ExceptionParam]) as ctx: ctx.pass_params(exception=exception) ctx.should_return(exception) @pytest.mark.anyio async def test_default(app: App): from plugins.param.param_default import default async with app.test_dependent(default, allow_types=[DefaultParam]) as ctx: ctx.should_return(1) def test_priority(): from plugins.param.priority import complex_priority dependent = Dependent[None].parse( call=complex_priority, allow_types=[ DependParam, BotParam, EventParam, StateParam, MatcherParam, ArgParam, ExceptionParam, DefaultParam, ], ) for param in dependent.params: if param.name == "sub": assert isinstance(param.field_info, DependParam) elif param.name == "bot": assert isinstance(param.field_info, BotParam) elif param.name == "event": assert isinstance(param.field_info, EventParam) elif param.name == "state": assert isinstance(param.field_info, StateParam) elif param.name == "matcher": assert isinstance(param.field_info, MatcherParam) elif param.name == "arg": assert isinstance(param.field_info, ArgParam) elif param.name == "exception": assert isinstance(param.field_info, ExceptionParam) elif param.name == "default": assert isinstance(param.field_info, DefaultParam) else: raise ValueError(f"unknown param {param.name}") ================================================ FILE: tests/test_permission.py ================================================ from nonebug import App import pytest from nonebot.exception import SkippedException from nonebot.permission import ( MESSAGE, METAEVENT, NOTICE, REQUEST, SUPERUSER, USER, Message, MetaEvent, Notice, Permission, Request, SuperUser, User, ) from utils import make_fake_event @pytest.mark.anyio async def test_permission(app: App): async def falsy(): return False async def truthy(): return True async def skipped() -> bool: raise SkippedException def _is_eq(a: Permission, b: Permission) -> bool: return {d.call for d in a.checkers} == {d.call for d in b.checkers} assert _is_eq(Permission(truthy) | None, Permission(truthy)) assert _is_eq(Permission(truthy) | falsy, Permission(truthy, falsy)) assert _is_eq(Permission(truthy) | Permission(falsy), Permission(truthy, falsy)) assert _is_eq(None | Permission(truthy), Permission(truthy)) assert _is_eq(truthy | Permission(falsy), Permission(truthy, falsy)) event = make_fake_event()() async with app.test_api() as ctx: bot = ctx.create_bot() assert await Permission(falsy)(bot, event) is False assert await Permission(truthy)(bot, event) is True assert await Permission(skipped)(bot, event) is False assert await Permission(truthy, falsy)(bot, event) is True assert await Permission(truthy, skipped)(bot, event) is True @pytest.mark.anyio @pytest.mark.parametrize(("type", "expected"), [("message", True), ("notice", False)]) async def test_message(type: str, expected: bool): dependent = next(iter(MESSAGE.checkers)) checker = dependent.call assert isinstance(checker, Message) event = make_fake_event(_type=type)() assert await dependent(event=event) == expected @pytest.mark.anyio @pytest.mark.parametrize(("type", "expected"), [("message", False), ("notice", True)]) async def test_notice(type: str, expected: bool): dependent = next(iter(NOTICE.checkers)) checker = dependent.call assert isinstance(checker, Notice) event = make_fake_event(_type=type)() assert await dependent(event=event) == expected @pytest.mark.anyio @pytest.mark.parametrize(("type", "expected"), [("message", False), ("request", True)]) async def test_request(type: str, expected: bool): dependent = next(iter(REQUEST.checkers)) checker = dependent.call assert isinstance(checker, Request) event = make_fake_event(_type=type)() assert await dependent(event=event) == expected @pytest.mark.anyio @pytest.mark.parametrize( ("type", "expected"), [("message", False), ("meta_event", True)] ) async def test_metaevent(type: str, expected: bool): dependent = next(iter(METAEVENT.checkers)) checker = dependent.call assert isinstance(checker, MetaEvent) event = make_fake_event(_type=type)() assert await dependent(event=event) == expected @pytest.mark.anyio @pytest.mark.parametrize( ("type", "user_id", "expected"), [ ("message", "test", True), ("message", "foo", False), ("message", "faketest", True), ("message", None, False), ("notice", "test", True), ], ) async def test_superuser(app: App, type: str, user_id: str, expected: bool): dependent = next(iter(SUPERUSER.checkers)) checker = dependent.call assert isinstance(checker, SuperUser) event = make_fake_event(_type=type, _user_id=user_id)() async with app.test_api() as ctx: bot = ctx.create_bot() assert await dependent(bot=bot, event=event) == expected @pytest.mark.anyio @pytest.mark.parametrize( ("session_ids", "session_id", "expected"), [ (("user", "foo"), "user", True), (("user", "foo"), "bar", False), (("user", "foo"), None, False), ], ) async def test_user( app: App, session_ids: tuple[str, ...], session_id: str | None, expected: bool ): dependent = next(iter(USER(*session_ids).checkers)) checker = dependent.call assert isinstance(checker, User) event = make_fake_event(_session_id=session_id)() async with app.test_api() as ctx: bot = ctx.create_bot() assert await dependent(bot=bot, event=event) == expected ================================================ FILE: tests/test_plugin/test_get.py ================================================ from pydantic import BaseModel, Field import pytest import nonebot from nonebot.plugin import PluginManager, _managers def test_get_plugin(): # check simple plugin plugin = nonebot.get_plugin("export") assert plugin assert plugin.id_ == "export" assert plugin.name == "export" assert plugin.module_name == "plugins.export" # check sub plugin plugin = nonebot.get_plugin("nested:nested_subplugin") assert plugin assert plugin.id_ == "nested:nested_subplugin" assert plugin.name == "nested_subplugin" assert plugin.module_name == "plugins.nested.plugins.nested_subplugin" def test_get_plugin_by_module_name(): # check get plugin by exact module name plugin = nonebot.get_plugin_by_module_name("plugins.nested") assert plugin assert plugin.id_ == "nested" assert plugin.name == "nested" assert plugin.module_name == "plugins.nested" # check get plugin by sub module name plugin = nonebot.get_plugin_by_module_name("plugins.nested.utils") assert plugin assert plugin.id_ == "nested" assert plugin.name == "nested" assert plugin.module_name == "plugins.nested" # check get plugin by sub plugin exact module name plugin = nonebot.get_plugin_by_module_name( "plugins.nested.plugins.nested_subplugin" ) assert plugin assert plugin.id_ == "nested:nested_subplugin" assert plugin.name == "nested_subplugin" assert plugin.module_name == "plugins.nested.plugins.nested_subplugin" def test_get_available_plugin(): old_managers = _managers.copy() _managers.clear() try: _managers.append(PluginManager(["plugins.export", "plugin.require"])) # check get available plugins plugin_ids = nonebot.get_available_plugin_names() assert plugin_ids == {"export", "require"} finally: _managers.clear() _managers.extend(old_managers) def test_get_plugin_config(): class Config(BaseModel): plugin_config: int # check get plugin config config = nonebot.get_plugin_config(Config) assert isinstance(config, Config) assert config.plugin_config == 1 def test_get_plugin_config_with_env(monkeypatch: pytest.MonkeyPatch): monkeypatch.setenv("PLUGIN_CONFIG_ONE", "no_dummy_val") monkeypatch.setenv("PLUGIN_SUB_CONFIG__TWO", "two") monkeypatch.setenv("PLUGIN_CFG_THREE", "33") monkeypatch.setenv("CONFIG_FROM_INIT", "impossible") class SubConfig(BaseModel): two: str = "dummy_val" class Config(BaseModel): plugin_config: int plugin_config_one: str = "dummy_val" plugin_sub_config: SubConfig = Field(default_factory=SubConfig) plugin_config_three: int = Field(default=3, alias="plugin_cfg_three") config_from_init: str = "dummy_val" config = nonebot.get_plugin_config(Config) assert config.plugin_config == 1 assert config.plugin_config_one == "no_dummy_val" assert config.plugin_sub_config.two == "two" assert config.plugin_config_three == 33 assert config.config_from_init == "init" ================================================ FILE: tests/test_plugin/test_load.py ================================================ from collections.abc import Callable from dataclasses import asdict from functools import wraps from pathlib import Path import sys from typing import TypeVar from typing_extensions import ParamSpec import pytest import nonebot from nonebot.plugin import ( Plugin, PluginManager, _managers, _plugins, inherit_supported_adapters, ) P = ParamSpec("P") R = TypeVar("R") def _recover(func: Callable[P, R]) -> Callable[P, R]: @wraps(func) def _wrapper(*args: P.args, **kwargs: P.kwargs) -> R: origin_managers = _managers.copy() origin_plugins = _plugins.copy() try: return func(*args, **kwargs) finally: _managers.clear() _managers.extend(origin_managers) _plugins.clear() _plugins.update(origin_plugins) return _wrapper @_recover def test_load_plugin(): # check regular assert nonebot.load_plugin("dynamic.simple") # check path assert nonebot.load_plugin(Path("dynamic/path.py")) # check not found assert nonebot.load_plugin("some_plugin_not_exist") is None def test_load_plugins(load_plugin: set[Plugin], load_builtin_plugin: set[Plugin]): loaded_plugins = { plugin for plugin in nonebot.get_loaded_plugins() if not plugin.parent_plugin } assert loaded_plugins >= load_plugin | load_builtin_plugin # check simple plugin assert "plugins.export" in sys.modules assert "plugin._hidden" not in sys.modules # check sub plugin plugin = nonebot.get_plugin("nested:nested_subplugin") assert plugin assert "plugins.nested.plugins.nested_subplugin" in sys.modules assert plugin.parent_plugin is nonebot.get_plugin("nested") # check load again with pytest.raises(RuntimeError): PluginManager(plugins=["plugins.export"]).load_all_plugins() with pytest.raises(RuntimeError): PluginManager(search_path=["plugins"]).load_all_plugins() def test_load_nested_plugin(): parent_plugin = nonebot.get_plugin("nested") sub_plugin = nonebot.get_plugin("nested:nested_subplugin") sub_plugin2 = nonebot.get_plugin("nested:nested_subplugin2") assert parent_plugin assert sub_plugin assert sub_plugin2 assert sub_plugin.parent_plugin is parent_plugin assert sub_plugin2.parent_plugin is parent_plugin assert parent_plugin.sub_plugins == {sub_plugin, sub_plugin2} @_recover def test_load_json(): nonebot.load_from_json("./plugins.json") with pytest.raises(TypeError): nonebot.load_from_json("./plugins.invalid.json") @_recover def test_load_toml(): nonebot.load_from_toml("./plugins.legacy.toml") nonebot.load_from_toml("./plugins.toml") with pytest.raises(ValueError, match="Cannot find"): nonebot.load_from_toml("./plugins.empty.toml") with pytest.raises(TypeError): nonebot.load_from_toml("./plugins.invalid.toml") @_recover def test_bad_plugin(): nonebot.load_plugins("bad_plugins") assert nonebot.get_plugin("bad_plugin") is None @_recover def test_require_loaded(monkeypatch: pytest.MonkeyPatch): def _patched_find(name: str): pytest.fail("require existing plugin should not call find_manager_by_name") with monkeypatch.context() as m: m.setattr("nonebot.plugin.load._find_manager_by_name", _patched_find) # require use module name nonebot.require("plugins.export") # require use plugin id nonebot.require("export") nonebot.require("nested:nested_subplugin") @_recover def test_require_not_loaded(monkeypatch: pytest.MonkeyPatch): pm = PluginManager(["dynamic.require_not_loaded"], ["dynamic/require_not_loaded/"]) _managers.append(pm) num_managers = len(_managers) origin_load = PluginManager.load_plugin def _patched_load(self: PluginManager, name: str): assert self is pm return origin_load(self, name) with monkeypatch.context() as m: m.setattr(PluginManager, "load_plugin", _patched_load) # require standalone plugin nonebot.require("dynamic.require_not_loaded") # require searched plugin nonebot.require("dynamic.require_not_loaded.subplugin1") nonebot.require("require_not_loaded:subplugin2") assert len(_managers) == num_managers @_recover def test_require_not_declared(): num_managers = len(_managers) nonebot.require("dynamic.require_not_declared") assert len(_managers) == num_managers + 1 assert _managers[-1].plugins == {"dynamic.require_not_declared"} @_recover def test_require_not_found(): with pytest.raises(RuntimeError): nonebot.require("some_plugin_not_exist") def test_plugin_metadata(): from plugins.metadata import Config, FakeAdapter plugin = nonebot.get_plugin("metadata") assert plugin assert plugin.metadata assert asdict(plugin.metadata) == { "name": "测试插件", "description": "测试插件元信息", "usage": "无法使用", "type": "application", "homepage": "https://nonebot.dev", "config": Config, "supported_adapters": {"~onebot.v11", "plugins.metadata:FakeAdapter"}, "extra": {"author": "NoneBot"}, } assert plugin.metadata.get_supported_adapters() == {FakeAdapter} def test_inherit_supported_adapters_not_found(): with pytest.raises(RuntimeError): inherit_supported_adapters("some_plugin_not_exist") with pytest.raises(ValueError, match="has no metadata!"): inherit_supported_adapters("export") @pytest.mark.parametrize( ("inherit_plugins", "expected"), [ (("echo",), None), ( ("metadata",), { "nonebot.adapters.onebot.v11", "plugins.metadata:FakeAdapter", }, ), ( ("metadata_2",), { "nonebot.adapters.onebot.v11", "nonebot.adapters.onebot.v12", }, ), ( ("metadata_3",), { "nonebot.adapters.onebot.v11", "nonebot.adapters.onebot.v12", "nonebot.adapters.qq", }, ), ( ("metadata", "metadata_2"), { "nonebot.adapters.onebot.v11", }, ), ( ("metadata", "metadata_3"), { "nonebot.adapters.onebot.v11", }, ), ( ("metadata_2", "metadata_3"), { "nonebot.adapters.onebot.v11", "nonebot.adapters.onebot.v12", }, ), ( ("metadata", "metadata_2", "metadata_3"), { "nonebot.adapters.onebot.v11", }, ), ( ("metadata", "echo"), { "nonebot.adapters.onebot.v11", "plugins.metadata:FakeAdapter", }, ), ( ("metadata", "metadata_2", "echo"), { "nonebot.adapters.onebot.v11", }, ), ], ) def test_inherit_supported_adapters_combine( inherit_plugins: tuple[str], expected: set[str] ): assert inherit_supported_adapters(*inherit_plugins) == expected ================================================ FILE: tests/test_plugin/test_manager.py ================================================ from nonebot.plugin import PluginManager, _managers def test_load_plugin_name(): m = PluginManager(plugins=["dynamic.manager"]) try: _managers.append(m) # load by plugin id module1 = m.load_plugin("manager") # load by module name module2 = m.load_plugin("dynamic.manager") assert module1 assert module2 assert module1 is module2 finally: _managers.remove(m) ================================================ FILE: tests/test_plugin/test_on.py ================================================ from collections.abc import Callable import pytest import nonebot from nonebot.adapters import Event from nonebot.matcher import Matcher, matchers from nonebot.rule import ( CommandRule, EndswithRule, FullmatchRule, IsTypeRule, KeywordsRule, RegexRule, ShellCommandRule, StartswithRule, ) from nonebot.typing import T_RuleChecker @pytest.mark.parametrize( ("matcher_name", "pre_rule_factory", "has_permission"), [ pytest.param("matcher_on", None, True), pytest.param("matcher_on_metaevent", None, False), pytest.param("matcher_on_message", None, True), pytest.param("matcher_on_notice", None, False), pytest.param("matcher_on_request", None, False), pytest.param( "matcher_on_startswith", lambda e: StartswithRule(("test",)), True ), pytest.param("matcher_on_endswith", lambda e: EndswithRule(("test",)), True), pytest.param("matcher_on_fullmatch", lambda e: FullmatchRule(("test",)), True), pytest.param("matcher_on_keyword", lambda e: KeywordsRule("test"), True), pytest.param("matcher_on_command", lambda e: CommandRule([("test",)]), True), pytest.param( "matcher_on_shell_command", lambda e: ShellCommandRule([("test",)], None), True, ), pytest.param("matcher_on_regex", lambda e: RegexRule("test"), True), pytest.param("matcher_on_type", lambda e: IsTypeRule(e), True), pytest.param( "matcher_prefix_cmd", lambda e: CommandRule([("prefix", "sub"), ("help",), ("help", "foo")]), True, ), pytest.param( "matcher_prefix_shell_cmd", lambda e: ShellCommandRule( [("prefix", "sub"), ("help",), ("help", "foo")], None ), True, ), pytest.param( "matcher_prefix_aliases_cmd", lambda e: CommandRule( [("prefix", "sub"), ("prefix", "help"), ("prefix", "help", "foo")] ), True, ), pytest.param( "matcher_prefix_aliases_shell_cmd", lambda e: ShellCommandRule( [("prefix", "sub"), ("prefix", "help"), ("prefix", "help", "foo")], None ), True, ), pytest.param("matcher_group_on", None, True), pytest.param("matcher_group_on_metaevent", None, False), pytest.param("matcher_group_on_message", None, True), pytest.param("matcher_group_on_notice", None, False), pytest.param("matcher_group_on_request", None, False), pytest.param( "matcher_group_on_startswith", lambda e: StartswithRule(("test",)), True, ), pytest.param( "matcher_group_on_endswith", lambda e: EndswithRule(("test",)), True, ), pytest.param( "matcher_group_on_fullmatch", lambda e: FullmatchRule(("test",)), True, ), pytest.param("matcher_group_on_keyword", lambda e: KeywordsRule("test"), True), pytest.param( "matcher_group_on_command", lambda e: CommandRule([("test",)]), True, ), pytest.param( "matcher_group_on_shell_command", lambda e: ShellCommandRule([("test",)], None), True, ), pytest.param("matcher_group_on_regex", lambda e: RegexRule("test"), True), pytest.param("matcher_group_on_type", lambda e: IsTypeRule(e), True), ], ) def test_on( matcher_name: str, pre_rule_factory: Callable[[type[Event]], T_RuleChecker] | None, has_permission: bool, ): import plugins.plugin.matchers as module from plugins.plugin.matchers import ( TestEvent, expire_time, handler, permission, priority, rule, state, ) matcher = getattr(module, matcher_name) assert issubclass(matcher, Matcher), f"{matcher_name} should be a Matcher" pre_rule = pre_rule_factory(TestEvent) if pre_rule_factory else None plugin = nonebot.get_plugin("plugin") assert plugin, "plugin should be loaded" assert {dependent.call for dependent in matcher.rule.checkers} == ( {pre_rule, rule} if pre_rule else {rule} ) if has_permission: assert {dependent.call for dependent in matcher.permission.checkers} == { permission } else: assert not matcher.permission.checkers assert [dependent.call for dependent in matcher.handlers] == [handler] assert matcher.temp is True assert matcher.expire_time == expire_time assert matcher in matchers[priority] assert matcher.block is True assert matcher._default_state == state assert matcher.plugin is plugin assert matcher in plugin.matcher assert matcher.module is module assert matcher.plugin_id == "plugin" assert matcher.plugin_name == "plugin" assert matcher.module_name == "plugins.plugin.matchers" def test_runtime_on(): import plugins.plugin.matchers as module from plugins.plugin.matchers import matcher_on_factory matcher = matcher_on_factory() plugin = nonebot.get_plugin("plugin") assert plugin, "plugin should be loaded" try: assert matcher.plugin is plugin assert matcher not in plugin.matcher assert matcher.module is module assert matcher.plugin_id == "plugin" assert matcher.plugin_name == "plugin" assert matcher.module_name == "plugins.plugin.matchers" finally: matcher.destroy() ================================================ FILE: tests/test_rule.py ================================================ import re from re import Match from nonebug import App import pytest from nonebot.consts import ( CMD_ARG_KEY, CMD_KEY, CMD_WHITESPACE_KEY, ENDSWITH_KEY, FULLMATCH_KEY, KEYWORD_KEY, PREFIX_KEY, REGEX_MATCHED, SHELL_ARGS, SHELL_ARGV, STARTSWITH_KEY, ) from nonebot.exception import ParserExit, SkippedException from nonebot.rule import ( CMD_RESULT, TRIE_VALUE, ArgumentParser, CommandRule, EndswithRule, FullmatchRule, IsTypeRule, KeywordsRule, Namespace, RegexRule, Rule, ShellCommandRule, StartswithRule, ToMeRule, TrieRule, command, endswith, fullmatch, is_type, keyword, regex, shell_command, startswith, to_me, ) from nonebot.typing import T_State from utils import FakeMessage, FakeMessageSegment, make_fake_event @pytest.mark.anyio async def test_rule(app: App): async def falsy(): return False async def truthy(): return True async def skipped() -> bool: raise SkippedException def _is_eq(a: Rule, b: Rule) -> bool: return {d.call for d in a.checkers} == {d.call for d in b.checkers} assert _is_eq(Rule(truthy) & None, Rule(truthy)) assert _is_eq(Rule(truthy) & falsy, Rule(truthy, falsy)) assert _is_eq(Rule(truthy) & Rule(falsy), Rule(truthy, falsy)) assert _is_eq(None & Rule(truthy), Rule(truthy)) assert _is_eq(truthy & Rule(falsy), Rule(truthy, falsy)) event = make_fake_event()() async with app.test_api() as ctx: bot = ctx.create_bot() assert await Rule(falsy)(bot, event, {}) is False assert await Rule(truthy)(bot, event, {}) is True assert await Rule(skipped)(bot, event, {}) is False assert await Rule(truthy, falsy)(bot, event, {}) is False assert await Rule(truthy, skipped)(bot, event, {}) is False @pytest.mark.anyio async def test_trie(app: App): TrieRule.add_prefix("/fake-prefix", TRIE_VALUE("/", ("fake-prefix",))) async with app.test_api() as ctx: bot = ctx.create_bot() message = FakeMessage("/fake-prefix some args") event = make_fake_event(_message=message)() state = {} TrieRule.get_value(bot, event, state) assert state[PREFIX_KEY] == CMD_RESULT( command=("fake-prefix",), raw_command="/fake-prefix", command_arg=FakeMessage("some args"), command_start="/", command_whitespace=" ", ) message = FakeMessageSegment.text("/fake-prefix ") + FakeMessageSegment.image( "fake url" ) event = make_fake_event(_message=message)() state = {} TrieRule.get_value(bot, event, state) assert state[PREFIX_KEY] == CMD_RESULT( command=("fake-prefix",), raw_command="/fake-prefix", command_arg=FakeMessage(FakeMessageSegment.image("fake url")), command_start="/", command_whitespace=" ", ) message = FakeMessageSegment.text("/fake-prefix ") + FakeMessageSegment.text( " some args" ) event = make_fake_event(_message=message)() state = {} TrieRule.get_value(bot, event, state) assert state[PREFIX_KEY] == CMD_RESULT( command=("fake-prefix",), raw_command="/fake-prefix", command_arg=FakeMessage("some args"), command_start="/", command_whitespace=" ", ) message = ( FakeMessageSegment.text("/fake-prefix ") + FakeMessageSegment.text(" ") + FakeMessageSegment.text(" some args") ) event = make_fake_event(_message=message)() state = {} TrieRule.get_value(bot, event, state) assert state[PREFIX_KEY] == CMD_RESULT( command=("fake-prefix",), raw_command="/fake-prefix", command_arg=FakeMessage("some args"), command_start="/", command_whitespace=" ", ) del TrieRule.prefix["/fake-prefix"] @pytest.mark.anyio @pytest.mark.parametrize( ("msg", "ignorecase", "type", "text", "expected"), [ ("prefix", False, "message", "prefix_", True), ("prefix", False, "message", "Prefix_", False), ("prefix", True, "message", "prefix_", True), ("prefix", True, "message", "Prefix_", True), ("prefix", False, "message", "prefoo", False), ("prefix", False, "message", "fooprefix", False), ("prefix", False, "message", None, False), (("prefix", "foo"), False, "message", "fooprefix", True), ("prefix", False, "notice", "prefix", True), ("prefix", False, "notice", "foo", False), ], ) async def test_startswith( msg: str | tuple[str, ...], ignorecase: bool, type: str, text: str | None, expected: bool, ): test_startswith = startswith(msg, ignorecase) dependent = next(iter(test_startswith.checkers)) checker = dependent.call msg = (msg,) if isinstance(msg, str) else msg assert isinstance(checker, StartswithRule) assert checker.msg == msg assert checker.ignorecase == ignorecase message = text if text is None else FakeMessage(text) event = make_fake_event(_type=type, _message=message)() for prefix in msg: state = {STARTSWITH_KEY: prefix} assert await dependent(event=event, state=state) == expected @pytest.mark.anyio @pytest.mark.parametrize( ("msg", "ignorecase", "type", "text", "expected"), [ ("suffix", False, "message", "_suffix", True), ("suffix", False, "message", "_Suffix", False), ("suffix", True, "message", "_suffix", True), ("suffix", True, "message", "_Suffix", True), ("suffix", False, "message", "suffoo", False), ("suffix", False, "message", "suffixfoo", False), ("suffix", False, "message", None, False), (("suffix", "foo"), False, "message", "suffixfoo", True), ("suffix", False, "notice", "suffix", True), ("suffix", False, "notice", "foo", False), ], ) async def test_endswith( msg: str | tuple[str, ...], ignorecase: bool, type: str, text: str | None, expected: bool, ): test_endswith = endswith(msg, ignorecase) dependent = next(iter(test_endswith.checkers)) checker = dependent.call msg = (msg,) if isinstance(msg, str) else msg assert isinstance(checker, EndswithRule) assert checker.msg == msg assert checker.ignorecase == ignorecase message = text if text is None else FakeMessage(text) event = make_fake_event(_type=type, _message=message)() for suffix in msg: state = {ENDSWITH_KEY: suffix} assert await dependent(event=event, state=state) == expected @pytest.mark.anyio @pytest.mark.parametrize( ("msg", "ignorecase", "type", "text", "expected"), [ ("fullmatch", False, "message", "fullmatch", True), ("fullmatch", False, "message", "Fullmatch", False), ("fullmatch", True, "message", "fullmatch", True), ("fullmatch", True, "message", "Fullmatch", True), ("fullmatch", False, "message", "fullfoo", False), ("fullmatch", False, "message", "_fullmatch_", False), ("fullmatch", False, "message", None, False), (("fullmatch", "foo"), False, "message", "fullmatchfoo", False), ("fullmatch", False, "notice", "fullmatch", True), ("fullmatch", False, "notice", "foo", False), ], ) async def test_fullmatch( msg: str | tuple[str, ...], ignorecase: bool, type: str, text: str | None, expected: bool, ): test_fullmatch = fullmatch(msg, ignorecase) dependent = next(iter(test_fullmatch.checkers)) checker = dependent.call msg = (msg,) if isinstance(msg, str) else msg assert isinstance(checker, FullmatchRule) assert checker.msg == msg assert checker.ignorecase == ignorecase message = text if text is None else FakeMessage(text) event = make_fake_event(_type=type, _message=message)() for full in msg: state = {FULLMATCH_KEY: full} assert await dependent(event=event, state=state) == expected @pytest.mark.anyio @pytest.mark.parametrize( ("kws", "type", "text", "expected"), [ (("key",), "message", "_key_", True), (("key", "foo"), "message", "_foo_", True), (("key",), "message", None, False), (("key",), "message", "foo", False), (("key",), "notice", "_key_", True), (("key",), "notice", "foo", False), ], ) async def test_keyword( kws: tuple[str, ...], type: str, text: str | None, expected: bool, ): test_keyword = keyword(*kws) dependent = next(iter(test_keyword.checkers)) checker = dependent.call assert isinstance(checker, KeywordsRule) assert checker.keywords == kws message = text if text is None else FakeMessage(text) event = make_fake_event(_type=type, _message=message)() for kw in kws: state = {KEYWORD_KEY: kw} assert await dependent(event=event, state=state) == expected @pytest.mark.anyio @pytest.mark.parametrize( ("cmds", "force_whitespace", "cmd", "whitespace", "arg_text", "expected"), [ # command tests ((("help",),), None, ("help",), None, None, True), ((("help",),), None, ("foo",), None, None, False), ((("help", "foo"),), None, ("help", "foo"), None, None, True), ((("help", "foo"),), None, ("help", "bar"), None, None, False), ((("help",), ("foo",)), None, ("help",), None, None, True), ((("help",), ("foo",)), None, ("bar",), None, None, False), # whitespace tests ((("help",),), True, ("help",), " ", "arg", True), ((("help",),), True, ("help",), None, "arg", False), ((("help",),), True, ("help",), None, None, True), ((("help",),), False, ("help",), " ", "arg", False), ((("help",),), False, ("help",), None, "arg", True), ((("help",),), False, ("help",), None, None, True), ((("help",),), " ", ("help",), " ", "arg", True), ((("help",),), " ", ("help",), "\n", "arg", False), ((("help",),), " ", ("help",), None, "arg", False), ((("help",),), " ", ("help",), None, None, True), ], ) async def test_command( cmds: tuple[tuple[str, ...]], force_whitespace: str | bool | None, cmd: tuple[str, ...], whitespace: str | None, arg_text: str | None, expected: bool, ): test_command = command(*cmds, force_whitespace=force_whitespace) dependent = next(iter(test_command.checkers)) checker = dependent.call assert isinstance(checker, CommandRule) assert checker.cmds == cmds arg = arg_text if arg_text is None else FakeMessage(arg_text) state = { PREFIX_KEY: {CMD_KEY: cmd, CMD_WHITESPACE_KEY: whitespace, CMD_ARG_KEY: arg} } assert await dependent(state=state) == expected @pytest.mark.anyio async def test_shell_command(): state: T_State CMD = ("test",) Message = FakeMessage MessageSegment = Message.get_segment_class() test_not_cmd = shell_command(CMD) dependent = next(iter(test_not_cmd.checkers)) checker = dependent.call assert isinstance(checker, ShellCommandRule) message = Message() event = make_fake_event(_message=message)() state = {PREFIX_KEY: {CMD_KEY: ("not",), CMD_ARG_KEY: message}} assert not await dependent(event=event, state=state) test_no_parser = shell_command(CMD) dependent = next(iter(test_no_parser.checkers)) checker = dependent.call assert isinstance(checker, ShellCommandRule) message = Message() event = make_fake_event(_message=message)() state = {PREFIX_KEY: {CMD_KEY: CMD, CMD_ARG_KEY: message}} assert await dependent(event=event, state=state) assert state[SHELL_ARGV] == [] assert SHELL_ARGS not in state test_lexical_error = shell_command(CMD) dependent = next(iter(test_lexical_error.checkers)) checker = dependent.call assert isinstance(checker, ShellCommandRule) message = Message("-a '1") event = make_fake_event(_message=message)() state = {PREFIX_KEY: {CMD_KEY: CMD, CMD_ARG_KEY: message}} assert await dependent(event=event, state=state) assert state[SHELL_ARGV] is None parser = ArgumentParser("test") parser.add_argument("-a", required=True) test_lexical_error_with_parser = shell_command(CMD, parser=ArgumentParser("test")) dependent = next(iter(test_lexical_error_with_parser.checkers)) checker = dependent.call assert isinstance(checker, ShellCommandRule) message = Message("-a '1") event = make_fake_event(_message=message)() state = {PREFIX_KEY: {CMD_KEY: CMD, CMD_ARG_KEY: message}} assert await dependent(event=event, state=state) assert state[SHELL_ARGV] is None assert isinstance(state[SHELL_ARGS], ParserExit) assert state[SHELL_ARGS].status != 0 test_simple_parser = shell_command(CMD, parser=parser) dependent = next(iter(test_simple_parser.checkers)) checker = dependent.call assert isinstance(checker, ShellCommandRule) message = Message("-a 1") event = make_fake_event(_message=message)() state = {PREFIX_KEY: {CMD_KEY: CMD, CMD_ARG_KEY: message}} assert await dependent(event=event, state=state) assert state[SHELL_ARGV] == ["-a", "1"] assert state[SHELL_ARGS] == Namespace(a="1") test_parser_help = shell_command(CMD, parser=parser) dependent = next(iter(test_parser_help.checkers)) checker = dependent.call assert isinstance(checker, ShellCommandRule) message = Message("-h") event = make_fake_event(_message=message)() state = {PREFIX_KEY: {CMD_KEY: CMD, CMD_ARG_KEY: message}} assert await dependent(event=event, state=state) assert state[SHELL_ARGV] == ["-h"] assert isinstance(state[SHELL_ARGS], ParserExit) assert state[SHELL_ARGS].status == 0 assert state[SHELL_ARGS].message == parser.format_help() test_parser_error = shell_command(CMD, parser=parser) dependent = next(iter(test_parser_error.checkers)) checker = dependent.call assert isinstance(checker, ShellCommandRule) message = Message() event = make_fake_event(_message=message)() state = {PREFIX_KEY: {CMD_KEY: CMD, CMD_ARG_KEY: message}} assert await dependent(event=event, state=state) assert state[SHELL_ARGV] == [] assert isinstance(state[SHELL_ARGS], ParserExit) assert state[SHELL_ARGS].status != 0 assert state[SHELL_ARGS].message.startswith(parser.format_usage() + "test: error:") test_parser_remain_args = shell_command(CMD, parser=parser) dependent = next(iter(test_parser_remain_args.checkers)) checker = dependent.call assert isinstance(checker, ShellCommandRule) message = MessageSegment.text("-a 1 2") + MessageSegment.image("test") event = make_fake_event(_message=message)() state = {PREFIX_KEY: {CMD_KEY: CMD, CMD_ARG_KEY: message}} assert await dependent(event=event, state=state) assert state[SHELL_ARGV] == ["-a", "1", "2", MessageSegment.image("test")] assert isinstance(state[SHELL_ARGS], ParserExit) assert state[SHELL_ARGS].status != 0 assert state[SHELL_ARGS].message.startswith(parser.format_usage() + "test: error:") test_message_parser = shell_command(CMD, parser=parser) dependent = next(iter(test_message_parser.checkers)) checker = dependent.call assert isinstance(checker, ShellCommandRule) message = MessageSegment.text("-a") + MessageSegment.image("test") event = make_fake_event(_message=message)() state = {PREFIX_KEY: {CMD_KEY: CMD, CMD_ARG_KEY: message}} assert await dependent(event=event, state=state) assert state[SHELL_ARGV] == ["-a", MessageSegment.image("test")] assert state[SHELL_ARGS] == Namespace(a=MessageSegment.image("test")) parser = ArgumentParser("test", exit_on_error=False) parser.add_argument("-a", required=True) test_not_exit = shell_command(CMD, parser=parser) dependent = next(iter(test_not_exit.checkers)) checker = dependent.call assert isinstance(checker, ShellCommandRule) message = Message() event = make_fake_event(_message=message)() state = {PREFIX_KEY: {CMD_KEY: CMD, CMD_ARG_KEY: message}} assert await dependent(event=event, state=state) assert state[SHELL_ARGV] == [] assert isinstance(state[SHELL_ARGS], ParserExit) assert state[SHELL_ARGS].status != 0 @pytest.mark.anyio @pytest.mark.parametrize( ("pattern", "type", "text", "expected", "matched"), [ ( r"(?Pkey\d)", "message", "_key1_", True, re.search(r"(?Pkey\d)", "_key1_"), ), (r"foo", "message", None, False, None), (r"foo", "notice", "foo", True, re.search(r"foo", "foo")), (r"foo", "notice", "bar", False, None), ], ) async def test_regex( pattern: str, type: str, text: str | None, expected: bool, matched: Match[str] | None, ): test_regex = regex(pattern) dependent = next(iter(test_regex.checkers)) checker = dependent.call assert isinstance(checker, RegexRule) assert checker.regex == pattern message = text if text is None else FakeMessage(text) event = make_fake_event(_type=type, _message=message)() state = {} assert await dependent(event=event, state=state) == expected result: Match[str] | None = state.get(REGEX_MATCHED) if matched is None: assert result is None else: assert isinstance(result, Match) assert result.group() == matched.group() assert result.span() == matched.span() @pytest.mark.anyio @pytest.mark.parametrize("expected", [True, False]) async def test_to_me(expected: bool): test_to_me = to_me() dependent = next(iter(test_to_me.checkers)) checker = dependent.call assert isinstance(checker, ToMeRule) event = make_fake_event(_to_me=expected)() assert await dependent(event=event) == expected @pytest.mark.anyio async def test_is_type(): Event1 = make_fake_event() Event2 = make_fake_event() Event3 = make_fake_event() test_type = is_type(Event1, Event2) dependent = next(iter(test_type.checkers)) checker = dependent.call assert isinstance(checker, IsTypeRule) event = Event1() assert await dependent(event=event) event = Event3() assert not await dependent(event=event) ================================================ FILE: tests/test_single_session.py ================================================ from contextlib import asynccontextmanager import pytest from utils import make_fake_event @pytest.mark.anyio async def test_matcher_mutex(): from nonebot.plugins.single_session import _running_matcher, matcher_mutex am = asynccontextmanager(matcher_mutex) event = make_fake_event()() event_1 = make_fake_event()() event_2 = make_fake_event(_session_id="test1")() event_3 = make_fake_event(_session_id=None)() async with am(event) as ctx: assert ctx is False assert not _running_matcher async with am(event) as ctx: async with am(event_1) as ctx_1: assert ctx is False assert ctx_1 is True assert not _running_matcher async with am(event) as ctx: async with am(event_2) as ctx_2: assert ctx is False assert ctx_2 is False assert not _running_matcher async with am(event_3) as ctx_3: assert ctx_3 is False assert not _running_matcher ================================================ FILE: tests/test_utils.py ================================================ import json from typing import ClassVar, Dict, List, Literal, TypeVar, Union # noqa: UP035 from nonebot.utils import ( DataclassEncoder, escape_tag, generic_check_issubclass, is_async_gen_callable, is_coroutine_callable, is_gen_callable, ) from utils import FakeMessage, FakeMessageSegment def test_loguru_escape_tag(): assert escape_tag("red") == r"\red\" assert escape_tag("white") == r"\white\" assert escape_tag("white") == "\\white\\" assert escape_tag("white") == r"\white\" assert escape_tag("white") == "\\white\\" def test_generic_check_issubclass(): assert generic_check_issubclass(int, (int, float)) assert not generic_check_issubclass(str, (int, float)) assert generic_check_issubclass(Union[int, float, None], (int, float)) # noqa: UP007 assert generic_check_issubclass(int | float | None, (int, float)) assert generic_check_issubclass(Literal[1, 2, 3], int) assert not generic_check_issubclass(Literal[1, 2, "3"], int) assert generic_check_issubclass(List[int], list) # noqa: UP006 assert generic_check_issubclass(Dict[str, int], dict) # noqa: UP006 assert generic_check_issubclass(list[int], list) assert generic_check_issubclass(dict[str, int], dict) assert not generic_check_issubclass(ClassVar[int], int) assert generic_check_issubclass(TypeVar("T", int, float), (int, float)) assert generic_check_issubclass(TypeVar("T", bound=int), (int, float)) def test_is_coroutine_callable(): async def test1(): ... def test2(): ... class TestClass1: async def __call__(self): ... class TestClass2: def __call__(self): ... assert is_coroutine_callable(test1) assert not is_coroutine_callable(test2) assert not is_coroutine_callable(TestClass1) assert is_coroutine_callable(TestClass1()) assert not is_coroutine_callable(TestClass2) def test_is_gen_callable(): def test1(): yield async def test2(): yield def test3(): ... class TestClass1: def __call__(self): yield class TestClass2: async def __call__(self): yield class TestClass3: def __call__(self): ... assert is_gen_callable(test1) assert not is_gen_callable(test2) assert not is_gen_callable(test3) assert is_gen_callable(TestClass1()) assert not is_gen_callable(TestClass2()) assert not is_gen_callable(TestClass3()) def test_is_async_gen_callable(): async def test1(): yield def test2(): yield async def test3(): ... class TestClass1: async def __call__(self): yield class TestClass2: def __call__(self): yield class TestClass3: async def __call__(self): ... assert is_async_gen_callable(test1) assert not is_async_gen_callable(test2) assert not is_async_gen_callable(test3) assert is_async_gen_callable(TestClass1()) assert not is_async_gen_callable(TestClass2()) assert not is_async_gen_callable(TestClass3()) def test_dataclass_encoder(): simple = json.dumps("123", cls=DataclassEncoder) assert simple == '"123"' ms = FakeMessageSegment.nested(FakeMessage(FakeMessageSegment.text("text"))) s = json.dumps(ms, cls=DataclassEncoder) assert s == ( "{" '"type": "node", ' '"data": {"content": [{"type": "text", "data": {"text": "text"}}]}' "}" ) ================================================ FILE: tests/utils.py ================================================ from collections.abc import Iterable, Mapping from typing_extensions import override from pydantic import create_model from nonebot.adapters import Adapter, Bot, Event, Message, MessageSegment def escape_text(s: str, *, escape_comma: bool = True) -> str: s = s.replace("&", "&").replace("[", "[").replace("]", "]") if escape_comma: s = s.replace(",", ",") return s class FakeAdapter(Adapter): @classmethod @override def get_name(cls) -> str: return "fake" @override async def _call_api(self, bot: Bot, api: str, **data): raise NotImplementedError class FakeMessageSegment(MessageSegment["FakeMessage"]): @classmethod @override def get_message_class(cls): return FakeMessage @override def __str__(self) -> str: return self.data["text"] if self.type == "text" else f"[fake:{self.type}]" @classmethod def text(cls, text: str): return cls("text", {"text": text}) @staticmethod def image(url: str): return FakeMessageSegment("image", {"url": url}) @staticmethod def nested(content: "FakeMessage"): return FakeMessageSegment("node", {"content": content}) @override def is_text(self) -> bool: return self.type == "text" class FakeMessage(Message[FakeMessageSegment]): @classmethod @override def get_segment_class(cls): return FakeMessageSegment @staticmethod @override def _construct(msg: str | Iterable[Mapping]): if isinstance(msg, str): yield FakeMessageSegment.text(msg) else: for seg in msg: yield FakeMessageSegment(**seg) return @override def __add__(self, other: str | FakeMessageSegment | Iterable[FakeMessageSegment]): other = escape_text(other) if isinstance(other, str) else other return super().__add__(other) def make_fake_event( _base: type[Event] | None = None, _type: str = "message", _name: str = "test", _description: str = "test", _user_id: str | None = "test", _session_id: str | None = "test", _message: Message | None = None, _to_me: bool = True, **fields, ) -> type[Event]: Base = _base or Event class FakeEvent(Base): @override def get_type(self) -> str: return _type @override def get_event_name(self) -> str: return _name @override def get_event_description(self) -> str: return _description @override def get_user_id(self) -> str: if _user_id is not None: return _user_id raise NotImplementedError @override def get_session_id(self) -> str: if _session_id is not None: return _session_id raise NotImplementedError @override def get_message(self) -> "Message": if _message is not None: return _message raise NotImplementedError @override def is_tome(self) -> bool: return _to_me return create_model("FakeEvent", __base__=FakeEvent, **fields) ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "target": "ES2020", "lib": ["ESNext"], "module": "NodeNext", "declaration": true, "declarationMap": false, "sourceMap": false, "jsx": "react-native", "noEmit": true, /* Strict Type-Checking Options */ "strict": true, "strictNullChecks": true, "strictFunctionTypes": true, "strictBindCallApply": true, "strictPropertyInitialization": true, "noImplicitThis": true, "alwaysStrict": true, /* Additional Checks */ // "noUnusedLocals": false, // ensured by eslint, should not block compilation // "noImplicitReturns": true, // "noFallthroughCasesInSwitch": true, /* Disabled on purpose (handled by ESLint, should not block compilation) */ "noUnusedParameters": false, /* Module Resolution Options */ "moduleResolution": "nodenext", "allowSyntheticDefaultImports": true, "esModuleInterop": true, "isolatedModules": true, /* Advanced Options */ "resolveJsonModule": true, "skipLibCheck": true, // @types/webpack and webpack/types.d.ts are not the same thing /* Use tslib */ "importHelpers": true, "noEmitHelpers": true }, "include": ["./**/.eslintrc.js", "./**/.stylelintrc.js"], "exclude": ["node_modules", "**/lib/**/*"] } ================================================ FILE: website/docs/README.md ================================================ --- sidebar_position: 0 id: index slug: / --- # 概览 NoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架(下称 NoneBot),它基于 Python 的类型注解和异步优先特性(兼容同步),能够为你的需求实现提供便捷灵活的支持。同时,NoneBot 拥有大量的开发者为其开发插件,用户无需编写任何代码,仅需完成环境配置及插件安装,就可以正常使用 NoneBot。 需要注意的是,NoneBot 仅支持 **Python 3.9 以上版本** ## 特色 ### 异步优先 NoneBot 基于 Python [asyncio](https://docs.python.org/zh-cn/3/library/asyncio.html) / [trio](https://trio.readthedocs.io/en/stable/) 编写,并在异步机制的基础上进行了一定程度的同步函数兼容。 ### 完整的类型注解 NoneBot 参考 [PEP 484](https://www.python.org/dev/peps/pep-0484/) 等 PEP 完整实现了类型注解,通过 Pyright(Pylance) 检查。配合编辑器的类型推导功能,能将绝大多数的 Bug 杜绝在编辑器中([编辑器支持](./editor-support))。 ### 开箱即用 NoneBot 提供了使用便捷、具有交互式功能的命令行工具--`nb-cli`,使得用户初次接触 NoneBot 时更容易上手。使用方法请阅读本文档[指南](./quick-start.mdx)以及 [CLI 文档](https://cli.nonebot.dev/)。 ### 插件系统 插件系统是 NoneBot 的核心,通过它可以实现机器人的模块化以及功能扩展,便于维护和管理。 ### 依赖注入系统 NoneBot 采用了一套自行定义的依赖注入系统,可以让事件的处理过程更加的简洁、清晰,增加代码的可读性,减少代码冗余。 #### 什么是依赖注入 [**『依赖注入』**](https://zh.wikipedia.org/wiki/%E6%8E%A7%E5%88%B6%E5%8F%8D%E8%BD%AC)意思是,在编程中,有一种方法可以让你的代码声明它工作和使用所需要的东西,即**『依赖』**。 系统(在这里是指 NoneBot)将负责做任何需要的事情,为你的代码提供这些必要依赖(即**『注入』**依赖性) 这在你有以下情形的需求时非常有用: - 这部分代码拥有共享的逻辑(同样的代码逻辑多次重复) - 共享数据库以及网络请求连接会话 - 比如 `httpx.AsyncClient`、`aiohttp.ClientSession` 和 `sqlalchemy.Session` - 机器人用户权限检查以及认证 - 还有更多... 它在完成上述工作的同时,还能尽量减少代码的耦合和重复 ================================================ FILE: website/docs/advanced/adapter.md ================================================ --- sidebar_position: 1 description: 注册适配器与指定平台交互 options: menu: - category: advanced weight: 20 --- # 使用适配器 适配器 (Adapter) 是机器人与平台交互的核心桥梁,它负责在驱动器和机器人插件之间转换与传递消息。 ## 适配器功能与组成 适配器通常有两种功能,分别是**接收事件**和**调用平台接口**。其中,接收事件是指将驱动器收到的事件消息转换为 NoneBot 定义的事件模型,然后交由机器人插件处理;调用平台接口是指将机器人插件调用平台接口的数据转换为平台指定的格式,然后交由驱动器发送,并接收接口返回数据。 为了实现这两种功能,适配器通常由四个部分组成: - **Adapter**:负责转换事件和调用接口,正确创建 Bot 对象并注册到 NoneBot 中。 - **Bot**:负责存储平台机器人相关信息,并提供回复事件的方法。 - **Event**:负责定义事件内容,以及事件主体对象。 - **Message**:负责正确序列化消息,以便机器人插件处理。 ## 注册适配器 在使用适配器之前,我们需要先将适配器注册到驱动器中,这样适配器就可以通过驱动器接收事件和调用接口了。我们以 Console 适配器为例,来看看如何注册适配器: ```python {2,5} title=bot.py import nonebot from nonebot.adapters.console import Adapter driver = nonebot.get_driver() driver.register_adapter(Adapter) ``` 我们首先需要从适配器模块中导入所需要的适配器类,然后通过驱动器的 `register_adapter` 方法将适配器注册到驱动器中即可。如果我们需要多平台支持,可以多次调用 `register_adapter` 方法来注册多个适配器。 ## 获取已注册的适配器 NoneBot 提供了 `get_adapter` 方法来获取已注册的适配器,我们可以通过适配器的名称或类型来获取指定的适配器实例: ```python import nonebot from nonebot.adapters.console import Adapter adapters = nonebot.get_adapters() console_adapter = nonebot.get_adapter(Adapter) console_adapter = nonebot.get_adapter(Adapter.get_name()) ``` ## 获取 Bot 对象 当前所有适配器已连接的 Bot 对象可以通过 `get_bots` 方法获取,这是一个以机器人 ID 为键的字典: ```python import nonebot bots = nonebot.get_bots() ``` 我们也可以通过 `get_bot` 方法获取指定 ID 的 Bot 对象。如果省略 ID 参数,将会返回所有 Bot 中的第一个: ```python import nonebot bot = nonebot.get_bot("bot_id") ``` 如果需要获取指定适配器连接的 Bot 对象,我们可以通过适配器的 `bots` 属性获取,这也是一个以机器人 ID 为键的字典: ```python import nonebot from nonebot.adapters.console import Adapter console_adapter = nonebot.get_adapter(Adapter) bots = console_adapter.bots ``` Bot 对象都具有一个 `self_id` 属性,它是机器人的唯一 ID,由适配器填写,通常为机器人的帐号 ID 或者 APP ID。 ## 获取事件通用信息 适配器的所有事件模型均继承自 `Event` 基类,在[事件类型与重载](../appendices/overload.md)一节中,我们也提到了如何使用基类抽象方法来获取事件通用信息。基类能提供如下信息: ### 事件类型 事件类型通常为 `meta_event`、`message`、`notice`、`request`。 ```python type: str = event.get_type() ``` ### 事件名称 事件名称由适配器定义,通常用于日志记录。 ```python name: str = event.get_event_name() ``` ### 事件描述 事件描述由适配器定义,通常用于日志记录。 ```python description: str = event.get_event_description() ``` ### 事件日志字符串 事件日志字符串由事件名称和事件描述组成,用于日志记录。 ```python log: str = event.get_log_string() ``` ### 事件主体 ID 事件主体 ID 通常为机器人用户 ID。 ```python user_id: str = event.get_user_id() ``` ### 事件会话 ID 事件会话 ID 通常为机器人用户 ID 与群聊/频道 ID 组合而成。 ```python session_id: str = event.get_session_id() ``` ### 事件消息 如果事件包含消息,则可以通过该方法获取,否则会产生异常。 ```python message: Message = event.get_message() ``` ### 事件纯文本消息 通常为事件消息的纯文本内容,如果事件不包含消息,则会产生异常。 ```python text: str = event.get_plaintext() ``` ### 事件是否与机器人有关 由适配器实现的判断,通常将事件目标主体为机器人、消息中包含“@机器人”或以“机器人的昵称”开始视为与机器人有关。 ```python is_tome: bool = event.is_tome() ``` ## 更多 官方支持的适配器和社区贡献的适配器均可在[商店](/store/adapters)中查看。如果你想要开发自己的适配器,可以参考[开发文档](../developer/adapter-writing.md)。欢迎通过商店发布你的适配器。 ================================================ FILE: website/docs/advanced/dependency.mdx ================================================ --- sidebar_position: 6 description: 通过依赖注入获取上下文信息 options: menu: - category: advanced weight: 70 --- # 依赖注入 import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; 在事件处理流程中,事件响应器具有自己独立的上下文,例如:当前的事件、机器人等信息。在 NoneBot 中,这些信息通过依赖注入的方式提供给事件处理函数,可以让代码更加整洁可读、提升复用能力。 在了解如何使用依赖注入获取上下文信息之前,我们需要先了解两个概念: - `Dependent`:使用依赖注入的函数或其他任意可调用对象。如:事件处理函数、自定义的依赖函数等。 - `Dependency`:依赖注入的对象。如:当前事件、机器人等。 在之前的文档中,我们已经多次使用了依赖注入来获取事件信息。通过对函数参数依照一定规则填写类型注解,即可获得想要的上下文信息。任何一个事件处理函数在添加到事件处理流程时,都会根据一定规则提前将其解析成一个 `Dependent` 对象,方便运行时进行注入。如果遇到无法解析的参数,将会抛出 `ValueError("Unknown parameter")` 的异常。整个依赖注入系统可以分为两部分: - 参数解析 - 依据一定规则解析函数参数,识别 `Dependency` 依赖。 - 生成 `Dependent` 对象。 - 执行 - 根据已经解析的 `Dependency` 依赖,执行调用。 - 将所有 `Dependency` 的返回值根据参数名传入并调用 `Dependent` 。 :::danger 警告 在依赖注入中,类型注解是非常重要的,因为它不仅可以决定依赖注入的对象,还可以触发[重载机制](../appendices/overload.md#重载)。如果类型注解与实际获得数据类型不一致,将会跳过当前 `Dependent` 对象(即事件处理函数)。 ::: :::tip 提示 如果对于依赖注入的解析流程有疑问,可以调整[日志等级配置项](../appendices/config.mdx#log-level)为 `TRACE`,查看依赖解析日志。 ::: ## 同步支持 对于依赖注入系统中的 `Dependent` 或者 `Dependency` 对象,均支持同步类型的函数或可调用对象。例如: ```python {6,10} from nonebot import on_command from nonebot.params import Depends matcher = on_command("foo") def dependency() -> str: return "something" @matcher.handle() def _(result: str = Depends(dependency)): ... ``` ## 非依赖参数 在依赖注入解析中,任何无法解析的参数如果带有默认值,将会被视为非依赖参数。这些参数在依赖运行时将不会被注入而使用函数默认值。例如: ```python async def _(foo: str = "bar"): ... ``` ## 类型依赖注入 这一类的依赖注入仅需要在函数参数中添加对应的类型注解即可。 ### Bot 获取当前事件的 Bot 对象。 通过标注参数为 `Bot` 类型,或者一系列 `Bot` 类型,即可获取到当前事件的 Bot 对象。为兼容性考虑,如果参数名为 `bot` 且无类型注解,也会视为 Bot 依赖注入。 Bot 依赖注入支持重载(即:可以标注参数为子类型)且具有[重载优先检查权](../appendices/overload.md#重载)。 ```python from nonebot.adapters import Bot from nonebot.adapters.console import Bot as ConsoleBot from nonebot.adapters.onebot.v11 import Bot as OneBotV11Bot async def _(foo: Bot): ... async def _(foo: ConsoleBot | OneBotV11Bot): ... async def _(bot): ... # 兼容性处理 ``` ```python from typing import Union from nonebot.adapters import Bot from nonebot.adapters.console import Bot as ConsoleBot from nonebot.adapters.onebot.v11 import Bot as OneBotV11Bot async def _(foo: Bot): ... async def _(foo: Union[ConsoleBot, OneBotV11Bot]): ... async def _(bot): ... # 兼容性处理 ``` ### Event 获取当前事件。 通过标注参数为 `Event` 类型,或者一系列 `Event` 类型,即可获取到当前事件。为兼容性考虑,如果参数名为 `event` 且无类型注解,也会视为 Event 依赖注入。 Event 依赖注入支持重载(即:可以标注参数为子类型)且具有[重载优先检查权](../appendices/overload.md#重载)。 ```python from nonebot.adapters import Event from nonebot.adapters.onebot.v11 import PrivateMessageEvent, GroupMessageEvent async def _(foo: Event): ... async def _(foo: PrivateMessageEvent | GroupMessageEvent): ... async def _(event): ... # 兼容性处理 ``` ```python from typing import Union from nonebot.adapters import Event from nonebot.adapters.onebot.v11 import PrivateMessageEvent, GroupMessageEvent async def _(foo: Event): ... async def _(foo: Union[PrivateMessageEvent, GroupMessageEvent]): ... async def _(event): ... # 兼容性处理 ``` ### State 获取当前[会话状态](../appendices/session-state.md)。 通过标注参数为 `T_State` 类型,即可获取到当前会话状态。为兼容性考虑,如果参数名为 `state` 且无类型注解,也会视为 State 依赖注入。 ```python from nonebot.typing import T_State async def _(foo: T_State): ... ``` ### Matcher 获取当前事件响应器实例。常用于使用[事件响应器操作](../appendices/session-control.mdx)。 通过标注参数为 `Matcher` 类型,或者一系列 `Matcher` 类型,即可获取到当前事件。为兼容性考虑,如果参数名为 `matcher` 且无类型注解,也会视为 Matcher 依赖注入。 Matcher 依赖注入支持重载(即:可以标注参数为子类型)且具有[重载优先检查权](../appendices/overload.md#重载)。 ```python from nonebot.matcher import Matcher async def _(foo: Matcher): ... async def _(matcher): ... # 兼容性处理 ``` ### Exception 获取事件响应器运行中抛出的异常。该依赖注入目前仅在事件响应器运行后处理 Hook 中可用。 通过标注参数为异常类型,或者一系列异常类型,即可获取到事件响应器运行中抛出的异常。 ```python {5,8} from nonebot.message import run_postprocessor from nonebot.exception import ActionFailed, NetworkError @run_postprocessor async def _(e: Exception): ... @run_postprocessor async def _(e: ActionFailed | NetworkError): ... ``` ```python {6,9} from typing import Union from nonebot.message import run_postprocessor from nonebot.exception import ActionFailed, NetworkError @run_postprocessor async def _(e: Exception): ... @run_postprocessor async def _(e: Union[ActionFailed, NetworkError]): ... ``` ## 子依赖 在依赖注入系统中,我们可以定义一个子依赖,来执行自定义的操作,提高代码复用性以及处理性能。 ### 定义子依赖 子依赖使用 `Depends` 标记进行定义,其参数即依赖的函数或可调用对象,同样会被解析为 `Dependent` 对象,将会在依赖注入期间执行。我们来看一个例子: ```python {5,15} from typing import Annotated from nonebot import on_command from nonebot.adapters import Event from nonebot.params import Depends test = on_command("test") async def check(event: Event) -> Event: if event.get_user_id() in BLACKLIST: await test.finish() return event @test.handle() async def _(event: Annotated[Event, Depends(check)]): ... ``` ```python {3,13} from nonebot import on_command from nonebot.adapters import Event from nonebot.params import Depends test = on_command("test") async def check(event: Event) -> Event: if event.get_user_id() in BLACKLIST: await test.finish() return event @test.handle() async def _(event: Event = Depends(check)): ... ``` 在上面的代码中,我们使用 `Depends` 标记定义了一个子依赖 `check`。它判断事件主体用户是否在黑名单中,如果在,则直接结束事件处理流程。如果不在,则返回事件对象,以便事件处理函数可以继续执行。 通过将 `Depends` 包裹的子依赖作为参数的默认值,我们就可以在执行事件处理函数之前执行子依赖,并将其返回值作为参数传入事件处理函数。子依赖和普通的事件处理函数并没有区别,同样可以使用依赖注入,并且可以返回任何类型的值。但需要注意的是,如果事件处理函数参数的类型注解与子依赖返回值的类型**不一致**,将会触发[重载](../appendices/overload.md)而跳过当前事件处理函数。 特别的,我们可以为 `Dependent` 对象定义一系列前置子依赖,它们会在参数执行前被顺序执行,且返回值将会被忽略,例如: ```python {11} from nonebot import on_command from nonebot.adapters import Event from nonebot.params import Depends test = on_command("test") async def check(event: Event): if event.get_user_id() in BLACKLIST: await test.finish() @test.handle(parameterless=[Depends(check)]) async def _(): ... ``` ### 依赖缓存 NoneBot 在执行子依赖时,会将其返回值缓存起来。当我们在使用子依赖时,`Depends` 具有一个参数 `use_cache`,默认为 `True`。此时在事件处理流程中,多次使用同一个子依赖时,将会使用缓存中的结果而不会重复执行。这在很多情景中非常有用,例如: ```python {7} import random from typing import Annotated async def random_result() -> int: return random.randint(1, 100) async def _(x: Annotated[int, Depends(random_result)]): print(x) ``` ```python {6} import random async def random_result() -> int: return random.randint(1, 100) async def _(x: int = Depends(random_result)): print(x) ``` 此时,在同一事件处理流程中,这个随机函数的返回值将会保持一致。如果我们希望每次都重新执行子依赖,可以将 `use_cache` 设置为 `False`。 ```python {7} import random from typing import Annotated async def random_result() -> int: return random.randint(1, 100) async def _(x: Annotated[int, Depends(random_result, use_cache=False)]): print(x) ``` ```python {6} import random async def random_result() -> int: return random.randint(1, 100) async def _(x: int = Depends(random_result, use_cache=False)): print(x) ``` :::tip 提示 缓存的生命周期与当前接收到的事件相同。接收到事件后,子依赖在首次执行时缓存,在该事件处理完成后,缓存就会被清除。 ::: ### 类型转换与校验 在依赖注入系统中,我们可以对子依赖的返回值进行自动类型转换与校验。这个功能由 Pydantic 支持,因此我们通过参数类型注解自动使用 Pydantic 支持的类型转换。例如: ```python {6,9} from typing import Annotated from nonebot.params import Depends from nonebot.adapters import Event def get_user_id(event: Event) -> str: return event.get_user_id() async def _(user_id: Annotated[int, Depends(get_user_id, validate=True)]): print(user_id) ``` ```python {4,7} from nonebot.params import Depends from nonebot.adapters import Event def get_user_id(event: Event) -> str: return event.get_user_id() async def _(user_id: int = Depends(get_user_id, validate=True)): print(user_id) ``` 在进行类型自动转换的同时,Pydantic 还支持对数据进行更多的限制,如:大于、小于、长度等。使用方法如下: ```python {7,10} from typing import Annotated from pydantic import Field from nonebot.params import Depends from nonebot.adapters import Event def get_user_id(event: Event) -> str: return event.get_user_id() async def _(user_id: Annotated[int, Depends(get_user_id, validate=Field(gt=100))]): print(user_id) ``` ```python {5,8} from pydantic import Field from nonebot.params import Depends from nonebot.adapters import Event def get_user_id(event: Event) -> str: return event.get_user_id() async def _(user_id: int = Depends(get_user_id, validate=Field(gt=100))): print(user_id) ``` ### 类作为依赖 在前面的事例中,我们使用了函数作为子依赖。实际上,我们还可以使用类作为依赖。当我们在实例化一个类的时候,其实我们就在调用它,类本身也是一个可调用对象。例如: ```python {16} from typing import Annotated from dataclasses import dataclass from nonebot.params import Depends from nonebot.adapters import Event from nonebot.typing import T_State def get_context(state: T_State) -> dict: return state.setdefault("context", {}) @dataclass class ClassDependency: event: Event context: dict = Depends(get_context) async def _(data: Annotated[ClassDependency, Depends(ClassDependency)]): print(data.event, data.context) ``` ```python {15} from dataclasses import dataclass from nonebot.params import Depends from nonebot.adapters import Event from nonebot.typing import T_State def get_context(state: T_State) -> dict: return state.setdefault("context", {}) @dataclass class ClassDependency: event: Event context: dict = Depends(get_context) async def _(data: ClassDependency = Depends(ClassDependency)): print(data.event, data.context) ``` 可以看到,我们使用 `dataclass` 定义了一个类。由于这个类的 `__init__` 方法可以被依赖注入系统解析,因此,我们可以将其作为子依赖进行声明。特别地,对于类依赖,`Depends` 的参数可以为空,NoneBot 将会使用参数的类型注解进行解析与推断: ```python from typing import Annotated async def _(data: Annotated[ClassDependency, Depends()]): print(data.event, data.context) ``` ```python async def _(data: ClassDependency = Depends()): print(data.event, data.context) ``` ### 生成器作为依赖 NoneBot 的依赖注入支持依赖项在事件处理流程结束后进行一些额外的工作,比如数据库 session 或者网络 IO 的关闭,互斥锁的解锁等等。同时,由于[依赖缓存](#依赖缓存)的存在,我们可以通过这种方式来实现共享一个 session 等功能。 要实现上述功能,我们可以用生成器函数作为依赖项,我们用 `yield` 关键字取代 `return` 关键字,并在 `yield` 之后进行额外的工作。 我们可以看下述代码段, 使用 `httpx.AsyncClient` 异步网络 IO,并在事件处理流程中共用一个 client: ```python {15} from typing import Annotated from collections.abc import AsyncGenerator import httpx from nonebot.params import Depends async def get_client() -> AsyncGenerator[httpx.AsyncClient, None]: try: async with httpx.AsyncClient() as client: yield client finally: # 在这里进行额外的工作 @test.handle() async def _(x: Annotated[httpx.AsyncClient, Depends(get_client)]): resp = await x.get("https://nonebot.dev") ``` ```python {15} from collections.abc import AsyncGenerator import httpx from nonebot.params import Depends async def get_client() -> AsyncGenerator[httpx.AsyncClient, None]: try: async with httpx.AsyncClient() as client: yield client finally: # 在这里进行额外的工作 @test.handle() async def _(x: httpx.AsyncClient = Depends(get_client)): resp = await x.get("https://nonebot.dev") ``` :::caution 注意 生成器作为依赖时,其中只能进行一次 `yield`,否则将会触发异常。如果对此有疑问并想探究原因,可以参考 [contextmanager](https://docs.python.org/zh-cn/3/library/contextlib.html#contextlib.contextmanager) 和 [asynccontextmanager](https://docs.python.org/zh-cn/3/library/contextlib.html#contextlib.asynccontextmanager) 文档。事实上,NoneBot 内部就使用了这两个装饰器。 ::: ### 可调用对象作为依赖 在 Python 里,为类定义 `__call__` 方法就可以使得这个类的实例成为一个可调用对象。因此,我们也可以将定义了 `__call__` 方法的类的实例作为依赖。事实上,NoneBot 的[内置响应规则](./matcher.md#内置响应规则)就广泛使用了这种方式,以 `is_type` 规则为例: ```python from nonebot.adapters import Event class IsTypeRule: def __init__(self, *types: type[Event]): self.types = types async def __call__(self, event: Event) -> bool: return isinstance(event, self.types) ``` 我们在使用 `is_type` 时,即实例化了 `IsTypeRule` 类,然后将实例作为响应规则依赖项传入。 ## 其他依赖注入 这一类的依赖注入通常基于子依赖编写,为我们开发者提供更方便的途径获取上下文信息。 ### EventType 获取当前事件的类型。 ```python {4} from typing import Annotated from nonebot.params import EventType async def _(foo: Annotated[str, EventType()]): ... ``` ```python {3} from nonebot.params import EventType async def _(foo: str = EventType()): ... ``` ### EventMessage 获取当前事件的消息。 ```python {5} from typing import Annotated from nonebot.adapters import Message from nonebot.params import EventMessage async def _(foo: Annotated[Message, EventMessage()]): ... ``` ```python {4} from nonebot.adapters import Message from nonebot.params import EventMessage async def _(foo: Message = EventMessage()): ... ``` ### EventPlainText 获取当前事件的消息纯文本部分。 ```python {4} from typing import Annotated from nonebot.params import EventPlainText async def _(foo: Annotated[str, EventPlainText()]): ... ``` ```python {3} from nonebot.params import EventPlainText async def _(foo: str = EventPlainText()): ... ``` ### EventToMe 获取当前事件是否与机器人相关。 ```python {4} from typing import Annotated from nonebot.params import EventToMe async def _(foo: Annotated[bool, EventToMe()]): ... ``` ```python {3} from nonebot.params import EventToMe async def _(foo: bool = EventToMe()): ... ``` ### Command 获取当前命令型消息的元组形式命令名。 ```python {4} from typing import Annotated from nonebot.params import Command async def _(foo: Annotated[tuple[str, ...], Command()]): ... ``` ```python {4} from nonebot.params import Command async def _(foo: tuple[str, ...] = Command()): ... ``` :::tip 提示 命令详情只能在**触发命令型事件响应器时**获取。如果在事件处理后续流程中获取,则会获取到不同的值。 ::: ### RawCommand 获取当前命令型消息的文本形式命令名。 ```python {4} from typing import Annotated from nonebot.params import RawCommand async def _(foo: Annotated[str, RawCommand()]): ... ``` ```python {3} from nonebot.params import RawCommand async def _(foo: str = RawCommand()): ... ``` :::tip 提示 命令详情只能在**触发命令型事件响应器时**获取。如果在事件处理后续流程中获取,则会获取到不同的值。 ::: ### CommandArg 获取命令型消息命令后跟随的参数。 ```python {5} from typing import Annotated from nonebot.adapters import Message from nonebot.params import CommandArg async def _(foo: Annotated[Message, CommandArg()]): ... ``` ```python {4} from nonebot.adapters import Message from nonebot.params import CommandArg async def _(foo: Message = CommandArg()): ... ``` :::tip 提示 命令详情只能在**触发命令型事件响应器时**获取。如果在事件处理后续流程中获取,则会获取到不同的值。 ::: ### CommandStart 获取命令型消息命令前缀。 ```python {4} from typing import Annotated from nonebot.params import CommandStart async def _(foo: Annotated[str, CommandStart()]): ... ``` ```python {3} from nonebot.params import CommandStart async def _(foo: str = CommandStart()): ... ``` :::tip 提示 命令详情只能在**触发命令型事件响应器时**获取。如果在事件处理后续流程中获取,则会获取到不同的值。 ::: ### CommandWhitespace 获取命令型消息命令与参数间空白符。 ```python {4} from typing import Annotated from nonebot.params import CommandWhitespace async def _(foo: Annotated[str, CommandWhitespace()]): ... ``` ```python {3} from nonebot.params import CommandWhitespace async def _(foo: str = CommandWhitespace()): ... ``` :::tip 提示 命令详情只能在**触发命令型事件响应器时**获取。如果在事件处理后续流程中获取,则会获取到不同的值。 ::: ### ShellCommandArgv 获取 shell 命令解析前的参数列表,列表中可能包含文本字符串和富文本消息段(如:图片)。当词法解析出错的时候,返回值将为 `None`。通过重载机制即可处理两种不同的情况。 ```python {4} from typing import Annotated from nonebot import on_shell_command from nonebot.params import ShellCommandArgv matcher = on_shell_command("cmd") # 解析失败 @matcher.handle() async def _(foo: Annotated[None, ShellCommandArgv()]): ... # 解析成功 @matcher.handle() async def _(foo: Annotated[list[str | MessageSegment], ShellCommandArgv()]): ... ``` ```python {4} from nonebot import on_shell_command from nonebot.params import ShellCommandArgv matcher = on_shell_command("cmd") # 解析失败 @matcher.handle() async def _(foo: None = ShellCommandArgv()): ... # 解析成功 @matcher.handle() async def _(foo: list[str | MessageSegment] = ShellCommandArgv()): ... ``` ```python {4} from typing import Union, Annotated from nonebot import on_shell_command from nonebot.params import ShellCommandArgv matcher = on_shell_command("cmd") # 解析失败 @matcher.handle() async def _(foo: Annotated[None, ShellCommandArgv()]): ... # 解析成功 @matcher.handle() async def _(foo: Annotated[list[Union[str, MessageSegment]], ShellCommandArgv()]): ... ``` ```python {4} from typing import Union from nonebot import on_shell_command from nonebot.params import ShellCommandArgv matcher = on_shell_command("cmd") # 解析失败 @matcher.handle() async def _(foo: None = ShellCommandArgv()): ... # 解析成功 @matcher.handle() async def _(foo: list[Union[str, MessageSegment]] = ShellCommandArgv()): ... ``` ### ShellCommandArgs 获取 shell 命令解析后的参数 Namespace,支持 MessageSegment 富文本(如:图片)。 :::tip 提示 如果参数解析成功,则为 parser 返回的 Namespace;如果参数解析失败,则为 [`ParserExit`](../api/exception.md#ParserExit) 异常,并携带错误码与错误信息。在前置词法解析失败时,返回值也为 [`ParserExit`](../api/exception.md#ParserExit) 异常。通过重载机制即可处理两种不同的情况。 由于 `ArgumentParser` 在解析到 `--help` 参数时也会抛出异常,这种情况下错误码为 `0` 且错误信息即为帮助信息。 ::: ```python {14,22} from typing import Annotated from nonebot import on_shell_command from nonebot.exception import ParserExit from nonebot.params import ShellCommandArgs from nonebot.rule import Namespace, ArgumentParser parser = ArgumentParser("demo") # parser.add_argument ... matcher = on_shell_command("cmd", parser=parser) # 解析失败 @matcher.handle() async def _(foo: Annotated[ParserExit, ShellCommandArgs()]): if foo.status == 0: foo.message # help message else: foo.message # error message # 解析成功 @matcher.handle() async def _(foo: Annotated[Namespace, ShellCommandArgs()]): arg_dict = vars(foo) ``` ```python {12,20} from nonebot import on_shell_command from nonebot.exception import ParserExit from nonebot.params import ShellCommandArgs from nonebot.rule import Namespace, ArgumentParser parser = ArgumentParser("demo") # parser.add_argument ... matcher = on_shell_command("cmd", parser=parser) # 解析失败 @matcher.handle() async def _(foo: ParserExit = ShellCommandArgs()): if foo.status == 0: foo.message # help message else: foo.message # error message # 解析成功 @matcher.handle() async def _(foo: Namespace = ShellCommandArgs()): arg_dict = vars(foo) ``` ### RegexMatched 获取正则匹配结果的对象。 ```python {5} from re import Match from typing import Annotated from nonebot.params import RegexMatched async def _(foo: Annotated[Match[str], RegexMatched()]): ... ``` ```python {4} from re import Match from nonebot.params import RegexMatched async def _(foo: Match[str] = RegexMatched()): ... ``` ### RegexStr 获取正则匹配结果的文本。 ```python {4} from typing import Annotated from nonebot.params import RegexStr async def _(foo: Annotated[str, RegexStr()]): ... ``` ```python {3} from nonebot.params import RegexStr async def _(foo: str = RegexStr()): ... ``` ### RegexGroup 获取正则匹配结果的 group 元组。 ```python {4} from typing import Any, Annotated from nonebot.params import RegexGroup async def _(foo: Annotated[tuple[Any, ...], RegexGroup()]): ... ``` ```python {4} from typing import Any from nonebot.params import RegexGroup async def _(foo: tuple[Any, ...] = RegexGroup()): ... ``` ### RegexDict 获取正则匹配结果的 group 字典。 ```python {4} from typing import Any, Annotated from nonebot.params import RegexDict async def _(foo: Annotated[dict[str, Any], RegexDict()]): ... ``` ```python {4} from typing import Any from nonebot.params import RegexDict async def _(foo: dict[str, Any] = RegexDict()): ... ``` ### Startswith 获取触发响应器的消息前缀字符串。 ```python {4} from typing import Annotated from nonebot.params import Startswith async def _(foo: Annotated[str, Startswith()]): ... ``` ```python {3} from nonebot.params import Startswith async def _(foo: str = Startswith()): ... ``` ### Endswith 获取触发响应器的消息后缀字符串。 ```python {4} from typing import Annotated from nonebot.params import Endswith async def _(foo: Annotated[str, Endswith()]): ... ``` ```python {3} from nonebot.params import Endswith async def _(foo: str = Endswith()): ... ``` ### Fullmatch 获取触发响应器的消息字符串。 ```python {4} from typing import Annotated from nonebot.params import Fullmatch async def _(foo: Annotated[str, Fullmatch()]): ... ``` ```python {3} from nonebot.params import Fullmatch async def _(foo: str = Fullmatch()): ... ``` ### Keyword 获取触发响应器的关键字字符串。 ```python {4} from typing import Annotated from nonebot.params import Keyword async def _(foo: Annotated[str, Keyword()]): ... ``` ```python {3} from nonebot.params import Keyword async def _(foo: str = Keyword()): ... ``` ### Received 获取某次 `receive` 接收的事件。 ```python {7} from typing import Annotated from nonebot.adapters import Event from nonebot.params import Received @matcher.receive("id") async def _(foo: Annotated[Event, Received("id")]): ... ``` ```python {5} from nonebot.adapters import Event from nonebot.params import Received @matcher.receive("id") async def _(foo: Event = Received("id")): ... ``` ### LastReceived 获取最近一次 `receive` 接收的事件。 ```python {7} from typing import Annotated from nonebot.adapters import Event from nonebot.params import LastReceived @matcher.receive("any") async def _(foo: Annotated[Event, LastReceived()]): ... ``` ```python {5} from nonebot.adapters import Event from nonebot.params import LastReceived @matcher.receive("any") async def _(foo: Event = LastReceived()): ... ``` ### ReceivePromptResult 获取某次 `receive` 发送提示消息的结果。 ```python {6} from typing import Any, Annotated from nonebot.params import ReceivePromptResult @matcher.receive("id", prompt="prompt") async def _(result: Annotated[Any, ReceivePromptResult("id")]): ... ``` ```python {6} from typing import Any from nonebot.params import ReceivePromptResult @matcher.receive("id", prompt="prompt") async def _(result: Any = ReceivePromptResult("id")): ... ``` ### Arg 获取某次 `got` 接收的参数。如果 `Arg` 参数留空,则使用函数的参数名作为要获取的参数。 ```python {7,8} from typing import Annotated from nonebot.params import Arg from nonebot.adapters import Message @matcher.got("key") async def _(key: Annotated[Message, Arg()]): ... async def _(foo: Annotated[Message, Arg("key")]): ... ``` ```python {5,6} from nonebot.params import Arg from nonebot.adapters import Message @matcher.got("key") async def _(key: Message = Arg()): ... async def _(foo: Message = Arg("key")): ... ``` ### ArgStr 获取某次 `got` 接收的参数,并转换为字符串。如果 `Arg` 参数留空,则使用函数的参数名作为要获取的参数。 ```python {6,7} from typing import Annotated from nonebot.params import ArgStr @matcher.got("key") async def _(key: Annotated[str, ArgStr()]): ... async def _(foo: Annotated[str, ArgStr("key")]): ... ``` ```python {4,5} from nonebot.params import ArgStr @matcher.got("key") async def _(key: str = ArgStr()): ... async def _(foo: str = ArgStr("key")): ... ``` ### ArgPlainText 获取某次 `got` 接收的参数的纯文本部分。如果 `Arg` 参数留空,则使用函数的参数名作为要获取的参数。 ```python {6,7} from typing import Annotated from nonebot.params import ArgPlainText @matcher.got("key") async def _(key: Annotated[str, ArgPlainText()]): ... async def _(foo: Annotated[str, ArgPlainText("key")]): ... ``` ```python {4,5} from nonebot.params import ArgPlainText @matcher.got("key") async def _(key: str = ArgPlainText()): ... async def _(foo: str = ArgPlainText("key")): ... ``` ### ArgPromptResult 获取某次 `got` 发送提示消息的结果。如果 `Arg` 参数留空,则使用函数的参数名作为要获取的参数。 ```python {6,7} from typing import Any, Annotated from nonebot.params import ArgPromptResult @matcher.got("key", prompt="prompt") async def _(result: Annotated[Any, ArgPromptResult()]): ... async def _(result: Annotated[Any, ArgPromptResult("key")]): ... ``` ```python {6,7} from typing import Any from nonebot.params import ArgPromptResult @matcher.got("key", prompt="prompt") async def _(result: Any = ArgPromptResult()): ... async def _(result: Any = ArgPromptResult("key")): ... ``` ### PausePromptResult 获取最近一次 `pause` 发送提示消息的结果。 ```python {6} from typing import Any, Annotated from nonebot.params import PausePromptResult @matcher.handle() async def _(): await matcher.pause(prompt="prompt") @matcher.handle() async def _(result: Annotated[Any, PausePromptResult()]): ... ``` ```python {6} from typing import Any from nonebot.params import PausePromptResult @matcher.handle() async def _(): await matcher.pause(prompt="prompt") @matcher.handle() async def _(result: Any = PausePromptResult()): ... ``` ================================================ FILE: website/docs/advanced/driver.md ================================================ --- sidebar_position: 0 description: 选择合适的驱动器运行机器人 options: menu: - category: advanced weight: 10 --- # 选择驱动器 驱动器 (Driver) 是机器人运行的基石,它是机器人初始化的第一步,主要负责数据收发。 :::important 提示 驱动器的选择通常与机器人所使用的协议适配器相关,如果不知道该选择哪个驱动器,可以先阅读相关协议适配器文档说明。 ::: :::tip 提示 如何**安装**驱动器请参考[安装驱动器](../tutorial/store.mdx#安装驱动器)。 ::: ## 驱动器类型 驱动器类型大体上可以分为两种: - `Forward`:即客户端型驱动器,多用于使用 HTTP 轮询,连接 WebSocket 服务器等情形。 - `Reverse`:即服务端型驱动器,多用于使用 WebHook,接收 WebSocket 客户端连接等情形。 客户端型驱动器可以分为以下两种: 1. 异步发送 HTTP 请求,自定义 `HTTP Method`、`URL`、`Header`、`Body`、`Cookie`、`Proxy`、`Timeout` 等。 2. 异步建立 WebSocket 连接上下文,自定义 `WebSocket URL`、`Header`、`Cookie`、`Proxy`、`Timeout` 等。 服务端型驱动器目前有: 1. ASGI 应用框架,具有以下功能: - 协议适配器自定义 HTTP 上报地址以及对上报数据处理的回调函数。 - 协议适配器自定义 WebSocket 连接请求地址以及对 WebSocket 请求处理的回调函数。 - 用户可以向 ASGI 应用添加任何服务端相关功能,如:[添加自定义路由](./routing.md)。 ## 配置驱动器 驱动器的配置方法已经在[配置](../appendices/config.mdx)章节中简单进行了介绍,这里将详细介绍驱动器配置的格式。 NoneBot 中的客户端和服务端型驱动器可以相互配合使用,但服务端型驱动器**仅能选择一个**。所有驱动器模块都会包含一个 `Driver` 子类,即驱动器类,他可以作为驱动器单独运行。同时,客户端驱动器模块中还会提供一个 `Mixin` 子类,用于在与其他驱动器配合使用时加载。因此,驱动器配置格式采用特殊语法:`[:][+[:]]*`。 其中,`` 代表**驱动器模块路径**;`` 代表**驱动器类名**,默认为 `Driver`;`` 代表**驱动器混入类名**,默认为 `Mixin`。即,我们需要选择一个主要驱动器,然后在其基础上配合使用其他驱动器的功能。主要驱动器可以为客户端或服务端类型,但混入类驱动器只能为客户端类型。 特别的,为了简化内置驱动器模块路径,我们可以使用 `~` 符号作为内置驱动器模块路径的前缀,如 `~fastapi` 代表使用内置驱动器 `fastapi`。NoneBot 内置了多个驱动器适配,但需要安装额外依赖才能使用,具体请参考[安装驱动器](../tutorial/store.mdx#安装驱动器)。常见的驱动器配置如下: ```dotenv DRIVER=~fastapi DRIVER=~aiohttp DRIVER=~httpx+~websockets DRIVER=~fastapi+~httpx+~websockets ``` ## 获取驱动器 在 NoneBot 框架初始化完成后,我们就可以通过 `get_driver()` 方法获取全局驱动器实例: ```python from nonebot import get_driver driver = get_driver() ``` ## 内置驱动器 ### None **类型:**服务端驱动器 NoneBot 内置的空驱动器,不提供任何收发数据功能,可以在不需要外部网络连接时使用。 ```env DRIVER=~none ``` ### FastAPI(默认) **类型:**ASGI 服务端驱动器 > FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints. [FastAPI](https://fastapi.tiangolo.com/) 是一个易上手、高性能的异步 Web 框架,具有极佳的编写体验。 FastAPI 可以通过类型注解、依赖注入等方式实现输入参数校验、自动生成 API 文档等功能,也可以挂载其他 ASGI、WSGI 应用。 ```env DRIVER=~fastapi ``` #### FastAPI 配置项 ##### `fastapi_openapi_url` 类型:`str | None` 默认值:`None` 说明:`FastAPI` 提供的 `OpenAPI` JSON 定义地址,如果为 `None`,则不提供 `OpenAPI` JSON 定义。 ##### `fastapi_docs_url` 类型:`str | None` 默认值:`None` 说明:`FastAPI` 提供的 `Swagger` 文档地址,如果为 `None`,则不提供 `Swagger` 文档。 ##### `fastapi_redoc_url` 类型:`str | None` 默认值:`None` 说明:`FastAPI` 提供的 `ReDoc` 文档地址,如果为 `None`,则不提供 `ReDoc` 文档。 ##### `fastapi_include_adapter_schema` 类型:`bool` 默认值:`True` 说明:`FastAPI` 提供的 `OpenAPI` JSON 定义中是否包含适配器路由的 `Schema`。 ##### `fastapi_reload` :::caution 警告 不推荐开启该配置项,在 Windows 平台上开启该功能有可能会造成预料之外的影响!替代方案:使用 `nb-cli` 命令行工具以及参数 `--reload` 启动 NoneBot。 ```bash nb run --reload ``` 开启该功能后,在 uvicorn 运行时(FastAPI 提供的 ASGI 底层,即 reload 功能的实际来源),asyncio 使用的事件循环会被 uvicorn 从默认的 `ProactorEventLoop` 强制切换到 `SelectorEventLoop`。 > 相关信息参考 [uvicorn#529](https://github.com/encode/uvicorn/issues/529),[uvicorn#1070](https://github.com/encode/uvicorn/pull/1070),[uvicorn#1257](https://github.com/encode/uvicorn/pull/1257) 后者(`SelectorEventLoop`)在 Windows 平台的可使用性不如前者(`ProactorEventLoop`),包括但不限于 1. 不支持创建子进程 2. 最多只支持 512 个套接字 3. ... > 具体信息参考 [Python 文档](https://docs.python.org/zh-cn/3/library/asyncio-platforms.html#windows) 所以,一些使用了 asyncio 的库因此可能无法正常工作,如: 1. [playwright](https://playwright.dev/python/docs/library#incompatible-with-selectoreventloop-of-asyncio-on-windows) 如果在开启该功能后,原本**正常运行**的代码报错,且打印的异常堆栈信息和 asyncio 有关(异常一般为 `NotImplementedError`), 你可能就需要考虑相关库对事件循环的支持,以及是否启用该功能。 ::: 类型:`bool` 默认值:`False` 说明:是否开启 `uvicorn` 的 `reload` 功能,需要在机器人入口文件提供 ASGI 应用路径。 ```python title=bot.py app = nonebot.get_asgi() nonebot.run(app="bot:app") ``` ##### `fastapi_reload_dirs` 类型:`List[str] | None` 默认值:`None` 说明:重载监控文件夹列表,默认为 uvicorn 默认值 ##### `fastapi_reload_delay` 类型:`float | None` 默认值:`None` 说明:重载延迟,默认为 uvicorn 默认值 ##### `fastapi_reload_includes` 类型:`List[str] | None` 默认值:`None` 说明:要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值 ##### `fastapi_reload_excludes` 类型:`List[str] | None` 默认值:`None` 说明:不要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值 ##### `fastapi_extra` 类型:`Dist[str, Any]` 默认值:`{}` 说明:传递给 `FastAPI` 的其他参数 ### Quart **类型:**ASGI 服务端驱动器 > Quart is an asyncio reimplementation of the popular Flask microframework API. [Quart](https://quart.palletsprojects.com/) 是一个类 Flask 的异步版本,拥有与 Flask 非常相似的接口和使用方法。 ```env DRIVER=~quart ``` #### Quart 配置项 ##### `quart_reload` :::caution 警告 不推荐开启该配置项,在 Windows 平台上开启该功能有可能会造成预料之外的影响!替代方案:使用 `nb-cli` 命令行工具以及参数 `--reload` 启动 NoneBot。 ```bash nb run --reload ``` ::: 类型:`bool` 默认值:`False` 说明:是否开启 `uvicorn` 的 `reload` 功能,需要在机器人入口文件提供 ASGI 应用路径。 ```python title=bot.py app = nonebot.get_asgi() nonebot.run(app="bot:app") ``` ##### `quart_reload_dirs` 类型:`List[str] | None` 默认值:`None` 说明:重载监控文件夹列表,默认为 uvicorn 默认值 ##### `quart_reload_delay` 类型:`float | None` 默认值:`None` 说明:重载延迟,默认为 uvicorn 默认值 ##### `quart_reload_includes` 类型:`List[str] | None` 默认值:`None` 说明:要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值 ##### `quart_reload_excludes` 类型:`List[str] | None` 默认值:`None` 说明:不要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值 ##### `quart_extra` 类型:`Dist[str, Any]` 默认值:`{}` 说明:传递给 `Quart` 的其他参数 ### HTTPX **类型:**HTTP 客户端驱动器 :::caution 注意 本驱动器仅支持 HTTP 请求,不支持 WebSocket 连接请求。 ::: > [HTTPX](https://www.python-httpx.org/) is a fully featured HTTP client for Python 3, which provides sync and async APIs, and support for both HTTP/1.1 and HTTP/2. ```env DRIVER=~httpx ``` ### websockets **类型:**WebSocket 客户端驱动器 :::caution 注意 本驱动器仅支持 WebSocket 连接请求,不支持 HTTP 请求。 ::: > [websockets](https://websockets.readthedocs.io/) is a library for building WebSocket servers and clients in Python with a focus on correctness, simplicity, robustness, and performance. ```env DRIVER=~websockets ``` ### AIOHTTP **类型:**HTTP/WebSocket 客户端驱动器 > [AIOHTTP](https://docs.aiohttp.org/): Asynchronous HTTP Client/Server for asyncio and Python. ```env DRIVER=~aiohttp ``` ================================================ FILE: website/docs/advanced/matcher-provider.md ================================================ --- sidebar_position: 10 description: 自定义事件响应器存储 options: menu: - category: advanced weight: 110 --- # 事件响应器存储 事件响应器是 NoneBot 处理事件的核心,它们默认存储在一个字典中。在进入会话状态后,事件响应器将会转为临时响应器,作为最高优先级同样存储于该字典中。因此,事件响应器的存储类似于会话存储,它决定了整个 NoneBot 对事件的处理行为。 NoneBot 默认使用 Python 的字典将事件响应器存储于内存中,但是我们也可以自定义事件响应器存储,将事件响应器存储于其他地方,例如 Redis 等。这样我们就可以实现持久化、在多实例间共享会话状态等功能。 ## 编写存储提供者 事件响应器的存储提供者 `MatcherProvider` 抽象类继承自 `MutableMapping[int, list[type[Matcher]]]`,即以优先级为键,以事件响应器列表为值的映射。我们可以方便地进行逐优先级事件传播。 编写一个自定义的存储提供者,只需要继承并实现 `MatcherProvider` 抽象类: ```python from nonebot.matcher import MatcherProvider class CustomProvider(MatcherProvider): ... ``` ## 设置存储提供者 我们可以通过 `matchers.set_provider` 方法设置存储提供者: ```python {3} from nonebot.matcher import matchers matchers.set_provider(CustomProvider) assert isinstance(matchers.provider, CustomProvider) ``` ================================================ FILE: website/docs/advanced/matcher.md ================================================ --- sidebar_position: 5 description: 事件响应器组成与内置响应规则 options: menu: - category: advanced weight: 60 --- # 事件响应器进阶 在[指南](../tutorial/matcher.md)与[深入](../appendices/rule.md)中,我们已经介绍了事件响应器的基本用法以及响应规则、权限控制等功能。在这一节中,我们将介绍事件响应器的组成,内置的响应规则,与第三方响应规则拓展。 :::tip 提示 事件响应器允许继承,你可以通过直接继承 `Matcher` 类来创建一个新的事件响应器。 ::: ## 事件响应器组成 ### 事件响应器类型 事件响应器类型 `type` 即是该响应器所要响应的事件类型,只有在接收到的事件类型与该响应器的类型相同时,才会触发该响应器。如果类型为空字符串 `""`,则响应器将会响应所有类型的事件。事件响应器类型的检查在所有其他检查(权限控制、响应规则)之前进行。 NoneBot 内置了四种常用事件类型:`meta_event`、`message`、`notice`、`request`,分别对应元事件、消息、通知、请求。通常情况下,协议适配器会将事件合理地分类至这四种类型中。如果有其他类型的事件需要响应,可以自行定义新的类型。 ### 事件触发权限 事件触发权限 `permission` 是一个 `Permission` 对象,这在[权限控制](../appendices/permission.mdx)一节中已经介绍过。事件触发权限会在事件响应器的类型检查通过后进行检查,如果权限检查通过,则执行响应规则检查。 ### 事件响应规则 事件响应规则 `rule` 是一个 `Rule` 对象,这在[响应规则](../appendices/rule.md)一节中已经介绍过。事件响应器的响应规则会在事件响应器的权限检查通过后进行匹配,如果响应规则检查通过,则触发该响应器。 ### 响应优先级 响应优先级 `priority` 是一个正整数,用于指定响应器的优先级。响应器的优先级越小,越先被触发。如果响应器的优先级相同,则按照响应器的注册顺序进行触发。 ### 阻断 阻断 `block` 是一个布尔值,用于指定响应器是否阻断事件的传播。如果阻断为 `True`,则在该响应器被触发后,事件将不会再传播给其他下一优先级的响应器。 NoneBot 内置的事件响应器中,所有非 `command` 规则的 `message` 类型的事件响应器都会阻断事件传递,其他则不会。 在部分情况中,可以使用 [`stop_propagation`](../appendices/session-control.mdx#stop_propagation) 方法动态阻止事件传播,该方法需要 handler 在参数中获取 matcher 实例后调用方法。 ### 有效期 事件响应器的有效期分为 `temp` 和 `expire_time` 。`temp` 是一个布尔值,用于指定响应器是否为临时响应器。如果为 `True`,则该响应器在被触发后会被自动销毁。`expire_time` 是一个 `datetime` 对象,用于指定响应器的过期时间。如果 `expire_time` 不为 `None`,则在该时间点后,该响应器会被自动销毁。 ### 默认状态 事件响应器的默认状态 `default_state` 是一个 `dict` 对象,用于指定响应器的默认状态。在响应器被触发时,响应器将会初始化默认状态然后开始执行事件处理流程。 ## 基本辅助函数 NoneBot 为四种类型的事件响应器提供了五个基本的辅助函数: - `on`:创建任何类型的事件响应器。 - `on_metaevent`:创建元事件响应器。 - `on_message`:创建消息事件响应器。 - `on_request`:创建请求事件响应器。 - `on_notice`:创建通知事件响应器。 除了 `on` 函数具有一个 `type` 参数外,其余参数均相同: - `rule`:响应规则,可以是 `Rule` 对象或者 `RuleChecker` 函数。 - `permission`:事件触发权限,可以是 `Permission` 对象或者 `PermissionChecker` 函数。 - `handlers`:事件处理函数列表。 - `temp`:是否为临时响应器。 - `expire_time`:响应器的过期时间。 - `priority`:响应器的优先级。 - `block`:是否阻断事件传播。 - `state`:响应器的默认状态。 在消息类型的事件响应器的基础上,NoneBot 还内置了一些常用的响应规则,并结合为辅助函数来方便我们快速创建指定功能的响应器。下面我们逐个介绍。 ## 内置响应规则 :::tip 响应规则的使用方法可以参考 [深入 - 响应规则](../appendices/rule.md)。 ::: ### `startswith` `startswith` 响应规则用于匹配消息纯文本部分的开头是否与指定字符串(或一系列字符串)相同。可选参数 `ignorecase` 用于指定是否忽略大小写,默认为 `False`。 例如,我们可以创建一个匹配消息开头为 `!` 或者 `/` 的规则: ```python from nonebot.rule import startswith rule = startswith(("!", "/"), ignorecase=False) ``` 也可以直接使用辅助函数新建一个响应器: ```python from nonebot import on_startswith matcher = on_startswith(("!", "/"), ignorecase=False) ``` ### `endswith` `endswith` 响应规则用于匹配消息纯文本部分的结尾是否与指定字符串(或一系列字符串)相同。可选参数 `ignorecase` 用于指定是否忽略大小写,默认为 `False`。 例如,我们可以创建一个匹配消息结尾为 `.` 或者 `。` 的规则: ```python from nonebot.rule import endswith rule = endswith((".", "。"), ignorecase=False) ``` 也可以直接使用辅助函数新建一个响应器: ```python from nonebot import on_endswith matcher = on_endswith((".", "。"), ignorecase=False) ``` ### `fullmatch` `fullmatch` 响应规则用于匹配消息纯文本部分是否与指定字符串(或一系列字符串)完全相同。可选参数 `ignorecase` 用于指定是否忽略大小写,默认为 `False`。 例如,我们可以创建一个匹配消息为 `ping` 或者 `pong` 的规则: ```python from nonebot.rule import fullmatch rule = fullmatch(("ping", "pong"), ignorecase=False) ``` 也可以直接使用辅助函数新建一个响应器: ```python from nonebot import on_fullmatch matcher = on_fullmatch(("ping", "pong"), ignorecase=False) ``` ### `keyword` `keyword` 响应规则用于匹配消息纯文本部分是否包含指定字符串(或一系列字符串)。 例如,我们可以创建一个匹配消息中包含 `hello` 或者 `hi` 的规则: ```python from nonebot.rule import keyword rule = keyword("hello", "hi") ``` 也可以直接使用辅助函数新建一个响应器: ```python from nonebot import on_keyword matcher = on_keyword({"hello", "hi"}) ``` ### `command` `command` 是最常用的响应规则,它用于匹配消息是否为命令。它会根据配置中的 [Command Start 和 Command Separator](../appendices/config.mdx#command-start-和-command-separator) 来判断消息是否为命令。 例如,当我们配置了 `Command Start` 为 `/`,`Command Separator` 为 `.` 时: ```python from nonebot.rule import command # 匹配 "/help" 或者 "/帮助" 开头的消息 rule = command("help", "帮助") # 匹配 "/help.cmd" 开头的消息 rule = command(("help", "cmd")) ``` 也可以直接使用辅助函数新建一个响应器: ```python from nonebot import on_command matcher = on_command("help", aliases={"帮助"}) ``` 此外,`command` 响应规则默认允许消息命令与参数间不加空格,如果需要严格匹配命令与参数间的空白符,可以使用 `command` 函数的 `force_whitespace` 参数。`force_whitespace` 参数可以是 bool 类型或者具体的字符串,默认为 `False`。如果为 `True`,则命令与参数间必须有任意个数的空白符;如果为字符串,则命令与参数间必须有且与给定字符串一致的空白符。 ```python rule = command("help", force_whitespace=True) rule = command("help", force_whitespace=" ") ``` 命令解析后的结果可以通过 [`Command`](./dependency.mdx#command)、[`RawCommand`](./dependency.mdx#rawcommand)、[`CommandArg`](./dependency.mdx#commandarg)、[`CommandStart`](./dependency.mdx#commandstart)、[`CommandWhitespace`](./dependency.mdx#commandwhitespace) 依赖注入获取。 ### `shell_command` `shell_command` 响应规则用于匹配类 shell 命令形式的消息。它首先与 [`command`](#command) 响应规则一样进行命令匹配,如果匹配成功,则会进行进一步的参数解析。参数解析采用 `argparse` 标准库进行,在此基础上添加了消息序列 `Message` 支持。 例如,我们可以创建一个匹配 `/cmd` 命令并且带有 `-v` 选项与默认 `-h` 帮助选项的规则: ```python from nonebot.rule import shell_command, ArgumentParser parser = ArgumentParser() parser.add_argument("-v", "--verbose", action="store_true") rule = shell_command("cmd", parser=parser) ``` 更多关于 `argparse` 的使用方法请参考 [argparse 文档](https://docs.python.org/zh-cn/3/library/argparse.html)。我们也可以选择不提供 `parser` 参数,这样 `shell_command` 将不会解析参数,但会提供参数列表 `argv`。 直接使用辅助函数新建一个响应器: ```python from nonebot import on_shell_command from nonebot.rule import ArgumentParser parser = ArgumentParser() parser.add_argument("-v", "--verbose", action="store_true") matcher = on_shell_command("cmd", parser=parser) ``` 参数解析后的结果可以通过 [`ShellCommandArgv`](./dependency.mdx#shellcommandargv)、[`ShellCommandArgs`](./dependency.mdx#shellcommandargs) 依赖注入获取。 ### `regex` `regex` 响应规则用于匹配消息是否与指定正则表达式匹配。 :::tip 提示 正则表达式匹配使用 search 而非 match,如需从头匹配请使用 `r"^xxx"` 模式来确保匹配开头。 ::: 例如,我们可以创建一个匹配消息中包含字母并且忽略大小写的规则: ```python from nonebot.rule import regex rule = regex(r"[a-z]+", flags=re.IGNORECASE) ``` 也可以直接使用辅助函数新建一个响应器: ```python from nonebot import on_regex matcher = on_regex(r"[a-z]+", flags=re.IGNORECASE) ``` 正则匹配后的结果可以通过 [`RegexStr`](./dependency.mdx#regexstr)、[`RegexGroup`](./dependency.mdx#regexgroup)、[`RegexDict`](./dependency.mdx#regexdict) 依赖注入获取。 ### `to_me` `to_me` 响应规则用于匹配事件是否与机器人相关。 例如: ```python from nonebot.rule import to_me rule = to_me() ``` ### `is_type` `is_type` 响应规则用于匹配事件类型是否为指定类型(或者一系列类型)。 例如,我们可以创建一个匹配 OneBot v11 私聊和群聊消息事件的规则: ```python from nonebot.rule import is_type from nonebot.adapters.onebot.v11 import PrivateMessageEvent, GroupMessageEvent rule = is_type(PrivateMessageEvent, GroupMessageEvent) ``` ## 响应器组 为了更方便的管理一系列功能相近的响应器,NoneBot 提供了两种响应器组,它们可以帮助我们进行响应器的统一管理。 ### `CommandGroup` `CommandGroup` 可以用于管理一系列具有相同前置命令的子命令响应器。 例如,我们创建 `/cmd`、`/cmd.sub`、`/cmd.help` 三个命令,他们具有相同的优先级: ```python from nonebot import CommandGroup group = CommandGroup("cmd", priority=10) cmd = group.command(tuple()) sub_cmd = group.command("sub") help_cmd = group.command("help") ``` 命令别名 aliases 默认不会添加 `CommandGroup` 设定的前缀,如果需要为 aliases 添加前缀,可以添加 `prefix_aliases=True` 参数: ```python from nonebot import CommandGroup group = CommandGroup("cmd", prefix_aliases=True) cmd = group.command(tuple()) help_cmd = group.command("help", aliases={"帮助"}) ``` 这样就能成功匹配 `/cmd`、`/cmd.help`、`/cmd.帮助` 命令。如果未设置,将默认匹配 `/cmd`、`/cmd.help`、`/帮助` 命令。 ### `MatcherGroup` `MatcherGroup` 可以用于管理一系列具有相同属性的响应器。 例如,我们创建一个具有相同响应规则的响应器组: ```python from nonebot.rule import to_me from nonebot import MatcherGroup group = MatcherGroup(rule=to_me()) matcher1 = group.on_message() matcher2 = group.on_message() ``` ## 第三方响应规则 ### Alconna [`nonebot-plugin-alconna`](https://github.com/nonebot/plugin-alconna) 是一类提供了拓展响应规则的插件。 该插件使用 [Alconna](https://github.com/ArcletProject/Alconna) 作为命令解析器, 是一个简单、灵活、高效的命令参数解析器, 并且不局限于解析命令式字符串。 该插件提供了一类新的事件响应器辅助函数 `on_alconna`,以及 `AlconnaResult` 等依赖注入函数。 基于 `Alconna` 的特性,该插件同时提供了一系列便捷的消息段标注。 标注可用于在 `Alconna` 中匹配消息中除 text 外的其他消息段,也可用于快速创建各适配器下的消息段。所有标注位于 `nonebot_plugin_alconna.adapters` 中。 该插件同时通过提供 `UniMessage` (通用消息模型) 实现了**跨平台接收和发送消息**的功能。 详情请阅读最佳实践中的 [命令解析拓展](../best-practice/alconna/README.mdx) 章节。 ================================================ FILE: website/docs/advanced/plugin-info.md ================================================ --- sidebar_position: 2 description: 填写与获取插件相关的信息 options: menu: - category: advanced weight: 30 --- # 插件信息 NoneBot 是一个插件化的框架,可以通过加载插件来扩展功能。同时,我们也可以通过 NoneBot 的插件系统来获取相关信息,例如插件的名称、使用方法,用于收集帮助信息等。下面我们将介绍如何为插件添加元数据,以及如何获取插件信息。 ## 插件元数据 在 NoneBot 中,插件 [`Plugin`](../api/plugin/model.md#Plugin) 对象中存储了插件系统所需要的一系列信息。包括插件的索引名称、插件模块、插件中的事件响应器、插件父子关系等。通常,只有插件开发者才需要关心这些信息,而插件使用者或者机器人用户想要看到的是插件使用方法等帮助信息。因此,我们可以为插件添加插件元数据 `PluginMetadata`,它允许插件开发者为插件添加一些额外的信息。这些信息编写于插件模块的顶层,可以直接通过源码查看,或者通过 NoneBot 插件系统获取收集到的信息,通过其他方式发送给机器人用户等。 现在,假设我们有一个插件 `example`, 它的模块结构如下: ```tree {4-6} title=Project 📦 awesome-bot ├── 📂 awesome_bot │ └── 📂 plugins | └── 📂 example | ├── 📜 __init__.py | └── 📜 config.py ├── 📜 pyproject.toml └── 📜 README.md ``` 我们需要在插件顶层模块 `example/__init__.py` 中添加插件元数据,如下所示: ```python {1,5-12} title=example/__init__.py from nonebot.plugin import PluginMetadata from .config import Config __plugin_meta__ = PluginMetadata( name="示例插件", description="这是一个示例插件", usage="没什么用", type="application", config=Config, extra={}, ) ``` 我们可以看到,插件元数据 `PluginMetadata` 有三个基本属性:插件名称、插件描述、插件使用方法。除此之外,还有几个可选的属性(具体填写见[发布插件](../developer/plugin-publishing.mdx#插件元数据)章节): - `type`:插件类别,发布插件必填。当前有效类别有:`library`(为其他插件编写提供功能),`application`(向机器人用户提供功能); - `homepage`:插件项目主页,发布插件必填; - `config`:插件的[配置类](../appendices/config.mdx#插件配置),发布插件时如有配置类则必须填写; - `supported_adapters`:支持的适配器模块名集合,若插件只使用了 NoneBot 基本抽象,应显式填写 `None`; - `extra`:一个字典,可以用于存储任意信息。其他插件可以通过约定 `extra` 字典的键名来达成收集某些特殊信息的目的。 请注意,这里的**插件名称**是供使用者或机器人用户查看的人类可读名称,与插件索引名称无关。**插件索引名称(插件模块名称)**仅用于 NoneBot 插件系统**内部索引**。 ## 获取插件信息 NoneBot 提供了多种获取插件对象的方法,例如获取当前所有已导入的插件: ```python import nonebot plugins: set[Plugin] = nonebot.get_loaded_plugins() ``` 也可以通过插件索引名称获取插件对象: ```python import nonebot plugin: Plugin | None = nonebot.get_plugin("example") ``` 或者通过模块路径获取插件对象: ```python import nonebot plugin: Plugin | None = nonebot.get_plugin_by_module_name("awesome_bot.plugins.example") ``` 如果需要获取所有当前声明的插件名称(可能还未加载),可以使用 `get_available_plugin_names` 函数: ```python import nonebot plugin_names: set[str] = nonebot.get_available_plugin_names() ``` 插件对象 `Plugin` 中包含了多个属性: - `name`:插件索引名称 - `module`:插件模块 - `module_name`:插件模块路径 - `manager`:插件管理器 - `matcher`:插件中定义的事件响应器 - `parent_plugin`:插件的父插件 - `sub_plugins`:插件的子插件集合 - `metadata`:插件元数据 通过这些属性以及插件元数据,我们就可以收集所需要的插件信息了。 ================================================ FILE: website/docs/advanced/plugin-nesting.md ================================================ --- sidebar_position: 3 description: 编写与加载嵌套插件 options: menu: - category: advanced weight: 40 --- # 嵌套插件 NoneBot 支持嵌套插件,即一个插件可以包含其他插件。通过这种方式,我们可以将一个大型插件拆分成多个功能子插件,使得插件更加清晰、易于维护。我们可以直接在插件中使用 NoneBot 加载插件的方法来加载子插件。 ## 创建嵌套插件 我们可以在使用 `nb-cli` 命令[创建插件](../tutorial/create-plugin.md#创建插件)时,选择直接通过模板创建一个嵌套插件: ```bash $ nb plugin create [?] 插件名称: parent [?] 使用嵌套插件? (y/N) Y [?] 输出目录: awesome_bot/plugins ``` 或者使用 `nb plugin create --sub-plugin` 选项直接创建一个嵌套插件。 ## 已有插件 如果你已经有一个插件,想要在其中嵌套加载子插件,可以在插件的 `__init__.py` 中添加如下代码: ```python title=parent/__init__.py import nonebot from pathlib import Path sub_plugins = nonebot.load_plugins( str(Path(__file__).parent.joinpath("plugins").resolve()) ) ``` 这样,`parent` 插件就会加载 `parent/plugins` 目录下的所有插件。NoneBot 会正确识别这些插件的父子关系,你可以在 `parent` 的插件信息中看到这些子插件的信息,也可以在子插件信息中看到它们的父插件信息。 ================================================ FILE: website/docs/advanced/requiring.md ================================================ --- sidebar_position: 4 description: 使用其他插件提供的功能 options: menu: - category: advanced weight: 50 --- # 跨插件访问 NoneBot 插件化系统的设计使得插件之间可以功能独立、各司其职,我们可以更好地维护和扩展插件。但是,有时候我们可能需要在不同插件之间调用功能。NoneBot 生态中就有一类插件,它们专为其他插件提供功能支持,如:[定时任务插件](../best-practice/scheduler.md)、[数据存储插件](../best-practice/data-storing.md)等。这时候我们就需要在插件之间进行跨插件访问。 ## 插件跟踪 由于 NoneBot 插件系统通过 [Import Hooks](https://docs.python.org/3/reference/import.html#import-hooks) 的方式实现插件加载与跟踪管理,因此我们**不能**在 NoneBot 跟踪插件前进行模块 import,这会导致插件加载失败。即,我们不能在使用 NoneBot 提供的加载插件方法前,直接使用 `import` 语句导入插件。 对于在项目目录下的插件,我们通常直接使用 `load_from_toml` 等方法一次性加载所有插件。由于这些插件已经被声明,即便插件导入顺序不同,NoneBot 也能正确跟踪插件。此时,我们不需要对跨插件访问进行特殊处理。但当我们使用了外部插件,如果没有事先声明或加载插件,NoneBot 并不会将其当作插件进行跟踪,可能会出现意料之外的错误出现。 简单来说,我们必须在 `import` 外部插件之前,确保依赖的外部插件已经被声明或加载。 ## 插件依赖声明 NoneBot 提供了一种方法来确保我们依赖的插件已经被正确加载,即使用 `require` 函数。通过 `require` 函数,我们可以在当前插件中声明依赖的插件,NoneBot 会在加载当前插件时,检查依赖的插件是否已经被加载,如果没有,会尝试优先加载依赖的插件。 假设我们有一个插件 `a` 依赖于插件 `b`,我们可以在插件 `a` 中使用 `require` 函数声明其依赖于插件 `b`: ```python {3} title=a/__init__.py from nonebot import require require("b") from b import some_function ``` 其中,`require` 函数的参数为插件索引名称或者外部插件的模块名称。在完成依赖声明后,我们可以在插件 `a` 中直接导入插件 `b` 所提供的功能。 ================================================ FILE: website/docs/advanced/routing.md ================================================ --- sidebar_position: 9 description: 添加服务端路由规则 options: menu: - category: advanced weight: 100 --- # 添加路由 在[驱动器](./driver.md)一节中,我们了解了驱动器的两种类型。既然驱动器可以作为服务端运行,那么我们就可以向驱动器添加路由规则,从而实现自定义的 API 接口等功能。在添加路由规则时,我们需要注意驱动器的类型,详情可以参考[选择驱动器](./driver.md#配置驱动器)。 NoneBot 中,我们可以通过两种途径向 ASGI 驱动器添加路由规则: 1. 通过 NoneBot 的兼容层建立路由规则。 2. 直接向 ASGI 应用添加路由规则。 这两种途径各有优劣,前者可以在各种服务端型驱动器下运行,但并不能直接使用 ASGI 应用框架提供的特性与功能;后者直接使用 ASGI 应用,更自由、功能完整,但只能在特定类型驱动器下运行。 在向驱动器添加路由规则时,我们需要注意驱动器是否为服务端类型,我们可以通过以下方式判断: ```python from nonebot import get_driver from nonebot.drivers import ASGIMixin # highlight-next-line can_use = isinstance(get_driver(), ASGIMixin) ``` ## 通过兼容层添加路由 NoneBot 兼容层定义了两个数据类 `HTTPServerSetup` 和 `WebSocketServerSetup`,分别用于定义 HTTP 服务端和 WebSocket 服务端的路由规则。 ### HTTP 路由 `HTTPServerSetup` 具有四个属性: - `path`:路由路径,不支持特殊占位表达式。类型为 `URL`。 - `method`:请求方法。类型为 `str`。 - `name`:路由名称,不可重复。类型为 `str`。 - `handle_func`:路由处理函数。类型为 `Callable[[Request], Awaitable[Response]]`。 例如,我们添加一个 `/hello` 的路由,当请求方法为 `GET` 时,返回 `200 OK` 以及返回体信息: ```python from nonebot import get_driver from nonebot.drivers import URL, Request, Response, ASGIMixin, HTTPServerSetup async def hello(request: Request) -> Response: return Response(200, content="Hello, world!") if isinstance((driver := get_driver()), ASGIMixin): driver.setup_http_server( HTTPServerSetup( path=URL("/hello"), method="GET", name="hello", handle_func=hello, ) ) ``` 对于 `Request` 和 `Response` 的详细信息,可以参考 [API 文档](../api/drivers/index.md)。 ### WebSocket 路由 `WebSocketServerSetup` 具有三个属性: - `path`:路由路径,不支持特殊占位表达式。类型为 `URL`。 - `name`:路由名称,不可重复。类型为 `str`。 - `handle_func`:路由处理函数。类型为 `Callable[[WebSocket], Awaitable[Any]]`。 例如,我们添加一个 `/ws` 的路由,发送所有接收到的数据: ```python from nonebot import get_driver from nonebot.drivers import URL, ASGIMixin, WebSocket, WebSocketServerSetup async def ws_handler(ws: WebSocket): await ws.accept() try: while True: data = await ws.receive() await ws.send(data) except WebSocketClosed as e: # handle closed ... finally: with contextlib.suppress(Exception): await websocket.close() # do some cleanup if isinstance((driver := get_driver()), ASGIMixin): driver.setup_websocket_server( WebSocketServerSetup( path=URL("/ws"), name="ws", handle_func=ws_handler, ) ) ``` 对于 `WebSocket` 的详细信息,可以参考 [API 文档](../api/drivers/index.md)。 ## 使用 ASGI 应用添加路由 ### 获取 ASGI 应用 NoneBot 服务端类型的驱动器具有两个属性 `server_app` 和 `asgi`,分别对应驱动框架应用和 ASGI 应用。通常情况下,这两个应用是同一个对象。我们可以通过 `get_app()` 方法快速获取: ```python import nonebot app = nonebot.get_app() asgi = nonebot.get_asgi() ``` ### 添加路由规则 在获取到了 ASGI 应用后,我们就可以直接使用 ASGI 应用框架提供的功能来添加路由规则了。这里我们以 [FastAPI](./driver.md#fastapi默认) 为例,演示如何添加路由规则。 在下面的代码中,我们添加了一个 `GET` 类型的 `/api` 路由,具体方法参考 [FastAPI 文档](https://fastapi.tiangolo.com/)。 ```python import nonebot from fastapi import FastAPI app: FastAPI = nonebot.get_app() @app.get("/api") async def custom_api(): return {"message": "Hello, world!"} ``` ================================================ FILE: website/docs/advanced/runtime-hook.md ================================================ --- sidebar_position: 8 description: 在特定的生命周期中执行代码 options: menu: - category: advanced weight: 90 --- # 钩子函数 > [钩子编程](https://zh.wikipedia.org/wiki/%E9%92%A9%E5%AD%90%E7%BC%96%E7%A8%8B)(hooking),也称作“挂钩”,是计算机程序设计术语,指通过拦截软件模块间的函数调用、消息传递、事件传递来修改或扩展操作系统、应用程序或其他软件组件的行为的各种技术。处理被拦截的函数调用、事件、消息的代码,被称为钩子(hook)。 在 NoneBot 中有一系列预定义的钩子函数,可以分为两类:**全局钩子函数**和**事件处理钩子函数**,这些钩子函数可以用装饰器的形式来使用。 ## 全局钩子函数 全局钩子函数是指 NoneBot 针对其本身运行过程的钩子函数。 这些钩子函数是由驱动器来运行的,故需要先[获得全局驱动器](./driver.md#获取驱动器)。 ### 启动准备 这个钩子函数会在 NoneBot 启动时运行。很多时候,我们并不希望在模块被导入时就执行一些耗时操作,如:连接数据库,这时候我们可以在这个钩子函数中进行这些操作。 ```python from nonebot import get_driver driver = get_driver() @driver.on_startup async def do_something(): pass ``` ### 终止处理 这个钩子函数会在 NoneBot 终止时运行。我们可以在这个钩子函数中进行一些清理工作,如:关闭数据库连接。 ```python from nonebot import get_driver driver = get_driver() @driver.on_shutdown async def do_something(): pass ``` ### Bot 连接处理 这个钩子函数会在任何协议适配器连接 `Bot` 对象至 NoneBot 时运行。支持依赖注入,可以直接注入 `Bot` 对象。 ```python from nonebot import get_driver driver = get_driver() @driver.on_bot_connect async def do_something(bot: Bot): pass ``` ### Bot 断开处理 这个钩子函数会在 `Bot` 断开与 NoneBot 的连接时运行。支持依赖注入,可以直接注入 `Bot` 对象。 ```python from nonebot import get_driver driver = get_driver() @driver.on_bot_disconnect async def do_something(bot: Bot): pass ``` ## 事件处理钩子函数 这些钩子函数指的是影响 NoneBot 进行**事件处理**的函数, 这些函数可以跟普通的事件处理函数一样接受相应的参数。 ### 事件预处理 这个钩子函数会在 NoneBot 接收到新的事件时运行。支持依赖注入,可以注入 `Bot` 对象、事件、会话状态。在这个钩子函数内抛出 `nonebot.exception.IgnoredException` 会使 NoneBot 忽略该事件。 ```python from nonebot.exception import IgnoredException from nonebot.message import event_preprocessor @event_preprocessor async def do_something(event: Event): if not event.is_tome(): raise IgnoredException("some reason") ``` ### 事件后处理 这个钩子函数会在 NoneBot 处理事件完成后运行。支持依赖注入,可以注入 `Bot` 对象、事件、会话状态。 ```python from nonebot.message import event_postprocessor @event_postprocessor async def do_something(event: Event): pass ``` ### 运行预处理 这个钩子函数会在 NoneBot 运行事件响应器前运行。支持依赖注入,可以注入 `Bot` 对象、事件、事件响应器、会话状态。在这个钩子函数内抛出 `nonebot.exception.IgnoredException` 也会使 NoneBot 忽略本次运行。 ```python from nonebot.message import run_preprocessor from nonebot.exception import IgnoredException @run_preprocessor async def do_something(event: Event, matcher: Matcher): if not event.is_tome(): raise IgnoredException("some reason") ``` ### 运行后处理 这个钩子函数会在 NoneBot 运行事件响应器后运行。支持依赖注入,可以注入 `Bot` 对象、事件、事件响应器、会话状态、运行中产生的异常。 ```python from nonebot.message import run_postprocessor @run_postprocessor async def do_something(event: Event, matcher: Matcher, exception: Optional[Exception]): pass ``` ### 平台接口调用钩子 这个钩子函数会在 `Bot` 对象调用平台接口时运行。在这个钩子函数中,我们可以通过引起 `MockApiException` 异常来阻止 `Bot` 对象调用平台接口并返回指定的结果。 ```python from nonebot.adapters import Bot from nonebot.exception import MockApiException @Bot.on_calling_api async def handle_api_call(bot: Bot, api: str, data: Dict[str, Any]): if api == "send_msg": raise MockApiException(result={"message_id": 123}) ``` ### 平台接口调用后钩子 这个钩子函数会在 `Bot` 对象调用平台接口后运行。在这个钩子函数中,我们可以通过引起 `MockApiException` 异常来忽略平台接口返回的结果并返回指定的结果。 ```python from nonebot.adapters import Bot from nonebot.exception import MockApiException @Bot.on_called_api async def handle_api_result( bot: Bot, exception: Optional[Exception], api: str, data: Dict[str, Any], result: Any ): if not exception and api == "send_msg": raise MockApiException(result={**result, "message_id": 123}) ``` ================================================ FILE: website/docs/advanced/session-updating.md ================================================ --- sidebar_position: 7 description: 控制会话响应对象 options: menu: - category: advanced weight: 80 --- # 会话更新 在 NoneBot 中,在某个事件响应器对事件响应后,即是进入了会话状态,会话状态会持续到整个事件响应流程结束。会话过程中,机器人可以与用户进行多次交互。每次需要等待用户事件时,NoneBot 将会复制一个新的临时事件响应器,并更新该事件响应器使其响应当前会话主体的消息,这个过程称为会话更新。 会话更新分为两部分:**更新[事件响应器类型](./matcher.md#事件响应器类型)**和**更新[事件触发权限](./matcher.md#事件触发权限)**。 ## 更新事件响应器类型 通常情况下,与机器人用户进行的会话都是通过消息事件进行的,因此会话更新后的默认响应事件类型为 `message`。如果希望接收一个特定类型的消息,比如 `notice` 等,我们需要自定义响应事件类型更新函数。响应事件类型更新函数是一个 `Dependent`,可以使用依赖注入。 ```python {3-5} foo = on_message() @foo.type_updater async def _() -> str: return "notice" ``` 在注册了上述响应事件类型更新函数后,当我们需要等待用户事件时,将只会响应 `notice` 类型的事件。如果希望在会话过程中的不同阶段响应不同类型的事件,我们就需要使用更复杂的逻辑来更新响应事件类型(如:根据会话状态),这里将不再展示。 ## 更新事件触发权限 会话通常是由机器人与用户进行的一对一交互,因此会话更新后的默认触发权限为当前事件的会话 ID。这个会话 ID 由协议适配器生成,通常由用户 ID 和群 ID 等组成。如果希望实现更复杂的会话功能(如:多用户同时参与的会话),我们需要自定义触发权限更新函数。触发权限更新函数是一个 `Dependent`,可以使用依赖注入。 ```python {5-7} from nonebot.permission import User foo = on_message() @foo.permission_updater async def _(event: Event, matcher: Matcher) -> Permission: return Permission(User.from_event(event, perm=matcher.permission)) ``` 上述权限更新函数是默认的权限更新函数,它将会话的触发权限更新为当前事件的会话 ID。如果我们希望响应多个用户的消息,我们可以如下修改: ```python {5-7} from nonebot.permission import USER foo = on_message() @foo.permission_updater async def _(matcher: Matcher) -> Permission: return USER("session1", "session2", perm=matcher.permission) ``` 请注意,此处为全大写字母的 `USER` 权限,它可以匹配多个会话 ID。通过这种方式,我们可以实现多用户同时参与的会话。 我们已经了解了如何控制会话的更新,相信你已经能够实现更复杂的会话功能了,例如多人小游戏等等。欢迎将你的作品分享到[插件商店](/store/plugins)。 ================================================ FILE: website/docs/api/.gitkeep ================================================ ================================================ FILE: website/docs/api/adapters/_category_.json ================================================ { "position": 15 } ================================================ FILE: website/docs/api/dependencies/_category_.json ================================================ { "position": 13 } ================================================ FILE: website/docs/api/drivers/_category_.json ================================================ { "position": 14 } ================================================ FILE: website/docs/api/plugin/_category_.json ================================================ { "position": 12 } ================================================ FILE: website/docs/appendices/api-calling.mdx ================================================ --- sidebar_position: 4 description: 使用平台接口,完成更多功能 options: menu: - category: appendices weight: 50 --- # 使用平台接口 import Messenger from "@/components/Messenger"; 在 NoneBot 中,除了使用事件响应器操作发送文本消息外,我们还可以直接通过使用协议适配器提供的方法来使用平台特定的接口,完成发送特殊消息、获取信息等其他平台提供的功能。同时,在部分无法使用事件响应器的情况中,例如[定时任务](../best-practice/scheduler.md),我们也可以使用平台接口来完成需要的功能。 ## 发送平台特殊消息 在之前的章节中,我们介绍了如何向用户发送文本消息以及[如何处理平台消息](../tutorial/message.md),现在我们来向用户发送平台特殊消息。 :::caution 注意 在以下的示例中,我们将使用 `Console` 协议适配器来演示如何发送平台消息。在实际使用中,你需要确保你使用的**消息序列类型**与你所要发送的**平台类型**一致。 ::: ```python {4,7-17} title=weather/__init__.py import inspect from nonebot.adapters.console import MessageSegment @weather.got("location", prompt=MessageSegment.emoji("question") + "请输入地名") async def got_location(location: str = ArgPlainText()): result = await weather.send( MessageSegment.markdown( inspect.cleandoc( f""" # {location} - 今天 ⛅ 多云 20℃~24℃ """ ) ) ) ``` 在上面的示例中,我们使用了 `Console` 协议适配器提供的 `MessageSegment` 类来发送平台特定的消息 `emoji` 和 `markdown`。这两种消息可以显示在终端中,但是无法在其他平台上使用。在事件响应器操作中,我们可以使用 `str`、消息序列、消息段、消息模板四种类型来发送消息,但其中只有 `str` 和[纯文本形式的消息模板类型](../tutorial/message.md#使用消息模板)消息可以在所有平台上使用。 `send` 事件响应器操作实际上是由协议适配器通过调用平台 API 来实现的,通常会将 API 调用的结果作为返回值返回。 ## 调用平台 API 在 NoneBot 中,我们可以通过 `Bot` 对象来调用协议适配器支持的平台 API,来完成更多的功能。 ### 获取 Bot 在调用平台 API 之前,我们首先要获得 Bot 对象。有两种方式可以获得 Bot 对象。 在事件处理流程的上下文中,我们可以直接使用依赖注入 Bot 来获取: ```python {1,4} title=weather/__init__.py from nonebot.adapters import Bot @weather.got("location", prompt="请输入地名") async def got_location(bot: Bot, location: str = ArgPlainText()): ... ``` 依赖注入会确保你获得的 Bot 对象与类型注解的 Bot 类型一致。也就是说,如果你使用的是 Bot 基类,将会允许任何平台的 Bot 对象;如果你使用的是平台特定的 Bot 类型,将会只允许该平台的 Bot 对象,其他类型的 Bot 将会跳过这个事件处理函数。更多详情请参考[事件处理重载](./overload.md)。 在其他情况下,我们可以通过 NoneBot 提供的方法来获取 Bot 对象,这些方法将会在[使用适配器](../advanced/adapter.md#获取-bot-对象)中详细介绍: ```python {4,6} from nonebot import get_bot # 获取当前所有 Bot 中的第一个 bot = get_bot() # 获取指定 ID 的 Bot bot = get_bot("bot_id") ``` ### 调用 API 在获得 Bot 对象后,我们可以通过 Bot 的实例方法来调用平台 API: ```python {2,5} # 通过 bot.api_name(**kwargs) 的方法调用 API result = await bot.get_user_info(user_id=12345678) # 通过 bot.call_api(api_name, **kwargs) 的方法调用 API result = await bot.call_api("get_user_info", user_id=12345678) ``` :::caution 注意 实际可以使用的 API 以及参数取决于平台提供的接口以及协议适配器的实现,请参考协议适配器以及平台文档。 ::: 在了解了如何调用 API 后,我们可以来改进 `weather` 插件,使得消息发送后,调用 `Console` 接口响铃提醒机器人用户: ```python {4,18} title=weather/__init__.py from nonebot.adapters.console import Bot, MessageSegment @weather.got("location", prompt=MessageSegment.emoji("question") + "请输入地名") async def got_location(bot: Bot, location: str = ArgPlainText()): await weather.send( MessageSegment.markdown( inspect.cleandoc( f""" # {location} - 今天 ⛅ 多云 20℃~24℃ """ ) ) ) await bot.bell() ``` ================================================ FILE: website/docs/appendices/config.mdx ================================================ --- sidebar_position: 0 description: 读取用户配置来控制插件行为 options: menu: - category: appendices weight: 10 --- # 配置 import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; 配置是项目中非常重要的一部分,为了方便我们控制机器人的行为,NoneBot 提供了一套配置系统。下面我们将会补充[指南](../quick-start.mdx)中的天气插件,使其能够读取用户配置。在这之前,我们需要先了解一下配置系统,如果你已经了解了 NoneBot 中的配置方法,可以跳转到[编写插件配置](#插件配置)。 NoneBot 使用 [`pydantic`](https://docs.pydantic.dev/) 以及 [`python-dotenv`](https://saurabh-kumar.com/python-dotenv/) 来读取 dotenv 配置文件以及环境变量,从而控制机器人行为。配置文件需要符合 dotenv 格式,复杂数据类型需使用 JSON 格式或 [pydantic 支持格式](https://docs.pydantic.dev/usage/types/)填写。 NoneBot 内置的配置项列表及含义可以在[内置配置项](#内置配置项)中查看。 :::caution 注意 NoneBot 自 2.2.0 起兼容了 Pydantic v1 与 v2 版本,以下文档中 Pydantic 相关示例均采用 v2 版本用法。 如果在使用商店或其他第三方插件的过程中遇到 Pydantic 相关警告或报错,例如: ```python pydantic_core._pydantic_core.ValidationError: 1 validation error for Config Input should be a valid dictionary or instance of Config [type=model_type, input_value=Config(...), input_type=Config] ``` 请考虑降级 Pydantic 至 v1 版本: ```bash pip install --force-reinstall 'pydantic~=1.10' ``` ::: ## 配置项的加载 在 NoneBot 中,我们可以把配置途径分为 **直接传入**、**系统环境变量**、**dotenv 配置文件** 三种,其加载优先级依次由高到低。 ### 直接传入 在 NoneBot 初始化的过程中,可以通过 `nonebot.init()` 传入任意合法的 Python 变量,也可以在初始化完成后直接赋值。 通常,在初始化前的传参会在机器人的入口文件(如 `bot.py`)中进行,而初始化后的赋值可以在任何地方进行。 ```python {4,8,9} title=bot.py import nonebot # 初始化时 nonebot.init(custom_config1="config on init") # 初始化后 config = nonebot.get_driver().config config.custom_config1 = "changed after init" config.custom_config2 = "new config after init" ``` ### 系统环境变量 在 dotenv 配置文件中定义的配置项,也会在环境变量中进行寻找。如果在环境变量中发现同名配置项(大小写不敏感),将会覆盖 dotenv 中所填值。 例如,在 dotenv 配置文件中存在配置项 `custom_config`: ```dotenv CUSTOM_CONFIG=config in dotenv ``` 同时,设置环境变量: ```bash # windows cmd set CUSTOM_CONFIG 'config in environment variables' # windows powershell $Env:CUSTOM_CONFIG='config in environment variables' # linux/macOS export CUSTOM_CONFIG='config in environment variables' ``` 那最终 NoneBot 所读取的内容为环境变量中的内容,即 `config in environment variables`。 :::caution 注意 如果一个环境变量既不是 NoneBot 的[**内置配置项**](#内置配置项),也不是任何插件所定义的[**插件配置**](#插件配置),那么 NoneBot 不会自发读取该环境变量,需要在 dotenv 配置文件中先行声明。 ::: ### dotenv 配置文件 dotenv 是一种便捷的跨平台配置通用模式,也是我们推荐的配置方式。 NoneBot 在启动时将会从系统环境变量或者 `.env` 文件中寻找配置项 `ENVIRONMENT` (大小写不敏感),默认值为 `prod`。这将决定 NoneBot 后续进一步加载环境配置的文件路径 `.env.{ENVIRONMENT}`。 #### 配置项解析 dotenv 文件中的配置值使用 JSON 进行解析。如果配置项值无法被解析,将作为**字符串**处理。例如: ```dotenv STRING_CONFIG=some string LIST_CONFIG=[1, 2, 3] DICT_CONFIG={"key": "value"} MULTILINE_CONFIG=' [ { "item_key": "item_value" } ] ' EMPTY_CONFIG= NULL_CONFIG ``` 将被解析为: ```python dotenv_config = { "string_config": "some string", "list_config": [1, 2, 3], "dict_config": {"key": "value"}, "multiline_config": [{"item_key": "item_value"}], "empty_config": "", "null_config": None } ``` 特别的,NoneBot 支持使用 `env_nested_delimiter` 配置嵌套字典,在层与层之间使用 `__` 分隔即可: ```dotenv DICT={"k1": "v1", "k2": null} DICT__K2=v2 DICT__K3=v3 DICT__INNER__K4=v4 ``` 将被解析为: ```python dotenv_config = { "dict": { "k1": "v1", "k2": "v2", "k3": "v3", "inner": { "k4": "v4" } } } ``` #### .env 文件 `.env` 文件是基础配置文件,该文件中的配置项在不同环境下都会被加载,但会被 `.env.{ENVIRONMENT}` 文件中的配置所**覆盖**。 我们可以在 `.env` 文件中写入当前的环境信息: ```dotenv ENVIRONMENT=dev COMMON_CONFIG=common config # 这个配置项在任何环境中都会被加载 ``` 这样,我们在启动 NoneBot 时就会从 `.env.dev` 文件中加载剩余配置项。 :::tip 提示 在生产环境中,可以通过设置环境变量 `ENVIRONMENT=prod` 来确保 NoneBot 读取正确的环境配置。 ::: #### .env.\{ENVIRONMENT\} 文件 `.env.{ENVIRONMENT}` 文件类似于预设,可以让我们在多套不同的配置方案中灵活切换,默认 NoneBot 会读取 `.env.prod` 配置。如果你使用了 `nb-cli` 创建 `simple` 项目,那么将含有两套预设配置:`.env.dev` 和 `.env.prod`。 在 NoneBot 初始化时,可以指定加载某个环境配置文件: ```python nonebot.init(_env_file=".env.dev") ``` 这将忽略在 `.env` 文件或环境变量中指定的 `ENVIRONMENT` 配置项。 ## 读取全局配置项 NoneBot 的全局配置对象可以通过 `driver` 获取,如: ```python import nonebot config = nonebot.get_driver().config ``` 如果我们需要获取某个配置项,可以直接通过 `config` 对象的属性访问: ```python superusers = config.superusers ``` 如果配置项不存在,将会抛出异常。 ## 插件配置 在一个涉及大量配置项的项目中,通过直接读取全局配置项的方式显然并不高效。同时,由于额外的全局配置项没有预先定义,开发时编辑器将无法提示字段与类型,并且运行时没有对配置项直接进行合法性检查。那么就需要一种方式来规范定义插件配置项。 在 NoneBot 中,我们使用强大高效的 `pydantic` 来定义配置模型,这个模型可以被用于配置的读取和类型检查等。例如在 `weather` 插件目录中新建 `config.py` 来定义一个模型: ```python title=weather/config.py from pydantic import BaseModel, field_validator class Config(BaseModel): weather_api_key: str weather_command_priority: int = 10 weather_plugin_enabled: bool = True @field_validator("weather_command_priority") @classmethod def check_priority(cls, v: int) -> int: if v >= 1: return v raise ValueError("weather command priority must greater than 1") ``` 在 `config.py` 中,我们定义了一个 `Config` 类,它继承自 `pydantic.BaseModel`,并定义了一些配置项。在 `Config` 类中,我们还定义了一个 `check_priority` 方法,它用于检查 `weather_command_priority` 配置项的合法性。更多关于 `pydantic` 的编写方式,可以参考 [pydantic 官方文档](https://docs.pydantic.dev/)。 在定义好配置模型后,我们可以在插件加载时通过配置模型获取插件配置: ```python {5,11} title=weather/__init__.py from nonebot import get_plugin_config from .config import Config plugin_config = get_plugin_config(Config) weather = on_command( "天气", rule=to_me(), aliases={"weather", "查天气"}, priority=plugin_config.weather_command_priority, block=True, ) ``` 然后,我们便可以从 `plugin_config` 中读取配置了,例如 `plugin_config.weather_api_key`。 这种方式可以简洁、高效地读取配置项,同时也可以设置默认值或者在运行时对配置项进行合法性检查,防止由于配置项导致的插件出错等情况出现。 :::tip 可配置的事件响应优先级 发布插件应该为自身的事件响应器提供可配置的优先级,以便插件使用者可以自定义多个插件间的响应顺序。 ::: :::tip 插件配置获取逻辑 无论是否在 dotenv 文件中声明了插件配置项,使用 `get_plugin_config` 获取插件配置模型中定义的配置项时都遵循[**配置项的加载**](#配置项的加载)一节中的优先级顺序进行读取。 ::: ### 避免插件配置名称冲突 由于插件配置项是从全局配置和环境变量中读取的,通常我们需要在配置项名称前面添加前缀名,以防止配置项冲突。例如在上方的示例中,我们就添加了配置项前缀 `weather_`。但是这样会导致使用配置项时变量名过长,此时我们可以使用 `pydantic` 的 `alias` 或者通过配置 scope 来简化配置项名称。这里我们以 scope 配置为例: ```python title=weather/config.py from pydantic import BaseModel class ScopedConfig(BaseModel): api_key: str command_priority: int = 10 plugin_enabled: bool = True class Config(BaseModel): weather: ScopedConfig ``` ```python title=weather/__init__.py from nonebot import get_plugin_config from .config import Config plugin_config = get_plugin_config(Config).weather ``` 这样我们就可以省略插件配置项名称中的前缀 `weather_` 了。但需要注意的是,如果我们使用了 scope 配置,那么在配置文件中也需要使用 [`env_nested_delimiter` 格式](#配置项解析),例如: ```dotenv WEATHER__API_KEY=123456 WEATHER__COMMAND_PRIORITY=10 ``` ## 内置配置项 配置项 API 文档可以前往 [Config 类](../api/config.md#Config)查看。 ### Driver - **类型**: `str` - **默认值**: `"~fastapi"` NoneBot 运行所使用的驱动器。具体配置方法可以参考[安装驱动器](../tutorial/store.mdx#安装驱动器)和[选择驱动器](../advanced/driver.md)。 ```dotenv DRIVER=~fastapi+~httpx+~websockets ``` ```bash # windows cmd set DRIVER '~fastapi+~httpx+~websockets' # windows powershell $Env:DRIVER='~fastapi+~httpx+~websockets' # linux/macOS export DRIVER='~fastapi+~httpx+~websockets' ``` ```python title=bot.py import nonebot nonebot.init(driver="~fastapi+~httpx+~websockets") ``` ### Host - **类型**: `IPvAnyAddress` - **默认值**: `127.0.0.1` 当 NoneBot 作为服务端时,监听的 IP / 主机名。 ```dotenv HOST=127.0.0.1 ``` ```bash # windows cmd set HOST '127.0.0.1' # windows powershell $Env:HOST='127.0.0.1' # linux/macOS export HOST='127.0.0.1' ``` ```python title=bot.py import nonebot nonebot.init(host="127.0.0.1") ``` ### Port - **类型**: `int` (1 ~ 65535) - **默认值**: `8080` 当 NoneBot 作为服务端时,监听的端口。 ```dotenv PORT=8080 ``` ```bash # windows cmd set PORT '8080' # windows powershell $Env:PORT='8080' # linux/macOS export PORT='8080' ``` ```python title=bot.py import nonebot nonebot.init(port=8080) ``` ### Log Level - **类型**: `int | str` - **默认值**: `INFO` NoneBot 日志输出等级,可以为 `int` 类型等级或等级名称。具体等级对照表参考 [loguru 日志等级](https://loguru.readthedocs.io/en/stable/api/logger.html#levels)。 :::tip 提示 日志等级名称应为大写,如 `INFO`。 ::: ```dotenv LOG_LEVEL=DEBUG ``` ```bash # windows cmd set LOG_LEVEL 'DEBUG' # windows powershell $Env:LOG_LEVEL='DEBUG' # linux/macOS export LOG_LEVEL='DEBUG' ``` ```python title=bot.py import nonebot nonebot.init(log_level="DEBUG") ``` ### API Timeout - **类型**: `float | None` - **默认值**: `30.0` 调用平台接口的超时时间,单位为秒。`None` 表示不设置超时时间。 ```dotenv API_TIMEOUT=10.0 ``` ```bash # windows cmd set API_TIMEOUT '10.0' # windows powershell $Env:API_TIMEOUT='10.0' # linux/macOS export API_TIMEOUT='10.0' ``` ```python title=bot.py import nonebot nonebot.init(api_timeout=10.0) ``` ### SuperUsers - **类型**: `set[str]` - **默认值**: `set()` 机器人超级用户,可以使用权限 [`SUPERUSER`](../api/permission.md#SUPERUSER)。 ```dotenv SUPERUSERS=["123123123"] ``` ```bash # windows cmd set SUPERUSERS '["123123123"]' # windows powershell $Env:SUPERUSERS='["123123123"]' # linux/macOS export SUPERUSERS='["123123123"]' ``` ```python title=bot.py import nonebot nonebot.init(superusers={"123123123"}) ``` ### Nickname - **类型**: `set[str]` - **默认值**: `set()` 机器人昵称,通常协议适配器会根据用户是否 @bot 或者是否以机器人昵称开头来判断是否是向机器人发送的消息。 ```dotenv NICKNAME=["bot"] ``` ```bash # windows cmd set NICKNAME '["bot"]' # windows powershell $Env:NICKNAME='["bot"]' # linux/macOS export NICKNAME='["bot"]' ``` ```python title=bot.py import nonebot nonebot.init(nickname={"bot"}) ``` ### Command Start 和 Command Separator - **类型**: `set[str]` - **默认值**: - Command Start: `{"/"}` - Command Separator: `{"."}` 命令消息的起始符和分隔符。用于 [`command`](../advanced/matcher.md#command) 规则。 ```dotenv COMMAND_START=["/", ""] COMMAND_SEP=[".", " "] ``` ```bash # windows cmd set COMMAND_START '["/", ""]' set COMMAND_SEP '[".", " "]' # windows powershell $Env:COMMAND_START='["/", ""]' $Env:COMMAND_SEP='[".", " "]' # linux/macOS export COMMAND_START='["/", ""]' export COMMAND_SEP='[".", " "]' ``` ```python title=bot.py import nonebot nonebot.init(command_start={"/", ""}, command_sep={".", " "}) ``` ### Session Expire Timeout - **类型**: `timedelta` - **默认值**: `timedelta(minutes=2)` 用户会话超时时间,配置格式参考 [Datetime Types](https://docs.pydantic.dev/latest/api/standard_library_types/#datetimetimedelta)。 ```dotenv SESSION_EXPIRE_TIMEOUT=00:02:00 ``` ```bash # windows cmd set SESSION_EXPIRE_TIMEOUT '00:02:00' # windows powershell $Env:SESSION_EXPIRE_TIMEOUT='00:02:00' # linux/macOS export SESSION_EXPIRE_TIMEOUT='00:02:00' ``` ```python title=bot.py import nonebot nonebot.init(session_expire_timeout=120) ``` ================================================ FILE: website/docs/appendices/log.md ================================================ --- sidebar_position: 6 description: 记录与控制日志 options: menu: - category: appendices weight: 70 --- # 日志 无论是在开发还是在生产环境中,日志都是一个重要的功能,可以帮助我们了解运行状况、排查问题等。虽然我们可以使用 `print` 来将需要的信息输出到控制台,但是这种方式难以控制,而且不利于日志的归档、分析等。NoneBot 使用优秀的 [Loguru](https://loguru.readthedocs.io/) 库来进行日志记录。 ## 记录日志 我们可以从 NoneBot 中导入 `logger` 对象,然后使用 `logger` 对象的方法来记录日志。 ```python from nonebot import logger logger.trace("This is a trace message") logger.debug("This is a debug message") logger.info("This is an info message") logger.success("This is a success message") logger.warning("This is a warning message") logger.error("This is an error message") logger.critical("This is a critical message") ``` 我们仅需一行代码即可记录对应级别的日志。日志可以通过配置 [`LOG_LEVEL` 配置项](./config.mdx#log-level)来过滤输出等级,控制台中仅会输出大于等于 `LOG_LEVEL` 的日志。默认的 `LOG_LEVEL` 为 `INFO`,即只会输出 `INFO`、`SUCCESS`、`WARNING`、`ERROR`、`CRITICAL` 级别的日志。 如果需要记录 `Exception traceback` 日志,可以向 `logger` 添加 `exception` 选项: ```python {4} try: 1 / 0 except ZeroDivisionError: logger.opt(exception=True).error("ZeroDivisionError") ``` 如果需要输出彩色日志,可以向 `logger` 添加 `colors` 选项: ```python logger.opt(colors=True).warning("We got a BIG problem") ``` 更多日志记录方法请参考 [Loguru 文档](https://loguru.readthedocs.io/)。 ## 自定义日志输出 NoneBot 在启动时会添加一个默认的日志处理器,该处理器会将日志输出到**stdout**,并且根据 `LOG_LEVEL` 配置项过滤日志等级。 默认的日志格式为: ```text {time:MM-DD HH:mm:ss} [{level}] {name} | {message} ``` 我们可以从 `nonebot.log` 模块导入以使用 NoneBot 的默认格式和过滤器: ```python from nonebot.log import default_format, default_filter ``` 如果需要自定义日志格式,我们需要移除 NoneBot 默认的日志处理器并添加新的日志处理器。例如,在机器人入口文件中 `nonebot.init` 之前添加以下内容: ```python title=bot.py from nonebot.log import logger_id # 移除 NoneBot 默认的日志处理器 logger.remove(logger_id) # 添加新的日志处理器 logger.add( sys.stdout, level=0, diagnose=True, format="{time:MM-DD HH:mm:ss} [{level}] {name} | {message}", filter=default_filter ) ``` 如果想要输出日志到文件,我们可以使用 `logger.add` 方法添加文件处理器: ```python title=bot.py logger.add("error.log", level="ERROR", format=default_format, rotation="1 week") ``` 更多日志处理器的使用方法请参考 [Loguru 文档](https://loguru.readthedocs.io/)。 ## 重定向 logging 日志 `logging` 是 Python 标准库中的日志模块,NoneBot 提供了一个 logging handler 用于将 `logging` 日志重定向到 `loguru` 处理。 ```python from nonebot.log import LoguruHandler # root logger 添加 LoguruHandler logging.basicConfig(handlers=[LoguruHandler()]) # 或者为其他 logging.Logger 添加 LoguruHandler logger.addHandler(LoguruHandler()) ``` ================================================ FILE: website/docs/appendices/overload.md ================================================ --- sidebar_position: 7 description: 根据事件类型进行不同的处理 options: menu: - category: appendices weight: 80 --- # 事件类型与重载 在之前的示例中,我们已经了解了如何[获取事件信息](../tutorial/event-data.mdx)以及[使用平台接口](./api-calling.mdx)。但是,事件信息通常不仅仅包含消息这一个内容,还有其他平台提供的信息,例如消息发送时间、消息发送者等等。同时,在使用平台接口时,我们需要确保使用的**平台接口**与所要发送的**平台类型**一致,对不同类型的事件需要做出不同的处理。在本章节中,我们将介绍如何获取事件更多的信息以及根据事件类型进行不同的处理。 ## 事件类型 在 NoneBot 中,事件均是 `nonebot.adapters.Event` 基类的子类型,基类对一些必要的属性进行了抽象,子类型则根据不同的平台进行了实现。在[自定义权限](./permission.mdx#自定义权限)一节中,我们就使用了 `Event` 的抽象方法 `get_user_id` 来获取事件发送者 ID,这个方法由协议适配器进行了实现,返回机器人用户对应的平台 ID。更多的基类抽象方法可以在[使用适配器](../advanced/adapter.md#获取事件通用信息)中查看。 既然事件是基类的子类型,我们实际可以获得的信息通常多于基类抽象方法所提供的。如果我们不满足于基类能获得的信息,我们可以小小的修改一下事件处理函数的事件参数类型注解,使其变为子类型,这样我们就可以通过协议适配器定义的子类型来获取更多的信息。我们以 `Console` 协议适配器为例: ```python {4} title=weather/__init__.py from nonebot.adapters.console import MessageEvent @weather.got("location", prompt="请输入地名") async def got_location(event: MessageEvent, location: str = ArgPlainText()): await weather.finish(f"{event.time.strftime('%Y-%m-%d')} {location} 的天气是...") ``` 在上面的代码中,我们获取了 `Console` 协议适配器的消息事件提供的发送时间 `time` 属性。 :::caution 注意 如果**基类**就能满足你的需求,那么就**不要修改**事件参数类型注解,这样可以使你的代码更加**通用**,可以在更多平台上运行。如何根据不同平台事件类型进行不同的处理,我们将在[重载](#重载)一节中介绍。 ::: ## 重载 我们在编写机器人时,常常会遇到这样一个问题:如何对私聊和群聊消息进行不同的处理?如何对不同平台的事件进行不同的处理?针对这些问题,NoneBot 提供了一个便捷而高效的解决方案 ── 重载。简单来说,依赖函数会根据其参数的类型注解来决定是否执行,忽略不符合其参数类型注解的情况。这样,我们就可以通过修改事件参数类型注解来实现对不同事件的处理,或者修改 `Bot` 参数类型注解来实现使用不同平台的接口。我们以 `OneBot` 协议适配器为例: ```python {4,8} from nonebot.adapters.onebot.v11 import PrivateMessageEvent, GroupMessageEvent @matcher.handle() async def handle_private(event: PrivateMessageEvent): await matcher.finish("私聊消息") @matcher.handle() async def handle_group(event: GroupMessageEvent): await matcher.finish("群聊消息") ``` 这样,机器人用户就会在私聊和群聊中分别收到不同的回复。同样的,我们也可以通过修改 `Bot` 参数类型注解来实现使用不同平台的接口: ```python from nonebot.adapters.console import Bot as ConsoleBot from nonebot.adapters.onebot.v11 import Bot as OneBot @matcher.handle() async def handle_console(bot: ConsoleBot): await bot.bell() @matcher.handle() async def handle_onebot(bot: OneBot): await bot.send_group_message(group_id=123123, message="OneBot") ``` :::caution 注意 重载机制对所有的参数类型注解都有效,因此,依赖注入也可以使用这个特性来对不同的返回值进行处理。 但 Bot、Event 和 Matcher 三者的参数类型注解具有最高检查优先级,如果三者任一类型注解不匹配,那么其他依赖注入将不会执行(如:`Depends`)。 ::: :::tip 提示 如何更好地编写一个跨平台的插件,我们将在[最佳实践](../best-practice/multi-adapter.mdx)中介绍。 ::: ================================================ FILE: website/docs/appendices/permission.mdx ================================================ --- sidebar_position: 5 description: 控制事件响应器的权限 options: menu: - category: appendices weight: 60 --- # 权限控制 import Messenger from "@site/src/components/Messenger"; **权限控制**是机器人在实际应用中需要解决的重点问题之一,NoneBot 提供了灵活的权限控制机制 —— `Permission`。 类似于响应规则 `Rule`,`Permission` 是由非负整数个 `PermissionChecker` 所共同组成的**用于筛选事件**的对象。但需要特别说明的是,权限和响应规则有如下区别: 1. 权限检查**先于**响应规则检查 2. `Permission` 只需**其中一个** `PermissionChecker` 返回 `True` 时就会检查通过 3. 权限检查进行时,上下文中并不存在会话状态 `state` 4. `Rule` 仅在**初次触发**事件响应器时进行检查,在余下的会话中并不会限制事件;而 `Permission` 会**持续生效**,在连续对话中一直对事件主体加以限制。 ## 基础使用 通常情况下,`Permission` 更侧重于对于**触发事件的机器人用户**的筛选,例如由 NoneBot 自身提供的 `SUPERUSER` 权限,便是筛选出会话发起者是否为超级用户。它可以对输入的用户进行鉴别,如果符合要求则会被认为通过并返回 `True`,反之则返回 `False`。 简单来说,`Permission` 是一个用于筛选出符合要求的用户的机制,可以通过 `Permission` 精确的控制响应对象的覆盖范围,从而拒绝掉我们所不希望的事件。 例如,我们可以在 `weather` 插件中添加一个超级用户可用的指令: ```python {3,9} title=weather/__init__.py from typing import Tuple from nonebot.params import Command from nonebot.permission import SUPERUSER manage = on_command( ("天气", "启用"), rule=to_me(), aliases={("天气", "禁用")}, permission=SUPERUSER, ) @manage.handle() async def control(cmd: Tuple[str, str] = Command()): _, action = cmd if action == "启用": plugin_config.weather_plugin_enabled = True elif action == "禁用": plugin_config.weather_plugin_enabled = False await manage.finish(f"天气插件已{action}") ``` 如上方示例所示,在注册事件响应器时,我们设置了 `permission` 参数,那么这个事件处理器在触发事件前的检查阶段会对用户身份进行验证,如果不符合我们设置的条件(此处即为**超级用户**)则不会响应。此时,我们向机器人发送 `/天气.禁用` 指令,机器人不会有任何响应,因为我们还不是机器人的超级管理员。我们在 dotenv 文件中设置了 `SUPERUSERS` 配置项之后,机器人就会响应我们的指令了。 ```dotenv title=.env SUPERUSERS=["console_user"] ``` ## 自定义权限 与事件响应规则类似,`PermissionChecker` 也是一个返回值为 `bool` 类型的依赖函数,即 `PermissionChecker` 支持依赖注入。例如,我们可以限制用户的指令调用次数: ```python title=weather/__init__.py from nonebot.adapters import Event fake_db: Dict[str, int] = {} async def limit_permission(event: Event): count = fake_db.setdefault(event.get_user_id(), 100) if count > 0: fake_db[event.get_user_id()] -= 1 return True return False weather = on_command("天气", permission=limit_permission) ``` ## 权限组合 权限之间可以通过 `|` 运算符进行组合,使得任意一个权限检查返回 `True` 时通过。例如: ```python {4-6} perm1 = Permission(foo_checker) perm2 = Permission(bar_checker) perm = perm1 | perm2 perm = perm1 | bar_checker perm = foo_checker | perm2 ``` 同样的,我们也无需担心组合了一个 `None` 值,`Permission` 会自动忽略 `None` 值。 ```python assert (perm | None) is perm ``` ## 主动使用权限 除了在事件响应器中使用权限外,我们也可以主动使用权限来判断事件是否符合条件。例如: ```python {3} perm = Permission(some_checker) result: bool = await perm(bot, event) ``` 我们只需要传入 `Bot` 实例、事件,`Permission` 会并发调用所有 `PermissionChecker` 进行检查,并返回结果。 ================================================ FILE: website/docs/appendices/rule.md ================================================ --- sidebar_position: 1 description: 自定义响应规则 options: menu: - category: appendices weight: 20 --- # 响应规则 机器人在实际应用中,往往会接收到多种多样的事件类型,NoneBot 通过响应规则来控制事件的处理。 在[指南](../tutorial/matcher.md#为事件响应器添加参数)中,我们为 `weather` 命令添加了一个 `rule=to_me()` 参数,这个参数就是一个响应规则,确保只有在私聊或者 `@bot` 时才会响应。 响应规则是一个 `Rule` 对象,它由一系列的 `RuleChecker` 函数组成,每个 `RuleChecker` 函数都会检查事件是否符合条件,如果所有的检查都通过,则事件会被处理。 ## RuleChecker `RuleChecker` 是一个返回值为 `bool` 类型的依赖函数,即 `RuleChecker` 支持依赖注入。我们可以根据上一节中添加的[配置项](./config.mdx#插件配置),在 `weather` 插件目录中编写一个响应规则: ```python {7,8} title=weather/__init__.py from nonebot import get_plugin_config from .config import Config plugin_config = get_plugin_config(Config) async def is_enable() -> bool: return plugin_config.weather_plugin_enabled weather = on_command("天气", rule=is_enable) ``` 在上面的代码中,我们定义了一个函数 `is_enable`,它会检查配置项 `weather_plugin_enabled` 是否为 `True`。这个函数 `is_enable` 即为一个 `RuleChecker`。 ## Rule `Rule` 是若干个 `RuleChecker` 的集合,它会并发调用每个 `RuleChecker`,只有当所有 `RuleChecker` 检查通过时匹配成功。例如:我们可以组合两个 `RuleChecker`,一个用于检查插件是否启用,一个用于检查用户是否在黑名单中: ```python {10} from nonebot.rule import Rule from nonebot.adapters import Event async def is_enable() -> bool: return plugin_config.weather_plugin_enabled async def is_blacklisted(event: Event) -> bool: return event.get_user_id() not in BLACKLIST rule = Rule(is_enable, is_blacklisted) weather = on_command("天气", rule=rule) ``` ## 合并响应规则 在定义响应规则时,我们可以将规则进行细分,来更好地复用规则。而在使用时,我们需要合并多个规则。除了使用 `Rule` 对象来组合多个 `RuleChecker` 外,我们还可以对 `Rule` 对象进行合并。在原 `weather` 插件中,我们可以将 `rule=to_me()` 与 `rule=is_enable` 使用 `&` 运算符合并: ```python {13} title=weather/__init__.py from nonebot.rule import to_me from nonebot import get_plugin_config from .config import Config plugin_config = get_plugin_config(Config) async def is_enable() -> bool: return plugin_config.weather_plugin_enabled weather = on_command( "天气", rule=to_me() & is_enable, aliases={"weather", "查天气"}, priority=plugin_config.weather_command_priority, block=True, ) ``` 这样,`weather` 命令就只会在插件启用且在私聊或者 `@bot` 时才会响应。 合并响应规则可以有多种形式,例如: ```python {4-6} rule1 = Rule(foo_checker) rule2 = Rule(bar_checker) rule = rule1 & rule2 rule = rule1 & bar_checker rule = foo_checker & rule2 ``` 同时,我们也无需担心合并了一个 `None` 值,`Rule` 会忽略 `None` 值。 ```python assert (rule & None) is rule ``` ## 主动使用响应规则 除了在事件响应器中使用响应规则外,我们也可以主动使用响应规则来判断事件是否符合条件。例如: ```python {3} rule = Rule(some_checker) result: bool = await rule(bot, event, state) ``` 我们只需要传入 `Bot` 对象、事件和会话状态,`Rule` 会并发调用所有 `RuleChecker` 进行检查,并返回结果。 ## 内置响应规则 NoneBot 内置了一些常用的响应规则,可以直接通过事件响应器辅助函数或者自行合并其他规则使用。内置响应规则列表可以参考[事件响应器进阶](../advanced/matcher.md) ================================================ FILE: website/docs/appendices/session-control.mdx ================================================ --- sidebar_position: 2 description: 更灵活的会话控制 options: menu: - category: appendices weight: 30 --- # 会话控制 import Messenger from "@site/src/components/Messenger"; 在[指南](../tutorial/event-data.mdx#使用依赖注入)的 `weather` 插件中,我们使用依赖注入获取了机器人用户发送的地名参数,并根据地名参数进行相应的回复。但是,一问一答的对话模式仅仅适用于简单的对话场景,如果我们想要实现更复杂的对话模式,就需要使用会话控制。 ## 询问并获取用户输入 在 `weather` 插件中,我们对于用户未输入地名参数的情况直接回复了 `请输入地名` 并结束了事件流程。但是,这样用户体验并不好,需要重新输入指令和地名参数才能获取天气回复。我们现在来实现询问并获取用户地名参数的功能。 ### 询问用户 我们可以使用事件响应器操作中的 `got` 装饰器来表示当前事件处理流程需要询问并获取用户输入的消息: ```python {6} title=weather/__init__.py @weather.handle() async def handle_function(args: Message = CommandArg()): if location := args.extract_plain_text(): await weather.finish(f"今天{location}的天气是...") @weather.got("location", prompt="请输入地名") async def got_location(): ... ``` 在上面的代码中,我们使用 `got` 事件响应器操作来向用户发送 `prompt` 消息,并等待用户的回复。用户的回复消息将会被作为 `location` 参数存储于事件响应器状态中。 :::tip 提示 事件处理函数根据定义的顺序依次执行。 ::: ### 获取用户输入 在询问以及用户回复之后,我们就可以获取到我们需要的 `location` 参数了。我们使用 `ArgPlainText` 依赖注入来获取参数纯文本信息: ```python {9} title=weather/__init__.py from nonebot.params import ArgPlainText @weather.handle() async def handle_function(args: Message = CommandArg()): if location := args.extract_plain_text(): await weather.finish(f"今天{location}的天气是...") @weather.got("location", prompt="请输入地名") async def got_location(location: str = ArgPlainText()): await weather.finish(f"今天{location}的天气是...") ``` 在上面的代码中,我们在 `got_location` 函数中定义了一个依赖注入参数 `location`,他的值将会是用户回复的消息纯文本信息。获取到用户输入的地名参数后,我们就可以进行天气查询并回复了。 :::tip 提示 如果想要获取用户回复的消息对象 `Message` ,可以使用 `Arg` 依赖注入。 ::: ### 跳过询问 在上面的代码中,如果用户在输入天气指令时,同时提供了地名参数,我们直接回复了天气信息,这部分的逻辑是和询问用户地名参数之后的逻辑一致的。如果在复杂的业务场景下,我们希望这部分代码应该复用以减少代码冗余。我们可以使用事件响应器操作中的 `set_arg` 来主动设置一个参数: ```python {4,6} title=weather/__init__.py from nonebot.matcher import Matcher @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()): await weather.finish(f"今天{location}的天气是...") ``` 请注意,设置参数需要使用依赖注入来获取 `Matcher` 实例以确保上下文正确,且参数值应为 `Message` 对象。 在 `location` 参数被设置之后,`got` 事件响应器操作将不再会询问并等待用户的回复,而是直接进入 `got_location` 函数。 ## 请求重新输入 在实际的业务场景中,用户的输入很有可能并非是我们所期望的,而结束事件处理流程让用户重新发送指令也不是一个好的体验。这时我们可以使用 `reject` 事件响应器操作来请求用户重新输入: ```python {8,9} title=weather/__init__.py @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}的天气是...") ``` 在上面的代码中,我们在 `got_location` 函数中判断用户输入的地名是否在支持的城市列表中,如果不在,则使用 `reject` 事件响应器操作。操作将会向用户发送 `reject` 参数中的消息,并等待用户回复后,重新执行 `got_location` 函数。通过 `got` 和 `reject` 事件响应器操作,我们实现了类似于**循环**的执行方式。 `reject` 事件响应器操作与 `finish` 类似,NoneBot 会在向机器人用户发送消息内容后抛出 `RejectedException` 异常来暂停事件响应流程以等待用户输入。也就是说,在 `reject` 被执行后,后续的程序同样是不会被执行的。 ## 更多事件响应器操作 在之前的章节中,我们已经大致了解了五个事件响应器操作:`handle`、`got`、`finish`、`send` 和 `reject`。现在我们来完整地介绍一下这些操作。 事件响应器操作可以分为两大类:**交互操作**和**流程控制操作**。我们可以通过交互操作来与用户进行交互,而流程控制操作则可以用来控制事件处理流程的执行。 :::tip 提示 事件处理流程按照事件处理函数添加顺序执行,已经结束的事件处理函数不可能被恢复执行。 ::: ### handle `handle` 事件响应器操作是一个装饰器,用于向事件处理流程添加一个事件处理函数。 ```python @matcher.handle() async def handle_func(): ... ``` `handle` 装饰器支持嵌套操作,即一个事件处理函数可以被添加多次: ```python @matcher.handle() @matcher.handle() async def handle_func(): # 这个函数会被执行两次 ... ``` ### got `got` 事件响应器操作也是一个装饰器,它会在当前装饰的事件处理函数运行之前,中断当前事件处理流程,等待接收一个新的事件。它可以通过 `prompt` 参数来向用户发送询问消息,然后等待用户的回复消息,贴近对话形式会话。 `got` 装饰器接受一个参数 `key` 和一个可选参数 `prompt`。当会话状态中不存在 `key` 对应的消息时,会向用户发送 `prompt` 参数的消息,并等待用户回复。`prompt` 参数的类型和 [`send`](#send) 事件响应器操作的参数类型一致。 在事件处理函数中,可以通过依赖注入的方式来获取接收到的消息,参考:[`Arg`](../advanced/dependency.mdx#arg)、[`ArgStr`](../advanced/dependency.mdx#argstr)、[`ArgPlainText`](../advanced/dependency.mdx#argplaintext)。 ```python @matcher.got("key", prompt="请输入...") async def got_func(key: Message = Arg()): ... ``` `got` 装饰器支持与 `got` 和 `receive` 装饰器嵌套操作,即一个事件处理函数可以在接收多个事件或消息后执行: ```python @matcher.got("key1", prompt="请输入key1...") @matcher.got("key2", prompt="请输入key2...") @matcher.receive("key3") async def got_func(key1: Message = Arg(), key2: Message = Arg(), key3: Event = Received("key3")): ... ``` ### receive `receive` 事件响应器操作也是一个装饰器,它会在当前装饰的事件处理函数运行之前,中断当前事件处理流程,等待接收一个新的事件。与 `got` 不同的是,`receive` 不会向用户发送询问消息,并且等待一个用户事件。可以接收的事件类型取决于[会话更新](../advanced/session-updating.md)。 `receive` 装饰器接受一个可选参数 id,用于标识当前需要接收的事件,如果不指定,则默认为空 `""`。 在事件处理函数中,可以通过依赖注入的方式来获取接收到的事件,参考:[`Received`](../advanced/dependency.mdx#received)、[`LastReceived`](../advanced/dependency.mdx#lastreceived)。 ```python @matcher.receive("id") async def receive_func(event: Event = Received("id")): ... ``` `receive` 装饰器支持与 `got` 和 `receive` 装饰器嵌套操作,即一个事件处理函数可以在接收多个事件或消息后执行: ```python @matcher.receive("key1") @matcher.got("key2", prompt="请输入key2...") @matcher.got("key3", prompt="请输入key3...") async def receive_func(key1: Event = Received("key1"), key2: Message = Arg(), key3: Message = Arg()): ... ``` ### send `send` 事件响应器操作用于向用户回复一条消息。协议适配器会根据当前 event 选择回复的途径。 `send` 操作接受一个参数 message 和其他任何协议适配器接受的参数。message 参数类型可以是字符串、消息序列、消息段或者消息模板。消息模板将会使用会话状态字典进行渲染后发送。 这个操作等同于使用 `bot.send(event, message, **kwargs)`,但不需要自行传入 `event`。 ```python @matcher.handle() async def _(): await matcher.send("Hello world!") ``` ### finish 向用户回复一条消息(可选),并立即结束**整个处理流程**。 参数与 [`send`](#send) 相同。 ```python @matcher.handle() async def _(): await matcher.finish("Hello world!") # 下面的代码不会被执行 ``` ### pause 向用户回复一条消息(可选),立即结束**当前**事件处理函数,等待接收一个新的事件后进入**下一个**事件处理函数。 参数与 [`send`](#send) 相同。 ```python @matcher.handle() async def _(): if need_confirm: await matcher.pause("请在两分钟内确认执行") @matcher.handle() async def _(): ... ``` ### reject 向用户回复一条消息(可选),立即结束**当前**事件处理函数,等待接收一个新的事件后再次执行**当前**事件处理函数。 `reject` 可以用于拒绝当前 `receive` 接收的事件或 `got` 接收的参数。通常在用户回复不符合格式或标准需要重新输入,或者用于循环进行用户交互。 参数与 [`send`](#send) 相同。 ```python @matcher.got("arg") async def _(arg: str = ArgPlainText()): if not is_valid(arg): await matcher.reject("Invalid arg!") ``` ### reject_arg 向用户回复一条消息(可选),立即结束**当前**事件处理函数,等待接收一个新的消息后再次执行**当前**事件处理函数。 `reject_arg` 用于拒绝指定 `got` 接收的参数,通常在嵌套装饰器时使用。 `reject_arg` 操作接受一个 key 参数以及可选的 prompt 参数。prompt 参数与 [`send`](#send) 相同。 ```python @matcher.got("a") @matcher.got("b") async def _(a: str = ArgPlainText(), b: str = ArgPlainText()): if a not in b: await matcher.reject_arg("a", "Invalid a!") ``` ### reject_receive 向用户回复一条消息(可选),立即结束**当前**事件处理函数,等待接收一个新的事件后再次执行**当前**事件处理函数。 `reject_receive` 用于拒绝指定 `receive` 接收的事件,通常在嵌套装饰器时使用。 `reject_receive` 操作接受一个可选的 id 参数以及可选的 prompt 参数。id 参数默认为空 `""`,prompt 参数与 [`send`](#send) 相同。 ```python @matcher.receive("a") @matcher.receive("b") async def _(a: Event = Received("a"), b: Event = Received("b")): if a.get_user_id() != b.get_user_id(): await matcher.reject_receive("a") ``` ### skip 立即结束当前事件处理函数,进入下一个事件处理函数。 通常在依赖注入中使用,用于跳过当前事件处理函数的执行。 ```python from nonebot.params import Depends async def dependency(): matcher.skip() @matcher.handle() async def _(check=Depends(dependency)): # 这个函数不会被执行 ``` ### stop_propagation 阻止事件向更低优先级的事件响应器传播。 ```python from nonebot.matcher import Matcher @foo.handle() async def _(matcher: Matcher): matcher.stop_propagation() ``` :::caution 注意 `stop_propagation` 操作是实例方法,需要先通过依赖注入获取事件响应器实例再进行调用。 ::: ### get_arg 获取一个 `got` 接收的参数。 `get_arg` 操作接受一个 key 参数和一个可选的 default 参数。当参数不存在时,将返回 default 或 `None`。 ```python from nonebot.matcher import Matcher @matcher.handle() async def _(matcher: Matcher): key = matcher.get_arg("key", default=None) ``` ### set_arg 设置 / 覆盖一个 `got` 接收的参数。 `set_arg` 操作接受一个 key 参数和一个 value 参数。请注意,value 参数必须是消息序列对象,如需存储其他数据请使用[会话状态](./session-state.md)。 ```python from nonebot.matcher import Matcher @matcher.handle() async def _(matcher: Matcher): matcher.set_arg("key", Message("value")) ``` ### get_receive 获取一个 `receive` 接收的事件。 `get_receive` 操作接受一个 id 参数和一个可选的 default 参数。当事件不存在时,将返回 default 或 `None`。 ```python from nonebot.matcher import Matcher @matcher.handle() async def _(matcher: Matcher): event = matcher.get_receive("id", default=None) ``` ### get_last_receive 获取最近的一个 `receive` 接收的事件。 `get_last_receive` 操作接受一个可选的 default 参数。当事件不存在时,将返回 default 或 `None`。 ```python from nonebot.matcher import Matcher @matcher.handle() async def _(matcher: Matcher): event = matcher.get_last_receive(default=None) ``` ### set_receive 设置 / 覆盖一个 `receive` 接收的事件。 `set_receive` 操作接受一个 id 参数和一个 event 参数。请注意,event 参数必须是事件对象,如需存储其他数据请使用[会话状态](./session-state.md)。 ```python from nonebot.matcher import Matcher @matcher.handle() async def _(matcher: Matcher): matcher.set_receive("key", Event()) ``` ================================================ FILE: website/docs/appendices/session-state.md ================================================ --- sidebar_position: 3 description: 会话状态信息 options: menu: - category: appendices weight: 40 --- # 会话状态 在事件处理流程中,和用户交互的过程即是会话。在会话中,我们可能需要记录一些信息,例如用户的重试次数等等,以便在会话中的不同阶段进行判断和处理。这些信息都可以存储于会话状态中。 NoneBot 中的会话状态是一个字典,可以通过类型 `T_State` 来获取。字典内可以存储任意类型的数据,但是要注意的是,NoneBot 本身会在会话状态中存储一些信息,因此不要使用 [NoneBot 使用的键名](../api/consts.md)。 ```python from nonebot.typing import T_State @matcher.got("key", prompt="请输入密码") async def _(state: T_State, key: str = ArgPlainText()): if key != "some password": try_count = state.get("try_count", 1) if try_count >= 3: await matcher.finish("密码错误次数过多") else: state["try_count"] = try_count + 1 await matcher.reject("密码错误,请重新输入") await matcher.finish("密码正确") ``` 会话状态的生命周期与事件处理流程相同,在期间的任何一个事件处理函数都可以进行读写。 ```python from nonebot.typing import T_State @matcher.handle() async def _(state: T_State): state["key"] = "value" @matcher.handle() async def _(state: T_State): await matcher.finish(state["key"]) ``` 会话状态还可以用于发送动态消息,消息模板在发送时会使用会话状态字典进行渲染。消息模板的使用方法已经在[消息处理](../tutorial/message.md#使用消息模板)中介绍过,这里不再赘述。 ```python from nonebot.typing import T_State from nonebot.adapters import MessageTemplate @matcher.handle() async def _(state: T_State): state["username"] = "user" @matcher.got("password", prompt=MessageTemplate("请输入 {username} 的密码")) async def _(): await matcher.finish(MessageTemplate("密码为 {password}")) ``` ================================================ FILE: website/docs/appendices/whats-next.md ================================================ --- sidebar_position: 99 description: 下一步──进阶! --- # 下一步 至此,我们已经了解了 NoneBot 的大多数功能用法,相信你已经可以独自写出一个插件了。现在你可以选择: - 即刻开始插件编写! - 更深入地了解 NoneBot 的[更多功能和原理](../advanced/plugin-info.md)! ================================================ FILE: website/docs/best-practice/alconna/README.mdx ================================================ 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#安装插件)来了解并选择安装插件的方式。如: 在**项目目录**下执行以下命令: ```shell nb plugin install nonebot-plugin-alconna ``` ```shell pip install nonebot-plugin-alconna ``` ```shell pdm add nonebot-plugin-alconna ``` ## 导入插件 由于 `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` 来改写这个插件。
插件示例 ```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}的天气是...") ```
```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) ================================================ FILE: website/docs/best-practice/alconna/_category_.json ================================================ { "label": "命令解析拓展", "position": 6 } ================================================ FILE: website/docs/best-practice/alconna/builtins.mdx ================================================ --- 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` 插件能将用户发送的消息原样返回。 #### help `help` 插件能列出所有 Alconna 指令。同时还能查询某个指令对应的插件信息。 help 插件的帮助信息如下: ``` /help ## 注释 query: 选择某条命令的id或者名称查看具体帮助 显示所有命令帮助 用法: 可以使用 --hide 参数来显示隐藏命令,使用 -P 参数来显示命令所属插件名称 可用的子命令有: * 是否列出命令所属命名空间 -N│--namespace│命名空间 [target: str] ## 注释 target: 指定的命名空间 该子命令内可用的选项有: * 列出所有命名空间 --list 可用的选项有: * 查看指定页数的命令帮助 --page * 查看命令所属插件的信息 -P│插件信息│--plugin-info * 是否列出隐藏命令 隐藏│-H│--hide ``` #### lang `lang` 插件能切换 i18n 的语言设置。 lang 插件的帮助信息如下: ``` /lang i18n配置相关功能 可用的选项有: * 查看支持的语言列表 list [name: str] * 切换语言 switch [locale: str] ``` 其中 `list` 选项可以查找某一插件下的语言支持情况 (例如 `/lang list nonebot_plugin_alconna`)。 #### switch `switch` 插件能用来启用/禁用某个命令,其使用方法与 `help` 类似。 #### with `with` 插件能在当前会话中设置一个局部命令前缀,以便于有多个子命令的指令使用。 with 插件的帮助信息如下: ``` .with [name: str] with 指令 用法: 设置局部命令前缀 可用的选项有: * 设置可能的生效时间 --expire│expire * 取消当前前缀 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 的音乐分享卡片 ================================================ FILE: website/docs/best-practice/alconna/command.md ================================================ --- sidebar_position: 2 description: Alconna 基本介绍 --- # Alconna 本体 [`Alconna`](https://github.com/ArcletProject/Alconna) 隶属于 `ArcletProject`,是一个简单、灵活、高效的命令参数解析器, 并且不局限于解析命令式字符串。 我们先通过一个例子来讲解 **Alconna** 的核心 —— `Args`, `Subcommand`, `Option`: ```python from arclet.alconna import Alconna, Args, Subcommand, Option alc = Alconna( "pip", Subcommand( "install", Args["package", str], Option("-r|--requirement", Args["file", str]), Option("-i|--index-url", Args["url", str]), ) ) res = alc.parse("pip install nonebot2 -i URL") print(res) # matched=True, header_match=(origin='pip' result='pip' matched=True groups={}), subcommands={'install': (value=Ellipsis args={'package': 'nonebot2'} options={'index-url': (value=None args={'url': 'URL'})} subcommands={})}, other_args={'package': 'nonebot2', 'url': 'URL'} print(res.all_matched_args) # {'package': 'nonebot2', 'url': 'URL'} ``` 这段代码通过`Alconna`创捷了一个接受主命令名为`pip`, 子命令为`install`且子命令接受一个 **Args** 参数`package`和二个 **Option** 参数`-r`和`-i`的命令参数解析器, 通过`parse`方法返回解析结果 **Arparma** 的实例。 ## 命令头 命令头是指命令的前缀 (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", "bar"] | "baz" | `"foobaz"` 或 `"barbaz"` | 纯文字头 | | [int, bool] | "foo" | `[123, "foo"]` 或 `[False, "foo"]` | 类型头 | | [123, 4567] | "foo" | `[123, "foo"]` 或 `[4567, "foo"]` | 元素头 | | [nepattern.NUMBER] | "bar" | `[123, "bar"]` 或 `[123.456, "bar"]` | 表达式头 | | [123, "foo"] | "bar" | `[123, "bar"]` 或 `"foobar"` 或 `["foo", "bar"]` | 混合头 | | [(int, "foo"), (456, "bar")] | "baz" | `[123, "foobaz"]` 或 `[456, "foobaz"]` 或 `[456, "barbaz"]` | 对头 | 对于无前缀的类型头,此时会将传入的值尝试转为 BasePattern,例如 `int` 会转为 `nepattern.INTEGER`。如此该命令头会匹配对应的类型, 例如 `int` 会匹配 `123` 或 `"456"`,但不会匹配 `"foo"`。解析后,Alconna 会将命令头匹配到的值转为对应的类型,例如 `int` 会将 `"123"` 转为 `123`。 :::tip **正则内容只在命令名上生效,前缀中的正则会被转义** ::: 除了通过传入 `re:xxx` 来使用正则表达式外,Alconna 还提供了一种更加简洁的方式来使用正则表达式,称为 Bracket Header: ```python alc = Alconna(".rd{roll:int}") assert alc.parse(".rd123").header["roll"] == 123 ``` Bracket Header 类似 python 里的 f-string 写法,通过 `"{}"` 声明匹配类型 `"{}"` 中的内容为 "name:type or pat": - `"{}"`, `"{:}"` ⇔ `"(.+)"`, 占位符 - `"{foo}"` ⇔ `"(?P<foo>.+)"` - `"{:\d+}"` ⇔ `"(\d+)"` - `"{foo:int}"` ⇔ `"(?P<foo>\d+)"`,其中 `"int"` 部分若能转为 `BasePattern` 则读取里面的表达式 ## 参数声明(Args) `Args` 是用于声明命令参数的组件, 可以通过以下几种方式构造 **Args** : - `Args[key, var, default][key1, var1, default1][...]` - `Args[(key, var, default)]` - `Args.key[var, default]` 其中,key **一定**是字符串,而 var 一般为参数的类型,default 为具体的值或者 **arclet.alconna.args.Field** 其与函数签名类似,但是允许含有默认值的参数在前;同时支持 keyword-only 参数不依照构造顺序传入 (但是仍需要在非 keyword-only 参数之后)。 ### key `key` 的作用是用以标记解析出来的参数并存放于 **Arparma** 中,以方便用户调用。 其有三种为 Args 注解的标识符: `?`、`/`、 `!`, 标识符与 key 之间建议以 `;` 分隔: - `!` 标识符表示该处传入的参数应**不是**规定的类型,或**不在**指定的值中。 - `?` 标识符表示该参数为**可选**参数,会在无参数匹配时跳过。 - `/` 标识符表示该参数的类型注解需要隐藏。 另外,对于参数的注释也可以标记在 `key` 中,其与 key 或者标识符 以 `#` 分割: `foo#这是注释;?` 或 `foo?#这是注释` :::tip `Args` 中的 `key` 在实际命令中并不需要传入(keyword 参数除外): ```python from arclet.alconna import Alconna, Args alc = Alconna("test", Args["foo", str]) alc.parse("test --foo abc") # 错误 alc.parse("test abc") # 正确 ``` 若需要 `test --foo abc`,你应该使用 `Option`: ```python from arclet.alconna import Alconna, Args, Option alc = Alconna("test", Option("--foo", Args["foo", str])) ``` ::: ### var var 负责命令参数的**类型检查**与**类型转化** `Args` 的`var`表面上看需要传入一个 `type`,但实际上它需要的是一个 `nepattern.BasePattern` 的实例: ```python from arclet.alconna import Args from nepattern import BasePattern # 表示 foo 参数需要匹配一个 @number 样式的字符串 args = Args["foo", BasePattern("@\d+")] ``` `pip` 示例中可以传入 `str` 是因为 `str` 已经注册在了 `nepattern.global_patterns` 中,因此会替换为 `nepattern.global_patterns[str]` `nepattern.global_patterns`默认支持的类型有: - `str`: 匹配任意字符串 - `int`: 匹配整数 - `float`: 匹配浮点数 - `bool`: 匹配 `True` 与 `False` 以及他们小写形式 - `hex`: 匹配 `0x` 开头的十六进制字符串 - `url`: 匹配网址 - `email`: 匹配 `xxxx@xxx` 的字符串 - `ipv4`: 匹配 `xxx.xxx.xxx.xxx` 的字符串 - `list`: 匹配类似 `["foo","bar","baz"]` 的字符串 - `dict`: 匹配类似 `{"foo":"bar","baz":"qux"}` 的字符串 - `datetime`: 传入一个 `datetime` 支持的格式字符串,或时间戳 - `Any`: 匹配任意类型 - `AnyString`: 匹配任意类型,转为 `str` - `Number`: 匹配 `int` 与 `float`,转为 `int` 同时可以使用 typing 中的类型: - `Literal[X]`: 匹配其中的任意一个值 - `Union[X, Y]`: 匹配其中的任意一个类型 - `Optional[xxx]`: 会自动将默认值设为 `None`,并在解析失败时使用默认值 - `List[X]`: 匹配一个列表,其中的元素为 `X` 类型 - `Dict[X, Y]`: 匹配一个字典,其中的 key 为 `X` 类型,value 为 `Y` 类型 - ... :::tip 几类特殊的传入标记: - `"foo"`: 匹配字符串 "foo" (若没有某个 `BasePattern` 与之关联) - `RawStr("foo")`: 匹配字符串 "foo" (即使有 `BasePattern` 与之关联也不会被替换) - `"foo|bar|baz"`: 匹配 "foo" 或 "bar" 或 "baz" - `[foo, bar, Baz, ...]`: 匹配其中的任意一个值或类型 - `Callable[[X], Y]`: 匹配一个参数为 `X` 类型的值,并返回通过该函数调用得到的 `Y` 类型的值 - `"re:xxx"`: 匹配一个正则表达式 `xxx`,会返回 Match[0] - `"rep:xxx"`: 匹配一个正则表达式 `xxx`,会返回 `re.Match` 对象 - `{foo: bar, baz: qux}`: 匹配字典中的任意一个键, 并返回对应的值 (特殊的键 ... 会匹配任意的值) - ... **特别的**,你可以不传入 `var`,此时会使用 `key` 作为 `var`, 匹配 `key` 字符串。 ::: #### MultiVar 与 KeyWordVar `MultiVar` 是一个特殊的标注,用于告知解析器该参数可以接受多个值,类似于函数中的 `*args`,其构造方法形如 `MultiVar(str)`。 同样的还有 `KeyWordVar`,类似于函数中的 `*, name: type`,其构造方法形如 `KeyWordVar(str)`,用于告知解析器该参数为一个 keyword-only 参数。 :::tip `MultiVar` 与 `KeyWordVar` 组合时,代表该参数为一个可接受多个 key-value 的参数,类似于函数中的 `**kwargs`,其构造方法形如 `MultiVar(KeyWordVar(str))` `MultiVar` 与 `KeyWordVar` 也可以传入 `default` 参数,用于指定默认值 `MultiVar` 不能在 `KeyWordVar` 之后传入 ::: #### AllParam `AllParam` 是一个特殊的标注,用于告知解析器该参数接收命令中在此位置之后的所有参数并**结束解析**,可以认为是**泛匹配参数**。 `AllParam` 可直接使用 (`Args["xxx", AllParam]`), 也可以传入指定的接收类型 (`Args["xxx", AllParam(str)]`)。 :::tip 在 `nonebot_plugin_alconna` 下,`AllParam` 的返回值为 [`UniMessage`](./uniseg/message.mdx) ::: ### default `default` 传入的是该参数的默认值或者 `Field`,以携带对于该参数的更多信息。 默认情况下 (即不声明) `default` 的值为特殊值 `Empty`。这也意味着你可以将默认值设置为 `None` 表示默认值为空值。 `Field` 构造需要的参数说明如下: - default: 参数单元的默认值 - alias: 参数单元默认值的别名 - completion: 参数单元的补全说明生成函数 - unmatch_tips: 参数单元的错误提示生成函数,其接收一个表示匹配失败的元素的参数 - missing_tips: 参数单元的缺失提示生成函数 ## 选项与子命令(Option & Subcommand) `Option` 和 `Subcommand` 可以传入一组 `alias`,如 `Option("--foo|-F|--FOO|-f")`,`Subcommand("foo", alias=["F"])` 传入别名后,选项与子命令会选择其中长度最长的作为其名称。若传入为 "--foo|-f",则命令名称为 "--foo" :::tip 特别提醒!!! Option 的名字或别名**没有要求**必须在前面写上 `-` Option 与 Subcommand 的唯一区别在于 Subcommand 可以传入自己的 **Option** 与 **Subcommand** ::: 他们拥有如下共同参数: - `help_text`: 传入该组件的帮助信息 - `dest`: 被指定为解析完成时标注匹配结果的标识符,不传入时默认为选项或子命令的名称 (name) - `requires`: 一段指定顺序的字符串列表,作为唯一的前置序列与命令嵌套替换 对于命令 `test foo bar baz qux ` 来讲,因为`foo bar baz` 仅需要判断是否相等, 所以可以这么编写: ```python Alconna("test", Option("qux", Args.a[int], requires=["foo", "bar", "baz"])) ``` - `default`: 默认值,在该组件未被解析时使用使用该值替换。 特别的,使用 `OptionResult` 或 `SubcomanndResult` 可以设置包括参数字典在内的默认值: ```python from arclet.alconna import Option, OptionResult opt1 = Option("--foo", default=False) opt2 = Option("--foo", default=OptionResult(value=False, args={"bar": 1})) ``` ### Action `Option` 可以特别设置传入一类 `Action`,作为解析操作 `Action` 分为三类: - `store`: 无 Args 时, 仅存储一个值, 默认为 Ellipsis; 有 Args 时, 后续的解析结果会覆盖之前的值 - `append`: 无 Args 时, 将多个值存为列表, 默认为 Ellipsis; 有 Args 时, 每个解析结果会追加到列表中, 当存在默认值并且不为列表时, 会自动将默认值变成列表, 以保证追加的正确性 - `count`: 无 Args 时, 计数器加一; 有 Args 时, 表现与 STORE 相同, 当存在默认值并且不为数字时, 会自动将默认值变成 1, 以保证计数器的正确性。 `Alconna` 提供了预制的几类 `Action`: - `store`(默认),`store_value`,`store_true`,`store_false` - `append`,`append_value` - `count` ## 解析结果 `Alconna.parse` 会返回由 **Arparma** 承载的解析结果 `Arparma` 有如下属性: - 调试类 - matched: 是否匹配成功 - error_data: 解析失败时剩余的数据 - error_info: 解析失败时的异常内容 - origin: 原始命令,可以类型标注 - 分析类 - header_match: 命令头部的解析结果,包括原始头部、解析后头部、解析结果与可能的正则匹配组 - main_args: 命令的主参数的解析结果 - options: 命令所有选项的解析结果 - subcommands: 命令所有子命令的解析结果 - other_args: 除主参数外的其他解析结果 - all_matched_args: 所有 Args 的解析结果 ### 路径查询 `Arparma` 同时提供了便捷的查询方法 `query[type]()`,会根据传入的 `path` 查找参数并返回 `path` 支持如下: - `main_args`, `options`, ...: 返回对应的属性 - `args`: 返回 all_matched_args - `args.`: 返回 all_matched_args 中 `key` 键对应的值 - `main_args.`: 返回主命令的解析参数字典中 `key` 键对应的值 - ``: 返回选项/子命令 `node` 的解析结果 (OptionResult | SubcommandResult) - `.value`: 返回选项/子命令 `node` 的解析值 - `.args`: 返回选项/子命令 `node` 的解析参数字典 - `.`, `.args.`: 返回选项/子命令 `node` 的参数字典中 `key` 键对应的值 以及: - `options.`: 返回选项 `opt` 的解析结果 (OptionResult) - `options..value`: 返回选项 `opt` 的解析值 - `options..args`: 返回选项 `opt` 的解析参数字典 - `options..`, `options..args.`: 返回选项 `opt` 的参数字典中 `key` 键对应的值 - `subcommands.`: 返回子命令 `subcmd` 的解析结果 (SubcommandResult) - `subcommands..value`: 返回子命令 `subcmd` 的解析值 - `subcommands..args`: 返回子命令 `subcmd` 的解析参数字典 - `subcommands..`, `subcommands..args.`: 返回子命令 `subcmd` 的参数字典中 `key` 键对应的值 ## 元数据(CommandMeta) `Alconna` 的元数据相当于其配置,拥有以下条目: - `description`: 命令的描述 - `usage`: 命令的用法 - `example`: 命令的使用样例 - `author`: 命令的作者 - `fuzzy_match`: 命令是否开启模糊匹配 - `fuzzy_threshold`: 模糊匹配阈值 - `raise_exception`: 命令是否抛出异常 - `hide`: 命令是否对 manager 隐藏 - `hide_shortcut`: 命令的快捷指令是否在 help 信息中隐藏 - `keep_crlf`: 命令解析时是否保留换行字符 - `compact`: 命令是否允许第一个参数紧随头部 - `strict`: 命令是否严格匹配,若为 False 则未知参数将作为名为 $extra 的参数 - `context_style`: 命令上下文插值的风格,None 为关闭,bracket 为 `{...}`,parentheses 为 `$(...)` - `extra`: 命令的自定义额外信息 元数据一定使用 `meta=...` 形式传入: ```python from arclet.alconna import Alconna, CommandMeta alc = Alconna(..., meta=CommandMeta("foo", example="bar")) ``` ## 命名空间配置 命名空间配置 (以下简称命名空间) 相当于 `Alconna` 的默认配置,其优先度低于 `CommandMeta`。 `Alconna` 默认使用 "Alconna" 命名空间。 命名空间有以下几个属性: - name: 命名空间名称 - prefixes: 默认前缀配置 - separators: 默认分隔符配置 - formatter_type: 默认格式化器类型 - fuzzy_match: 默认是否开启模糊匹配 - raise_exception: 默认是否抛出异常 - builtin_option_name: 默认的内置选项名称(--help, --shortcut, --comp) - disable_builtin_options: 默认禁用的内置选项(--help, --shortcut, --comp) - enable_message_cache: 默认是否启用消息缓存 - compact: 默认是否开启紧凑模式 - strict: 命令是否严格匹配 - context_style: 命令上下文插值的风格 - ... ### 新建命名空间并替换 ```python from arclet.alconna import Alconna, namespace, Namespace, Subcommand, Args, config ns = Namespace("foo", prefixes=["/"]) # 创建 "foo"命名空间配置, 它要求创建的Alconna的主命令前缀必须是/ alc = Alconna("pip", Subcommand("install", Args["package", str]), namespace=ns) # 在创建Alconna时候传入命名空间以替换默认命名空间 # 可以通过with方式创建命名空间 with namespace("bar") as np1: np1.prefixes = ["!"] # 以上下文管理器方式配置命名空间,此时配置会自动注入上下文内创建的命令 np1.formatter_type = ShellTextFormatter # 设置此命名空间下的命令的 formatter 默认为 ShellTextFormatter np1.builtin_option_name["help"] = {"帮助", "-h"} # 设置此命名空间下的命令的帮助选项名称 # 你还可以使用config来管理所有命名空间并切换至任意命名空间 config.namespaces["foo"] = ns # 将命名空间挂载到 config 上 alc = Alconna("pip", Subcommand("install", Args["package", str]), namespace=config.namespaces["foo"]) # 也是同样可以切换到"foo"命名空间 ``` ### 修改默认的命名空间 ```python from arclet.alconna import config, namespace, Namespace config.default_namespace.prefixes = [...] # 直接修改默认配置 np = Namespace("xxx", prefixes=[...]) config.default_namespace = np # 更换默认的命名空间 with namespace(config.default_namespace.name) as np: np.prefixes = [...] ``` ## 快捷指令 快捷命令可以做到标识一段命令, 并且传递参数给原命令 一般情况下你可以通过 `Alconna.shortcut` 进行快捷指令操作 (创建,删除) `shortcut` 的第一个参数为快捷指令名称,第二个参数为 `ShortcutArgs`,作为快捷指令的配置: ```python class ShortcutArgs(TypedDict): """快捷指令参数""" command: NotRequired[str] """快捷指令的命令""" args: NotRequired[list[Any]] """快捷指令的附带参数""" fuzzy: NotRequired[bool] """是否允许命令后随参数""" prefix: NotRequired[bool] """是否调用时保留指令前缀""" wrapper: NotRequired[ShortcutRegWrapper] """快捷指令的正则匹配结果的额外处理函数""" humanized: NotRequired[str] """快捷指令的人类可读描述""" ``` ### args的使用 ```python from arclet.alconna import Alconna, Args alc = Alconna("setu", Args["count", int]) alc.shortcut("涩图(\d+)张", {"args": ["{0}"]}) # 'Alconna::setu 的快捷指令: "涩图(\\d+)张" 添加成功' alc.parse("涩图3张").query("count") # 3 ``` ### command的使用 ```python from arclet.alconna import Alconna, Args alc = Alconna("eval", Args["content", str]) alc.shortcut("echo", {"command": "eval print(\\'{*}\\')"}) # 'Alconna::eval 的快捷指令: "echo" 添加成功' alc.shortcut("echo", delete=True) # 删除快捷指令 # 'Alconna::eval 的快捷指令: "echo" 删除成功' @alc.bind() # 绑定一个命令执行器, 若匹配成功则会传入参数, 自动执行命令执行器 def cb(content: str): eval(content, {}, {}) alc.parse('eval print(\\"hello world\\")') # hello world alc.parse("echo hello world!") # hello world! ``` 当 `fuzzy` 为 False 时,第一个例子中传入 `"涩图1张 abc"` 之类的快捷指令将视为解析失败 快捷指令允许三类特殊的 placeholder: - `{%X}`: 如 `setu {%0}`,表示此处填入快捷指令后随的第 X 个参数。 例如,若快捷指令为 `涩图`, 配置为 `{"command": "setu {%0}"}`, 则指令 `涩图 1` 相当于 `setu 1` - `{*}`: 表示此处填入所有后随参数,并且可以通过 `{*X}` 的方式指定组合参数之间的分隔符。 - `{X}`: 表示此处填入可能的正则匹配的组: - 若 `command` 中存在匹配组 `(xxx)`,则 `{X}` 表示第 X 个匹配组的内容 - 若 `command` 中存储匹配组 `(?P...)`, 则 `{X}` 表示 **名字** 为 X 的匹配结果 除此之外, 通过 **Alconna** 内置选项 `--shortcut` 可以动态操作快捷指令 例如: - `cmd --shortcut ` 来增加一个快捷指令 - `cmd --shortcut list` 来列出当前指令的所有快捷指令 - `cmd --shortcut delete key` 来删除一个快捷指令 ```python from arclet.alconna import Alconna, Args alc = Alconna("eval", Args["content", str]) alc.shortcut("echo", {"command": "eval print(\\'{*}\\')"}) alc.parse("eval --shortcut list") # 'echo' ``` ## 紧凑命令 `Alconna`, `Option` 与 `Subcommand` 可以设置 `compact=True` 使得解析命令时允许名称与后随参数之间没有分隔: ```python from arclet.alconna import Alconna, Option, CommandMeta, Args alc = Alconna("test", Args["foo", int], Option("BAR", Args["baz", str], compact=True), meta=CommandMeta(compact=True)) assert alc.parse("test123 BARabc").matched ``` 这使得我们可以实现如下命令: ```python from arclet.alconna import Alconna, Option, Args, append alc = Alconna("gcc", Option("--flag|-F", Args["content", str], action=append, compact=True)) print(alc.parse("gcc -Fabc -Fdef -Fxyz").query[list]("flag.content")) # ['abc', 'def', 'xyz'] ``` 当 `Option` 的 `action` 为 `count` 时,其自动支持 `compact` 特性: ```python from arclet.alconna import Alconna, Option, count alc = Alconna("pp", Option("--verbose|-v", action=count, default=0)) print(alc.parse("pp -vvv").query[int]("verbose.value")) # 3 ``` ## 模糊匹配 模糊匹配会应用在任意需要进行名称判断的地方,如 **命令名称**,**选项名称** 和 **参数名称** (如指定需要传入参数名称)。 ```python from arclet.alconna import Alconna, CommandMeta alc = Alconna("test_fuzzy", meta=CommandMeta(fuzzy_match=True)) alc.parse("test_fuzy") # test_fuzy is not matched. Do you mean "test_fuzzy"? ``` ## 半自动补全 半自动补全为用户提供了推荐后续输入的功能 补全默认通过 `--comp` 或 `-cp` 或 `?` 触发:(命名空间配置可修改名称) ```python from arclet.alconna import Alconna, Args, Option alc = Alconna("test", Args["abc", int]) + Option("foo") + Option("bar") alc.parse("test --comp") ''' output 以下是建议的输入: * * --help * -h * -sct * --shortcut * foo * bar ''' ``` ## Duplication **Duplication** 用来提供更好的自动补全,类似于 **ArgParse** 的 **Namespace** 普通情况下使用,需要利用到 **ArgsStub**、**OptionStub** 和 **SubcommandStub** 三个部分 以pip为例,其对应的 Duplication 应如下构造: ```python from arclet.alconna import Alconna, Args, Option, OptionResult, Duplication, SubcommandStub, Subcommand, count class MyDup(Duplication): verbose: OptionResult install: SubcommandStub alc = Alconna( "pip", Subcommand( "install", Args["package", str], Option("-r|--requirement", Args["file", str]), Option("-i|--index-url", Args["url", str]), ), Option("-v|--version"), Option("-v|--verbose", action=count), ) res = alc.parse("pip -v install ...") # 不使用duplication获得的提示较少 print(res.query("install")) # (value=Ellipsis args={'package': '...'} options={} subcommands={}) result = alc.parse("pip -v install ...", duplication=MyDup) print(result.install) # SubcommandStub(_origin=Subcommand('install', args=Args('package': str)), _value=Ellipsis, available=True, args=ArgsStub(_origin=Args('package': str), _value={'package': '...'}, available=True), dest='install', options=[OptionStub(_origin=Option('requirement', args=Args('file': str)), _value=None, available=False, args=ArgsStub(_origin=Args('file': str), _value={}, available=False), dest='requirement', aliases=['r', 'requirement'], name='requirement'), OptionStub(_origin=Option('index-url', args=Args('url': str)), _value=None, available=False, args=ArgsStub(_origin=Args('url': str), _value={}, available=False), dest='index-url', aliases=['index-url', 'i'], name='index-url')], subcommands=[], name='install') ``` **Duplication** 也可以如 **Namespace** 一样直接标明参数名称和类型: ```python from typing import Optional from arclet.alconna import Duplication class MyDup(Duplication): package: str file: Optional[str] = None url: Optional[str] = None ``` ## 上下文插值 当 `context_style` 条目被设置后,传入的命令中符合上下文插值的字段会被自动替换成当前上下文中的信息。 上下文可以在 `parse` 中传入: ```python from arclet.alconna import Alconna, Args, CommandMeta alc = Alconna("test", Args["foo", int], meta=CommandMeta(context_style="parentheses")) alc.parse("test $(bar)", {"bar": 123}) # {"foo": 123} ``` context_style 的值分两种: - `"bracket"`: 插值格式为 `{...}`,例如 `{foo}` - `"parentheses"`: 插值格式为 `$(...)`,例如 `$(bar)` ================================================ FILE: website/docs/best-practice/alconna/config.md ================================================ --- sidebar_position: 4 description: 配置项 --- # 配置项 ## alconna_auto_send_output - **类型**: `bool | None` - **默认值**: `None` 是否全局启用输出信息自动发送,不启用则会在触发特殊内置选项后仍然将解析结果传递至响应器。 ## alconna_use_command_start - **类型**: `bool` - **默认值**: `False` 是否读取 Nonebot 的配置项 `COMMAND_START` 来作为全局的 Alconna 命令前缀 ## alconna_global_completion - **类型**: [`CompConfig | None`](./matcher.mdx#补全会话) - **默认值**: `None` 全局的补全会话配置 (不代表全局启用补全会话)。 ## alconna_use_origin - **类型**: `bool` - **默认值**: `False` 是否全局使用原始消息 (即未经过 to_me 等处理的),该选项会影响到 Alconna 的匹配行为。 ## alconna_use_command_sep - **类型**: `bool` - **默认值**: `False` 是否读取 Nonebot 的配置项 `COMMAND_SEP` 来作为全局的 Alconna 命令分隔符。 ## alconna_global_extensions - **类型**: `list[str]` - **默认值**: `[]` 全局加载的扩展,其读取路径以 . 分隔,如 `foo.bar.baz:DemoExtension`。 对于内置扩展,路径为 `nonebot_plugin_alconna.builtins.extensions` 下的模块名,如 `ReplyMergeExtension`,可以使用 `@` 来缩写路径, 如 `@reply:ReplyMergeExtension`。 ## alconna_context_style - **类型**: `Optional[Literal["bracket", "parentheses"]]` - **默认值**: `None` 全局命令上下文插值的风格,None 为关闭,bracket 为 `{...}`,parentheses 为 `$(...)`。 ## alconna_enable_saa_patch - **类型**: `bool` - **默认值**: `False` 是否启用 SAA 补丁。 ## alconna_apply_filehost - **类型**: `bool` - **默认值**: `False` 是否启用文件托管。 ## alconna_apply_fetch_targets - **类型**: `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 自身发送的消息。 ================================================ FILE: website/docs/best-practice/alconna/matcher.mdx ================================================ --- 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("添加成功") ``` ## 声明 `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`:命令的源命令 - `: Match[]`:上述的匹配项,使用 `key` 作为查询路径 - `xxx: Query[] = Query(, default)`:上述的查询项,必需声明默认值以设置查询路径 `path` - 当用来查询选项/子命令是否存在时,可不写 `Query[]` - `xxx: Extension`:当前 `AlconnaMatcher` 使用的指定类型的匹配扩展 - `xxx: SelectedExtensions`:当前 `AlconnaMatcher` 使用的所有可用的匹配扩展 - `: `: 其他情况 - 当 `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. ```yaml title="zh-CN.yml" # 中文语言文件 demo: command: role-group: add: 添加 {name} 成功! ``` ```yaml title="en-US.yml" # 英文语言文件 demo: command: role-group: add: Add {name} successfully! ``` ```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("添加成功") ``` 此时,当用户输入 `添加教师` 时,会自动提示用户输入姓名,手机号和教师号,用户输入后会自动进入下一个提示: 补全会话配置如下: ```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] """进行补全会话时是否阻塞响应器""" ``` ================================================ FILE: website/docs/best-practice/alconna/shortcut.md ================================================ --- 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 ") .option("writer", "--anonymous", {"id": 0}) .usage("book [-w | --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 ") .option("writer", "--anonymous", {"id": 0}) .usage("book [-w | --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 " - name: writer opt: "--anonymous" default: id: 1 usage: book [-w | --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") ``` ================================================ FILE: website/docs/best-practice/alconna/uniseg/README.md ================================================ # 通用消息组件 `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) ``` ================================================ FILE: website/docs/best-practice/alconna/uniseg/_category_.json ================================================ { "label": "通用消息组件", "position": 5 } ================================================ FILE: website/docs/best-practice/alconna/uniseg/message.mdx ================================================ --- sidebar_position: 3 description: 消息序列 --- import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; # 通用消息序列 `uniseg` 提供了一个类似于 `Message` 的 `UniMessage` 类型,其元素为[通用消息段](./segment.md)。 你可以用如下方式获取 `UniMessage`: 通过提供的 `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) ... ``` 注意,`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) ``` ## 发送消息 你还可以通过 `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 = (attrs: dict, children: Segment[]) => T; type Transformer = boolean | Fragment | Render; ``` ### 字符串操作 类似于 `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 格式。 ================================================ FILE: website/docs/best-practice/alconna/uniseg/segment.md ================================================ --- 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 的处理方法。 ================================================ FILE: website/docs/best-practice/alconna/uniseg/utils.mdx ================================================ --- sidebar_position: 4 description: 辅助模型 --- import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; # 辅助功能 `uniseg` 模块同时提供了多种方法以通用消息操作。 :::note 这些方法中与 `event`, `bot` 相关的参数都会尝试从上下文中获取对象。 ::: ## 消息事件 ID 消息事件 ID 是用来标识当前消息事件的唯一 ID,通常用于回复/撤回/编辑/表态当前消息。 通过提供的 `MessageId` 或 `MsgId` 依赖注入器来获取消息事件 id。 ```python from nonebot_plugin_alconna.uniseg import MsgId matcher = on_xxx(...) @matcher.handle() asycn def _(msg_id: MsgId): ... ``` ```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) ``` :::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] """额外信息,用于适配器扩展""" ``` 通过提供的 `MessageTarget` 或 `MsgTarget` 依赖注入器来获取消息发送对象。 ```python from nonebot_plugin_alconna.uniseg import MsgTarget matcher = on_xxx(...) @matcher.handle() asycn def _(target: MsgTarget): ... ``` ```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) ``` 主动构造一个发送对象时,则需要如下参数: - `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()) ``` ================================================ FILE: website/docs/best-practice/data-storing.md ================================================ --- sidebar_position: 1 description: 存储数据文件到本地 --- # 数据存储 在使用插件的过程中,难免会需要存储一些持久化数据,例如用户的个人信息、群组的信息等。除了使用数据库等第三方存储之外,还可以使用本地文件来自行管理数据。NoneBot 提供了 `nonebot-plugin-localstore` 插件,可用于获取正确的数据存储路径并写入数据。 ## 安装插件 在使用前请先安装 `nonebot-plugin-localstore` 插件至项目环境中,可参考[获取商店插件](../tutorial/store.mdx#安装插件)来了解并选择安装插件的方式。如: 在**项目目录**下执行以下命令: ```bash nb plugin install nonebot-plugin-localstore ``` ## 使用插件 `nonebot-plugin-localstore` 插件兼容 Windows、Linux 和 macOS 等操作系统,使用时无需关心操作系统的差异。同时插件提供 `nb-cli` 脚本,可以使用 `nb localstore` 命令来检查数据存储路径。 在使用本插件前同样需要使用 `require` 方法进行**加载**并**导入**需要使用的方法,可参考 [跨插件访问](../advanced/requiring.md) 一节进行了解,如: ```python from nonebot import require require("nonebot_plugin_localstore") import nonebot_plugin_localstore as store # 获取插件缓存目录 cache_dir = store.get_plugin_cache_dir() # 获取插件缓存文件 cache_file = store.get_plugin_cache_file("file_name") # 获取插件数据目录 data_dir = store.get_plugin_data_dir() # 获取插件数据文件 data_file = store.get_plugin_data_file("file_name") # 获取插件配置目录 config_dir = store.get_plugin_config_dir() # 获取插件配置文件 config_file = store.get_plugin_config_file("file_name") ``` :::danger 警告 在 Windows 和 macOS 系统下,插件的数据目录和配置目录是同一个目录,因此在使用时需要注意避免文件名冲突。 ::: 插件提供的方法均返回一个 `pathlib.Path` 路径,可以参考 [pathlib 文档](https://docs.python.org/zh-cn/3/library/pathlib.html)来了解如何使用。常用的方法有: ```python from pathlib import Path data_file = store.get_plugin_data_file("file_name") # 写入文件内容 data_file.write_text("Hello World!") # 读取文件内容 data = data_file.read_text() ``` :::note 提示 对于嵌套插件,子插件的存储目录将位于父插件存储目录下。 ::: ## 配置项 ### localstore_use_cwd 使用当前工作目录作为数据存储目录,以下数据目录配置项默认值将会对应变更 默认值:`False` ```dotenv LOCALSTORE_USE_CWD=true ``` ### localstore_cache_dir 自定义缓存目录 默认值: 当 `localstore_use_cwd` 为 `True` 时,缓存目录为 `/cache`,否则: - macOS: `~/Library/Caches/nonebot2` - Unix: `~/.cache/nonebot2` (XDG default) - Windows: `C:\Users\\AppData\Local\nonebot2\Cache` ```dotenv LOCALSTORE_CACHE_DIR=/tmp/cache ``` ### localstore_data_dir 自定义数据目录 默认值: 当 `localstore_use_cwd` 为 `True` 时,数据目录为 `/data`,否则: - macOS: `~/Library/Application Support/nonebot2` - Unix: `~/.local/share/nonebot2` or in $XDG_DATA_HOME, if defined - Win XP (not roaming): `C:\Documents and Settings\\Application Data\nonebot2` - Win 7 (not roaming): `C:\Users\\AppData\Local\nonebot2` ```dotenv LOCALSTORE_DATA_DIR=/tmp/data ``` ### localstore_config_dir 自定义配置目录 默认值: 当 `localstore_use_cwd` 为 `True` 时,配置目录为 `/config`,否则: - macOS: same as user_data_dir - Unix: `~/.config/nonebot2` - Win XP (roaming): `C:\Documents and Settings\\Local Settings\Application Data\nonebot2` - Win 7 (roaming): `C:\Users\\AppData\Roaming\nonebot2` ```dotenv LOCALSTORE_CONFIG_DIR=/tmp/config ``` ### localstore_plugin_cache_dir 自定义插件缓存目录 默认值:`{}` ```dotenv LOCALSTORE_PLUGIN_CACHE_DIR=' { "plugin_id": "/tmp/plugin_cache" } ' ``` ### localstore_plugin_data_dir 自定义插件数据目录 默认值:`{}` ```dotenv LOCALSTORE_PLUGIN_DATA_DIR=' { "plugin_id": "/tmp/plugin_data" } ' ``` ### localstore_plugin_config_dir 自定义插件配置目录 默认值:`{}` ```dotenv LOCALSTORE_PLUGIN_CONFIG_DIR=' { "plugin_id": "/tmp/plugin_config" } ' ``` ================================================ FILE: website/docs/best-practice/database/README.mdx ================================================ import TabItem from "@theme/TabItem"; import Tabs from "@theme/Tabs"; # 数据库 [`nonebot-plugin-orm`](https://github.com/nonebot/plugin-orm) 是 NoneBot 的数据库支持插件。 本插件基于 [SQLAlchemy](https://www.sqlalchemy.org/) 和 [Alembic](https://alembic.sqlalchemy.org/),提供了许多与 NoneBot 紧密集成的功能: - 多 Engine / Connection 支持 - Session 管理 - 关系模型管理、依赖注入支持 - 数据库迁移 ## 安装 ```shell nb plugin install nonebot-plugin-orm ``` ```shell pip install nonebot-plugin-orm ``` ```shell pdm add nonebot-plugin-orm ``` ## 数据库驱动和后端 本插件只提供了 ORM 功能,没有数据库后端,也没有直接连接数据库后端的能力。 所以你需要另行安装数据库驱动和数据库后端,并且配置数据库连接信息。 ### SQLite [SQLite](https://www.sqlite.org/) 是一个轻量级的嵌入式数据库,它的数据以单文件的形式存储在本地,不需要单独的数据库后端。 SQLite 非常适合用于开发环境和小型应用,但是不适合用于大型应用的生产环境。 虽然不需要另行安装数据库后端,但你仍然需要安装数据库驱动: ```shell pip install "nonebot-plugin-orm[sqlite]" ``` ```shell pdm add "nonebot-plugin-orm[sqlite]" ``` 默认情况下,数据库文件为 `/nonebot-plugin-orm/db.sqlite3`(数据目录由 [nonebot-plugin-localstore](../data-storing) 提供)。 或者,你可以通过配置 `SQLALCHEMY_DATABASE_URL` 来指定数据库文件路径: ```shell SQLALCHEMY_DATABASE_URL=sqlite+aiosqlite:///file_path ``` ### PostgreSQL [PostgreSQL](https://www.postgresql.org/) 是世界上最先进的开源关系数据库之一,对各种高级且广泛应用的功能有最好的支持,是中小型应用的首选数据库。 ```shell pip install nonebot-plugin-orm[postgresql] ``` ```shell pdm add nonebot-plugin-orm[postgresql] ``` ```shell SQLALCHEMY_DATABASE_URL=postgresql+psycopg://user:password@host:port/dbname[?key=value&key=value...] ``` ### MySQL / MariaDB [MySQL](https://www.mysql.com/) 和 [MariaDB](https://mariadb.com/) 是经典的开源关系数据库,适合用于中小型应用。 ```shell pip install nonebot-plugin-orm[mysql] ``` ```shell pdm add nonebot-plugin-orm[mysql] ``` ```shell SQLALCHEMY_DATABASE_URL=mysql+aiomysql://user:password@host:port/dbname[?key=value&key=value...] ``` ## 使用 本插件提供了数据库迁移功能(此功能依赖于 [nb-cli 脚手架](../../quick-start#安装脚手架))。 在安装了新的插件或机器人之后,你需要执行一次数据库迁移操作,将数据库同步至与机器人一致的状态: ```shell nb orm upgrade ``` 运行完毕后,可以检查一下: ```shell nb orm check ``` 如果输出是 `没有检测到新的升级操作`,那么恭喜你,数据库已经迁移完成了,你可以启动机器人了。 ================================================ FILE: website/docs/best-practice/database/_category_.json ================================================ { "label": "数据库", "position": 7 } ================================================ FILE: website/docs/best-practice/database/developer/README.md ================================================ # 开发者指南 开发者指南内容较多,故分为了一个示例以及数个专题。 阅读(并且最好跟随实践)示例后,你将会对使用 `nonebot-plugin-orm` 开发插件有一个基本的认识。 如果想要更深入地学习关于 [SQLAlchemy](https://www.sqlalchemy.org/) 和 [Alembic](https://alembic.sqlalchemy.org/) 的知识,或者在使用过程中遇到了问题,可以查阅专题以及其官方文档。 ## 示例 ### 模型定义 首先,我们需要设计存储的数据的结构。 例如天气插件,需要存储**什么地方 (`location`)** 的**天气是什么 (`weather`)**。 其中,一个地方只会有一种天气,而不同地方可能有相同的天气。 所以,我们可以设计出如下的模型: ```python title=weather/__init__.py showLineNumbers from nonebot_plugin_orm import Model from sqlalchemy.orm import Mapped, mapped_column class Weather(Model): location: Mapped[str] = mapped_column(primary_key=True) weather: Mapped[str] ``` 其中,`primary_key=True` 意味着此列 (`location`) 是主键,即内容是唯一的且非空的。 每一个模型必须有至少一个主键。 我们可以用以下代码检查模型生成的数据库模式是否正确: ```python from sqlalchemy.schema import CreateTable print(CreateTable(Weather.__table__)) ``` ```sql CREATE TABLE weather_weather ( location VARCHAR NOT NULL, weather VARCHAR NOT NULL, CONSTRAINT pk_weather_weather PRIMARY KEY (location) ) ``` 可以注意到表名是 `weather_weather` 而不是 `Weather` 或者 `weather`。 这是因为 `nonebot-plugin-orm` 会自动为模型生成一个表名,规则是:`<插件模块名>_<类名小写>`。 你也可以通过指定 `__tablename__` 属性来自定义表名: ```python {2} class Weather(Model): __tablename__ = "weather" ... ``` ```sql {1} CREATE TABLE weather ( ... ) ``` 但是,并不推荐你这么做,因为这可能会导致不同插件间的表名重复,引发冲突。 特别是当你会发布插件时,你并不知道其他插件会不会使用相同的表名。 ### 首次迁移 我们成功定义了模型,现在启动机器人试试吧: ```shell $ nb run 01-02 15:04:05 [SUCCESS] nonebot | NoneBot is initializing... 01-02 15:04:05 [ERROR] nonebot_plugin_orm | 启动检查失败 01-02 15:04:05 [ERROR] nonebot | Application startup failed. Exiting. Traceback (most recent call last): ... click.exceptions.UsageError: 检测到新的升级操作: [('add_table', Table('weather', MetaData(), Column('location', String(), table=, primary_key=True, nullable=False), Column('weather', String(), table=, nullable=False), schema=None))] ``` 咦,发生了什么? `nonebot-plugin-orm` 试图阻止我们启动机器人。 原来是我们定义了模型,但是数据库中并没有对应的表,这会导致插件不能正常运行。 所以,我们需要迁移数据库。 首先,我们需要创建一个迁移脚本: ```shell nb orm revision -m "first revision" --branch-label weather ``` 其中,`-m` 参数是迁移脚本的描述,`--branch-label` 参数是迁移脚本的分支,一般为插件模块名。 执行命令过后,出现了一个 `weather/migrations` 目录,其中有一个 `xxxxxxxxxxxx_first_revision.py` 文件: ```shell {4,5} weather ├── __init__.py ├── config.py └── migrations └── xxxxxxxxxxxx_first_revision.py ``` 这就是我们创建的迁移脚本,它记录了数据库模式的变化。 我们可以查看一下它的内容: ```python title=weather/migrations/xxxxxxxxxxxx_first_revision.py {25-33,39-41} showLineNumbers """first revision 迁移 ID: xxxxxxxxxxxx 父迁移: 创建时间: 2006-01-02 15:04:05.999999 """ from __future__ import annotations from collections.abc import Sequence import sqlalchemy as sa from alembic import op revision: str = "xxxxxxxxxxxx" down_revision: str | Sequence[str] | None = None branch_labels: str | Sequence[str] | None = ("weather",) depends_on: str | Sequence[str] | None = None def upgrade(name: str = "") -> None: if name: return # ### commands auto generated by Alembic - please adjust! ### op.create_table( "weather_weather", sa.Column("location", sa.String(), nullable=False), sa.Column("weather", sa.String(), nullable=False), sa.PrimaryKeyConstraint("location", name=op.f("pk_weather_weather")), info={"bind_key": "weather"}, ) # ### end Alembic commands ### def downgrade(name: str = "") -> None: if name: return # ### commands auto generated by Alembic - please adjust! ### op.drop_table("weather_weather") # ### end Alembic commands ### ``` 可以注意到脚本的主体部分(其余是模版代码,请勿修改)是: ```python # ### commands auto generated by Alembic - please adjust! ### op.create_table( # CREATE TABLE "weather_weather", # weather_weather sa.Column("location", sa.String(), nullable=False), # location VARCHAR NOT NULL, sa.Column("weather", sa.String(), nullable=False), # weather VARCHAR NOT NULL, sa.PrimaryKeyConstraint("location", name=op.f("pk_weather_weather")), # CONSTRAINT pk_weather_weather PRIMARY KEY (location) info={"bind_key": "weather"}, ) # ### end Alembic commands ### ``` ```python # ### commands auto generated by Alembic - please adjust! ### op.drop_table("weather_weather") # DROP TABLE weather_weather; # ### end Alembic commands ### ``` 虽然我们不是很懂这些代码的意思,但是可以注意到它们几乎与 SQL 语句 (DDL) 一一对应。 显然,它们是用来创建和删除表的。 我们还可以注意到,`upgrade()` 和 `downgrade()` 函数中的代码是**互逆**的。 也就是说,执行一次 `upgrade()` 函数,再执行一次 `downgrade()` 函数后,数据库的模式就会回到原来的状态。 这就是迁移脚本的作用:记录数据库模式的变化,以便我们在不同的环境中(例如开发环境和生产环境)**可复现地**、**可逆地**同步数据库模式,正如 git 对我们的代码做的事情那样。 对了,不要忘记还有一段注释:`commands auto generated by Alembic - please adjust!`。 它在提醒我们,这些代码是由 Alembic 自动生成的,我们应该检查它们,并且根据需要进行调整。 :::caution 注意 迁移脚本冗长且繁琐,我们一般不会手写它们,而是由 Alembic 自动生成。 一般情况下,Alembic 足够智能,可以正确地生成迁移脚本。 但是,在复杂或有歧义的情况下,我们可能需要手动调整迁移脚本。 所以,**永远要检查迁移脚本,并且在开发环境中测试!** **迁移脚本中任何一处错误都足以使数据付之东流!** ::: 确定迁移脚本正确后,我们就可以执行迁移脚本,将数据库模式同步到数据库中: ```shell nb orm upgrade ``` 现在,我们可以正常启动机器人了。 开发过程中,我们可能会频繁地修改模型,这意味着我们需要频繁地创建并执行迁移脚本,非常繁琐。 实际上,此时我们不在乎数据安全,只需要数据库模式与模型定义一致即可。 所以,我们可以关闭 `nonebot-plugin-orm` 的启动检查: ```shell title=.env.dev ALEMBIC_STARTUP_CHECK=false ``` 现在,每次启动机器人时,数据库模式会自动与模型定义同步,无需手动迁移。 ### 会话管理 我们已经成功定义了模型,并且迁移了数据库,现在可以开始使用数据库了……吗? 并不能,因为模型只是数据结构的定义,并不能通过它操作数据(如果你曾经使用过 [Tortoise ORM](https://tortoise.github.io/),可能会知道 `await Weather.get(location="上海")` 这样的面向对象编程。 但是 SQLAlchemy 不同,选择了命令式编程)。 我们需要使用**会话**操作数据: ```python title=weather/__init__.py {10,13} showLineNumbers from nonebot import on_command from nonebot.adapters import Message from nonebot.params import CommandArg from nonebot_plugin_orm import async_scoped_session weather = on_command("天气") @weather.handle() async def _(session: async_scoped_session, args: Message = CommandArg()): location = args.extract_plain_text() if wea := await session.get(Weather, location): await weather.finish(f"今天{location}的天气是{wea.weather}") await weather.finish(f"未查询到{location}的天气") ``` 我们通过 `session: async_scoped_session` 依赖注入获得了一个会话,然后使用 `await session.get(Weather, location)` 查询数据库。 `async_scoped_session` 是一个有作用域限制的会话,作用域为当前事件、当前事件响应器。 会话产生的模型实例(例如此处的 `wea := await session.get(Weather, location)`)作用域与会话相同。 :::caution 注意 此处提到的“会话”指的是 ORM 会话,而非 [NoneBot 会话](../../../appendices/session-control),两者的生命周期也是不同的(NoneBot 会话的生命周期中可能包含多个事件,不同的事件也会有不同的事件响应器)。 具体而言,就是不要将 ORM 会话和模型实例存储在 NoneBot 会话状态中: ```python {12} from nonebot.params import ArgPlainText from nonebot.typing import T_State @weather.got("location", prompt="请输入地名") async def _(state: T_State, session: async_scoped_session, location: str = ArgPlainText()): wea = await session.get(Weather, location) if not wea: await weather.finish(f"未查询到{location}的天气") state["weather"] = wea # 不要这么做,除非你知道自己在做什么 ``` 当然非要这么做也不是不可以: ```python {6} @weather.handle() async def _(state: T_State, session: async_scoped_session): # 通过 await session.merge(state["weather"]) 获得了此 ORM 会话中的相应模型实例, # 而非直接使用会话状态中的模型实例, # 因为先前的 ORM 会话已经关闭了。 wea = await session.merge(state["weather"]) await weather.finish(f"今天{state['location']}的天气是{wea.weather}") ``` ::: 当有数据更改时,我们需要提交事务,也要注意会话作用域问题: ```python title=weather/__init__.py {12,20} showLineNumbers from nonebot.params import Depends async def get_weather( session: async_scoped_session, args: Message = CommandArg() ) -> Weather: location = args.extract_plain_text() if not (wea := await session.get(Weather, location)): wea = Weather(location=location, weather="未知") session.add(wea) # await session.commit() # 不应该在其他地方提交事务 return wea @weather.handle() async def _(session: async_scoped_session, wea: Weather = Depends(get_weather)): await weather.send(f"今天的天气是{wea.weather}") await session.commit() # 而应该在事件响应器结束前提交事务 ``` 当然我们也可以获得一个新的会话,不过此时就要手动管理会话了: ```python title=weather/__init__.py {5-6} showLineNumbers from nonebot_plugin_orm import get_session async def get_weather(location: str) -> str: session = get_session() async with session.begin(): wea = await session.get(Weather, location) if not wea: wea = Weather(location=location, weather="未知") session.add(wea) return wea.weather @weather.handle() async def _(args: Message = CommandArg()): wea = await get_weather(args.extract_plain_text()) await weather.send(f"今天的天气是{wea}") ``` ### 依赖注入 在上面的示例中,我们都是通过会话获得数据的。 不过,我们也可以通过依赖注入获得数据: ```python title=weather/__init__.py {12-14} showLineNumbers from sqlalchemy import select from nonebot.params import Depends from nonebot_plugin_orm import SQLDepends def extract_arg_plain_text(args: Message = CommandArg()) -> str: return args.extract_plain_text() @weather.handle() async def _( wea: Weather = SQLDepends( select(Weather).where(Weather.location == Depends(extract_arg_plain_text)) ), ): await weather.send(f"今天的天气是{wea.weather}") ``` 其中,`SQLDepends` 是一个特殊的依赖注入,它会根据类型标注和 SQL 语句提供数据,SQL 语句中也可以有子依赖。 不同的类型标注也会获得不同形式的数据: ```python title=weather/__init__.py {5} showLineNumbers from collections.abc import Sequence @weather.handle() async def _( weas: Sequence[Weather] = SQLDepends( select(Weather).where(Weather.weather == Depends(extract_arg_plain_text)) ), ): await weather.send(f"今天的天气是{weas[0].weather}的城市有{','.join(wea.location for wea in weas)}") ``` 支持的类型标注请参见 [依赖注入](dependency)。 我们也可以像 [类作为依赖](../../../advanced/dependency#类作为依赖) 那样,在类属性中声明子依赖: ```python title=weather/__init__.py {5-6,10} showLineNumbers from collections.abc import Sequence class Weather(Model): location: Mapped[str] = mapped_column(primary_key=True) weather: Mapped[str] = Depends(extract_arg_plain_text) # weather: Annotated[Mapped[str], Depends(extract_arg_plain_text)] # Annotated 支持 @weather.handle() async def _(weas: Sequence[Weather]): await weather.send( f"今天的天气是{weas[0].weather}的城市有{','.join(wea.location for wea in weas)}" ) ``` ================================================ FILE: website/docs/best-practice/database/developer/_category_.json ================================================ { "label": "开发者指南", "position": 3 } ================================================ FILE: website/docs/best-practice/database/developer/dependency.md ================================================ --- sidebar_position: 3 description: 依赖注入 --- # 依赖注入 `nonebot-plugin-orm` 提供了强大且灵活的依赖注入,可以方便地帮助你获取数据库会话和查询数据。 ## 数据库会话 ### AsyncSession 新数据库会话,常用于有独立的数据库操作逻辑的插件。 ```python {13,26} from nonebot import on_message from nonebot.params import Depends from nonebot_plugin_orm import AsyncSession, Model, async_scoped_session from sqlalchemy.orm import Mapped, mapped_column message = on_message() class Message(Model): id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) async def get_message(session: AsyncSession) -> Message: # 等价于 session = get_session() async with session: msg = Message() session.add(msg) await session.commit() await session.refresh(msg) return msg @message.handle() async def _(session: async_scoped_session, msg: Message = Depends(get_message)): await session.rollback() # 无法回退 get_message() 中的更改 await message.send(str(msg.id)) # msg 被存储,msg.id 递增 ``` ### async_scoped_session 数据库作用域会话,常用于事件响应器和有与响应逻辑相关的数据库操作逻辑的插件。 ```python {13,26} from nonebot import on_message from nonebot.params import Depends from nonebot_plugin_orm import Model, async_scoped_session from sqlalchemy.orm import Mapped, mapped_column message = on_message() class Message(Model): id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) async def get_message(session: async_scoped_session) -> Message: # 等价于 session = get_scoped_session() msg = Message() session.add(msg) await session.flush() await session.refresh(msg) return msg @message.handle() async def _(session: async_scoped_session, msg: Message = Depends(get_message)): await session.rollback() # 可以回退 get_message() 中的更改 await message.send(str(msg.id)) # msg 没有被存储,msg.id 不变 ``` ## 查询数据 ### Model 支持类作为依赖。 ```python from typing import Annotated from nonebot.params import Depends from nonebot_plugin_orm import Model from sqlalchemy.orm import Mapped, mapped_column def get_id() -> int: ... class Message(Model): id: Annotated[Mapped[int], Depends(get_id)] = mapped_column( primary_key=True, autoincrement=True ) async def _(msg: Message): # 等价于 msg = ( # await (await session.stream(select(Message).where(Message.id == get_id()))) # .scalars() # .one_or_none() # ) ... ``` ### SQLDepends 参数为一个 SQL 语句,决定依赖注入的内容,SQL 语句中可以使用子依赖。 ```python {11-13} from nonebot.params import Depends from nonebot_plugin_orm import Model, SQLDepends from sqlalchemy import select def get_id() -> int: ... async def _( model: Model = SQLDepends(select(Model).where(Model.id == Depends(get_id))), ): ... ``` 参数可以是任意 SQL 语句,但不建议使用 `select` 以外的语句,因为语句可能没有返回值(`returning` 除外),而且代码不清晰。 ### 类型标注 类型标注决定依赖注入的数据结构,主要影响以下几个层面: - 迭代器(`session.execute()`)或异步迭代器(`session.stream()`) - 标量(`session.execute().scalars()`)或元组(`session.execute()`) - 一个(`session.execute().one_or_none()`,注意 `None` 时可能触发 [重载](../../../appendices/overload#重载))或全部(`session.execute()` / `session.execute().all()`) - 连续(`session().execute()`)或分块(`session.execute().partitions()`) 具体如下(可以使用父类型作为类型标注): - ```python async def _(rows_partitions: AsyncIterator[Sequence[Tuple[Model, ...]]]): # 等价于 rows_partitions = await (await session.stream(sql).partitions()) async for partition in rows_partitions: for row in partition: print(row[0], row[1], ...) ``` - ```python async def _(model_partitions: AsyncIterator[Sequence[Model]]): # 等价于 model_partitions = await (await session.stream(sql).scalars().partitions()) async for partition in model_partitions: for model in partition: print(model) ``` - ```python async def _(row_partitions: Iterator[Sequence[Tuple[Model, ...]]]): # 等价于 row_partitions = await session.execute(sql).partitions() for partition in rows_partitions: for row in partition: print(row[0], row[1], ...) ``` - ```python async def _(model_partitions: Iterator[Sequence[Model]]): # 等价于 model_partitions = await (await session.execute(sql).scalars().partitions()) for partition in model_partitions: for model in partition: print(model) ``` - ```python async def _(rows: sa_async.AsyncResult[Tuple[Model, ...]]): # 等价于 rows = await session.stream(sql) async for row in rows: print(row[0], row[1], ...) ``` - ```python async def _(models: sa_async.AsyncScalarResult[Model]): # 等价于 models = await session.stream(sql).scalars() async for model in models: print(model) ``` - ```python async def _(rows: sa.Result[Tuple[Model, ...]]): # 等价于 rows = await session.execute(sql) for row in rows: print(row[0], row[1], ...) ``` - ```python async def _(models: sa.ScalarResult[Model]): # 等价于 models = await session.execute(sql).scalars() for model in models: print(model) ``` - ```python async def _(rows: Sequence[Tuple[Model, ...]]): # 等价于 rows = await (await session.stream(sql).all()) for row in rows: print(row[0], row[1], ...) ``` - ```python async def _(models: Sequence[Model]): # 等价于 models = await (await session.stream(sql).scalars().all()) for model in models: print(model) ``` - ```python async def _(row: Tuple[Model, ...]): # 等价于 row = await (await session.stream(sql).one_or_none()) print(row[0], row[1], ...) ``` - ```python async def _(model: Model): # 等价于 model = await (await session.stream(sql).scalars().one_or_none()) print(model) ``` ================================================ FILE: website/docs/best-practice/database/developer/test.md ================================================ --- sidebar_position: 2 description: 测试 --- # 测试 百思不如一试,测试是发现问题的最佳方式。 不同的用户会有不同的配置,为了提高项目的兼容性,我们需要在不同数据库后端上测试。 手动进行大量的、重复的测试不可靠,也不现实,因此我们推荐使用 [GitHub Actions](https://github.com/features/actions) 进行自动化测试: ```yaml title=.github/workflows/test.yml {12-42,52-53} showLineNumbers name: Test on: push: branches: - main jobs: test: runs-on: ubuntu-latest strategy: matrix: db: - sqlite+aiosqlite:///db.sqlite3 - postgresql+psycopg://postgres:postgres@localhost:5432/postgres - mysql+aiomysql://mysql:mysql@localhost:3306/mymysql fail-fast: false env: SQLALCHEMY_DATABASE_URL: ${{ matrix.db }} services: postgresql: image: ${{ startsWith(matrix.db, 'postgresql') && 'postgres' || '' }} env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: postgres ports: - 5432:5432 mysql: image: ${{ startsWith(matrix.db, 'mysql') && 'mysql' || '' }} env: MYSQL_ROOT_PASSWORD: mysql MYSQL_USER: mysql MYSQL_PASSWORD: mysql MYSQL_DATABASE: mymysql ports: - 3306:3306 steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 - name: Install dependencies run: pip install -r requirements.txt - name: Run migrations run: pipx run nb-cli orm upgrade - name: Run tests run: pytest ``` 如果项目还需要考虑跨平台和跨 Python 版本兼容,测试矩阵中还需要增加这两个维度。 但是,我们没必要在所有平台和 Python 版本上运行所有数据库的测试,因为很显然,PostgreSQL 和 MySQL 这类独立的数据库后端不会受平台和 Python 影响,而且 Github Actions 的非 Linux 平台不支持运行独立服务: | | Python 3.9 | Python 3.10 | Python 3.11 | Python 3.12 | | ----------- | ---------- | ----------- | ----------- | --------------------------- | | **Linux** | SQLite | SQLite | SQLite | SQLite / PostgreSQL / MySQL | | **Windows** | SQLite | SQLite | SQLite | SQLite | | **macOS** | SQLite | SQLite | SQLite | SQLite | ```yaml title=.github/workflows/test.yml {12-24} showLineNumbers name: Test on: push: branches: - main jobs: test: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] python-version: ["3.9", "3.10", "3.11", "3.12"] db: ["sqlite+aiosqlite:///db.sqlite3"] include: - os: ubuntu-latest python-version: "3.12" db: postgresql+psycopg://postgres:postgres@localhost:5432/postgres - os: ubuntu-latest python-version: "3.12" db: mysql+aiomysql://mysql:mysql@localhost:3306/mymysql fail-fast: false env: SQLALCHEMY_DATABASE_URL: ${{ matrix.db }} services: postgresql: image: ${{ startsWith(matrix.db, 'postgresql') && 'postgres' || '' }} env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: postgres ports: - 5432:5432 mysql: image: ${{ startsWith(matrix.db, 'mysql') && 'mysql' || '' }} env: MYSQL_ROOT_PASSWORD: mysql MYSQL_USER: mysql MYSQL_PASSWORD: mysql MYSQL_DATABASE: mymysql ports: - 3306:3306 steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: pip install -r requirements.txt - name: Run migrations run: pipx run nb-cli orm upgrade - name: Run tests run: pytest ``` ================================================ FILE: website/docs/best-practice/database/user.md ================================================ --- sidebar_position: 2 description: 用户指南 --- # 用户指南 `nonebot-plugin-orm` 功能强大且复杂,使用上有一定难度。 不过,对于用户而言,只需要掌握部分功能即可。 :::caution 注意 请注意区分插件的项目名(如:`nonebot-plugin-wordcloud`)和模块名(如:`nonebot_plugin_wordcloud`)。`nonebot-plugin-orm` 中统一使用插件模块名。参见 [插件命名规范](../../developer/plugin-publishing#插件命名规范)。 ::: ## 示例 ### 创建新机器人 我们想要创建一个机器人,并安装 `nonebot-plugin-wordcloud` 插件,只需要执行以下命令: ```shell nb init # 初始化项目文件夹 pip install nonebot-plugin-orm[sqlite] # 安装 nonebot-plugin-orm,并附带 SQLite 支持 nb plugin install nonebot-plugin-wordcloud # 安装插件 # nb orm heads # 查看有什么插件使用到了数据库(可选) nb orm upgrade # 升级数据库 # nb orm check # 检查一下数据库模式是否与模型定义一致(可选) nb run # 启动机器人 ``` ### 卸载插件 我们已经安装了 `nonebot-plugin-wordcloud` 插件,但是现在想要卸载它,并且**删除它的数据**,只需要执行以下命令: ```shell nb plugin uninstall nonebot-plugin-wordcloud # 卸载插件 # nb orm heads # 查看有什么插件使用到了数据库。(可选) nb orm downgrade nonebot_plugin_wordcloud@base # 降级数据库,删除数据 # nb orm check # 检查一下数据库模式是否与模型定义一致(可选) ``` ## CLI 接下来,让我们了解下示例中出现的 CLI 命令的含义: ### heads 显示所有的分支头。一般一个分支对应一个插件。 ```shell nb orm heads ``` 输出格式为 `<迁移 ID> (<插件模块名>) (<头部类型>)`: ``` 46327b837dd8 (nonebot_plugin_chatrecorder) (head) 9492159f98f7 (nonebot_plugin_user) (head) 71a72119935f (nonebot_plugin_session_orm) (effective head) ade8cdca5470 (nonebot_plugin_wordcloud) (head) ``` ### upgrade 升级数据库。每次安装新的插件或更新插件版本后,都需要执行此命令。 ```shell nb orm upgrade <插件模块名>@<迁移 ID> ``` 其中,`<插件模块名>@<迁移 ID>` 是可选参数。如果不指定,则会将所有分支升级到最新版本,这也是最常见的用法: ```shell nb orm upgrade ``` ### downgrade 降级数据库。当需要回滚插件版本或删除插件时,可以执行此命令。 ```shell nb orm downgrade <插件模块名>@<迁移 ID> ``` 其中,`<迁移 ID>` 也可以是 `base`,即回滚到初始状态。常用于卸载插件后删除其数据: ```shell nb orm downgrade <插件模块名>@base ``` ### check 检查数据库模式是否与模型定义一致。机器人启动前会自动运行此命令(`ALEMBIC_STARTUP_CHECK=true` 时),并在检查失败时阻止启动。 ```shell nb orm check ``` ## 配置 ### sqlalchemy_database_url 默认数据库连接 URL。参见 [数据库驱动和后端](.#数据库驱动和后端) 和 [引擎配置 — SQLAlchemy 2.0 文档](https://docs.sqlalchemy.org/en/20/core/engines.html#database-urls)。 ```shell SQLALCHEMY_DATABASE_URL=dialect+driver://username:password@host:port/database ``` ### sqlalchemy_bind bind keys(一般为插件模块名)到数据库连接 URL、[`create_async_engine()`](https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html#sqlalchemy.ext.asyncio.create_async_engine) 参数字典或 [`AsyncEngine`](https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html#sqlalchemy.ext.asyncio.AsyncEngine) 实例的字典。 例如,我们想要让 `nonebot-plugin-wordcloud` 插件使用一个 SQLite 数据库,并开启 [Echo 选项](https://docs.sqlalchemy.org/en/20/core/engines.html#sqlalchemy.create_engine.params.echo) 便于 debug,而其他插件使用默认的 PostgreSQL 数据库,可以这样配置: ```shell SQLALCHEMY_BINDS='{ "": "postgresql+psycopg://scott:tiger@localhost/mydatabase", "nonebot_plugin_wordcloud": { "url": "sqlite+aiosqlite://", "echo": true } }' ``` ### sqlalchemy_engine_options [`create_async_engine()`](https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html#sqlalchemy.ext.asyncio.create_async_engine) 默认参数字典。 ```shell SQLALCHEMY_ENGINE_OPTIONS='{ "pool_size": 5, "max_overflow": 10, "pool_timeout": 30, "pool_recycle": 3600, "echo": true }' ``` ### sqlalchemy_echo 开启 [Echo 选项](https://docs.sqlalchemy.org/en/20/core/engines.html#sqlalchemy.create_engine.params.echo) 和 [Echo Pool 选项](https://docs.sqlalchemy.org/en/20/core/engines.html#sqlalchemy.create_engine.params.echo_pool) 便于 debug。 ```shell SQLALCHEMY_ECHO=true ``` :::caution 注意 以上配置之间有覆盖关系,遵循特殊优先于一般的原则,具体为 [`sqlalchemy_database_url`](#sqlalchemy_database_url) > [`sqlalchemy_bind`](#sqlalchemy_bind) > [`sqlalchemy_echo`](#sqlalchemy_echo) > [`sqlalchemy_engine_options`](#sqlalchemy_engine_options)。 但覆盖顺序并非显而易见,出于清晰考虑,请只配置必要的选项。 ::: ================================================ FILE: website/docs/best-practice/deployment.mdx ================================================ --- sidebar_position: 3 description: 部署你的机器人 --- # 部署 import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; 在编写完成各类插件后,我们需要长期运行机器人来使得用户能够正常使用。通常,我们会使用云服务器来部署机器人。 我们在开发插件时,机器人运行的环境称为开发环境;而在部署后,机器人运行的环境称为生产环境。与开发环境不同的是,在生产环境中,开发者通常不能随意地修改/添加/删除代码,开启或停止服务。 ## 部署前准备 ### 项目依赖管理 由于部署后的机器人运行在生产环境中,因此,为确保机器人能够正常运行,我们需要保证机器人的运行环境与开发环境一致。我们可以通过以下几种方式来进行依赖管理: [Poetry](https://python-poetry.org/) 是一个 Python 项目的依赖管理工具。它可以通过声明项目所依赖的库,为你管理(安装/更新)它们。Poetry 提供了一个 `poetry.lock` 文件,以确保可重复安装,并可以构建用于分发的项目。 Poetry 会在安装依赖时自动生成 `poetry.lock` 文件,在**项目目录**下执行以下命令: ```bash # 初始化 poetry 配置 poetry init # 添加项目依赖,这里以 nonebot2[fastapi] 为例 poetry add nonebot2[fastapi] ``` [PDM](https://pdm.fming.dev/) 是一个现代 Python 项目的依赖管理工具。它采用 [PEP621](https://www.python.org/dev/peps/pep-0621/) 标准,依赖解析快速;同时支持 [PEP582](https://www.python.org/dev/peps/pep-0582/) 和 [virtualenv](https://virtualenv.pypa.io/)。PDM 提供了一个 `pdm.lock` 文件,以确保可重复安装,并可以构建用于分发的项目。 PDM 会在安装依赖时自动生成 `pdm.lock` 文件,在**项目目录**下执行以下命令: ```bash # 初始化 pdm 配置 pdm init # 添加项目依赖,这里以 nonebot2[fastapi] 为例 pdm add nonebot2[fastapi] ``` [pip](https://pip.pypa.io/) 是 Python 包管理工具。他并不是一个依赖管理工具,为了尽可能保证环境的一致性,我们可以使用 `requirements.txt` 文件来声明依赖。 ```bash pip freeze > requirements.txt ``` ### 安装 Docker [Docker](https://www.docker.com/) 是一个应用容器引擎,可以让开发者打包应用以及依赖包到一个可移植的镜像中,然后发布到服务器上。 我们可以参考 [Docker 官方文档](https://docs.docker.com/get-docker/) 来安装 Docker 。 在 Linux 上,我们可以使用以下一键脚本来安装 Docker 以及 Docker Compose Plugin: ```bash curl -fsSL https://get.docker.com | sh -s -- --mirror Aliyun ``` 在 Windows/macOS 上,我们可以使用 [Docker Desktop](https://docs.docker.com/desktop/) 来安装 Docker 以及 Docker Compose Plugin。 ### 安装脚手架 Docker 插件 我们可以使用 [nb-cli-plugin-docker](https://github.com/nonebot/cli-plugin-docker) 来快速部署机器人。 插件可以帮助我们生成配置文件并构建 Docker 镜像,以及启动/停止/重启机器人。使用以下命令安装脚手架 Docker 插件: ```bash nb self install nb-cli-plugin-docker ``` ## Docker 部署 ### 快速部署 使用脚手架命令即可一键生成配置并部署: ```bash nb docker up ``` 当看到 `Running` 字样时,说明机器人已经启动成功。我们可以通过以下命令来查看机器人的运行日志: ```bash nb docker logs ``` ```bash docker compose logs ``` 如果需要停止机器人,我们可以使用以下命令: ```bash nb docker down ``` ```bash docker compose down ``` ### 自定义部署 在部分情况下,我们需要事先生成 Docker 配置文件,再到生产环境进行部署;或者自动生成的配置文件并不能满足复杂场景,需要根据实际需求手动修改配置文件。我们可以使用以下命令来生成基础配置文件: ```bash nb docker generate ``` nb-cli 将会在项目目录下生成 `docker-compose.yml` 和 `Dockerfile` 等配置文件。在 nb-cli 完成配置文件的生成后,我们可以根据部署环境的实际情况使用 nb-cli 或者 Docker Compose 来启动机器人。 我们可以参考 [Dockerfile 文件规范](https://docs.docker.com/engine/reference/builder/)和 [Compose 文件规范](https://docs.docker.com/compose/compose-file/)修改这两个文件。 修改完成后我们可以直接启动或者手动构建镜像: ```bash # 启动机器人 nb docker up # 手动构建镜像 nb docker build ``` ```bash # 启动机器人 docker compose up -d # 手动构建镜像 docker compose build ``` ### 持续集成 我们可以使用 GitHub Actions 来实现持续集成(CI),我们只需要在 GitHub 上发布 Release 即可自动构建镜像并推送至镜像仓库。 首先,我们需要在 [Docker Hub](https://hub.docker.com/) (或者其他平台,如:[GitHub Packages](https://github.com/features/packages)、[阿里云容器镜像服务](https://www.alibabacloud.com/zh/product/container-registry)等)上创建镜像仓库,用于存放镜像。 前往项目仓库的 `Settings` > `Secrets` > `actions` 栏目 `New Repository Secret` 添加构建所需的密钥: - `DOCKERHUB_USERNAME`: 你的 Docker Hub 用户名 - `DOCKERHUB_TOKEN`: 你的 Docker Hub PAT([创建方法](https://docs.docker.com/docker-hub/access-tokens/)) 将以下文件添加至**项目目录**下的 `.github/workflows/` 目录下,并将文件中高亮行中的仓库名称替换为你的仓库名称: ```yaml title=.github/workflows/build.yml name: Docker Hub Release on: push: tags: - "v*" jobs: docker: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 - name: Set up QEMU uses: docker/setup-qemu-action@v2 - name: Setup Docker uses: docker/setup-buildx-action@v2 - name: Login to DockerHub uses: docker/login-action@v2 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Generate Tags uses: docker/metadata-action@v4 id: metadata with: images: | # highlight-next-line {organization}/{repository} tags: | type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=sha type=raw,value=latest - name: Build and Publish uses: docker/build-push-action@v4 with: context: . push: true tags: ${{ steps.metadata.outputs.tags }} labels: ${{ steps.metadata.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max ``` ### 持续部署 在完成发布并构建镜像后,我们可以自动将镜像部署到服务器上。 前往项目仓库的 `Settings` > `Secrets` > `actions` 栏目 `New Repository Secret` 添加部署所需的密钥: - `DEPLOY_HOST`: 部署服务器的 SSH 地址 - `DEPLOY_USER`: 部署服务器用户名 - `DEPLOY_KEY`: 部署服务器私钥([创建方法](https://github.com/appleboy/ssh-action#setting-up-a-ssh-key)) - `DEPLOY_PATH`: 部署服务器上的项目路径 将以下文件添加至**项目目录**下的 `.github/workflows/` 目录下,在构建成功后触发部署: ```yaml title=.github/workflows/deploy.yml name: Deploy on: workflow_run: workflows: - Docker Hub Release types: - completed jobs: deploy: runs-on: ubuntu-latest if: ${{ github.event.workflow_run.conclusion == 'success' }} steps: - name: Start Deployment uses: bobheadxi/deployments@v1 id: deployment with: step: start token: ${{ secrets.GITHUB_TOKEN }} env: bot - name: Run Remote SSH Command uses: appleboy/ssh-action@master env: DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }} with: host: ${{ secrets.DEPLOY_HOST }} username: ${{ secrets.DEPLOY_USER }} key: ${{ secrets.DEPLOY_KEY }} envs: DEPLOY_PATH script: | cd $DEPLOY_PATH docker compose up -d --pull always - name: update deployment status uses: bobheadxi/deployments@v0.6.2 if: always() with: step: finish token: ${{ secrets.GITHUB_TOKEN }} status: ${{ job.status }} env: ${{ steps.deployment.outputs.env }} deployment_id: ${{ steps.deployment.outputs.deployment_id }} ``` 将上一部分的 `docker-compose.yml` 文件以及 `.env.prod` 配置文件添加至 `DEPLOY_PATH` 目录下,并修改 `docker-compose.yml` 文件中的镜像配置,替换为 Docker Hub 的仓库名称: ```diff - build: . + image: {organization}/{repository}:latest ``` ================================================ FILE: website/docs/best-practice/error-tracking.md ================================================ --- sidebar_position: 2 description: 使用 sentry 进行错误跟踪 --- # 错误跟踪 在应用实际运行过程中,可能会出现各种各样的错误。可能是由于代码逻辑错误,也可能是由于用户输入错误,甚至是由于第三方服务的错误。这些错误都会导致应用的运行出现问题,这时候就需要对错误进行跟踪,以便及时发现问题并进行修复。NoneBot 提供了 `nonebot-plugin-sentry` 插件,支持 [sentry](https://sentry.io/) 平台,可以方便地进行错误跟踪。 ## 安装插件 在使用前请先安装 `nonebot-plugin-sentry` 插件至项目环境中,可参考[获取商店插件](../tutorial/store.mdx#安装插件)来了解并选择安装插件的方式。如: 在**项目目录**下执行以下命令: ```bash nb plugin install nonebot-plugin-sentry ``` ## 使用插件 在安装完成之后,仅需要对插件进行简单的配置即可使用。 ### 获取 sentry DSN 前往 [sentry](https://sentry.io/) 平台,注册并创建一个新的项目,然后在项目设置中找到 `Client Keys (DSN)`,复制其中的 `DSN` 值。 ### 配置插件 :::caution 注意 错误跟踪通常在生产环境中使用,因此开发环境中 `sentry_dsn` 留空即会停用插件。 ::: 在项目 dotenv 配置文件中添加以下配置即可使用: ```dotenv SENTRY_DSN= ``` ## 配置项 配置项具体含义参考 [Sentry Docs](https://docs.sentry.io/platforms/python/configuration/options/)。 - `sentry_dsn: str` - `sentry_debug: bool = False` - `sentry_release: str | None = None` - `sentry_release: str | None = None` - `sentry_environment: str | None = nonebot env` - `sentry_server_name: str | None = None` - `sentry_sample_rate: float = 1.` - `sentry_max_breadcrumbs: int = 100` - `sentry_attach_stacktrace: bool = False` - `sentry_send_default_pii: bool = False` - `sentry_in_app_include: List[str] = Field(default_factory=list)` - `sentry_in_app_exclude: List[str] = Field(default_factory=list)` - `sentry_request_bodies: str = "medium"` - `sentry_with_locals: bool = True` - `sentry_ca_certs: str | None = None` - `sentry_before_send: Callable[[Any, Any], Any | None] | None = None` - `sentry_before_breadcrumb: Callable[[Any, Any], Any | None] | None = None` - `sentry_transport: Any | None = None` - `sentry_http_proxy: str | None = None` - `sentry_https_proxy: str | None = None` - `sentry_shutdown_timeout: int = 2` ================================================ FILE: website/docs/best-practice/htmlkit-render.md ================================================ --- sidebar_position: 8 description: 轻量化 HTML 绘图 --- # 轻量化 HTML 绘图 图片是机器人交互中不可或缺的一部分,对于信息展示的直观性、美观性有很大的作用。 基于 PIL 直接绘制图片具有良好的性能和存储开销,但是难以调试、维护过程式的绘图代码。 使用浏览器渲染类插件可以方便地绘制网页,且能够直接通过 JS 对网页效果进行编程,但是它占用的存储和内存空间相对可观。 NoneBot 提供的 `nonebot-plugin-htmlkit` 提供了另一种基于 HTML 和 CSS 语法的轻量化绘图选择:它基于 `litehtml` 解析库,无须安装额外的依赖即可使用,没有进程间通信带来的额外开销,且在支持 `webp` `avif` 等丰富图片格式的前提下,安装用的 wheel 文件大小仅有约 10 MB。 作为粗略的性能参考,在一台 Ryzen 7 9700X 的 Windows 电脑上,渲染 [PEP 7](https://peps.python.org/pep-0007/) 的 HTML 页面(分辨率为 800x5788,大小约 1.4MB,从本地文件系统读取 CSS)大约需要 100ms,每个渲染任务内存最高占用约为 40MB. ## 安装插件 在使用前请先安装 `nonebot-plugin-htmlkit` 插件至项目环境中,可参考[获取商店插件](../tutorial/store.mdx#安装插件)来了解并选择安装插件的方式。如: 在**项目目录**下执行以下命令: ```bash nb plugin install nonebot-plugin-htmlkit ``` `nonebot-plugin-htmlkit` 插件目前兼容以下系统架构: - Windows x64 - macOS arm64(M-系列芯片) - Linux x64 (非 Alpine 等 musl 系发行版) - Linux arm64 (非 Alpine 等 musl 系发行版) :::caution 访问网络内容 如果需要访问网络资源(如 http(s) 网页内容),NoneBot 需要客户端型驱动器(Forward)。内置的驱动器有 `~httpx` 与 `~aiohttp`。 详见[选择驱动器](../advanced/driver.md)。 ::: ## 使用插件 ### 加载插件 在使用本插件前同样需要使用 `require` 方法进行**加载**并**导入**需要使用的方法,可参考 [跨插件访问](../advanced/requiring.md) 一节进行了解,如: ```python from nonebot import require require("nonebot_plugin_htmlkit") from nonebot_plugin_htmlkit import html_to_pic, md_to_pic, template_to_pic, text_to_pic ``` 插件会自动使用[配置中的参数](#配置-fontconfig)初始化 `fontconfig` 以提供字体查找功能。 ### 渲染 API `nonebot-plugin-htmlkit` 主要提供以下**异步**渲染函数: #### html_to_pic ```python async def html_to_pic( html: str, *, base_url: str = "", dpi: float = 144.0, max_width: float = 800.0, device_height: float = 600.0, default_font_size: float = 12.0, font_name: str = "sans-serif", allow_refit: bool = True, image_format: Literal["png", "jpeg"] = "png", jpeg_quality: int = 100, lang: str = "zh", culture: str = "CN", img_fetch_fn: ImgFetchFn = combined_img_fetcher, css_fetch_fn: CSSFetchFn = combined_css_fetcher, urljoin_fn: Callable[[str, str], str] = urllib3.parse.urljoin, ) -> bytes: ... ``` 最核心的渲染函数。 `base_url` 和 `urljoin_fn` 控制着传入 `image_fetch_fn` 和 `css_fetch_fn` 回调的 url 内容。 `allow_refit` 如果为真,渲染时会自动缩小产出图片的宽度到最适合的宽度,否则必定产出 `max_width` 宽度的图片。 `max_width` 与 `device_height` 会在 `@media` 判断中被使用。 `img_fetch_fn` 预期为一个异步可调用对象(函数),接收图片 url 并返回对应 url 的 jpeg 或 png 二进制数据(`bytes`),可在拒绝加载时返回 `None`. `css_fetch_fn` 预期为一个异步可调用对象(函数),接收目标 CSS url 并返回对应 url 的 CSS 文本(`str`),可在拒绝加载时返回 `None`. 以下为辅助的封装函数,关键字参数若未特殊说明均与 `html_to_pic` 含义相同。 #### text_to_pic ```python async def text_to_pic( text: str, css_path: str = "", *, max_width: int = 500, allow_refit: bool = True, image_format: Literal["png", "jpeg"] = "png", jpeg_quality: int = 100, ) -> bytes: ... ``` 可用于渲染多行文本。 `text` 会被放置于 `
` 中,可据此编写 CSS 来改变文本表现。 #### md_to_pic ```python async def md_to_pic( md: str = "", md_path: str = "", css_path: str = "", *, max_width: int = 500, img_fetch_fn: ImgFetchFn = combined_img_fetcher, allow_refit: bool = True, image_format: Literal["png", "jpeg"] = "png", jpeg_quality: int = 100, ) -> bytes: ... ``` 可用于渲染 Markdown 文本。默认为 GitHub Markdown Light 风格,支持基于 `pygments` 的代码高亮。 `md` 和 `md_path` 二选一,前者设置时应为 Markdown 的文本,后者设置时应为指向 Markdown 文本文件的路径。 #### template_to_pic ```python async def template_to_pic( template_path: str | PathLike[str] | Sequence[str | PathLike[str]], template_name: str, templates: Mapping[Any, Any], filters: None | Mapping[str, Any] = None, *, max_width: int = 500, device_height: int = 600, base_url: str | None = None, img_fetch_fn: ImgFetchFn = combined_img_fetcher, css_fetch_fn: CSSFetchFn = combined_css_fetcher, allow_refit: bool = True, image_format: Literal["png", "jpeg"] = "png", jpeg_quality: int = 100, ) -> bytes: ... ``` 渲染 jinja2 模板。 `template_path` 为 jinja2 环境的路径,`template_name` 是环境中要加载模板的名字,`templates` 为传入模板的参数,`filters` 为过滤器名 -> 自定义过滤器的映射。 ### 控制外部资源获取 通过传入 `img_fetch_fn` 与 `css_fetch_fn`,我们可以在实际访问资源前进行审查,修改资源的来源,或是对 IO 操作进行缓存。 `img_fetch_fn` 预期为一个异步可调用对象(函数),接收图片 url 并返回对应 url 的 jpeg 或 png 二进制数据(`bytes`),可在拒绝加载时返回 `None`. `css_fetch_fn` 预期为一个异步可调用对象(函数),接收目标 CSS url 并返回对应 url 的 CSS 文本(`str`),可在拒绝加载时返回 `None`. 如果你想要禁用外部资源加载/只从文件系统加载/只从网络加载,可以使用 `none_fetcher` `filesystem_***_fetcher` `network_***_fetcher`。 默认的 fetcher 行为(对于 `file://` 从文件系统加载,其余从网络加载)位于 `combined_***_fetcher`,可以通过对其封装实现缓存等操作。 ## 配置项 ### 配置 fontconfig `htmlkit` 使用 `fontconfig` 查找字体,请参阅 [`fontconfig 用户手册`](https://fontconfig.pages.freedesktop.org/fontconfig/fontconfig-user) 了解环境变量的具体含义、如何通过编写配置文件修改字体配置等。 #### fontconfig_file - **类型**: `str | None` - **默认值**: `None` 覆盖默认的配置文件路径。 #### fontconfig_path - **类型**: `str | None` - **默认值**: `None` 覆盖默认的配置目录。 #### fontconfig_sysroot - **类型**: `str | None` - **默认值**: `None` 覆盖默认的 sysroot。 #### fc_debug - **类型**: `str | None` - **默认值**: `None` 设置 Fontconfig 的 debug 级别。 #### fc_dbg_match_filter - **类型**: `str | None` - **默认值**: `None` 当 `FC_DEBUG` 设置为 `MATCH2` 时,过滤 debug 输出。 #### fc_lang - **类型**: `str | None` - **默认值**: `None` 设置默认语言,否则从 `LOCALE` 环境变量获取。 #### fontconfig_use_mmap - **类型**: `str | None` - **默认值**: `None` 是否使用 `mmap(2)` 读取字体缓存。 ================================================ FILE: website/docs/best-practice/multi-adapter.mdx ================================================ --- sidebar_position: 4 description: 插件跨平台支持 --- # 插件跨平台支持 import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; ## 使用 NoneBot 本身 由于不同平台的事件与接口之间,存在着极大的差异性,NoneBot 通过[重载](../appendices/overload.md)的方式,使得插件可以在不同平台上正确响应。但为了减少跨平台的兼容性问题,我们应该尽可能的使用基类方法实现原生跨平台,而不是使用特定平台的方法。当基类方法无法满足需求时,我们可以使用依赖注入的方式,将特定平台的事件或机器人注入到事件处理函数中,实现针对特定平台的处理。 :::tip 提示 如果需要在多平台上**使用**跨平台插件,首先应该根据[注册适配器](../advanced/adapter.md#注册适配器)一节,为机器人注册各平台对应的适配器。 ::: ### 基于基类的跨平台 在[事件通用信息](../advanced/adapter.md#获取事件通用信息)中,我们了解了事件基类能够提供的通用信息。同时,[事件响应器操作](../appendices/session-control.mdx#更多事件响应器操作)也为我们提供了基本的用户交互方式。使用这些方法,可以让我们的插件运行在任何平台上。例如,一个简单的命令处理插件: ```python {5,11} from nonebot import on_command from nonebot.adapters import Event async def is_blacklisted(event: Event) -> bool: return event.get_user_id() not in BLACKLIST weather = on_command("天气", rule=is_blacklisted, priority=10, block=True) @weather.handle() async def handle_function(): await weather.finish("今天的天气是...") ``` 由于此插件仅使用了事件通用信息和事件响应器操作的纯文本交互方式,这些方法不使用特定平台的信息或接口,因此是原生跨平台的,并不需要额外处理。但在一些较为复杂的需求下,例如发送图片消息时,并非所有平台都具有统一的接口,因此基类便无能为力,我们需要引入特定平台的适配器了。 ### 基于重载的跨平台 重载是 NoneBot 跨平台操作的核心,在[事件类型与重载](../appendices/overload.md#重载)一节中,我们初步了解了如何通过类型注解来实现针对不同平台事件的处理方式。在[依赖注入](../advanced/dependency.mdx)一节中,我们又对依赖注入的使用方法进行了详细的介绍。结合这两节内容,我们可以实现更复杂的跨平台操作。 #### 处理近似事件 对于一系列**差异不大**的事件,我们往往具有相同的处理逻辑。这时,我们不希望将相同的逻辑编写两遍,而应该复用代码,以实现在同一个事件处理函数中处理多个近似事件。我们可以使用[事件重载](../advanced/dependency.mdx#event)的特性来实现这一功能。例如: ```python from nonebot import on_command from nonebot.adapters import Message from nonebot.params import CommandArg from nonebot.adapters.onebot.v11 import MessageEvent as OnebotV11MessageEvent from nonebot.adapters.onebot.v12 import MessageEvent as OnebotV12MessageEvent echo = on_command("echo", priority=10, block=True) @echo.handle() async def handle_function(event: OnebotV11MessageEvent | OnebotV12MessageEvent, args: Message = CommandArg()): await echo.finish(args) ``` ```python from typing import Union from nonebot import on_command from nonebot.adapters import Message from nonebot.params import CommandArg from nonebot.adapters.onebot.v11 import MessageEvent as OnebotV11MessageEvent from nonebot.adapters.onebot.v12 import MessageEvent as OnebotV12MessageEvent echo = on_command("echo", priority=10, block=True) @echo.handle() async def handle_function(event: Union[OnebotV11MessageEvent, OnebotV12MessageEvent], args: Message = CommandArg()): await echo.finish(args) ``` #### 在依赖注入中使用重载 NoneBot 依赖注入系统提供了自定义子依赖的方法,子依赖的类型同样会影响到事件处理函数的重载行为。例如: ```python from datetime import datetime from nonebot import on_command from nonebot.adapters.console import MessageEvent echo = on_command("echo", priority=10, block=True) def get_event_time(event: MessageEvent): return event.time # 处理控制台消息事件 @echo.handle() async def handle_function(time: datetime = Depends(get_event_time)): await echo.finish(time.strftime("%Y-%m-%d %H:%M:%S")) ``` 示例中 ,我们为 `handle_function` 事件处理函数注入了自定义的 `get_event_time` 子依赖,而此子依赖注入参数为 Console 适配器的 `MessageEvent`。因此 `handle_function` 仅会响应 Console 适配器的 `MessageEvent` ,而不能响应其他事件。 #### 处理多平台事件 不同平台的事件之间,往往存在着极大的差异性。为了满足我们插件的跨平台运行,通常我们需要抽离业务逻辑,以保证代码的复用性。一个合理的做法是,在事件响应器的处理流程中,首先先针对不同平台的事件分别进行处理,提取出核心业务逻辑所需要的信息;然后再将这些信息传递给业务逻辑处理函数;最后将业务逻辑的输出以各平台合适的方式返回给用户。也就是说,与平台绑定的处理部分应该与平台无关部分尽量分离。例如: ```python import inspect from nonebot import on_command from nonebot.typing import T_State from nonebot.matcher import Matcher from nonebot.adapters import Message from nonebot.params import CommandArg, ArgPlainText from nonebot.adapters.console import Bot as ConsoleBot from nonebot.adapters.onebot.v11 import Bot as OnebotBot from nonebot.adapters.console import MessageSegment as ConsoleMessageSegment weather = on_command("天气", priority=10, block=True) @weather.handle() async def handle_function(matcher: Matcher, args: Message = CommandArg()): if args.extract_plain_text(): matcher.set_arg("location", args) async def get_weather(state: T_State, location: str = ArgPlainText()): if location not in ["北京", "上海", "广州", "深圳"]: await weather.reject(f"你想查询的城市 {location} 暂不支持,请重新输入!") state["weather"] = "⛅ 多云 20℃~24℃" # 处理控制台询问 @weather.got( "location", prompt=ConsoleMessageSegment.emoji("question") + "请输入地名", parameterless=[Depends(get_weather)], ) async def handle_console(bot: ConsoleBot): pass # 处理 OneBot 询问 @weather.got( "location", prompt="请输入地名", parameterless=[Depends(get_weather)], ) async def handle_onebot(bot: OnebotBot): pass # 通过依赖注入或事件处理函数来进行业务逻辑处理 # 处理控制台回复 @weather.handle() async def handle_console_reply(bot: ConsoleBot, state: T_State, location: str = ArgPlainText()): await weather.send( ConsoleMessageSegment.markdown( inspect.cleandoc( f""" # {location} - 今天 {state['weather']} """ ) ) ) # 处理 OneBot 回复 @weather.handle() async def handle_onebot_reply(bot: OnebotBot, state: T_State, location: str = ArgPlainText()): await weather.send(f"今天{location}的天气是{state['weather']}") ``` ## 使用插件 得益于众多开发者为 NoneBot 社区做出的贡献,我们可以通过一系列插件来完成跨平台插件的开发。 这些插件可以分为三类: ### 事件处理 - [all4one](https://github.com/nonepkg/nonebot-plugin-all4one): 将不同平台的事件转为符合 OneBot V12 协议的插件 - 支持的适配器: OneBot V11/V12, Discord, QQ, Telegram ### 消息处理 - [alconna](https://github.com/nonebot/plugin-alconna): 对几乎所有适配器中消息的收发、撤回、编辑、表态的统一插件 - 支持的适配器: OneBot V11/V12, Telegram, Feishu, Github, QQ, Ding, Console, Kaiheila, Mirai, NtChat, Minecraft, Discord, Satori, Red, Dodo, Kritor, Tailchat, Mail, WXMP, Heybox, Gewechat - [send-anything-anywhere](https://github.com/felinae98/nonebot-plugin-send-anything-anywhere): 帮助处理不同适配器消息的适配和发送的插件 - 支持的适配器: OneBot V11/V12, Kaiheila, Telegram, Feishu, Red, DoDo, Satori, QQ, Discord ### 会话信息提取 - [uninfo](https://github.com/RF-Tar-Railt/nonebot-plugin-uninfo): 多平台的会话信息(用户、群组、频道)获取插件 - 支持的适配器: OneBot V11/V12, Telegram, Feishu, QQ, Console, Kaiheila, Mirai, Minecraft, Discord, Satori, Dodo, Kritor, Mail, WXMP, Gewechat - [session](https://github.com/noneplugin/nonebot-plugin-session): 会话信息提取与会话 id 定义插件 - 支持的适配器: OneBot V11/V12, Console, Kaiheila, Telegram, Feishu, Red, DoDo, Satori, QQ, Discord - [userinfo](https://github.com/noneplugin/nonebot-plugin-userinfo: 用户信息获取插件 - 支持的适配器: OneBot V11/V12, Console, Kaiheila, Telegram, Feishu, Red, DoDo, Satori, QQ, Discord ================================================ FILE: website/docs/best-practice/scheduler.md ================================================ --- sidebar_position: 0 description: 定时执行任务 --- # 定时任务 [APScheduler](https://apscheduler.readthedocs.io/en/3.x/) (Advanced Python Scheduler) 是一个 Python 第三方库,其强大的定时任务功能被广泛应用于各个场景。在 NoneBot 中,定时任务作为一个额外功能,依赖于基于 APScheduler 开发的 [`nonebot-plugin-apscheduler`](https://github.com/nonebot/plugin-apscheduler) 插件进行支持。 ## 安装插件 在使用前请先安装 `nonebot-plugin-apscheduler` 插件至项目环境中,可参考[获取商店插件](../tutorial/store.mdx#安装插件)来了解并选择安装插件的方式。如: 在**项目目录**下执行以下命令: ```bash nb plugin install nonebot-plugin-apscheduler ``` ## 使用插件 `nonebot-plugin-apscheduler` 本质上是对 [APScheduler](https://apscheduler.readthedocs.io/en/3.x/) 进行了封装以适用于 NoneBot 开发,因此其使用方式与 APScheduler 本身并无显著区别。在此我们会简要介绍其调用方法,更多的使用方面的功能请参考[APScheduler 官方文档](https://apscheduler.readthedocs.io/en/3.x/userguide.html)。 ### 导入调度器 由于 `nonebot_plugin_apscheduler` 作为插件,因此需要在使用前对其进行**加载**并**导入**其中的 `scheduler` 调度器来创建定时任务。使用 `require` 方法可轻松完成这一过程,可参考 [跨插件访问](../advanced/requiring.md) 一节进行了解。 ```python from nonebot import require require("nonebot_plugin_apscheduler") from nonebot_plugin_apscheduler import scheduler ``` ### 添加定时任务 在 [APScheduler 官方文档](https://apscheduler.readthedocs.io/en/3.x/userguide.html#adding-jobs) 中提供了以下两种直接添加任务的方式: ```python from nonebot import require require("nonebot_plugin_apscheduler") from nonebot_plugin_apscheduler import scheduler # 基于装饰器的方式 @scheduler.scheduled_job("cron", hour="*/2", id="job_0", args=[1], kwargs={arg2: 2}) async def run_every_2_hour(arg1: int, arg2: int): pass # 基于 add_job 方法的方式 def run_every_day(arg1: int, arg2: int): pass scheduler.add_job( run_every_day, "interval", days=1, id="job_1", args=[1], kwargs={arg2: 2} ) ``` :::caution 注意 由于 APScheduler 的定时任务并不是**由事件响应器所触发的事件**,因此其任务函数无法同[事件处理函数](../tutorial/handler.mdx#事件处理函数)一样通过[依赖注入](../tutorial/event-data.mdx#认识依赖注入)获取上下文信息,也无法通过事件响应器对象的方法进行任何操作,因此我们需要使用[调用平台 API](../appendices/api-calling.mdx#调用平台-api)的方式来获取信息或收发消息。 相对于事件处理依赖而言,编写定时任务更像是编写普通的函数,需要我们自行获取信息以及发送信息,请**不要**将事件处理依赖的特殊语法用于定时任务! ::: 关于 APScheduler 的更多使用方法,可以参考 [APScheduler 官方文档](https://apscheduler.readthedocs.io/en/3.x/index.html) 进行了解。 ### 配置项 #### apscheduler_autostart - **类型**: `bool` - **默认值**: `True` 是否自动启动 `scheduler` ,若不启动需要自行调用 `scheduler.start()`。 #### apscheduler_log_level - **类型**: `int` - **默认值**: `30` apscheduler 输出的日志等级 - `WARNING` = `30` (默认) - `INFO` = `20` - `DEBUG` = `10` (只有在开启 nonebot 的 debug 模式才会显示 debug 日志) #### apscheduler_config - **类型**: `dict` - **默认值**: `{ "apscheduler.timezone": "Asia/Shanghai" }` `apscheduler` 的相关配置。参考[配置调度器](https://apscheduler.readthedocs.io/en/latest/userguide.html#scheduler-config), [配置参数](https://apscheduler.readthedocs.io/en/latest/modules/schedulers/base.html#apscheduler.schedulers.base.BaseScheduler) 配置需要包含 `apscheduler.` 作为前缀,例如 `apscheduler.timezone`。 ================================================ FILE: website/docs/best-practice/testing/README.mdx ================================================ --- sidebar_position: 1 description: 使用 NoneBug 进行单元测试 slug: /best-practice/testing/ --- # 配置与测试事件响应器 import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; > 在计算机编程中,单元测试(Unit Testing)又称为模块测试,是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。 为了保证代码的正确运行,我们不仅需要对错误进行跟踪,还需要对代码进行正确性检验,也就是测试。NoneBot 提供了一个测试工具——NoneBug,它是一个 [pytest](https://docs.pytest.org/en/stable/) 插件,可以帮助我们便捷地进行单元测试。 :::tip 提示 建议在阅读本文档前先阅读 [pytest 官方文档](https://docs.pytest.org/en/stable/)来了解 pytest 的相关术语和基本用法。 ::: ## 安装 NoneBug 在**项目目录**下激活虚拟环境后运行以下命令安装 NoneBug: ```bash poetry add nonebug -G test ``` ```bash pdm add nonebug -dG test ``` ```bash pip install nonebug ``` 要运行 NoneBug 测试,还需要额外安装 pytest 异步插件 `pytest-asyncio` 或 `anyio` 以支持异步测试。文档中,我们以 `pytest-asyncio` 为例: ```bash poetry add pytest-asyncio -G test ``` ```bash pdm add pytest-asyncio -dG test ``` ```bash pip install pytest-asyncio ``` ## 配置测试 在开始测试之前,我们需要对测试进行一些配置,以正确启动我们的机器人。 首先我们需要配置 pytest-asyncio,在 `pyproject.toml` 的 pytest 配置部分添加: ```toml [tool.pytest.ini_options] asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "session" ``` 然后,我们在 `tests` 目录下新建 `conftest.py` 文件,添加以下内容: ```python title=tests/conftest.py import pytest import nonebot from pytest_asyncio import is_async_test # 导入适配器 from nonebot.adapters.console import Adapter as ConsoleAdapter def pytest_collection_modifyitems(items: list[pytest.Item]): pytest_asyncio_tests = (item for item in items if is_async_test(item)) session_scope_marker = pytest.mark.asyncio(loop_scope="session") for async_test in pytest_asyncio_tests: async_test.add_marker(session_scope_marker, append=False) @pytest.fixture(scope="session", autouse=True) async def after_nonebot_init(after_nonebot_init: None): # 加载适配器 driver = nonebot.get_driver() driver.register_adapter(ConsoleAdapter) # 加载插件 nonebot.load_from_toml("pyproject.toml") ``` 这样,我们就可以在测试中使用机器人的插件了。通常,我们不需要自行初始化 NoneBot,NoneBug 已经为我们运行了 `nonebot.init()`。如果需要自定义 NoneBot 初始化的参数,我们可以在 `conftest.py` 中添加 `pytest_configure` 钩子函数。例如,我们可以修改 NoneBot 配置环境为 `test` 并从环境变量中输入配置: ```python {4,6,8-10} title=tests/conftest.py import os import pytest from nonebug import NONEBOT_INIT_KWARGS os.environ["ENVIRONMENT"] = "test" def pytest_configure(config: pytest.Config): config.stash[NONEBOT_INIT_KWARGS] = {"secret": os.getenv("INPUT_SECRET")} ``` NoneBug 默认也会为我们管理 lifespan 的 startup 与 shutdown。如果不希望 NoneBug 管理 lifespan,你可以在 `pytest_configure` 里添加以下配置: ```python import pytest from nonebug import NONEBOT_START_LIFESPAN def pytest_configure(config: pytest.Config): config.stash[NONEBOT_START_LIFESPAN] = False ``` ## 编写插件测试 在配置完成插件加载后,我们就可以在测试中使用插件了。NoneBug 通过 pytest fixture `app` 提供各种测试方法,我们可以在测试中使用它来测试插件。现在,我们创建一个测试脚本来测试[深入指南](../../appendices/session-control.mdx)中编写的天气插件。首先,我们先要导入我们需要的模块:
插件示例 ```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}的天气是...") ```
```python {4,5,9,11-16} title=tests/test_weather.py from datetime import datetime import pytest from nonebug import App from nonebot.adapters.console import User, Message, MessageEvent @pytest.mark.asyncio async def test_weather(app: App): from awesome_bot.plugins.weather import weather event = MessageEvent( time=datetime.now(), self_id="test", message=Message("/天气 北京"), user=User(id="user"), ) ``` 在上面的代码中,我们引入了 NoneBug 的测试 `App` 对象,以及必要的适配器消息与事件定义等。在测试函数 `test_weather` 中,我们导入了要进行测试的事件响应器 `weather`。请注意,由于需要等待 NoneBot 初始化并加载插件完毕,插件内容必须在**测试函数内部**进行导入。然后,我们创建了一个 `MessageEvent` 事件对象,它模拟了一个用户发送了 `/天气 北京` 的消息。接下来,我们使用 `app.test_matcher` 方法来测试 `weather` 事件响应器: ```python {11-15} title=tests/test_weather.py @pytest.mark.asyncio async def test_weather(app: App): from awesome_bot.plugins.weather import weather event = MessageEvent( time=datetime.now(), self_id="test", message=Message("/天气 北京"), user=User(id="user"), ) async with app.test_matcher(weather) as ctx: bot = ctx.create_bot() ctx.receive_event(bot, event) ctx.should_call_send(event, "今天北京的天气是...", result=None) ctx.should_finished(weather) ``` 这里我们使用 `async with` 语句并通过参数指定要测试的事件响应器 `weather` 来进入测试上下文。在测试上下文中,我们可以使用 `ctx.create_bot` 方法创建一个虚拟的机器人实例,并使用 `ctx.receive_event` 方法来模拟机器人接收到消息事件。然后,我们就可以定义预期行为来测试机器人是否正确运行。在上面的代码中,我们使用 `ctx.should_call_send` 方法来断言机器人应该发送 `今天北京的天气是...` 这条消息,并且将发送函数的调用结果作为第三个参数返回给事件处理函数。如果断言失败,测试将会不通过。我们也可以使用 `ctx.should_finished` 方法来断言机器人应该结束会话。 为了测试更复杂的情况,我们可以为添加更多的测试用例。例如,我们可以测试用户输入了一个不支持的地名时机器人的反应: ```python {17-21,23-26} title=tests/test_weather.py def make_event(message: str = "") -> MessageEvent: return MessageEvent( time=datetime.now(), self_id="test", message=Message(message), user=User(id="user"), ) @pytest.mark.asyncio async def test_weather(app: App): from awesome_bot.plugins.weather import weather async with app.test_matcher(weather) as ctx: ... # 省略前面的测试用例 async with app.test_matcher(weather) as ctx: bot = ctx.create_bot() event = make_event("/天气 南京") ctx.receive_event(bot, event) ctx.should_call_send(event, "你想查询的城市 南京 暂不支持,请重新输入!", result=None) ctx.should_rejected(weather) event = make_event("北京") ctx.receive_event(bot, event) ctx.should_call_send(event, "今天北京的天气是...", result=None) ctx.should_finished(weather) ``` 在上面的代码中,我们使用 `ctx.should_rejected` 来断言机器人应该请求用户重新输入。然后,我们再次使用 `ctx.receive_event` 方法来模拟用户回复了 `北京`,并使用 `ctx.should_finished` 来断言机器人应该结束会话。 更多的 NoneBug 用法将在后续章节中介绍。 ================================================ FILE: website/docs/best-practice/testing/_category_.json ================================================ { "label": "单元测试", "position": 5 } ================================================ FILE: website/docs/best-practice/testing/behavior.mdx ================================================ --- sidebar_position: 2 description: 测试事件响应、平台接口调用和会话控制 --- # 测试事件响应与会话操作 import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; 在 NoneBot 接收到事件时,事件响应器根据优先级依次通过权限、响应规则来判断当前事件是否应该触发。事件响应流程中,机器人可能会通过 `send` 发送消息或者调用平台接口来执行预期的操作。因此,我们需要对这两种操作进行单元测试。 在上一节中,我们对单个事件响应器进行了简单测试。但是在实际场景中,机器人可能定义了多个事件响应器,由于优先级和响应规则的存在,预期的事件响应器可能并不会被触发。NoneBug 支持同时测试多个事件响应器,以此来测试机器人的整体行为。 ## 测试事件响应 NoneBug 提供了六种定义 `Rule` 和 `Permission` 预期行为的方法: - `should_pass_rule` - `should_not_pass_rule` - `should_ignore_rule` - `should_pass_permission` - `should_not_pass_permission` - `should_ignore_permission` :::tip 提示 事件响应器类型的检查属于 `Permission` 的一部分,因此可以通过 `should_pass_permission` 和 `should_not_pass_permission` 方法来断言事件响应器类型的检查。 ::: 下面我们根据插件示例来测试事件响应行为,我们首先定义两个事件响应器作为测试的对象: ```python title=example.py from nonebot import on_command def never_pass(): return False foo = on_command("foo") bar = on_command("bar", permission=never_pass) ``` 在这两个事件响应器中,`foo` 当收到 `/foo` 消息时会执行,而 `bar` 则不会执行。我们使用 NoneBug 来测试它们: ```python {21,22,28,29} title=tests/test_example.py from datetime import datetime import pytest from nonebug import App from nonebot.adapters.console import User, Message, MessageEvent def make_event(message: str = "") -> MessageEvent: return MessageEvent( time=datetime.now(), self_id="test", message=Message(message), user=User(id="user"), ) @pytest.mark.asyncio async def test_example(app: App): from awesome_bot.plugins.example import foo, bar async with app.test_matcher(foo) as ctx: bot = ctx.create_bot() event = make_event("/foo") ctx.receive_event(bot, event) ctx.should_pass_rule() ctx.should_pass_permission() async with app.test_matcher(bar) as ctx: bot = ctx.create_bot() event = make_event("/foo") ctx.receive_event(bot, event) ctx.should_not_pass_rule() ctx.should_not_pass_permission() ``` 在上面的代码中,我们分别对 `foo` 和 `bar` 事件响应器进行响应测试。我们使用 `ctx.should_pass_rule` 和 `ctx.should_pass_permission` 断言 `foo` 事件响应器应该被触发,使用 `ctx.should_not_pass_rule` 和 `ctx.should_not_pass_permission` 断言 `bar` 事件响应器应该被忽略。 ```python title=tests/test_example.py from datetime import datetime import pytest from nonebug import App from nonebot.adapters.console import User, Message, MessageEvent def make_event(message: str = "") -> MessageEvent: return MessageEvent( time=datetime.now(), self_id="test", message=Message(message), user=User(id="user"), ) @pytest.mark.asyncio async def test_example(app: App): from awesome_bot.plugins.example import foo, bar async with app.test_matcher() as ctx: bot = ctx.create_bot() event = make_event("/foo") ctx.receive_event(bot, event) ctx.should_pass_rule(foo) ctx.should_pass_permission(foo) ctx.should_not_pass_rule(bar) ctx.should_not_pass_permission(bar) ``` 在上面的代码中,我们对 `foo` 和 `bar` 事件响应器一起进行响应测试。我们使用 `ctx.should_pass_rule` 和 `ctx.should_pass_permission` 断言 `foo` 事件响应器应该被触发,使用 `ctx.should_not_pass_rule` 和 `ctx.should_not_pass_permission` 断言 `bar` 事件响应器应该被忽略。通过参数,我们可以指定断言的事件响应器。 当然,如果需要忽略某个事件响应器的响应规则和权限检查,强行进入响应流程,我们可以使用 `should_ignore_rule` 和 `should_ignore_permission` 方法: ```python {21,22} title=tests/test_example.py from datetime import datetime import pytest from nonebug import App from nonebot.adapters.console import User, Message, MessageEvent def make_event(message: str = "") -> MessageEvent: return MessageEvent( time=datetime.now(), self_id="test", message=Message(message), user=User(id="user"), ) @pytest.mark.asyncio async def test_example(app: App): from awesome_bot.plugins.example import foo, bar async with app.test_matcher(bar) as ctx: bot = ctx.create_bot() event = make_event("/foo") ctx.receive_event(bot, event) ctx.should_ignore_rule(bar) ctx.should_ignore_permission(bar) ``` 在忽略了响应规则和权限检查之后,就会进入 `bar` 事件响应器的响应流程。 ## 测试平台接口使用 上一节的示例插件测试中,我们已经尝试了测试插件对事件的消息回复。通常情况下,事件处理流程中对平台接口的使用会通过事件响应器操作或者调用平台 API 两种途径进行。针对这两种途径,NoneBug 分别提供了 `ctx.should_call_send` 和 `ctx.should_call_api` 方法来测试平台接口的使用情况。 1. `should_call_send` 定义事件响应器预期发送的消息,即通过[事件响应器操作 send](../../appendices/session-control.mdx#send)进行的操作。`should_call_send` 有四个参数: - `event`:回复的目标事件。 - `message`:预期的消息对象,可以是 `str`、`Message` 或 `MessageSegment`。 - `result`:send 的返回值,将会返回给插件。 - `bot`(可选):发送消息的 bot 对象。 - `**kwargs`:send 方法的额外参数。 2. `should_call_api` 定义事件响应器预期调用的平台 API 接口,即通过[调用平台 API](../../appendices/api-calling.mdx#调用平台-api)进行的操作。`should_call_api` 有四个参数: - `api`:API 名称。 - `data`:预期的请求数据。 - `result`:call_api 的返回值,将会返回给插件。 - `adapter`(可选):调用 API 的平台适配器对象。 - `**kwargs`:call_api 方法的额外参数。 下面是一个使用 `should_call_send` 和 `should_call_api` 方法的示例: 我们先定义一个测试插件,在响应流程中向用户发送一条消息并调用 `Console` 适配器的 `bell` API。 ```python {8,9} title=example.py from nonebot import on_command from nonebot.adapters.console import Bot foo = on_command("foo") @foo.handle() async def _(bot: Bot): await foo.send("message") await bot.bell() ``` 然后我们对该插件进行测试: ```python title=tests/test_example.py from datetime import datetime import pytest import nonebot from nonebug import App from nonebot.adapters.console import Bot, User, Adapter, Message, MessageEvent def make_event(message: str = "") -> MessageEvent: return MessageEvent( time=datetime.now(), self_id="test", message=Message(message), user=User(id="user"), ) @pytest.mark.asyncio async def test_example(app: App): from awesome_bot.plugins.example import foo async with app.test_matcher(foo) as ctx: # highlight-start adapter = nonebot.get_adapter(Adapter) bot = ctx.create_bot(base=Bot, adapter=adapter) # highlight-end event = make_event("/foo") ctx.receive_event(bot, event) # highlight-start ctx.should_call_send(event, "message", result=None, bot=bot) ctx.should_call_api("bell", {}, result=None, adapter=adapter) # highlight-end ``` 请注意,对于在依赖注入中使用了非基类对象的情况,我们需要在 `create_bot` 方法中指定 `base` 和 `adapter` 参数,确保不会因为重载功能而出现非预期情况。 ## 测试会话控制 在[会话控制](../../appendices/session-control.mdx)一节中,我们介绍了如何使用事件响应器操作来实现对用户的交互式会话。在上一节的示例插件测试中,我们其实已经使用了 `ctx.should_finished` 来断言会话结束。NoneBug 针对各种流程控制操作分别提供了相应的方法来定义预期的会话处理行为。它们分别是: - `should_finished`:断言会话结束,对应 `matcher.finish` 操作。 - `should_rejected`:断言会话等待用户输入并重新执行当前事件处理函数,对应 `matcher.reject` 系列操作。 - `should_paused`: 断言会话等待用户输入并执行下一个事件处理函数,对应 `matcher.pause` 操作。 我们仅需在测试用例中的正确位置调用这些方法,就可以断言会话的预期行为。例如: ```python title=example.py from nonebot import on_command from nonebot.typing import T_State foo = on_command("foo") @foo.got("key", prompt="请输入密码") async def _(state: T_State, key: str = ArgPlainText()): if key != "some password": try_count = state.get("try_count", 1) if try_count >= 3: await foo.finish("密码错误次数过多") else: state["try_count"] = try_count + 1 await foo.reject("密码错误,请重新输入") await foo.finish("密码正确") ``` ```python title=tests/test_example.py from datetime import datetime import pytest from nonebug import App from nonebot.adapters.console import User, Message, MessageEvent def make_event(message: str = "") -> MessageEvent: return MessageEvent( time=datetime.now(), self_id="test", message=Message(message), user=User(id="user"), ) @pytest.mark.asyncio async def test_example(app: App): from awesome_bot.plugins.example import foo async with app.test_matcher(foo) as ctx: bot = ctx.create_bot() event = make_event("/foo") ctx.receive_event(bot, event) ctx.should_call_send(event, "请输入密码", result=None) ctx.should_rejected(foo) event = make_event("wrong password") ctx.receive_event(bot, event) ctx.should_call_send(event, "密码错误,请重新输入", result=None) ctx.should_rejected(foo) event = make_event("wrong password") ctx.receive_event(bot, event) ctx.should_call_send(event, "密码错误,请重新输入", result=None) ctx.should_rejected(foo) event = make_event("wrong password") ctx.receive_event(bot, event) ctx.should_call_send(event, "密码错误次数过多", result=None) ctx.should_finished(foo) ``` ================================================ FILE: website/docs/best-practice/testing/mock-network.md ================================================ --- sidebar_position: 3 description: 模拟网络通信以进行测试 --- # 模拟网络通信 NoneBot 驱动器提供了多种方法来帮助适配器进行网络通信,主要包括客户端和服务端两种类型。模拟网络通信可以帮助我们更加接近实际机器人应用场景,进行更加真实的集成测试。同时,通过这种途径,我们还可以完成对适配器的测试。 NoneBot 中的网络通信主要包括以下几种: - HTTP 服务端(WebHook) - WebSocket 服务端 - HTTP 客户端 - WebSocket 客户端 下面我们将分别介绍如何使用 NoneBug 来模拟这几种通信方式。 ## 测试 HTTP 服务端 当 NoneBot 作为 ASGI 服务端应用时,我们可以定义一系列的路由来处理 HTTP 请求,适配器同样也可以通过定义路由来响应机器人相关的网络通信。下面假设我们使用了一个适配器 `fake` ,它定义了一个路由 `/fake/http` ,用于接收平台 WebHook 并处理。实际应用测试时,应将该路由地址替换为**真实适配器注册的路由地址**。 我们首先需要获取测试用模拟客户端: ```python {5,6} title=tests/test_http_server.py from nonebug import App @pytest.mark.asyncio async def test_http_server(app: App): async with app.test_server() as ctx: client = ctx.get_client() ``` 默认情况下,`app.test_server()` 会通过 `nonebot.get_asgi` 获取测试对象,我们也可以通过参数指定 ASGI 应用: ```python async with app.test_server(asgi=asgi_app) as ctx: ... ``` 获取到模拟客户端后,即可像 `requests`、`httpx` 等库类似的方法进行使用: ```python {3,11-14,16} title=tests/test_http_server.py import nonebot from nonebug import App from nonebot.adapters.fake import Adapter @pytest.mark.asyncio async def test_http_server(app: App): adapter = nonebot.get_adapter(Adapter) async with app.test_server() as ctx: client = ctx.get_client() response = await client.post("/fake/http", json={"bot_id": "fake"}) assert response.status_code == 200 assert response.json() == {"status": "success"} assert "fake" in nonebot.get_bots() adapter.bot_disconnect(nonebot.get_bot("fake")) ``` 在上面的测试中,我们向 `/fake/http` 发送了一个模拟 POST 请求,适配器将会对该请求进行处理,我们可以通过检查请求返回是否正确、Bot 对象是否创建等途径来验证机器人是否正确运行。在完成测试后,我们通常需要对 Bot 对象进行清理,以避免对其他测试产生影响。 ## 测试 WebSocket 服务端 当 NoneBot 作为 ASGI 服务端应用时,我们还可以定义一系列的路由来处理 WebSocket 通信。下面假设我们使用了一个适配器 `fake` ,它定义了一个路由 `/fake/ws` ,用于处理平台 WebSocket 连接信息。实际应用测试时,应将该路由地址替换为**真实适配器注册的路由地址**。 我们同样需要通过 `app.test_server()` 获取测试用模拟客户端,这里就不再赘述。在获取到模拟客户端后,我们可以通过 `client.websocket_connect` 方法来模拟 WebSocket 连接: ```python {3,11-15} title=tests/test_ws_server.py import nonebot from nonebug import App from nonebot.adapters.fake import Adapter @pytest.mark.asyncio async def test_ws_server(app: App): adapter = nonebot.get_adapter(Adapter) async with app.test_server() as ctx: client = ctx.get_client() async with client.websocket_connect("/fake/ws") as ws: await ws.send_json({"bot_id": "fake"}) response = await ws.receive_json() assert response == {"status": "success"} assert "fake" in nonebot.get_bots() ``` 在上面的测试中,我们向 `/fake/ws` 进行了 WebSocket 模拟通信,通过发送消息与机器人进行交互,然后检查机器人发送的信息是否正确。 ## 测试 HTTP 客户端 ~~暂不支持~~ ## 测试 WebSocket 客户端 ~~暂不支持~~ ================================================ FILE: website/docs/community/contact.md ================================================ --- sidebar-position: 0 description: 遇到问题如何获取帮助 --- # 参与讨论 如果在安装或者开发 NoneBot 过程中遇到了任何问题,或者有新奇的点子,欢迎参与我们的社区讨论: 1. 点击下方链接前往 GitHub,前往 Issues 页面,在 `New Issue` Template 中选择 `Question` NoneBot:[![NoneBot project link](https://img.shields.io/github/stars/nonebot/nonebot2?style=social)](https://github.com/nonebot/nonebot2) 2. 通过 QQ 群(点击下方链接直达) [![QQ Chat Group](https://img.shields.io/badge/QQ%E7%BE%A4-768887710-orange?style=social)](https://jq.qq.com/?_wv=1027&k=5OFifDh) 3. 通过 QQ 频道 [![QQ Channel](https://img.shields.io/badge/QQ%E9%A2%91%E9%81%93-NoneBot-orange?style=social)](https://qun.qq.com/qqweb/qunpro/share?_wv=3&_wwv=128&appChannel=share&inviteCode=7b4a3&appChannel=share&businessType=9&from=246610&biz=ka) 4. 通过 Discord 服务器(点击下方链接直达) [![Discord Server](https://discordapp.com/api/guilds/847819937858584596/widget.png?style=shield)](https://discord.gg/VKtE6Gdc4h) ================================================ FILE: website/docs/community/contributing.md ================================================ --- sidebar-position: 1 description: 如何为 NoneBot 贡献代码 --- # 贡献指南 ## Code of Conduct 请参阅 [Code of Conduct](https://github.com/nonebot/nonebot2/blob/master/CODE_OF_CONDUCT.md)。 ## 参与开发 请参阅 [Contributing](https://github.com/nonebot/nonebot2/blob/master/CONTRIBUTING.md)。 ## 鸣谢 感谢以下开发者对 NoneBot2 作出的贡献: ================================================ FILE: website/docs/developer/adapter-writing.md ================================================ --- sidebar_position: 1 description: 编写适配器对接新的平台 --- # 编写适配器 在编写适配器之前,我们需要先了解[适配器的功能与组成](../advanced/adapter#适配器功能与组成),适配器通常由 `Adapter`、`Bot`、`Event` 和 `Message` 四个部分组成,在编写适配器时,我们需要继承 NoneBot 中的基类,并根据实际平台来编写每个部分功能。 ## 组织结构 NoneBot 适配器项目通常以 `nonebot-adapter-{adapter-name}` 作为项目名,并以**命名空间包**的形式编写,即在 `nonebot/adapters/{adapter-name}` 目录中编写实际代码,例如: ```tree 📦 nonebot-adapter-{adapter-name} ├── 📂 nonebot │ ├── 📂 adapters │ │ ├── 📂 {adapter-name} │ │ │ ├── 📜 __init__.py │ │ │ ├── 📜 adapter.py │ │ │ ├── 📜 bot.py │ │ │ ├── 📜 config.py │ │ │ ├── 📜 event.py │ │ │ └── 📜 message.py ├── 📜 pyproject.toml └── 📜 README.md ``` :::tip 提示 上述的项目结构仅作推荐,不做强制要求,保证实际可用性即可。 ::: ### 使用 NB-CLI 创建项目 我们可以使用脚手架快速创建项目: ```shell nb adapter create ``` 按照指引,输入适配器名称以及存储位置,即可创建一个带有基本结构的适配器项目。 ## 组成部分 :::tip 提示 本章节的代码中提到的 `Adapter`、`Bot`、`Event` 和 `Message` 等,均为下文中适配器所编写的类,而非 NoneBot 中的基类。 ::: ### Log 适配器在处理时通常需要打印日志,但直接使用 NoneBot 的默认 `logger` 不方便区分适配器输出和其它日志。因此我们可以使用 NoneBot 提供的 `logger_wrapper` 方法,自定义一个 `log` 函数用于快捷打印适配器日志: ```python {3} title=log.py from nonebot.utils import logger_wrapper log = logger_wrapper("your_adapter_name") ``` 这个 `log` 函数会在默认 `logger` 中添加适配器名称前缀,它接收三个参数:日志等级、日志内容以及可选的异常,具体用法如下: ```python from .log import log log("DEBUG", "A DEBUG log.") log("INFO", "A INFO log.") try: ... except Exception as e: log("ERROR", "something error.", e) ``` ### Config 通常适配器需要一些配置项,例如平台连接密钥等。适配器的配置方法与[插件配置](../appendices/config#%E6%8F%92%E4%BB%B6%E9%85%8D%E7%BD%AE)类似,例如: ```python title=config.py from pydantic import BaseModel class Config(BaseModel): xxx_id: str xxx_token: str ``` 配置项的读取将在下方 [Adapter](#adapter) 中介绍。 ### Adapter Adapter 负责转换事件、调用接口,以及正确创建 Bot 对象并注册到 NoneBot 中。在编写平台相关内容之前,我们需要继承基类,并实现适配器的基本信息: ```python {9,11,14,18} title=adapter.py from typing import Any from typing_extensions import override from nonebot.drivers import Driver from nonebot import get_plugin_config from nonebot.adapters import Adapter as BaseAdapter from .config import Config class Adapter(BaseAdapter): @override def __init__(self, driver: Driver, **kwargs: Any): super().__init__(driver, **kwargs) # 读取适配器所需的配置项 self.adapter_config: Config = get_plugin_config(Config) @classmethod @override def get_name(cls) -> str: """适配器名称""" return "your_adapter_name" ``` #### 与平台交互 NoneBot 提供了多种 [Driver](../advanced/driver) 来帮助适配器进行网络通信,主要分为客户端和服务端两种类型。我们需要**根据平台文档和特性**选择合适的通信方式,并编写相关方法用于初始化适配器,与平台建立连接和进行交互: ##### 客户端通信方式 ```python {12,23,24} title=adapter.py import asyncio from typing_extensions import override from nonebot import get_plugin_config from nonebot.exception import WebSocketClosed from nonebot.drivers import Request, WebSocketClientMixin class Adapter(BaseAdapter): @override def __init__(self, driver: Driver, **kwargs: Any): super().__init__(driver, **kwargs) self.adapter_config: Config = get_plugin_config(Config) self.task: Optional[asyncio.Task] = None # 存储 ws 任务 self.setup() def setup(self) -> None: if not isinstance(self.driver, WebSocketClientMixin): # 判断用户配置的Driver类型是否符合适配器要求,不符合时应抛出异常 raise RuntimeError( f"Current driver {self.config.driver} doesn't support websocket client connections!" f"{self.get_name()} Adapter need a WebSocket Client Driver to work." ) # 在 NoneBot 启动和关闭时进行相关操作 self.driver.on_startup(self.startup) self.driver.on_shutdown(self.shutdown) async def startup(self) -> None: """定义启动时的操作,例如和平台建立连接""" self.task = asyncio.create_task(self._forward_ws()) # 建立 ws 连接 async def _forward_ws(self): request = Request( method="GET", url="your_platform_websocket_url", headers={"token": "..."}, # 鉴权请求头 ) while True: try: async with self.websocket(request) as ws: try: # 处理 websocket ... except WebSocketClosed as e: log( "ERROR", "WebSocket Closed", e, ) except Exception as e: log( "ERROR", "Error while process data from " "websocket platform_websocket_url. " "Trying to reconnect...", e, ) finally: # 这里要断开 Bot 连接 except Exception as e: # 尝试重连 log( "ERROR", "Error while setup websocket to " "platform_websocket_url. Trying to reconnect...", e, ) await asyncio.sleep(3) # 重连间隔 async def shutdown(self) -> None: """定义关闭时的操作,例如停止任务、断开连接""" # 断开 ws 连接 if self.task is not None and not self.task.done(): self.task.cancel() ``` ##### 服务端通信方式 ```python {30,38} title=adapter.py from nonebot import get_plugin_config from nonebot.drivers import ( Request, ASGIMixin, WebSocket, HTTPServerSetup, WebSocketServerSetup ) class Adapter(BaseAdapter): @override def __init__(self, driver: Driver, **kwargs: Any): super().__init__(driver, **kwargs) self.adapter_config: Config = get_plugin_config(Config) self.setup() def setup(self) -> None: if not isinstance(self.driver, ASGIMixin): raise RuntimeError( f"Current driver {self.config.driver} doesn't support asgi server!" f"{self.get_name()} Adapter need a asgi server driver to work." ) # 建立服务端路由 # HTTP Webhook 路由 http_setup = HTTPServerSetup( URL("your_webhook_url"), # 路由地址 "POST", # 接收的方法 "WEBHOOK name", # 路由名称 self._handle_http, # 处理函数 ) self.setup_http_server(http_setup) # 反向 Websocket 路由 ws_setup = WebSocketServerSetup( URL("your_websocket_url"), # 路由地址 "WebSocket name", # 路由名称 self._handle_ws, # 处理函数 ) self.setup_websocket_server(ws_setup) async def _handle_http(self, request: Request) -> Response: """HTTP 路由处理函数,只有一个类型为 Request 的参数,且返回值类型为 Response""" ... return Response( status_code=200, # 状态码 headers={"something": "something"}, # 响应头 content="xxx", # 响应内容 ) async def _handle_ws(self, websocket: WebSocket) -> Any: """WebSocket 路由处理函数,只有一个类型为 WebSocket 的参数""" ... ``` 更多通信交互方式可以参考以下适配器: - [OneBot](https://github.com/nonebot/adapter-onebot/blob/master/nonebot/adapters/onebot/v11/adapter.py) - `WebSocket 客户端`、`WebSocket 服务端`、`HTTP WEBHOOK`、`HTTP POST` - [QQ](https://github.com/nonebot/adapter-qq/blob/master/nonebot/adapters/qq/adapter.py) - `WebSocket 服务端`、`HTTP WEBHOOK` - [Telegram](https://github.com/nonebot/adapter-telegram/blob/beta/nonebot/adapters/telegram/adapter.py) - `HTTP WEBHOOK` #### 建立 Bot 连接 在与平台建立连接后,我们需要将 [Bot](#bot) 实例化,并调用适配器提供的的 `bot_connect` 方法告知 NoneBot 建立了 Bot 连接。在与平台断开连接或出现某些异常进行重连时,我们需要调用 `bot_disconnect` 方法告知 NoneBot 断开了 Bot 连接。 ```python {7,8,11} title=adapter.py from .bot import Bot class Adapter(BaseAdapter): def _handle_connect(self): bot_id = ... # 通过配置或者平台 API 等方式,获取到 Bot 的 ID bot = Bot(self, self_id=bot_id) # 实例化 Bot self.bot_connect(bot) # 建立 Bot 连接 def _handle_disconnect(self): self.bot_disconnect(bot) # 断开 Bot 连接 ``` #### 转换 Event 事件 在接收到来自平台的事件数据后,我们需要将其转为适配器的 [Event](#event),并调用 Bot 的 `handle_event` 方法来让 Bot 对事件进行处理: ```python title=adapter.py import asyncio from typing import Any, Dict from nonebot.compat import type_validate_python from .bot import Bot from .event import Event from .log import log class Adapter(BaseAdapter): @classmethod def payload_to_event(cls, payload: Dict[str, Any]) -> Event: """根据平台事件的特性,转换平台 payload 为具体 Event Event 模型继承自 pydantic.BaseModel,具体请参考 pydantic 文档 """ # 做一层异常处理,以应对平台事件数据的变更 try: return type_validate_python(your_event_class, payload) except Exception as e: # 无法正常解析为具体 Event 时,给出日志提示 log( "WARNING", f"Parse event error: {str(payload)}", ) # 也可以尝试转为基础 Event 进行处理 return type_validate_python(Event, payload) async def _forward(self, bot: Bot): payload: Dict[str, Any] # 接收到的事件数据 event = self.payload_to_event(payload) # 让 bot 对事件进行处理 asyncio.create_task(bot.handle_event(event)) ``` #### 调用平台 API 我们需要实现 `Adapter` 的 `_call_api` 方法,使开发者能够调用平台提供的 API。如果通过 WebSocket 通信可以通过 `send` 方法来发送数据,如果采用 HTTP 请求,则需要通过 NoneBot 提供的 `Request` 对象,调用 `driver` 的 `request` 方法来发送请求。 ```python {11} title=adapter.py from typing import Any from typing_extensions import override from nonebot.drivers import Request, WebSocket from .bot import Bot class Adapter(BaseAdapter): @override async def _call_api(self, bot: Bot, api: str, **data: Any) -> Any: log("DEBUG", f"Calling API {api}") # 给予日志提示 platform_data = your_handle_data_method(data) # 自行将数据转为平台所需要的格式 # 采用 HTTP 请求的方式,需要构造一个 Request 对象 request = Request( method="GET", # 请求方法 url=api, # 接口地址 headers=..., # 请求头,通常需要包含鉴权信息 params=platform_data, # 自行处理数据的传输形式 # json=platform_data, # data=platform_data, ) # 发送请求,返回结果 return await self.driver.request(request) # 采用 WebSocket 通信的方式,可以直接调用 send 方法发送数据 # 通过某种方式获取到 bot 对应的 websocket 对象 ws: WebSocket = your_get_websocket_method(bot.self_id) await ws.send_text(platform_data) # 发送 str 类型的数据 await ws.send_bytes(platform_data) # 发送 bytes 类型的数据 await ws.send(platform_data) # 是以上两种方式的合体 # 接收并返回结果,同样的,也有 str 和 bytes 的区别 return await ws.receive_text() return await ws.receive_bytes() return await ws.receive() ``` `调用平台 API` 实现方式具体可以参考以下适配器: Websocket: - [OneBot V11](https://github.com/nonebot/adapter-onebot/blob/54270edbbdb2a71332d744f90b1a3d7f4bf6463a/nonebot/adapters/onebot/v11/adapter.py#L167-L177) - [OneBot V12](https://github.com/nonebot/adapter-onebot/blob/54270edbbdb2a71332d744f90b1a3d7f4bf6463a/nonebot/adapters/onebot/v12/adapter.py#L204-L218) HTTP: - [OneBot V11](https://github.com/nonebot/adapter-onebot/blob/54270edbbdb2a71332d744f90b1a3d7f4bf6463a/nonebot/adapters/onebot/v11/adapter.py#L179-L215) - [OneBot V12](https://github.com/nonebot/adapter-onebot/blob/54270edbbdb2a71332d744f90b1a3d7f4bf6463a/nonebot/adapters/onebot/v12/adapter.py#L220-L266) - [QQ](https://github.com/nonebot/adapter-qq/blob/dc5d437e101f0e3db542de3300758a035ed7036e/nonebot/adapters/qq/adapter.py#L599-L605) - [Telegram](https://github.com/nonebot/adapter-telegram/blob/4a8633627e619245516767f5503dec2f58fe2193/nonebot/adapters/telegram/adapter.py#L148-L253) - [飞书](https://github.com/nonebot/adapter-feishu/blob/f8ab05e6d57a5e9013b944b0d019ca777725dfb0/nonebot/adapters/feishu/adapter.py#L201-L218) ### Bot Bot 是机器人开发者能够直接获取并使用的核心对象,负责存储平台机器人相关信息,并提供回复事件、调用 API 的上层方法。我们需要继承基类 `Bot`,并实现相关方法: ```python {20,25,34} title=bot.py from typing import TYPE_CHECKING, Any, Union from typing_extensions import override from nonebot.message import handle_event from nonebot.adapters import Bot as BaseBot from .event import Event from .message import Message, MessageSegment if TYPE_CHECKING: from .adapter import Adapter class Bot(BaseBot): """ your_adapter_name 协议 Bot 适配。 """ @override def __init__(self, adapter: Adapter, self_id: str, **kwargs: Any): super().__init__(adapter, self_id) self.adapter: Adapter = adapter # 一些有关 Bot 的信息也可以在此定义和存储 async def handle_event(self, event: Event): # 根据需要,对事件进行某些预处理,例如: # 检查事件是否和机器人有关操作,去除事件消息首尾的 @bot # 检查事件是否有回复消息,调用平台 API 获取原始消息的消息内容 ... # 调用 handle_event 让 NoneBot 对事件进行处理 await handle_event(self, event) @override async def send( self, event: Event, message: Union[str, Message, MessageSegment], **kwargs: Any, ) -> Any: # 根据平台实现 Bot 回复事件的方法 # 将消息处理为平台所需的格式后,调用发送消息接口进行发送,例如: data = message_to_platform_data(message) await self.send_message( data=data, ... ) ``` ### Event Event 是 NoneBot 中的事件主体对象,所有平台消息在进入处理流程前需要转换为 NoneBot 事件。我们需要继承基类 `Event`,并实现相关方法: ```python {5,8,13,18,23,28,33} title=event.py from typing_extensions import override from nonebot.compat import model_dump from nonebot.adapters import Event as BaseEvent class Event(BaseEvent): @override def get_event_name(self) -> str: # 返回事件的名称,用于日志打印 return "event name" @override def get_event_description(self) -> str: # 返回事件的描述,用于日志打印,请注意转义 loguru tag return escape_tag(repr(model_dump(self))) @override def get_message(self): # 获取事件消息的方法,根据事件具体实现,如果事件非消息类型事件,则抛出异常 raise ValueError("Event has no message!") @override def get_user_id(self) -> str: # 获取用户 ID 的方法,根据事件具体实现,如果事件没有用户 ID,则抛出异常 raise ValueError("Event has no context!") @override def get_session_id(self) -> str: # 获取事件会话 ID 的方法,根据事件具体实现,如果事件没有相关 ID,则抛出异常 raise ValueError("Event has no context!") @override def is_tome(self) -> bool: # 判断事件是否和机器人有关 return False ``` 然后根据平台消息的类型,编写各种不同的事件,并且注意要根据事件类型实现 `get_type` 方法,具体请参考[事件类型](../advanced/adapter#事件类型)。消息类型事件还应重写 `get_message` 和 `get_user_id` 等方法,例如: ```python {7,16,20,25,34,42} title=event.py from .message import Message class HeartbeatEvent(Event): """心跳时间,通常为元事件""" @override def get_type(self) -> str: return "meta_event" class MessageEvent(Event): """消息事件""" message_id: str user_id: str @override def get_type(self) -> str: return "message" @override def get_message(self) -> Message: # 返回事件消息对应的 NoneBot Message 对象 return self.message @override def get_user_id(self) -> str: return self.user_id class JoinRoomEvent(Event): """加入房间事件,通常为通知事件""" user_id: str room_id: str @override def get_type(self) -> str: return "notice" class ApplyAddFriendEvent(Event): """申请添加好友事件,通常为请求事件""" user_id: str @override def get_type(self) -> str: return "request" ``` ### Message Message 负责正确序列化消息,以便机器人插件处理。我们需要继承 `MessageSegment` 和 `Message` 两个类,并实现相关方法: ```python {9,12,17,22,27,30,36} title=message.py from typing import Type, Iterable from typing_extensions import override from nonebot.utils import escape_tag from nonebot.adapters import Message as BaseMessage from nonebot.adapters import MessageSegment as BaseMessageSegment class MessageSegment(BaseMessageSegment["Message"]): @classmethod @override def get_message_class(cls) -> Type["Message"]: # 返回适配器的 Message 类型本身 return Message @override def __str__(self) -> str: # 返回该消息段的纯文本表现形式,通常在日志中展示 return "text of MessageSegment" @override def is_text(self) -> bool: # 判断该消息段是否为纯文本 return self.type == "text" class Message(BaseMessage[MessageSegment]): @classmethod @override def get_segment_class(cls) -> Type[MessageSegment]: # 返回适配器的 MessageSegment 类型本身 return MessageSegment @staticmethod @override def _construct(msg: str) -> Iterable[MessageSegment]: # 实现从字符串中构造消息数组,如无字符串嵌入格式可直接返回文本类型 MessageSegment ... ``` 然后根据平台具体的消息类型,来实现各种 `MessageSegment` 消息段,具体可以参考以下适配器: - [OneBot V11](https://github.com/nonebot/adapter-onebot/blob/54270edbbdb2a71332d744f90b1a3d7f4bf6463a/nonebot/adapters/onebot/v11/message.py#L25-L259) - [QQ](https://github.com/nonebot/adapter-qq/blob/dc5d437e101f0e3db542de3300758a035ed7036e/nonebot/adapters/qq/message.py#L30-L520) - [Telegram](https://github.com/nonebot/adapter-telegram/blob/4a8633627e619245516767f5503dec2f58fe2193/nonebot/adapters/telegram/message.py#L13-L414) ## 适配器测试 关于适配器测试相关内容在这里不再展开,开发者可以根据需要进行合适的测试。这里为开发者提供几个常见问题的解决方法: 1. 在测试中无法导入 editable 模式安装的适配器代码。在 pytest 的 `conftest.py` 内添加如下代码: ```python title=tests/conftest.py from pathlib import Path import nonebot.adapters nonebot.adapters.__path__.append( # type: ignore str((Path(__file__).parent.parent / "nonebot" / "adapters").resolve()) ) ``` 2. 需要计算适配器测试覆盖率,请在 `pyproject.toml` 中添加 pytest 配置: ```toml title=pyproject.toml [tool.pytest.ini_options] addopts = "--cov nonebot/adapters/{adapter-name} --cov-report term-missing" ``` ## 后续工作 在完成适配器代码的编写后,如果想要将适配器发布到 NoneBot 商店,我们需要将适配器发布到 PyPI 中,然后前往[商店](/store/adapters)页面,切换到适配器页签,点击**发布适配器**按钮,填写适配器相关信息并提交。 另外建议编写适配器文档或者一些插件开发示例,以便其他开发者使用我们的适配器。 ================================================ FILE: website/docs/developer/plugin-publishing.mdx ================================================ --- sidebar_position: 0 description: 在商店发布自己的插件 --- # 发布插件 import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; NoneBot 为开发者提供了分享插件的官方商店。本指南囊括**从创建项目到发布到 PyPI,最终提交商店审核**的全过程。 :::warning 警告 如果你的插件只是满足自用需求,则完全可以选择**不发布插件**。发布插件**不是**使用插件的必要条件。 NoneBot 社区对于插件有一定质量要求,对于不符合要求的插件,社区成员将会要求修改,直至符合要求后才能通过审核;如果长期未更新修改,社区将会关闭当前请求,之后如需发布请重新提交发布插件请求。相应的要求会在本章节以下部分介绍。 ::: :::tip 提示 本章节仅包含插件发布流程指导,插件开发请查阅前述章节。 ::: ## 准备工作 ### 插件命名规范 NoneBot 插件使用下述命名规范: - 对于**项目名**,建议统一以 `nonebot-plugin-` 开头,之后为拟定的插件名字,词间用横杠 `-` 分隔; - **项目名**用于代码仓库名称、PyPI 包的发布名称等; - 本文使用 `nonebot-plugin-{your-plugin-name}` 为例。 - 对于**模块名**,建议与**项目名**一致,但词间用下划线 `_` 分隔,即统一以 `nonebot_plugin_` 开头,之后为拟定的插件名字; - **模块名**用于程序导入使用,应为插件文件(夹)的名称; - 本文使用 `nonebot_plugin_{your_plugin_name}` 为例。 ### 项目结构 :::tip 提示 本段所述的项目结构仅作推荐,不做强制要求。 ::: 插件程序本身结构可参考[插件结构](../tutorial/create-plugin.md#插件结构)一节,唯一区别在于,插件包可以直接处于项目顶层。 插件项目的一种组织结构如下: ```tree 📦 nonebot-plugin-{your-plugin-name} ├── 📂 nonebot_plugin_{your_plugin_name} │ ├── 📜 __init__.py │ └── 📜 config.py ├── 📜 pyproject.toml └── 📜 README.md ``` 功能开发可以在 `__init__.py` 中进行或在包内部创建其他模块并在 `__init__.py` 中导入。 ### 从项目模板开始 为降低新手门槛,我们提供三条清晰、完整、可复制的发布路径。 :::tip 提示 你只需选择一条与你习惯一致的路径,**完整跟随即可成功发布**。无需在不同工具间切换或猜测配置。 ::: NoneBot 生态目前有如下插件项目模板: - [RF-Tar-Railt/nonebot-plugin-template](https://github.com/RF-Tar-Railt/nonebot-plugin-template) 此路径使用 **PDM** 项目管理器,符合 PEP 621 标准,自动化程度高。 - [fllesser/nonebot-plugin-template](https://github.com/fllesser/nonebot-plugin-template) 此路径使用 **uv** 项目管理器和 **PoeThePoet** 任务运行器,构建速度快,适合追求效率的开发者。 - [A-kirami/nonebot-plugin-template](https://github.com/A-kirami/nonebot-plugin-template) 此路径使用 **Poetry** 项目管理器,适合熟悉传统 Python 生态的开发者。 #### 1. 创建项目 1. 访问上述三个模板之一。 2. 点击 **“Use this template”** → **“Create a new repository”**。 3. 仓库名称填写:`nonebot-plugin-{your-plugin-name}`(此部分以 `nonebot-plugin-weather` 为例)。 4. 点击 **“Create repository from template”**。 #### 2. 配置发布权限 1. 进入新仓库 → **Settings** → **Actions** → **General**。 2. 在 **Workflow permissions** 下,勾选 **“Read and write permissions”** → 点击 **Save**。 #### 3. 全局替换项目信息 在仓库中点击 **“Add file”** → **“Create new file”**,创建一个空文件 `LICENSE`,选择开源协议并提交(此操作会触发工作流)。 然后在本地克隆仓库,使用编辑器对以下内容进行**全局替换**: :::tip 提示 此部分以“天气插件”为例,实际的替换内容应该根据你所创建的插件名称等相应调整。 ::: | 原内容 | 替换为 | | ------------------------------ | ---------------------------------- | | `nonebot-plugin-template` | `nonebot-plugin-weather` | | `nonebot_plugin_template` | `nonebot_plugin_weather` | | `` | `天气查询` | | `` | `查询指定城市的实时天气与未来预报` | | `` | `你的GitHub用户名` | | `` | `你的邮箱` | #### 4. 安装依赖与开发 ```bash # 安装 PDM(若未安装) curl -sSL https://pdm-project.org/install-pdm.py | python3 - # 安装项目依赖(自动创建虚拟环境) pdm sync # 添加新依赖(如 httpx) pdm add httpx ``` ```bash # 安装 uv(Windows) powershell -c "irm https://astral.sh/uv/install.ps1 | iex" # 安装 uv(macOS/Linux) curl -LsSf https://astral.sh/uv/install.sh | sh # 安装所有依赖(含 dev) uv sync --all-groups -p 3.12 # 添加新依赖 uv add httpx ``` ```bash # 安装 Poetry(推荐方式) curl -sSL https://install.python-poetry.org | python3 - # 安装项目依赖 poetry install # 添加新依赖 poetry add httpx ``` #### 5. 更新版本并发布 [bump-my-version](https://github.com/callowayproject/bump-my-version) 是一个功能强大、可配置的 Python 项目版本更新工具,支持自动提交到 Git 等 VCS。 ```bash # 安装 bump-my-version pdm add --dev bump-my-version # 更新 patch 版本 pdm run bump patch # 推送 tag 触发发布 git push origin --tags ``` ```bash # 更新 patch 版本 uv run poe bump patch # 推送 tag 触发发布 git push origin --tags ``` ```bash # 安装 bump-my-version poetry add --dev bump-my-version # 更新 patch 版本 poetry run bump patch # 推送 tag 触发发布 git push origin --tags ``` 需要安装 PDM 插件 [pdm-bump](https://github.com/carstencodes/pdm-bump)。 ```bash # 安装 pdm-bump pdm self add pdm-bump # 更新 patch 版本 pdm bump patch # 推送 tag 触发发布 git push origin --tags ``` ```bash # 更新 patch 版本 uv version --bump patch # 创建相应提交与标签 git add pyproject.toml git commit -m "chore: release v0.1.1" # 替换为实际的版本号 git tag v0.1.1 # 替换为实际的版本号 # 推送 tag 触发发布 git push origin --tags ``` ```bash # 更新版本(自动提交并打标签) poetry version patch # 推送 tag 触发发布 git push origin --tags ``` 手动更新 `pyproject.toml` 中的 `version` 字段,然后推送 tag 触发发布工作流 ```bash git add pyproject.toml git commit -m "chore: release v0.1.1" # 替换为实际的版本号 git tag v0.1.1 # 替换为实际的版本号 git push origin --tags ``` 推送 `v*` 标签后,模板提供的 GitHub Actions 工作流将自动构建并发布到 PyPI。 #### 6. 发布到 [PyPI](https://pypi.org) 不同模板使用的发布方式可能不同,具体配置流程参考对应模板的详细使用指南。 根据选用的构建系统,在项目的 `pyproject.toml` 中填入必要信息后进行构建与发布。 :::tip 提示 不同构建工具的使用可能存在差别。本文仅以 [`pdm`](https://pdm-project.org/zh/latest/), [`poetry`](https://python-poetry.org/docs/), [`setuptools`](https://setuptools.pypa.io/en/latest/) 构建系统**本地构建与发布**为示例讲解,其余构建/管理工具等和自动化构建的用法请读者自行探索。 ::: ```bash poetry publish --build # 构建并发布 # 等效于以下两个命令 poetry build # 只构建 poetry publish # 只发布先前的构建 ``` ```bash pdm publish # 构建并发布 # 等效于以下两个命令 pdm build # 只构建 pdm publish --no-build # 只发布先前的构建 ``` ```bash pip install build twine # 安装通用构建与发布工具 python -m build --sdist --wheel . # 只构建 twine upload dist/* # 只发布先前的构建 ``` :::tip 提示 发布前建议自行测试构建包是否可用,避免遗漏代码文件或资源文件等问题。 ::: ## 基本要求 无论你选择哪条路径,以下内容**必须**完成,否则无法通过商店自动检查: ### 能够正确加载 插件包必须能够被 NoneBot 正确加载,在商店审核中会通过 **NoneFlow** 自动化加载测试进行。 #### 依赖其他插件 如果插件依赖其他插件提供的功能,则**必须**在代码中使用 `require()` 来引入该插件,然后才能 `import` 该插件提供的功能。具体细节参阅[跨插件访问](../advanced/requiring.md)。 使用示例如下: ```python title=nonebot_plugin_weather/__init__.py from nonebot import require require("nonebot_plugin_apscheduler") from nonebot_plugin_apscheduler import scheduler ``` #### 不能零配置加载的插件 如果插件需要必要配置项才能正常导入,则**必须**在商店提交表单中填写必要的配置项内容。 但一种更好的做法是,**将插件设计为零配置即可加载**(允许缺少必要配置项时插件仍能正常导入,但不执行需要相应配置项的功能),尤其是对于一些必要配置含有敏感信息(如密钥、Token、API Key 等)的插件。这样可以避免在商店提交表单时填写敏感信息的风险。 ### 插件元数据 插件包**必须**填写元数据才能通过 **NoneFlow** 自动化检查。 下面是一个示例: ```python title=nonebot_plugin_weather/__init__.py from nonebot.plugin import PluginMetadata from .config import Config __plugin_meta__ = PluginMetadata( # 基本信息(必填) name="天气查询", # 插件名称 description="查询指定城市的实时天气与未来预报", # 插件介绍 usage="发送【天气 城市名】获取天气信息", # 插件用法 # 发布额外信息 type="application", # 插件分类 # 发布必填,当前有效类型有:`library`(为其他插件编写提供功能),`application`(向机器人用户提供功能)。 homepage="https://github.com/你的用户名/nonebot-plugin-weather", # 发布必填。 config=Config, # 插件配置项类,如果有配置类则必须填写。 supported_adapters={"~onebot.v11"}, # 支持的适配器集合,其中 `~` 在此处代表前缀 `nonebot.adapters.`,其余适配器亦按此格式填写。 # 若插件只使用了 NoneBot 基本抽象,应显式填写 None,否则应该列出插件支持的适配器。 ) ``` :::caution 注意 `__plugin_meta__` 变量**必须**处于插件最外层(如 `__init__.py` 中),否则无法正常识别。 一般做法是在 `__init__.py` 中定义 `__plugin_meta__`。 ::: #### 继承其他插件支持的适配器 如果你的插件依赖于其他插件提供的支持功能,而其他插件可能支持更少的适配器,这时就应该使用 [inherit_supported_adapters()](../api/plugin/load#inherit-supported-adapters) 函数来继承其他插件支持的适配器。 示例用法如下: ```python title=nonebot_plugin_weather/__init__.py from nonebot import require from nonebot.plugin import PluginMetadata, inherit_supported_adapters from .config import Config require("nonebot_plugin_alconna") # 必须先 require 才能被 inherit_supported_adapters 处理 __plugin_meta__ = PluginMetadata( name="天气查询", description="查询指定城市的实时天气与未来预报", usage="发送【天气 城市名】获取天气信息", type="application", homepage="https://github.com/你的用户名/nonebot-plugin-weather", config=Config, supported_adapters=inherit_supported_adapters("nonebot_plugin_alconna"), # 继承 nonebot_plugin_alconna 插件的适配器支持列表 ) ``` ### 准备项目主页 通常可以使用 GitHub 项目页面作为项目主页,在 `README.md` 文件中编写插件介绍等内容。 内容大致包括: - 插件功能介绍; - 安装方法 - **必须**有 NB-CLI 方式安装 - 可选依赖可以给出其他安装方式 - **不得**使用旧式的 `bot.py` 配置 - 插件配置项(如 `Config` 类字段,若无可跳过) - 插件设置的触发规则(若无可跳过) - 插件的其它用法(按需编写) - 效果图、权限说明(按需编写) ## 质量要求 以下内容**强烈建议**完成,否则社区成员将会要求修改: ### 依赖管理原则 - **必须**包含 `nonebot2`。 - **必须**将插件直接使用的适配器加入依赖列表,如:使用 OneBot 适配器的插件应添加 `nonebot-adapter-onebot` 依赖; - **禁止**使用 `==` 锁定单一版本,使用 `>=` 或 `~=`。 - **禁止**添加 `nonebot`(V1)作为依赖。 - 所有在代码中 `import` 的第三方库,必须在 `pyproject.toml` 的 `dependencies` 中列出。 ### 避免误用同步操作 NoneBot 是一个异步框架,插件中**禁止**使用任何可能阻塞事件循环的同步操作,例如: - 同步 HTTP 请求(如 `requests` 库); **推荐**操作(以 `httpx` 为例): ```python import httpx async with httpx.AsyncClient() as client: response = await client.get("https://api.example.com/data") # 异步操作,不阻塞机器人 ``` **禁止**操作: ```python import requests requests.get("https://api.example.com/data") # 同步操作,会阻塞机器人 ``` - 其他可能长时间运行阻塞事件循环的操作。 ### 本地文件存储 如果插件需要在本地存储数据、配置或缓存文件,**必须**使用 [`nonebot-plugin-localstore`](https://github.com/nonebot/plugin-localstore) 管理,具体细节参阅[本地存储](../best-practice/data-storing.md)章节。 参考示例: ```python title=nonebot_plugin_weather/__init__.py from pathlib import Path from nonebot import require require("nonebot_plugin_localstore") import nonebot_plugin_localstore as store # 获取插件缓存文件(夹)路径 weather_cache_dir: Path = store.get_plugin_cache_dir() weather_cache_file: Path = store.get_plugin_cache_file("cache.json") # 获取插件配置文件(夹)路径 weather_config_dir: Path = store.get_plugin_config_dir() weather_config_file: Path = store.get_plugin_config_file("config.toml") # 获取插件数据文件(夹)路径 weather_data_dir: Path = store.get_plugin_data_dir() weather_data_file: Path = store.get_plugin_data_file("resource-index.json") ``` ## 商店审核 ### 提交申请 完成在 PyPI 的插件发布流程后,前往[商店](/store/plugins)页面,切换到插件页签,点击 **发布插件** 按钮。 在弹出的插件信息提交表单内,填入您所要发布的相应插件信息。请注意,如果插件需要必要配置项才能正常导入,请在“插件配置项”中填写必要的内容(请勿填写密钥等敏感信息)。 完成填写后,点击 **发布** 按钮,这将自动跳转 NoneBot 仓库内的“发布插件”页面。确认信息无误后点击页面下方的 `Submit new issue` 按钮进行最终提交即可。 ### 等待插件审核 插件发布 Issue 创建后,将会经过 **NoneFlow Bot** 的自动前置检查,以确保插件信息正确无误、插件能被正确加载。 :::tip 提示 若插件检查未通过或信息有误,**不必**关闭当前 Issue。只需更新插件并上传到 PyPI/修改信息后勾选插件测试勾选框即可重新触发插件检查。 ::: 之后,NoneBot 的维护者和一些志愿者会初步检查插件代码,帮助减少该插件的问题。 完成这些步骤后,您的插件将会被自动合并到[商店](/store/plugins),而您也将成为 [**NoneBot 贡献者**](https://github.com/nonebot/nonebot2/graphs/contributors)的一员。 ================================================ FILE: website/docs/editor-support.md ================================================ --- sidebar_position: 2 description: 配置编辑器以获得最佳体验 --- # 编辑器支持 框架基于 [PEP 484](https://www.python.org/dev/peps/pep-0484/)、[PEP 561](https://www.python.org/dev/peps/pep-0561/)、[PEP 8](https://www.python.org/dev/peps/pep-0008/) 等规范进行开发并且**拥有完整类型注解**。框架使用 Pyright(Pylance)工具进行类型检查,确保代码可以被编辑器正确解析。 ## CLI 脚手架提供的编辑器工具支持 在使用 NB-CLI [创建项目](./quick-start.mdx#创建项目)时,如果选择了用于插件开发的 `simple` 模板,其会根据选择的开发工具,**自动配置项目根目录下的 `.vscode/extensions.json` 文件**,以推荐最匹配的 VS Code 插件,同时自动将相应的预设配置项写入 `pyproject.toml` 作为“开箱即用”配置,从而提升开发体验。 ```bash [?] 选择一个要使用的模板: simple (插件开发者) ... [?] 要使用哪些开发工具? ``` ### 支持的开发工具 1. Pyright (Pylance) [VS Code 插件](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance) | [项目](https://github.com/microsoft/pyright) | [文档](https://microsoft.github.io/pyright/) 由微软开发的 Python 静态类型检查器和语言服务器,提供智能感知、跳转定义、查找引用、实时错误检查等强大功能。 作为 VS Code 官方推荐的 Python 语言服务器,与 Pylance 扩展配合使用,能提供最流畅、最准确的代码补全和类型推断体验,是绝大多数开发者的首选。 2. Ruff [VS Code 插件](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff) | [项目](https://github.com/astral-sh/ruff) | [文档](https://docs.astral.sh/ruff/) 一个用 Rust 编写的超快 Python 代码格式化和 lint 工具,完全兼容 `black`、`isort`、`flake8` 等主流工具的规则。 速度极快(比 `black` 和 `flake8` 快 100 倍以上),配置简单,能自动格式化代码并检测潜在错误、代码风格问题(尤其是误用同步网络请求库),是提升代码质量和开发效率的必备利器。 3. MyPy [VS Code 插件](https://marketplace.visualstudio.com/items?itemName=matangover.mypy) | [项目](https://github.com/python/mypy) | [文档](https://mypy.readthedocs.io/en/stable/index.html) 一个官方实现的 Python 静态类型检查器,通过分析代码中的类型注解来发现类型错误。 4. BasedPyright [VS Code 插件](https://marketplace.visualstudio.com/items?itemName=detachhead.basedpyright) | [项目](https://github.com/DetachHead/basedpyright) | [文档](https://docs.basedpyright.com/) 一个基于 Pyright 的、由社区维护的替代性 Python 语言服务器,旨在提供更优的类型检查支持与接近 Pylance 的更好的使用体验。 相较于 Pylance,BasedPyright 允许配合 VS Code 之外的其他编辑器使用,同时也复刻了部分 Pylance 限定的功能。 如果您是高级用户,希望尝试 Pylance 的替代方案,或遇到 Pylance 在特定环境下的兼容性问题,可以考虑使用 BasedPyright。 :::caution 提示 为避免 `Pylance` 和 `BasedPyright` 相互冲突导致配置混乱甚至异常,脚手架默认不允许在创建项目时同时配置这两者。 如果确实需要同时使用,请在创建项目时选择 Pylance/Pyright 并根据[相关文档](https://docs.basedpyright.com/latest/installation/ides/#vscode-vscodium)进行手动配置。 ::: ### 配置效果 选择上述工具后,NB-CLI 会在您的项目根目录下生成一个 `.vscode/extensions.json` 文件并在 `pyproject.toml` 文件中写入相应的配置项。当您在 VS Code 中打开此项目时,IDE 会自动弹出提示,建议您安装这些推荐的扩展,一键即可完成开发环境的初始化,让您可以立即开始编写代码,无需手动搜索和安装插件。 ## 编辑器推荐配置 ### 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` 前往编辑。 ================================================ FILE: website/docs/ospp/2021.md ================================================ --- sidebar_position: 0 description: 开源软件供应链点亮计划 - 暑期 2021 mdx: format: md --- # 暑期 2021 **开源软件供应链点亮计划 - 暑期 2021** 是**中国科学院软件研究所**与 **openEuler 社区**共同举办的一项面向高校学生的暑期活动,旨在鼓励在校学生积极参与开源软件的开发维护,促进优秀开源软件社区的蓬勃发展。关于具体的活动规划、报名方式,请查看该活动的 [官网](https://summer.iscas.ac.cn/) 和 [帮助文档](https://summer.iscas.ac.cn/help/)。 NoneBot 社区有幸作为开源社区参与了本次活动,下面列出了目前我们已经发布的项目,欢迎感兴趣的同学在上面给出的活动官网报名,或通过 [contact@nonebot.dev](mailto:contact@nonebot.dev) 联系我们。 ## NoneBot v1 ### 更新 NoneBot v1 文档中的“指南”部分 由于 NoneBot v1 和 aiocqhttp 最初基于的 QQ 机器人平台不再提供服务,CQHTTP 接口也转型且改名为 OneBot 标准,目前 NoneBot v1 文档的“指南”部分和 aiocqhttp 文档有部分过时内容需要更新。我们希望将其中与旧的机器人平台相关的内容改为基于 go-cqhttp 或通用的 OneBot 表述,同时对 NoneBot v1 的 awesome-bot 示例做一次全面检查,修改其中可能已经不可用的部分。 **难度**:低 **导师**:[@cleoold](https://github.com/cleoold) **产出要求** - 修改“指南”文档和 aiocqhttp 文档中与旧的 QQ 机器人平台相关的部分 - 检查 awesome-bot 示例是否有已经过时/不可用的地方,并更新/修复 - 修改“图灵机器人”案例,使用其它 AI 聊天 API 提供商(需先做简单调研) **技术要求** - 熟悉 Python 编程语言及 asyncio 机制 - 了解 Git 基本用法 - 了解聊天机器人基本开发过程 - 了解 VuePress 更佳 ### NoneBot v1 API 文档自动生成 目前 NoneBot v1 的文档中“API”部分是手动编写的,在更新代码接口的同时需要手动更新文档,可能造成文档与代码不匹配,形成额外的维护成本。我们希望将 API 文档改为直接编写在 Python docstring 中,通过工具自动生成 API 文档。 **难度**:中 **导师**:[@cleoold](https://github.com/cleoold) **产出要求** - 调研市面上常见的 Python API 文档生成工具 - 在代码中补充 API 文档 - 编写或应用开源工具自动生成 API 文档 - 配置 GitHub Actions 或其它 CI 自动化构建和部署 API 文档 **技术要求** - 熟悉 Python 编程语言及 asyncio 和 Type Hints - 了解 Git 基本用法 - 了解 Sphinx 等文档生成工具更佳 - 了解 GitHub Actions 等 CI 工具更佳 ## NoneBot v2 ### NoneBot v2 自动化测试框架“NoneBug” 在聊天机器人的开发过程中,一套自动化的测试机制是非常重要的,特别是对于 NoneBot 2 这类为大型机器人开发而设计的项目来说,需要手动测试每一个边际条件是非常痛苦的。我们希望能够开发一款基于 NoneBot 2 插件机制的自动化测试框架,为 NoneBot 2 用户提供一套易用便捷、高度灵活的自动化测试框架。 **难度**:高 **导师**:[@yanyongyu](https://github.com/yanyongyu) **产出要求** - 调研现有的 Python 和其它语言集成测试框架 - 设计 NoneBug 的用户 API 和实现方式 - 实现 NoneBug 自动化测试框架 - 编写详细的使用文档 **技术要求** - 熟悉 Python 编程语言及 asyncio 和 Type Hints - 了解 Git 基本用法 - 了解 NoneBot v2 的基本原理和使用方式 - 了解主流的 Python 自动化测试框架 ### NoneBot v2 Telegram 适配器 目前 NoneBot v2 已支持 OneBot、Mirai HTTP API、钉钉协议,社区反馈有更多的平台需求,希望能在 NoneBot v2 获得更多的跨平台支持,提高机器人的便携性。同时,我们也希望随着新平台加入,提升现有 NoneBot v2 核心代码的平台通用性。Telegram 是一款较为广泛使用的安全即时聊天软件,同时其官方提供了丰富的聊天机器人 API,因此我们希望为 NoneBot v2 编写一个 Telegram 适配器来支持 Telegram 机器人的开发。 **难度**:中 **导师**:[@yanyongyu](https://github.com/yanyongyu) **产出要求** - 调研 Telegram Bot API 以及 WebHook 等官方接口 - 编写 Telegram 适配器并能够使用 - 代码遵守项目 Contributing 规范 **技术要求** - 熟悉 Python 编程语言及 asyncio 和 Type Hints - 了解 Git 基本用法 - 了解 Web 开发相关知识 - 了解 Sphinx 等文档生成工具更佳 ### NoneBot v2 飞书适配器 目前 NoneBot v2 已支持 OneBot、Mirai HTTP API、钉钉协议,社区反馈有更多的平台需求,希望能在 NoneBot v2 获得更多的跨平台支持,提高机器人的便携性。同时,我们也希望随着新平台加入,提升现有 NoneBot v2 核心代码的平台通用性。飞书是目前企业用户广泛使用的即时聊天和协作软件,其官方提供了丰富的聊天机器人 API,因此我们希望为 NoneBot v2 编写一个飞书适配器来支持飞书机器人的开发。 **难度**:中 **导师**:[@yanyongyu](https://github.com/yanyongyu) **产出要求** - 调研飞书机器人 API 以及 WebHook 等官方接口 - 编写飞书适配器并能够使用 - 代码遵守项目 Contributing 规范 **技术要求** - 熟悉 Python 编程语言及 asyncio 和 Type Hints - 了解 Git 基本用法 - 了解 Web 开发相关知识 - 了解 Sphinx 等文档生成工具更佳 ## OneBot ### 设计 OneBot v12 接口标准 目前的 OneBot 标准的 v11 版本仍然与 QQ 平台有较多耦合,我们希望在 v12 去掉与 QQ 耦合的历史包袱,形成一个通用的、可扩展的、易于使用的同时易于实现的聊天机器人接口标准。 **难度**:中 **导师**:[@richardchien](https://github.com/richardchien) **产出要求** - 调研各聊天机器人平台的官方/非官方接口特点 - 通用化 OneBot 核心 API,分离 QQ 特定的 API,去掉无用 API - 优化现有的通信、消息表示机制 - 补充 QQ 特定的缺失 API - 文档需符合风格指南 **技术要求** - 熟悉至少两个聊天平台的聊天机器人开发 - 了解 Git 基本用法 - 了解使用不同语言编写聊天机器人时的常用实践 - 对文档的优雅性与美观性有追求更佳 ### 实现 Rust 版 libonebot 目前最常用的 OneBot 实现包括 go-cqhttp、onebot-kotlin、node-onebot 等,这些实现都各自重复实现了 Web 通信、消息解析、配置读写等功能,当社区中的开发者想针对一个新的聊天平台实现 OneBot 时,他们往往同样需要再次实现类似逻辑。我们希望使用 Rust 编写一个 libonebot 模块,该模块实现所有 OneBot 实现所共享的功能,从而方便其他开发者们使用 Rust 快速编写具体的 OneBot 实现。同时,我们希望借此项目在聊天机器人社区中推广 Rust 编程语言。 > 注:这里的逻辑是 libonebot + 针对某聊天平台的对接代码 = 某聊天平台的 OneBot 实现,libonebot 要做的是让 OneBot 实现的开发者只需编写针对特定聊天平台的对接代码,而无需关心 OneBot 标准定义的通信方式、消息格式等。 **难度**:高 **导师**:[@richardchien](https://github.com/richardchien) **产出要求** - 实现所有 OneBot 实现所共享的功能,包括 Web 通信、消息解析、配置读写等 - 充分考虑同时兼容 OneBot v11 和 v12 接口 - 能够根据用户(OneBot 实现的开发者)所实现的接口自动实现类似 get_available_apis 等接口 - 编写详细的使用文档 - 如果可能,与 v12 设计项目联动,实现第一手 v12 支持 **技术要求** - 熟悉聊天机器人开发 - 熟悉 Rust Web 开发 ### 实现自选语言版 libonebot 目前最常用的 OneBot 实现包括 go-cqhttp、onebot-kotlin、node-onebot 等,这些实现都各自重复实现了 Web 通信、消息解析、配置读写等功能,当社区中的开发者想针对一个新的聊天平台实现 OneBot 时,他们往往同样需要再次实现类似逻辑。我们希望使用 Python、Go、Kotlin、Node、PHP、C#.NET 等主流语言(任选一个)编写 libonebot 模块,该模块实现所有 OneBot 实现所共享的功能,从而方便其他开发者们使用对应语言快速编写具体的 OneBot 实现。 > 注:这里的逻辑是 libonebot + 针对某聊天平台的对接代码 = 某聊天平台的 OneBot 实现,libonebot 要做的是让 OneBot 实现的开发者只需编写针对特定聊天平台的对接代码,而无需关心 OneBot 标准定义的通信方式、消息格式等。 **难度**:中 **导师**:[@richardchien](https://github.com/richardchien) **产出要求** - 实现所有 OneBot 实现所共享的功能,包括 Web 通信、消息解析、配置读写等 - 充分考虑同时兼容 OneBot v11 和 v12 接口 - 编写详细的使用文档 - 如果可能,实现更多附加特性,如根据用户(OneBot 实现的开发者)所实现的接口自动实现类似 get_available_apis 等接口、实现第一手 v12 支持等 **技术要求** - 熟悉聊天机器人开发 - 熟悉所选语言的 Web 开发 ================================================ FILE: website/docs/ospp/2022.md ================================================ --- sidebar_position: 1 description: 开源之夏 - 暑期 2022 mdx: format: md --- # 暑期 2022 **开源之夏 - 暑期 2022** 是由**开源软件供应链点亮计划**发起、由**中国科学院软件研究所**与 **openEuler 社区**主办的一项面向高校学生的暑期活动,类似 Google Summer of Code(GSoC),旨在鼓励在校学生积极参与开源软件的开发维护,促进优秀开源软件社区的蓬勃发展。关于具体的活动规划、报名方式,请查看该活动的 [官网](https://summer-ospp.ac.cn/) 和 [帮助文档](https://summer-ospp.ac.cn/help/)。 NoneBot 社区有幸作为开源社区 [参与](https://summer-ospp.ac.cn/#/org/orgdetail/e1fb5b8d-125a-4138-b756-25bd32c0a31a/) 了本次活动,下面列出了目前我们已经发布的项目,欢迎感兴趣的同学加入 QQ 群 [737131827](https://jq.qq.com/?_wv=1027&k=PEgyGeEu) 或通过 [contact@nonebot.dev](mailto:contact@nonebot.dev) 联系我们。 ## NoneBot2 命令行 CLI 交互体验升级 NoneBot2 为用户提供了命令行脚手架 ──`nb-cli`,辅助用户更好地上手项目以及进行开发。nb-cli 主要包括:创建项目、运行项目、安装与卸载插件、部署项目等功能。随着 NoneBot2 Beta 版本的发布,脚手架功能存在一定的定位不明确、功能体验不佳。本项目旨在重新设计 nb-cli 功能框架,完善功能,优化用户体验。 **难度**:进阶 **导师**:[@yanyongyu](https://github.com/yanyongyu) **产出要求** - 设计 nb-cli 功能框架 - 明确各功能模块 - 设计用户交互模式 - 完成 nb-cli 主要功能代码 - 项目管理 - 插件管理 - 其它 - 同步更新使用文档 **技术要求** - 熟悉 Python 命令行交互代码编写 - 熟悉 NoneBot2 框架功能 - 熟悉 NoneBot2 项目组织方式 **成果仓库** - - ## NoneBot2 命令行即时交互通信设计与实现 NoneBot2 在早期提供了基于网页的 nonebot-plugin-test 插件,无需平台适配接入即可对机器人进行测试,方便了开发者直观的感受机器人文本交互功能。我们希望提供一款基于命令行的适配器/驱动器,用于无平台适配接入、可以运行机器人的场景进行功能体验或测试。 **难度**:进阶 **导师**:[@mnixry](https://github.com/mnixry) **产出要求** - 设计命令行与 NoneBot2 通信模式 - 直接调用/HTTP/WebSocket - 设计命令行交互界面 - 实现相应适配器/驱动器 - 同步更新使用说明文档 **技术要求** - 熟悉 Python 命令行交互代码编写 - 熟悉 NoneBot2 框架功能 - 熟悉 NoneBot2 项目组织方式 **成果仓库** - ## NoneBot2 用户上手与深入教程设计 NoneBot2 为用户提供了详细的文档介绍,辅助用户更好的上手项目以及进行开发。文档分为基础与进阶两个部分。基础部分帮助新用户快速上手开发,主要包括:安装 NoneBot2、使用脚手架、创建配置项目、使用适配器、加载插件、定义消息事件、处理消息事件、调用平台 API 等。进阶部分向已经熟悉开发流程的用户介绍更多高级技巧,主要包括:NoneBot2 工作原理、定时任务、权限控制、钩子函数、跨插件访问、单元测试、发布插件等。目前文档对于用户而言过于费解,导致用户难以理解 NoneBot2 开发。本项目旨在优化文档内容,使其更加通俗易懂,不让文档成为用户上手的阻碍,同时完善进阶内容,让有更复杂需求的用户,同样能从文档中受益。 相关 issue: - - **难度**:进阶 **导师**:[@SK-415](https://github.com/SK-415) **产出要求** - 文档通俗易懂 - 附有适当的图片指引(如 asciinema) - 内容完整,由浅入深 - 适当的界面美化,合理分配布局 **技术要求** - 熟悉文档结构组织与语言表达 - 熟悉 NoneBot2 框架功能 - 熟悉 NoneBot2 项目组织方式 **成果仓库** - ================================================ FILE: website/docs/ospp/2023.md ================================================ --- sidebar_position: 2 description: 开源之夏 - 暑期 2023 mdx: format: md --- # 暑期 2023 **开源之夏 - 暑期 2023** 是由**开源软件供应链点亮计划**发起、由**中国科学院软件研究所**与 **openEuler 社区**主办的一项面向高校学生的暑期活动,类似 Google Summer of Code(GSoC),旨在鼓励在校学生积极参与开源软件的开发维护,促进优秀开源软件社区的蓬勃发展。关于具体的活动规划、报名方式,请查看该活动的 [官网](https://summer-ospp.ac.cn/) 和 [帮助文档](https://summer-ospp.ac.cn/help/)。 NoneBot 社区有幸作为开源社区 [参与](https://summer-ospp.ac.cn/org/orgdetail/e1fb5b8d-125a-4138-b756-25bd32c0a31a?lang=zh) 了本次活动,下面列出了目前我们已经发布的项目,欢迎感兴趣的同学通过 [contact@nonebot.dev](mailto:contact@nonebot.dev) 联系我们。 ## NoneBot 项目管理图形化面板 NoneBot 目前提供了开箱即用的命令行脚手架来帮助初次使用的用户更快的上手编写应用。但是,对于未有一定开发经验的用户,命令行的使用仍具有一定的困难。此外,其他项目如 koishi、vue 等,均可通过图形化界面的形式为用户提供更便捷的项目开发。因此,我们希望借助现有命令行脚手架的可扩展特性,提供一个项目管理面板服务,以网页的形式帮助用户开发 NoneBot 应用。 **难度**:进阶 **导师**:[@mnixry](https://github.com/mnixry) **产出要求** - 设计并实现项目管理面板相关功能 - 创建与管理项目 - 配置与运行项目 - NoneBot 插件管理 - 实现相应 nb-cli 插件提供面板服务 - 代码符合 NoneBot Contributing 规范 **技术要求** - 熟悉 nb-cli 相关功能 - 熟悉 NoneBot 框架功能 - 熟悉前后端相关实现方式 **成果仓库** - ## NoneBot Discord 适配器 NoneBot 作为一个跨平台聊天机器人框架,目前已有 OneBot、飞书、Telegram、QQ 频道等诸多平台的适配支持。作为众多用户期待的平台适配之一,我们希望借此机会接入 Discord 聊天机器人。 **难度**:进阶 **导师**:[@iyume](https://github.com/iyume) **产出要求** - 调研 Discord Bot 相关功能与接口 - 设计与编写 NoneBot Discord 适配器 - 代码符合 NoneBot Contributing 规范 **技术要求** - 熟悉 NoneBot 框架功能 - 熟悉 NoneBot 各模块职责与适配器编写 **成果仓库** - ## NoneBot 数据库支持插件 NoneBot 的插件系统为用户实现应用提供了极高的便捷性,但因此也增加了插件统一管理的难度。目前,我们发现许多用户发布的插件中存在文件存储结构化数据、数据存放散乱等现象,同时插件间也可能产生冲突。因此,我们希望提供一个统一的数据存储与管理方式,便于用户读写应用数据。 **难度**:进阶 **导师**:[@yanyongyu](https://github.com/yanyongyu) **产出要求** - 设计并实现 ORM 插件 - 提供关系模型定义功能 - 提供模型迁移与管理功能 - 能较好的支持 Python 类型检查与推导 - 编写相应的用户使用文档 - 代码符合 NoneBot Contributing 规范 **技术要求** - 熟悉 NoneBot 框架功能与插件编写 - 熟悉 SQLAlchemy 等 ORM 框架 - 熟悉 SQLAlchemy ORM - 熟悉 alembic 等迁移工具 - 熟悉 nb-cli 插件编写 **成果仓库** - ================================================ FILE: website/docs/ospp/2024.md ================================================ --- sidebar_position: 3 description: 开源之夏 - 暑期 2024 mdx: format: md --- # 暑期 2024 **开源之夏 - 暑期 2024** 是**中国科学院软件研究所**发起的**开源软件供应链点亮计划**系列暑期活动,旨在鼓励高校学生积极参与开源软件的开发维护,促进优秀开源软件社区的蓬勃发展。活动联合各大开源社区,针对重要开源软件的开发与维护提供项目开发任务,并向全球高校学生开放报名。关于具体的活动规划、报名方式,请查看该活动的 [官网](https://summer-ospp.ac.cn/) 和 [帮助文档](https://summer-ospp.ac.cn/help/)。 NoneBot 社区有幸作为开源社区 [参与](https://summer-ospp.ac.cn/org/orgdetail/e1fb5b8d-125a-4138-b756-25bd32c0a31a?lang=zh) 了本次活动,下面列出了目前我们已经发布的项目,欢迎感兴趣的同学通过 [contact@nonebot.dev](mailto:contact@nonebot.dev) 联系我们。 ## NonePress 官网组件库更新与优化 NoneBot 官网目前采用基于 TailwindCSS 自研的 NonePress 组件库及 Docusaurus 框架进行构建。由于相关依赖版本迭代迅速,目前官网组件库已产生了较大的版本落后。本项目希望在跟进框架新版本的基础上,对文档整体视觉体验进行重新设计,提升页面的无障碍访问性,基于 React Hydrate 特性实现完整的静态网站生成(SSG)以提升搜索引擎优化(SEO)水平。在解决以上问题的基础上,可对网页的开发以及生产构建性能做相应的优化提升,例如在生产构建使用自有的 webpack loader、替换现有的热重载逻辑以减少开发环境启动耗时等。 **难度**:进阶 **导师**:[@yanyongyu](https://github.com/yanyongyu) **产出要求** - 基于 Docusaurus v3 重构 NonePress 组件库及相关插件 - 升级相关依赖并重新打造 Docusaurus theme(布局与组件) - 根据需求实现/修改 Docusaurus 插件使得官网内容构建正常 - 能够提升页面渲染性能与 MDX 相关能力 - 升级官网采用新版组件库 - Algolia 索引与 SEO 正常 - 桌面端与移动端显示正常 - 优化官网开发与生产构建体验 - (可选)优化官网部分页面 - 优化官网过长的 changelog - 优化官网插件商店的展示细节 **技术要求** - 熟练掌握 TS、PostCSS、TSX、MDX等相关技术 - 掌握 React、Docusaurus、tailwind css 等框架 - 熟悉静态网站生成 SSG、SEO 优化与 Algolia 索引原理等 **成果仓库** - ## NoneFlow 社区自动化工作流管理优化 NoneFlow 在 NoneBot 社区中承担着重要的角色,它由 NoneBot 框架基于 GitHub APP 编写而成,能够自动化的完成许多复杂流程的处理,如:用户请求提交插件到商店时进行自动化检测,并在人工审核通过后自动存储至 registry;定时自动更新 registry 内插件信息,跟进插件新版本情况等。但是,在长期的使用中发现了一些问题和不足的地方,例如:项目本身结构复杂耦合,添加新自动化流程与维护现有流程困难;目前采用了 GitHub 用户名作为插件作者名,但已有不少插件作者改名;插件存储至 registry 并定时更新,缺少统计相关信息以帮助商店更好的展示当前插件状态;插件作者想要修改插件信息时无法便捷的找到操作方式等。本项目希望针对以上问题与不足的地方进行修复与优化,提升用户体验。 **难度**:进阶 **导师**:[@uy/sun](https://github.com/he0119) **产出要求** - 重构现有工作流处理结构 - 整合现有 Issue、Pull Request、Git 相关操作 - 提供用户修改信息的处理方式 - 正确处理 PR 的 Open、Close、Draft 状态 - 修复流程中存在的问题 - 插件作者名正确展示 - registry 定时更新中需要插件测试环境隔离 - 在 registry 定时更新的同时提供统计数据 **技术要求** - 掌握 GitHub APP 开发 - 熟悉 GitHub REST API、GraphQL 等 - 熟悉 GitHub APP 权限限制 - 熟悉 NoneBot 框架与 Python 相关技术 - 熟悉 Git、GitHub Action、GitHub 工作流 **成果仓库** - ## NoneBlockly 低代码框架开发 经过深入分析社区反馈,我们发现部分新手因不熟悉编程概念或框架本身而遇到问题。为了解决初学者在使用面向开发者的聊天机器人框架 NoneBot 时遇到的挑战,我们计划引入 Blockly 提供低代码编程支持。通过减少常见的编码错误和降低入门门槛,使框架对初学者更加友好,从而提升用户体验并有助于 NoneBot 生态的成长。本项目将基于 Blockly 实现 NoneBot 插件的低代码编写,使得用户能够快速搭建聊天机器人。 **难度**:进阶 **导师**:[@mnixry](https://github.com/mnixry) **产出要求** - 实现 NoneBlockly 低代码开发框架 - 能够基于 Alconna 编写跨平台插件 - 确保插件对 Python 和 NoneBot 版本的兼容性 - 支持对多种类型 NoneBot 事件的响应 - 支持对 NoneBot 消息对象的便捷操作 - 集成 localstore 文件存储、apscheduler 定时任务、网络请求等常用功能 - 对接 NB-CLI 脚手架,通过脚手架扩展使用低代码框架 **技术要求** - 掌握 Python 与 NoneBot 框架的使用 - 熟悉 NoneBot 插件的开发,包括事件响应与消息处理等 - 熟悉 NoneBot 生态组件(Alconna、localstore、apscheduler等)的使用 - 了解 NB-CLI 脚手架的扩展开发 - 熟悉 Blockly 低代码框架的使用和开发 **成果仓库** - ================================================ FILE: website/docs/ospp/2025.md ================================================ --- sidebar_position: 4 description: 开源之夏 - 暑期 2025 mdx: format: md --- # 暑期 2025 **开源之夏 - 暑期 2025** 是**中国科学院软件研究所**发起的**开源软件供应链点亮计划**系列暑期活动,旨在鼓励高校学生积极参与开源软件的开发维护,培养和发掘更多优秀的开发者,促进优秀开源软件社区的蓬勃发展,助力开源软件供应链建设。活动联合各大开源社区,针对重要开源软件的开发与维护提供项目开发任务,并向全球高校学生开放报名。关于具体的活动规划、报名方式,请查看该活动的 [官网](https://summer-ospp.ac.cn/) 和 [帮助文档](https://summer-ospp.ac.cn/help/)。 NoneBot 社区有幸作为开源社区 [参与](https://summer-ospp.ac.cn/org/orgdetail/e1fb5b8d-125a-4138-b756-25bd32c0a31a?lang=zh) 了本次活动,下面列出了目前我们已经发布的项目,欢迎感兴趣的同学通过 [contact@nonebot.dev](mailto:contact@nonebot.dev) 联系我们。 ## NoneBot HTML 图片渲染插件 文字与图片一直是聊天机器人的两大主流交互方式,而图片的渲染一直是用户开发应用的一大痛点。常见的方式包括 PIL 图片编辑、浏览器渲染 HTML 截图等。PIL 图片编辑依赖人工构建图片布局,容易出现自适应问题,且提升图片特效、美观程度需要极大的开发成本。浏览器渲染方案通过 HTML 与 CSS 能够轻松完成美观自适应能力强的布局,但其部署门槛较高,难以支撑较大规模调用量。而其他轻量化渲染引擎通常不具有完整 HTML/CSS 现代化标准实现,且未提供 Python Binding 直接使用。 本项目希望调研并实现一种高效、便捷的图片渲染方案。该方案需要在保障跨平台一致性、最大程度保证 HTML 与 CSS 现代化标准的前提下,低成本(资源消耗与吞吐量)将 HTML 渲染为对应图片。 **难度**:进阶 **导师**:[@MelodyKnit](https://github.com/MelodyKnit) **产出要求** - 调研 HTML/CSS 渲染引擎 - 调研 litehtml 等渲染引擎 标准支持能力与兼容性 - 基于渲染引擎实现 HTML 图片渲染插件 - 将渲染引擎通过 binding 等方式集成为 Python 模块 - 基于集成模块实现 HTML 图片渲染能力 - 编写插件使用文档 **技术要求** - 掌握 Python 及其异步编程 - 熟悉 NoneBot 框架及其插件编写 - 了解浏览器与 HTML 渲染原理 **成果仓库** - ## NB-CLI 命令行工具交互优化 NB-CLI 作为 NoneBot 生态的核心入门与管理工具,主要负责新手引导项目创建、项目运行以及插件管理几大功能。目前该脚手架工具仍存在几点缺陷: - 作为插件管理工具,由于存储数据的局限性,无法很好地展示用户项目当前安装插件状态,并进行卸载等操作; - 当前插件管理高度依赖云端 registry 提供插件信息,在离线情况下完全无法使用; - 由于插件信息繁多,工具未能向用户展示充分的信息,交互复杂 体验较差。 以上问题对用户使用 NB-CLI 管理项目插件造成了极大的阻碍。 本项目希望重点针对插件管理部分,重构工具插件管理模块,完善框架缺陷,并通过缓存等方式确保可用性。其次,调研同类工具方案与 TUI 等相关技术,优化信息展示能力、用户交互方式,提升工具整体交互体验。 **相关链接** - https://github.com/nonebot/nb-cli/issues/138 - https://github.com/nonebot/nb-cli/issues/140 **难度**:基础 **导师**:[@yanyongyu](https://github.com/yanyongyu) **产出要求** - 重构 NB-CLI 插件管理模块 - 优化项目插件信息存储方式,支持列出、卸载插件等操作 - 通过缓存 registry 数据等方式确保离线场景的可用性 - 提升 NB-CLI 交互体验 - 调研同类工具方案与 TUI 等相关技术 - 优化 registry 多字段信息展示能力 - 基于 TUI 等技术优化用户交互方式,提升整体交互体验 **技术要求** - 熟练掌握 Python 及其异步编程 - 熟悉 NoneBot 框架与 NB-CLI 使用方法 - 了解 TUI 等终端交互技术 **成果仓库** - ================================================ FILE: website/docs/quick-start.mdx ================================================ --- sidebar_position: 1 description: 尝试使用 NoneBot options: menu: - category: tutorial weight: 10 --- import Asciinema from "@site/src/components/Asciinema"; import Messenger from "@site/src/components/Messenger"; # 快速上手 :::caution 前提条件 - 请确保你的 Python 版本 >= 3.9 - **我们强烈建议使用虚拟环境进行开发**,如果没有使用虚拟环境,请确保已经卸载可能存在的 NoneBot v1!!! ```bash pip uninstall nonebot ``` ::: 在本章节中,我们将介绍如何使用脚手架来创建一个 NoneBot 简易项目。项目将基于 nb-cli 脚手架运行,并允许我们从商店安装插件。 ## 安装脚手架 确保你已经安装了 Python 3.9 及以上版本,然后在命令行中执行以下命令: 1. 安装 [pipx](https://pypa.github.io/pipx/) ```bash python -m pip install --user pipx python -m pipx ensurepath ``` 如果在此步骤的输出中出现了“open a new terminal”或者“re-login”字样,那么请关闭当前终端并重新打开一个新的终端。 2. 安装脚手架 ```bash pipx install nb-cli ``` 安装完成后,你可以在命令行使用 `nb` 命令来使用脚手架。如果出现无法找到命令的情况(例如出现“Command not found”字样),请参考 [pipx 文档](https://pypa.github.io/pipx/) 检查你的环境变量。 ## 创建项目 使用脚手架来创建一个项目: ```bash nb create ``` 这一指令将会执行创建项目的流程,你将会看到一些询问: 1. 项目模板 ```bash [?] 选择一个要使用的模板: bootstrap (初学者或用户) ``` 这里我们选择 `bootstrap` 模板,它是一个简单的项目模板,能够安装商店插件。如果你需要**自行编写插件**,这里请选择 `simple` 模板。 2. 项目名称 ```bash [?] 项目名称: awesome-bot ``` 这里我们以 `awesome-bot` 为例,作为项目名称。你可以根据自己的需要来命名。 3. 其他选项 请注意,多选项使用**空格**选中或取消,**回车**确认。 ```bash [?] 要使用哪些适配器? Console (基于终端的交互式适配器) [?] 要使用哪些驱动器? FastAPI (FastAPI 驱动器) [?] 要使用什么本地存储策略? 用户全局 (默认,适用于单用户下单实例) [?] 立即安装依赖? (Y/n) Yes [?] 创建虚拟环境? (Y/n) Yes ``` 这里我们选择了创建虚拟环境,nb-cli 在之后的操作中将会自动使用这个虚拟环境。如果你不需要自动创建虚拟环境或者已经创建了其他虚拟环境,nb-cli 将会安装依赖至当前激活的 Python 虚拟环境。 4. 选择内置插件 ```bash [?] 要使用哪些内置插件? echo ``` 这里我们选择 `echo` 插件作为示例。这是一个简单的复读回显插件,可以用于测试你的机器人是否正常运行。 ## 运行项目 在项目创建完成后,你可以在**项目目录**中使用以下命令来运行项目: ```bash nb run ``` 你现在应该已经运行起来了你的第一个 NoneBot 项目了!请注意,生成的项目中使用了 `FastAPI` 驱动器和 `Console` 适配器,你之后可以自行修改配置或安装其他适配器。 ## 尝试使用 在项目运行起来后,`Console` 适配器会在你的终端启动交互模式,你可以直接在输入框中输入 `/echo hello world` 来测试你的机器人是否正常运行。 ================================================ FILE: website/docs/tutorial/application.mdx ================================================ --- sidebar_position: 0 description: 创建一个 NoneBot 项目 options: menu: - category: tutorial weight: 20 --- # 手动创建项目 import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; 在[快速上手](../quick-start.mdx)中,我们已经介绍了如何安装和使用 `nb-cli` 创建一个项目。在本章节中,我们将简要介绍如何在不使用 `nb-cli` 的方式创建一个机器人项目的**最小实例**并启动。如果你想要了解 NoneBot 的启动流程,也可以阅读本章节。 :::caution 警告 我们十分不推荐直接创建机器人项目,请优先考虑使用 nb-cli 进行项目创建。 ::: 一个机器人项目的**最小实例**中**至少**需要包含以下内容: - 入口文件:初始化并运行机器人的 Python 文件 - 配置文件:存储机器人启动所需的配置 - 插件:为机器人提供具体的功能 下面我们创建一个项目文件夹,来存放项目所需文件,以下步骤均在该文件夹中进行。 ## 安装依赖 在创建项目前,我们首先需要将项目所需依赖安装至环境中。 1. (可选)创建虚拟环境,以 venv 为例 ```bash # 创建虚拟环境 python -m venv .venv --prompt nonebot2 # 激活虚拟环境 .venv\Scripts\activate ``` ```bash # 创建虚拟环境 python -m venv .venv --prompt nonebot2 # 激活虚拟环境 source .venv/bin/activate ``` 2. 安装 nonebot2 以及驱动器,以 Fastapi 驱动器为例 ```bash pip install "nonebot2[fastapi]" ``` ```bash pip install "nonebot2[fastapi]" ``` 驱动器包名可以在 [驱动器商店](/store/drivers) 中找到,请替换上文方括号中的内容。 3. 安装适配器,以 Console 适配器为例 ```bash pip install nonebot-adapter-console ``` 适配器包名可以在 [适配器商店](/store/adapters) 中找到。 ## 创建配置文件 配置文件用于存放 NoneBot 运行所需要的配置项,使用 [`pydantic`](https://docs.pydantic.dev/) 以及 [`python-dotenv`](https://saurabh-kumar.com/python-dotenv/) 来读取配置。配置项需符合 dotenv 格式,复杂类型数据需使用 JSON 格式填写。具体可选配置方式以及配置项详情参考[配置](../appendices/config.mdx)。 在**项目文件夹**中创建一个名为 `.env` 的文件,并写入以下内容: ```bash title=.env HOST=0.0.0.0 # 配置 NoneBot 监听的 IP / 主机名 PORT=8080 # 配置 NoneBot 监听的端口 COMMAND_START=["/"] # 配置命令起始字符 COMMAND_SEP=["."] # 配置命令分割字符 ``` ## 创建入口文件 入口文件( Entrypoint )顾名思义,是用来初始化并运行机器人的 Python 文件。入口文件需要完成框架的初始化、注册适配器、加载插件等工作。 :::tip 提示 如果你使用 `nb-cli` 创建项目,入口文件不会被创建,该文件功能会被 `nb run` 命令代替。 ::: 在**项目文件夹**中创建一个 `bot.py` 文件,并写入以下内容: ```python title=bot.py import nonebot from nonebot.adapters.console import Adapter as ConsoleAdapter # 避免重复命名 # 初始化 NoneBot nonebot.init() # 注册适配器 driver = nonebot.get_driver() driver.register_adapter(ConsoleAdapter) # 在这里加载插件 nonebot.load_builtin_plugins("echo") # 内置插件 # nonebot.load_plugin("thirdparty_plugin") # 第三方插件 # nonebot.load_plugins("awesome_bot/plugins") # 本地插件 if __name__ == "__main__": nonebot.run() ``` 我们暂时不需要了解其中内容的含义,这些将会在稍后的章节中逐一介绍。在创建完成以上文件并确认已安装所需适配器和插件后,即可运行机器人。 ## 运行机器人 在**项目文件夹**中,使用配置好环境的 Python 解释器运行入口文件: ```bash # 激活虚拟环境(未使用虚拟环境时跳过此行) .venv\Scripts\activate # 运行机器人 python bot.py ``` ```bash # 激活虚拟环境(未使用虚拟环境时跳过此行) source .venv/bin/activate # 运行机器人 python bot.py ``` 如果你后续使用了 `nb-cli` ,你仍可以使用 `nb run` 命令来运行机器人,`nb-cli` 会自动检测入口文件 `bot.py` 是否存在并运行。同时,你也可以使用 `nb run --reload` 来自动检测代码的更改并自动重新运行入口文件。 ================================================ FILE: website/docs/tutorial/create-plugin.md ================================================ --- sidebar_position: 3 description: 创建并加载自定义插件 options: menu: - category: tutorial weight: 50 --- # 插件编写准备 在正式编写插件之前,我们需要先了解一下插件的概念。 ## 插件结构 在 NoneBot 中,插件即是 Python 的一个[模块(module)](https://docs.python.org/zh-cn/3/glossary.html#term-module)。NoneBot 会在导入时对这些模块做一些特殊的处理使得他们成为一个插件。插件间应尽量减少耦合,可以进行有限制的相互调用,NoneBot 能够正确解析插件间的依赖关系。 ### 单文件插件 一个普通的 `.py` 文件即可以作为一个插件,例如创建一个 `foo.py` 文件: ```tree title=Project 📂 plugins └── 📜 foo.py ``` 这个时候模块 `foo` 已经可以被称为一个插件了,尽管它还什么都没做。 ### 包插件 一个包含 `__init__.py` 的文件夹即是一个常规 Python [包 `package`](https://docs.python.org/zh-cn/3/glossary.html#term-regular-package),例如创建一个 `foo` 文件夹: ```tree title=Project 📂 plugins └── 📂 foo └── 📜 __init__.py ``` 这个时候包 `foo` 同样是一个合法的插件,插件内容可以在 `__init__.py` 文件中编写。 ## 创建插件 :::caution 注意 如果在之前的[快速上手](../quick-start.mdx)章节中已经使用 `bootstrap` 模板创建了项目,那么你需要做出如下修改: 1. 在项目目录中创建一个两层文件夹 `awesome_bot/plugins` ```tree title=Project 📦 awesome-bot ├── 📂 .venv ├── 📂 awesome_bot │ └── 📂 plugins ├── 📜 .env.prod ├── 📜 pyproject.toml └── 📜 README.md ``` 2. 修改 `pyproject.toml` 文件中的 `nonebot` 配置项,在 `plugin_dirs` 中添加 `awesome_bot/plugins` ```toml title=pyproject.toml [tool.nonebot] plugin_dirs = ["awesome_bot/plugins"] ``` ::: :::caution 注意 如果在之前的[创建项目](./application.mdx)章节中手动创建了相关文件,那么你需要做出如下修改: 1. 在项目目录中创建一个两层文件夹 `awesome_bot/plugins` ```tree title=Project 📦 awesome-bot ├── 📂 awesome_bot │ └── 📂 plugins └── 📜 bot.py ``` 2. 修改 `bot.py` 文件中的加载插件部分,取消注释或者添加如下代码 ```python title=bot.py # 在这里加载插件 nonebot.load_builtin_plugins("echo") # 内置插件 nonebot.load_plugins("awesome_bot/plugins") # 本地插件 ``` ::: 创建插件可以通过 `nb-cli` 命令从完整模板创建,也可以手动新建空白文件。通过以下命令创建一个名为 `weather` 的插件: ```bash $ nb plugin create [?] 插件名称: weather [?] 使用嵌套插件? (y/N) N [?] 请输入插件存储位置: awesome_bot/plugins ``` `nb-cli` 会在 `awesome_bot/plugins` 目录下创建一个名为 `weather` 的文件夹,其中包含的文件将在稍后章节中用到。 ```tree title=Project 📦 awesome-bot ├── 📂 .venv ├── 📂 awesome_bot │ └── 📂 plugins | └── 📂 weather | ├── 📜 __init__.py | └── 📜 config.py ├── 📜 .env.prod ├── 📜 pyproject.toml └── 📜 README.md ``` ## 加载插件 :::danger 警告 请勿在插件被加载前 `import` 插件模块,这会导致 NoneBot 无法将其转换为插件而出现意料之外的情况。 ::: 加载插件是在机器人入口文件中完成的,需要在框架初始化之后,运行之前进行。 请注意,加载的插件模块名称(插件文件名或文件夹名)**不能相同**,且每一个插件**只能被加载一次**,重复加载将会导致异常。 如果你使用 `nb-cli` 管理插件,那么你可以跳过这一节,`nb-cli` 将会自动处理加载。 如果你**使用自定义的入口文件** `bot.py`,那么你需要在 `bot.py` 中加载插件。 ```python {5} title=bot.py import nonebot nonebot.init() # 加载插件 nonebot.run() ``` 加载插件的方式有多种,但在底层的加载逻辑是一致的。以下是为加载插件提供的几种方式: ### `load_plugin` 通过点分割模块名称或使用 [`pathlib`](https://docs.python.org/zh-cn/3/library/pathlib.html) 的 `Path` 对象来加载插件。通常用于加载第三方插件或者项目插件。例如: ```python from pathlib import Path nonebot.load_plugin("path.to.your.plugin") # 加载第三方插件 nonebot.load_plugin(Path("./path/to/your/plugin.py")) # 加载项目插件 ``` :::caution 注意 请注意,本地插件的路径应该为相对机器人 **入口文件(通常为 bot.py)** 可导入的,例如在项目 `plugins` 目录下。 ::: ### `load_plugins` 加载传入插件目录中的所有插件,通常用于加载一系列本地编写的项目插件。例如: ```python nonebot.load_plugins("src/plugins", "path/to/your/plugins") ``` :::caution 注意 请注意,插件目录应该为相对机器人 **入口文件(通常为 bot.py)** 可导入的,例如在项目 `plugins` 目录下。 ::: ### `load_all_plugins` 这种加载方式是以上两种方式的混合,加载所有传入的插件模块名称,以及所有给定目录下的插件。例如: ```python nonebot.load_all_plugins(["path.to.your.plugin"], ["path/to/your/plugins"]) ``` ### `load_from_json` 通过 JSON 文件加载插件,是 [`load_all_plugins`](#load_all_plugins) 的 JSON 变种。通过读取 JSON 文件中的 `plugins` 字段和 `plugin_dirs` 字段进行加载。例如: ```json title=plugin_config.json { "plugins": ["path.to.your.plugin"], "plugin_dirs": ["path/to/your/plugins"] } ``` ```python nonebot.load_from_json("plugin_config.json", encoding="utf-8") ``` :::tip 提示 如果 JSON 配置文件中的字段无法满足你的需求,可以使用 [`load_all_plugins`](#load_all_plugins) 方法自行读取配置来加载插件。 ::: ### `load_from_toml` 通过 TOML 文件加载插件,是 [`load_all_plugins`](#load_all_plugins) 的 TOML 变种。通过读取 TOML 文件中的 `[tool.nonebot]` Table 中的 `plugin_dirs` Array 与 `[tool.nonebot.plugins]` Table 中的多个 Array 进行加载。例如: ```toml title=plugin_config.toml [tool.nonebot] plugin_dirs = ["path/to/your/plugins"] [tool.nonebot.plugins] "@local" = ["path.to.your.plugin"] # 本地插件等非插件商店来源的插件 "nonebot-plugin-someplugin" = ["nonebot_plugin_someplugin"] # 插件商店来源的插件 ``` ```python nonebot.load_from_toml("plugin_config.toml", encoding="utf-8") ``` :::tip 提示 如果 TOML 配置文件中的字段无法满足你的需求,可以使用 [`load_all_plugins`](#load_all_plugins) 方法自行读取配置来加载插件。 ::: ### `load_builtin_plugin` 加载一个内置插件,传入的插件名必须为 NoneBot 内置插件。该方法是 [`load_plugin`](#load_plugin) 的封装。例如: ```python nonebot.load_builtin_plugin("echo") ``` ### `load_builtin_plugins` 加载传入插件列表中的所有内置插件。例如: ```python nonebot.load_builtin_plugins("echo", "single_session") ``` ### 其他加载方式 有关其他插件加载的方式,可参考[跨插件访问](../advanced/requiring.md)和[嵌套插件](../advanced/plugin-nesting.md)。 ================================================ FILE: website/docs/tutorial/event-data.mdx ================================================ --- sidebar_position: 6 description: 通过依赖注入获取所需事件信息 options: menu: - category: tutorial weight: 80 --- # 获取事件信息 import Messenger from "@site/src/components/Messenger"; 在 NoneBot 事件处理流程中,获取事件信息并做出对应的操作是非常常见的场景。本章节中我们将介绍如何通过**依赖注入**获取事件信息。 ## 认识依赖注入 在事件处理流程中,事件响应器具有自己独立的上下文,例如:当前响应的事件、收到事件的机器人或者其他处理流程中新增的信息等。这些数据可以根据我们的需求,通过依赖注入的方式,在执行事件处理流程中注入到事件处理函数中。 相对于传统的信息获取方法,通过依赖注入获取信息的最大特色在于**按需获取**。如果该事件处理函数不需要任何额外信息即可运行,那么可以不进行依赖注入。如果事件处理函数需要额外的数据,可以通过依赖注入的方式灵活的标注出需要的依赖,在函数运行时便会被按需注入。 ## 使用依赖注入 使用依赖注入获取上下文信息的方法十分简单,我们仅需要在函数的参数中声明所需的依赖,并正确的将函数添加为事件处理依赖即可。在 NoneBot 中,我们可以直接使用 `nonebot.params` 模块中定义的参数类型来声明依赖。 例如,我们可以继续改进上一章节中的 `weather` 插件,使其可以获取到 `天气` 命令的地名参数,并根据地名返回天气信息。 ```python {9,11} title=weather/__init__.py from nonebot import on_command from nonebot.rule import to_me from nonebot.adapters import Message from nonebot.params import CommandArg weather = on_command("天气", rule=to_me(), aliases={"weather", "查天气"}, priority=10, block=True) @weather.handle() async def handle_function(args: Message = CommandArg()): # 提取参数纯文本作为地名,并判断是否有效 if location := args.extract_plain_text(): await weather.finish(f"今天{location}的天气是...") else: await weather.finish("请输入地名") ``` 如上方示例所示,我们使用了 `args` 作为注入参数名,注入的内容为 `CommandArg()`,也就是**消息命令后跟随的内容**。在这个示例中,我们获得的参数会被检查是否有效,对无效参数则会结束事件。 :::tip 提示 命令与参数之间可以不需要空格,`CommandArg()` 获取的信息为命令后跟随的内容并去除了头部空白符。例如:`/天气 上海` 消息的参数为 `上海`。 ::: :::tip 提示 `:=` 是 Python 3.8 引入的新语法 [Assignment Expressions](https://docs.python.org/zh-cn/3/reference/expressions.html#assignment-expressions),也称为海象表达式,可以在表达式中直接赋值。 ::: NoneBot 提供了多种依赖注入类型,可以获取不同的信息,具体内容可参考[依赖注入](../advanced/dependency.mdx)。 ================================================ FILE: website/docs/tutorial/fundamentals.md ================================================ --- sidebar_position: 1 description: NoneBot 机器人构成及基本使用 options: menu: - category: tutorial weight: 30 --- # 机器人的构成 了解机器人的基本构成有助于你更好地使用 NoneBot,本章节将介绍 NoneBot 中的基本组成部分,稍后的文档中将会使用到这些概念。 使用 NoneBot 框架搭建的机器人具有以下几个基本组成部分: 1. NoneBot 机器人框架主体:负责连接各个组成部分,提供基本的机器人功能 2. 驱动器 `Driver`:客户端/服务端的功能实现,负责接收和发送消息(通常为 HTTP 通信) 3. 适配器 `Adapter`:驱动器的上层,负责将**平台消息**与 NoneBot 事件/操作系统的消息格式相互转换 4. 插件 `Plugin`:机器人的功能实现,通常为负责处理事件并进行一系列的操作 除 NoneBot 机器人框架主体外,其他部分均可按需选择、互相搭配,但由于平台的兼容性问题,部分插件可能仅在某些特定平台上可用(这由插件编写者决定)。 在接下来的章节中,我们将重点介绍机器人功能实现,即插件 `Plugin` 部分。 ================================================ FILE: website/docs/tutorial/handler.mdx ================================================ --- sidebar_position: 5 description: 处理接收到的特定事件 options: menu: - category: tutorial weight: 70 --- # 事件处理 import Messenger from "@site/src/components/Messenger"; 在我们收到事件,并被某个事件响应器正确响应后,便正式开启了对于这个事件的**处理流程**。 ## 认识事件处理流程 就像我们在解决问题时需要遵循流程一样,处理一个事件也需要一套流程。在事件响应器对一个事件进行响应之后,会依次执行一系列的**事件处理依赖**(通常是函数)。简单来说,事件处理流程并不是一个函数、一个对象或一个方法,而是一整套由开发者设计的流程。 在这个流程中,我们**目前**只需要了解两个概念:函数形式的“事件处理依赖”(下称“事件处理函数”)和“事件响应器操作”。 ## 事件处理函数 在事件响应器中,事件处理流程可以由一个或多个“事件处理函数”组成,这些事件处理函数将会按照顺序依次对事件进行处理,直到全部执行完成或被中断。我们可以采用事件响应器的“事件处理函数装饰器”来添加这些“事件处理函数”。 顾名思义,“事件处理函数装饰器”是一个[装饰器(decorator)](https://docs.python.org/zh-cn/3/glossary.html#term-decorator),那么它的使用方法也同[函数定义](https://docs.python.org/zh-cn/3/reference/compound_stmts.html#function-definitions)中所展示的包装用法相同。例如: ```python {6-8} title=weather/__init__.py from nonebot import on_command from nonebot.rule import to_me weather = on_command("天气", rule=to_me(), aliases={"weather", "查天气"}, priority=10, block=True) @weather.handle() async def handle_function(): pass # do something here ``` 如上方示例所示,我们使用 `weather` 响应器的 `handle` 装饰器装饰了一个函数 `handle_function`。`handle_function` 函数会被添加到 `weather` 的事件处理流程中。在 `weather` 响应器被触发之后,将会依次调用 `weather` 响应器的事件处理函数,即 `handle_function` 来对事件进行处理。 ## 事件响应器操作 在事件处理流程中,我们可以使用事件响应器操作来进行一些交互或改变事件处理流程,例如向机器人用户发送消息或提前结束事件处理流程等。 事件响应器操作与事件处理函数装饰器类似,通常作为事件响应器 `Matcher` 的[类方法](https://docs.python.org/zh-cn/3/library/functions.html#classmethod)存在,因此事件响应器操作的调用方法也是 `Matcher.func()` 的形式。不过不同的是,事件响应器操作并不是装饰器,因此并不需要@进行标注。 ```python {8,9} title=weather/__init__.py from nonebot import on_command from nonebot.rule import to_me weather = on_command("天气", rule=to_me(), aliases={"weather", "查天气"}, priority=10, block=True) @weather.handle() async def handle_function(): # await weather.send("天气是...") await weather.finish("天气是...") ``` 如上方示例所示,我们使用 `weather` 响应器的 `finish` 操作方法向机器人用户回复了 `天气是...` 并结束了事件处理流程。效果如下: 值得注意的是,在执行 `finish` 方法时,NoneBot 会在向机器人用户发送消息内容后抛出 `FinishedException` 异常来结束事件响应流程。也就是说,在 `finish` 被执行后,后续的程序是不会被执行的。如果你需要回复机器人用户消息但不想事件处理流程结束,可以使用注释的部分中展示的 `send` 方法。 :::danger 警告 由于 `finish` 是通过抛出 `FinishedException` 异常来结束事件的,因此异常可能会被未加限制的 `try-except` 捕获,影响事件处理流程正确处理,导致无法正常结束此事件。请务必在异常捕获中指定错误类型或排除所有 [MatcherException](../api/exception.md#MatcherException) 类型的异常(如下所示),或将 `finish` 移出捕获范围进行使用。 ```python from nonebot.exception import MatcherException try: await weather.finish("天气是...") except MatcherException: raise except Exception as e: pass # do something here ``` ::: 目前 NoneBot 提供了多种事件响应器操作,其中包括用于机器人用户交互与流程控制两大类,进阶使用方法可以查看[会话控制](../appendices/session-control.mdx)。 ================================================ FILE: website/docs/tutorial/matcher.md ================================================ --- sidebar_position: 4 description: 响应接收到的特定事件 options: menu: - category: tutorial weight: 60 --- # 事件响应器 事件响应器(Matcher)是对接收到的事件进行响应的基本单元,所有的事件响应器都继承自 `Matcher` 基类。 在 NoneBot 中,事件响应器可以通过一系列特定的规则**筛选**出**具有某种特征的事件**,并按照**特定的流程**交由**预定义的事件处理依赖**进行处理。例如,在[快速上手](../quick-start.mdx)中,我们使用了内置插件 `echo` ,它定义的事件响应器能响应机器人用户发送的“/echo hello world”消息,提取“hello world”信息并作为回复消息发送。 ## 事件响应器辅助函数 NoneBot 中所有事件响应器均继承自 `Matcher` 基类,但直接使用 `Matcher.new()` 方法创建事件响应器过于繁琐且不能记录插件信息。因此,NoneBot 中提供了一系列“事件响应器辅助函数”(下称“辅助函数”)来辅助我们用**最简的方式**创建**带有不同规则预设**的事件响应器,提高代码可读性和书写效率。通常情况下,我们只需要使用辅助函数即可完成事件响应器的创建。 在 NoneBot 中,辅助函数以 `on()` 或 `on_()` 形式出现(例如 `on_command()`),调用后根据不同的参数返回一个 `Type[Matcher]` 类型的新事件响应器。 目前 NoneBot 提供了多种功能各异的辅助函数、具有共同命令名称前缀的命令组以及具有共同参数的响应器组,均可以从 `nonebot` 模块直接导入使用,具体内容参考[事件响应器进阶](../advanced/matcher.md)。 ## 创建事件响应器 在上一节[创建插件](./create-plugin.md#创建插件)中,我们创建了一个 `weather` 插件,现在我们来实现他的功能。 我们直接使用 `on_command()` 辅助函数来创建一个事件响应器: ```python {3} title=weather/__init__.py from nonebot import on_command weather = on_command("天气") ``` 这样,我们就获得一个名为 `weather` 的事件响应器了,这个事件响应器会对 `/天气` 开头的消息进行响应。 :::tip 提示 如果一条消息中包含“@机器人”或以“机器人的昵称”开始,例如 `@bot /天气` 时,协议适配器会将 `event.is_tome()` 判断为 `True` ,同时也会自动去除 `@bot`,即事件响应器收到的信息内容为 `/天气`,方便进行命令匹配。 ::: ### 为事件响应器添加参数 在辅助函数中,我们可以添加一些参数来对事件响应器进行更加精细的调整,例如事件响应器的优先级、匹配规则等。例如: ```python {4} title=weather/__init__.py from nonebot import on_command from nonebot.rule import to_me weather = on_command("天气", rule=to_me(), aliases={"weather", "查天气"}, priority=10, block=True) ``` 这样,我们就获得了一个可以响应 `天气`、`weather`、`查天气` 三个命令的响应规则,需要私聊或 `@bot` 时才会响应,优先级为 10(越小越优先),阻断事件向后续优先级传播的事件响应器了。这些内容的意义和使用方法将会在后续的章节中一一介绍。 :::tip 提示 需要注意的是,不同的辅助函数有不同的可选参数,在使用之前可以参考[事件响应器进阶 - 基本辅助函数](../advanced/matcher.md#基本辅助函数)或 [API 文档](../api/plugin/on.md#on)。 ::: ================================================ FILE: website/docs/tutorial/message.md ================================================ --- sidebar_position: 7 description: 处理消息序列与消息段 options: menu: - category: tutorial weight: 90 --- # 处理消息 在不同平台中,一条消息可能会有承载有各种不同的表现形式,它可能是一段纯文本、一张图片、一段语音、一篇富文本文章,也有可能是多种类型的组合等等。 在 NoneBot 中,为确保消息的正常处理与跨平台兼容性,采用了扁平化的消息序列形式,即 `Message` 对象。消息序列是 NoneBot 中的消息载体,无论是接收还是发送的消息,都采用消息序列的形式进行处理。 ## 认识消息类型 ### 消息序列 `Message` 在 NoneBot 中,消息序列 `Message` 的主要作用是用于表达“一串消息”。由于消息序列继承自 `List[MessageSegment]`,所以 `Message` 的本质是由若干消息段所组成的序列。因此,消息序列的使用方法与 `List` 有很多相似之处,例如切片、索引、拼接等。 在上一节的[使用依赖注入](./event-data.mdx#使用依赖注入)中,我们已经通过依赖注入 `CommandArg()` 获取了命令的参数,它的类型即是消息序列。我们使用了消息序列的 `extract_plain_text()` 方法来获取消息序列中的纯文本内容。 ### 消息段 `MessageSegment` 顾名思义,消息段 `MessageSegment` 是一段消息。由于消息序列的本质是由若干消息段所组成的序列,消息段可以被认为是构成消息序列的最小单位。简单来说,消息序列类似于一个自然段,而消息段则是组成自然段的一句话。同时,作为特殊消息载体的存在,绝大多数的平台都有着**独特的消息类型**,这些独特的内容均需要由对应的**协议适配器**所提供,以适应不同平台中的消息模式。**这也意味着,你需要导入对应的协议适配器中的消息序列和消息段后才能使用其特殊的工厂方法。** :::caution 注意 消息段的类型是由协议适配器提供的,因此你需要参考协议适配器的文档并导入对应的消息段后才能使用其特殊的消息类型。 在上一节的[使用依赖注入](./event-data.mdx#使用依赖注入)中,我们导入的为 `nonebot.adapters.Message` 抽象基类,因此我们无法使用平台特有的消息类型。仅能使用 `str` 作为纯文本消息回复。 ::: ## 使用消息序列 :::caution 注意 在以下的示例中,为了更好的理解多种类型的消息组成方式,我们将使用 `Console` 协议适配器来演示消息序列的使用方法。在实际使用中,你需要确保你使用的**消息序列类型**与你所要发送的**平台类型**一致。 ::: 通常情况下,适配器在接收到消息时,会将消息转换为消息序列,可以通过依赖注入 [`EventMessage`](../advanced/dependency.mdx#eventmessage),或者使用 `event.get_message()` 获取。 由于消息序列是 `List[MessageSegment]` 的子类,所以你总是可以用和操作 `List` 类似的方式来处理消息序列。例如: ```python >>> from nonebot.adapters.console import Message, MessageSegment >>> message = Message([ MessageSegment(type="text", data={"text":"hello"}), MessageSegment(type="markdown", data={"markup":"**world**"}), ]) >>> for segment in message: ... print(segment.type, segment.data) ... text {'text': 'hello'} markdown {'markup': '**world**'} >>> len(message) 2 ``` ### 构造消息序列 在使用事件响应器操作发送消息时,既可以使用 `str` 作为消息,也可以使用 `Message`、`MessageSegment` 或者 `MessageTemplate`。那么,我们就需要先构造一个消息序列。消息序列可以通过多种方式构造: #### 直接构造 `Message` 类可以直接实例化,支持 `str`、`MessageSegment`、`Iterable[MessageSegment]` 或适配器自定义类型的参数。 ```python from nonebot.adapters.console import Message, MessageSegment # str Message("Hello, world!") # MessageSegment Message(MessageSegment.text("Hello, world!")) # List[MessageSegment] Message([MessageSegment.text("Hello, world!")]) ``` #### 运算构造 `Message` 对象可以通过 `str`、`MessageSegment` 相加构造,详情请参考[拼接消息](#拼接消息)。 #### 从字典数组构造 `Message` 对象支持 Pydantic 自定义类型构造,可以使用 Pydantic 的 `TypeAdapter` 方法进行构造。 ```python from pydantic import TypeAdapter from nonebot.adapters.console import Message, MessageSegment # 由字典构造消息段 TypeAdapter(MessageSegment).validate_python( {"type": "text", "data": {"text": "text"}} ) == MessageSegment.text("text") # 由字典数组构造消息序列 TypeAdapter(Message).validate_python( [MessageSegment.text("text"), {"type": "text", "data": {"text": "text"}}], ) == Message([MessageSegment.text("text"), MessageSegment.text("text")]) ``` ### 获取消息纯文本 由于消息中存在各种类型的消息段,因此 `str(message)` 通常**不能得到消息的纯文本**,而是一个消息序列的字符串表示。 NoneBot 为消息段定义了一个方法 `is_text()` ,可以用于判断消息段是否为纯文本;也可以使用 `message.extract_plain_text()` 方法获取消息纯文本。 ```python from nonebot.adapters.console import Message, MessageSegment # 判断消息段是否为纯文本 MessageSegment.text("text").is_text() == True # 提取消息纯文本字符串 Message( [MessageSegment.text("text"), MessageSegment.markdown("**markup**")] ).extract_plain_text() == "text" ``` ### 遍历 消息序列继承自 `List[MessageSegment]` ,因此可以使用 `for` 循环遍历消息段。 ```python for segment in message: ... ``` ### 比较 消息和消息段都可以使用 `==` 或 `!=` 运算符比较是否相同。 ```python MessageSegment.text("text") != MessageSegment.text("foo") some_message == Message([MessageSegment.text("text")]) ``` ### 检查消息段 我们可以通过 `in` 运算符或消息序列的 `has` 方法来: ```python # 是否存在消息段 MessageSegment.text("text") in message # 是否存在指定类型的消息段 "text" in message ``` 我们还可以使用消息序列的 `only` 方法来检查消息中是否仅包含指定的消息段。 ```python # 是否都为指定消息段 message.only(MessageSegment.text("test")) # 是否仅包含指定类型的消息段 message.only("text") ``` ### 过滤、索引与切片 消息序列对列表的索引与切片进行了增强,在原有列表 `int` 索引与 `slice` 切片的基础上,支持 `type` 过滤索引与切片。 ```python from nonebot.adapters.console import Message, MessageSegment message = Message( [ MessageSegment.text("test"), MessageSegment.markdown("test2"), MessageSegment.markdown("test3"), MessageSegment.text("test4"), ] ) # 索引 message[0] == MessageSegment.text("test") # 切片 message[0:2] == Message( [MessageSegment.text("test"), MessageSegment.markdown("test2")] ) # 类型过滤 message["markdown"] == Message( [MessageSegment.markdown("test2"), MessageSegment.markdown("test3")] ) # 类型索引 message["markdown", 0] == MessageSegment.markdown("test2") # 类型切片 message["markdown", 0:2] == Message( [MessageSegment.markdown("test2"), MessageSegment.markdown("test3")] ) ``` 我们也可以通过消息序列的 `include`、`exclude` 方法进行类型过滤。 ```python message.include("text", "markdown") message.exclude("text") ``` 同样的,消息序列对列表的 `index`、`count` 方法也进行了增强,可以用于索引指定类型的消息段。 ```python # 指定类型首个消息段索引 message.index("markdown") == 1 # 指定类型消息段数量 message.count("markdown") == 2 ``` 此外,消息序列添加了一个 `get` 方法,可以用于获取指定类型指定个数的消息段。 ```python # 获取指定类型指定个数的消息段 message.get("markdown", 1) == Message([MessageSegment.markdown("test2")]) ``` ### 拼接消息 `str`、`Message`、`MessageSegment` 对象之间可以直接相加,相加均会返回一个新的 `Message` 对象。 ```python # 消息序列与消息段相加 Message([MessageSegment.text("text")]) + MessageSegment.text("text") # 消息序列与字符串相加 Message([MessageSegment.text("text")]) + "text" # 消息序列与消息序列相加 Message([MessageSegment.text("text")]) + Message([MessageSegment.text("text")]) # 字符串与消息序列相加 "text" + Message([MessageSegment.text("text")]) # 消息段与消息段相加 MessageSegment.text("text") + MessageSegment.text("text") # 消息段与字符串相加 MessageSegment.text("text") + "text" # 消息段与消息序列相加 MessageSegment.text("text") + Message([MessageSegment.text("text")]) # 字符串与消息段相加 "text" + MessageSegment.text("text") ``` 如果需要在当前消息序列后直接拼接新的消息段,可以使用 `Message.append`、`Message.extend` 方法,或者使用自加。 ```python msg = Message([MessageSegment.text("text")]) # 自加 msg += "text" msg += MessageSegment.text("text") msg += Message([MessageSegment.text("text")]) # 附加 msg.append("text") msg.append(MessageSegment.text("text")) # 扩展 msg.extend([MessageSegment.text("text")]) ``` 我们也可以通过消息段或消息序列的 `join` 方法来拼接一串消息: ```python seg = MessageSegment.text("text") msg = seg.join( [ MessageSegment.text("first"), Message( [ MessageSegment.text("second"), MessageSegment.text("third"), ] ) ] ) msg == Message( [ MessageSegment.text("first"), MessageSegment.text("text"), MessageSegment.text("second"), MessageSegment.text("third"), ] ) ``` ### 使用消息模板 为了提供安全可靠的跨平台模板字符,我们提供了一个消息模板功能来构建消息序列 它在以下常见场景中尤其有用: - 多行富文本编排(包含图片,文字以及表情等) - 客制化(由 Bot 最终用户提供消息模板时) 在事实上,它的用法和 `str.format` 极为相近,所以你在使用的时候,总是可以参考[Python 文档](https://docs.python.org/zh-cn/3/library/stdtypes.html#str.format)来达到你想要的效果,这里给出几个简单的例子。 默认情况下,消息模板采用 `str` 纯文本形式的格式化: ```python title=基础格式化用法 >>> from nonebot.adapters import MessageTemplate >>> MessageTemplate("{} {}").format("hello", "world") 'hello world' ``` 如果 `Message.template` 构建消息模板,那么消息模板将采用消息序列形式的格式化,此时的消息将会是平台特定的: :::caution 注意 使用 `Message.template` 构建消息模板时,应注意消息序列为平台适配器提供的类型,不能使用 `nonebot.adapters.Message` 基类作为模板构建。使用基类构建模板与使用 `str` 构建模板的效果是一样的,因此请使用上述的 `MessageTemplate` 类直接构建模板。: ::: ```python title=平台格式化用法 >>> from nonebot.adapters.console import Message, MessageSegment >>> Message.template("{} {}").format("hello", "world") Message( MessageSegment.text("hello"), MessageSegment.text(" "), MessageSegment.text("world") ) ``` 消息模板支持使用消息段进行格式化: ```python title=对消息段进行安全的拼接 >>> from nonebot.adapters.console import Message, MessageSegment >>> Message.template("{}{}").format(MessageSegment.markdown("**markup**"), "world") Message( MessageSegment(type='markdown', data={'markup': '**markup**'}), MessageSegment(type='text', data={'text': 'world'}) ) ``` 消息模板同样支持使用消息序列作为模板: ```python title=以消息对象作为模板 >>> from nonebot.adapters.console import Message, MessageSegment >>> Message.template( ... MessageSegment.text("{user_id}") + MessageSegment.emoji("tada") + ... MessageSegment.text("{message}") ... ).format_map({"user_id": 123456, "message": "hello world"}) Message( MessageSegment(type='text', data={'text': '123456'}), MessageSegment(type='emoji', data={'emoji': 'tada'}), MessageSegment(type='text', data={'text': 'hello world'}) ) ``` :::caution 注意 只有消息序列中的文本类型消息段才能被格式化,其他类型的消息段将会原样添加。 ::: 消息模板支持使用拓展控制符来控制消息段类型: ```python title=使用消息段的拓展控制符 >>> from nonebot.adapters.console import Message, MessageSegment >>> Message.template("{name:emoji}").format(name='tada') Message(MessageSegment(type='emoji', data={'name': 'tada'})) ``` ================================================ FILE: website/docs/tutorial/store.mdx ================================================ --- sidebar_position: 2 description: 从商店安装适配器和插件 options: menu: - category: tutorial weight: 40 --- # 获取商店内容 import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; import Asciinema from "@site/src/components/Asciinema"; :::tip 提示 如果你暂时没有获取商店内容的需求,可以跳过本章节。 ::: NoneBot 提供了一个[商店](/store/plugins),商店内容均由社区开发者贡献。你可以在商店中查找你需要的适配器和插件等,进行安装或者参考其文档等。 商店中每个内容的卡片都包含了其名称和简介等信息,点击**卡片右上角**链接图标即可跳转到其主页。 与此同时,NB-CLI 也提供了一个 TUI 版本的商店界面,可通过 `nb adapter store`、`nb plugin store`、`nb driver store` 命令或 CLI 交互式界面进入。其提供了接近网页商店的体验,同时允许快捷安装到当前项目。 ## 安装插件 在商店插件页面中,点击你需要安装的插件下方的 `点击复制安装命令` 按钮,即可复制 `nb-cli` 命令。 请在你的**项目目录**下执行该命令。`nb-cli` 会自动安装插件并将其添加到加载列表中。 ```bash nb plugin install <插件名称> ``` ```bash $ nb plugin install [?] 想要安装的插件名称: <插件名称> ``` ```bash pip install <插件包名> ``` 插件包名可以在商店插件卡片中找到,或者使用 `nb-cli` 搜索插件显示的详情中找到。安装完成后,需要参考[加载插件章节](./create-plugin.md#加载插件)自行加载。 如果想要查看插件列表,可以使用以下命令 ```bash # 列出商店所有插件 nb plugin list # 搜索商店插件 nb plugin search [可选关键词] ``` 升级和卸载插件可以使用以下命令 ```bash nb plugin update <插件名称> nb plugin uninstall <插件名称> ``` ```bash $ nb plugin update [?] 想要安装的插件名称: <插件名称> $ nb plugin uninstall [?] 想要卸载的插件名称: <插件名称> ``` ```bash pip install --upgrade <插件包名> pip uninstall <插件包名> ``` 插件包名可以在商店插件卡片中找到,或者使用 `nb-cli` 搜索插件显示的详情中找到。卸载完成后,需要自行移除插件加载。 ## 安装适配器 安装适配器与安装插件类似,只是将命令换为 `nb adapter`,这里就不再赘述。 请在你的**项目目录**下执行该命令。`nb-cli` 会自动安装适配器并将其添加到注册列表中。 ```bash nb adapter install <适配器名称> ``` ```bash $ nb adapter install [?] 想要安装的适配器名称: <适配器名称> ``` ```bash pip install <适配器包名> ``` 适配器包名可以在商店适配器卡片中找到,或者使用 `nb-cli` 搜索适配器显示的详情中找到。安装完成后,需要参考[注册适配器章节](../advanced/adapter.md#注册适配器)自行注册。 如果想要查看适配器列表,可以使用以下命令 ```bash # 列出商店所有适配器 nb adapter list # 搜索商店适配器 nb adapter search [可选关键词] ``` 升级和卸载适配器可以使用以下命令 ```bash nb adapter update <适配器名称> nb adapter uninstall <适配器名称> ``` ```bash $ nb adapter update [?] 想要安装的适配器名称: <适配器名称> $ nb adapter uninstall [?] 想要卸载的适配器名称: <适配器名称> ``` ```bash pip install --upgrade <适配器包名> pip uninstall <适配器包名> ``` 适配器包名可以在商店适配器卡片中找到,或者使用 `nb-cli` 搜索适配器显示的详情中找到。卸载完成后,需要自行移除适配器加载。 ## 安装驱动器 安装驱动器与安装插件同样类似,只是将命令换为 `nb driver`,这里就不再赘述。 如果你使用了虚拟环境,请在你的**项目目录**下执行该命令,`nb-cli` 会自动安装驱动器到虚拟环境中。 请注意 `nb-cli` 并不会在安装驱动器后修改项目所使用的驱动器,请自行参考[配置方法](../appendices/config.mdx)章节以及 [`DRIVER` 配置项](../appendices/config.mdx#driver)修改驱动器。 ```bash nb driver install <驱动器名称> ``` ```bash $ nb driver install [?] 想要安装的驱动器名称: <驱动器名称> ``` ```bash pip install <驱动器包名> ``` 驱动器包名可以在商店驱动器卡片中找到,或者使用 `nb-cli` 搜索驱动器显示的详情中找到。 如果想要查看驱动器列表,可以使用以下命令 ```bash # 列出商店所有驱动器 nb driver list # 搜索商店驱动器 nb driver search [可选关键词] ``` 升级和卸载驱动器可以使用以下命令 ```bash nb driver update <驱动器名称> nb driver uninstall <驱动器名称> ``` ```bash $ nb driver update [?] 想要安装的驱动器名称: <驱动器名称> $ nb driver uninstall [?] 想要卸载的驱动器名称: <驱动器名称> ``` ```bash pip install --upgrade <驱动器包名> pip uninstall <驱动器包名> ``` 驱动器包名可以在商店驱动器卡片中找到,或者使用 `nb-cli` 搜索驱动器显示的详情中找到。卸载完成后,需要自行移除适配器加载。 ================================================ FILE: website/docusaurus.config.ts ================================================ import { themes } from "prism-react-renderer"; import type { Config } from "@docusaurus/types"; import type { Options as ChangelogOptions } from "@nullbot/docusaurus-plugin-changelog"; import type * as Preset from "@nullbot/docusaurus-preset-nonepress"; // color mode config const colorMode: Preset.ThemeConfig["colorMode"] = { defaultMode: "light", respectPrefersColorScheme: true, }; // navbar config const navbar: Preset.ThemeConfig["navbar"] = { title: "NoneBot", logo: { alt: "NoneBot", src: "logo.png", href: "/", target: "_self", height: 32, width: 32, }, hideOnScroll: false, items: [ { label: "指南", type: "docsMenu", category: "tutorial", }, { label: "深入", type: "docsMenu", category: "appendices", }, { label: "进阶", type: "docsMenu", category: "advanced", }, { label: "API", type: "doc", docId: "api/index", }, { label: "更多", type: "dropdown", to: "/store/plugins", items: [ { label: "最佳实践", type: "doc", docId: "best-practice/scheduler", }, { label: "开发者", type: "doc", docId: "developer/plugin-publishing", }, { label: "社区", type: "doc", docId: "community/contact" }, { label: "开源之夏", type: "doc", docId: "ospp/2025" }, { label: "商店", to: "/store/plugins" }, { label: "更新日志", to: "/changelog/" }, { label: "论坛", href: "https://discussions.nonebot.dev" }, ], }, ], }; // footer config const footer: Preset.ThemeConfig["footer"] = { style: "light", logo: { alt: "NoneBot", src: "logo.png", href: "/", target: "_self", height: 32, width: 32, }, copyright: `Copyright © ${new Date().getFullYear()} NoneBot. All rights reserved.`, links: [ { title: "Learn", items: [ { label: "Introduction", to: "/docs/" }, { label: "QuickStart", to: "/docs/quick-start" }, { label: "Changelog", to: "/changelog/" }, ], }, { title: "NoneBot Team", items: [ { label: "Homepage", href: "https://nonebot.dev", }, { label: "NoneBot V1", href: "https://v1.nonebot.dev", }, { label: "NoneBot CLI", href: "https://cli.nonebot.dev" }, ], }, { title: "Related", items: [ { label: "OneBot", href: "https://onebot.dev/" }, { label: "go-cqhttp", href: "https://docs.go-cqhttp.org/" }, { label: "Mirai", href: "https://mirai.mamoe.net/" }, ], }, ], }; // prism config const lightCodeTheme = themes.github; const darkCodeTheme = themes.dracula; const prism: Preset.ThemeConfig["prism"] = { theme: lightCodeTheme, darkTheme: darkCodeTheme, additionalLanguages: ["docker", "ini"], }; // algolia config const algolia: Preset.ThemeConfig["algolia"] = { appId: "X0X5UACHZQ", apiKey: "ac03e1ac2bd0812e2ea38c0cc1ea38c5", indexName: "nonebot", contextualSearch: true, }; // nonepress config const nonepress: Preset.ThemeConfig["nonepress"] = { tailwindConfig: require("./tailwind.config"), navbar: { docsVersionDropdown: { dropdownItemsAfter: [ { label: "1.x", href: "https://v1.nonebot.dev/", }, ], }, socialLinks: [ { icon: ["fab", "github"], href: "https://github.com/nonebot/nonebot2", }, ], }, footer: { socialLinks: [ { icon: ["fab", "github"], href: "https://github.com/nonebot/nonebot2", }, { icon: ["fab", "qq"], href: "https://jq.qq.com/?_wv=1027&k=5OFifDh", }, { icon: ["fab", "telegram"], href: "https://t.me/botuniverse", }, { icon: ["fab", "discord"], href: "https://discord.gg/VKtE6Gdc4h", }, ], }, }; // theme config const themeConfig: Preset.ThemeConfig = { colorMode, navbar, footer, prism, algolia, nonepress, }; export default async function createConfigAsync() { return { title: "NoneBot", tagline: "跨平台 Python 异步机器人框架", favicon: "icons/favicon.ico", // Set the production url of your site here url: "https://nonebot.dev", // Set the // pathname under which your site is served // For GitHub pages deployment, it is often '//' baseUrl: process.env.BASE_URL || "/", // GitHub pages deployment config. // If you aren't using GitHub pages, you don't need these. organizationName: "nonebot", // Usually your GitHub org/user name. projectName: "nonebot2", // Usually your repo name. onBrokenLinks: "throw", onBrokenMarkdownLinks: "warn", // Even if you don't use internalization, you can use this field to set useful // metadata like html lang. For example, if your site is Chinese, you may want // to replace "en" with "zh-Hans". i18n: { defaultLocale: "zh-Hans", locales: ["zh-Hans"], }, headTags: [ // 百度搜索资源平台 // https://ziyuan.baidu.com/ { tagName: "meta", attributes: { name: "baidu-site-verification", content: "codeva-0GTZpDnDrW", }, }, ], scripts: [ // 百度统计 // https://tongji.baidu.com/ { type: "text/javascript", charset: "UTF-8", src: "https://hm.baidu.com/hm.js?875efa50097818701ee681edd63eaac6", async: true, }, // 万维广告 // https://wwads.cn/ { type: "text/javascript", charset: "UTF-8", src: "https://cdn.wwads.cn/js/makemoney.js", async: true, }, // uwu logo { type: "text/javascript", charset: "UTF-8", src: "/uwu.js", }, ], presets: [ [ "@nullbot/docusaurus-preset-nonepress", /** @type {import('@nullbot/docusaurus-preset-nonepress').Options} */ { docs: { sidebarPath: require.resolve("./sidebars.js"), // Please change this to your repo. editUrl: "https://github.com/nonebot/nonebot2/edit/master/website/", showLastUpdateAuthor: true, showLastUpdateTime: true, // exclude: [ // "**/_*.{js,jsx,ts,tsx,md,mdx}", // "**/_*/**", // "**/*.test.{js,jsx,ts,tsx}", // "**/__tests__/**", // ], // async sidebarItemsGenerator({ // isCategoryIndex: defaultCategoryIndexMatcher, // defaultSidebarItemsGenerator, // ...args // }) { // return defaultSidebarItemsGenerator({ // ...args, // isCategoryIndex(doc) { // // disable category index convention for generated API docs // if ( // doc.directories.length > 0 && // doc.directories.at(-1) === "api" // ) { // return false; // } // return defaultCategoryIndexMatcher(doc); // }, // }); // }, }, // theme: { // customCss: require.resolve("./src/css/custom.css"), // }, sitemap: { changefreq: "daily", priority: 0.5, }, gtag: { trackingID: "G-MRS1GMZG0F", }, }, ], ], future: { experimental_faster: true, v4: true, }, plugins: [ require("./src/plugins/webpack-plugin.ts"), [ "@nullbot/docusaurus-plugin-changelog", { changelogPath: "src/changelog/changelog.md", changelogHeader: `description: Changelog toc_max_heading_level: 2 sidebar_custom_props: sidebar_id: changelog sidebar_version: current`, } satisfies ChangelogOptions, ], ], markdown: { mdx1Compat: { headingIds: true, }, }, themeConfig, } satisfies Config; } ================================================ FILE: website/package.json ================================================ { "name": "nonebot", "version": "2.0.0", "description": "跨平台 Python 异步机器人框架", "private": true, "homepage": "https://nonebot.dev/", "repository": "https://github.com/nonebot/nonebot2/", "bugs": { "url": "https://github.com/nonebot/nonebot2/issues" }, "license": "MIT", "scripts": { "docusaurus": "docusaurus", "start": "docusaurus start --host 0.0.0.0 --port 3000", "build": "docusaurus build", "build:fast": "cross-env BUILD_FAST=true yarn build", "build:fast:rsdoctor": "cross-env BUILD_FAST=true RSDOCTOR=true yarn build", "build:fast:profile": "cross-env BUILD_FAST=true node --cpu-prof --cpu-prof-dir .cpu-prof ./node_modules/.bin/docusaurus build", "swizzle": "docusaurus swizzle", "deploy": "docusaurus deploy", "clear": "docusaurus clear", "serve": "docusaurus serve", "write-translations": "docusaurus write-translations", "write-heading-ids": "docusaurus write-heading-ids", "typecheck": "tsc", "prettier": "prettier --config ../.prettierrc --write ." }, "dependencies": { "@docusaurus/core": "^3.7.0", "@docusaurus/eslint-plugin": "3.7.0", "@mdx-js/react": "^3.0.0", "@nullbot/docusaurus-plugin-changelog": "^3.0.0", "@nullbot/docusaurus-preset-nonepress": "^3.0.0", "clsx": "^2.0.0", "copy-text-to-clipboard": "^3.2.0", "prism-react-renderer": "^2.3.0", "react": "^19.0.0", "react-color": "^2.19.3", "react-dom": "^19.0.0", "react-use-pagination": "^2.0.1" }, "devDependencies": { "@docusaurus/faster": "^3.7.0", "@docusaurus/module-type-aliases": "^3.7.0", "@nullbot/docusaurus-tsconfig": "^3.0.0", "@types/react-color": "^3.0.10", "asciinema-player": "^3.5.0", "typescript": "~5.7.2" }, "browserslist": { "production": [ ">0.5%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] }, "engines": { "node": ">=18.0" } } ================================================ FILE: website/sidebars.ts ================================================ /** * Creating a sidebar enables you to: - create an ordered group of docs - render a sidebar for each doc of that group - provide next/previous navigation The sidebars can be generated from the filesystem, or explicitly defined here. Create as many sidebars as you want. */ import path from "path"; import { getChangelogItemsSync } from "@nullbot/docusaurus-plugin-changelog"; import type { SidebarsConfig } from "@docusaurus/plugin-content-docs"; const changelogPath = path.join(__dirname, "src/changelog/changelog.md"); const changelogItems = getChangelogItemsSync(changelogPath, 10); const sidebars: SidebarsConfig = { tutorial: [ { type: "category", label: "开始", collapsible: false, items: ["index", "quick-start", "editor-support"], }, { type: "category", label: "指南", items: [ { type: "autogenerated", dirName: "tutorial", }, ], }, { type: "category", label: "深入", items: [ { type: "autogenerated", dirName: "appendices", }, ], }, { type: "category", label: "进阶", items: [ { type: "autogenerated", dirName: "advanced", }, ], }, { type: "category", label: "最佳实践", items: [ { type: "autogenerated", dirName: "best-practice", }, ], }, { type: "category", label: "开发者", items: [ { type: "autogenerated", dirName: "developer", }, ], }, ], api: [{ type: "autogenerated", dirName: "api" }], ecosystem: [ { type: "category", label: "关于我们", collapsible: false, items: [ { type: "autogenerated", dirName: "community", }, ], }, { type: "category", label: "开源之夏", collapsible: true, items: [ { type: "autogenerated", dirName: "ospp", }, ], }, { type: "category", label: "社区资源", collapsible: false, items: [ { type: "link", label: "插件商店", href: "/store/plugins", }, { type: "link", label: "适配器商店", href: "/store/adapters", }, { type: "link", label: "驱动器商店", href: "/store/drivers", }, { type: "link", label: "机器人商店", href: "/store/bots", }, { type: "link", label: "Awesome NoneBot", href: "https://awesome.nonebot.dev", }, { type: "link", label: "论坛", href: "https://discussions.nonebot.dev", }, ], }, ], changelog: [ { type: "category", label: "更新日志", collapsible: false, items: changelogItems.map<{ type: "link"; label: string; href: string }>( (chunk, index) => ({ type: "link", label: chunk[0]!.title, href: `/changelog/${index > 0 ? index.toString() : ""}`, }) ), }, ], }; export default sidebars; ================================================ FILE: website/src/changelog/changelog.md ================================================ --- description: Changelog toc_max_heading_level: 2 --- # 更新日志 ## 最近更新 ### 💥 破坏性变更 - 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: aiohttp 驱动未处理 WSMsgType.CLOSED 类型 [@shoucandanghehe](https://github.com/shoucandanghehe) ([#3862](https://github.com/nonebot/nonebot2/pull/3862)) ### 📝 文档 - Docs: 修复插件元数据链接错误 [@yanyongyu](https://github.com/yanyongyu) ([#3894](https://github.com/nonebot/nonebot2/pull/3894)) - Docs: 完善对「发布插件」章节的文档描述 [@NCBM](https://github.com/NCBM) ([#3865](https://github.com/nonebot/nonebot2/pull/3865)) - Docs: Docker 部署镜像添加 latest tag [@AhsokaTano26](https://github.com/AhsokaTano26) ([#3787](https://github.com/nonebot/nonebot2/pull/3787)) - Docs: 调整文档 `on_command` import 路径 [@Xfjie314](https://github.com/Xfjie314) ([#3747](https://github.com/nonebot/nonebot2/pull/3747)) - Docs: 修复插件编写准备文档中的文本错误 [@Xfjie314](https://github.com/Xfjie314) ([#3746](https://github.com/nonebot/nonebot2/pull/3746)) - Docs: 修复格式化导致的语法错误 [@yanyongyu](https://github.com/yanyongyu) ([#3737](https://github.com/nonebot/nonebot2/pull/3737)) ### 💫 杂项 - 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: Codex [@noneflow](https://github.com/noneflow) ([#3889](https://github.com/nonebot/nonebot2/pull/3889)) - Plugin: nonebot-plugin-nbnhhsh [@noneflow](https://github.com/noneflow) ([#3887](https://github.com/nonebot/nonebot2/pull/3887)) - Plugin: mc服务器白名单管理工具 [@noneflow](https://github.com/noneflow) ([#3813](https://github.com/nonebot/nonebot2/pull/3813)) - Plugin: 特朗普社媒监控 [@noneflow](https://github.com/noneflow) ([#3882](https://github.com/nonebot/nonebot2/pull/3882)) - Plugin: The Betterest Mute Cat [@noneflow](https://github.com/noneflow) ([#3869](https://github.com/nonebot/nonebot2/pull/3869)) - Plugin: nonebot-plugin-cardimg [@noneflow](https://github.com/noneflow) ([#3857](https://github.com/nonebot/nonebot2/pull/3857)) - Plugin: Nonebot-Plugin-Rikka [@noneflow](https://github.com/noneflow) ([#3875](https://github.com/nonebot/nonebot2/pull/3875)) - Plugin: nonebot-plugin-peek [@noneflow](https://github.com/noneflow) ([#3859](https://github.com/nonebot/nonebot2/pull/3859)) - Plugin: 自动合成emoji [@noneflow](https://github.com/noneflow) ([#3867](https://github.com/nonebot/nonebot2/pull/3867)) - Plugin: 今日doro结局 [@noneflow](https://github.com/noneflow) ([#3852](https://github.com/nonebot/nonebot2/pull/3852)) - Plugin: Phira Server Manager [@noneflow](https://github.com/noneflow) ([#3855](https://github.com/nonebot/nonebot2/pull/3855)) - Plugin: Shiro Web Console [@noneflow](https://github.com/noneflow) ([#3832](https://github.com/nonebot/nonebot2/pull/3832)) - Plugin: 群聊拟人 [@noneflow](https://github.com/noneflow) ([#3820](https://github.com/nonebot/nonebot2/pull/3820)) - Plugin: BitTorrent磁力搜索 [@noneflow](https://github.com/noneflow) ([#3844](https://github.com/nonebot/nonebot2/pull/3844)) - Plugin: MCP Client [@noneflow](https://github.com/noneflow) ([#3842](https://github.com/nonebot/nonebot2/pull/3842)) - Plugin: Dice Helper [@noneflow](https://github.com/noneflow) ([#3840](https://github.com/nonebot/nonebot2/pull/3840)) - Plugin: Uniconfig-配置文件管理器 [@noneflow](https://github.com/noneflow) ([#3849](https://github.com/nonebot/nonebot2/pull/3849)) - Plugin: osugreek [@noneflow](https://github.com/noneflow) ([#3836](https://github.com/nonebot/nonebot2/pull/3836)) - Plugin: 基于QQ音乐歌单的音乐推荐 [@noneflow](https://github.com/noneflow) ([#3838](https://github.com/nonebot/nonebot2/pull/3838)) - Plugin: nonebot-plugin-bili2mp4 [@noneflow](https://github.com/noneflow) ([#3792](https://github.com/nonebot/nonebot2/pull/3792)) - Plugin: 战地6战绩查询 [@noneflow](https://github.com/noneflow) ([#3815](https://github.com/nonebot/nonebot2/pull/3815)) - Plugin: Tavily Search [@noneflow](https://github.com/noneflow) ([#3834](https://github.com/nonebot/nonebot2/pull/3834)) - Plugin: 群消息中继 [@noneflow](https://github.com/noneflow) ([#3804](https://github.com/nonebot/nonebot2/pull/3804)) - Plugin: 互联网异常事件监测 [@noneflow](https://github.com/noneflow) ([#3831](https://github.com/nonebot/nonebot2/pull/3831)) - Plugin: 词汇黑名单审查 [@noneflow](https://github.com/noneflow) ([#3817](https://github.com/nonebot/nonebot2/pull/3817)) - Plugin: 汉化进度记录 [@noneflow](https://github.com/noneflow) ([#3807](https://github.com/nonebot/nonebot2/pull/3807)) - Plugin: nonebot-plugin-ai-groupmate [@noneflow](https://github.com/noneflow) ([#3766](https://github.com/nonebot/nonebot2/pull/3766)) - Plugin: nonebot_plugin_boardgamehelper [@noneflow](https://github.com/noneflow) ([#3800](https://github.com/nonebot/nonebot2/pull/3800)) - Plugin: 即梦绘画 [@noneflow](https://github.com/noneflow) ([#3797](https://github.com/nonebot/nonebot2/pull/3797)) - Plugin: 快捷回复 [@noneflow](https://github.com/noneflow) ([#3795](https://github.com/nonebot/nonebot2/pull/3795)) - Plugin: pErithacus [@noneflow](https://github.com/noneflow) ([#3767](https://github.com/nonebot/nonebot2/pull/3767)) - Plugin: MC服务器状态查询 [@noneflow](https://github.com/noneflow) ([#3781](https://github.com/nonebot/nonebot2/pull/3781)) - Plugin: Instagram RapidAPI 解析 [@noneflow](https://github.com/noneflow) ([#3784](https://github.com/nonebot/nonebot2/pull/3784)) - Plugin: 今天是什么小猪 [@noneflow](https://github.com/noneflow) ([#3773](https://github.com/nonebot/nonebot2/pull/3773)) - Plugin: 御神签 [@noneflow](https://github.com/noneflow) ([#3777](https://github.com/nonebot/nonebot2/pull/3777)) - Plugin: 火车迷铁路工具箱 [@noneflow](https://github.com/noneflow) ([#3770](https://github.com/nonebot/nonebot2/pull/3770)) - Plugin: TerraLink [@noneflow](https://github.com/noneflow) ([#3775](https://github.com/nonebot/nonebot2/pull/3775)) - Plugin: 安安说 [@noneflow](https://github.com/noneflow) ([#3726](https://github.com/nonebot/nonebot2/pull/3726)) - Plugin: 安安的素描本聊天框 [@noneflow](https://github.com/noneflow) ([#3762](https://github.com/nonebot/nonebot2/pull/3762)) - Plugin: manosoba-reply-generator [@noneflow](https://github.com/noneflow) ([#3753](https://github.com/nonebot/nonebot2/pull/3753)) - Plugin: 模板绘图 [@noneflow](https://github.com/noneflow) ([#3752](https://github.com/nonebot/nonebot2/pull/3752)) - Plugin: iPinfo [@noneflow](https://github.com/noneflow) ([#3759](https://github.com/nonebot/nonebot2/pull/3759)) - Plugin: 电子课程表 [@noneflow](https://github.com/noneflow) ([#3743](https://github.com/nonebot/nonebot2/pull/3743)) - Plugin: 魔裁 Memes [@noneflow](https://github.com/noneflow) ([#3755](https://github.com/nonebot/nonebot2/pull/3755)) - Plugin: 图像对称处理 [@noneflow](https://github.com/noneflow) ([#3748](https://github.com/nonebot/nonebot2/pull/3748)) - Plugin: 每日人品 [@noneflow](https://github.com/noneflow) ([#3735](https://github.com/nonebot/nonebot2/pull/3735)) - Plugin: nonebot_plugin_markdown2img [@noneflow](https://github.com/noneflow) ([#3730](https://github.com/nonebot/nonebot2/pull/3730)) - Plugin: B站解析助手 [@noneflow](https://github.com/noneflow) ([#3728](https://github.com/nonebot/nonebot2/pull/3728)) ### 🍻 机器人发布 - Bot: Rosmontis.io [@noneflow](https://github.com/noneflow) ([#3878](https://github.com/nonebot/nonebot2/pull/3878)) ### 🍻 适配器发布 - Adapter: 云湖适配器 [@noneflow](https://github.com/noneflow) ([#3741](https://github.com/nonebot/nonebot2/pull/3741)) ## v2.4.4 ### 🚀 新功能 - Feature: 允许插件从环境变量中读取配置项并支持 alias [@AzideCupric](https://github.com/AzideCupric) ([#3673](https://github.com/nonebot/nonebot2/pull/3673)) - Feature: 更新 NB-CLI 新版插件加载格式与文档 [@NCBM](https://github.com/NCBM) ([#3614](https://github.com/nonebot/nonebot2/pull/3614)) ### 🐛 Bug 修复 - Fix: log level 配置项无法使用 int 类型配置 [@yanyongyu](https://github.com/yanyongyu) ([#3732](https://github.com/nonebot/nonebot2/pull/3732)) - Fix: 兼容 pydantic v2.12 `FieldInfo` 改动 [@yanyongyu](https://github.com/yanyongyu) ([#3722](https://github.com/nonebot/nonebot2/pull/3722)) ### 📝 文档 - Docs: 更新适配器编写指南中的链接 [@xjh2009](https://github.com/xjh2009) ([#3731](https://github.com/nonebot/nonebot2/pull/3731)) - Feature: 更新 NB-CLI 新版插件加载格式与文档 [@NCBM](https://github.com/NCBM) ([#3614](https://github.com/nonebot/nonebot2/pull/3614)) - Docs: 添加 htmlkit 文档至最佳实践 [@BlueGlassBlock](https://github.com/BlueGlassBlock) ([#3682](https://github.com/nonebot/nonebot2/pull/3682)) - Docs: 修复 userinfo 插件链接 [@XieXiLin2](https://github.com/XieXiLin2) ([#3660](https://github.com/nonebot/nonebot2/pull/3660)) - Docs: 升级 docusaurus 3.8.1 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#3649](https://github.com/nonebot/nonebot2/pull/3649)) - Docs: 更新文档《手动创建项目》 [@Chen-Luan](https://github.com/Chen-Luan) ([#3623](https://github.com/nonebot/nonebot2/pull/3623)) - Docs: 增加 B站直播间 适配器说明 [@MingxuanGame](https://github.com/MingxuanGame) ([#3636](https://github.com/nonebot/nonebot2/pull/3636)) - Docs: 增加 VoceChat 适配器说明 [@5656565566](https://github.com/5656565566) ([#3627](https://github.com/nonebot/nonebot2/pull/3627)) ### 💫 杂项 - CI: 严格约束 `test_depend` CPython 版本范围 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#3713](https://github.com/nonebot/nonebot2/pull/3713)) - CI: 升级文档构建 node 版本 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#3668](https://github.com/nonebot/nonebot2/pull/3668)) - CI: 测试矩阵加入 Python 3.13 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#3605](https://github.com/nonebot/nonebot2/pull/3605)) ### 🍻 插件发布 - Plugin: 海龟汤游戏 [@noneflow](https://github.com/noneflow) ([#3697](https://github.com/nonebot/nonebot2/pull/3697)) - Plugin: 每日必应壁纸 [@noneflow](https://github.com/noneflow) ([#3721](https://github.com/nonebot/nonebot2/pull/3721)) - Plugin: pxchat [@noneflow](https://github.com/noneflow) ([#3712](https://github.com/nonebot/nonebot2/pull/3712)) - Plugin: nonebot-plugin-memory [@noneflow](https://github.com/noneflow) ([#3701](https://github.com/nonebot/nonebot2/pull/3701)) - Plugin: 远程文件打开 [@noneflow](https://github.com/noneflow) ([#3717](https://github.com/nonebot/nonebot2/pull/3717)) - Plugin: MC新闻更新检测 [@noneflow](https://github.com/noneflow) ([#3699](https://github.com/nonebot/nonebot2/pull/3699)) - Plugin: kook卡片消息编写适配插件 [@noneflow](https://github.com/noneflow) ([#3708](https://github.com/nonebot/nonebot2/pull/3708)) - Plugin: 链接分享自动解析 [@noneflow](https://github.com/noneflow) ([#3706](https://github.com/nonebot/nonebot2/pull/3706)) - Plugin: 怪物猎人集会码插件 [@noneflow](https://github.com/noneflow) ([#3684](https://github.com/nonebot/nonebot2/pull/3684)) - Plugin: nonebot-plugin-htmlkit [@noneflow](https://github.com/noneflow) ([#3695](https://github.com/nonebot/nonebot2/pull/3695)) - Plugin: 言令 [@noneflow](https://github.com/noneflow) ([#3675](https://github.com/nonebot/nonebot2/pull/3675)) - Plugin: 算法比赛助手 [@noneflow](https://github.com/noneflow) ([#3672](https://github.com/nonebot/nonebot2/pull/3672)) - Plugin: 复盘打卡 [@noneflow](https://github.com/noneflow) ([#3681](https://github.com/nonebot/nonebot2/pull/3681)) - Plugin: DMP 饥荒管理平台机器人 [@noneflow](https://github.com/noneflow) ([#3616](https://github.com/nonebot/nonebot2/pull/3616)) - Plugin: 谁是卧底小游戏 [@noneflow](https://github.com/noneflow) ([#3629](https://github.com/nonebot/nonebot2/pull/3629)) - Plugin: 夸克自动转存 [@noneflow](https://github.com/noneflow) ([#3671](https://github.com/nonebot/nonebot2/pull/3671)) - Plugin: 禁止复读 [@noneflow](https://github.com/noneflow) ([#3644](https://github.com/nonebot/nonebot2/pull/3644)) - Plugin: 蔚蓝档案今日运势 [@noneflow](https://github.com/noneflow) ([#3653](https://github.com/nonebot/nonebot2/pull/3653)) - Plugin: 分布式黑名单插件 [@noneflow](https://github.com/noneflow) ([#3655](https://github.com/nonebot/nonebot2/pull/3655)) - Plugin: 图片手办化 [@noneflow](https://github.com/noneflow) ([#3662](https://github.com/nonebot/nonebot2/pull/3662)) - Plugin: Akash Image Generator [@noneflow](https://github.com/noneflow) ([#3651](https://github.com/nonebot/nonebot2/pull/3651)) - Plugin: 让我看看!! [@noneflow](https://github.com/noneflow) ([#3648](https://github.com/nonebot/nonebot2/pull/3648)) - Plugin: ImageLibrary [@noneflow](https://github.com/noneflow) ([#3620](https://github.com/nonebot/nonebot2/pull/3620)) - Plugin: 抽象 [@noneflow](https://github.com/noneflow) ([#3638](https://github.com/nonebot/nonebot2/pull/3638)) - Plugin: 卖若插件 [@noneflow](https://github.com/noneflow) ([#3631](https://github.com/nonebot/nonebot2/pull/3631)) - Plugin: HuaEr聊天bot [@noneflow](https://github.com/noneflow) ([#3564](https://github.com/nonebot/nonebot2/pull/3564)) - Plugin: Remove nonebot_plugin_cnrail [@noneflow](https://github.com/noneflow) ([#3645](https://github.com/nonebot/nonebot2/pull/3645)) - Plugin: Remove nonebot_plugin_pingti [@noneflow](https://github.com/noneflow) ([#3646](https://github.com/nonebot/nonebot2/pull/3646)) - Plugin: Anipusher推送机 [@noneflow](https://github.com/noneflow) ([#3582](https://github.com/nonebot/nonebot2/pull/3582)) - Plugin: nonebot-plugin-simple-setu [@noneflow](https://github.com/noneflow) ([#3594](https://github.com/nonebot/nonebot2/pull/3594)) - Plugin: Alisten [@noneflow](https://github.com/noneflow) ([#3635](https://github.com/nonebot/nonebot2/pull/3635)) - Plugin: MC玩家皮肤渲染 [@noneflow](https://github.com/noneflow) ([#3613](https://github.com/nonebot/nonebot2/pull/3613)) - Plugin: EconomyValue [@noneflow](https://github.com/noneflow) ([#3566](https://github.com/nonebot/nonebot2/pull/3566)) ### 🍻 机器人发布 - Bot: Amrita [@noneflow](https://github.com/noneflow) ([#3641](https://github.com/nonebot/nonebot2/pull/3641)) ### 🍻 适配器发布 - Adapter: B站直播间 [@noneflow](https://github.com/noneflow) ([#3592](https://github.com/nonebot/nonebot2/pull/3592)) - Adapter: nonebot-adapter-vocechat [@noneflow](https://github.com/noneflow) ([#3536](https://github.com/nonebot/nonebot2/pull/3536)) ## v2.4.3 ### 🚀 新功能 - Feature: 支持 PEP 695 类型别名 [@yanyongyu](https://github.com/yanyongyu) ([#3621](https://github.com/nonebot/nonebot2/pull/3621)) - Feature: 升级至新版本 websockets client API [@yanyongyu](https://github.com/yanyongyu) ([#3606](https://github.com/nonebot/nonebot2/pull/3606)) - Feature: 细化内置驱动器请求参数中的超时控制颗粒度 [@Ailitonia](https://github.com/Ailitonia) ([#3571](https://github.com/nonebot/nonebot2/pull/3571)) - Feature: 为 `HTTPClient` 等内置驱动器添加流式请求方法 [@Ailitonia](https://github.com/Ailitonia) ([#3560](https://github.com/nonebot/nonebot2/pull/3560)) ### 📝 文档 - Docs: 更新 Alconna 主页面 [@RF-Tar-Railt](https://github.com/RF-Tar-Railt) ([#3598](https://github.com/nonebot/nonebot2/pull/3598)) - Docs: 添加插件 metadata 缺失的 docstring [@yanyongyu](https://github.com/yanyongyu) ([#3583](https://github.com/nonebot/nonebot2/pull/3583)) - Docs: 修复组织成员提交 issue 时不遵守表单 [@ProgramRipper](https://github.com/ProgramRipper) ([#3558](https://github.com/nonebot/nonebot2/pull/3558)) - Docs: 增加 `EFChat` 适配器说明 [@molanp](https://github.com/molanp) ([#3544](https://github.com/nonebot/nonebot2/pull/3544)) - Docs: 插件发布表单描述优化 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#3520](https://github.com/nonebot/nonebot2/pull/3520)) - Docs: 增加 `Milky` 适配器说明 [@RF-Tar-Railt](https://github.com/RF-Tar-Railt) ([#3492](https://github.com/nonebot/nonebot2/pull/3492)) - Docs: 添加 OSPP 2025 项目 [@yanyongyu](https://github.com/yanyongyu) ([#3466](https://github.com/nonebot/nonebot2/pull/3466)) - Docs: 更新最佳实践 `Alconna` 章节 [@RF-Tar-Railt](https://github.com/RF-Tar-Railt) ([#3447](https://github.com/nonebot/nonebot2/pull/3447)) - Docs: 修复移动端侧边栏折叠状态异常 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#3414](https://github.com/nonebot/nonebot2/pull/3414)) - Docs: 添加 Gewechat 适配器描述 [@Shine-Light](https://github.com/Shine-Light) ([#3372](https://github.com/nonebot/nonebot2/pull/3372)) ### 💫 杂项 - Dev: 迁移使用 uv 管理项目依赖 [@yanyongyu](https://github.com/yanyongyu) ([#3607](https://github.com/nonebot/nonebot2/pull/3607)) - Fix: `RUF005` tuple 拼接 [@Ailitonia](https://github.com/Ailitonia) ([#3572](https://github.com/nonebot/nonebot2/pull/3572)) - Plugin: 更新插件维护情况 [@Agnes4m](https://github.com/Agnes4m) ([#3555](https://github.com/nonebot/nonebot2/pull/3555)) - Plugin: 修改 nailong 插件作者 [@superbot-ai445](https://github.com/superbot-ai445) ([#3554](https://github.com/nonebot/nonebot2/pull/3554)) - CI: 适配 NoneFlow 4.4.0 [@he0119](https://github.com/he0119) ([#3539](https://github.com/nonebot/nonebot2/pull/3539)) - Develop: 修复 devcontainer feature 配置 [@yanyongyu](https://github.com/yanyongyu) ([#3515](https://github.com/nonebot/nonebot2/pull/3515)) - CI: 修复 Ruff 并发组名称 [@KomoriDev](https://github.com/KomoriDev) ([#3434](https://github.com/nonebot/nonebot2/pull/3434)) ### 🍻 插件发布 - Plugin: FinalShell 离线激活码 [@noneflow](https://github.com/noneflow) ([#3574](https://github.com/nonebot/nonebot2/pull/3574)) - Plugin: 天气查询 [@noneflow](https://github.com/noneflow) ([#3596](https://github.com/nonebot/nonebot2/pull/3596)) - Plugin: LazyTea命令拓展 [@noneflow](https://github.com/noneflow) ([#3604](https://github.com/nonebot/nonebot2/pull/3604)) - Plugin: AWS Manager [@noneflow](https://github.com/noneflow) ([#3550](https://github.com/nonebot/nonebot2/pull/3550)) - Plugin: 三角洲助手 [@noneflow](https://github.com/noneflow) ([#3590](https://github.com/nonebot/nonebot2/pull/3590)) - Plugin: 猜猜病 [@noneflow](https://github.com/noneflow) ([#3585](https://github.com/nonebot/nonebot2/pull/3585)) - Plugin: LLM-Helper [@noneflow](https://github.com/noneflow) ([#3569](https://github.com/nonebot/nonebot2/pull/3569)) - Plugin: 更好的电子钓鱼 [@noneflow](https://github.com/noneflow) ([#3576](https://github.com/nonebot/nonebot2/pull/3576)) - Plugin: nonebot-plugin-flomic [@noneflow](https://github.com/noneflow) ([#3529](https://github.com/nonebot/nonebot2/pull/3529)) - Plugin: LazyTea [@noneflow](https://github.com/noneflow) ([#3548](https://github.com/nonebot/nonebot2/pull/3548)) - Plugin: LitePerm 权限管理插件 [@noneflow](https://github.com/noneflow) ([#3562](https://github.com/nonebot/nonebot2/pull/3562)) - Plugin: 三角洲鼠鼠偷吃模拟器 [@noneflow](https://github.com/noneflow) ([#3541](https://github.com/nonebot/nonebot2/pull/3541)) - Plugin: 维维表情包搜索器 [@noneflow](https://github.com/noneflow) ([#3546](https://github.com/nonebot/nonebot2/pull/3546)) - Plugin: 喜(悲)报生成器 [@noneflow](https://github.com/noneflow) ([#3527](https://github.com/nonebot/nonebot2/pull/3527)) - Plugin: 依赖注入扩展 [@noneflow](https://github.com/noneflow) ([#3552](https://github.com/nonebot/nonebot2/pull/3552)) - Plugin: nonebot-plugin-orm [@noneflow](https://github.com/noneflow) ([#3557](https://github.com/nonebot/nonebot2/pull/3557)) - Plugin: 币安小助手 [@noneflow](https://github.com/noneflow) ([#3525](https://github.com/nonebot/nonebot2/pull/3525)) - Plugin: nonebot_plugin_sunset_reminder [@noneflow](https://github.com/noneflow) ([#3538](https://github.com/nonebot/nonebot2/pull/3538)) - Plugin: nonebot-plugin-NobleDuel [@noneflow](https://github.com/noneflow) ([#3521](https://github.com/nonebot/nonebot2/pull/3521)) - Plugin: 绝区零角色数据获取 [@noneflow](https://github.com/noneflow) ([#3512](https://github.com/nonebot/nonebot2/pull/3512)) - Plugin: 命令冷却 [@noneflow](https://github.com/noneflow) ([#3499](https://github.com/nonebot/nonebot2/pull/3499)) - Plugin: Hacker News [@noneflow](https://github.com/noneflow) ([#3502](https://github.com/nonebot/nonebot2/pull/3502)) - Plugin: nonebot_plugin_df_armor_repair_simulator [@noneflow](https://github.com/noneflow) ([#3506](https://github.com/nonebot/nonebot2/pull/3506)) - Plugin: nonebot-plugin-emojilike-automonkey [@noneflow](https://github.com/noneflow) ([#3386](https://github.com/nonebot/nonebot2/pull/3386)) - Plugin: bfvplayerlist [@noneflow](https://github.com/noneflow) ([#3450](https://github.com/nonebot/nonebot2/pull/3450)) - Plugin: nonebot-plugin-pokemonle [@noneflow](https://github.com/noneflow) ([#3504](https://github.com/nonebot/nonebot2/pull/3504)) - Plugin: NoneBotCloversClient [@noneflow](https://github.com/noneflow) ([#3494](https://github.com/nonebot/nonebot2/pull/3494)) - Plugin: NYATuringTest [@noneflow](https://github.com/noneflow) ([#3479](https://github.com/nonebot/nonebot2/pull/3479)) - Plugin: PicMenu Next [@noneflow](https://github.com/noneflow) ([#3488](https://github.com/nonebot/nonebot2/pull/3488)) - Plugin: nonebot-plugin-ehentai [@noneflow](https://github.com/noneflow) ([#3475](https://github.com/nonebot/nonebot2/pull/3475)) - Plugin: 真寻农场 [@noneflow](https://github.com/noneflow) ([#3477](https://github.com/nonebot/nonebot2/pull/3477)) - Plugin: 修复 QQ 图床 SSL 错误 [@noneflow](https://github.com/noneflow) ([#3482](https://github.com/nonebot/nonebot2/pull/3482)) - Plugin: 多源日报 [@noneflow](https://github.com/noneflow) ([#3468](https://github.com/nonebot/nonebot2/pull/3468)) - Plugin: QQ账号详细信息查询 [@noneflow](https://github.com/noneflow) ([#3472](https://github.com/nonebot/nonebot2/pull/3472)) - Plugin: doro大冒险 [@noneflow](https://github.com/noneflow) ([#3465](https://github.com/nonebot/nonebot2/pull/3465)) - Plugin: nonebot_plugin_paper [@noneflow](https://github.com/noneflow) ([#3431](https://github.com/nonebot/nonebot2/pull/3431)) - Plugin: Web侧载 [@noneflow](https://github.com/noneflow) ([#3470](https://github.com/nonebot/nonebot2/pull/3470)) - Plugin: 群聊广告哒咩 [@noneflow](https://github.com/noneflow) ([#3446](https://github.com/nonebot/nonebot2/pull/3446)) - Plugin: 词库语言进阶版 [@noneflow](https://github.com/noneflow) ([#3459](https://github.com/nonebot/nonebot2/pull/3459)) - Plugin: nonebot-plugin-mhguesser [@noneflow](https://github.com/noneflow) ([#3464](https://github.com/nonebot/nonebot2/pull/3464)) - Plugin: 扑克对决 [@noneflow](https://github.com/noneflow) ([#3409](https://github.com/nonebot/nonebot2/pull/3409)) - Plugin: 一言+ [@noneflow](https://github.com/noneflow) ([#3444](https://github.com/nonebot/nonebot2/pull/3444)) - Plugin: nonebot-plugin-ban-sticker [@noneflow](https://github.com/noneflow) ([#3429](https://github.com/nonebot/nonebot2/pull/3429)) - Plugin: 塔吉多助手 [@noneflow](https://github.com/noneflow) ([#3455](https://github.com/nonebot/nonebot2/pull/3455)) - Plugin: nonebot-plugin-jmcomic [@noneflow](https://github.com/noneflow) ([#3391](https://github.com/nonebot/nonebot2/pull/3391)) - Plugin: nonebot-plugin-custom-face [@noneflow](https://github.com/noneflow) ([#3449](https://github.com/nonebot/nonebot2/pull/3449)) - Plugin: 简易左轮禁言 [@noneflow](https://github.com/noneflow) ([#3453](https://github.com/nonebot/nonebot2/pull/3453)) - Plugin: mcmod百科插件 [@noneflow](https://github.com/noneflow) ([#3443](https://github.com/nonebot/nonebot2/pull/3443)) - Plugin: LaTeX 在线渲染插件 [@noneflow](https://github.com/noneflow) ([#3314](https://github.com/nonebot/nonebot2/pull/3314)) - Plugin: nonebot-plugin-anywhere-llm [@noneflow](https://github.com/noneflow) ([#3393](https://github.com/nonebot/nonebot2/pull/3393)) - Plugin: 贴吧监控 [@noneflow](https://github.com/noneflow) ([#3375](https://github.com/nonebot/nonebot2/pull/3375)) - Plugin: bfvservermap [@noneflow](https://github.com/noneflow) ([#3451](https://github.com/nonebot/nonebot2/pull/3451)) - Plugin: github_release_notifier [@noneflow](https://github.com/noneflow) ([#3388](https://github.com/nonebot/nonebot2/pull/3388)) - Plugin: 暗语消息 [@noneflow](https://github.com/noneflow) ([#3433](https://github.com/nonebot/nonebot2/pull/3433)) - Plugin: 森空岛 [@noneflow](https://github.com/noneflow) ([#3420](https://github.com/nonebot/nonebot2/pull/3420)) - Plugin: asmr100 [@noneflow](https://github.com/noneflow) ([#3418](https://github.com/nonebot/nonebot2/pull/3418)) - Plugin: 报错处理器 [@noneflow](https://github.com/noneflow) ([#3411](https://github.com/nonebot/nonebot2/pull/3411)) - Plugin: AI 群聊助手 [@noneflow](https://github.com/noneflow) ([#3424](https://github.com/nonebot/nonebot2/pull/3424)) - Plugin: nonebot-plugin-furryyunhei [@noneflow](https://github.com/noneflow) ([#3383](https://github.com/nonebot/nonebot2/pull/3383)) - Plugin: MC服务器/玩家查询 [@noneflow](https://github.com/noneflow) ([#3422](https://github.com/nonebot/nonebot2/pull/3422)) - Plugin: none_plugin_oi_helper [@noneflow](https://github.com/noneflow) ([#3416](https://github.com/nonebot/nonebot2/pull/3416)) - Plugin: Gemini Vision [@noneflow](https://github.com/noneflow) ([#3413](https://github.com/nonebot/nonebot2/pull/3413)) - Plugin: nonebot-plugin-zssm [@noneflow](https://github.com/noneflow) ([#3403](https://github.com/nonebot/nonebot2/pull/3403)) - Plugin: 禁漫下载 [@noneflow](https://github.com/noneflow) ([#3395](https://github.com/nonebot/nonebot2/pull/3395)) - Plugin: JMComic插件 [@noneflow](https://github.com/noneflow) ([#3398](https://github.com/nonebot/nonebot2/pull/3398)) - Plugin: MoEllm聊天 [@noneflow](https://github.com/noneflow) ([#3351](https://github.com/nonebot/nonebot2/pull/3351)) - Plugin: whoasked [@noneflow](https://github.com/noneflow) ([#3367](https://github.com/nonebot/nonebot2/pull/3367)) - Plugin: 金价查询 [@noneflow](https://github.com/noneflow) ([#3378](https://github.com/nonebot/nonebot2/pull/3378)) - Plugin: 丁真语音生成器 [@noneflow](https://github.com/noneflow) ([#3316](https://github.com/nonebot/nonebot2/pull/3316)) - Plugin: LLM调用nonebot插件 [@noneflow](https://github.com/noneflow) ([#3380](https://github.com/nonebot/nonebot2/pull/3380)) - Plugin: SuggarChat CloudFlare协议扩展附属插件 [@noneflow](https://github.com/noneflow) ([#3371](https://github.com/nonebot/nonebot2/pull/3371)) - Plugin: No Dirty Message [@noneflow](https://github.com/noneflow) ([#3360](https://github.com/nonebot/nonebot2/pull/3360)) - Plugin: maimai猜歌小游戏 [@noneflow](https://github.com/noneflow) ([#3318](https://github.com/nonebot/nonebot2/pull/3318)) - Plugin: nonebot-plugin-aiochatllm [@noneflow](https://github.com/noneflow) ([#3358](https://github.com/nonebot/nonebot2/pull/3358)) - Plugin: 拟人回复bot [@noneflow](https://github.com/noneflow) ([#3353](https://github.com/nonebot/nonebot2/pull/3353)) ### 🍻 机器人发布 - Bot: nsybot [@noneflow](https://github.com/noneflow) ([#3610](https://github.com/nonebot/nonebot2/pull/3610)) - Bot: Muicebot [@noneflow](https://github.com/noneflow) ([#3523](https://github.com/nonebot/nonebot2/pull/3523)) - Bot: LiteBot [@noneflow](https://github.com/noneflow) ([#3514](https://github.com/nonebot/nonebot2/pull/3514)) ### 🍻 适配器发布 - Adapter: nonebot-adapter-efchat [@noneflow](https://github.com/noneflow) ([#3496](https://github.com/nonebot/nonebot2/pull/3496)) - Adapter: 删除 Gewechat 适配器 [@Shine-Light](https://github.com/Shine-Light) ([#3516](https://github.com/nonebot/nonebot2/pull/3516)) - Adapter: nonebot-adapter-milky [@noneflow](https://github.com/noneflow) ([#3491](https://github.com/nonebot/nonebot2/pull/3491)) - Adapter: Gewechat [@noneflow](https://github.com/noneflow) ([#3306](https://github.com/nonebot/nonebot2/pull/3306)) ## v2.4.2 ### 🚀 新功能 - Feature: 添加 pydantic validator 兼容函数 [@RF-Tar-Railt](https://github.com/RF-Tar-Railt) ([#3291](https://github.com/nonebot/nonebot2/pull/3291)) ### 🐛 Bug 修复 - Fix: shell command 词法解析错误未捕获 [@yanyongyu](https://github.com/yanyongyu) ([#3290](https://github.com/nonebot/nonebot2/pull/3290)) ### 📝 文档 - Docs: 添加第三方插件模版 [@fllesser](https://github.com/fllesser) ([#3361](https://github.com/nonebot/nonebot2/pull/3361)) - Docs: 商店头像 skeleton [@StarHeartHunt](https://github.com/StarHeartHunt) ([#3362](https://github.com/nonebot/nonebot2/pull/3362)) - Docs: 添加 localstore `use_cwd` 配置项文档 [@yanyongyu](https://github.com/yanyongyu) ([#3345](https://github.com/nonebot/nonebot2/pull/3345)) - Docs: 商店插件可用性筛选 \& 更新排序 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#3334](https://github.com/nonebot/nonebot2/pull/3334)) - Docs: 添加 微信公众平台 适配器描述 [@YangRucheng](https://github.com/YangRucheng) ([#3264](https://github.com/nonebot/nonebot2/pull/3264)) - Docs: 添加 黑盒语音 适配器描述 [@lclbm](https://github.com/lclbm) ([#3259](https://github.com/nonebot/nonebot2/pull/3259)) ### 💫 杂项 - Lint: 修复 async 函数返回值 [@yanyongyu](https://github.com/yanyongyu) ([#3364](https://github.com/nonebot/nonebot2/pull/3364)) - Fix: 修复 pyright 类型推导问题 [@yanyongyu](https://github.com/yanyongyu) ([#3347](https://github.com/nonebot/nonebot2/pull/3347)) - Fix: 修复 ruff lint 错误 [@yanyongyu](https://github.com/yanyongyu) ([#3346](https://github.com/nonebot/nonebot2/pull/3346)) - Plugin: 删除插件 `pjsk` [@lgc2333](https://github.com/lgc2333) ([#3332](https://github.com/nonebot/nonebot2/pull/3332)) - CI: 使用官方版本 ruff action [@yanyongyu](https://github.com/yanyongyu) ([#3286](https://github.com/nonebot/nonebot2/pull/3286)) - CI: pyright 版本与 pylance 保持一致 [@yanyongyu](https://github.com/yanyongyu) ([#3285](https://github.com/nonebot/nonebot2/pull/3285)) - CI: 临时降级 release-drafter [@yanyongyu](https://github.com/yanyongyu) ([#3284](https://github.com/nonebot/nonebot2/pull/3284)) - Plugin: 删除 bawiki [@lgc2333](https://github.com/lgc2333) ([#3265](https://github.com/nonebot/nonebot2/pull/3265)) - Plugin: 删除插件 nonebot_plugin_clovers [@KarisAya](https://github.com/KarisAya) ([#3254](https://github.com/nonebot/nonebot2/pull/3254)) ### 🍻 插件发布 - Plugin: ChatGPT (OpenAI API 接口版) [@noneflow](https://github.com/noneflow) ([#3340](https://github.com/nonebot/nonebot2/pull/3340)) - Plugin: 卡bin查询 [@noneflow](https://github.com/noneflow) ([#3324](https://github.com/nonebot/nonebot2/pull/3324)) - Plugin: 唐菲检测 [@noneflow](https://github.com/noneflow) ([#3338](https://github.com/nonebot/nonebot2/pull/3338)) - Plugin: nonebot-plugin-whois [@noneflow](https://github.com/noneflow) ([#3330](https://github.com/nonebot/nonebot2/pull/3330)) - Plugin: Meme Stickers [@noneflow](https://github.com/noneflow) ([#3333](https://github.com/nonebot/nonebot2/pull/3333)) - Plugin: 群昵称更新 [@noneflow](https://github.com/noneflow) ([#3336](https://github.com/nonebot/nonebot2/pull/3336)) - Plugin: 简易AI聊天 [@noneflow](https://github.com/noneflow) ([#3327](https://github.com/nonebot/nonebot2/pull/3327)) - Plugin: Vocu 语音插件 [@noneflow](https://github.com/noneflow) ([#3322](https://github.com/nonebot/nonebot2/pull/3322)) - Plugin: LuoguLuck|洛谷运势 [@noneflow](https://github.com/noneflow) ([#3320](https://github.com/nonebot/nonebot2/pull/3320)) - Plugin: 群聊配置 [@noneflow](https://github.com/noneflow) ([#3311](https://github.com/nonebot/nonebot2/pull/3311)) - Plugin: llmchat [@noneflow](https://github.com/noneflow) ([#3309](https://github.com/nonebot/nonebot2/pull/3309)) - Plugin: [ 缩写翻译 ] 能不能好好说话 - nbnhhsh [@noneflow](https://github.com/noneflow) ([#3296](https://github.com/nonebot/nonebot2/pull/3296)) - Plugin: SuggarChat OpenAI协议聊天插件 [@noneflow](https://github.com/noneflow) ([#3222](https://github.com/nonebot/nonebot2/pull/3222)) - Plugin: nonebot-plugin-ACMD [@noneflow](https://github.com/noneflow) ([#3283](https://github.com/nonebot/nonebot2/pull/3283)) - Plugin: AI群聊机器人 [@noneflow](https://github.com/noneflow) ([#3258](https://github.com/nonebot/nonebot2/pull/3258)) - Plugin: 追番小工具 [@noneflow](https://github.com/noneflow) ([#3279](https://github.com/nonebot/nonebot2/pull/3279)) - Plugin: BotTap [@noneflow](https://github.com/noneflow) ([#3288](https://github.com/nonebot/nonebot2/pull/3288)) - Plugin: DeepSeek [@noneflow](https://github.com/noneflow) ([#3281](https://github.com/nonebot/nonebot2/pull/3281)) - Plugin: 群文件管理 [@noneflow](https://github.com/noneflow) ([#3271](https://github.com/nonebot/nonebot2/pull/3271)) - Plugin: 中英文笑话 [@noneflow](https://github.com/noneflow) ([#3277](https://github.com/nonebot/nonebot2/pull/3277)) - Plugin: 定时提醒 [@noneflow](https://github.com/noneflow) ([#3275](https://github.com/nonebot/nonebot2/pull/3275)) - Plugin: NeuroDraw [@noneflow](https://github.com/noneflow) ([#3269](https://github.com/nonebot/nonebot2/pull/3269)) - Plugin: Remove nonebot_plugin_bili_push [@noneflow](https://github.com/noneflow) ([#3260](https://github.com/nonebot/nonebot2/pull/3260)) - Plugin: 堡垒之夜游戏插件 [@noneflow](https://github.com/noneflow) ([#3251](https://github.com/nonebot/nonebot2/pull/3251)) - Plugin: nonebot-plugin-pictranslator [@noneflow](https://github.com/noneflow) ([#3257](https://github.com/nonebot/nonebot2/pull/3257)) - Plugin: 玉! [@noneflow](https://github.com/noneflow) ([#3247](https://github.com/nonebot/nonebot2/pull/3247)) - Plugin: nonebot_plugin_palworld [@noneflow](https://github.com/noneflow) ([#3244](https://github.com/nonebot/nonebot2/pull/3244)) - Plugin: 群聊总结 [@noneflow](https://github.com/noneflow) ([#3242](https://github.com/nonebot/nonebot2/pull/3242)) - Plugin: 恶魔轮盘轻量版 [@noneflow](https://github.com/noneflow) ([#3224](https://github.com/nonebot/nonebot2/pull/3224)) - Plugin: 羡慕 koishi [@noneflow](https://github.com/noneflow) ([#3234](https://github.com/nonebot/nonebot2/pull/3234)) - Plugin: 每日天文一图 [@noneflow](https://github.com/noneflow) ([#3229](https://github.com/nonebot/nonebot2/pull/3229)) - Plugin: 音频文件BPM计算器 [@noneflow](https://github.com/noneflow) ([#3217](https://github.com/nonebot/nonebot2/pull/3217)) - Plugin: nonebot_plugin_qbittorrent_manager [@noneflow](https://github.com/noneflow) ([#3208](https://github.com/nonebot/nonebot2/pull/3208)) - Plugin: 群成员检测 dev版 [@noneflow](https://github.com/noneflow) ([#3214](https://github.com/nonebot/nonebot2/pull/3214)) - Plugin: Liar's Bar [@noneflow](https://github.com/noneflow) ([#3212](https://github.com/nonebot/nonebot2/pull/3212)) - Plugin: 他们在聊什么 [@noneflow](https://github.com/noneflow) ([#3210](https://github.com/nonebot/nonebot2/pull/3210)) - Plugin: 小真寻的WebUi [@noneflow](https://github.com/noneflow) ([#3206](https://github.com/nonebot/nonebot2/pull/3206)) - Plugin: 夸克搜 [@noneflow](https://github.com/noneflow) ([#3203](https://github.com/nonebot/nonebot2/pull/3203)) ### 🍻 机器人发布 - Bot: PickStarsBot [@noneflow](https://github.com/noneflow) ([#3273](https://github.com/nonebot/nonebot2/pull/3273)) - Bot: Nekro Agent Bot [@noneflow](https://github.com/noneflow) ([#3267](https://github.com/nonebot/nonebot2/pull/3267)) - Bot: AntiFraudBot [@noneflow](https://github.com/noneflow) ([#3263](https://github.com/nonebot/nonebot2/pull/3263)) ### 🍻 适配器发布 - Adapter: WXMP [@noneflow](https://github.com/noneflow) ([#3219](https://github.com/nonebot/nonebot2/pull/3219)) - Adapter: 黑盒语音 [@noneflow](https://github.com/noneflow) ([#3249](https://github.com/nonebot/nonebot2/pull/3249)) ## v2.4.1 ### 🚀 新功能 - Feature: 存储 matcher 发送 prompt 的结果 [@yanyongyu](https://github.com/yanyongyu) ([#3155](https://github.com/nonebot/nonebot2/pull/3155)) - Feature: 提升已加载的适配器日志等级 [@yanyongyu](https://github.com/yanyongyu) ([#3110](https://github.com/nonebot/nonebot2/pull/3110)) ### 🐛 Bug 修复 - Fix: httpx proxy 与 aiohttp timeout 参数新版本修改 [@yanyongyu](https://github.com/yanyongyu) ([#3152](https://github.com/nonebot/nonebot2/pull/3152)) - Fix: 屏蔽 pydantic 2.10.0 [@yanyongyu](https://github.com/yanyongyu) ([#3137](https://github.com/nonebot/nonebot2/pull/3137)) ### 📝 文档 - Docs: 添加 localstore 插件配置 [@yanyongyu](https://github.com/yanyongyu) ([#3197](https://github.com/nonebot/nonebot2/pull/3197)) - Docs: 使用勾选框而不是评论来重新测试插件 [@he0119](https://github.com/he0119) ([#3158](https://github.com/nonebot/nonebot2/pull/3158)) - Docs: 添加 pytest-asyncio 配置 [@yanyongyu](https://github.com/yanyongyu) ([#3136](https://github.com/nonebot/nonebot2/pull/3136)) - Docs: 移除侧栏遮罩及启用构建加速 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#3135](https://github.com/nonebot/nonebot2/pull/3135)) - Docs: 添加 Mail 适配器说明 [@mobyw](https://github.com/mobyw) ([#3134](https://github.com/nonebot/nonebot2/pull/3134)) - Docs: 修复 wwads 造成的 client 水合不匹配 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#3106](https://github.com/nonebot/nonebot2/pull/3106)) - Docs: 修复 wwads [@StarHeartHunt](https://github.com/StarHeartHunt) ([#3105](https://github.com/nonebot/nonebot2/pull/3105)) - Docs: 修复侧边栏折叠状态问题 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#3101](https://github.com/nonebot/nonebot2/pull/3101)) - Docs: Changelog 按页码挂载 route [@StarHeartHunt](https://github.com/StarHeartHunt) ([#3100](https://github.com/nonebot/nonebot2/pull/3100)) - Docs: 修复 changelog 链接 [@yanyongyu](https://github.com/yanyongyu) ([#3098](https://github.com/nonebot/nonebot2/pull/3098)) ### 💫 杂项 - Develop: 发布议题模板增加 Publish 标签 [@he0119](https://github.com/he0119) ([#3174](https://github.com/nonebot/nonebot2/pull/3174)) - Plugin: 移除插件 riffusion [@lgc2333](https://github.com/lgc2333) ([#3171](https://github.com/nonebot/nonebot2/pull/3171)) - CI: 删除 NoneFlow 中关于 pre-commit 的部分 [@he0119](https://github.com/he0119) ([#3166](https://github.com/nonebot/nonebot2/pull/3166)) - Develop: 完全使用 ruff 替代 isort 与 black [@yanyongyu](https://github.com/yanyongyu) ([#3151](https://github.com/nonebot/nonebot2/pull/3151)) - Plugin: 删除`function` 插件,添加 `batch-withdrawal` 插件标签 [@zhongwen-4](https://github.com/zhongwen-4) ([#3118](https://github.com/nonebot/nonebot2/pull/3118)) - Plugin: 删除插件 `nonebot-plugin-llob-master` [@kanbereina](https://github.com/kanbereina) ([#3115](https://github.com/nonebot/nonebot2/pull/3115)) ### 🍻 插件发布 - Plugin: nonebot_plugin_pjsekaihelper [@noneflow](https://github.com/noneflow) ([#3180](https://github.com/nonebot/nonebot2/pull/3180)) - Plugin: Prometheus 监控 [@noneflow](https://github.com/noneflow) ([#3199](https://github.com/nonebot/nonebot2/pull/3199)) - Plugin: 加群自动审批 [@noneflow](https://github.com/noneflow) ([#3201](https://github.com/nonebot/nonebot2/pull/3201)) - Plugin: nonebot-plugin-flo-luck [@noneflow](https://github.com/noneflow) ([#3188](https://github.com/nonebot/nonebot2/pull/3188)) - Plugin: 求生之路addons文件管理 [@noneflow](https://github.com/noneflow) ([#3194](https://github.com/nonebot/nonebot2/pull/3194)) - Plugin: 好友与群邀请管理 [@noneflow](https://github.com/noneflow) ([#3190](https://github.com/nonebot/nonebot2/pull/3190)) - Plugin: 简易群聊屏蔽 [@noneflow](https://github.com/noneflow) ([#3196](https://github.com/nonebot/nonebot2/pull/3196)) - Plugin: Arcaea表情包生成器 [@noneflow](https://github.com/noneflow) ([#3160](https://github.com/nonebot/nonebot2/pull/3160)) - Plugin: PicStatus Template ZhenXun [@noneflow](https://github.com/noneflow) ([#3192](https://github.com/nonebot/nonebot2/pull/3192)) - Plugin: nonebot-plugin-ollama [@noneflow](https://github.com/noneflow) ([#3182](https://github.com/nonebot/nonebot2/pull/3182)) - Plugin: 南无阿弥陀佛 [@noneflow](https://github.com/noneflow) ([#3178](https://github.com/nonebot/nonebot2/pull/3178)) - Plugin: 奶龙魔法 [@noneflow](https://github.com/noneflow) ([#3175](https://github.com/nonebot/nonebot2/pull/3175)) - Plugin: CSGO_MARKET [@noneflow](https://github.com/noneflow) ([#3148](https://github.com/nonebot/nonebot2/pull/3148)) - Plugin: 谷歌 Gemini 多模态助手 [@noneflow](https://github.com/noneflow) ([#3165](https://github.com/nonebot/nonebot2/pull/3165)) - Plugin: 名片赞,表情回应插件 [@noneflow](https://github.com/noneflow) ([#3170](https://github.com/nonebot/nonebot2/pull/3170)) - Plugin: PCR签到重制版 [@noneflow](https://github.com/noneflow) ([#3157](https://github.com/nonebot/nonebot2/pull/3157)) - Plugin: 发言统计 [@noneflow](https://github.com/noneflow) ([#3162](https://github.com/nonebot/nonebot2/pull/3162)) - Plugin: 链接分享解析器重制版 [@noneflow](https://github.com/noneflow) ([#3154](https://github.com/nonebot/nonebot2/pull/3154)) - Plugin: ZXWB词库问答 [@noneflow](https://github.com/noneflow) ([#3143](https://github.com/nonebot/nonebot2/pull/3143)) - Plugin: nonebot-plugin-hyp [@noneflow](https://github.com/noneflow) ([#3140](https://github.com/nonebot/nonebot2/pull/3140)) - Plugin: nonebot-plugin-zepplife [@noneflow](https://github.com/noneflow) ([#3132](https://github.com/nonebot/nonebot2/pull/3132)) - Plugin: 权限控制 [@noneflow](https://github.com/noneflow) ([#3112](https://github.com/nonebot/nonebot2/pull/3112)) - Plugin: nonebot-plugin-api-scheduler [@noneflow](https://github.com/noneflow) ([#3133](https://github.com/nonebot/nonebot2/pull/3133)) - Plugin: nb商店插件安装器web版 [@noneflow](https://github.com/noneflow) ([#3126](https://github.com/nonebot/nonebot2/pull/3126)) - Plugin: 恩情课文 [@noneflow](https://github.com/noneflow) ([#3123](https://github.com/nonebot/nonebot2/pull/3123)) - Plugin: 自动点赞订阅赞 [@noneflow](https://github.com/noneflow) ([#3103](https://github.com/nonebot/nonebot2/pull/3103)) - Plugin: 违禁词撤回 [@noneflow](https://github.com/noneflow) ([#3117](https://github.com/nonebot/nonebot2/pull/3117)) - Plugin: b站弹幕监控 [@noneflow](https://github.com/noneflow) ([#3091](https://github.com/nonebot/nonebot2/pull/3091)) - Plugin: nonebot_plugin_better_broadcast [@noneflow](https://github.com/noneflow) ([#3114](https://github.com/nonebot/nonebot2/pull/3114)) - Plugin: PyPi下载统计 [@noneflow](https://github.com/noneflow) ([#3109](https://github.com/nonebot/nonebot2/pull/3109)) - Plugin: nonebot-plugin-leetcodeapi-khasa [@noneflow](https://github.com/noneflow) ([#3077](https://github.com/nonebot/nonebot2/pull/3077)) - Plugin: Ohh My Bot [@noneflow](https://github.com/noneflow) ([#3089](https://github.com/nonebot/nonebot2/pull/3089)) ### 🍻 机器人发布 - Bot: Mio澪 [@noneflow](https://github.com/noneflow) ([#3121](https://github.com/nonebot/nonebot2/pull/3121)) ### 🍻 适配器发布 - Adapter: Mail [@noneflow](https://github.com/noneflow) ([#3129](https://github.com/nonebot/nonebot2/pull/3129)) ## v2.4.0 ### 🚀 新功能 - Feature: 跳过部分非必要的 task group 创建 [@yanyongyu](https://github.com/yanyongyu) ([#3095](https://github.com/nonebot/nonebot2/pull/3095)) - Feature: 迁移至结构化并发框架 AnyIO [@yanyongyu](https://github.com/yanyongyu) ([#3053](https://github.com/nonebot/nonebot2/pull/3053)) - Feature: 添加 websockets 驱动器 proxy 连接警告 [@shoucandanghehe](https://github.com/shoucandanghehe) ([#2916](https://github.com/nonebot/nonebot2/pull/2916)) ### 🐛 Bug 修复 - Fix: 修复结构化并发子依赖取消缓存问题 [@yanyongyu](https://github.com/yanyongyu) ([#3084](https://github.com/nonebot/nonebot2/pull/3084)) ### 📝 文档 - Docs: 新增 nonebug 新版启动需要的配置 [@yanyongyu](https://github.com/yanyongyu) ([#3087](https://github.com/nonebot/nonebot2/pull/3087)) - Docs: 修复侧边栏滚动 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#3062](https://github.com/nonebot/nonebot2/pull/3062)) - Docs: 升级到 Docusaurus V3 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#2956](https://github.com/nonebot/nonebot2/pull/2956)) - Docs: 修改文档示例代码与部分表述 [@yixinNB](https://github.com/yixinNB) ([#2797](https://github.com/nonebot/nonebot2/pull/2797)) - Docs: 添加钩子函数 IgnoredException 用法 [@refparo](https://github.com/refparo) ([#2912](https://github.com/nonebot/nonebot2/pull/2912)) ### 💫 杂项 - Plugin: 移除不再维护的插件 [@ssttkkl](https://github.com/ssttkkl) ([#3040](https://github.com/nonebot/nonebot2/pull/3040)) - Plugin: 删除不再维护的 simplemusic hikarisearch 插件 [@MeetWq](https://github.com/MeetWq) ([#2933](https://github.com/nonebot/nonebot2/pull/2933)) - Plugin: 删除插件 `nonebot-plugin-ntqq-restart` [@kanbereina](https://github.com/kanbereina) ([#2926](https://github.com/nonebot/nonebot2/pull/2926)) - Adapter: 移除社区版 mirai 适配器 [@RF-Tar-Railt](https://github.com/RF-Tar-Railt) ([#2909](https://github.com/nonebot/nonebot2/pull/2909)) ### 🍻 插件发布 - Plugin: Comfyui绘图插件 [@noneflow](https://github.com/noneflow) ([#3081](https://github.com/nonebot/nonebot2/pull/3081)) - Plugin: 每日wife [@noneflow](https://github.com/noneflow) ([#3094](https://github.com/nonebot/nonebot2/pull/3094)) - Plugin: nonebot_plugin_impart [@noneflow](https://github.com/noneflow) ([#3079](https://github.com/nonebot/nonebot2/pull/3079)) - Plugin: Pix图库 [@noneflow](https://github.com/noneflow) ([#3083](https://github.com/nonebot/nonebot2/pull/3083)) - Plugin: nonebot_plugin_partner_join [@noneflow](https://github.com/noneflow) ([#3051](https://github.com/nonebot/nonebot2/pull/3051)) - Plugin: pong [@noneflow](https://github.com/noneflow) ([#3066](https://github.com/nonebot/nonebot2/pull/3066)) - Plugin: Bot的消息也是消息 [@noneflow](https://github.com/noneflow) ([#3064](https://github.com/nonebot/nonebot2/pull/3064)) - Plugin: BiliMusic Downloader [@noneflow](https://github.com/noneflow) ([#3046](https://github.com/nonebot/nonebot2/pull/3046)) - Plugin: 防撤回 [@noneflow](https://github.com/noneflow) ([#3055](https://github.com/nonebot/nonebot2/pull/3055)) - Plugin: nonebot_plugin_mai_arcade [@noneflow](https://github.com/noneflow) ([#3047](https://github.com/nonebot/nonebot2/pull/3047)) - Plugin: DDNet 成绩查询 [@noneflow](https://github.com/noneflow) ([#3031](https://github.com/nonebot/nonebot2/pull/3031)) - Plugin: 省流 [@noneflow](https://github.com/noneflow) ([#3052](https://github.com/nonebot/nonebot2/pull/3052)) - Plugin: FishSpeechTTS [@noneflow](https://github.com/noneflow) ([#3050](https://github.com/nonebot/nonebot2/pull/3050)) - Plugin: 语音点歌 [@noneflow](https://github.com/noneflow) ([#3037](https://github.com/nonebot/nonebot2/pull/3037)) - Plugin: Gotify [@noneflow](https://github.com/noneflow) ([#3043](https://github.com/nonebot/nonebot2/pull/3043)) - Plugin: 涩图插件 [@noneflow](https://github.com/noneflow) ([#3039](https://github.com/nonebot/nonebot2/pull/3039)) - Plugin: boom [@noneflow](https://github.com/noneflow) ([#3017](https://github.com/nonebot/nonebot2/pull/3017)) - Plugin: 恶魔轮盘赌 [@noneflow](https://github.com/noneflow) ([#3033](https://github.com/nonebot/nonebot2/pull/3033)) - Plugin: 机厅 [@noneflow](https://github.com/noneflow) ([#3029](https://github.com/nonebot/nonebot2/pull/3029)) - Plugin: PM帮助 [@noneflow](https://github.com/noneflow) ([#3023](https://github.com/nonebot/nonebot2/pull/3023)) - Plugin: NailongRemove [@noneflow](https://github.com/noneflow) ([#2972](https://github.com/nonebot/nonebot2/pull/2972)) - Plugin: 团购 [@noneflow](https://github.com/noneflow) ([#3027](https://github.com/nonebot/nonebot2/pull/3027)) - Plugin: 真寻日报 [@noneflow](https://github.com/noneflow) ([#3021](https://github.com/nonebot/nonebot2/pull/3021)) - Plugin: 运行状态 [@noneflow](https://github.com/noneflow) ([#3019](https://github.com/nonebot/nonebot2/pull/3019)) - Plugin: 西工大翱翔门户成绩监控 [@noneflow](https://github.com/noneflow) ([#3013](https://github.com/nonebot/nonebot2/pull/3013)) - Plugin: nb插件更新器 [@noneflow](https://github.com/noneflow) ([#3015](https://github.com/nonebot/nonebot2/pull/3015)) - Plugin: 涩涩保存器 [@noneflow](https://github.com/noneflow) ([#2988](https://github.com/nonebot/nonebot2/pull/2988)) - Plugin: nonebot_plugin_BFVsearch [@noneflow](https://github.com/noneflow) ([#3008](https://github.com/nonebot/nonebot2/pull/3008)) - Plugin: lingyi_chat [@noneflow](https://github.com/noneflow) ([#3006](https://github.com/nonebot/nonebot2/pull/3006)) - Plugin: ZXPM插件管理 [@noneflow](https://github.com/noneflow) ([#3003](https://github.com/nonebot/nonebot2/pull/3003)) - Plugin: MinecraftWatcher [@noneflow](https://github.com/noneflow) ([#3010](https://github.com/nonebot/nonebot2/pull/3010)) - Plugin: BF5_grouptools [@noneflow](https://github.com/noneflow) ([#3004](https://github.com/nonebot/nonebot2/pull/3004)) - Plugin: lolinfo [@noneflow](https://github.com/noneflow) ([#2997](https://github.com/nonebot/nonebot2/pull/2997)) - Plugin: osu! Match Monitor [@noneflow](https://github.com/noneflow) ([#2985](https://github.com/nonebot/nonebot2/pull/2985)) - Plugin: Marsho AI插件 [@noneflow](https://github.com/noneflow) ([#2993](https://github.com/nonebot/nonebot2/pull/2993)) - Plugin: nonechat [@noneflow](https://github.com/noneflow) ([#2990](https://github.com/nonebot/nonebot2/pull/2990)) - Plugin: nonebot_plugin_SimpleToWrite [@noneflow](https://github.com/noneflow) ([#2995](https://github.com/nonebot/nonebot2/pull/2995)) - Plugin: Beat Saber查分器 [@noneflow](https://github.com/noneflow) ([#2974](https://github.com/nonebot/nonebot2/pull/2974)) - Plugin: githubmodels [@noneflow](https://github.com/noneflow) ([#2945](https://github.com/nonebot/nonebot2/pull/2945)) - Plugin: 给我点颜色瞧瞧 [@noneflow](https://github.com/noneflow) ([#2984](https://github.com/nonebot/nonebot2/pull/2984)) - Plugin: pjsk-helper [@noneflow](https://github.com/noneflow) ([#2980](https://github.com/nonebot/nonebot2/pull/2980)) - Plugin: 趣味内容插件 [@noneflow](https://github.com/noneflow) ([#2981](https://github.com/nonebot/nonebot2/pull/2981)) - Plugin: 计算器:游戏 [@noneflow](https://github.com/noneflow) ([#2976](https://github.com/nonebot/nonebot2/pull/2976)) - Plugin: nonebot-plugin-yareminder [@noneflow](https://github.com/noneflow) ([#2964](https://github.com/nonebot/nonebot2/pull/2964)) - Plugin: 批量撤回 [@noneflow](https://github.com/noneflow) ([#2966](https://github.com/nonebot/nonebot2/pull/2966)) - Plugin: inspect [@noneflow](https://github.com/noneflow) ([#2971](https://github.com/nonebot/nonebot2/pull/2971)) - Plugin: 通用信息 [@noneflow](https://github.com/noneflow) ([#2969](https://github.com/nonebot/nonebot2/pull/2969)) - Plugin: SSE日志输出流 [@noneflow](https://github.com/noneflow) ([#2960](https://github.com/nonebot/nonebot2/pull/2960)) - Plugin: WITFF [@noneflow](https://github.com/noneflow) ([#2955](https://github.com/nonebot/nonebot2/pull/2955)) - Plugin: weather-rank [@noneflow](https://github.com/noneflow) ([#2949](https://github.com/nonebot/nonebot2/pull/2949)) - Plugin: 二维码生成器 [@noneflow](https://github.com/noneflow) ([#2942](https://github.com/nonebot/nonebot2/pull/2942)) - Plugin: 次元星辰 [@noneflow](https://github.com/noneflow) ([#2935](https://github.com/nonebot/nonebot2/pull/2935)) - Plugin: nonebot-plugin-tarina-lang-turbo [@noneflow](https://github.com/noneflow) ([#2938](https://github.com/nonebot/nonebot2/pull/2938)) - Plugin: 狼人杀 [@noneflow](https://github.com/noneflow) ([#2932](https://github.com/nonebot/nonebot2/pull/2932)) - Plugin: 阿瓦隆 [@noneflow](https://github.com/noneflow) ([#2915](https://github.com/nonebot/nonebot2/pull/2915)) - Plugin: 消音器 [@noneflow](https://github.com/noneflow) ([#2919](https://github.com/nonebot/nonebot2/pull/2919)) - Plugin: 悠悠 [@noneflow](https://github.com/noneflow) ([#2928](https://github.com/nonebot/nonebot2/pull/2928)) - Plugin: LLOneBot-Master [@noneflow](https://github.com/noneflow) ([#2925](https://github.com/nonebot/nonebot2/pull/2925)) - Plugin: 无情的发图姬 [@noneflow](https://github.com/noneflow) ([#2923](https://github.com/nonebot/nonebot2/pull/2923)) - Plugin: maimai DX 查分 [@noneflow](https://github.com/noneflow) ([#2921](https://github.com/nonebot/nonebot2/pull/2921)) - Plugin: Minecraft查服 [@noneflow](https://github.com/noneflow) ([#2882](https://github.com/nonebot/nonebot2/pull/2882)) - Plugin: lagrange [@noneflow](https://github.com/noneflow) ([#2898](https://github.com/nonebot/nonebot2/pull/2898)) - Plugin: nekro-agent [@noneflow](https://github.com/noneflow) ([#2896](https://github.com/nonebot/nonebot2/pull/2896)) - Plugin: nonebot_plugin_mute [@noneflow](https://github.com/noneflow) ([#2893](https://github.com/nonebot/nonebot2/pull/2893)) - Plugin: LiteyukiBot(plugin) [@noneflow](https://github.com/noneflow) ([#2905](https://github.com/nonebot/nonebot2/pull/2905)) - Plugin: 复读6 [@noneflow](https://github.com/noneflow) ([#2900](https://github.com/nonebot/nonebot2/pull/2900)) ### 🍻 机器人发布 - Bot: CanrotBot [@noneflow](https://github.com/noneflow) ([#3086](https://github.com/nonebot/nonebot2/pull/3086)) - Bot: 小安提Bot [@noneflow](https://github.com/noneflow) ([#3061](https://github.com/nonebot/nonebot2/pull/3061)) ## v2.3.3 ### 🚀 新功能 - Feature: 优化依赖注入在 pydantic v2 下的性能 [@yanyongyu](https://github.com/yanyongyu) ([#2870](https://github.com/nonebot/nonebot2/pull/2870)) - Feature: 添加遗漏的类型标注 [@yanyongyu](https://github.com/yanyongyu) ([#2856](https://github.com/nonebot/nonebot2/pull/2856)) ### 🐛 Bug 修复 - Fix: 错误的类型标注和 annotated 处理 [@yanyongyu](https://github.com/yanyongyu) ([#2828](https://github.com/nonebot/nonebot2/pull/2828)) ### 📝 文档 - Docs: 添加 Windows Powershell 设置环境变量方法 [@LeoQuote](https://github.com/LeoQuote) ([#2874](https://github.com/nonebot/nonebot2/pull/2874)) - Docs: 更新 localstore 插件文档 [@yanyongyu](https://github.com/yanyongyu) ([#2871](https://github.com/nonebot/nonebot2/pull/2871)) ### 💫 杂项 - Plugin: 修改插件 system-command 信息 [@tkgs0](https://github.com/tkgs0) ([#2862](https://github.com/nonebot/nonebot2/pull/2862)) - Plugin: 修改 nonebot-plugin-fishing 插件作者 [@ALittleBot](https://github.com/ALittleBot) ([#2854](https://github.com/nonebot/nonebot2/pull/2854)) - Bot: 更新 Minecraft QQBot 信息 [@Lonely-Sails](https://github.com/Lonely-Sails) ([#2838](https://github.com/nonebot/nonebot2/pull/2838)) - Plugin: 移除 kanonbot 插件 [@SuperGuGuGu](https://github.com/SuperGuGuGu) ([#2819](https://github.com/nonebot/nonebot2/pull/2819)) - Plugin: 更新插件 sparkapi 信息 [@CCLMSY](https://github.com/CCLMSY) ([#2812](https://github.com/nonebot/nonebot2/pull/2812)) - Plugin: 修改插件 miragetank \& charpic 信息 [@1umine](https://github.com/1umine) ([#2807](https://github.com/nonebot/nonebot2/pull/2807)) ### 🍻 插件发布 - Plugin: nonebot-plugin-wait-a-minute [@noneflow](https://github.com/noneflow) ([#2902](https://github.com/nonebot/nonebot2/pull/2902)) - Plugin: 你看我像 [@noneflow](https://github.com/noneflow) ([#2895](https://github.com/nonebot/nonebot2/pull/2895)) - Plugin: dify插件 [@noneflow](https://github.com/noneflow) ([#2889](https://github.com/nonebot/nonebot2/pull/2889)) - Plugin: mai2_pcount [@noneflow](https://github.com/noneflow) ([#2891](https://github.com/nonebot/nonebot2/pull/2891)) - Plugin: nonebot-plugin-ehentai-search [@noneflow](https://github.com/noneflow) ([#2885](https://github.com/nonebot/nonebot2/pull/2885)) - Plugin: pokepoke_miss [@noneflow](https://github.com/noneflow) ([#2883](https://github.com/nonebot/nonebot2/pull/2883)) - Plugin: 聊天截图伪造 [@noneflow](https://github.com/noneflow) ([#2880](https://github.com/nonebot/nonebot2/pull/2880)) - Plugin: ba-tools [@noneflow](https://github.com/noneflow) ([#2867](https://github.com/nonebot/nonebot2/pull/2867)) - Plugin: 精华消息管理 [@noneflow](https://github.com/noneflow) ([#2873](https://github.com/nonebot/nonebot2/pull/2873)) - Plugin: B站收藏夹监视器 [@noneflow](https://github.com/noneflow) ([#2869](https://github.com/nonebot/nonebot2/pull/2869)) - Plugin: Alist [@noneflow](https://github.com/noneflow) ([#2865](https://github.com/nonebot/nonebot2/pull/2865)) - Plugin: 🦌管签到 [@noneflow](https://github.com/noneflow) ([#2859](https://github.com/nonebot/nonebot2/pull/2859)) - Plugin: 漂流瓶 [@noneflow](https://github.com/noneflow) ([#2861](https://github.com/nonebot/nonebot2/pull/2861)) - Plugin: 奇怪的小功能 [@noneflow](https://github.com/noneflow) ([#2851](https://github.com/nonebot/nonebot2/pull/2851)) - Plugin: SunoAI音乐生成 [@noneflow](https://github.com/noneflow) ([#2853](https://github.com/nonebot/nonebot2/pull/2853)) - Plugin: 谁是卷王 [@noneflow](https://github.com/noneflow) ([#2849](https://github.com/nonebot/nonebot2/pull/2849)) - Plugin: GPT-SoVITS 语音合成 [@noneflow](https://github.com/noneflow) ([#2847](https://github.com/nonebot/nonebot2/pull/2847)) - Plugin: 基于清影的AI视频生成 [@noneflow](https://github.com/noneflow) ([#2843](https://github.com/nonebot/nonebot2/pull/2843)) - Plugin: 命令行 [@noneflow](https://github.com/noneflow) ([#2840](https://github.com/nonebot/nonebot2/pull/2840)) - Plugin: exe_code [@noneflow](https://github.com/noneflow) ([#2835](https://github.com/nonebot/nonebot2/pull/2835)) - Plugin: nonebot-plugin-autopush [@noneflow](https://github.com/noneflow) ([#2833](https://github.com/nonebot/nonebot2/pull/2833)) - Plugin: vv_helper [@noneflow](https://github.com/noneflow) ([#2825](https://github.com/nonebot/nonebot2/pull/2825)) - Plugin: nonebot_plugin_game_torrent [@noneflow](https://github.com/noneflow) ([#2827](https://github.com/nonebot/nonebot2/pull/2827)) - Plugin: 每日油价 [@noneflow](https://github.com/noneflow) ([#2822](https://github.com/nonebot/nonebot2/pull/2822)) - Plugin: wordle [@noneflow](https://github.com/noneflow) ([#2818](https://github.com/nonebot/nonebot2/pull/2818)) - Plugin: 再润 [@noneflow](https://github.com/noneflow) ([#2816](https://github.com/nonebot/nonebot2/pull/2816)) - Plugin: 漫展/展览查询 [@noneflow](https://github.com/noneflow) ([#2811](https://github.com/nonebot/nonebot2/pull/2811)) - Plugin: 鸣潮wiki [@noneflow](https://github.com/noneflow) ([#2804](https://github.com/nonebot/nonebot2/pull/2804)) - Plugin: cloudfare R2 客服端 [@noneflow](https://github.com/noneflow) ([#2806](https://github.com/nonebot/nonebot2/pull/2806)) - Plugin: AnyMate小助手 [@noneflow](https://github.com/noneflow) ([#2761](https://github.com/nonebot/nonebot2/pull/2761)) ### 🍻 机器人发布 - Bot: Minecraft_QQBot [@noneflow](https://github.com/noneflow) ([#2837](https://github.com/nonebot/nonebot2/pull/2837)) - Bot: 星辰 Bot [@noneflow](https://github.com/noneflow) ([#2824](https://github.com/nonebot/nonebot2/pull/2824)) ## v2.3.2 ### 🐛 Bug 修复 - Fix: 修复 ForwardRef eval 时参数 recursive_guard 缺失 [@he0119](https://github.com/he0119) ([#2778](https://github.com/nonebot/nonebot2/pull/2778)) ### 📝 文档 - Docs: 修改导航栏开源之夏链接 [@KomoriDev](https://github.com/KomoriDev) ([#2798](https://github.com/nonebot/nonebot2/pull/2798)) - Docs: `on_keyword` 参数类型错误 [@TaskManagerOL](https://github.com/TaskManagerOL) ([#2795](https://github.com/nonebot/nonebot2/pull/2795)) - Docs: 修复单元测试示例代码 [@mobyw](https://github.com/mobyw) ([#2741](https://github.com/nonebot/nonebot2/pull/2741)) - Docs: 修改依赖注入定义链接 [@Weltolk](https://github.com/Weltolk) ([#2733](https://github.com/nonebot/nonebot2/pull/2733)) ### 🍻 插件发布 - Plugin: 指令更新NapCat [@noneflow](https://github.com/noneflow) ([#2791](https://github.com/nonebot/nonebot2/pull/2791)) - Plugin: QQ群-Discord 互通 [@noneflow](https://github.com/noneflow) ([#2788](https://github.com/nonebot/nonebot2/pull/2788)) - Plugin: nonebot_plugin_obastatus [@noneflow](https://github.com/noneflow) ([#2780](https://github.com/nonebot/nonebot2/pull/2780)) - Plugin: b站消息转发 [@noneflow](https://github.com/noneflow) ([#2785](https://github.com/nonebot/nonebot2/pull/2785)) - Plugin: Daily Task [@noneflow](https://github.com/noneflow) ([#2769](https://github.com/nonebot/nonebot2/pull/2769)) - Plugin: EVE ONLINE 多功能机器人 版本 - v0.2.3 [@noneflow](https://github.com/noneflow) ([#2782](https://github.com/nonebot/nonebot2/pull/2782)) - Plugin: NTQQ自动登录/断连重启 [@noneflow](https://github.com/noneflow) ([#2786](https://github.com/nonebot/nonebot2/pull/2786)) - Plugin: asmr [@noneflow](https://github.com/noneflow) ([#2775](https://github.com/nonebot/nonebot2/pull/2775)) - Plugin: 日麻猜手牌小游戏 [@noneflow](https://github.com/noneflow) ([#2777](https://github.com/nonebot/nonebot2/pull/2777)) - Plugin: 绝地潜兵信息查询小助手 [@noneflow](https://github.com/noneflow) ([#2772](https://github.com/nonebot/nonebot2/pull/2772)) - Plugin: MCSM小助手 [@noneflow](https://github.com/noneflow) ([#2773](https://github.com/nonebot/nonebot2/pull/2773)) - Plugin: 多模态AI工具 [@noneflow](https://github.com/noneflow) ([#2758](https://github.com/nonebot/nonebot2/pull/2758)) - Plugin: nonebot-plugin-easymarkdown [@noneflow](https://github.com/noneflow) ([#2767](https://github.com/nonebot/nonebot2/pull/2767)) - Plugin: 峯驰外包 [@noneflow](https://github.com/noneflow) ([#2765](https://github.com/nonebot/nonebot2/pull/2765)) - Plugin: 鸣潮抽卡记录分析 [@noneflow](https://github.com/noneflow) ([#2763](https://github.com/nonebot/nonebot2/pull/2763)) - Plugin: nonebot-plugin-xjie-weather [@noneflow](https://github.com/noneflow) ([#2756](https://github.com/nonebot/nonebot2/pull/2756)) - Plugin: 颜值评分 [@noneflow](https://github.com/noneflow) ([#2752](https://github.com/nonebot/nonebot2/pull/2752)) - Plugin: 学园偶像大师算分插件 [@noneflow](https://github.com/noneflow) ([#2750](https://github.com/nonebot/nonebot2/pull/2750)) - Plugin: nonebot-plugin-lynchpined [@noneflow](https://github.com/noneflow) ([#2748](https://github.com/nonebot/nonebot2/pull/2748)) - Plugin: QQShell [@noneflow](https://github.com/noneflow) ([#2745](https://github.com/nonebot/nonebot2/pull/2745)) - Plugin: ai唱歌 [@noneflow](https://github.com/noneflow) ([#2743](https://github.com/nonebot/nonebot2/pull/2743)) - Plugin: 复读姬+1 PlusOne [@noneflow](https://github.com/noneflow) ([#2732](https://github.com/nonebot/nonebot2/pull/2732)) - Plugin: 高优先级关闭信号钩子插件 [@noneflow](https://github.com/noneflow) ([#2737](https://github.com/nonebot/nonebot2/pull/2737)) - Plugin: 插件响应鉴权 [@noneflow](https://github.com/noneflow) ([#2727](https://github.com/nonebot/nonebot2/pull/2727)) - Plugin: DG-Lab-Play [@noneflow](https://github.com/noneflow) ([#2729](https://github.com/nonebot/nonebot2/pull/2729)) ## v2.3.1 ### 🐛 Bug 修复 - Fix: State ForwardRef 检测错误 [@yanyongyu](https://github.com/yanyongyu) ([#2698](https://github.com/nonebot/nonebot2/pull/2698)) ### 📝 文档 - Docs: 修正 匹配扩展 中的示例 [@KomoriDev](https://github.com/KomoriDev) ([#2722](https://github.com/nonebot/nonebot2/pull/2722)) - Docs: 更新 Mirai 适配器说明 [@RF-Tar-Railt](https://github.com/RF-Tar-Railt) ([#2715](https://github.com/nonebot/nonebot2/pull/2715)) - Docs: 添加 Tailchat 适配器说明 [@eya46](https://github.com/eya46) ([#2694](https://github.com/nonebot/nonebot2/pull/2694)) - Docs: 添加 uwu logo [@StarHeartHunt](https://github.com/StarHeartHunt) ([#2689](https://github.com/nonebot/nonebot2/pull/2689)) ### 💫 杂项 - Plugin: 移除已在 PyPI 上删除的 `covid` 插件和 `molar-mass` 插件 [@NCBM](https://github.com/NCBM) ([#2712](https://github.com/nonebot/nonebot2/pull/2712)) ### 🍻 插件发布 - Plugin: 自定义人格和AI绘图的混合聊天BOT [@noneflow](https://github.com/noneflow) ([#2724](https://github.com/nonebot/nonebot2/pull/2724)) - Plugin: nonebot-plugin-calc24 [@noneflow](https://github.com/noneflow) ([#2721](https://github.com/nonebot/nonebot2/pull/2721)) - Plugin: nonebot-plugin-tsugu-bangdream-bot [@noneflow](https://github.com/noneflow) ([#2719](https://github.com/nonebot/nonebot2/pull/2719)) - Plugin: 科大讯飞星火大语言模型官方API聊天机器人插件 [@noneflow](https://github.com/noneflow) ([#2717](https://github.com/nonebot/nonebot2/pull/2717)) - Plugin: nonebot_plugin_valve_server_query [@noneflow](https://github.com/noneflow) ([#2711](https://github.com/nonebot/nonebot2/pull/2711)) - Plugin: 库洛游戏信息 [@noneflow](https://github.com/noneflow) ([#2706](https://github.com/nonebot/nonebot2/pull/2706)) - Plugin: BanG Dream! Tsugu Frontend [@noneflow](https://github.com/noneflow) ([#2708](https://github.com/nonebot/nonebot2/pull/2708)) - Plugin: 神秘学助手 [@noneflow](https://github.com/noneflow) ([#2700](https://github.com/nonebot/nonebot2/pull/2700)) - Plugin: nonebot-plugin-furryfusion [@noneflow](https://github.com/noneflow) ([#2705](https://github.com/nonebot/nonebot2/pull/2705)) - Plugin: nonebot-plugin-RanFurryPic [@noneflow](https://github.com/noneflow) ([#2703](https://github.com/nonebot/nonebot2/pull/2703)) - Plugin: with_ai_agents [@noneflow](https://github.com/noneflow) ([#2697](https://github.com/nonebot/nonebot2/pull/2697)) - Plugin: 番剧下载 [@noneflow](https://github.com/noneflow) ([#2691](https://github.com/nonebot/nonebot2/pull/2691)) ### 🍻 适配器发布 - Adapter: Mirai [@noneflow](https://github.com/noneflow) ([#2714](https://github.com/nonebot/nonebot2/pull/2714)) - Adapter: Tailchat [@noneflow](https://github.com/noneflow) ([#2693](https://github.com/nonebot/nonebot2/pull/2693)) ## v2.3.0 ### 💥 破坏性变更 - Feature: 嵌套插件名称作用域优化 [@yanyongyu](https://github.com/yanyongyu) ([#2665](https://github.com/nonebot/nonebot2/pull/2665)) - Remove: 移除 Python 3.8 支持 [@yanyongyu](https://github.com/yanyongyu) ([#2641](https://github.com/nonebot/nonebot2/pull/2641)) ### 🚀 新功能 - Feature: 嵌套插件名称作用域优化 [@yanyongyu](https://github.com/yanyongyu) ([#2665](https://github.com/nonebot/nonebot2/pull/2665)) - Feature: 优化调用栈识别 [@yanyongyu](https://github.com/yanyongyu) ([#2644](https://github.com/nonebot/nonebot2/pull/2644)) - Feature: 支持 HTTP 客户端会话 [@yanyongyu](https://github.com/yanyongyu) ([#2627](https://github.com/nonebot/nonebot2/pull/2627)) - Develop: 添加 ruff RUF 规则 [@he0119](https://github.com/he0119) ([#2598](https://github.com/nonebot/nonebot2/pull/2598)) ### 🐛 Bug 修复 - Fix: none 系列驱动器启动失败时未退出应用 [@yanyongyu](https://github.com/yanyongyu) ([#2687](https://github.com/nonebot/nonebot2/pull/2687)) - Bug: inherit_supported_adapters 在展开缩写前取交集 [@AzideCupric](https://github.com/AzideCupric) ([#2654](https://github.com/nonebot/nonebot2/pull/2654)) - Bug: 添加 HTTP 客户端会话上下文检查 [@yanyongyu](https://github.com/yanyongyu) ([#2632](https://github.com/nonebot/nonebot2/pull/2632)) - Fix: 将 aiohttp 的 quote_fields 默认设为 False [@j1g5awi](https://github.com/j1g5awi) ([#2619](https://github.com/nonebot/nonebot2/pull/2619)) ### 📝 文档 - Docs: 数据库最佳实践 [@ProgramRipper](https://github.com/ProgramRipper) ([#2545](https://github.com/nonebot/nonebot2/pull/2545)) - Docs: 更新最佳实践的 Alconna 部分 [@RF-Tar-Railt](https://github.com/RF-Tar-Railt) ([#2686](https://github.com/nonebot/nonebot2/pull/2686)) - Docs: 添加 OSPP 2024 项目说明 [@yanyongyu](https://github.com/yanyongyu) ([#2676](https://github.com/nonebot/nonebot2/pull/2676)) - Docs: 更新 Villa 适配器说明 [@CMHopeSunshine](https://github.com/CMHopeSunshine) ([#2661](https://github.com/nonebot/nonebot2/pull/2661)) - Docs: 添加 Kritor 适配器说明 [@RF-Tar-Railt](https://github.com/RF-Tar-Railt) ([#2660](https://github.com/nonebot/nonebot2/pull/2660)) - Docs: 更新最佳实践的 Alconna 部分 [@RF-Tar-Railt](https://github.com/RF-Tar-Railt) ([#2656](https://github.com/nonebot/nonebot2/pull/2656)) - Docs: 添加 RocketChat 适配器说明 [@yanyongyu](https://github.com/yanyongyu) ([#2640](https://github.com/nonebot/nonebot2/pull/2640)) - Docs: 商店卡片样式调整 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#2633](https://github.com/nonebot/nonebot2/pull/2633)) - Docs: 为商店插件卡片添加更多展示内容 [@AzideCupric](https://github.com/AzideCupric) ([#2626](https://github.com/nonebot/nonebot2/pull/2626)) - Docs: 修复 `RegexMatched` 文档类型标注错误 [@A-kirami](https://github.com/A-kirami) ([#2629](https://github.com/nonebot/nonebot2/pull/2629)) - Docs: 修复 `RegexMatched​` 文档高亮行错误 [@A-kirami](https://github.com/A-kirami) ([#2628](https://github.com/nonebot/nonebot2/pull/2628)) - Docs: 为商店的详情卡片添加跳转链接 [@AzideCupric](https://github.com/AzideCupric) ([#2623](https://github.com/nonebot/nonebot2/pull/2623)) - Docs: 添加 `RegexMatched` 依赖注入文档 [@A-kirami](https://github.com/A-kirami) ([#2618](https://github.com/nonebot/nonebot2/pull/2618)) - Docs: 添加百度搜索资源验证 [@yanyongyu](https://github.com/yanyongyu) ([#2590](https://github.com/nonebot/nonebot2/pull/2590)) ### 💫 杂项 - CI: 修复 NoneFlow reaction 范围 [@yanyongyu](https://github.com/yanyongyu) ([#2685](https://github.com/nonebot/nonebot2/pull/2685)) - CI: 修复测试 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#2682](https://github.com/nonebot/nonebot2/pull/2682)) - CI: NoneFlow 添加 reaction 响应提示 [@yanyongyu](https://github.com/yanyongyu) ([#2677](https://github.com/nonebot/nonebot2/pull/2677)) - Plugin: 移除不维护的插件 `eitherchoice` [@lgc2333](https://github.com/lgc2333) ([#2599](https://github.com/nonebot/nonebot2/pull/2599)) ### 🍻 插件发布 - Plugin: 表情包保存器 [@noneflow](https://github.com/noneflow) ([#2684](https://github.com/nonebot/nonebot2/pull/2684)) - Plugin: HelpWithPic [@noneflow](https://github.com/noneflow) ([#2681](https://github.com/nonebot/nonebot2/pull/2681)) - Plugin: cyberfurry [@noneflow](https://github.com/noneflow) ([#2679](https://github.com/nonebot/nonebot2/pull/2679)) - Plugin: 三爻易数 [@noneflow](https://github.com/noneflow) ([#2675](https://github.com/nonebot/nonebot2/pull/2675)) - Plugin: 战双表情 [@noneflow](https://github.com/noneflow) ([#2669](https://github.com/nonebot/nonebot2/pull/2669)) - Plugin: QQ频道-Discord 互通 [@noneflow](https://github.com/noneflow) ([#2667](https://github.com/nonebot/nonebot2/pull/2667)) - Plugin: Yinying-Chat [@noneflow](https://github.com/noneflow) ([#2662](https://github.com/nonebot/nonebot2/pull/2662)) - Plugin: 淫语 [@noneflow](https://github.com/noneflow) ([#2650](https://github.com/nonebot/nonebot2/pull/2650)) - Plugin: 飞花令 [@noneflow](https://github.com/noneflow) ([#2648](https://github.com/nonebot/nonebot2/pull/2648)) - Plugin: Hx_YinYing [@noneflow](https://github.com/noneflow) ([#2646](https://github.com/nonebot/nonebot2/pull/2646)) - Plugin: clovers插件框架 [@noneflow](https://github.com/noneflow) ([#2643](https://github.com/nonebot/nonebot2/pull/2643)) - Plugin: nonebot-plugin-nai3 [@noneflow](https://github.com/noneflow) ([#2639](https://github.com/nonebot/nonebot2/pull/2639)) - Plugin: nonebot-plugin-auto-bot-selector [@noneflow](https://github.com/noneflow) ([#2635](https://github.com/nonebot/nonebot2/pull/2635)) - Plugin: Chikari_economy [@noneflow](https://github.com/noneflow) ([#2631](https://github.com/nonebot/nonebot2/pull/2631)) - Plugin: diffsinger [@noneflow](https://github.com/noneflow) ([#2625](https://github.com/nonebot/nonebot2/pull/2625)) - Plugin: ghtiles [@noneflow](https://github.com/noneflow) ([#2622](https://github.com/nonebot/nonebot2/pull/2622)) - Plugin: 人类友好数据配置 [@noneflow](https://github.com/noneflow) ([#2616](https://github.com/nonebot/nonebot2/pull/2616)) - Plugin: nonebot-plugin-pallas-repeater [@noneflow](https://github.com/noneflow) ([#2614](https://github.com/nonebot/nonebot2/pull/2614)) - Plugin: nonebot-plugin-duel [@noneflow](https://github.com/noneflow) ([#2612](https://github.com/nonebot/nonebot2/pull/2612)) - Plugin: Sekai Stickers [@noneflow](https://github.com/noneflow) ([#2610](https://github.com/nonebot/nonebot2/pull/2610)) - Plugin: 100orangejuice [@noneflow](https://github.com/noneflow) ([#2601](https://github.com/nonebot/nonebot2/pull/2601)) - Plugin: Steam Info [@noneflow](https://github.com/noneflow) ([#2608](https://github.com/nonebot/nonebot2/pull/2608)) - Plugin: nonebot-plugin-dice-narrator [@noneflow](https://github.com/noneflow) ([#2606](https://github.com/nonebot/nonebot2/pull/2606)) - Plugin: a2s查询 [@noneflow](https://github.com/noneflow) ([#2603](https://github.com/nonebot/nonebot2/pull/2603)) - Plugin: 赛博钓鱼 [@noneflow](https://github.com/noneflow) ([#2596](https://github.com/nonebot/nonebot2/pull/2596)) - Plugin: 人性化的ChatGLM [@noneflow](https://github.com/noneflow) ([#2592](https://github.com/nonebot/nonebot2/pull/2592)) - Plugin: nonebot-plugin-vits-tts [@noneflow](https://github.com/noneflow) ([#2595](https://github.com/nonebot/nonebot2/pull/2595)) ### 🍻 适配器发布 - Adapter: Kritor [@noneflow](https://github.com/noneflow) ([#2659](https://github.com/nonebot/nonebot2/pull/2659)) - Adapter: RocketChat [@noneflow](https://github.com/noneflow) ([#2637](https://github.com/nonebot/nonebot2/pull/2637)) ## v2.2.1 ### 🚀 新功能 - Feature: 优化 pydantic 兼容函数 `model_dump` 和 `type_validate_json` [@MingxuanGame](https://github.com/MingxuanGame) ([#2579](https://github.com/nonebot/nonebot2/pull/2579)) ### 🐛 Bug 修复 - Fix: 修改遗漏的过时 Pydantic 方法 [@yanyongyu](https://github.com/yanyongyu) ([#2577](https://github.com/nonebot/nonebot2/pull/2577)) - Fix: `Message.__contains__()` 未考虑 `bool(MessageSegment)` 存在 False 情况导致的异常结果 [@lgc2333](https://github.com/lgc2333) ([#2572](https://github.com/nonebot/nonebot2/pull/2572)) ### 📝 文档 - Docs: 更新 Session Expire Timeout​ 文档 [@MingxuanGame](https://github.com/MingxuanGame) ([#2585](https://github.com/nonebot/nonebot2/pull/2585)) - Docs: 添加适配器测试注意事项 [@yanyongyu](https://github.com/yanyongyu) ([#2570](https://github.com/nonebot/nonebot2/pull/2570)) ### 💫 杂项 - Plugin: 修改 phigros 相关内容 [@XTxiaoting14332](https://github.com/XTxiaoting14332) ([#2578](https://github.com/nonebot/nonebot2/pull/2578)) ### 🍻 插件发布 - Plugin: 运行状态 [@noneflow](https://github.com/noneflow) ([#2587](https://github.com/nonebot/nonebot2/pull/2587)) - Plugin: nonebot-plugin-bf1marneserverlist [@noneflow](https://github.com/noneflow) ([#2584](https://github.com/nonebot/nonebot2/pull/2584)) - Plugin: splatoon3游戏nso查询 [@noneflow](https://github.com/noneflow) ([#2576](https://github.com/nonebot/nonebot2/pull/2576)) - Plugin: Chikari_yinpa [@noneflow](https://github.com/noneflow) ([#2573](https://github.com/nonebot/nonebot2/pull/2573)) ## v2.2.0 ### 🚀 新功能 - Feature: 添加插件 Pydantic 相关使用方法 [@yanyongyu](https://github.com/yanyongyu) ([#2563](https://github.com/nonebot/nonebot2/pull/2563)) - Feature: 兼容 Pydantic v2 [@yanyongyu](https://github.com/yanyongyu) ([#2544](https://github.com/nonebot/nonebot2/pull/2544)) - Feature: 使用自定义配置加载替代 `pydantic-settings` [@yanyongyu](https://github.com/yanyongyu) ([#2521](https://github.com/nonebot/nonebot2/pull/2521)) - Feature: 带参数的 `RegexStr()` [@ProgramRipper](https://github.com/ProgramRipper) ([#2499](https://github.com/nonebot/nonebot2/pull/2499)) ### 🐛 Bug 修复 - Fix: websockets 驱动器连接关闭 code 获取错误 [@yanyongyu](https://github.com/yanyongyu) ([#2537](https://github.com/nonebot/nonebot2/pull/2537)) - Fix: 修复 `echo` 发送空消息 [@yanyongyu](https://github.com/yanyongyu) ([#2525](https://github.com/nonebot/nonebot2/pull/2525)) - Fix: `MessageTemplate` 禁止访问私有属性 [@mnixry](https://github.com/mnixry) ([#2509](https://github.com/nonebot/nonebot2/pull/2509)) ### 📝 文档 - Docs: 更新 Alconna 文档 [@lengmianzz](https://github.com/lengmianzz) ([#2568](https://github.com/nonebot/nonebot2/pull/2568)) - Docs: 添加产品赞助列表 [@yanyongyu](https://github.com/yanyongyu) ([#2566](https://github.com/nonebot/nonebot2/pull/2566)) - Docs: 修复表单标签状态更新 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#2558](https://github.com/nonebot/nonebot2/pull/2558)) - Docs: 添加 CITATION 文件 [@yanyongyu](https://github.com/yanyongyu) ([#2520](https://github.com/nonebot/nonebot2/pull/2520)) ### 💫 杂项 - Plugin: 移除不再维护的几款插件 [@mnixry](https://github.com/mnixry) ([#2561](https://github.com/nonebot/nonebot2/pull/2561)) - CI: 更新 prettier 配置 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#2546](https://github.com/nonebot/nonebot2/pull/2546)) - Plugin: 恢复删除的插件 `nonebot-plugin-eitherchoice` [@lgc2333](https://github.com/lgc2333) ([#2502](https://github.com/nonebot/nonebot2/pull/2502)) ### 🍻 插件发布 - Plugin: 定时提醒 [@noneflow](https://github.com/noneflow) ([#2559](https://github.com/nonebot/nonebot2/pull/2559)) - Plugin: 黑名单插件 [@noneflow](https://github.com/noneflow) ([#2554](https://github.com/nonebot/nonebot2/pull/2554)) - Plugin: ChatGPT 聊天 [@noneflow](https://github.com/noneflow) ([#2556](https://github.com/nonebot/nonebot2/pull/2556)) - Plugin: BA模拟抽卡 [@noneflow](https://github.com/noneflow) ([#2550](https://github.com/nonebot/nonebot2/pull/2550)) - Plugin: 随机发送图片 [@noneflow](https://github.com/noneflow) ([#2548](https://github.com/nonebot/nonebot2/pull/2548)) - Plugin: 哪吒监控插件 [@noneflow](https://github.com/noneflow) ([#2552](https://github.com/nonebot/nonebot2/pull/2552)) - Plugin: SakuraFrp [@noneflow](https://github.com/noneflow) ([#2543](https://github.com/nonebot/nonebot2/pull/2543)) - Plugin: haruka_bot_red [@noneflow](https://github.com/noneflow) ([#2541](https://github.com/nonebot/nonebot2/pull/2541)) - Plugin: nonebot-plugin-gemini [@noneflow](https://github.com/noneflow) ([#2527](https://github.com/nonebot/nonebot2/pull/2527)) - Plugin: 最终台词 [@noneflow](https://github.com/noneflow) ([#2523](https://github.com/nonebot/nonebot2/pull/2523)) - Plugin: nonebot-plugin-nekoimage [@noneflow](https://github.com/noneflow) ([#2534](https://github.com/nonebot/nonebot2/pull/2534)) - Plugin: 谷歌Bard聊天 [@noneflow](https://github.com/noneflow) ([#2529](https://github.com/nonebot/nonebot2/pull/2529)) - Plugin: nonebot-plugin-mypower [@noneflow](https://github.com/noneflow) ([#2533](https://github.com/nonebot/nonebot2/pull/2533)) - Plugin: 文心一言4适配 [@noneflow](https://github.com/noneflow) ([#2516](https://github.com/nonebot/nonebot2/pull/2516)) - Plugin: 最佳平替 [@noneflow](https://github.com/noneflow) ([#2519](https://github.com/nonebot/nonebot2/pull/2519)) - Plugin: 随机MC图 [@noneflow](https://github.com/noneflow) ([#2512](https://github.com/nonebot/nonebot2/pull/2512)) - Plugin: nonebot_plugin_nikke [@noneflow](https://github.com/noneflow) ([#2508](https://github.com/nonebot/nonebot2/pull/2508)) - Plugin: nonebot-plugin-imagemaster [@noneflow](https://github.com/noneflow) ([#2504](https://github.com/nonebot/nonebot2/pull/2504)) - Plugin: Waiter 插件 [@noneflow](https://github.com/noneflow) ([#2506](https://github.com/nonebot/nonebot2/pull/2506)) - Plugin: AntiMonkey [@noneflow](https://github.com/noneflow) ([#2501](https://github.com/nonebot/nonebot2/pull/2501)) ## v2.1.3 ### 🐛 Bug 修复 - Fix: 新增 `Lifespan.on_ready()` 供适配器使用 [@ProgramRipper](https://github.com/ProgramRipper) ([#2483](https://github.com/nonebot/nonebot2/pull/2483)) - Fix: 忽略 Pyright 对动态类创建的检查错误 [@yanyongyu](https://github.com/yanyongyu) ([#2486](https://github.com/nonebot/nonebot2/pull/2486)) ### 📝 文档 - Docs: 商店详情卡片添加宽度限制与文本省略 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#2473](https://github.com/nonebot/nonebot2/pull/2473)) - Docs: 修复商店发布 上一步 按钮显示问题 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#2464](https://github.com/nonebot/nonebot2/pull/2464)) - Docs: 添加商店表单支持 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#2460](https://github.com/nonebot/nonebot2/pull/2460)) - Docs: 修复事件后处理函数类型 docstring 错误 [@lgc2333](https://github.com/lgc2333) ([#2459](https://github.com/nonebot/nonebot2/pull/2459)) - Docs: 修改 QQ 频道为 QQ [@bingqiu456](https://github.com/bingqiu456) ([#2457](https://github.com/nonebot/nonebot2/pull/2457)) - Docs: 更新最佳实践的 Alconna 部分 [@RF-Tar-Railt](https://github.com/RF-Tar-Railt) ([#2443](https://github.com/nonebot/nonebot2/pull/2443)) ### 💫 杂项 - Plugin: 更新 splatoon3 插件地址 [@Cypas](https://github.com/Cypas) ([#2494](https://github.com/nonebot/nonebot2/pull/2494)) - Plugin: 删除不维护的 `eitherchoice` 插件 [@lgc2333](https://github.com/lgc2333) ([#2491](https://github.com/nonebot/nonebot2/pull/2491)) - Plugin: 移除不再维护的插件 [@j1g5awi](https://github.com/j1g5awi) ([#2474](https://github.com/nonebot/nonebot2/pull/2474)) - Plugin: 移除不再维护的插件 [@NCBM](https://github.com/NCBM) ([#2472](https://github.com/nonebot/nonebot2/pull/2472)) - Plugin: 移除不再维护的插件 [@MeetWq](https://github.com/MeetWq) ([#2471](https://github.com/nonebot/nonebot2/pull/2471)) - CI: 测试矩阵添加 Python 3.12 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#2441](https://github.com/nonebot/nonebot2/pull/2441)) ### 🍻 插件发布 - Plugin: Phigros查分器(Adapter-qq) [@noneflow](https://github.com/noneflow) ([#2497](https://github.com/nonebot/nonebot2/pull/2497)) - Plugin: Riffusion [@noneflow](https://github.com/noneflow) ([#2493](https://github.com/nonebot/nonebot2/pull/2493)) - Plugin: nonebot_plugin_longtu [@noneflow](https://github.com/noneflow) ([#2490](https://github.com/nonebot/nonebot2/pull/2490)) - Plugin: CNRail [@noneflow](https://github.com/noneflow) ([#2488](https://github.com/nonebot/nonebot2/pull/2488)) - Plugin: ba塔罗牌,运势与魔法占卜! [@noneflow](https://github.com/noneflow) ([#2481](https://github.com/nonebot/nonebot2/pull/2481)) - Plugin: 群聊 NSFW 图片检测 [@noneflow](https://github.com/noneflow) ([#2477](https://github.com/nonebot/nonebot2/pull/2477)) - Plugin: sm.ms图床 [@noneflow](https://github.com/noneflow) ([#2470](https://github.com/nonebot/nonebot2/pull/2470)) - Plugin: 文件托管支持 [@noneflow](https://github.com/noneflow) ([#2468](https://github.com/nonebot/nonebot2/pull/2468)) - Plugin: 短链接服务支持 [@noneflow](https://github.com/noneflow) ([#2466](https://github.com/nonebot/nonebot2/pull/2466)) - Plugin: 用户 [@noneflow](https://github.com/noneflow) ([#2463](https://github.com/nonebot/nonebot2/pull/2463)) - Plugin: DALL-E 3绘图 [@noneflow](https://github.com/noneflow) ([#2452](https://github.com/nonebot/nonebot2/pull/2452)) - Plugin: 局域网唤醒 [@noneflow](https://github.com/noneflow) ([#2449](https://github.com/nonebot/nonebot2/pull/2449)) - Plugin: nonebot-plugin-bertvits2 [@noneflow](https://github.com/noneflow) ([#2446](https://github.com/nonebot/nonebot2/pull/2446)) - Plugin: Nonebot2 Any 多平台服务 [@noneflow](https://github.com/noneflow) ([#2442](https://github.com/nonebot/nonebot2/pull/2442)) ### 🍻 机器人发布 - Bot: Sakiko [@noneflow](https://github.com/noneflow) ([#2439](https://github.com/nonebot/nonebot2/pull/2439)) ### 🍻 适配器发布 - Adapter: DoDo [@noneflow](https://github.com/noneflow) ([#2456](https://github.com/nonebot/nonebot2/pull/2456)) ## v2.1.2 ### 🚀 新功能 - Feature: 添加多消息段命令解析支持 [@RainEggplant](https://github.com/RainEggplant) ([#2419](https://github.com/nonebot/nonebot2/pull/2419)) ### 🐛 Bug 修复 - Fix: 修复依赖注入对 Literal 检查报错 [@yanyongyu](https://github.com/yanyongyu) ([#2433](https://github.com/nonebot/nonebot2/pull/2433)) ### 📝 文档 - Docs: 修复 Alconna 文档 typo [@StarHeartHunt](https://github.com/StarHeartHunt) ([#2429](https://github.com/nonebot/nonebot2/pull/2429)) - Docs: 文档启用百度统计 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#2424](https://github.com/nonebot/nonebot2/pull/2424)) - Docs: 更新最佳实践 Alconna [@RF-Tar-Railt](https://github.com/RF-Tar-Railt) ([#2401](https://github.com/nonebot/nonebot2/pull/2401)) - Docs: 修改商店发布的跳转链接 [@KomoriDev](https://github.com/KomoriDev) ([#2387](https://github.com/nonebot/nonebot2/pull/2387)) - Docs: 修复文档主页 Features 不居中 [@MingxuanGame](https://github.com/MingxuanGame) ([#2390](https://github.com/nonebot/nonebot2/pull/2390)) ### 💫 杂项 - Fix: 修复升级 pytest-asyncio 0.22 pytest collect 问题 [@yanyongyu](https://github.com/yanyongyu) ([#2436](https://github.com/nonebot/nonebot2/pull/2436)) - Plugin: 移除 `nonebot-plugin-nya-music` 插件 [@nikissXI](https://github.com/nikissXI) ([#2398](https://github.com/nonebot/nonebot2/pull/2398)) - CI: 调整商店数据存放位置与内容 [@he0119](https://github.com/he0119) ([#2385](https://github.com/nonebot/nonebot2/pull/2385)) - Adapter: 修改频道适配器为 QQ 适配器 [@yanyongyu](https://github.com/yanyongyu) ([#2382](https://github.com/nonebot/nonebot2/pull/2382)) ### 🍻 插件发布 - Plugin: 定时广播插件 [@noneflow](https://github.com/noneflow) ([#2432](https://github.com/nonebot/nonebot2/pull/2432)) - Plugin: 选择困难症 [@noneflow](https://github.com/noneflow) ([#2428](https://github.com/nonebot/nonebot2/pull/2428)) - Plugin: nonebot-plugin-getbapics [@noneflow](https://github.com/noneflow) ([#2423](https://github.com/nonebot/nonebot2/pull/2423)) - Plugin: nonebot-plugin-maimaidx [@noneflow](https://github.com/noneflow) ([#2422](https://github.com/nonebot/nonebot2/pull/2422)) - Plugin: BlueArchive Title Generator [@noneflow](https://github.com/noneflow) ([#2418](https://github.com/nonebot/nonebot2/pull/2418)) - Plugin: VRChat查询 [@noneflow](https://github.com/noneflow) ([#2411](https://github.com/nonebot/nonebot2/pull/2411)) - Plugin: FGO猜从者 [@noneflow](https://github.com/noneflow) ([#2416](https://github.com/nonebot/nonebot2/pull/2416)) - Plugin: 肯定机 [@noneflow](https://github.com/noneflow) ([#2409](https://github.com/nonebot/nonebot2/pull/2409)) - Plugin: morep-finder [@noneflow](https://github.com/noneflow) ([#2407](https://github.com/nonebot/nonebot2/pull/2407)) - Plugin: op-finder [@noneflow](https://github.com/noneflow) ([#2403](https://github.com/nonebot/nonebot2/pull/2403)) - Plugin: nonebot-plugin-playercheck [@noneflow](https://github.com/noneflow) ([#2400](https://github.com/nonebot/nonebot2/pull/2400)) - Plugin: talk with eop ai [@noneflow](https://github.com/noneflow) ([#2397](https://github.com/nonebot/nonebot2/pull/2397)) - Plugin: 算法比赛查询和今日比赛自动提醒 [@noneflow](https://github.com/noneflow) ([#2395](https://github.com/nonebot/nonebot2/pull/2395)) - Plugin: 屏蔽词插件 [@noneflow](https://github.com/noneflow) ([#2392](https://github.com/nonebot/nonebot2/pull/2392)) - Plugin: Nonebot Agent [@noneflow](https://github.com/noneflow) ([#2389](https://github.com/nonebot/nonebot2/pull/2389)) - Plugin: 聚能环 [@noneflow](https://github.com/noneflow) ([#2384](https://github.com/nonebot/nonebot2/pull/2384)) ### 🍻 机器人发布 - Bot: 芙芙 [@noneflow](https://github.com/noneflow) ([#2426](https://github.com/nonebot/nonebot2/pull/2426)) - Bot: 妃爱 [@noneflow](https://github.com/noneflow) ([#2413](https://github.com/nonebot/nonebot2/pull/2413)) ### 🍻 适配器发布 - Adapter: Satori [@noneflow](https://github.com/noneflow) ([#2405](https://github.com/nonebot/nonebot2/pull/2405)) ## v2.1.1 ### 🚀 新功能 - Feature: 优先使用 `Annotated` 的最后一个子依赖 [@ProgramRipper](https://github.com/ProgramRipper) ([#2360](https://github.com/nonebot/nonebot2/pull/2360)) - Feature: 优化检查事件响应器的日志 [@A-kirami](https://github.com/A-kirami) ([#2355](https://github.com/nonebot/nonebot2/pull/2355)) ### 🐛 Bug 修复 - Fix: bot.call_api 在被 called api hook mock 后应该忽略 exception [@Ailitonia](https://github.com/Ailitonia) ([#2374](https://github.com/nonebot/nonebot2/pull/2374)) ### 📝 文档 - Docs: 修复商店搜索信息的错字 [@KomoriDev](https://github.com/KomoriDev) ([#2377](https://github.com/nonebot/nonebot2/pull/2377)) - Docs: 修复侧边栏 TOC 在 SSR 模式下的渲染问题 [@yanyongyu](https://github.com/yanyongyu) ([#2376](https://github.com/nonebot/nonebot2/pull/2376)) - Docs: 升级新版 NonePress 主题 [@yanyongyu](https://github.com/yanyongyu) ([#2375](https://github.com/nonebot/nonebot2/pull/2375)) - Docs: 增加赞助者显示 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#2371](https://github.com/nonebot/nonebot2/pull/2371)) - Docs: 更新 `get_asgi` 函数的文档字符串 [@A-kirami](https://github.com/A-kirami) ([#2359](https://github.com/nonebot/nonebot2/pull/2359)) ### 💫 杂项 - Develop: 禁用 Pyright Bytes Promotion 配置 [@yanyongyu](https://github.com/yanyongyu) ([#2379](https://github.com/nonebot/nonebot2/pull/2379)) - Plugin: 修改 `Sekai Stickers` 插件信息 [@lgc2333](https://github.com/lgc2333) ([#2372](https://github.com/nonebot/nonebot2/pull/2372)) - CI: 使用更现代的功能 [@he0119](https://github.com/he0119) ([#2362](https://github.com/nonebot/nonebot2/pull/2362)) - Docs: 添加 wwads [@yanyongyu](https://github.com/yanyongyu) ([#2361](https://github.com/nonebot/nonebot2/pull/2361)) ### 🍻 插件发布 - Plugin: 大电老师活字印刷 [@noneflow](https://github.com/noneflow) ([#2370](https://github.com/nonebot/nonebot2/pull/2370)) - Plugin: nonebot-plugin-video-api [@noneflow](https://github.com/noneflow) ([#2367](https://github.com/nonebot/nonebot2/pull/2367)) - Plugin: 青年大学习提交 [@noneflow](https://github.com/noneflow) ([#2357](https://github.com/nonebot/nonebot2/pull/2357)) ## v2.1.0 ### 🚀 新功能 - Feature: 为 Matcher.HANDLER_PARAM_TYPES 补增类型 [@RF-Tar-Railt](https://github.com/RF-Tar-Railt) ([#2352](https://github.com/nonebot/nonebot2/pull/2352)) - Feature: 为事件响应器添加更多源码信息 [@yanyongyu](https://github.com/yanyongyu) ([#2351](https://github.com/nonebot/nonebot2/pull/2351)) - Feature: 补充依赖注入部分情况下类型错误时的日志提示 [@A-kirami](https://github.com/A-kirami) ([#2343](https://github.com/nonebot/nonebot2/pull/2343)) - Feature: 支持子依赖定义 Pydantic 类型校验 [@yanyongyu](https://github.com/yanyongyu) ([#2310](https://github.com/nonebot/nonebot2/pull/2310)) - Feature: 细化 driver 职责类型 [@yanyongyu](https://github.com/yanyongyu) ([#2296](https://github.com/nonebot/nonebot2/pull/2296)) ### 🐛 Bug 修复 - Fix: 修复依赖注入解析类型标注错误 [@yanyongyu](https://github.com/yanyongyu) ([#2338](https://github.com/nonebot/nonebot2/pull/2338)) - Fix: 设置 file request 默认 filename [@eya46](https://github.com/eya46) ([#2284](https://github.com/nonebot/nonebot2/pull/2284)) ### 📝 文档 - Docs: 更新最佳实践部分的 Alconna 章节 [@RF-Tar-Railt](https://github.com/RF-Tar-Railt) ([#2349](https://github.com/nonebot/nonebot2/pull/2349)) - Docs: 添加 Discord 适配器描述,补充 Villa 适配器协议链接 [@CMHopeSunshine](https://github.com/CMHopeSunshine) ([#2316](https://github.com/nonebot/nonebot2/pull/2316)) - Docs: 添加 Red 适配器描述 [@RF-Tar-Railt](https://github.com/RF-Tar-Railt) ([#2313](https://github.com/nonebot/nonebot2/pull/2313)) - Docs: 更新最佳实践部分的 Alconna 章节 [@RF-Tar-Railt](https://github.com/RF-Tar-Railt) ([#2303](https://github.com/nonebot/nonebot2/pull/2303)) - Docs: 修复 Alconna 中 `CommandResult` 描述错误 [@KomoriDev](https://github.com/KomoriDev) ([#2282](https://github.com/nonebot/nonebot2/pull/2282)) - Docs: 修复子依赖部分代码行号错误 [@A-kirami](https://github.com/A-kirami) ([#2279](https://github.com/nonebot/nonebot2/pull/2279)) - Docs: 补充 `get_last_receive` 示例 [@A-kirami](https://github.com/A-kirami) ([#2278](https://github.com/nonebot/nonebot2/pull/2278)) - Docs: 修复文档中错误的标点 [@A-kirami](https://github.com/A-kirami) ([#2275](https://github.com/nonebot/nonebot2/pull/2275)) - Docs: 修复配置文档中 `Nickname` 属性的描述错误 [@A-kirami](https://github.com/A-kirami) ([#2271](https://github.com/nonebot/nonebot2/pull/2271)) - Docs: 适配器编写教程 [@CMHopeSunshine](https://github.com/CMHopeSunshine) ([#2079](https://github.com/nonebot/nonebot2/pull/2079)) - Docs: 更新贡献指南 [@A-kirami](https://github.com/A-kirami) ([#2255](https://github.com/nonebot/nonebot2/pull/2255)) - Docs: 修复文档 Last updated author 错误 [@eya46](https://github.com/eya46) ([#2241](https://github.com/nonebot/nonebot2/pull/2241)) - Docs: 更新最佳实践部分的 Alconna 章节 [@RF-Tar-Railt](https://github.com/RF-Tar-Railt) ([#2237](https://github.com/nonebot/nonebot2/pull/2237)) ### 💫 杂项 - Plugin: 删除插件 nonebot-plugin-heisi [@yzyyz1387](https://github.com/yzyyz1387) ([#2353](https://github.com/nonebot/nonebot2/pull/2353)) - CI: 更新到 node 18 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#2344](https://github.com/nonebot/nonebot2/pull/2344)) - CI: 插件测试使用最新的稳定版 Python 版本 [@he0119](https://github.com/he0119) ([#2336](https://github.com/nonebot/nonebot2/pull/2336)) - Plugin: 删除不再维护的插件 [@ZM25XC](https://github.com/ZM25XC) ([#2330](https://github.com/nonebot/nonebot2/pull/2330)) - Plugin: 删除插件 poe ai [@nikissXI](https://github.com/nikissXI) ([#2308](https://github.com/nonebot/nonebot2/pull/2308)) - Plugin: 移除不再维护的插件,修改插件信息 [@Well2333](https://github.com/Well2333) ([#2292](https://github.com/nonebot/nonebot2/pull/2292)) - Fix: 修复 ruff 发现的问题 [@yanyongyu](https://github.com/yanyongyu) ([#2286](https://github.com/nonebot/nonebot2/pull/2286)) - Develop: 添加 dependabot actions 更新检查 [@yanyongyu](https://github.com/yanyongyu) ([#2256](https://github.com/nonebot/nonebot2/pull/2256)) - Develop: 添加 git attributes 定义 [@yanyongyu](https://github.com/yanyongyu) ([#2210](https://github.com/nonebot/nonebot2/pull/2210)) ### 🍻 插件发布 - Plugin: 文心一言 [@noneflow](https://github.com/noneflow) ([#2342](https://github.com/nonebot/nonebot2/pull/2342)) - Plugin: nonebot_plugin_group_whitelist [@noneflow](https://github.com/noneflow) ([#2320](https://github.com/nonebot/nonebot2/pull/2320)) - Plugin: 森空岛明日方舟签到器 [@noneflow](https://github.com/noneflow) ([#2340](https://github.com/nonebot/nonebot2/pull/2340)) - Plugin: 女装 ! [@noneflow](https://github.com/noneflow) ([#2337](https://github.com/nonebot/nonebot2/pull/2337)) - Plugin: helper_plus [@noneflow](https://github.com/noneflow) ([#2324](https://github.com/nonebot/nonebot2/pull/2324)) - Plugin: nonebot-plugin-souti [@noneflow](https://github.com/noneflow) ([#2334](https://github.com/nonebot/nonebot2/pull/2334)) - Plugin: Alconna 帮助工具 [@noneflow](https://github.com/noneflow) ([#2326](https://github.com/nonebot/nonebot2/pull/2326)) - Plugin: 消息伪造 [@noneflow](https://github.com/noneflow) ([#2312](https://github.com/nonebot/nonebot2/pull/2312)) - Plugin: 二维码 [@noneflow](https://github.com/noneflow) ([#2302](https://github.com/nonebot/nonebot2/pull/2302)) - Plugin: httpcat-状态猫 😺 [@noneflow](https://github.com/noneflow) ([#2306](https://github.com/nonebot/nonebot2/pull/2306)) - Plugin: 雪豹闭嘴 [@noneflow](https://github.com/noneflow) ([#2300](https://github.com/nonebot/nonebot2/pull/2300)) - Plugin: Nonebot Requests [@noneflow](https://github.com/noneflow) ([#2294](https://github.com/nonebot/nonebot2/pull/2294)) - Plugin: 双向聊天插件 [@noneflow](https://github.com/noneflow) ([#2291](https://github.com/nonebot/nonebot2/pull/2291)) - Plugin: 识别动漫 gal 角色 [@noneflow](https://github.com/noneflow) ([#2288](https://github.com/nonebot/nonebot2/pull/2288)) - Plugin: arxiv 订阅 [@noneflow](https://github.com/noneflow) ([#2285](https://github.com/nonebot/nonebot2/pull/2285)) - Plugin: SUDO [@noneflow](https://github.com/noneflow) ([#2277](https://github.com/nonebot/nonebot2/pull/2277)) - Plugin: 消息推送插件 [@noneflow](https://github.com/noneflow) ([#2273](https://github.com/nonebot/nonebot2/pull/2273)) - Plugin: 周易蓍草占卜 [@noneflow](https://github.com/noneflow) ([#2268](https://github.com/nonebot/nonebot2/pull/2268)) - Plugin: 欧若可骰娘 [@noneflow](https://github.com/noneflow) ([#2266](https://github.com/nonebot/nonebot2/pull/2266)) - Plugin: 科大讯飞星火大模型聊天 [@noneflow](https://github.com/noneflow) ([#2258](https://github.com/nonebot/nonebot2/pull/2258)) - Plugin: 剑网三查询和推送 [@noneflow](https://github.com/noneflow) ([#2254](https://github.com/nonebot/nonebot2/pull/2254)) - Plugin: Muteme(我禁我自己) [@noneflow](https://github.com/noneflow) ([#2252](https://github.com/nonebot/nonebot2/pull/2252)) - Plugin: MC 版本更新检测 [@noneflow](https://github.com/noneflow) ([#2247](https://github.com/nonebot/nonebot2/pull/2247)) - Plugin: KanonBot [@noneflow](https://github.com/noneflow) ([#2244](https://github.com/nonebot/nonebot2/pull/2244)) - Plugin: CSGO 饰品查询机器人 [@noneflow](https://github.com/noneflow) ([#2225](https://github.com/nonebot/nonebot2/pull/2225)) - Plugin: talk with poe ai [@noneflow](https://github.com/noneflow) ([#2230](https://github.com/nonebot/nonebot2/pull/2230)) - Plugin: 命运方舟流浪商人卡牌刷新提示 [@noneflow](https://github.com/noneflow) ([#2234](https://github.com/nonebot/nonebot2/pull/2234)) - Plugin: Savepic [@noneflow](https://github.com/noneflow) ([#2232](https://github.com/nonebot/nonebot2/pull/2232)) - Plugin: 跨平台账户绑定 [@noneflow](https://github.com/noneflow) ([#2227](https://github.com/nonebot/nonebot2/pull/2227)) - Plugin: Among US 中的 TOH 模组职业介绍 [@noneflow](https://github.com/noneflow) ([#2221](https://github.com/nonebot/nonebot2/pull/2221)) - Plugin: NoneMeme [@noneflow](https://github.com/noneflow) ([#2219](https://github.com/nonebot/nonebot2/pull/2219)) - Plugin: The World [@noneflow](https://github.com/noneflow) ([#2216](https://github.com/nonebot/nonebot2/pull/2216)) - Plugin: Bot 上下线邮件通知 [@noneflow](https://github.com/noneflow) ([#2214](https://github.com/nonebot/nonebot2/pull/2214)) - Plugin: bot 断连通知 [@noneflow](https://github.com/noneflow) ([#2212](https://github.com/nonebot/nonebot2/pull/2212)) ### 🍻 机器人发布 - Bot: OCNbot [@noneflow](https://github.com/noneflow) ([#2261](https://github.com/nonebot/nonebot2/pull/2261)) - Bot: 星见 Kirami [@noneflow](https://github.com/noneflow) ([#2263](https://github.com/nonebot/nonebot2/pull/2263)) - Bot: 不正经的妹妹 [@noneflow](https://github.com/noneflow) ([#2249](https://github.com/nonebot/nonebot2/pull/2249)) ### 🍻 适配器发布 - Adapter: Discord [@noneflow](https://github.com/noneflow) ([#2315](https://github.com/nonebot/nonebot2/pull/2315)) - Adapter: RedProtocol [@noneflow](https://github.com/noneflow) ([#2239](https://github.com/nonebot/nonebot2/pull/2239)) ## v2.0.1 ### 🚀 新功能 - Develop: 添加 Pyright 检查 [@yanyongyu](https://github.com/yanyongyu) ([#2194](https://github.com/nonebot/nonebot2/pull/2194)) - Feature: 使用 `typing.override` 标记 [@yanyongyu](https://github.com/yanyongyu) ([#2193](https://github.com/nonebot/nonebot2/pull/2193)) - Feature: 补充响应器组属性 [@eya46](https://github.com/eya46) ([#2154](https://github.com/nonebot/nonebot2/pull/2154)) - Feature: CommandGroup 支持命令别名添加前缀选项 [@eya46](https://github.com/eya46) ([#2134](https://github.com/nonebot/nonebot2/pull/2134)) - Feature: 添加用于动态继承支持适配器数据的方法 [@NCBM](https://github.com/NCBM) ([#2127](https://github.com/nonebot/nonebot2/pull/2127)) - Feature: 添加内置插件的插件元数据 [@yanyongyu](https://github.com/yanyongyu) ([#2113](https://github.com/nonebot/nonebot2/pull/2113)) - Feature: 插件商店适配最新的插件元数据 [@he0119](https://github.com/he0119) ([#2094](https://github.com/nonebot/nonebot2/pull/2094)) - Feature: 依赖注入支持 Generic TypeVar 和 Matcher 重载 [@yanyongyu](https://github.com/yanyongyu) ([#2089](https://github.com/nonebot/nonebot2/pull/2089)) ### 🐛 Bug 修复 - Fix: 修复 Quart WS task 上下文错误 [@yanyongyu](https://github.com/yanyongyu) ([#2192](https://github.com/nonebot/nonebot2/pull/2192)) - Fix: 修复 dotenv 配置项为 None 将会跳过赋值 [@eya46](https://github.com/eya46) ([#2143](https://github.com/nonebot/nonebot2/pull/2143)) - Fix: 修复 `ArgParam` 不支持 `Annotated` [@eya46](https://github.com/eya46) ([#2124](https://github.com/nonebot/nonebot2/pull/2124)) - Fix: aiohttp 请求时 data 和 file 不能同时存在 [@j1g5awi](https://github.com/j1g5awi) ([#2088](https://github.com/nonebot/nonebot2/pull/2088)) - Fix: 修复因 loguru 更新导致的启动和关闭日志 name 不正常 [@DiheChen](https://github.com/DiheChen) ([#2080](https://github.com/nonebot/nonebot2/pull/2080)) ### 📝 文档 - Docs: 移动 Alconna 文档至最佳实践 [@RF-Tar-Railt](https://github.com/RF-Tar-Railt) ([#2208](https://github.com/nonebot/nonebot2/pull/2208)) - Docs: 移除商店中不符合现规范的 tag [@j1g5awi](https://github.com/j1g5awi) ([#2205](https://github.com/nonebot/nonebot2/pull/2205)) - Docs: 添加 scoped 插件配置指南 [@yanyongyu](https://github.com/yanyongyu) ([#2198](https://github.com/nonebot/nonebot2/pull/2198)) - Docs: 钩子函数代码片段补充 [@A-kirami](https://github.com/A-kirami) ([#2173](https://github.com/nonebot/nonebot2/pull/2173)) - Docs: 格式化钩子函数中的代码片段 [@A-kirami](https://github.com/A-kirami) ([#2172](https://github.com/nonebot/nonebot2/pull/2172)) - Docs: 补充 Message.only 文档 [@eya46](https://github.com/eya46) ([#2155](https://github.com/nonebot/nonebot2/pull/2155)) - Docs: 修复日志自定义文档 typo [@17TheWord](https://github.com/17TheWord) ([#2140](https://github.com/nonebot/nonebot2/pull/2140)) - Docs: 修复依赖注入文档 `ArgStr` 3.9+ 和 3.8+ 版本代码写反 [@eya46](https://github.com/eya46) ([#2126](https://github.com/nonebot/nonebot2/pull/2126)) - Docs: 删除商店插件发布多余模块 [@forchannot](https://github.com/forchannot) ([#2095](https://github.com/nonebot/nonebot2/pull/2095)) - Docs: 微调插件元数据的部分描述 [@NCBM](https://github.com/NCBM) ([#2096](https://github.com/nonebot/nonebot2/pull/2096)) - Docs: 完成发布插件教程 [@NCBM](https://github.com/NCBM) ([#2078](https://github.com/nonebot/nonebot2/pull/2078)) - Docs: 更新插件元数据的相关描述 [@NCBM](https://github.com/NCBM) ([#2087](https://github.com/nonebot/nonebot2/pull/2087)) - Docs: 添加 Villa 适配器到 README [@CMHopeSunshine](https://github.com/CMHopeSunshine) ([#2086](https://github.com/nonebot/nonebot2/pull/2086)) ### 💫 杂项 - Plugin: 黑白名单添加标签 [@A-kirami](https://github.com/A-kirami) ([#2170](https://github.com/nonebot/nonebot2/pull/2170)) - Plugin: 修改 nonebot-plugin-ocgbot-v2 插件名称 [@fireinsect](https://github.com/fireinsect) ([#2147](https://github.com/nonebot/nonebot2/pull/2147)) - Plugin: 更新 SparkGPT 插件描述 [@canxin121](https://github.com/canxin121) ([#2144](https://github.com/nonebot/nonebot2/pull/2144)) - Plugin: 修改 nonebot-plugin-ocgbot-v2 插件名称 [@fireinsect](https://github.com/fireinsect) ([#2141](https://github.com/nonebot/nonebot2/pull/2141)) - Plugin: 删除 nonebot-plugin-phlogo [@kexue-z](https://github.com/kexue-z) ([#2128](https://github.com/nonebot/nonebot2/pull/2128)) - Plugin: 修改 `nonebot-plugin-gw2` 模块名 [@Agnes4m](https://github.com/Agnes4m) ([#2123](https://github.com/nonebot/nonebot2/pull/2123)) - Develop: 添加 ruff linter [@yanyongyu](https://github.com/yanyongyu) ([#2114](https://github.com/nonebot/nonebot2/pull/2114)) - Plugin: 更新 `nonebot-plugin-msgbuf` 插件的名称等信息 [@NCBM](https://github.com/NCBM) ([#2119](https://github.com/nonebot/nonebot2/pull/2119)) - Plugin: 修改插件信息和仓库地址 [@Agnes4m](https://github.com/Agnes4m) ([#2115](https://github.com/nonebot/nonebot2/pull/2115)) - Test: 移除 httpbin 并整理测试 [@yanyongyu](https://github.com/yanyongyu) ([#2110](https://github.com/nonebot/nonebot2/pull/2110)) - CI: 缓存 NoneFlow 所需的 pre-commit hooks [@he0119](https://github.com/he0119) ([#2104](https://github.com/nonebot/nonebot2/pull/2104)) - Plugin: 移除过时未更新的插件\&Bot [@FYWinds](https://github.com/FYWinds) ([#2072](https://github.com/nonebot/nonebot2/pull/2072)) - Plugin: 删除插件 nonebot_plugin_r6s [@BalconyJH](https://github.com/BalconyJH) ([#2071](https://github.com/nonebot/nonebot2/pull/2071)) ### 🍻 插件发布 - Plugin: 方寸狭间 [@noneflow](https://github.com/noneflow) ([#2207](https://github.com/nonebot/nonebot2/pull/2207)) - Plugin: DALL-E 绘图 [@noneflow](https://github.com/noneflow) ([#2204](https://github.com/nonebot/nonebot2/pull/2204)) - Plugin: 指定戳一戳 [@noneflow](https://github.com/noneflow) ([#2202](https://github.com/nonebot/nonebot2/pull/2202)) - Plugin: templates_render [@noneflow](https://github.com/noneflow) ([#2197](https://github.com/nonebot/nonebot2/pull/2197)) - Plugin: MongoDB [@noneflow](https://github.com/noneflow) ([#2189](https://github.com/nonebot/nonebot2/pull/2189)) - Plugin: pjsk 表情 [@noneflow](https://github.com/noneflow) ([#2187](https://github.com/nonebot/nonebot2/pull/2187)) - Plugin: nonebot-plugin-wenan [@noneflow](https://github.com/noneflow) ([#2184](https://github.com/nonebot/nonebot2/pull/2184)) - Plugin: nonebot-plugin-picture-api [@noneflow](https://github.com/noneflow) ([#2180](https://github.com/nonebot/nonebot2/pull/2180)) - Plugin: Blocker [@noneflow](https://github.com/noneflow) ([#2178](https://github.com/nonebot/nonebot2/pull/2178)) - Plugin: nonebot-plugin-nobahpicture [@noneflow](https://github.com/noneflow) ([#2176](https://github.com/nonebot/nonebot2/pull/2176)) - Plugin: 过期事件过滤器 [@noneflow](https://github.com/noneflow) ([#2169](https://github.com/nonebot/nonebot2/pull/2169)) - Plugin: 猫猫虫咖波图片发送 [@noneflow](https://github.com/noneflow) ([#2167](https://github.com/nonebot/nonebot2/pull/2167)) - Plugin: nonebot-plugin-splatoon3 [@noneflow](https://github.com/noneflow) ([#2165](https://github.com/nonebot/nonebot2/pull/2165)) - Plugin: nonebot-plugin-cfassistant [@noneflow](https://github.com/noneflow) ([#2164](https://github.com/nonebot/nonebot2/pull/2164)) - Plugin: 算法竞赛比赛查询 [@noneflow](https://github.com/noneflow) ([#2159](https://github.com/nonebot/nonebot2/pull/2159)) - Plugin: nonebot-plugin-update [@noneflow](https://github.com/noneflow) ([#2153](https://github.com/nonebot/nonebot2/pull/2153)) - Plugin: 远程同意好友 [@noneflow](https://github.com/noneflow) ([#2146](https://github.com/nonebot/nonebot2/pull/2146)) - Plugin: 戳一戳事件 [@noneflow](https://github.com/noneflow) ([#2139](https://github.com/nonebot/nonebot2/pull/2139)) - Plugin: EitherChoice [@noneflow](https://github.com/noneflow) ([#2137](https://github.com/nonebot/nonebot2/pull/2137)) - Plugin: 用户信息 [@noneflow](https://github.com/noneflow) ([#2133](https://github.com/nonebot/nonebot2/pull/2133)) - Plugin: Diablo4 地狱狂潮 boss 提醒小助手 [@noneflow](https://github.com/noneflow) ([#2122](https://github.com/nonebot/nonebot2/pull/2122)) - Plugin: nonbot-plugin-ocgbot-v2 [@noneflow](https://github.com/noneflow) ([#2120](https://github.com/nonebot/nonebot2/pull/2120)) - Plugin: 错误告警 [@noneflow](https://github.com/noneflow) ([#2117](https://github.com/nonebot/nonebot2/pull/2117)) - Plugin: follow_withdraw [@noneflow](https://github.com/noneflow) ([#2112](https://github.com/nonebot/nonebot2/pull/2112)) - Plugin: 战雷查水表 [@noneflow](https://github.com/noneflow) ([#2103](https://github.com/nonebot/nonebot2/pull/2103)) - Plugin: bili_push [@noneflow](https://github.com/noneflow) ([#2101](https://github.com/nonebot/nonebot2/pull/2101)) - Plugin: AI 作曲 [@noneflow](https://github.com/noneflow) ([#2093](https://github.com/nonebot/nonebot2/pull/2093)) - Plugin: pcrjjc [@noneflow](https://github.com/noneflow) ([#2091](https://github.com/nonebot/nonebot2/pull/2091)) - Plugin: twitter 订阅 [@noneflow](https://github.com/noneflow) ([#2082](https://github.com/nonebot/nonebot2/pull/2082)) - Plugin: 链接防夹 [@noneflow](https://github.com/noneflow) ([#2074](https://github.com/nonebot/nonebot2/pull/2074)) - Plugin: 碧蓝航线攻略 [@noneflow](https://github.com/noneflow) ([#2076](https://github.com/nonebot/nonebot2/pull/2076)) ### 🍻 机器人发布 - Bot: 米缸 [@noneflow](https://github.com/noneflow) ([#2191](https://github.com/nonebot/nonebot2/pull/2191)) - Bot: 林汐 [@noneflow](https://github.com/noneflow) ([#2182](https://github.com/nonebot/nonebot2/pull/2182)) - Bot: web_bot [@noneflow](https://github.com/noneflow) ([#2131](https://github.com/nonebot/nonebot2/pull/2131)) - Bot: ReimeiBot-黎明机器人 [@noneflow](https://github.com/noneflow) ([#2107](https://github.com/nonebot/nonebot2/pull/2107)) ### 🍻 适配器发布 - Adapter: 大别野 [@noneflow](https://github.com/noneflow) ([#2085](https://github.com/nonebot/nonebot2/pull/2085)) ## v2.0.0 ### 💥 破坏性变更 - Feature: 支持 `re.Match` 依赖注入 [@yanyongyu](https://github.com/yanyongyu) ([#1950](https://github.com/nonebot/nonebot2/pull/1950)) ### 🚀 新功能 - Feature: 优化事件分发方法 [@yanyongyu](https://github.com/yanyongyu) ([#2067](https://github.com/nonebot/nonebot2/pull/2067)) - Feature: 移除部分依赖注入参数默认值检查 [@yanyongyu](https://github.com/yanyongyu) ([#2034](https://github.com/nonebot/nonebot2/pull/2034)) - Feature: 添加插件元数据字段 `type` `homepage` `supported_adapters` [@yanyongyu](https://github.com/yanyongyu) ([#2012](https://github.com/nonebot/nonebot2/pull/2012)) - Feature: 支持 `re.Match` 依赖注入 [@yanyongyu](https://github.com/yanyongyu) ([#1950](https://github.com/nonebot/nonebot2/pull/1950)) - Feature: 支持主动停止 `none` 系列驱动器 [@yanyongyu](https://github.com/yanyongyu) ([#1951](https://github.com/nonebot/nonebot2/pull/1951)) - Feature: 为消息类添加 `has` `join` `include` `exclude` 方法 [@yanyongyu](https://github.com/yanyongyu) ([#1895](https://github.com/nonebot/nonebot2/pull/1895)) ### 🐛 Bug 修复 - Fix: 修复插件 require 未声明插件会识别为子插件 [@yanyongyu](https://github.com/yanyongyu) ([#2040](https://github.com/nonebot/nonebot2/pull/2040)) - Fix: 修复命令强制空白符影响无参数情况 [@yanyongyu](https://github.com/yanyongyu) ([#1975](https://github.com/nonebot/nonebot2/pull/1975)) - Fix: `run_sync` 上下文 [@synodriver](https://github.com/synodriver) ([#1968](https://github.com/nonebot/nonebot2/pull/1968)) - Fix: shell command 包含富文本时报错信息出错 [@yanyongyu](https://github.com/yanyongyu) ([#1923](https://github.com/nonebot/nonebot2/pull/1923)) ### 📝 文档 - Docs: 添加 Alconna 响应器介绍 [@RF-Tar-Railt](https://github.com/RF-Tar-Railt) ([#2069](https://github.com/nonebot/nonebot2/pull/2069)) - Docs: 更新 README 适配器链接 [@yanyongyu](https://github.com/yanyongyu) ([#2068](https://github.com/nonebot/nonebot2/pull/2068)) - Docs: 使用 issue form 进行商店发布 [@yanyongyu](https://github.com/yanyongyu) ([#2010](https://github.com/nonebot/nonebot2/pull/2010)) - Docs: 修复获取事件信息文档代码范例中的高亮行 [@Lptr-byte](https://github.com/Lptr-byte) ([#1983](https://github.com/nonebot/nonebot2/pull/1983)) - Docs: 修复事件处理函数文档代码范例中缺失的 import [@Lptr-byte](https://github.com/Lptr-byte) ([#1982](https://github.com/nonebot/nonebot2/pull/1982)) - Docs: 修复获取事件信息文档代码范例中缺失的 import [@Lptr-byte](https://github.com/Lptr-byte) ([#1980](https://github.com/nonebot/nonebot2/pull/1980)) - Docs: 新增插件跨平台指南 [@Well2333](https://github.com/Well2333) ([#1938](https://github.com/nonebot/nonebot2/pull/1938)) - Docs: 开启 blank issues [@yanyongyu](https://github.com/yanyongyu) ([#1945](https://github.com/nonebot/nonebot2/pull/1945)) - Docs: 使用 issue 表单替换 issue 模板 [@A-kirami](https://github.com/A-kirami) ([#1928](https://github.com/nonebot/nonebot2/pull/1928)) - Docs: 修正教程中部分 import 缺失的问题 [@Well2333](https://github.com/Well2333) ([#1927](https://github.com/nonebot/nonebot2/pull/1927)) - Docs: 添加 Walle-Q 到 Readme [@yanyongyu](https://github.com/yanyongyu) ([#1891](https://github.com/nonebot/nonebot2/pull/1891)) - Docs: 更新部署文档 [@yanyongyu](https://github.com/yanyongyu) ([#1890](https://github.com/nonebot/nonebot2/pull/1890)) ### 💫 杂项 - Plugin: Hello World 添加 tag [@A-kirami](https://github.com/A-kirami) ([#2056](https://github.com/nonebot/nonebot2/pull/2056)) - Plugin: 修改 nonebot-plugin-logpile 的名称和描述 [@A-kirami](https://github.com/A-kirami) ([#2057](https://github.com/nonebot/nonebot2/pull/2057)) - Plugin: 移除 `nonebot_paddle_ocr` 和 `nonebot_poe_chat` [@canxin121](https://github.com/canxin121) ([#2039](https://github.com/nonebot/nonebot2/pull/2039)) - Plugin: 移除 `nonebot-plugin-rtfm` 插件 [@MingxuanGame](https://github.com/MingxuanGame) ([#2037](https://github.com/nonebot/nonebot2/pull/2037)) - Plugin: 移除 extrautils 工具拓展插件(暂停维护) [@NCBM](https://github.com/NCBM) ([#2033](https://github.com/nonebot/nonebot2/pull/2033)) - Adapter: 更新 Minecraft 适配器 [@17TheWord](https://github.com/17TheWord) ([#1972](https://github.com/nonebot/nonebot2/pull/1972)) - Docs: 更正 issue 表单部分内容 [@A-kirami](https://github.com/A-kirami) ([#1961](https://github.com/nonebot/nonebot2/pull/1961)) - Plugin: 更新 AutoReply 插件描述 [@lgc2333](https://github.com/lgc2333) ([#1949](https://github.com/nonebot/nonebot2/pull/1949)) - Plugin: 移除 `MC_QQ_MCRcon` [@17TheWord](https://github.com/17TheWord) ([#1948](https://github.com/nonebot/nonebot2/pull/1948)) - Plugin: 更新 lgc2333 插件仓库地址 [@lgc2333](https://github.com/lgc2333) ([#1935](https://github.com/nonebot/nonebot2/pull/1935)) - Plugin: 更新多功能哔哩哔哩解析工具 [@djkcyl](https://github.com/djkcyl) ([#1913](https://github.com/nonebot/nonebot2/pull/1913)) - CI: 跳过 PR 仓库为 fork 的情况 [@he0119](https://github.com/he0119) ([#1905](https://github.com/nonebot/nonebot2/pull/1905)) - Plugin: 移除旧版本的 GenshinUID [@KimigaiiWuyi](https://github.com/KimigaiiWuyi) ([#1904](https://github.com/nonebot/nonebot2/pull/1904)) - CI: 使用最新的 NoneFlow [@he0119](https://github.com/he0119) ([#1899](https://github.com/nonebot/nonebot2/pull/1899)) - CI: 使用 NoneFlow 管理工作流 [@yanyongyu](https://github.com/yanyongyu) ([#1892](https://github.com/nonebot/nonebot2/pull/1892)) - CI: 移除 poetry 版本限制 [@yanyongyu](https://github.com/yanyongyu) ([#1872](https://github.com/nonebot/nonebot2/pull/1872)) ### 🍻 插件发布 - Plugin: stablediffusion 绘画插件 [@noneflow](https://github.com/noneflow) ([#2066](https://github.com/nonebot/nonebot2/pull/2066)) - Plugin: 随机抽取自定义内容 [@noneflow](https://github.com/noneflow) ([#2064](https://github.com/nonebot/nonebot2/pull/2064)) - Plugin: NAGA 公交车 [@noneflow](https://github.com/noneflow) ([#2062](https://github.com/nonebot/nonebot2/pull/2062)) - Plugin: 本子标题关键词提取 [@noneflow](https://github.com/noneflow) ([#2058](https://github.com/nonebot/nonebot2/pull/2058)) - Plugin: puzzle [@noneflow](https://github.com/noneflow) ([#2054](https://github.com/nonebot/nonebot2/pull/2054)) - Plugin: homo_mathematician [@noneflow](https://github.com/noneflow) ([#2052](https://github.com/nonebot/nonebot2/pull/2052)) - Plugin: cuber [@noneflow](https://github.com/noneflow) ([#2048](https://github.com/nonebot/nonebot2/pull/2048)) - Plugin: nonebot-plugin-lua [@noneflow](https://github.com/noneflow) ([#2049](https://github.com/nonebot/nonebot2/pull/2049)) - Plugin: Github 仓库卡片 [@noneflow](https://github.com/noneflow) ([#2042](https://github.com/nonebot/nonebot2/pull/2042)) - Plugin: 股票看盘助手 [@noneflow](https://github.com/noneflow) ([#2032](https://github.com/nonebot/nonebot2/pull/2032)) - Plugin: 便携插件安装器 [@noneflow](https://github.com/noneflow) ([#2027](https://github.com/nonebot/nonebot2/pull/2027)) - Plugin: 会话 id [@noneflow](https://github.com/noneflow) ([#2025](https://github.com/nonebot/nonebot2/pull/2025)) - Plugin: SD 绘画插件 [@noneflow](https://github.com/noneflow) ([#2023](https://github.com/nonebot/nonebot2/pull/2023)) - Plugin: 《女神异闻录 5》预告信生成器 [@noneflow](https://github.com/noneflow) ([#2021](https://github.com/nonebot/nonebot2/pull/2021)) - Plugin: 小小的 WEBAPI 调用插件 [@noneflow](https://github.com/noneflow) ([#2020](https://github.com/nonebot/nonebot2/pull/2020)) - Plugin: MultiNCM [@noneflow](https://github.com/noneflow) ([#2018](https://github.com/nonebot/nonebot2/pull/2018)) - Plugin: 签到 [@noneflow](https://github.com/noneflow) ([#2014](https://github.com/nonebot/nonebot2/pull/2014)) - Plugin: 链接解析 [@noneflow](https://github.com/noneflow) ([#2011](https://github.com/nonebot/nonebot2/pull/2011)) - Plugin: 信鸽巴夫 [@noneflow](https://github.com/noneflow) ([#2008](https://github.com/nonebot/nonebot2/pull/2008)) - Plugin: 明日方舟抽卡模拟 [@noneflow](https://github.com/noneflow) ([#2005](https://github.com/nonebot/nonebot2/pull/2005)) - Plugin: 雷神工业 [@noneflow](https://github.com/noneflow) ([#2003](https://github.com/nonebot/nonebot2/pull/2003)) - Plugin: nonebot-plugin-logpile [@noneflow](https://github.com/noneflow) ([#1999](https://github.com/nonebot/nonebot2/pull/1999)) - Plugin: Spark-GPT [@noneflow](https://github.com/noneflow) ([#1997](https://github.com/nonebot/nonebot2/pull/1997)) - Plugin: 企鹅物流统计数据查询 [@noneflow](https://github.com/noneflow) ([#1995](https://github.com/nonebot/nonebot2/pull/1995)) - Plugin: CallAPI [@noneflow](https://github.com/noneflow) ([#1990](https://github.com/nonebot/nonebot2/pull/1990)) - Plugin: 群聊人数锁定 [@noneflow](https://github.com/noneflow) ([#1988](https://github.com/nonebot/nonebot2/pull/1988)) - Plugin: CSGO 开箱模拟器 [@noneflow](https://github.com/noneflow) ([#1986](https://github.com/nonebot/nonebot2/pull/1986)) - Plugin: wordle_help [@noneflow](https://github.com/noneflow) ([#1974](https://github.com/nonebot/nonebot2/pull/1974)) - Plugin: 星穹铁道活动日历 [@noneflow](https://github.com/noneflow) ([#1970](https://github.com/nonebot/nonebot2/pull/1970)) - Plugin: 水印大师 [@noneflow](https://github.com/noneflow) ([#1965](https://github.com/nonebot/nonebot2/pull/1965)) - Plugin: 图片/漫画翻译 [@noneflow](https://github.com/noneflow) ([#1955](https://github.com/nonebot/nonebot2/pull/1955)) - Plugin: 为美好群聊献上爆炎 [@noneflow](https://github.com/noneflow) ([#1953](https://github.com/nonebot/nonebot2/pull/1953)) - Plugin: 公共画板插件 [@noneflow](https://github.com/noneflow) ([#1957](https://github.com/nonebot/nonebot2/pull/1957)) - Plugin: 运行代码 [@noneflow](https://github.com/noneflow) ([#1942](https://github.com/nonebot/nonebot2/pull/1942)) - Plugin: brainfuck [@noneflow](https://github.com/noneflow) ([#1944](https://github.com/nonebot/nonebot2/pull/1944)) - Plugin: Mixin [@noneflow](https://github.com/noneflow) ([#1947](https://github.com/nonebot/nonebot2/pull/1947)) - Plugin: AppInsights 日志监控 [@noneflow](https://github.com/noneflow) ([#1940](https://github.com/nonebot/nonebot2/pull/1940)) - Plugin: nonebot_poe_chat [@noneflow](https://github.com/noneflow) ([#1937](https://github.com/nonebot/nonebot2/pull/1937)) - Plugin: 更改 BOT 群名片 [@noneflow](https://github.com/noneflow) ([#1934](https://github.com/nonebot/nonebot2/pull/1934)) - Plugin: Akinator [@noneflow](https://github.com/noneflow) ([#1925](https://github.com/nonebot/nonebot2/pull/1925)) - Plugin: Bilifan [@noneflow](https://github.com/noneflow) ([#1921](https://github.com/nonebot/nonebot2/pull/1921)) - Plugin: osu!入群审批 [@noneflow](https://github.com/noneflow) ([#1919](https://github.com/nonebot/nonebot2/pull/1919)) - Plugin: 与 ChatGpt 聊天 [@noneflow](https://github.com/noneflow) ([#1917](https://github.com/nonebot/nonebot2/pull/1917)) - Plugin: TataruBot2 [@noneflow](https://github.com/noneflow) ([#1915](https://github.com/nonebot/nonebot2/pull/1915)) - Plugin: 宝可梦融合 [@noneflow](https://github.com/noneflow) ([#1912](https://github.com/nonebot/nonebot2/pull/1912)) - Plugin: FuckYou [@noneflow](https://github.com/noneflow) ([#1910](https://github.com/nonebot/nonebot2/pull/1910)) - Plugin: SDGPT [@noneflow](https://github.com/noneflow) ([#1908](https://github.com/nonebot/nonebot2/pull/1908)) - Plugin: nonebot clock 群闹钟 ⏰ [@noneflow](https://github.com/noneflow) ([#1906](https://github.com/nonebot/nonebot2/pull/1906)) - Plugin: B 站直播间路灯 [@noneflow](https://github.com/noneflow) ([#1901](https://github.com/nonebot/nonebot2/pull/1901)) - Plugin: GenshinUID [@noneflow](https://github.com/noneflow) ([#1903](https://github.com/nonebot/nonebot2/pull/1903)) - Plugin: 多功能哔哩哔哩解析工具 [@noneflow](https://github.com/noneflow) ([#1898](https://github.com/nonebot/nonebot2/pull/1898)) - Plugin: Steam 游戏状态播报 [@yanyongyu](https://github.com/yanyongyu) ([#1887](https://github.com/nonebot/nonebot2/pull/1887)) - Plugin: AI 生成 PPT [@yanyongyu](https://github.com/yanyongyu) ([#1884](https://github.com/nonebot/nonebot2/pull/1884)) - Plugin: nonebot_paddle_ocr [@yanyongyu](https://github.com/yanyongyu) ([#1882](https://github.com/nonebot/nonebot2/pull/1882)) - Plugin: nonebot_api_paddle [@yanyongyu](https://github.com/yanyongyu) ([#1880](https://github.com/nonebot/nonebot2/pull/1880)) - Plugin: 来份睡眠套餐 [@yanyongyu](https://github.com/yanyongyu) ([#1876](https://github.com/nonebot/nonebot2/pull/1876)) - Plugin: 今日老婆 [@yanyongyu](https://github.com/yanyongyu) ([#1874](https://github.com/nonebot/nonebot2/pull/1874)) - Plugin: 激战 2!!! [@yanyongyu](https://github.com/yanyongyu) ([#1871](https://github.com/nonebot/nonebot2/pull/1871)) - Plugin: ROLL [@yanyongyu](https://github.com/yanyongyu) ([#1868](https://github.com/nonebot/nonebot2/pull/1868)) ### 🍻 机器人发布 - Bot: 狐尾 [@noneflow](https://github.com/noneflow) ([#2009](https://github.com/nonebot/nonebot2/pull/2009)) - Bot: ay 机器人 [@noneflow](https://github.com/noneflow) ([#1993](https://github.com/nonebot/nonebot2/pull/1993)) - Bot: March7th [@noneflow](https://github.com/noneflow) ([#1978](https://github.com/nonebot/nonebot2/pull/1978)) - Bot: XDbot2 [@noneflow](https://github.com/noneflow) ([#1932](https://github.com/nonebot/nonebot2/pull/1932)) - Bot: CoolQBot [@noneflow](https://github.com/noneflow) ([#1894](https://github.com/nonebot/nonebot2/pull/1894)) ### 🍻 适配器发布 - Adapter: Walle-Q [@yanyongyu](https://github.com/yanyongyu) ([#1889](https://github.com/nonebot/nonebot2/pull/1889)) ## v2.0.0rc4 ### 🚀 新功能 - Feature: 公开自定义 `on` 函数所需的函数 [@A-kirami](https://github.com/A-kirami) ([#1856](https://github.com/nonebot/nonebot2/pull/1856)) - Feature: 重构驱动器 lifespan 方法 [@yanyongyu](https://github.com/yanyongyu) ([#1860](https://github.com/nonebot/nonebot2/pull/1860)) - Test: 使用 conditional coverage 插件 [@yanyongyu](https://github.com/yanyongyu) ([#1858](https://github.com/nonebot/nonebot2/pull/1858)) - Feature: 在 Windows 上处理 SIGBREAK 信号 [@he0119](https://github.com/he0119) ([#1836](https://github.com/nonebot/nonebot2/pull/1836)) - Feature: 为子依赖添加 PEP593 `Annotated` 支持 [@mnixry](https://github.com/mnixry) ([#1832](https://github.com/nonebot/nonebot2/pull/1832)) - Feature: 为 `User` 权限添加便捷创建方法 [@yanyongyu](https://github.com/yanyongyu) ([#1825](https://github.com/nonebot/nonebot2/pull/1825)) - Feature: 移除内置响应规则事件类型限制 [@yanyongyu](https://github.com/yanyongyu) ([#1824](https://github.com/nonebot/nonebot2/pull/1824)) - Feature: 允许继承和使用 Matcher 子类 [@yanyongyu](https://github.com/yanyongyu) ([#1815](https://github.com/nonebot/nonebot2/pull/1815)) - Feature: 添加 `get_adapter` 类型 overload [@yanyongyu](https://github.com/yanyongyu) ([#1755](https://github.com/nonebot/nonebot2/pull/1755)) - Feature: 命令匹配支持强制指定空白符 [@yanyongyu](https://github.com/yanyongyu) ([#1748](https://github.com/nonebot/nonebot2/pull/1748)) - Feature: 添加获取已注册适配器的方法 [@yanyongyu](https://github.com/yanyongyu) ([#1747](https://github.com/nonebot/nonebot2/pull/1747)) - Feature: 使用 `tomllib` 读取 toml 配置 [@yanyongyu](https://github.com/yanyongyu) ([#1720](https://github.com/nonebot/nonebot2/pull/1720)) - Feature: 优化插件加载日志 [@yanyongyu](https://github.com/yanyongyu) ([#1716](https://github.com/nonebot/nonebot2/pull/1716)) - Feature: 在加载 driver 引发 ImportError 时,使用 `raise from e` [@shoucandanghehe](https://github.com/shoucandanghehe) ([#1689](https://github.com/nonebot/nonebot2/pull/1689)) - Feature: 添加端口配置项约束验证 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#1632](https://github.com/nonebot/nonebot2/pull/1632)) ### 🐛 Bug 修复 - Test: coverage condition invert [@yanyongyu](https://github.com/yanyongyu) ([#1862](https://github.com/nonebot/nonebot2/pull/1862)) - Fix: 检测运行时创建响应器的插件 [@yanyongyu](https://github.com/yanyongyu) ([#1857](https://github.com/nonebot/nonebot2/pull/1857)) - Fix: 修复事件响应器辅助函数丢失 block [@yanyongyu](https://github.com/yanyongyu) ([#1859](https://github.com/nonebot/nonebot2/pull/1859)) - Fix: 修复 bot hook 缺少依赖缓存和上下文管理 [@yanyongyu](https://github.com/yanyongyu) ([#1826](https://github.com/nonebot/nonebot2/pull/1826)) - Fix: 会话更新依赖注入缺少缓存和上下文管理 [@yanyongyu](https://github.com/yanyongyu) ([#1807](https://github.com/nonebot/nonebot2/pull/1807)) - Fix: 修复适配器能断开非自身所有的 Bot 对象 [@yanyongyu](https://github.com/yanyongyu) ([#1757](https://github.com/nonebot/nonebot2/pull/1757)) ### 📝 文档 - Docs: 修改 NoneBug 独立测试模式流程控制参数 [@yanyongyu](https://github.com/yanyongyu) ([#1866](https://github.com/nonebot/nonebot2/pull/1866)) - Docs: 添加 VSCode 配置项名称 [@yanyongyu](https://github.com/yanyongyu) ([#1863](https://github.com/nonebot/nonebot2/pull/1863)) - Docs: 添加 Message 基类模板使用警告 [@yanyongyu](https://github.com/yanyongyu) ([#1853](https://github.com/nonebot/nonebot2/pull/1853)) - Docs: 移除 Messenger 移动端预期外的蓝色遮罩 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#1842](https://github.com/nonebot/nonebot2/pull/1842)) - Docs: 更新指向文档的链接 [@he0119](https://github.com/he0119) ([#1841](https://github.com/nonebot/nonebot2/pull/1841)) - Docs: 更新 setup 动图 [@yanyongyu](https://github.com/yanyongyu) ([#1840](https://github.com/nonebot/nonebot2/pull/1840)) - Docs: 重写教程与进阶指南 [@yanyongyu](https://github.com/yanyongyu) ([#1604](https://github.com/nonebot/nonebot2/pull/1604)) - Docs: pip 安装指令添加引号 [@3yude](https://github.com/3yude) ([#1724](https://github.com/nonebot/nonebot2/pull/1724)) - Docs: 修正交互模式命令 [@3yude](https://github.com/3yude) ([#1719](https://github.com/nonebot/nonebot2/pull/1719)) ### 💫 杂项 - Plugin: 删除 bnhhsh [@lgc2333](https://github.com/lgc2333) ([#1792](https://github.com/nonebot/nonebot2/pull/1792)) - CI: 暂时修复 poetry 依赖安装 [@yanyongyu](https://github.com/yanyongyu) ([#1776](https://github.com/nonebot/nonebot2/pull/1776)) - Plugin: 修改链接分享解析器插件名称 [@zhiyu1998](https://github.com/zhiyu1998) ([#1715](https://github.com/nonebot/nonebot2/pull/1715)) - Bot: 移除 ShigureBot [@lgc2333](https://github.com/lgc2333) ([#1699](https://github.com/nonebot/nonebot2/pull/1699)) - CI: 发布机器人使用 latest 标签 [@he0119](https://github.com/he0119) ([#1690](https://github.com/nonebot/nonebot2/pull/1690)) - Fix: 修改 bilibili live 的模块路径 [@yanyongyu](https://github.com/yanyongyu) ([#1679](https://github.com/nonebot/nonebot2/pull/1679)) - Docs: 移除商店中的过期插件 2023 [@j1g5awi](https://github.com/j1g5awi) ([#1610](https://github.com/nonebot/nonebot2/pull/1610)) ### 🍻 插件发布 - Plugin: ChatGPT 网页端 API [@yanyongyu](https://github.com/yanyongyu) ([#1865](https://github.com/nonebot/nonebot2/pull/1865)) - Plugin: 原神 cos [@yanyongyu](https://github.com/yanyongyu) ([#1855](https://github.com/nonebot/nonebot2/pull/1855)) - Plugin: 颠倒问号 [@yanyongyu](https://github.com/yanyongyu) ([#1849](https://github.com/nonebot/nonebot2/pull/1849)) - Plugin: nonebot-plugin-miao [@yanyongyu](https://github.com/yanyongyu) ([#1851](https://github.com/nonebot/nonebot2/pull/1851)) - Plugin: 通括膨胀 [@yanyongyu](https://github.com/yanyongyu) ([#1847](https://github.com/nonebot/nonebot2/pull/1847)) - Plugin: Hello World [@yanyongyu](https://github.com/yanyongyu) ([#1845](https://github.com/nonebot/nonebot2/pull/1845)) - Plugin: 喵喵点歌 [@yanyongyu](https://github.com/yanyongyu) ([#1838](https://github.com/nonebot/nonebot2/pull/1838)) - Plugin: ChatGLM-6B API 版 [@yanyongyu](https://github.com/yanyongyu) ([#1834](https://github.com/nonebot/nonebot2/pull/1834)) - Plugin: ChatGLM [@yanyongyu](https://github.com/yanyongyu) ([#1831](https://github.com/nonebot/nonebot2/pull/1831)) - Plugin: 基于 OpenAI 的 AI 模拟面试官 [@yanyongyu](https://github.com/yanyongyu) ([#1829](https://github.com/nonebot/nonebot2/pull/1829)) - Plugin: 多平台热搜获取插件 [@yanyongyu](https://github.com/yanyongyu) ([#1823](https://github.com/nonebot/nonebot2/pull/1823)) - Plugin: 随机点名 [@yanyongyu](https://github.com/yanyongyu) ([#1819](https://github.com/nonebot/nonebot2/pull/1819)) - Plugin: 表情包制作(调用 API 版) [@yanyongyu](https://github.com/yanyongyu) ([#1821](https://github.com/nonebot/nonebot2/pull/1821)) - Plugin: 群聊语录库 [@yanyongyu](https://github.com/yanyongyu) ([#1817](https://github.com/nonebot/nonebot2/pull/1817)) - Plugin: 随机狗妈 [@yanyongyu](https://github.com/yanyongyu) ([#1813](https://github.com/nonebot/nonebot2/pull/1813)) - Plugin: apex 信息查询 [@yanyongyu](https://github.com/yanyongyu) ([#1811](https://github.com/nonebot/nonebot2/pull/1811)) - Plugin: unoconv 文件转换 [@yanyongyu](https://github.com/yanyongyu) ([#1809](https://github.com/nonebot/nonebot2/pull/1809)) - Plugin: 原神历史卡池 [@yanyongyu](https://github.com/yanyongyu) ([#1806](https://github.com/nonebot/nonebot2/pull/1806)) - Plugin: 括号补全 [@yanyongyu](https://github.com/yanyongyu) ([#1804](https://github.com/nonebot/nonebot2/pull/1804)) - Plugin: 修仙模拟器 [@yanyongyu](https://github.com/yanyongyu) ([#1802](https://github.com/nonebot/nonebot2/pull/1802)) - Plugin: 发 6 [@yanyongyu](https://github.com/yanyongyu) ([#1798](https://github.com/nonebot/nonebot2/pull/1798)) - Plugin: 群聊自定义表情包 [@yanyongyu](https://github.com/yanyongyu) ([#1795](https://github.com/nonebot/nonebot2/pull/1795)) - Plugin: RimoFun [@yanyongyu](https://github.com/yanyongyu) ([#1791](https://github.com/nonebot/nonebot2/pull/1791)) - Plugin: ChatPDF 文章分析 [@yanyongyu](https://github.com/yanyongyu) ([#1788](https://github.com/nonebot/nonebot2/pull/1788)) - Plugin: 和团子聊天! [@yanyongyu](https://github.com/yanyongyu) ([#1785](https://github.com/nonebot/nonebot2/pull/1785)) - Plugin: 多功能的 ChatGPT 机器人 [@yanyongyu](https://github.com/yanyongyu) ([#1781](https://github.com/nonebot/nonebot2/pull/1781)) - Plugin: ChatGPT 官方接口版 [@yanyongyu](https://github.com/yanyongyu) ([#1767](https://github.com/nonebot/nonebot2/pull/1767)) - Plugin: 明日方舟抽卡记录分析 [@yanyongyu](https://github.com/yanyongyu) ([#1786](https://github.com/nonebot/nonebot2/pull/1786)) - Plugin: Sanae [@yanyongyu](https://github.com/yanyongyu) ([#1775](https://github.com/nonebot/nonebot2/pull/1775)) - Plugin: 小爱课程表 [@yanyongyu](https://github.com/yanyongyu) ([#1773](https://github.com/nonebot/nonebot2/pull/1773)) - Plugin: AutoRepeater [@yanyongyu](https://github.com/yanyongyu) ([#1769](https://github.com/nonebot/nonebot2/pull/1769)) - Plugin: 60s 日历 [@yanyongyu](https://github.com/yanyongyu) ([#1765](https://github.com/nonebot/nonebot2/pull/1765)) - Plugin: 青年大学习提交(基础版) [@yanyongyu](https://github.com/yanyongyu) ([#1764](https://github.com/nonebot/nonebot2/pull/1764)) - Plugin: 青年大学习提交(Web UI) [@yanyongyu](https://github.com/yanyongyu) ([#1762](https://github.com/nonebot/nonebot2/pull/1762)) - Plugin: 网抑云 [@yanyongyu](https://github.com/yanyongyu) ([#1760](https://github.com/nonebot/nonebot2/pull/1760)) - Plugin: nonebot_plugin_eventdone [@yanyongyu](https://github.com/yanyongyu) ([#1758](https://github.com/nonebot/nonebot2/pull/1758)) - Plugin: 爱发电审核 [@yanyongyu](https://github.com/yanyongyu) ([#1750](https://github.com/nonebot/nonebot2/pull/1750)) - Plugin: 战地一入群审批 [@yanyongyu](https://github.com/yanyongyu) ([#1745](https://github.com/nonebot/nonebot2/pull/1745)) - Plugin: wf 的 wm 市场 [@yanyongyu](https://github.com/yanyongyu) ([#1742](https://github.com/nonebot/nonebot2/pull/1742)) - Plugin: 呆呆兽都会用的 chatbot 接 api [@yanyongyu](https://github.com/yanyongyu) ([#1740](https://github.com/nonebot/nonebot2/pull/1740)) - Plugin: 呆呆兽都会起来锻炼 H2E [@yanyongyu](https://github.com/yanyongyu) ([#1739](https://github.com/nonebot/nonebot2/pull/1739)) - Plugin: 修仙\_2.0 [@yanyongyu](https://github.com/yanyongyu) ([#1730](https://github.com/nonebot/nonebot2/pull/1730)) - Plugin: 发病语录 [@yanyongyu](https://github.com/yanyongyu) ([#1728](https://github.com/nonebot/nonebot2/pull/1728)) - Plugin: 峯驰物流 [@yanyongyu](https://github.com/yanyongyu) ([#1723](https://github.com/nonebot/nonebot2/pull/1723)) - Plugin: Bing Chat [@yanyongyu](https://github.com/yanyongyu) ([#1714](https://github.com/nonebot/nonebot2/pull/1714)) - Plugin: 视频、图片解析器 [@yanyongyu](https://github.com/yanyongyu) ([#1710](https://github.com/nonebot/nonebot2/pull/1710)) - Plugin: 你画我猜组队 [@yanyongyu](https://github.com/yanyongyu) ([#1705](https://github.com/nonebot/nonebot2/pull/1705)) - Plugin: 明日方舟工具箱 [@yanyongyu](https://github.com/yanyongyu) ([#1698](https://github.com/nonebot/nonebot2/pull/1698)) - Plugin: 原神深境螺旋数据查询 [@yanyongyu](https://github.com/yanyongyu) ([#1696](https://github.com/nonebot/nonebot2/pull/1696)) - Plugin: 工具拓展 [@yanyongyu](https://github.com/yanyongyu) ([#1694](https://github.com/nonebot/nonebot2/pull/1694)) - Plugin: OneBot 实现 [@yanyongyu](https://github.com/yanyongyu) ([#1692](https://github.com/nonebot/nonebot2/pull/1692)) - Plugin: 舞萌 maimai 插件版 [@yanyongyu](https://github.com/yanyongyu) ([#1687](https://github.com/nonebot/nonebot2/pull/1687)) - Plugin: ACMReminder [@yanyongyu](https://github.com/yanyongyu) ([#1686](https://github.com/nonebot/nonebot2/pull/1686)) - Plugin: 通用指令阻断 [@yanyongyu](https://github.com/yanyongyu) ([#1683](https://github.com/nonebot/nonebot2/pull/1683)) - Plugin: 今天吃喝什么(图片版) [@yanyongyu](https://github.com/yanyongyu) ([#1678](https://github.com/nonebot/nonebot2/pull/1678)) - Plugin: Q 群消息事件监控 [@yanyongyu](https://github.com/yanyongyu) ([#1672](https://github.com/nonebot/nonebot2/pull/1672)) - Plugin: DickyPK [@yanyongyu](https://github.com/yanyongyu) ([#1670](https://github.com/nonebot/nonebot2/pull/1670)) - Plugin: 每日人品 2 [@yanyongyu](https://github.com/yanyongyu) ([#1669](https://github.com/nonebot/nonebot2/pull/1669)) - Plugin: 娶群友 [@yanyongyu](https://github.com/yanyongyu) ([#1665](https://github.com/nonebot/nonebot2/pull/1665)) - Plugin: 我要一张 xx 涩图 [@yanyongyu](https://github.com/yanyongyu) ([#1663](https://github.com/nonebot/nonebot2/pull/1663)) - Plugin: AutoReply [@yanyongyu](https://github.com/yanyongyu) ([#1660](https://github.com/nonebot/nonebot2/pull/1660)) - Plugin: B 站热搜 [@yanyongyu](https://github.com/yanyongyu) ([#1658](https://github.com/nonebot/nonebot2/pull/1658)) - Plugin: MC Ping [@yanyongyu](https://github.com/yanyongyu) ([#1656](https://github.com/nonebot/nonebot2/pull/1656)) - Plugin: impact 淫趴 [@yanyongyu](https://github.com/yanyongyu) ([#1653](https://github.com/nonebot/nonebot2/pull/1653)) - Plugin: 更人性化的 GPT-Ai 聊天插件 [@yanyongyu](https://github.com/yanyongyu) ([#1651](https://github.com/nonebot/nonebot2/pull/1651)) - Plugin: uuid 生成器 [@yanyongyu](https://github.com/yanyongyu) ([#1649](https://github.com/nonebot/nonebot2/pull/1649)) - Plugin: 舔狗日记 [@yanyongyu](https://github.com/yanyongyu) ([#1646](https://github.com/nonebot/nonebot2/pull/1646)) - Plugin: 查找轻小说 [@yanyongyu](https://github.com/yanyongyu) ([#1644](https://github.com/nonebot/nonebot2/pull/1644)) - Plugin: XDU 校园服务 [@yanyongyu](https://github.com/yanyongyu) ([#1642](https://github.com/nonebot/nonebot2/pull/1642)) - Plugin: nonebot-plugin-mcport [@yanyongyu](https://github.com/yanyongyu) ([#1640](https://github.com/nonebot/nonebot2/pull/1640)) - Plugin: Alconna 命令工具 [@yanyongyu](https://github.com/yanyongyu) ([#1639](https://github.com/nonebot/nonebot2/pull/1639)) - Plugin: Group_Link_Guild [@yanyongyu](https://github.com/yanyongyu) ([#1637](https://github.com/nonebot/nonebot2/pull/1637)) - Plugin: 简易群管女生自用 99 新 [@yanyongyu](https://github.com/yanyongyu) ([#1635](https://github.com/nonebot/nonebot2/pull/1635)) - Plugin: 青岚 [@yanyongyu](https://github.com/yanyongyu) ([#1631](https://github.com/nonebot/nonebot2/pull/1631)) - Plugin: 对话超管 [@yanyongyu](https://github.com/yanyongyu) ([#1627](https://github.com/nonebot/nonebot2/pull/1627)) - Plugin: 摩尔质量计算器 [@yanyongyu](https://github.com/yanyongyu) ([#1625](https://github.com/nonebot/nonebot2/pull/1625)) - Plugin: 植物大战僵尸小游戏 [@yanyongyu](https://github.com/yanyongyu) ([#1622](https://github.com/nonebot/nonebot2/pull/1622)) ### 🍻 机器人发布 - Bot: 桃桃酱 [@yanyongyu](https://github.com/yanyongyu) ([#1801](https://github.com/nonebot/nonebot2/pull/1801)) - Bot: fubot [@yanyongyu](https://github.com/yanyongyu) ([#1783](https://github.com/nonebot/nonebot2/pull/1783)) - Bot: LOVE 酱 [@yanyongyu](https://github.com/yanyongyu) ([#1779](https://github.com/nonebot/nonebot2/pull/1779)) - Bot: 脑积水 [@yanyongyu](https://github.com/yanyongyu) ([#1771](https://github.com/nonebot/nonebot2/pull/1771)) - Bot: koishi [@yanyongyu](https://github.com/yanyongyu) ([#1681](https://github.com/nonebot/nonebot2/pull/1681)) - Bot: ChensQBOTv2 [@yanyongyu](https://github.com/yanyongyu) ([#1676](https://github.com/nonebot/nonebot2/pull/1676)) - Bot: 青岚 [@yanyongyu](https://github.com/yanyongyu) ([#1630](https://github.com/nonebot/nonebot2/pull/1630)) ## v2.0.0rc3 ### 🚀 新功能 - Feature: 添加事件响应器检查完成日志 [@A-kirami](https://github.com/A-kirami) ([#1578](https://github.com/nonebot/nonebot2/pull/1578)) - Remove: 移除默认安装 FastAPI [@yanyongyu](https://github.com/yanyongyu) ([#1557](https://github.com/nonebot/nonebot2/pull/1557)) - Feature: 支持给 `FastAPI` 和 `Quart` 传递额外的参数 [@A-kirami](https://github.com/A-kirami) ([#1543](https://github.com/nonebot/nonebot2/pull/1543)) - Feature: 添加 `logger` 重导出 [@A-kirami](https://github.com/A-kirami) ([#1526](https://github.com/nonebot/nonebot2/pull/1526)) - Feature: 将 block driver 转正为 none 驱动器 [@he0119](https://github.com/he0119) ([#1522](https://github.com/nonebot/nonebot2/pull/1522)) - Develop: 使用 pycln 自动移除未使用的 import [@yanyongyu](https://github.com/yanyongyu) ([#1481](https://github.com/nonebot/nonebot2/pull/1481)) - Feature: 添加正则匹配文本注入 [@A-kirami](https://github.com/A-kirami) ([#1457](https://github.com/nonebot/nonebot2/pull/1457)) - Feature: 支持主动销毁事件响应器 [@A-kirami](https://github.com/A-kirami) ([#1444](https://github.com/nonebot/nonebot2/pull/1444)) ### 🐛 Bug 修复 - Fix: 屏蔽 fastapi 0.89.0 [@yanyongyu](https://github.com/yanyongyu) ([#1574](https://github.com/nonebot/nonebot2/pull/1574)) - Fix: 修复子插件加载失败时没有从父插件中移除的问题 [@A-kirami](https://github.com/A-kirami) ([#1559](https://github.com/nonebot/nonebot2/pull/1559)) - Fix: 修复客户端请求未处理 cookies [@yanyongyu](https://github.com/yanyongyu) ([#1491](https://github.com/nonebot/nonebot2/pull/1491)) - Fix: `on_type` typing error [@yanyongyu](https://github.com/yanyongyu) ([#1482](https://github.com/nonebot/nonebot2/pull/1482)) - Fix: 修复 ArgumentParser 错误信息叠加问题 [@yanyongyu](https://github.com/yanyongyu) ([#1426](https://github.com/nonebot/nonebot2/pull/1426)) ### 📝 文档 - Docs: 修改更新部分文档 [@yanyongyu](https://github.com/yanyongyu) ([#1615](https://github.com/nonebot/nonebot2/pull/1615)) - Docs: 商店搜索大小写不敏感 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#1609](https://github.com/nonebot/nonebot2/pull/1609)) - Docs: 更新测试文档中的连接方式\&细化插件发布描述 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#1504](https://github.com/nonebot/nonebot2/pull/1504)) - Docs: 修复文档中部分超链接跳转到 `/store.html` 的问题 [@yzyyz1387](https://github.com/yzyyz1387) ([#1470](https://github.com/nonebot/nonebot2/pull/1470)) - Fix: 补充 `params` 模块的类型注解 [@A-kirami](https://github.com/A-kirami) ([#1458](https://github.com/nonebot/nonebot2/pull/1458)) - Docs: 移除文档 `自定义日志` 中多余的符号 [@A-kirami](https://github.com/A-kirami) ([#1448](https://github.com/nonebot/nonebot2/pull/1448)) - Docs: 完善 `调用平台 API` 部分 [@A-kirami](https://github.com/A-kirami) ([#1447](https://github.com/nonebot/nonebot2/pull/1447)) - Docs: 修正文档中部分配置文件示例的符号误用 [@MingxuanGame](https://github.com/MingxuanGame) ([#1432](https://github.com/nonebot/nonebot2/pull/1432)) ### 💫 杂项 - Plugin: 移除 nonebot-plugin-puppet [@j1g5awi](https://github.com/j1g5awi) ([#1605](https://github.com/nonebot/nonebot2/pull/1605)) - Plugin: 更新 MC 的插件信息 [@nikissXI](https://github.com/nikissXI) ([#1589](https://github.com/nonebot/nonebot2/pull/1589)) - Plugin: 移除 `nonebot-plugin-aidraw` [@A-kirami](https://github.com/A-kirami) ([#1588](https://github.com/nonebot/nonebot2/pull/1588)) - Plugins: 更新 ayaka_games 插件名和描述 [@bridgeL](https://github.com/bridgeL) ([#1586](https://github.com/nonebot/nonebot2/pull/1586)) - Plugin: 更新 tts_gal 插件名和描述 [@dpm12345](https://github.com/dpm12345) ([#1581](https://github.com/nonebot/nonebot2/pull/1581)) - Plugin: 移除 `nonebot_plugin_super_resolution` [@A-kirami](https://github.com/A-kirami) ([#1561](https://github.com/nonebot/nonebot2/pull/1561)) - Plugin: 更新 OlivOS.nb2 import 包名 [@j1g5awi](https://github.com/j1g5awi) ([#1560](https://github.com/nonebot/nonebot2/pull/1560)) - Develop: 添加 pyright 环境配置 [@yanyongyu](https://github.com/yanyongyu) ([#1554](https://github.com/nonebot/nonebot2/pull/1554)) - CI: 优化触发条件减少无效运行 [@he0119](https://github.com/he0119) ([#1545](https://github.com/nonebot/nonebot2/pull/1545)) - Plugin: 删除 ayaka_who_is_suspect 插件 [@bridgeL](https://github.com/bridgeL) ([#1525](https://github.com/nonebot/nonebot2/pull/1525)) - Fix: 修复异常在 traceback 中无法正常显示信息 [@he0119](https://github.com/he0119) ([#1521](https://github.com/nonebot/nonebot2/pull/1521)) - CI: 添加插件加载测试 [@he0119](https://github.com/he0119) ([#1519](https://github.com/nonebot/nonebot2/pull/1519)) - Plugin: 移除 `nonebot-plugin-filehost` [@mnixry](https://github.com/mnixry) ([#1516](https://github.com/nonebot/nonebot2/pull/1516)) - Plugin: 更新 `abstain_diary` 插件名和描述 [@Ikaros-521](https://github.com/Ikaros-521) ([#1509](https://github.com/nonebot/nonebot2/pull/1509)) - Plugin: 更新 gpt3 插件模块名 [@chrisyy2003](https://github.com/chrisyy2003) ([#1501](https://github.com/nonebot/nonebot2/pull/1501)) - Plugin: 更新 随机禁言 插件功能描述 [@Ikaros-521](https://github.com/Ikaros-521) ([#1495](https://github.com/nonebot/nonebot2/pull/1495)) - Plugin: 更新 multi chatgpt 插件仓库地址 [@chrisyy2003](https://github.com/chrisyy2003) ([#1487](https://github.com/nonebot/nonebot2/pull/1487)) - Plugin: 更新 ayaka_games 介绍 [@bridgeL](https://github.com/bridgeL) ([#1431](https://github.com/nonebot/nonebot2/pull/1431)) - Plugin: 修改 novelai send magiadice 插件模块名 [@sena-nana](https://github.com/sena-nana) ([#1423](https://github.com/nonebot/nonebot2/pull/1423)) ### 🍻 插件发布 - Plugin: 反向词典 [@yanyongyu](https://github.com/yanyongyu) ([#1619](https://github.com/nonebot/nonebot2/pull/1619)) - Plugin: PicMCStat [@yanyongyu](https://github.com/yanyongyu) ([#1614](https://github.com/nonebot/nonebot2/pull/1614)) - Plugin: 犯人在跳舞 [@yanyongyu](https://github.com/yanyongyu) ([#1608](https://github.com/nonebot/nonebot2/pull/1608)) - Plugin: 喵喵自记菜谱 [@yanyongyu](https://github.com/yanyongyu) ([#1599](https://github.com/nonebot/nonebot2/pull/1599)) - Plugin: 语音功能 [@yanyongyu](https://github.com/yanyongyu) ([#1597](https://github.com/nonebot/nonebot2/pull/1597)) - Plugin: OrangeDice! [@yanyongyu](https://github.com/yanyongyu) ([#1595](https://github.com/nonebot/nonebot2/pull/1595)) - Plugin: 简易谷歌翻译插件 [@yanyongyu](https://github.com/yanyongyu) ([#1593](https://github.com/nonebot/nonebot2/pull/1593)) - Plugin: 哔哩哔哩 q 群登录 [@yanyongyu](https://github.com/yanyongyu) ([#1591](https://github.com/nonebot/nonebot2/pull/1591)) - Plugin: 原神实时公告 [@yanyongyu](https://github.com/yanyongyu) ([#1585](https://github.com/nonebot/nonebot2/pull/1585)) - Plugin: 心灵鸡汤 [@yanyongyu](https://github.com/yanyongyu) ([#1580](https://github.com/nonebot/nonebot2/pull/1580)) - Plugin: Bing 每日图片获取 [@yanyongyu](https://github.com/yanyongyu) ([#1577](https://github.com/nonebot/nonebot2/pull/1577)) - Plugin: 星座运势 [@yanyongyu](https://github.com/yanyongyu) ([#1572](https://github.com/nonebot/nonebot2/pull/1572)) - Plugin: 回声洞 [@yanyongyu](https://github.com/yanyongyu) ([#1573](https://github.com/nonebot/nonebot2/pull/1573)) - Plugin: 整点报时 [@yanyongyu](https://github.com/yanyongyu) ([#1569](https://github.com/nonebot/nonebot2/pull/1569)) - Plugin: Hypixel 数据查询 [@yanyongyu](https://github.com/yanyongyu) ([#1556](https://github.com/nonebot/nonebot2/pull/1556)) - Plugin: 查找图片出处 [@yanyongyu](https://github.com/yanyongyu) ([#1553](https://github.com/nonebot/nonebot2/pull/1553)) - Plugin: 云签到 [@yanyongyu](https://github.com/yanyongyu) ([#1551](https://github.com/nonebot/nonebot2/pull/1551)) - Plugin: 图像标注 [@yanyongyu](https://github.com/yanyongyu) ([#1550](https://github.com/nonebot/nonebot2/pull/1550)) - Plugin: 对对联 [@yanyongyu](https://github.com/yanyongyu) ([#1542](https://github.com/nonebot/nonebot2/pull/1542)) - Plugin: 群聊学习 [@yanyongyu](https://github.com/yanyongyu) ([#1540](https://github.com/nonebot/nonebot2/pull/1540)) - Plugin: 求生之路 2——服务器操作 [@yanyongyu](https://github.com/yanyongyu) ([#1538](https://github.com/nonebot/nonebot2/pull/1538)) - Plugin: setu_customization [@yanyongyu](https://github.com/yanyongyu) ([#1537](https://github.com/nonebot/nonebot2/pull/1537)) - Plugin: 主动消息撤回 [@yanyongyu](https://github.com/yanyongyu) ([#1536](https://github.com/nonebot/nonebot2/pull/1536)) - Plugin: HttpCat🐱 猫猫 http 状态码 [@yanyongyu](https://github.com/yanyongyu) ([#1529](https://github.com/nonebot/nonebot2/pull/1529)) - Plugin: 命令探查 [@yanyongyu](https://github.com/yanyongyu) ([#1524](https://github.com/nonebot/nonebot2/pull/1524)) - Plugin: AnimalVoice_Convert [@yanyongyu](https://github.com/yanyongyu) ([#1518](https://github.com/nonebot/nonebot2/pull/1518)) - Plugin: 服务状态查询 [@yanyongyu](https://github.com/yanyongyu) ([#1513](https://github.com/nonebot/nonebot2/pull/1513)) - Plugin: 腾讯云图像变换 [@yanyongyu](https://github.com/yanyongyu) ([#1515](https://github.com/nonebot/nonebot2/pull/1515)) - Plugin: Ping [@yanyongyu](https://github.com/yanyongyu) ([#1508](https://github.com/nonebot/nonebot2/pull/1508)) - Plugin: 群友召唤术 [@yanyongyu](https://github.com/yanyongyu) ([#1503](https://github.com/nonebot/nonebot2/pull/1503)) - Plugin: 战地群聊天插件 [@yanyongyu](https://github.com/yanyongyu) ([#1506](https://github.com/nonebot/nonebot2/pull/1506)) - Plugin: 不要复读 [@yanyongyu](https://github.com/yanyongyu) ([#1500](https://github.com/nonebot/nonebot2/pull/1500)) - Plugin: JAVA MC 服务器信息查询 [@yanyongyu](https://github.com/yanyongyu) ([#1497](https://github.com/nonebot/nonebot2/pull/1497)) - Plugin: 防撤回 [@yanyongyu](https://github.com/yanyongyu) ([#1489](https://github.com/nonebot/nonebot2/pull/1489)) - Plugin: 随机禁言 [@yanyongyu](https://github.com/yanyongyu) ([#1486](https://github.com/nonebot/nonebot2/pull/1486)) - Plugin: 只因进化录 [@yanyongyu](https://github.com/yanyongyu) ([#1484](https://github.com/nonebot/nonebot2/pull/1484)) - Plugin: GPT3 [@yanyongyu](https://github.com/yanyongyu) ([#1480](https://github.com/nonebot/nonebot2/pull/1480)) - Plugin: 熊老板 [@yanyongyu](https://github.com/yanyongyu) ([#1472](https://github.com/nonebot/nonebot2/pull/1472)) - Plugin: QQ 群文件备份 [@yanyongyu](https://github.com/yanyongyu) ([#1478](https://github.com/nonebot/nonebot2/pull/1478)) - Plugin: 戒色打卡日记 [@yanyongyu](https://github.com/yanyongyu) ([#1475](https://github.com/nonebot/nonebot2/pull/1475)) - Plugin: nonebot_plugin_idiom [@yanyongyu](https://github.com/yanyongyu) ([#1469](https://github.com/nonebot/nonebot2/pull/1469)) - Plugin: 随机配色方案 [@yanyongyu](https://github.com/yanyongyu) ([#1466](https://github.com/nonebot/nonebot2/pull/1466)) - Plugin: multi-ChatGPT [@yanyongyu](https://github.com/yanyongyu) ([#1462](https://github.com/nonebot/nonebot2/pull/1462)) - Plugin: 权限控制 [@yanyongyu](https://github.com/yanyongyu) ([#1464](https://github.com/nonebot/nonebot2/pull/1464)) - Plugin: 汇率换算 [@yanyongyu](https://github.com/yanyongyu) ([#1452](https://github.com/nonebot/nonebot2/pull/1452)) - Plugin: 全群广播 [@yanyongyu](https://github.com/yanyongyu) ([#1450](https://github.com/nonebot/nonebot2/pull/1450)) - Plugin: 图片背景消除 [@yanyongyu](https://github.com/yanyongyu) ([#1446](https://github.com/nonebot/nonebot2/pull/1446)) - Plugin: 雀魂信息查询 [@yanyongyu](https://github.com/yanyongyu) ([#1443](https://github.com/nonebot/nonebot2/pull/1443)) - Plugin: ChatGPT [@yanyongyu](https://github.com/yanyongyu) ([#1439](https://github.com/nonebot/nonebot2/pull/1439)) - Plugin: 免费快捷点歌插件 [@yanyongyu](https://github.com/yanyongyu) ([#1436](https://github.com/nonebot/nonebot2/pull/1436)) - Plugin: 动画截图追溯来源 [@yanyongyu](https://github.com/yanyongyu) ([#1434](https://github.com/nonebot/nonebot2/pull/1434)) - Plugin: b 站图片下载 [@yanyongyu](https://github.com/yanyongyu) ([#1430](https://github.com/nonebot/nonebot2/pull/1430)) - Plugin: 记事本 [@yanyongyu](https://github.com/yanyongyu) ([#1420](https://github.com/nonebot/nonebot2/pull/1420)) - Plugin: 原神前瞻直播兑换码查询 [@yanyongyu](https://github.com/yanyongyu) ([#1422](https://github.com/nonebot/nonebot2/pull/1422)) ### 🍻 机器人发布 - Bot: SuzunoBot [@yanyongyu](https://github.com/yanyongyu) ([#1601](https://github.com/nonebot/nonebot2/pull/1601)) - Bot: 辞辞(cici)Bot [@yanyongyu](https://github.com/yanyongyu) ([#1583](https://github.com/nonebot/nonebot2/pull/1583)) - Bot: RanBot [@yanyongyu](https://github.com/yanyongyu) ([#1511](https://github.com/nonebot/nonebot2/pull/1511)) ### 🍻 适配器发布 - Adapter: BilibiliLive [@yanyongyu](https://github.com/yanyongyu) ([#1617](https://github.com/nonebot/nonebot2/pull/1617)) - Adapter: Spigot [@yanyongyu](https://github.com/yanyongyu) ([#1612](https://github.com/nonebot/nonebot2/pull/1612)) ## v2.0.0rc2 ### 💥 破坏性变更 - Feature: 使用 `importlib.metadata` 替换 `pkg_resources` [@A-kirami](https://github.com/A-kirami) ([#1388](https://github.com/nonebot/nonebot2/pull/1388)) ### 🚀 新功能 - Feature: 支持自定义 matchers 存储管理 [@yanyongyu](https://github.com/yanyongyu) ([#1395](https://github.com/nonebot/nonebot2/pull/1395)) - Feature: 升级 devcontainer 配置 [@yanyongyu](https://github.com/yanyongyu) ([#1392](https://github.com/nonebot/nonebot2/pull/1392)) - Feature: 使用 `importlib.metadata` 替换 `pkg_resources` [@A-kirami](https://github.com/A-kirami) ([#1388](https://github.com/nonebot/nonebot2/pull/1388)) - CI: 测试环境添加 Python 3.11 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#1366](https://github.com/nonebot/nonebot2/pull/1366)) - Feature: 新增 dotenv 嵌套配置项支持 [@yanyongyu](https://github.com/yanyongyu) ([#1324](https://github.com/nonebot/nonebot2/pull/1324)) - Feature: 添加 State 响应器触发消息注入 [@A-kirami](https://github.com/A-kirami) ([#1315](https://github.com/nonebot/nonebot2/pull/1315)) - Remove: 移除无用的 namespace 声明 [@yanyongyu](https://github.com/yanyongyu) ([#1306](https://github.com/nonebot/nonebot2/pull/1306)) ### 🐛 Bug 修复 - Fix: Bot `__getattr__` 不再对 `__xxx__` 方法返回 [@synodriver](https://github.com/synodriver) ([#1398](https://github.com/nonebot/nonebot2/pull/1398)) - Fix: 修复 run pre/post hook 没有在正确的上下文中运行 [@yanyongyu](https://github.com/yanyongyu) ([#1391](https://github.com/nonebot/nonebot2/pull/1391)) ### 📝 文档 - Docs: 添加 ntchat 社区适配器 [@JustUndertaker](https://github.com/JustUndertaker) ([#1414](https://github.com/nonebot/nonebot2/pull/1414)) ### 💫 杂项 - Plugin: b 站用户信息查询 [@Ikaros-521](https://github.com/Ikaros-521) ([#1410](https://github.com/nonebot/nonebot2/pull/1410)) - Plugin: 由于 Sena-nana 项目拆分,之前的插件地址更改 [@sena-nana](https://github.com/sena-nana) ([#1378](https://github.com/nonebot/nonebot2/pull/1378)) - Plugin: 更新 ayaka 插件的主页链接 [@bridgeL](https://github.com/bridgeL) ([#1346](https://github.com/nonebot/nonebot2/pull/1346)) - Plugin: 补充 novelai 插件信息 [@sena-nana](https://github.com/sena-nana) ([#1333](https://github.com/nonebot/nonebot2/pull/1333)) - Bot: 修改 Inkar Suki 描述 [@HornCopper](https://github.com/HornCopper) ([#1312](https://github.com/nonebot/nonebot2/pull/1312)) - Plugin: 修改插件 MCQQ MCRcon 主页地址 [@17TheWord](https://github.com/17TheWord) ([#1303](https://github.com/nonebot/nonebot2/pull/1303)) ### 🍻 插件发布 - Plugin: 谁在窥屏 [@yanyongyu](https://github.com/yanyongyu) ([#1416](https://github.com/nonebot/nonebot2/pull/1416)) - Plugin: 免费版 NovelAI 生图插件 [@yanyongyu](https://github.com/yanyongyu) ([#1408](https://github.com/nonebot/nonebot2/pull/1408)) - Plugin: sky 光遇 [@yanyongyu](https://github.com/yanyongyu) ([#1394](https://github.com/nonebot/nonebot2/pull/1394)) - Plugin: Colab-NovelAI [@yanyongyu](https://github.com/yanyongyu) ([#1390](https://github.com/nonebot/nonebot2/pull/1390)) - Plugin: b 站用户直播号、粉丝、舰团数查询 [@yanyongyu](https://github.com/yanyongyu) ([#1385](https://github.com/nonebot/nonebot2/pull/1385)) - Plugin: 投胎模拟器 [@yanyongyu](https://github.com/yanyongyu) ([#1382](https://github.com/nonebot/nonebot2/pull/1382)) - Plugin: Apex API Query [@yanyongyu](https://github.com/yanyongyu) ([#1375](https://github.com/nonebot/nonebot2/pull/1375)) - Plugin: 随个人 [@yanyongyu](https://github.com/yanyongyu) ([#1373](https://github.com/nonebot/nonebot2/pull/1373)) - Plugin: 动漫资源获取 [@yanyongyu](https://github.com/yanyongyu) ([#1371](https://github.com/nonebot/nonebot2/pull/1371)) - Plugin: 日麻小工具 [@yanyongyu](https://github.com/yanyongyu) ([#1365](https://github.com/nonebot/nonebot2/pull/1365)) - Plugin: 图像超分辨率增强 [@yanyongyu](https://github.com/yanyongyu) ([#1362](https://github.com/nonebot/nonebot2/pull/1362)) - Plugin: 二次元化图像 [@yanyongyu](https://github.com/yanyongyu) ([#1360](https://github.com/nonebot/nonebot2/pull/1360)) - Plugin: 日麻寄分器 [@yanyongyu](https://github.com/yanyongyu) ([#1357](https://github.com/nonebot/nonebot2/pull/1357)) - Plugin: 文本生成器 [@yanyongyu](https://github.com/yanyongyu) ([#1355](https://github.com/nonebot/nonebot2/pull/1355)) - Plugin: 反嘴臭插件 [@yanyongyu](https://github.com/yanyongyu) ([#1350](https://github.com/nonebot/nonebot2/pull/1350)) - Plugin: 用户\&群聊黑名单 [@yanyongyu](https://github.com/yanyongyu) ([#1348](https://github.com/nonebot/nonebot2/pull/1348)) - Plugin: NoneBot SQLAlchemy 封装 [@yanyongyu](https://github.com/yanyongyu) ([#1345](https://github.com/nonebot/nonebot2/pull/1345)) - Plugin: 通用抽图/语音 [@yanyongyu](https://github.com/yanyongyu) ([#1341](https://github.com/nonebot/nonebot2/pull/1341)) - Plugin: kfcrazy [@yanyongyu](https://github.com/yanyongyu) ([#1339](https://github.com/nonebot/nonebot2/pull/1339)) - Plugin: 二次元图像鉴赏 [@yanyongyu](https://github.com/yanyongyu) ([#1337](https://github.com/nonebot/nonebot2/pull/1337)) - Plugin: ayaka 衍生插件 - 坏词撤回 [@yanyongyu](https://github.com/yanyongyu) ([#1335](https://github.com/nonebot/nonebot2/pull/1335)) - Plugin: ayaka 衍生插件 - 时区助手 [@yanyongyu](https://github.com/yanyongyu) ([#1332](https://github.com/nonebot/nonebot2/pull/1332)) - Plugin: ayaka 衍生插件 - 谁是卧底 [@yanyongyu](https://github.com/yanyongyu) ([#1330](https://github.com/nonebot/nonebot2/pull/1330)) - Plugin: ayaka 衍生插件 - 小游戏合集 [@yanyongyu](https://github.com/yanyongyu) ([#1328](https://github.com/nonebot/nonebot2/pull/1328)) - Plugin: bnhhsh -「不能好好说话!」 [@yanyongyu](https://github.com/yanyongyu) ([#1326](https://github.com/nonebot/nonebot2/pull/1326)) - Plugin: AI 绘图 [@yanyongyu](https://github.com/yanyongyu) ([#1323](https://github.com/nonebot/nonebot2/pull/1323)) - Plugin: novelai [@yanyongyu](https://github.com/yanyongyu) ([#1319](https://github.com/nonebot/nonebot2/pull/1319)) - Plugin: 游戏王小程序查价 [@yanyongyu](https://github.com/yanyongyu) ([#1317](https://github.com/nonebot/nonebot2/pull/1317)) - Plugin: 监测群事件 [@yanyongyu](https://github.com/yanyongyu) ([#1320](https://github.com/nonebot/nonebot2/pull/1320)) - Plugin: 轮盘禁言小游戏 [@yanyongyu](https://github.com/yanyongyu) ([#1311](https://github.com/nonebot/nonebot2/pull/1311)) - Plugin: 真白萌自动签到 [@yanyongyu](https://github.com/yanyongyu) ([#1308](https://github.com/nonebot/nonebot2/pull/1308)) - Plugin: BiliRequestAll [@yanyongyu](https://github.com/yanyongyu) ([#1302](https://github.com/nonebot/nonebot2/pull/1302)) - Plugin: 监听者 [@yanyongyu](https://github.com/yanyongyu) ([#1299](https://github.com/nonebot/nonebot2/pull/1299)) ### 🍻 机器人发布 - Bot: Bread Dog Bot [@yanyongyu](https://github.com/yanyongyu) ([#1380](https://github.com/nonebot/nonebot2/pull/1380)) - Bot: hsbot [@yanyongyu](https://github.com/yanyongyu) ([#1369](https://github.com/nonebot/nonebot2/pull/1369)) ### 🍻 适配器发布 - Adapter: Ntchat [@yanyongyu](https://github.com/yanyongyu) ([#1314](https://github.com/nonebot/nonebot2/pull/1314)) ## v2.0.0-rc.1 ### 💥 破坏性变更 - Feature: `SUPERUSER` 权限匹配任意超管事件 [@AkiraXie](https://github.com/AkiraXie) ([#1275](https://github.com/nonebot/nonebot2/pull/1275)) - Remove: 移除过时的 State 注入参数 [@yanyongyu](https://github.com/yanyongyu) ([#1160](https://github.com/nonebot/nonebot2/pull/1160)) - Remove: 移除过时的 `nonebot.plugins` toml 配置 [@yanyongyu](https://github.com/yanyongyu) ([#1151](https://github.com/nonebot/nonebot2/pull/1151)) - Remove: 移除 Python 3.7 支持 [@yanyongyu](https://github.com/yanyongyu) ([#1148](https://github.com/nonebot/nonebot2/pull/1148)) - Remove: 删除过时的 Export 功能 [@yanyongyu](https://github.com/yanyongyu) ([#1125](https://github.com/nonebot/nonebot2/pull/1125)) ### 🚀 新功能 - Feature: `SUPERUSER` 权限匹配任意超管事件 [@AkiraXie](https://github.com/AkiraXie) ([#1275](https://github.com/nonebot/nonebot2/pull/1275)) - Feature: 改进 `CommandGroup` 与 `MatcherGroup` 的结构 [@A-kirami](https://github.com/A-kirami) ([#1240](https://github.com/nonebot/nonebot2/pull/1240)) - Feature: 调整日志输出格式与等级 [@yanyongyu](https://github.com/yanyongyu) ([#1233](https://github.com/nonebot/nonebot2/pull/1233)) - Feature: 优化依赖注入结构 [@yanyongyu](https://github.com/yanyongyu) ([#1227](https://github.com/nonebot/nonebot2/pull/1227)) - Featue: `load_plugin` 支持 `pathlib.Path` [@Lancercmd](https://github.com/Lancercmd) ([#1194](https://github.com/nonebot/nonebot2/pull/1194)) - Feature: 新增事件类型过滤 rule [@yanyongyu](https://github.com/yanyongyu) ([#1183](https://github.com/nonebot/nonebot2/pull/1183)) - Feature: shell command 添加富文本支持 [@yanyongyu](https://github.com/yanyongyu) ([#1171](https://github.com/nonebot/nonebot2/pull/1171)) ### 🐛 Bug 修复 - Fix: 内置规则和权限没有捕获错误 [@yanyongyu](https://github.com/yanyongyu) ([#1291](https://github.com/nonebot/nonebot2/pull/1291)) - Fix: 修复 User 会话权限更新嵌套问题 [@yanyongyu](https://github.com/yanyongyu) ([#1208](https://github.com/nonebot/nonebot2/pull/1208)) - Fix: 修复当消息与不支持的类型相加时抛出的异常类型错误 [@mnixry](https://github.com/mnixry) ([#1166](https://github.com/nonebot/nonebot2/pull/1166)) ### 💫 杂项 - Fix: 修正 GenshinUID 的发布类型 [@A-kirami](https://github.com/A-kirami) ([#1243](https://github.com/nonebot/nonebot2/pull/1243)) - Remove: 移除未使用的导入 [@A-kirami](https://github.com/A-kirami) ([#1236](https://github.com/nonebot/nonebot2/pull/1236)) - Plugin: 更新插件米游社辅助工具 tag [@Ljzd-PRO](https://github.com/Ljzd-PRO) ([#1221](https://github.com/nonebot/nonebot2/pull/1221)) - Plugin: 修改插件多功能简易群管信息 [@HuYihe2008](https://github.com/HuYihe2008) ([#1180](https://github.com/nonebot/nonebot2/pull/1180)) - Plugin: 修改插件多功能简易群管信息 [@HuYihe2008](https://github.com/HuYihe2008) ([#1159](https://github.com/nonebot/nonebot2/pull/1159)) - Plugin: 修改 QQ 续火花插件信息 [@GC-ZF](https://github.com/GC-ZF) ([#1158](https://github.com/nonebot/nonebot2/pull/1158)) - Plugin: 修改插件多功能简易群管信息 [@HuYihe2008](https://github.com/HuYihe2008) ([#1154](https://github.com/nonebot/nonebot2/pull/1154)) ### 🍻 插件发布 - Plugin: 文字识别 [@yanyongyu](https://github.com/yanyongyu) ([#1295](https://github.com/nonebot/nonebot2/pull/1295)) - Plugin: 在线编曲 [@yanyongyu](https://github.com/yanyongyu) ([#1293](https://github.com/nonebot/nonebot2/pull/1293)) - Plugin: 图灵机器人 [@yanyongyu](https://github.com/yanyongyu) ([#1289](https://github.com/nonebot/nonebot2/pull/1289)) - Plugin: PicStatus [@yanyongyu](https://github.com/yanyongyu) ([#1287](https://github.com/nonebot/nonebot2/pull/1287)) - Plugin: 阿里云盘福利码自动兑换 [@yanyongyu](https://github.com/yanyongyu) ([#1283](https://github.com/nonebot/nonebot2/pull/1283)) - Plugin: gal 角色语音生成 [@yanyongyu](https://github.com/yanyongyu) ([#1281](https://github.com/nonebot/nonebot2/pull/1281)) - Plugin: 漂流瓶 [@yanyongyu](https://github.com/yanyongyu) ([#1279](https://github.com/nonebot/nonebot2/pull/1279)) - Plugin: BWIKI 助手移植版 [@yanyongyu](https://github.com/yanyongyu) ([#1274](https://github.com/nonebot/nonebot2/pull/1274)) - Plugin: nonebot 物联网插件 [@yanyongyu](https://github.com/yanyongyu) ([#1265](https://github.com/nonebot/nonebot2/pull/1265)) - Plugin: 狼人杀插件 [@yanyongyu](https://github.com/yanyongyu) ([#1252](https://github.com/nonebot/nonebot2/pull/1252)) - Plugin: ayaka - 文字游戏开发辅助插件 [@yanyongyu](https://github.com/yanyongyu) ([#1254](https://github.com/nonebot/nonebot2/pull/1254)) - Plugin: 图像超分辨率重建 [@yanyongyu](https://github.com/yanyongyu) ([#1250](https://github.com/nonebot/nonebot2/pull/1250)) - Plugin: Minecraft Server 聊天同步 [@yanyongyu](https://github.com/yanyongyu) ([#1245](https://github.com/nonebot/nonebot2/pull/1245)) - Plugin: 查询 ETH 合并日期 [@yanyongyu](https://github.com/yanyongyu) ([#1232](https://github.com/nonebot/nonebot2/pull/1232)) - Plugin: 星际战甲事件查询 [@yanyongyu](https://github.com/yanyongyu) ([#1220](https://github.com/nonebot/nonebot2/pull/1220)) - Plugin: 米游社辅助工具 [@yanyongyu](https://github.com/yanyongyu) ([#1218](https://github.com/nonebot/nonebot2/pull/1218)) - Plugin: 原神每日材料查询 [@yanyongyu](https://github.com/yanyongyu) ([#1216](https://github.com/nonebot/nonebot2/pull/1216)) - Plugin: MC_QQ_MCRcon [@yanyongyu](https://github.com/yanyongyu) ([#1211](https://github.com/nonebot/nonebot2/pull/1211)) - Plugin: 原神角色展柜查询 [@yanyongyu](https://github.com/yanyongyu) ([#1209](https://github.com/nonebot/nonebot2/pull/1209)) - Plugin: 修仙模拟器 [@yanyongyu](https://github.com/yanyongyu) ([#1202](https://github.com/nonebot/nonebot2/pull/1202)) - Plugin: 赛博浅草寺 [@yanyongyu](https://github.com/yanyongyu) ([#1206](https://github.com/nonebot/nonebot2/pull/1206)) - Plugin: 不背单词 [@yanyongyu](https://github.com/yanyongyu) ([#1204](https://github.com/nonebot/nonebot2/pull/1204)) - Plugin: 自识别 todo [@yanyongyu](https://github.com/yanyongyu) ([#1193](https://github.com/nonebot/nonebot2/pull/1193)) - Plugin: 雨课堂自动签到 [@yanyongyu](https://github.com/yanyongyu) ([#1189](https://github.com/nonebot/nonebot2/pull/1189)) - Plugin: 反馈及通知 [@yanyongyu](https://github.com/yanyongyu) ([#1187](https://github.com/nonebot/nonebot2/pull/1187)) - Plugin: MagiaDice 骰娘及 TRPGLOG [@yanyongyu](https://github.com/yanyongyu) ([#1185](https://github.com/nonebot/nonebot2/pull/1185)) - Plugin: 面麻小助手 [@yanyongyu](https://github.com/yanyongyu) ([#1191](https://github.com/nonebot/nonebot2/pull/1191)) - Plugin: 话痨排行榜 [@yanyongyu](https://github.com/yanyongyu) ([#1182](https://github.com/nonebot/nonebot2/pull/1182)) - Plugin: 保存群聊闪照 [@yanyongyu](https://github.com/yanyongyu) ([#1179](https://github.com/nonebot/nonebot2/pull/1179)) - Plugin: 课表查询 [@yanyongyu](https://github.com/yanyongyu) ([#1168](https://github.com/nonebot/nonebot2/pull/1168)) - Plugin: 业余无线电助手 [@yanyongyu](https://github.com/yanyongyu) ([#1173](https://github.com/nonebot/nonebot2/pull/1173)) - Plugin: NoneBot 树形帮助插件 [@yanyongyu](https://github.com/yanyongyu) ([#1177](https://github.com/nonebot/nonebot2/pull/1177)) - Plugin: 工作性价比 [@yanyongyu](https://github.com/yanyongyu) ([#1175](https://github.com/nonebot/nonebot2/pull/1175)) - Plugin: 娶群友 [@yanyongyu](https://github.com/yanyongyu) ([#1170](https://github.com/nonebot/nonebot2/pull/1170)) - Plugin: PixivBot [@yanyongyu](https://github.com/yanyongyu) ([#1165](https://github.com/nonebot/nonebot2/pull/1165)) - Plugin: 日韩中 VITS 模型原神拟声 [@yanyongyu](https://github.com/yanyongyu) ([#1162](https://github.com/nonebot/nonebot2/pull/1162)) - Plugin: 每日人品 [@yanyongyu](https://github.com/yanyongyu) ([#1156](https://github.com/nonebot/nonebot2/pull/1156)) - Plugin: nonebot-plugin-drawer [@yanyongyu](https://github.com/yanyongyu) ([#1146](https://github.com/nonebot/nonebot2/pull/1146)) - Plugin: 小游戏合集 [@yanyongyu](https://github.com/yanyongyu) ([#1150](https://github.com/nonebot/nonebot2/pull/1150)) - Plugin: 简易群管(带入群欢迎) [@yanyongyu](https://github.com/yanyongyu) ([#1142](https://github.com/nonebot/nonebot2/pull/1142)) - Plugin: wiki 条目搜索、获取简介 [@yanyongyu](https://github.com/yanyongyu) ([#1133](https://github.com/nonebot/nonebot2/pull/1133)) - Plugin: bangumi 搜索 [@yanyongyu](https://github.com/yanyongyu) ([#1137](https://github.com/nonebot/nonebot2/pull/1137)) - Plugin: 疫情小助手-频道版 [@yanyongyu](https://github.com/yanyongyu) ([#1131](https://github.com/nonebot/nonebot2/pull/1131)) - Plugin: MC_QQ 通信 [@yanyongyu](https://github.com/yanyongyu) ([#1127](https://github.com/nonebot/nonebot2/pull/1127)) - Plugin: BAWiki [@yanyongyu](https://github.com/yanyongyu) ([#1129](https://github.com/nonebot/nonebot2/pull/1129)) ### 🍻 机器人发布 - Bot: IdhagnBot [@yanyongyu](https://github.com/yanyongyu) ([#1267](https://github.com/nonebot/nonebot2/pull/1267)) - Bot: LittlePaimon [@yanyongyu](https://github.com/yanyongyu) ([#1256](https://github.com/nonebot/nonebot2/pull/1256)) - Bot: GenshinUID [@yanyongyu](https://github.com/yanyongyu) ([#1226](https://github.com/nonebot/nonebot2/pull/1226)) - Bot: 小白机器人 [@yanyongyu](https://github.com/yanyongyu) ([#1224](https://github.com/nonebot/nonebot2/pull/1224)) ### 🍻 适配器发布 - Adapter: GitHub [@yanyongyu](https://github.com/yanyongyu) ([#1297](https://github.com/nonebot/nonebot2/pull/1297)) - Adapter: Console [@yanyongyu](https://github.com/yanyongyu) ([#1213](https://github.com/nonebot/nonebot2/pull/1213)) ## v2.0.0-beta.5 ### 🚀 新功能 - Feature: on_x 支持 expire_time 参数 [@Dobiichi-Origami](https://github.com/Dobiichi-Origami) ([#1106](https://github.com/nonebot/nonebot2/pull/1106)) - Feature: 正向驱动器 startup/shutdown hook 支持同步函数 [@synodriver](https://github.com/synodriver) ([#1104](https://github.com/nonebot/nonebot2/pull/1104)) ### 🐛 Bug 修复 - Fix: 修复插件父子关系识别错漏 [@yanyongyu](https://github.com/yanyongyu) ([#1121](https://github.com/nonebot/nonebot2/pull/1121)) - Fix: run post hook 应该处理 matcher.state [@AkiraXie](https://github.com/AkiraXie) ([#1119](https://github.com/nonebot/nonebot2/pull/1119)) - Fix: 修复 setuptools 未安装导致 ImportError [@yanyongyu](https://github.com/yanyongyu) ([#1116](https://github.com/nonebot/nonebot2/pull/1116)) - Fix: 修复 typing 中 T_RunPostProcessor 类型的注释描述不正确 [@A-kirami](https://github.com/A-kirami) ([#1057](https://github.com/nonebot/nonebot2/pull/1057)) ### 📝 文档 - Docs: 添加 nonemoji 并更新开发指南 [@yanyongyu](https://github.com/yanyongyu) ([#1088](https://github.com/nonebot/nonebot2/pull/1088)) - Docs: 修复 event message 类型注释错误 [@yanyongyu](https://github.com/yanyongyu) ([#1079](https://github.com/nonebot/nonebot2/pull/1079)) - Docs: 修复旧 Vuepress 文档缓存问题 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#1077](https://github.com/nonebot/nonebot2/pull/1077)) - Docs: 更新 Readme 贡献图片 [@yanyongyu](https://github.com/yanyongyu) ([#1074](https://github.com/nonebot/nonebot2/pull/1074)) - Docs: 注销旧 Vuepress 文档的 Service Worker [@StarHeartHunt](https://github.com/StarHeartHunt) ([#1073](https://github.com/nonebot/nonebot2/pull/1073)) - Docs: 修改 `权限控制` 一节中主动调用的错误 [@MingxuanGame](https://github.com/MingxuanGame) ([#1072](https://github.com/nonebot/nonebot2/pull/1072)) ### 💫 杂项 - Bot: 修改剑网三 bot 信息 [@JustUndertaker](https://github.com/JustUndertaker) ([#1107](https://github.com/nonebot/nonebot2/pull/1107)) ### 🍻 插件发布 - Plugin: 「能不能好好说话?」缩写翻译 [@yanyongyu](https://github.com/yanyongyu) ([#1118](https://github.com/nonebot/nonebot2/pull/1118)) - Plugin: 推送钩子 [@yanyongyu](https://github.com/yanyongyu) ([#1115](https://github.com/nonebot/nonebot2/pull/1115)) - Plugin: 易命令 [@yanyongyu](https://github.com/yanyongyu) ([#1111](https://github.com/nonebot/nonebot2/pull/1111)) - Plugin: 群昵称时间 [@yanyongyu](https://github.com/yanyongyu) ([#1109](https://github.com/nonebot/nonebot2/pull/1109)) - Plugin: 处理好友添加和群邀请 [@yanyongyu](https://github.com/yanyongyu) ([#1099](https://github.com/nonebot/nonebot2/pull/1099)) - Plugin: 明日方舟寻访记录分析 [@yanyongyu](https://github.com/yanyongyu) ([#1097](https://github.com/nonebot/nonebot2/pull/1097)) - Plugin: b 站视频每日推送 [@yanyongyu](https://github.com/yanyongyu) ([#1095](https://github.com/nonebot/nonebot2/pull/1095)) - Plugin: 自动回复(文 i)插件 [@yanyongyu](https://github.com/yanyongyu) ([#1090](https://github.com/nonebot/nonebot2/pull/1090)) - Plugin: ACC 计算工具 [@yanyongyu](https://github.com/yanyongyu) ([#1093](https://github.com/nonebot/nonebot2/pull/1093)) - Plugin: OSU 查分插件 [@yanyongyu](https://github.com/yanyongyu) ([#1082](https://github.com/nonebot/nonebot2/pull/1082)) - Plugin: 战地 1、5 战绩查询工具 [@yanyongyu](https://github.com/yanyongyu) ([#1087](https://github.com/nonebot/nonebot2/pull/1087)) - Plugin: 一起燚 xN 吧 [@yanyongyu](https://github.com/yanyongyu) ([#1085](https://github.com/nonebot/nonebot2/pull/1085)) - Plugin: 米游币商品自动兑换 [@yanyongyu](https://github.com/yanyongyu) ([#1076](https://github.com/nonebot/nonebot2/pull/1076)) - Plugin: 赛马 [@yanyongyu](https://github.com/yanyongyu) ([#1069](https://github.com/nonebot/nonebot2/pull/1069)) - Plugin: PicMenu [@yanyongyu](https://github.com/yanyongyu) ([#1071](https://github.com/nonebot/nonebot2/pull/1071)) - Plugin: nonebot-plugin-bread [@yanyongyu](https://github.com/yanyongyu) ([#1064](https://github.com/nonebot/nonebot2/pull/1064)) - Plugin: 黑白名单 [@yanyongyu](https://github.com/yanyongyu) ([#1061](https://github.com/nonebot/nonebot2/pull/1061)) - Plugin: BitTorrent [@yanyongyu](https://github.com/yanyongyu) ([#1059](https://github.com/nonebot/nonebot2/pull/1059)) ### 🍻 机器人发布 - Bot: SkadiBot [@yanyongyu](https://github.com/yanyongyu) ([#1113](https://github.com/nonebot/nonebot2/pull/1113)) - Bot: 真宵 Bot [@yanyongyu](https://github.com/yanyongyu) ([#1103](https://github.com/nonebot/nonebot2/pull/1103)) ## v2.0.0-beta.4 ### 🚀 新功能 - Feature: 添加插件元信息定义 [@yanyongyu](https://github.com/yanyongyu) ([#1046](https://github.com/nonebot/nonebot2/pull/1046)) - Feature: 日志记录自动检测终端是否支持彩色 [@BlueGlassBlock](https://github.com/BlueGlassBlock) ([#1034](https://github.com/nonebot/nonebot2/pull/1034)) - Feature: 优化插件加载内部逻辑 [@yanyongyu](https://github.com/yanyongyu) ([#1011](https://github.com/nonebot/nonebot2/pull/1011)) ### 🐛 Bug 修复 - Fix: 修复 MessageSegment 在有额外数据时报错 [@yanyongyu](https://github.com/yanyongyu) ([#1055](https://github.com/nonebot/nonebot2/pull/1055)) - Fix: 修复环境变量无法覆盖 dotenv 内配置项值 [@yanyongyu](https://github.com/yanyongyu) ([#1052](https://github.com/nonebot/nonebot2/pull/1052)) - Fix: 修复依赖注入 bot event 参数 union 校验失败 [@yanyongyu](https://github.com/yanyongyu) ([#1001](https://github.com/nonebot/nonebot2/pull/1001)) ### 📝 文档 - Docs:添加文档排版规范 [@j1g5awi](https://github.com/j1g5awi) ([#1005](https://github.com/nonebot/nonebot2/pull/1005)) - Docs: 更新 require 样例 [@yanyongyu](https://github.com/yanyongyu) ([#996](https://github.com/nonebot/nonebot2/pull/996)) - Docs: 更新 README 中的 QQ 频道图标 [@mnixry](https://github.com/mnixry) ([#997](https://github.com/nonebot/nonebot2/pull/997)) - Docs: 调整跨插件访问文档 [@AkiraXie](https://github.com/AkiraXie) ([#993](https://github.com/nonebot/nonebot2/pull/993)) ### 🍻 插件发布 - Plugin: 历史上的今天 [@yanyongyu](https://github.com/yanyongyu) ([#1049](https://github.com/nonebot/nonebot2/pull/1049)) - Plugin: smart_reply [@yanyongyu](https://github.com/yanyongyu) ([#1054](https://github.com/nonebot/nonebot2/pull/1054)) - Plugin: nonebot_plugin_setu4 [@yanyongyu](https://github.com/yanyongyu) ([#1051](https://github.com/nonebot/nonebot2/pull/1051)) - Plugin: 命令重启机器人 [@yanyongyu](https://github.com/yanyongyu) ([#1038](https://github.com/nonebot/nonebot2/pull/1038)) - Plugin: 青年大学习自动提交 [@yanyongyu](https://github.com/yanyongyu) ([#1036](https://github.com/nonebot/nonebot2/pull/1036)) - Plugin: 疫情小助手 [@yanyongyu](https://github.com/yanyongyu) ([#1033](https://github.com/nonebot/nonebot2/pull/1033)) - Plugin: 谁艾特我了 [@yanyongyu](https://github.com/yanyongyu) ([#1031](https://github.com/nonebot/nonebot2/pull/1031)) - Plugin: Hikari-战舰世界水表查询 [@yanyongyu](https://github.com/yanyongyu) ([#1025](https://github.com/nonebot/nonebot2/pull/1025)) - Plugin: Warframe 时间查询 [@yanyongyu](https://github.com/yanyongyu) ([#1023](https://github.com/nonebot/nonebot2/pull/1023)) - Plugin: imagetools [@yanyongyu](https://github.com/yanyongyu) ([#1021](https://github.com/nonebot/nonebot2/pull/1021)) - Plugin: 明日方舟工具箱 [@yanyongyu](https://github.com/yanyongyu) ([#1019](https://github.com/nonebot/nonebot2/pull/1019)) - Plugin: B 站视频伪分享卡片 [@yanyongyu](https://github.com/yanyongyu) ([#1014](https://github.com/nonebot/nonebot2/pull/1014)) - Plugin: TETRIS Stats [@yanyongyu](https://github.com/yanyongyu) ([#1009](https://github.com/nonebot/nonebot2/pull/1009)) - Plugin: 签到插件 [@yanyongyu](https://github.com/yanyongyu) ([#1007](https://github.com/nonebot/nonebot2/pull/1007)) - Plugin: 数据库连接插件 [@yanyongyu](https://github.com/yanyongyu) ([#995](https://github.com/nonebot/nonebot2/pull/995)) - Plugin: 百度翻译 [@yanyongyu](https://github.com/yanyongyu) ([#992](https://github.com/nonebot/nonebot2/pull/992)) - Plugin: MockingBird 语音 [@yanyongyu](https://github.com/yanyongyu) ([#989](https://github.com/nonebot/nonebot2/pull/989)) ### 🍻 机器人发布 - Bot: nya_bot [@yanyongyu](https://github.com/yanyongyu) ([#1045](https://github.com/nonebot/nonebot2/pull/1045)) - Bot: LiteyukiBot-轻雪机器人 [@yanyongyu](https://github.com/yanyongyu) ([#1003](https://github.com/nonebot/nonebot2/pull/1003)) ### 🍻 适配器发布 - Adapter: OneBot V12 [@yanyongyu](https://github.com/yanyongyu) ([#1027](https://github.com/nonebot/nonebot2/pull/1027)) ## v2.0.0-beta.3 ### 💥 破坏性变更 - Fix: 添加 export 方法 Deprecation 警告 [@yanyongyu](https://github.com/yanyongyu) ([#983](https://github.com/nonebot/nonebot2/pull/983)) - Feature: 支持 WebSocket 连接同时获取 str 或 bytes [@yanyongyu](https://github.com/yanyongyu) ([#962](https://github.com/nonebot/nonebot2/pull/962)) ### 🚀 新功能 - Feature: 支持 WebSocket 连接同时获取 str 或 bytes [@yanyongyu](https://github.com/yanyongyu) ([#962](https://github.com/nonebot/nonebot2/pull/962)) - Feature: 添加 `CommandStart` 依赖注入参数 [@MeetWq](https://github.com/MeetWq) ([#915](https://github.com/nonebot/nonebot2/pull/915)) - Feature: 添加 Rule, Permission 反向位运算支持 [@yanyongyu](https://github.com/yanyongyu) ([#872](https://github.com/nonebot/nonebot2/pull/872)) - Feature: 新增文本完整匹配规则 [@A-kirami](https://github.com/A-kirami) ([#797](https://github.com/nonebot/nonebot2/pull/797)) ### 🐛 Bug 修复 - Fix: 修复依赖注入默认值参数在 `__eq__` 被重写时报错的问题 [@yanyongyu](https://github.com/yanyongyu) ([#971](https://github.com/nonebot/nonebot2/pull/971)) - Fix: 修复`MessageTemplate`在没有格式化说明符时行为不正确的问题 [@mnixry](https://github.com/mnixry) ([#947](https://github.com/nonebot/nonebot2/pull/947)) - Fix: Bot Hook 没有捕获跳过异常 [@yanyongyu](https://github.com/yanyongyu) ([#905](https://github.com/nonebot/nonebot2/pull/905)) - Fix: 修复部分事件响应器参数类型中冗余的 Optional [@A-kirami](https://github.com/A-kirami) ([#904](https://github.com/nonebot/nonebot2/pull/904)) - Fix: 修复 event 类型检查会对类型进行自动转换 [@yanyongyu](https://github.com/yanyongyu) ([#876](https://github.com/nonebot/nonebot2/pull/876)) - Fix: 修复 `on_fullmatch` 返回类型错误 [@yanyongyu](https://github.com/yanyongyu) ([#815](https://github.com/nonebot/nonebot2/pull/815)) - Fix: 修复 DataclassEncoder 嵌套 encode 的问题 [@AkiraXie](https://github.com/AkiraXie) ([#812](https://github.com/nonebot/nonebot2/pull/812)) ### 📝 文档 - Docs: 修复定时任务一节中的部分拼写错误 [@Nova-Noir](https://github.com/Nova-Noir) ([#982](https://github.com/nonebot/nonebot2/pull/982)) - Fix: 商店搜索失效 [@yanyongyu](https://github.com/yanyongyu) ([#978](https://github.com/nonebot/nonebot2/pull/978)) - Docs: 添加 QQ 频道链接 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#961](https://github.com/nonebot/nonebot2/pull/961)) - Docs: 添加 nonebug 单元测试文档 [@MingxuanGame](https://github.com/MingxuanGame) ([#929](https://github.com/nonebot/nonebot2/pull/929)) - Docs: 添加 pm2 部署文档 [@evlic](https://github.com/evlic) ([#853](https://github.com/nonebot/nonebot2/pull/853)) - Docs: 更新 GitHub Action 部署文档 [@kexue-z](https://github.com/kexue-z) ([#937](https://github.com/nonebot/nonebot2/pull/937)) - Docs: 添加自定义匹配规则文档 [@yanyongyu](https://github.com/yanyongyu) ([#914](https://github.com/nonebot/nonebot2/pull/914)) - Docs: 修复适配器文档内商店链接 [@yanyongyu](https://github.com/yanyongyu) ([#861](https://github.com/nonebot/nonebot2/pull/861)) - Docs: tips for finding adapters' document link [@StarHeartHunt](https://github.com/StarHeartHunt) ([#860](https://github.com/nonebot/nonebot2/pull/860)) - Docs: 添加对 `fastapi_reload` 在 Windows 平台额外影响的说明 [@CherryGS](https://github.com/CherryGS) ([#830](https://github.com/nonebot/nonebot2/pull/830)) - Docs: 修复 ci/cd action 中错误的版本号 [@Bubbleioa](https://github.com/Bubbleioa) ([#819](https://github.com/nonebot/nonebot2/pull/819)) - Docs: 减小更新日志 toc 最大显示等级 [@yanyongyu](https://github.com/yanyongyu) ([#813](https://github.com/nonebot/nonebot2/pull/813)) - Docs: 修改议题模板中的错误链接 [@he0119](https://github.com/he0119) ([#807](https://github.com/nonebot/nonebot2/pull/807)) - Docs: 修改消息模板文档中错误的样例 [@mnixry](https://github.com/mnixry) ([#806](https://github.com/nonebot/nonebot2/pull/806)) - Docs: 更新贡献指南 [@yanyongyu](https://github.com/yanyongyu) ([#798](https://github.com/nonebot/nonebot2/pull/798)) ### 💫 杂项 - Plugin: nonebot-plugin-chess 改名为 nonebot-plugin-boardgame [@MeetWq](https://github.com/MeetWq) ([#953](https://github.com/nonebot/nonebot2/pull/953)) - Plugin: 网易云无损音乐下载更改 [@kitUIN](https://github.com/kitUIN) ([#924](https://github.com/nonebot/nonebot2/pull/924)) - Docs: 移除商店中的过期插件 [@j1g5awi](https://github.com/j1g5awi) ([#902](https://github.com/nonebot/nonebot2/pull/902)) - CI: 修复发布机器人的意外错误 [@he0119](https://github.com/he0119) ([#892](https://github.com/nonebot/nonebot2/pull/892)) - Docs: 替换和移除部分已经失效的插件 [@MeetWq](https://github.com/MeetWq) ([#879](https://github.com/nonebot/nonebot2/pull/879)) - Docs: 添加 netlify 标签 [@yanyongyu](https://github.com/yanyongyu) ([#816](https://github.com/nonebot/nonebot2/pull/816)) - Fix: 修改错误的插件 PyPI 项目名称 [@Lancercmd](https://github.com/Lancercmd) ([#804](https://github.com/nonebot/nonebot2/pull/804)) - CI: 添加更新日志自动更新 action [@yanyongyu](https://github.com/yanyongyu) ([#799](https://github.com/nonebot/nonebot2/pull/799)) ### 🍻 插件发布 - Plugin: imageutils [@yanyongyu](https://github.com/yanyongyu) ([#985](https://github.com/nonebot/nonebot2/pull/985)) - Plugin: 摸鱼日历 [@yanyongyu](https://github.com/yanyongyu) ([#980](https://github.com/nonebot/nonebot2/pull/980)) - Plugin: 走迷宫 [@yanyongyu](https://github.com/yanyongyu) ([#977](https://github.com/nonebot/nonebot2/pull/977)) - Plugin: 语录娱乐 [@yanyongyu](https://github.com/yanyongyu) ([#973](https://github.com/nonebot/nonebot2/pull/973)) - Plugin: 国内新冠疫情数据查询 [@yanyongyu](https://github.com/yanyongyu) ([#975](https://github.com/nonebot/nonebot2/pull/975)) - Plugin: nonebot_plugin_eventdone [@yanyongyu](https://github.com/yanyongyu) ([#966](https://github.com/nonebot/nonebot2/pull/966)) - Plugin: 幻影坦克图片合成 [@yanyongyu](https://github.com/yanyongyu) ([#968](https://github.com/nonebot/nonebot2/pull/968)) - Plugin: 合成字符画(GIF) [@yanyongyu](https://github.com/yanyongyu) ([#964](https://github.com/nonebot/nonebot2/pull/964)) - Plugin: 国际象棋 [@yanyongyu](https://github.com/yanyongyu) ([#957](https://github.com/nonebot/nonebot2/pull/957)) - Plugin: NoneBot2 文档搜索 [@yanyongyu](https://github.com/yanyongyu) ([#952](https://github.com/nonebot/nonebot2/pull/952)) - Plugin: 中国象棋 [@yanyongyu](https://github.com/yanyongyu) ([#949](https://github.com/nonebot/nonebot2/pull/949)) - Plugin: B 站视频封面提取 [@yanyongyu](https://github.com/yanyongyu) ([#946](https://github.com/nonebot/nonebot2/pull/946)) - Plugin: 一言 [@yanyongyu](https://github.com/yanyongyu) ([#944](https://github.com/nonebot/nonebot2/pull/944)) - Plugin: 答案之书 [@yanyongyu](https://github.com/yanyongyu) ([#942](https://github.com/nonebot/nonebot2/pull/942)) - Plugin: 支付宝到账语音 [@yanyongyu](https://github.com/yanyongyu) ([#940](https://github.com/nonebot/nonebot2/pull/940)) - Plugin: nonebot-plugin-dida [@yanyongyu](https://github.com/yanyongyu) ([#934](https://github.com/nonebot/nonebot2/pull/934)) - Plugin: 随机唐可可 [@yanyongyu](https://github.com/yanyongyu) ([#931](https://github.com/nonebot/nonebot2/pull/931)) - Plugin: splatoon2 新闻 [@yanyongyu](https://github.com/yanyongyu) ([#917](https://github.com/nonebot/nonebot2/pull/917)) - Plugin: nonebot_plugin_draw [@yanyongyu](https://github.com/yanyongyu) ([#910](https://github.com/nonebot/nonebot2/pull/910)) - Plugin: 扫雷游戏 [@yanyongyu](https://github.com/yanyongyu) ([#907](https://github.com/nonebot/nonebot2/pull/907)) - Plugin: 汉兜 Handle [@yanyongyu](https://github.com/yanyongyu) ([#899](https://github.com/nonebot/nonebot2/pull/899)) - Plugin: 多适配器帮助函数 [@yanyongyu](https://github.com/yanyongyu) ([#897](https://github.com/nonebot/nonebot2/pull/897)) - Plugin: 语句抽象化 [@yanyongyu](https://github.com/yanyongyu) ([#894](https://github.com/nonebot/nonebot2/pull/894)) - Plugin: 快速搜索 [@yanyongyu](https://github.com/yanyongyu) ([#889](https://github.com/nonebot/nonebot2/pull/889)) - Plugin: wordle 猜单词 [@yanyongyu](https://github.com/yanyongyu) ([#891](https://github.com/nonebot/nonebot2/pull/891)) - Plugin: MediaWiki 查询 [@yanyongyu](https://github.com/yanyongyu) ([#886](https://github.com/nonebot/nonebot2/pull/886)) - Plugin: HikariSearch [@yanyongyu](https://github.com/yanyongyu) ([#884](https://github.com/nonebot/nonebot2/pull/884)) - Plugin: 第二个 leetcode 查询插件 [@yanyongyu](https://github.com/yanyongyu) ([#882](https://github.com/nonebot/nonebot2/pull/882)) - Plugin: 成分姬 [@yanyongyu](https://github.com/yanyongyu) ([#878](https://github.com/nonebot/nonebot2/pull/878)) - Plugin: Arcaea 查分插件 [@yanyongyu](https://github.com/yanyongyu) ([#875](https://github.com/nonebot/nonebot2/pull/875)) - Plugin: QQ 自动同意好友申请 [@yanyongyu](https://github.com/yanyongyu) ([#871](https://github.com/nonebot/nonebot2/pull/871)) - Plugin: 21 点游戏插件 [@yanyongyu](https://github.com/yanyongyu) ([#865](https://github.com/nonebot/nonebot2/pull/865)) - Plugin: 色图生成 [@yanyongyu](https://github.com/yanyongyu) ([#863](https://github.com/nonebot/nonebot2/pull/863)) - Plugin: bilibili 通知插件 [@yanyongyu](https://github.com/yanyongyu) ([#859](https://github.com/nonebot/nonebot2/pull/859)) - Plugin: 订阅推送管理 [@yanyongyu](https://github.com/yanyongyu) ([#855](https://github.com/nonebot/nonebot2/pull/855)) - Plugin: 动漫新闻 [@yanyongyu](https://github.com/yanyongyu) ([#852](https://github.com/nonebot/nonebot2/pull/852)) - Plugin: 游戏王卡查 [@yanyongyu](https://github.com/yanyongyu) ([#846](https://github.com/nonebot/nonebot2/pull/846)) - Plugin: 二维码识别与发送 [@yanyongyu](https://github.com/yanyongyu) ([#843](https://github.com/nonebot/nonebot2/pull/843)) - Plugin: mockingbird [@yanyongyu](https://github.com/yanyongyu) ([#841](https://github.com/nonebot/nonebot2/pull/841)) - Plugin: QQ 自动续火花 [@yanyongyu](https://github.com/yanyongyu) ([#839](https://github.com/nonebot/nonebot2/pull/839)) - Plugin: 每日一句 [@yanyongyu](https://github.com/yanyongyu) ([#832](https://github.com/nonebot/nonebot2/pull/832)) - Plugin: 原神抽卡记录分析 [@yanyongyu](https://github.com/yanyongyu) ([#829](https://github.com/nonebot/nonebot2/pull/829)) - Plugin: YetAnotherPicSearch [@yanyongyu](https://github.com/yanyongyu) ([#825](https://github.com/nonebot/nonebot2/pull/825)) - Plugin: 60s 读世界小插件 [@yanyongyu](https://github.com/yanyongyu) ([#810](https://github.com/nonebot/nonebot2/pull/810)) - Plugin: pixiv.net p 站查询图片 [@yanyongyu](https://github.com/yanyongyu) ([#803](https://github.com/nonebot/nonebot2/pull/803)) ### 🍻 机器人发布 - Bot: 屑岛风 Bot [@yanyongyu](https://github.com/yanyongyu) ([#987](https://github.com/nonebot/nonebot2/pull/987)) - Bot: ShigureBot [@yanyongyu](https://github.com/yanyongyu) ([#959](https://github.com/nonebot/nonebot2/pull/959)) - Bot: Inkar Suki [@yanyongyu](https://github.com/yanyongyu) ([#955](https://github.com/nonebot/nonebot2/pull/955)) ## v2.0.0-beta.2 - 修复 `receive`, `got` 在参数为空消息时依旧会反复询问 - 修复文档商店分页显示错误 - 修复插件导入失败时,依然存在于已导入插件列表中 - 移除 `state` 依赖注入所需的默认值 `State()` - 增加 `fastapi` 配置项:是否将适配器路由包含在 schema 中 - 修改 `load_builtin_plugins` 函数,使其能够支持加载多个内置插件 - 新增 `load_builtin_plugin` 函数,用于加载单个内置插件 - 修改 `Message` 和 `MessageSegment` 类,完善 typing,转移 Mapping 构建支持至 pydantic validate - 调整项目结构,分离内部定义与用户接口 - 新增 Bot 连接事件钩子 (如 `driver.on_bot_connect` ) 的依赖注入 ## v2.0.0-beta.1 - 新增 `MessageTemplate` 对于 `str` 普通模板的支持 - 移除插件加载的 `NameSpace` 模式 - 修改 toml 加载插件时的键名为 `tool.nonebot` 以符合规范 - 新增 Handler 依赖注入支持,同步/异步支持 - 统一 `Processor`, `Rule`, `Permission`, `Processor` 使用 `Handler` - 修改内置 `Rule`, `Permission` 如 `startswith`, `command` 等使用 class 实现 - 更换文档框架 (docusaurus) 以及主题 (docusaurus-theme-nonepress) - 移除 Matcher `state_factory` 支持 ## v2.0.0a16 - 新增 `MessageTemplate` 可用于 `Message` 的模板生成 - 新增 `matcher.got` `matcher.send` `matcher.pause` `matcher.reject` `matcher.finish` 支持 `MessageTemplate` - 移除 `matcher.got` 原本的 `state format` 支持,由 `MessageTemplate` template 替代 - `adapter` 基类拆分为单独文件 - 修复 `fastapi` Driver Websocket 未能正确提供请求头部 - 新增 `fastapi` Driver 更多的 uvicorn 相关配置项 - 新增 `quart` Driver 更多的 uvicorn 相关配置项 - 修复 `endswith` Rule 错误的正则匹配 - 修复 `cqhttp` Adapter `image`, `record`, `video` 对 `BytesIO` 不正常的读取操作 ## v2.0.0a15 - 修复 `fastapi` Driver 未能正确进行 reconnect - 修复 `MessageSegment` 错误的 Mapping 映射 ## v2.0.0a14 - 修改日志等级,支持输出等级自定义 - 修复日志输出模块名错误 - 修改 `Matcher` 属性 `module` 类型 - 新增 `Matcher` 属性 `plugin_name` `module_name` `module_prefix` - 移除 `bot.call_api` 参数 `self_id` 切换机器人支持 - 修复 `type_updater` `permission_updater` 未传递的错误 - 修复 `type_updater` `permission_updater` 参数 `state` 错误 - 修复使用 `state_factory` 后导致无法在 session 内传递 `state` - 重构 `Driver` 及连接信息抽象 - 新增正向 Driver(Client) 支持 - 新增 `aiohttp` 正向 Driver - `fastapi` Driver 新增正向支持 ## v2.0.0a13.post1 - 分离 `handler` 与 `matcher` - 修复 `cqhttp` secret 校验出错 - 修复 `pydantic 1.8` 导致的 `alias` 问题 - 修改 `cqhttp` `ding` `session id`,不再允许跨群 - 修改 `shell_command` 存储 message - 修复 `cqhttp` 检查 reply 失败退出 - 新增 `call_api` hook 接口 - 优化 `import hook` ## v2.0.0a11 - 修改 `nonebot` 项目结构,分离所有 `adapter` - 修改插件加载逻辑,使用 `import hook` (PEP 302) - 新增插件加载方式: `json`, `toml` - 适配 `pydantic` ~1.8 - 移除 4 种内置事件类型限制,允许自定义事件类型 - 新增会话权限更新自定义,会话中断时更新权限以做到多人会话 ## v2.0.0a10 - 新增 `Quart Driver` 支持 - 修复 `mirai` 协议适配命令处理以及消息转义 ## v2.0.0a9 - 修复 `Message` 消息为 `None` 时的处理错误 - 修复 `Message.extract_plain_text` 返回为转义字符串的问题 - 修复命令处理错误地删除了后续空格 - 增加好友添加和加群请求事件 `approve`, `reject` 方法 - 新增 `mirai-api-http` 协议适配 - 修复 rule 运行时 state 覆盖问题,隔离 state - 新增 `shell like command` 支持 ## v2.0.0a8 - 修改 typing 类型注释 - 修改 event 基类接口 - 修复部分非法 CQ 码被识别导致报错 - 修复非 text 类型 CQ 码 data 未进行去转义 - 修复内置插件未进行去转义,修改内置插件为 cqhttp 定制 - 修复 `load_plugins` 加载不合法的包时出现 `spec` 为 `None` 的问题 - 出于**CQ 码安全性考虑**,使用 cqhttp 的 `bot.send` 或者 `matcher.send` 时默认对字符串进行转义 - 移动 cqhttp 相关 `Permission` 至 `nonebot.adapters.cqhttp` 包内 ## v2.0.0a7 - 修复 cqhttp 检查 to me 时出现 IndexError - 修复已失效的事件响应器仍会运行一次的 bug - 修改 cqhttp 检查 reply 时未去除后续 at 以及空格 - 添加 get_plugin 获取插件函数 - 添加插件 export, require 方法 - **移除**内置 apscheduler 定时任务支持 - **移除**内置协议适配默认加载 - 新增**钉钉**协议适配 - 移除原有共享型 `MatcherGroup` 改为默认型 `MatcherGroup` ## v2.0.0a6 - 修复 block 失效问题 (hotfix) ## v2.0.0a5 - 更新插件指南文档 - 修复临时事件响应器运行后删除造成的多次响应问题 ================================================ FILE: website/src/components/Asciinema/container.tsx ================================================ import React, { useEffect, useRef } from "react"; import * as AsciinemaPlayer from "asciinema-player"; export type AsciinemaOptions = { cols: number; rows: number; autoPlay: boolean; preload: boolean; loop: boolean; startAt: number | string; speed: number; idleTimeLimit: number; theme: string; poster: string; fit: string; fontSize: string; }; export type Props = { url: string; options?: Partial; }; export default function AsciinemaContainer({ url, options = {}, }: Props): React.ReactNode { const ref = useRef(null); useEffect(() => { AsciinemaPlayer.create(url, ref.current, options); }, [url, options]); return
; } ================================================ FILE: website/src/components/Asciinema/index.tsx ================================================ import React from "react"; import BrowserOnly from "@docusaurus/BrowserOnly"; import "asciinema-player/dist/bundle/asciinema-player.css"; import type { Props } from "./container"; import "./styles.css"; export type { Props } from "./container"; export default function Asciinema(props: Props): React.ReactNode { return ( Asciinema cast } > {() => { // eslint-disable-next-line @typescript-eslint/no-var-requires const AsciinemaContainer = require("./container.tsx").default; return ; }} ); } ================================================ FILE: website/src/components/Asciinema/styles.css ================================================ .ap-player svg { @apply inline-block; } .ap-container { @apply w-full my-4; } ================================================ FILE: website/src/components/Form/Adapter.tsx ================================================ import React from "react"; import { Form } from "."; export default function AdapterForm(): React.ReactNode { const formItems = [ { name: "基本信息", items: [ { type: "text", name: "name", labelText: "适配器名称", }, { type: "text", name: "description", labelText: "适配器描述" }, { type: "text", name: "homepage", labelText: "适配器项目仓库/主页链接", }, ], }, { name: "包信息", items: [ { type: "text", name: "pypi", labelText: "PyPI 项目名" }, { type: "text", name: "module", labelText: "适配器 import 包名" }, ], }, { name: "其他信息", items: [{ type: "tag", name: "tags", labelText: "标签" }], }, ]; const handleSubmit = (result: Record) => { window.open( `https://github.com/nonebot/nonebot2/issues/new?${new URLSearchParams({ template: "adapter_publish.yml", title: `Adapter: ${result.name}`, ...result, })}` ); }; return (
); } ================================================ FILE: website/src/components/Form/Bot.tsx ================================================ import React from "react"; import { Form } from "."; export default function BotForm(): React.ReactNode { const formItems = [ { name: "基本信息", items: [ { type: "text", name: "name", labelText: "机器人名称", }, { type: "text", name: "description", labelText: "机器人描述" }, { type: "text", name: "homepage", labelText: "机器人项目仓库/主页链接", }, ], }, { name: "其他信息", items: [{ type: "tag", name: "tags", labelText: "标签" }], }, ]; const handleSubmit = (result: Record) => { window.open( `https://github.com/nonebot/nonebot2/issues/new?${new URLSearchParams({ template: "bot_publish.yml", title: `Bot: ${result.name}`, ...result, })}` ); }; return ; } ================================================ FILE: website/src/components/Form/Items/Tag/index.tsx ================================================ import React, { useState } from "react"; import clsx from "clsx"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { ChromePicker, type ColorResult } from "react-color"; import "./styles.css"; import TagComponent from "@/components/Tag"; import type { Tag as TagType } from "@/types/tag"; export type Props = { allowTags: TagType[]; onTagUpdate: (tags: TagType[]) => void; }; export default function TagFormItem({ allowTags, onTagUpdate, }: Props): React.ReactNode { const [tags, setTags] = useState([]); const [label, setLabel] = useState(""); const [color, setColor] = useState("#ea5252"); const slicedTags = Array.from( new Set( allowTags .filter((tag) => tag.label.toLocaleLowerCase().includes(label)) .map((e) => e.label) ) ).slice(0, 5); const validateTag = () => { return label.length >= 1 && label.length <= 10; }; const newTag = () => { if (tags.length >= 3) { return; } if (validateTag()) { const tag: TagType = { label, color }; const newTags = [...tags, tag]; setTags(newTags); onTagUpdate(newTags); } }; const delTag = (index: number) => { const newTags = tags.filter((_, i) => i !== index); setTags(newTags); onTagUpdate(newTags); }; const onChangeColor = (color: ColorResult) => { setColor(color.hex as TagType["color"]); }; return ( <>
标签名称
setLabel(e.target.value)} /> {slicedTags.length > 0 && ( )}
标签颜色
); } ================================================ FILE: website/src/components/Form/Items/Tag/styles.css ================================================ .add-btn { @apply px-2 select-none cursor-pointer min-w-[64px] rounded-full hover:bg-opacity-[.08]; @apply flex justify-center items-center border-dashed border-2; &-disabled { @apply pointer-events-none opacity-60; } } .form-item { @apply basis-3/4; &-title { @apply basis-1/4 label-text; } &-input { @apply input input-sm input-bordered; } &-select { @apply select select-sm select-bordered; } &-container { @apply flex items-center mt-2; } } .fix-input-color { @apply !text-base-content !bg-base-100; input { @apply !text-base-content !bg-base-100; } } ================================================ FILE: website/src/components/Form/Plugin.tsx ================================================ import React from "react"; import Link from "@docusaurus/Link"; import { Form } from "."; export default function PluginForm(): React.ReactNode { const formItems = [ { name: "包信息", items: [ { type: "text", name: "pypi", labelText: "PyPI 项目名" }, { type: "text", name: "module", labelText: "插件模块名" }, ], }, { name: "其他信息", items: [{ type: "tag", name: "tags", labelText: "标签" }], }, ]; const handleSubmit = (result: Record) => { window.open( `https://github.com/nonebot/nonebot2/issues/new?${new URLSearchParams({ template: "plugin_publish.yml", title: `Plugin: ${result.pypi}`, ...result, })}` ); }; const description = (

请在发布前阅读{" "} NoneBot 插件发布流程指导 ,并确保满足其中所述条件。

); return ( ); } ================================================ FILE: website/src/components/Form/index.tsx ================================================ import React, { useEffect, useState } from "react"; import clsx from "clsx"; import "./styles.css"; import type { Resource } from "@/libs/store"; import { fetchRegistryData } from "@/libs/store"; import TagFormItem from "./Items/Tag"; import type { Tag as TagType } from "@/types/tag"; export type FormItemData = { type: string; name: string; labelText: string; }; export type FormItemGroup = { name: string; items: FormItemData[]; }; export type Props = { children?: React.ReactNode; description?: React.ReactNode; type: Resource["resourceType"]; formItems: FormItemGroup[]; handleSubmit: (result: Record) => void; }; export function Form({ type, children, description, formItems, handleSubmit, }: Props): React.ReactNode { const [currentStep, setCurrentStep] = useState(0); const [result, setResult] = useState>({}); const [allowTags, setAllowTags] = useState([]); // load tags asynchronously useEffect(() => { fetchRegistryData(type) .then((data) => setAllowTags( data .filter((item) => item.tags.length > 0) .map((ele) => ele.tags) .flat() ) ) .catch((e) => { console.error(e); }); }, [type]); const setFormValue = (key: string, value: string) => { setResult({ ...result, [key]: value }); }; const handleNextStep = () => { const currentStepNames = formItems[currentStep].items.map( (item) => item.name ); if (currentStepNames.every((name) => result[name])) { setCurrentStep(currentStep + 1); } }; const onPrev = () => currentStep > 0 && setCurrentStep(currentStep - 1); const onNext = () => currentStep < formItems.length - 1 ? handleNextStep() : handleSubmit(result); return ( <>
    {formItems.map((item, index) => (
  • {item.name}
  • ))}
{description && currentStep === 0 && (
{description}
)}
{children || formItems[currentStep].items.map((item) => ( ))}
); } export function FormItem({ type, name, labelText, allowTags, result, setResult, }: FormItemData & { allowTags: TagType[]; result: Record; setResult: (key: string, value: string) => void; }): React.ReactNode { return ( <> {type === "text" && ( setResult(name, e.target.value)} placeholder="请输入" className={clsx("form-input", { "form-input-error": !result[name], })} /> )} {type === "text" && !result[name] && ( )} {type === "tag" && ( setResult(name, JSON.stringify(tags))} /> )} ); } ================================================ FILE: website/src/components/Form/styles.css ================================================ .form-btn { @apply btn btn-sm btn-primary no-animation; &-prev { @apply mr-auto; } &-next { @apply ml-auto; } &-hidden { @apply !hidden; } } .form-input { @apply input input-bordered w-full; &-error { @apply input-error; } } .form-label { @apply text-xs; &-error { @apply text-error; } } ================================================ FILE: website/src/components/Home/Feature.tsx ================================================ import React from "react"; import CodeBlock from "@theme/CodeBlock"; export type Feature = { title: string; tagline?: string; description?: string; annotaion?: string; children?: React.ReactNode; }; export function HomeFeature({ title, tagline, description, annotaion, children, }: Feature): React.ReactNode { return (

{tagline}

{title}

{description}

{children}

{annotaion}

); } function HomeFeatureSingleColumn(props: Feature): React.ReactNode { return (
); } function HomeFeatureDoubleColumn({ features: [feature1, feature2], children, }: { features: [Feature, Feature]; children?: [React.ReactNode, React.ReactNode]; }): React.ReactNode { const [children1, children2] = children ?? []; return (
{children1} {children2}
); } function HomeFeatures(): React.ReactNode { return ( <> {[ "$ pipx install nb-cli", "$ nb", // "d8b db .d88b. d8b db d88888b d8888b. .d88b. d888888b", // "888o 88 .8P Y8. 888o 88 88' 88 `8D .8P Y8. `~~88~~'", // "88V8o 88 88 88 88V8o 88 88ooooo 88oooY' 88 88 88", // "88 V8o88 88 88 88 V8o88 88~~~~~ 88~~~b. 88 88 88", // "88 V888 `8b d8' 88 V888 88. 88 8D `8b d8' 88", // "VP V8P `Y88P' VP V8P Y88888P Y8888P' `Y88P' YP", "[?] What do you want to do?", "❯ Create a NoneBot project.", " Run the bot in current folder.", " Manage bot driver.", " Manage bot adapters.", " Manage bot plugins.", " ...", ].join("\n")} {[ "import nonebot", "# 加载一个插件", 'nonebot.load_plugin("path.to.your.plugin")', "# 从文件夹加载插件", 'nonebot.load_plugins("plugins")', "# 从配置文件加载多个插件", 'nonebot.load_from_json("plugins.json")', 'nonebot.load_from_toml("pyproject.toml")', ].join("\n")} {[ "import nonebot", "# OneBot", "from nonebot.adapters.onebot.v11 import Adapter as OneBotAdapter", "# QQ 机器人", "from nonebot.adapters.qq import Adapter as QQAdapter", "driver = nonebot.get_driver()", "driver.register_adapter(OneBotAdapter)", "driver.register_adapter(QQAdapter)", ].join("\n")} {[ "from nonebot import on_message", "# 注册一个消息响应器", "matcher = on_message()", "# 注册一个消息处理器", "# 并重复收到的消息", "@matcher.handle()", "async def handler(event: Event) -> None:", " await matcher.send(event.get_message())", ].join("\n")} {[ "from nonebot import on_command", "# 注册一个命令响应器", 'matcher = on_command("help", alias={"帮助"})', "# 注册一个命令处理器", "# 通过依赖注入获得命令名以及参数", "@matcher.handle()", "async def handler(cmd = Command(), arg = CommandArg()) -> None:", " await matcher.finish()", ].join("\n")} ); } export default React.memo(HomeFeatures); ================================================ FILE: website/src/components/Home/Hero.tsx ================================================ import React, { useCallback, useEffect, useRef, useState } from "react"; import Link from "@docusaurus/Link"; import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useNonepressThemeConfig } from "@nullbot/docusaurus-theme-nonepress/client"; // @ts-expect-error: we need to make package have type: module import copy from "copy-text-to-clipboard"; import IconCopy from "@theme/Icon/Copy"; import IconSuccess from "@theme/Icon/Success"; function HomeHeroInstallButton(): React.ReactNode { const code = "pipx run nb-cli create"; const [isCopied, setIsCopied] = useState(false); const copyTimeout = useRef(undefined); const handleCopyCode = useCallback(() => { copy(code); setIsCopied(true); copyTimeout.current = window.setTimeout(() => { setIsCopied(false); }, 1500); }, [code]); useEffect(() => () => window.clearTimeout(copyTimeout.current), []); return ( ); } function HomeHero(): React.ReactNode { const { siteConfig: { tagline }, } = useDocusaurusContext(); const { navbar: { logo }, } = useNonepressThemeConfig(); return (
uwu {logo!.alt}

None Bot

{tagline}

开始使用
); } export default React.memo(HomeHero); ================================================ FILE: website/src/components/Home/index.tsx ================================================ import React from "react"; import HomeFeatures from "./Feature"; import HomeHero from "./Hero"; import "./styles.css"; export default function HomeContent(): React.ReactNode { return (
); } ================================================ FILE: website/src/components/Home/styles.css ================================================ .home { &-container { @apply -mt-16; } &-hero { @apply relative flex flex-col items-center justify-center gap-4 h-screen; &-logo { @apply h-48 w-auto; } &-title { @apply text-5xl font-normal tracking-tight; } &-tagline { @apply text-sm font-medium uppercase tracking-wide text-base-content/70; } &-actions { @apply flex flex-col sm:flex-row gap-4; } &-copy { @apply font-normal normal-case text-base-content/70; } &-next { @apply absolute bottom-4; & svg { @apply animate-bounce text-primary text-4xl; } } } &-codeblock { @apply inline-block !max-w-[600px]; } } .home-hero-uwu { @apply hidden; } [data-uwu="true"] .home-hero-uwu { @apply block max-w-xs; } [data-uwu="true"] .home-hero-logo, [data-uwu="true"] .home-hero-title { @apply hidden; } ================================================ FILE: website/src/components/Messenger/index.tsx ================================================ import React from "react"; import clsx from "clsx"; import useBaseUrl from "@docusaurus/useBaseUrl"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useNonepressThemeConfig } from "@nullbot/docusaurus-theme-nonepress/client"; import ThemedImage from "@theme/ThemedImage"; import "./styles.css"; export type Message = { msg: string; position?: "left" | "right"; monospace?: boolean; }; function MessageBox({ msg, position = "left", monospace = false, }: Message): React.ReactNode { const { navbar: { logo }, } = useNonepressThemeConfig(); const sources = { light: useBaseUrl(logo!.src), dark: useBaseUrl(logo!.srcDark || logo!.src), }; const isRight = position === "right"; return (
{isRight ? ( ) : ( )}
").replace(/ /g, " "), }} />
); } export default function Messenger({ msgs = [], }: { msgs?: Message[]; }): React.ReactNode { return (
NoneBot
{msgs.map((msg, i) => ( ))}
); } ================================================ FILE: website/src/components/Messenger/styles.css ================================================ .messenger { &-container { @apply block w-full my-4 overflow-hidden; @apply rounded-lg outline-none bg-base-200; @apply transition-[background-color] duration-500; } &-title { @apply flex items-center h-12 px-4 bg-info text-white; &-back { @apply text-left text-base grow; } &-name { @apply flex-initial grow-0 text-xl font-bold; } &-more { @apply text-right text-base grow; } } &-chat { @apply p-4 min-h-[150px]; &-avatar { @apply !flex items-center justify-center; @apply w-10 rounded-full; &-user { @apply bg-info text-white; } } &-bubble { @apply bg-base-100 text-base-content [word-break:break-word]; @apply transition-[color,background-color] duration-500; } } &-footer { @apply px-4; &-action { @apply flex items-center gap-2; &-input { @apply flex-1; & > input { @apply transition-[color,background-color] duration-500; } } &-send { @apply flex-initial w-fit; } } &-tools { @apply grid grid-cols-6 items-center py-1; @apply text-center text-base text-base-content/60; } } } ================================================ FILE: website/src/components/Modal/index.tsx ================================================ import React, { useEffect, useState } from "react"; import clsx from "clsx"; import IconClose from "@theme/Icon/Close"; import "./styles.css"; export type Props = { children?: React.ReactNode; className?: string; title: string; useCustomTitle?: boolean; backdropExit?: boolean; setOpenModal: (isOpen: boolean) => void; }; export default function Modal({ setOpenModal, className, children, useCustomTitle, backdropExit, title, }: Props): React.ReactNode { const [transitionClass, setTransitionClass] = useState(""); const onFadeIn = () => setTransitionClass("fade-in"); const onFadeOut = () => setTransitionClass("fade-out"); const onTransitionEnd = () => transitionClass === "fade-out" && setOpenModal(false); useEffect(onFadeIn, []); return (
backdropExit && onFadeOut()} />
{!useCustomTitle && (
{title}
)} {children}
); } ================================================ FILE: website/src/components/Modal/styles.css ================================================ .nb-modal { &-title { @apply flex items-center font-bold; } &-root { @apply fixed z-[1300] inset-0 flex items-center justify-center; } &-container { @apply absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 min-w-[400px] lg:min-w-[600px]; @apply opacity-0; @apply transition-opacity duration-[225ms] ease-in-out delay-0; &.fade-in { @apply opacity-100; } &.fade-out { @apply opacity-0; } } &-backdrop { @apply fixed flex right-0 bottom-0 top-0 left-0 bg-transparent opacity-0; @apply transition-all duration-[225ms] ease-in-out delay-0 -z-[1]; &.fade-in { @apply opacity-100 bg-black/50; } &.fade-out { @apply opacity-0 bg-transparent; } } } ================================================ FILE: website/src/components/Paginate/index.tsx ================================================ import React, { useCallback } from "react"; import clsx from "clsx"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import type { usePagination } from "react-use-pagination"; import "./styles.css"; const MAX_LENGTH = 7; export type Props = Pick< ReturnType, | "totalPages" | "currentPage" | "setNextPage" | "setPreviousPage" | "setPage" | "previousEnabled" | "nextEnabled" > & { className?: string; }; export default function Paginate({ className, totalPages, currentPage, setPreviousPage, setNextPage, setPage, previousEnabled, nextEnabled, }: Props): React.ReactNode { // const [containerElement, setContainerElement] = useState( // null // ); // const ref = useCallback( // (element: HTMLElement | null) => { // setContainerElement(element); // }, // [setContainerElement] // ); // const maxWidth = useContentWidth( // containerElement?.parentElement ?? undefined // ); // const maxLength = Math.min( // (maxWidth && Math.floor(maxWidth / 50) - 2) || totalPages, // totalPages // ); const range = useCallback((start: number, end: number) => { const result = []; start = start > 0 ? start : 1; for (let i = start; i <= end; i++) { result.push(i); } return result; }, []); const pages: (React.ReactNode | number)[] = []; const ellipsis = ; const even = MAX_LENGTH % 2 === 0 ? 1 : 0; const left = Math.floor(MAX_LENGTH / 2); const right = totalPages - left + even + 1; currentPage += 1; if (totalPages <= MAX_LENGTH) { pages.push(...range(1, totalPages)); } else if (currentPage > left && currentPage < right) { const firstItem = 1; const lastItem = totalPages; const start = currentPage - left + 2; const end = currentPage + left - 2 - even; const secondItem = start - 1 === firstItem + 1 ? 2 : ellipsis; const beforeLastItem = end + 1 === lastItem - 1 ? end + 1 : ellipsis; pages.push(1, secondItem, ...range(start, end), beforeLastItem, totalPages); } else if (currentPage === left) { const end = currentPage + left - 1 - even; pages.push(...range(1, end), ellipsis, totalPages); } else if (currentPage === right) { const start = currentPage - left + 1; pages.push(1, ellipsis, ...range(start, totalPages)); } else { pages.push(...range(1, left), ellipsis, ...range(right, totalPages)); } return ( ); } ================================================ FILE: website/src/components/Paginate/styles.css ================================================ .paginate { &-container { @apply flex items-center justify-center gap-2; } &-button { @apply flex items-center justify-center cursor-pointer select-none; @apply w-8 h-8 text-sm leading-8 text-center bg-base-200 text-base-content; &.ellipsis { @apply !text-base-content cursor-default; } &.active { @apply bg-primary !text-primary-content cursor-default; } &:hover { @apply text-primary; } &:disabled { @apply !text-base-content/80 cursor-not-allowed; } } &-pager { @apply flex items-center justify-center gap-2; } } ================================================ FILE: website/src/components/Resource/Avatar/index.tsx ================================================ import { useState } from "react"; import Link from "@docusaurus/Link"; import clsx from "clsx"; interface Props { className?: string; authorLink: string; authorAvatar: string; } export default function Avatar({ authorLink, authorAvatar, className }: Props) { const [loaded, setLoaded] = useState(false); const onLoad = () => setLoaded(true); return (
{!loaded && (
)} Avatar
); } ================================================ FILE: website/src/components/Resource/Card/index.tsx ================================================ import React from "react"; import clsx from "clsx"; import Link from "@docusaurus/Link"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import Avatar from "@/components/Resource/Avatar"; import Tag from "@/components/Resource/Tag"; import ValidStatus from "@/components/Resource/ValidStatus"; import type { Resource } from "@/libs/store"; import "./styles.css"; export type Props = { resource: Resource; onClick?: () => void; onTagClick: (tag: string) => void; onAuthorClick: () => void; className?: string; }; export default function ResourceCard({ resource, onClick, onTagClick, onAuthorClick, className, }: Props): React.ReactNode { const isGithub = /^https:\/\/github.com\/[^/]+\/[^/]+/.test( resource.homepage ); const isPlugin = resource.resourceType === "plugin"; const registryLink = isPlugin && `https://registry.nonebot.dev/plugin/${resource.project_link}:${resource.module_name}`; const authorLink = `https://github.com/${resource.author}`; const authorAvatar = `${authorLink}.png?size=80`; return (
{resource.name}
{resource.is_official && ( )}
{resource.desc}
{resource.tags.map((tag, index) => ( onTagClick(tag.label)} /> ))}
{isGithub ? ( ) : ( )} {isPlugin && ( )}
{resource.author}
); } ================================================ FILE: website/src/components/Resource/Card/styles.css ================================================ .resource-card { &-container { @apply flex flex-col gap-y-2 w-full min-h-[12rem] p-4; @apply transition-colors duration-500 bg-base-200; @apply border-2 border-base-200 rounded-lg; @apply hover:border-primary; } &-header { @apply min-w-0 flex items-center w-full text-lg font-medium; &-title { @apply min-w-0 grow justify-start flex items-center flex-initial; } &-text { @apply min-w-0 cursor-help tooltip; } &-check { @apply ml-2 text-green-600 dark:text-green-400 fill-current; } &-expand { @apply flex-none fill-current cursor-pointer; } } &-desc { @apply flex-1 w-full text-sm text-ellipsis break-words; } &-footer { @apply flex flex-col w-full cursor-default; &-tags { @apply flex flex-wrap gap-1; } &-tag { @apply cursor-pointer; } &-divider { @apply m-0; } &-info { @apply flex items-center justify-between w-full; } &-group { @apply flex items-center justify-center gap-x-1 leading-none; } &-icon { @apply w-5 h-5 fill-current opacity-80; &:hover { @apply opacity-100; } } &-avatar { @apply w-5 h-5 rounded-full transition-shadow; &:hover { @apply ring-1 ring-primary ring-offset-base-100 ring-offset-1; } } &-author { @apply text-sm cursor-pointer; } } } ================================================ FILE: website/src/components/Resource/DetailCard/index.tsx ================================================ import { useEffect, useState } from "react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import copy from "copy-text-to-clipboard"; import Tag from "@/components/Resource/Tag"; import ValidStatus from "@/components/Resource/ValidStatus"; import type { Resource } from "@/libs/store"; import type { PyPIData } from "./types"; import Avatar from "../Avatar"; import "./styles.css"; export type Props = { resource: Resource; }; export default function ResourceDetailCard({ resource }: Props) { const [pypiData, setPypiData] = useState(null); const [copied, setCopied] = useState(false); const authorLink = `https://github.com/${resource.author}`; const authorAvatar = `${authorLink}.png?size=100`; const isPlugin = resource.resourceType === "plugin"; const registryLink = isPlugin && `https://registry.nonebot.dev/plugin/${resource.project_link}:${resource.module_name}`; const getProjectLink = (resource: Resource) => { switch (resource.resourceType) { case "plugin": case "adapter": case "driver": return resource.project_link; default: return null; } }; const getModuleName = (resource: Resource) => { switch (resource.resourceType) { case "plugin": case "adapter": return resource.module_name; case "driver": return resource.module_name.replace(/~/, "nonebot.drivers."); default: return null; } }; const getHomepageLink = (resource: Resource) => { switch (resource.resourceType) { case "plugin": case "adapter": case "driver": return resource.homepage; default: return null; } }; const getPypiProjectLink = (resource: Resource) => { switch (resource.resourceType) { case "plugin": case "adapter": return `https://pypi.org/project/${resource.project_link}`; default: return null; } }; const getPluginStatusUpdatedTime = (resource: Resource) => { switch (resource.resourceType) { case "plugin": return new Date(resource.time).toLocaleString(); default: return null; } }; const fetchPypiProject = (projectName: string) => fetch(`https://pypi.org/pypi/${projectName}/json`) .then((response) => response.json()) .then((data) => setPypiData(data)); const copyCommand = (resource: Resource) => { const projectLink = getProjectLink(resource); if (projectLink) { copy(`nb ${resource.resourceType} install ${projectLink}`); setCopied(true); setTimeout(() => setCopied(false), 2000); } }; useEffect(() => { const fetchingTasks: Promise[] = []; if (resource.resourceType === "bot" || resource.resourceType === "driver") { return; } if (resource.project_link) { fetchingTasks.push(fetchPypiProject(resource.project_link)); } Promise.all(fetchingTasks); }, [resource]); const projectLink = getProjectLink(resource) || "无"; const moduleName = getModuleName(resource) || "无"; const homepageLink = getHomepageLink(resource); const pypiProjectLink = getPypiProjectLink(resource); const updatedTime = getPluginStatusUpdatedTime(resource); return ( <>
{resource.name} {resource.is_official && (
官方
)}
{resource.author}
{resource.desc}
{resource.tags.map((tag, index) => ( ))}
{" "} {(pypiData && pypiData.info.requires_python) || "无"}
{" "} {(pypiData && pypiData.releases[pypiData.info.version] && `${ pypiData.releases[pypiData.info.version].reduce( (acc, curr) => acc + curr.size, 0 ) / 1000 }K`) || "无"}
{" "} {(pypiData && pypiData.info.license) || "无"}
{" "} {(pypiData && pypiData.info.version) || "无"}
{homepageLink && ( )} {pypiProjectLink && ( )}
{" "} {updatedTime}
); } ================================================ FILE: website/src/components/Resource/DetailCard/styles.css ================================================ .detail-card { &-header { @apply flex items-center align-middle; &-divider { @apply m-0; } } &-avatar { @apply mr-3 w-12 h-12 rounded-full; } &-title { @apply inline-flex flex-col h-12 justify-start; &-main { @apply font-bold; } &-sub { @apply text-sm; } } &-actions { @apply flex items-center gap-x-2 lg:ml-auto; &-button { @apply btn btn-sm; } &-mobile { @apply lg:hidden; } &-desktop { @apply max-lg:hidden; } } &-body { @apply flex flex-col w-full lg:flex-row; &-left { @apply flex flex-col min-h-[150px] lg:basis-3/4 max-w-[65%]; } &-divider { @apply divider lg:divider-horizontal; } &-right { @apply flex flex-col justify-start gap-y-2 lg:basis-1/4 lg:max-w-[45%]; } } &-meta-item { @apply text-sm truncate; &-link { @apply hover:text-primary hover:transition; } } } ================================================ FILE: website/src/components/Resource/DetailCard/types.ts ================================================ export type Downloads = { last_day: number; last_month: number; last_week: number; }; export type Info = { author: string; author_email: string; bugtrack_url: null; classifiers: string[]; description: string; description_content_type: string; docs_url: null; download_url: string; downloads: Downloads; home_page: string; keywords: string; license: string; maintainer: string; maintainer_email: string; name: string; package_url: string; platform: null; project_url: string; release_url: string; requires_dist: string[]; requires_python: string; summary: string; version: string; yanked: boolean; yanked_reason: null; }; export interface Digests { blake2b_256: string; md5: string; sha256: string; } export type Releases = { comment_text: string; digests: Digests; downloads: number; filename: string; has_sig: boolean; md5_digest: string; packagetype: string; python_version: string; requires_python: string; size: number; upload_time: Date; upload_time_iso_8601: Date; url: string; yanked: boolean; yanked_reason: null; }; export type PyPIData = { info: Info; last_serial: number; releases: { [key: string]: Releases[] }; urls: URL[]; vulnerabilities: unknown[]; }; ================================================ FILE: website/src/components/Resource/Tag/index.tsx ================================================ import React from "react"; import clsx from "clsx"; import { pickTextColor } from "@/libs/color"; import type { Tag } from "@/types/tag"; import "./styles.css"; export type Props = Tag & { className?: string; onClick?: React.MouseEventHandler; }; export default function ResourceTag({ label, color, className, onClick, }: Props): React.ReactNode { return ( {label} ); } ================================================ FILE: website/src/components/Resource/Tag/styles.css ================================================ .resource-tag { @apply inline-flex items-center justify-center; @apply text-xs font-mono w-fit h-5 px-2 rounded; } ================================================ FILE: website/src/components/Resource/ValidStatus/index.tsx ================================================ import React from "react"; import clsx from "clsx"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import type { Resource } from "@/libs/store"; import { ValidStatus } from "@/libs/valid"; import type { IconName } from "@fortawesome/fontawesome-common-types"; export const getValidStatus = (resource: Resource) => { switch (resource.resourceType) { case "plugin": if (resource.skip_test) { return ValidStatus.SKIP; } if (resource.valid) { return ValidStatus.VALID; } return ValidStatus.INVALID; default: return ValidStatus.MISSING; } }; export const validIcons: { [key in ValidStatus]: IconName; } = { [ValidStatus.VALID]: "plug-circle-check", [ValidStatus.INVALID]: "plug-circle-xmark", [ValidStatus.SKIP]: "plug-circle-exclamation", [ValidStatus.MISSING]: "plug-circle-exclamation", }; export type Props = { resource: Resource; validLink: string; className?: string; simple?: boolean; }; export default function ValidDisplay({ resource, validLink, className, simple, }: Props) { const validStatus = getValidStatus(resource); const isValid = validStatus === ValidStatus.VALID; const isInvalid = validStatus === ValidStatus.INVALID; const isSkip = validStatus === ValidStatus.SKIP; return ( validStatus !== ValidStatus.MISSING && (
{!simple && ( <> {isValid &&

插件已通过测试

} {isInvalid &&

插件未通过测试

} {isSkip &&

插件跳过测试

} )}
) ); } ================================================ FILE: website/src/components/Searcher/index.tsx ================================================ import React, { useRef } from "react"; import clsx from "clsx"; import { translate } from "@docusaurus/Translate"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import "./styles.css"; export type Props = { onChange: (value: string) => void; onSubmit: (value: string) => void; onBackspace: () => void; onClear: () => void; onTagClick: (index: number) => void; tags?: string[]; className?: string; placeholder?: string; disabled?: boolean; }; export default function Searcher({ onChange, onSubmit, onBackspace, onClear, onTagClick, tags = [], className, placeholder, disabled = false, }: Props): React.ReactNode { const ref = useRef(null); const handleSubmit = (e: React.FormEvent) => { onSubmit(e.currentTarget.value); e.currentTarget.value = ""; e.preventDefault(); }; const handleEscape = (e: React.KeyboardEvent) => { e.currentTarget.value = ""; }; const handleBackspace = (e: React.KeyboardEvent) => { if (e.currentTarget.value === "") { onBackspace(); e.preventDefault(); } }; const handleKeyDown = (e: React.KeyboardEvent) => { switch (e.key) { case "Enter": { handleSubmit(e); break; } case "Escape": { handleEscape(e); break; } case "Backspace": { handleBackspace(e); break; } default: break; } }; const handleClear = () => { if (ref.current) { ref.current.value = ""; ref.current.focus(); } onClear(); }; return (
{tags.map((tag, index) => (
onTagClick(index)} > {tag}
))} onChange(e.currentTarget.value)} onKeyDown={handleKeyDown} disabled={disabled} />
); } ================================================ FILE: website/src/components/Searcher/styles.css ================================================ .searcher { &-box { @apply flex items-center w-full rounded-3xl bg-base-200; @apply transition-[color,background-color] duration-500; } &-container { @apply flex-1 flex items-center flex-wrap gap-x-1 gap-y-2; @apply pl-5 py-3; } &-tag { @apply flex-initial shrink-0; @apply font-medium cursor-pointer select-none; } &-input { @apply flex-1 text-sm min-w-[10rem]; @apply bg-transparent border-none outline-none; } &-action { @apply flex-initial shrink-0 flex items-center justify-center cursor-pointer w-12 h-10; &-icon { @apply h-4 opacity-50; } &:hover &-icon.search { @apply hidden; } &:not(:hover) &-icon.close { @apply hidden; } } } ================================================ FILE: website/src/components/Store/Content/Adapter.tsx ================================================ import React, { useCallback, useEffect, useState } from "react"; import Translate from "@docusaurus/Translate"; import { usePagination } from "react-use-pagination"; import Admonition from "@theme/Admonition"; import AdapterForm from "@/components/Form/Adapter"; import Modal from "@/components/Modal"; import Paginate from "@/components/Paginate"; import ResourceCard from "@/components/Resource/Card"; import ResourceDetailCard from "@/components/Resource/DetailCard"; import Searcher from "@/components/Searcher"; import StoreToolbar, { type Action } from "@/components/Store/Toolbar"; import { authorFilter, tagFilter } from "@/libs/filter"; import { useSearchControl } from "@/libs/search"; import { fetchRegistryData, loadFailedTitle } from "@/libs/store"; import { useToolbar } from "@/libs/toolbar"; import type { Adapter } from "@/types/adapter"; export default function AdapterPage(): React.ReactNode { const [adapters, setAdapters] = useState(null); const adapterCount = adapters?.length ?? 0; const loading = adapters === null; const [error, setError] = useState(null); const [isOpenModal, setIsOpenModal] = useState(false); const [isOpenCardModal, setIsOpenCardModal] = useState(false); const [clickedAdapter, setClickedAdapter] = useState(null); const { filteredResources: filteredAdapters, searcherTags, addFilter, onSearchQueryChange, onSearchQuerySubmit, onSearchBackspace, onSearchClear, onSearchTagClick, } = useSearchControl(adapters ?? []); const filteredAdapterCount = filteredAdapters.length; const { startIndex, endIndex, totalPages, currentPage, setNextPage, setPreviousPage, setPage, previousEnabled, nextEnabled, } = usePagination({ totalItems: filteredAdapters.length, initialPageSize: 12, }); const currentAdapters = filteredAdapters.slice(startIndex, endIndex + 1); // load adapters asynchronously useEffect(() => { fetchRegistryData("adapter") .then(setAdapters) .catch((e) => { setError(e); console.error(e); }); }, []); const { filters: filterTools } = useToolbar({ resources: adapters ?? [], addFilter, }); const actionTool: Action = { label: "发布适配器", icon: ["fas", "plus"], onClick: () => { setIsOpenModal(true); }, }; const onCardClick = useCallback((adapter: Adapter) => { setClickedAdapter(adapter); setIsOpenCardModal(true); }, []); const onCardTagClick = useCallback( (tag: string) => { addFilter(tagFilter(tag)); }, [addFilter] ); const onCardAuthorClick = useCallback( (author: string) => { addFilter(authorFilter(author)); }, [addFilter] ); return ( <>

{adapterCount === filteredAdapterCount ? ( {"当前共有 {adapterCount} 个适配器"} ) : ( {"当前共有 {filteredAdapterCount} / {adapterCount} 个适配器"} )}

{error ? ( {error.message} ) : loading ? (

) : (
{currentAdapters.map((adapter, index) => ( onCardClick(adapter)} onTagClick={onCardTagClick} onAuthorClick={() => onCardAuthorClick(adapter.author)} /> ))}
)} {isOpenModal && ( )} {isOpenCardModal && ( {clickedAdapter && } )} ); } ================================================ FILE: website/src/components/Store/Content/Bot.tsx ================================================ import React, { useCallback, useEffect, useState } from "react"; import Translate from "@docusaurus/Translate"; import { usePagination } from "react-use-pagination"; import Admonition from "@theme/Admonition"; import BotForm from "@/components/Form/Bot"; import Modal from "@/components/Modal"; import Paginate from "@/components/Paginate"; import ResourceCard from "@/components/Resource/Card"; import Searcher from "@/components/Searcher"; import StoreToolbar, { type Action } from "@/components/Store/Toolbar"; import { authorFilter, tagFilter } from "@/libs/filter"; import { useSearchControl } from "@/libs/search"; import { fetchRegistryData, loadFailedTitle } from "@/libs/store"; import { useToolbar } from "@/libs/toolbar"; import type { Bot } from "@/types/bot"; export default function PluginPage(): React.ReactNode { const [bots, setBots] = useState(null); const botCount = bots?.length ?? 0; const loading = bots === null; const [error, setError] = useState(null); const [isOpenModal, setIsOpenModal] = useState(false); const { filteredResources: filteredBots, searcherTags, addFilter, onSearchQueryChange, onSearchQuerySubmit, onSearchBackspace, onSearchClear, onSearchTagClick, } = useSearchControl(bots ?? []); const filteredBotCount = filteredBots.length; const { startIndex, endIndex, totalPages, currentPage, setNextPage, setPreviousPage, setPage, previousEnabled, nextEnabled, } = usePagination({ totalItems: filteredBots.length, initialPageSize: 12, }); const currentBots = filteredBots.slice(startIndex, endIndex + 1); // load bots asynchronously useEffect(() => { fetchRegistryData("bot") .then(setBots) .catch((e) => { setError(e); console.error(e); }); }, []); const { filters: filterTools } = useToolbar({ resources: bots ?? [], addFilter, }); const actionTool: Action = { label: "发布机器人", icon: ["fas", "plus"], onClick: () => { setIsOpenModal(true); }, }; const onCardTagClick = useCallback( (tag: string) => { addFilter(tagFilter(tag)); }, [addFilter] ); const onAuthorClick = useCallback( (author: string) => { addFilter(authorFilter(author)); }, [addFilter] ); return ( <>

{botCount === filteredBotCount ? ( {"当前共有 {botCount} 个机器人"} ) : ( {"当前共有 {filteredBotCount} / {botCount} 个机器人"} )}

{error ? ( {error.message} ) : loading ? (

) : (
{currentBots.map((bot, index) => ( onAuthorClick(bot.author)} /> ))}
)} {isOpenModal && ( )} ); } ================================================ FILE: website/src/components/Store/Content/Driver.tsx ================================================ import React, { useCallback, useEffect, useState } from "react"; import Translate from "@docusaurus/Translate"; import { usePagination } from "react-use-pagination"; import Admonition from "@theme/Admonition"; import Modal from "@/components/Modal"; import Paginate from "@/components/Paginate"; import ResourceCard from "@/components/Resource/Card"; import ResourceDetailCard from "@/components/Resource/DetailCard"; import Searcher from "@/components/Searcher"; import { authorFilter, tagFilter } from "@/libs/filter"; import { useSearchControl } from "@/libs/search"; import { fetchRegistryData, loadFailedTitle } from "@/libs/store"; import type { Driver } from "@/types/driver"; export default function DriverPage(): React.ReactNode { const [drivers, setDrivers] = useState(null); const driverCount = drivers?.length ?? 0; const loading = drivers === null; const [error, setError] = useState(null); const [isOpenCardModal, setIsOpenCardModal] = useState(false); const [clickedDriver, setClickedDriver] = useState(null); const { filteredResources: filteredDrivers, searcherTags, addFilter, onSearchQueryChange, onSearchQuerySubmit, onSearchBackspace, onSearchClear, onSearchTagClick, } = useSearchControl(drivers ?? []); const filteredDriverCount = filteredDrivers.length; const { startIndex, endIndex, totalPages, currentPage, setNextPage, setPreviousPage, setPage, previousEnabled, nextEnabled, } = usePagination({ totalItems: filteredDrivers.length, initialPageSize: 12, }); const currentDrivers = filteredDrivers.slice(startIndex, endIndex + 1); // load drivers asynchronously useEffect(() => { fetchRegistryData("driver") .then(setDrivers) .catch((e) => { setError(e); console.error(e); }); }, []); const onCardClick = useCallback((driver: Driver) => { setClickedDriver(driver); setIsOpenCardModal(true); }, []); const onCardTagClick = useCallback( (tag: string) => { addFilter(tagFilter(tag)); }, [addFilter] ); const onAuthorClick = useCallback( (author: string) => { addFilter(authorFilter(author)); }, [addFilter] ); return ( <>

{driverCount === filteredDriverCount ? ( {"当前共有 {driverCount} 个驱动器"} ) : ( {"当前共有 {filteredDriverCount} / {driverCount} 个驱动器"} )}

{error ? ( {error.message} ) : loading ? (

) : (
{currentDrivers.map((driver, index) => ( onCardClick(driver)} onTagClick={onCardTagClick} onAuthorClick={() => onAuthorClick(driver.author)} /> ))}
)} {isOpenCardModal && ( {clickedDriver && } )} ); } ================================================ FILE: website/src/components/Store/Content/Plugin.tsx ================================================ import React, { useCallback, useEffect, useState } from "react"; import Translate, { translate } from "@docusaurus/Translate"; import { usePagination } from "react-use-pagination"; import Admonition from "@theme/Admonition"; import PluginForm from "@/components/Form/Plugin"; import Modal from "@/components/Modal"; import Paginate from "@/components/Paginate"; import ResourceCard from "@/components/Resource/Card"; import ResourceDetailCard from "@/components/Resource/DetailCard"; import Searcher from "@/components/Searcher"; import StoreToolbar, { type Action, type Sorter, } from "@/components/Store/Toolbar"; import { authorFilter, tagFilter } from "@/libs/filter"; import { useSearchControl } from "@/libs/search"; import { SortMode } from "@/libs/sorter"; import { fetchRegistryData, loadFailedTitle } from "@/libs/store"; import { useToolbar } from "@/libs/toolbar"; import type { Plugin } from "@/types/plugin"; export default function PluginPage(): React.ReactNode { const [plugins, setPlugins] = useState(null); const pluginCount = plugins?.length ?? 0; const loading = plugins === null; const [error, setError] = useState(null); const [isOpenModal, setIsOpenModal] = useState(false); const [isOpenCardModal, setIsOpenCardModal] = useState(false); const [clickedPlugin, setClickedPlugin] = useState(null); const [sortMode, setSortMode] = useState(SortMode.Default); const sorterTool: Sorter = { label: sortMode === SortMode.Default ? translate({ id: "pages.store.sorter.label.default", description: "The label of default sorter", message: "默认顺序", }) : translate({ id: "pages.store.sorter.label.updateDesc", description: "The label of updateDesc sorter", message: "更新时间倒序", }), icon: ["fas", "sort-amount-down"], active: sortMode === SortMode.UpdateDesc, onClick: () => { setSortMode( sortMode === SortMode.Default ? SortMode.UpdateDesc : SortMode.Default ); }, }; const getSortedPlugins = (plugins: Plugin[]): Plugin[] => { if (sortMode === SortMode.UpdateDesc) { return [...plugins].sort( (a, b) => new Date(b.time).getTime() - new Date(a.time).getTime() ); } return plugins; }; const { filteredResources: filteredPlugins, searcherTags, addFilter, onSearchQueryChange, onSearchQuerySubmit, onSearchBackspace, onSearchClear, onSearchTagClick, } = useSearchControl(getSortedPlugins(plugins ?? [])); const filteredPluginCount = filteredPlugins.length; const { startIndex, endIndex, totalPages, currentPage, setNextPage, setPreviousPage, setPage, previousEnabled, nextEnabled, } = usePagination({ totalItems: filteredPlugins.length, initialPageSize: 12, }); const currentPlugins = filteredPlugins.slice(startIndex, endIndex + 1); // load plugins asynchronously useEffect(() => { fetchRegistryData("plugin") .then(setPlugins) .catch((e) => { setError(e); console.error(e); }); }, []); const { filters: filterTools } = useToolbar({ resources: plugins ?? [], addFilter, }); const actionTool: Action = { label: "发布插件", icon: ["fas", "plus"], onClick: () => { setIsOpenModal(true); }, }; const onCardClick = useCallback((plugin: Plugin) => { setClickedPlugin(plugin); setIsOpenCardModal(true); }, []); const onCardTagClick = useCallback( (tag: string) => { addFilter(tagFilter(tag)); }, [addFilter] ); const onCardAuthorClick = useCallback( (author: string) => { addFilter(authorFilter(author)); }, [addFilter] ); return ( <>

{pluginCount === filteredPluginCount ? ( {"当前共有 {pluginCount} 个插件"} ) : ( {"当前共有 {filteredPluginCount} / {pluginCount} 个插件"} )}

{error ? ( {error.message} ) : loading ? (

) : (
{currentPlugins.map((plugin, index) => ( onCardClick(plugin)} onTagClick={onCardTagClick} onAuthorClick={() => onCardAuthorClick(plugin.author)} /> ))}
)} {isOpenModal && ( )} {isOpenCardModal && ( {clickedPlugin && } )} ); } ================================================ FILE: website/src/components/Store/Layout.tsx ================================================ import React from "react"; import { useDocsVersionCandidates } from "@docusaurus/plugin-content-docs/client"; import { PageMetadata } from "@docusaurus/theme-common"; import { useVersionedSidebar } from "@nullbot/docusaurus-plugin-getsidebar/client"; import { SidebarContentFiller } from "@nullbot/docusaurus-theme-nonepress/contexts"; import BackToTopButton from "@theme/BackToTopButton"; import Heading from "@theme/Heading"; import Layout from "@theme/Layout"; import Page from "@theme/Page"; import "./styles.css"; const SIDEBAR_ID = "ecosystem"; type Props = { title: string; children: React.ReactNode; }; function StorePage({ title, children }: Props): React.ReactNode { const sidebarItems = useVersionedSidebar( useDocsVersionCandidates()[0].name, SIDEBAR_ID )!; return (
{title} {children}
); } export default function StoreLayout({ title, ...props }: Props): React.ReactNode { return ( <> ); } ================================================ FILE: website/src/components/Store/Toolbar.tsx ================================================ import React, { useState } from "react"; import clsx from "clsx"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import type { IconProp } from "@fortawesome/fontawesome-svg-core"; export type Filter = { label: string; icon: IconProp; choices?: string[]; onSubmit: (query: string) => void; }; export type Sorter = { label: string; icon: IconProp; active: boolean; onClick: () => void; }; export type Action = { label: string; icon: IconProp; onClick: () => void; }; export type Props = { filters?: Filter[]; sorter?: Sorter; action?: Action; className?: string; }; function ToolbarFilter({ label, icon, choices, onSubmit, }: Filter): React.ReactNode { const [query, setQuery] = useState(""); const filteredChoices = choices ?.filter((choice) => choice.toLowerCase().includes(query.toLowerCase())) ?.slice(0, 5); const handleQuerySubmit = () => { if (filteredChoices && filteredChoices.length > 0) { onSubmit(filteredChoices[0]); } else if (choices === null) { onSubmit(query); } }; const onQueryKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter") { handleQuerySubmit(); e.preventDefault(); } }; const onChoiceKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter") { onSubmit(e.currentTarget.innerText); e.preventDefault(); } }; return (
setQuery(e.target.value)} onKeyDown={onQueryKeyDown} className="input input-sm input-bordered w-full" /> {filteredChoices && (
    {filteredChoices.map((choice, index) => (
  • onSubmit(choice)} onKeyDown={onChoiceKeyDown} > {choice}
  • ))}
)}
); } export default function StoreToolbar({ filters, sorter, action, className, }: Props): React.ReactNode | null { if (!(filters && filters.length > 0) && !action) { return null; } return ( <>
{filters?.map((filter, index) => ( ))} {sorter && (
)}
{action && (
)}
{sorter && (
)}
); } ================================================ FILE: website/src/components/Store/styles.css ================================================ .store { &-title { @apply text-center; } &-description { @apply text-center; } &-searcher { @apply max-w-2xl mx-auto my-4; } &-toolbar { @apply flex items-center justify-center my-4; &-second { @apply lg:hidden; } &-sorter { @apply max-lg:flex-1; &-desktop { @apply max-lg:hidden; } } &-filters { @apply flex grow gap-2; } &-dropdown { @apply w-36 z-10 m-0 p-2; @apply rounded-md bg-base-100 shadow-lg border border-base-200; } } &-loading { @apply text-primary; &-container { @apply text-center; } } &-container { @apply grid grid-cols-[repeat(auto-fill,minmax(300px,1fr))] gap-6 mt-4 mb-8; } } ================================================ FILE: website/src/components/Tag/index.tsx ================================================ import React from "react"; import clsx from "clsx"; import "./styles.css"; import { pickTextColor } from "@/libs/color"; import type { Tag as TagType } from "@/types/tag"; export default function Tag({ label, color, className, onClick, }: TagType & { className?: string; onClick?: React.MouseEventHandler; }): React.ReactNode { return ( {label} ); } ================================================ FILE: website/src/components/Tag/styles.css ================================================ .tag { @apply font-mono inline-flex px-3 rounded-full items-center align-middle; } ================================================ FILE: website/src/libs/color.ts ================================================ /** * Choose fg color by bg color * @see https://www.npmjs.com/package/colord * @see https://www.w3.org/TR/AERT/#color-contrast */ export function pickTextColor( bgColor: string, lightColor: string, darkColor: string ) { const color = bgColor.charAt(0) === "#" ? bgColor.substring(1, 7) : bgColor; const r = parseInt(color.substring(0, 2), 16); // hexToR const g = parseInt(color.substring(2, 4), 16); // hexToG const b = parseInt(color.substring(4, 6), 16); // hexToB return r * 0.299 + g * 0.587 + b * 0.114 > 186 ? darkColor : lightColor; } ================================================ FILE: website/src/libs/filter.ts ================================================ import { useCallback, useState } from "react"; import { translate } from "@docusaurus/Translate"; import { getValidStatus } from "@/components/Resource/ValidStatus"; import { ValidStatus } from "./valid"; import type { Resource } from "./store"; export type Filter = { type: string; id: string; displayName?: string; filter: (resource: T) => boolean; }; const validStatusDisplayName = { [ValidStatus.VALID]: translate({ id: "pages.store.filter.validateStatusDisplayName.valid", description: "The display name of validateStatus filter", message: "状态: 通过", }), [ValidStatus.INVALID]: translate({ id: "pages.store.filter.validateStatusDisplayName.invalid", description: "The display name of validateStatus filter", message: "状态: 未通过", }), [ValidStatus.SKIP]: translate({ id: "pages.store.filter.validateStatusDisplayName.skip", description: "The display name of validateStatus filter", message: "状态: 跳过", }), [ValidStatus.MISSING]: translate({ id: "pages.store.filter.validateStatusDisplayName.missing", description: "The display name of validateStatus filter", message: "状态: 缺失", }), }; export const validStatusFilter = ( validStatus: ValidStatus ): Filter => ({ type: "validStatus", id: `validStatus-${validStatus}`, displayName: validStatusDisplayName[validStatus], filter: (resource: Resource): boolean => resource.resourceType === "plugin" ? getValidStatus(resource) === validStatus : true, }); export const tagFilter = ( tag: string ): Filter => ({ type: "tag", id: `tag-${tag}`, displayName: translate( { id: "pages.store.filter.tagDisplayName", description: "The display name of tag filter", message: "标签: {tag}", }, { tag } ), filter: (resource: Resource): boolean => resource.tags.map((tag) => tag.label).includes(tag), }); export const officialFilter = ( official: boolean = true ): Filter => ({ type: "official", id: `official-${official}`, displayName: translate({ id: "pages.store.filter.officialDisplayName", description: "The display name of official filter", message: "非官方|官方", }).split("|")[Number(official)], filter: (resource: Resource): boolean => resource.is_official === official, }); export const authorFilter = ( author: string ): Filter => ({ type: "author", id: `author-${author}`, displayName: translate( { id: "pages.store.filter.authorDisplayName", description: "The display name of author filter", message: "作者: {author}", }, { author } ), filter: (resource: Resource): boolean => resource.author === author, }); export const queryFilter = ( query: string ): Filter => ({ type: "query", id: `query-${query}`, displayName: query, filter: (resource: Resource): boolean => { if (!query) { return true; } const queryLower = query.toLowerCase(); const pluginMatch = resource.resourceType === "plugin" && (resource.module_name?.toLowerCase().includes(queryLower) || resource.project_link?.toLowerCase().includes(queryLower)); const commonMatch = resource.name.toLowerCase().includes(queryLower) || resource.desc.toLowerCase().includes(queryLower) || resource.author.toLowerCase().includes(queryLower) || resource.tags.filter((t) => t.label.toLowerCase().includes(queryLower)) .length > 0; return pluginMatch || commonMatch; }, }); export function filterResources( resources: T[], filters: Filter[] ): T[] { return resources.filter((resource) => filters.every((filter) => filter.filter(resource)) ); } type useFilteredResourcesReturn = { filters: Filter[]; addFilter: (filter: Filter) => void; removeFilter: (filter: Filter | string) => void; filteredResources: T[]; }; export function useFilteredResources( resources: T[] ): useFilteredResourcesReturn { const [filters, setFilters] = useState[]>([]); const addFilter = useCallback( (filter: Filter) => { if (filters.some((f) => f.id === filter.id)) { return; } setFilters((filters) => [...filters, filter]); }, [filters, setFilters] ); const removeFilter = useCallback( (filter: Filter | string) => { setFilters((filters) => filters.filter((f) => typeof filter === "string" ? f.id !== filter : f !== filter ) ); }, [setFilters] ); const filteredResources = useCallback( () => filterResources(resources, filters), [resources, filters] ); return { filters, addFilter, removeFilter, filteredResources: filteredResources(), }; } ================================================ FILE: website/src/libs/search.ts ================================================ import { useCallback, useEffect, useState } from "react"; import { type Filter, useFilteredResources, queryFilter } from "./filter"; import type { Resource } from "./store"; type useSearchControlReturn = { filteredResources: T[]; searcherTags: string[]; addFilter: (filter: Filter) => void; onSearchQueryChange: (query: string) => void; onSearchQuerySubmit: () => void; onSearchBackspace: () => void; onSearchClear: () => void; onSearchTagClick: (index: number) => void; }; export function useSearchControl( resources: T[] ): useSearchControlReturn { const [currentFilter, setCurrentFilter] = useState | null>(null); const { filters, addFilter, removeFilter, filteredResources } = useFilteredResources(resources); // display tags in searcher (except current filter) const [searcherFilters, setSearcherFilters] = useState[]>([]); useEffect(() => { setSearcherFilters( filters.filter((f) => !(currentFilter && f === currentFilter)) ); }, [filters, currentFilter]); const onSearchQueryChange = useCallback( (newQuery: string) => { // remove current filter if query is empty if (newQuery === "") { currentFilter && removeFilter(currentFilter); setCurrentFilter(null); return; } const newFilter = queryFilter(newQuery); // do nothing if filter is not changed if (currentFilter?.id === newFilter.id) { return; } // remove old currentFilter currentFilter && removeFilter(currentFilter); // add new filter setCurrentFilter(newFilter); addFilter(newFilter); }, [currentFilter, setCurrentFilter, addFilter, removeFilter] ); const onSearchQuerySubmit = useCallback(() => { // set current filter to null to make filter permanent setCurrentFilter(null); }, [setCurrentFilter]); const onSearchBackspace = useCallback(() => { // remove last filter removeFilter(searcherFilters[searcherFilters.length - 1]); }, [removeFilter, searcherFilters]); const onSearchClear = useCallback(() => { // remove all filters searcherFilters.forEach((filter) => removeFilter(filter)); }, [removeFilter, searcherFilters]); const onSearchTagClick = useCallback( (index: number) => { removeFilter(searcherFilters[index]); }, [removeFilter, searcherFilters] ); return { filteredResources, searcherTags: searcherFilters.map((filter) => filter.displayName), addFilter, onSearchQueryChange, onSearchQuerySubmit, onSearchBackspace, onSearchClear, onSearchTagClick, }; } ================================================ FILE: website/src/libs/sorter.ts ================================================ export enum SortMode { Default, UpdateDesc, } ================================================ FILE: website/src/libs/store.ts ================================================ import { translate } from "@docusaurus/Translate"; import type { Adapter, AdaptersResponse } from "@/types/adapter"; import type { Bot, BotsResponse } from "@/types/bot"; import type { Driver, DriversResponse } from "@/types/driver"; import type { Plugin, PluginsResponse } from "@/types/plugin"; type RegistryDataResponseTypes = { adapter: AdaptersResponse; bot: BotsResponse; driver: DriversResponse; plugin: PluginsResponse; }; type RegistryDataType = keyof RegistryDataResponseTypes; type ResourceTypes = { adapter: Adapter; bot: Bot; driver: Driver; plugin: Plugin; }; export type Resource = Adapter | Bot | Driver | Plugin; export async function fetchRegistryData( dataType: T ): Promise { const resp = await fetch( `https://registry.nonebot.dev/${dataType}s.json` ).catch((e) => { throw new Error(`Failed to fetch ${dataType}s: ${e}`); }); if (!resp.ok) { throw new Error( `Failed to fetch ${dataType}s: ${resp.status} ${resp.statusText}` ); } const data = (await resp.json()) as RegistryDataResponseTypes[T]; return data.map( (resource) => ({ ...resource, resourceType: dataType }) as ResourceTypes[T] ); } export const loadFailedTitle = translate({ id: "pages.store.loadFailed.title", message: "加载失败", description: "Title to display when loading content failed", }); ================================================ FILE: website/src/libs/toolbar.ts ================================================ import { translate } from "@docusaurus/Translate"; import type { Filter as FilterTool } from "@/components/Store/Toolbar"; import { authorFilter, tagFilter, validStatusFilter, type Filter, } from "./filter"; import { ValidStatus } from "./valid"; import type { Resource } from "./store"; type Props = { resources: T[]; addFilter: (filter: Filter) => void; }; type useToolbarReturns = { filters: FilterTool[]; }; export function useToolbar({ resources, addFilter, }: Props): useToolbarReturns { const authorFilterTool: FilterTool = { label: "作者", icon: ["fas", "user"], choices: Array.from(new Set(resources.map((resource) => resource.author))), onSubmit: (author: string) => { addFilter(authorFilter(author)); }, }; const tagFilterTool: FilterTool = { label: "标签", icon: ["fas", "tag"], choices: Array.from( new Set( resources.flatMap((resource) => resource.tags.map((tag) => tag.label)) ) ), onSubmit: (tag: string) => { addFilter(tagFilter(tag)); }, }; const validateStatusFilterMapping: Record = { [translate({ id: "pages.store.filter.validateStatusDisplayName.valid", description: "The display name of validateStatus filter", message: "通过", })]: ValidStatus.VALID, [translate({ id: "pages.store.filter.validateStatusDisplayName.invalid", description: "The display name of validateStatus filter", message: "未通过", })]: ValidStatus.INVALID, [translate({ id: "pages.store.filter.validateStatusDisplayName.skip", description: "The display name of validateStatus filter", message: "跳过", })]: ValidStatus.SKIP, [translate({ id: "pages.store.filter.validateStatusDisplayName.missing", description: "The display name of validateStatus filter", message: "缺失", })]: ValidStatus.MISSING, }; const validStatusFilterTool: FilterTool = { label: "状态", icon: ["fas", "plug"], choices: Object.keys(validateStatusFilterMapping), onSubmit: (type: string) => { const validStatus = validateStatusFilterMapping[type]; if (!validStatus) { return; } addFilter(validStatusFilter(validStatus)); }, }; return { filters: [authorFilterTool, tagFilterTool, validStatusFilterTool], }; } ================================================ FILE: website/src/libs/valid.ts ================================================ export enum ValidStatus { VALID = "valid", INVALID = "invalid", SKIP = "skip", MISSING = "missing", } ================================================ FILE: website/src/pages/index.tsx ================================================ import React from "react"; import Layout from "@theme/Layout"; import HomeContent from "@/components/Home"; export default function Homepage(): React.ReactNode { return ( ); } ================================================ FILE: website/src/pages/store/adapters.tsx ================================================ import React from "react"; import { translate } from "@docusaurus/Translate"; import AdapterPageContent from "@/components/Store/Content/Adapter"; import StoreLayout from "@/components/Store/Layout"; export default function StoreAdapters(): React.ReactNode { const title = translate({ id: "pages.store.adapter.title", message: "适配器商店", description: "Title for the adapter store page", }); return ( ); } ================================================ FILE: website/src/pages/store/bots.tsx ================================================ import React from "react"; import { translate } from "@docusaurus/Translate"; import BotPageContent from "@/components/Store/Content/Bot"; import StoreLayout from "@/components/Store/Layout"; export default function StoreBots(): React.ReactNode { const title = translate({ id: "pages.store.bot.title", message: "机器人商店", description: "Title for the bot store page", }); return ( ); } ================================================ FILE: website/src/pages/store/drivers.tsx ================================================ import React from "react"; import { translate } from "@docusaurus/Translate"; import DriverPageContent from "@/components/Store/Content/Driver"; import StoreLayout from "@/components/Store/Layout"; export default function StoreDrivers(): React.ReactNode { const title = translate({ id: "pages.store.driver.title", message: "驱动器商店", description: "Title for the driver store page", }); return ( ); } ================================================ FILE: website/src/pages/store/index.tsx ================================================ import React from "react"; import { Redirect } from "@docusaurus/router"; export default function Store(): React.ReactNode { return ; } ================================================ FILE: website/src/pages/store/plugins.tsx ================================================ import React from "react"; import { translate } from "@docusaurus/Translate"; import PluginPageContent from "@/components/Store/Content/Plugin"; import StoreLayout from "@/components/Store/Layout"; export default function StorePlugins(): React.ReactNode { const title = translate({ id: "pages.store.plugin.title", message: "插件商店", description: "Title for the plugin store page", }); return ( ); } ================================================ FILE: website/src/plugins/webpack-plugin.ts ================================================ import path from "path"; import type { PluginConfig } from "@docusaurus/types"; export default (function webpackPlugin() { return { name: "webpack-plugin", configureWebpack() { return { resolve: { alias: { "@": path.resolve(__dirname, "../"), }, }, }; }, }; } satisfies PluginConfig); ================================================ FILE: website/src/theme/Footer/Copyright/index.tsx ================================================ import React from "react"; import Link from "@docusaurus/Link"; import Translate, { translate } from "@docusaurus/Translate"; import type { Props } from "@theme/Footer/Copyright"; import IconCloudflare from "@theme/Icon/Cloudflare"; import IconNetlify from "@theme/Icon/Netlify"; import OriginCopyright from "@theme-original/Footer/Copyright"; export default function FooterCopyright(props: Props) { return ( <>
Deployed by
); } ================================================ FILE: website/src/theme/Icon/Cloudflare.tsx ================================================ import React, { type ComponentProps } from "react"; export interface Props extends Omit, "viewBox"> {} export default function IconCloudflare(props: Props): React.ReactNode { return ( ); } ================================================ FILE: website/src/theme/Icon/Netlify.tsx ================================================ import React, { type ComponentProps } from "react"; export interface Props extends Omit, "viewBox"> {} export default function IconNetlify(props: Props): React.ReactNode { return ( ); } ================================================ FILE: website/src/theme/Page/TOC/Container/index.tsx ================================================ import React from "react"; import { useWindowSize } from "@nullbot/docusaurus-theme-nonepress/client"; import type { Props } from "@theme/Page/TOC/Container"; import OriginTOCContainer from "@theme-original/Page/TOC/Container"; import "./styles.css"; export default function TOCContainer({ children, ...props }: Props): React.ReactNode { const windowSize = useWindowSize(); const isClient = windowSize !== "ssr"; return ( {children} {isClient && (
)} ); } ================================================ FILE: website/src/theme/Page/TOC/Container/styles.css ================================================ .toc-ads { @apply max-w-full !m-0 !bg-transparent; & .wwads-text { @apply !text-base-content; @apply transition-[color] duration-500; } &-container { @apply shrink-0 w-full max-w-full px-4; } } ================================================ FILE: website/src/types/adapter.ts ================================================ import type { Tag } from "./tag"; type BaseAdapter = { module_name: string; project_link: string; name: string; desc: string; author: string; homepage: string; tags: Tag[]; is_official: boolean; }; export type Adapter = { resourceType: "adapter" } & BaseAdapter; export type AdaptersResponse = BaseAdapter[]; ================================================ FILE: website/src/types/bot.ts ================================================ import type { Tag } from "./tag"; type BaseBot = { name: string; desc: string; author: string; homepage: string; tags: Tag[]; is_official: boolean; }; export type Bot = { resourceType: "bot" } & BaseBot; export type BotsResponse = BaseBot[]; ================================================ FILE: website/src/types/driver.ts ================================================ import type { Tag } from "./tag"; type BaseDriver = { module_name: string; project_link: string; name: string; desc: string; author: string; homepage: string; tags: Tag[]; is_official: boolean; }; export type Driver = { resourceType: "driver" } & BaseDriver; export type DriversResponse = BaseDriver[]; ================================================ FILE: website/src/types/plugin.ts ================================================ import type { Tag } from "./tag"; type BasePlugin = { author: string; name: string; desc: string; homepage: string; is_official: boolean; module_name: string; project_link: string; skip_test: boolean; supported_adapters: string[] | null; tags: Array; time: string; type: string; valid: boolean; version: string; }; export type Plugin = { resourceType: "plugin" } & BasePlugin; export type PluginsResponse = BasePlugin[]; ================================================ FILE: website/src/types/tag.ts ================================================ export type Tag = { label: string; color: `#${string}`; }; ================================================ FILE: website/static/manifest.json ================================================ { "name": "NoneBot", "short_name": "NoneBot", "background-color": "#ffffff", "theme-color": "#ea5252", "description": "跨平台 Python 异步聊天机器人框架", "display": "standalone", "icons": [ { "src": "/icons/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, { "src": "/icons/android-chrome-384x384.png", "sizes": "384x384", "type": "image/png" } ] } ================================================ FILE: website/static/service-worker.js ================================================ self.addEventListener("install", () => { self.skipWaiting(); }); self.addEventListener("activate", () => { self.registration .unregister() .then(() => { return self.clients.matchAll(); }) .then((clients) => { clients.forEach((client) => client.navigate(client.url)); }); }); ================================================ FILE: website/static/uwu.js ================================================ if (location.search.includes("?uwu")) { document.documentElement.setAttribute("data-uwu", "true"); } ================================================ FILE: website/tailwind.config.ts ================================================ import typography from "@tailwindcss/typography"; import daisyui from "daisyui"; import themes from "daisyui/src/theming/themes"; const lightTheme = themes.light; const darkTheme = themes.dark; function excludeThemeColor( theme: { [key: string]: string }, exclude: string[] ): { [key: string]: string } { const newObj: { [key: string]: string } = {}; for (const key in theme) { if (exclude.includes(key)) { continue; } newObj[key] = theme[key]!; } return newObj; } export default { plugins: [typography, daisyui], daisyui: { base: false, themes: [ { light: { ...excludeThemeColor(lightTheme, [ "primary-content", "secondary-content", "accent-content", ]), primary: "#ea5252", "primary-content": "#ffffff", secondary: "#ef9fbc", accent: "#65c3c8", }, }, { dark: { ...excludeThemeColor(darkTheme, [ "primary-content", "secondary-content", "accent-content", ]), primary: "#ea5252", "primary-content": "#ffffff", secondary: "#ef9fbc", accent: "#65c3c8", }, }, ], darkTheme: false, }, darkMode: ["class", '[data-theme="dark"]'], }; ================================================ FILE: website/tsconfig.json ================================================ { // This file is not used in compilation. It is here just for a nice editor experience. "extends": "@nullbot/docusaurus-tsconfig", "compilerOptions": { "lib": ["DOM", "ESNext"], "baseUrl": ".", "paths": { "@/*": ["./src/*"], "@theme/*": ["./src/theme/*"] }, "resolveJsonModule": true, "allowArbitraryExtensions": true, // Duplicated from the root config, because TS does not support extending // multiple configs and we want to dogfood the @docusaurus/tsconfig one "allowUnreachableCode": false, "exactOptionalPropertyTypes": false, "noFallthroughCasesInSwitch": true, "noImplicitOverride": true, "noImplicitReturns": true, "noPropertyAccessFromIndexSignature": false, "noUncheckedIndexedAccess": true, "strict": true, "alwaysStrict": true, "noImplicitAny": true, "noImplicitThis": true, "strictBindCallApply": true, "strictFunctionTypes": true, "strictNullChecks": true, "strictPropertyInitialization": true, "useUnknownInCatchVariables": true, "noUnusedLocals": false, "noUnusedParameters": false, "importsNotUsedAsValues": "remove", // This is important. We run `yarn tsc` in website so we can catch issues // with our declaration files (mostly names that are forgotten to be // imported, invalid semantics...). Because we don't have end-to-end type // tests, removing this would make things much harder to catch. "skipLibCheck": false } } ================================================ FILE: website/versioned_docs/version-2.4.2/README.md ================================================ --- sidebar_position: 0 id: index slug: / --- # 概览 NoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架(下称 NoneBot),它基于 Python 的类型注解和异步优先特性(兼容同步),能够为你的需求实现提供便捷灵活的支持。同时,NoneBot 拥有大量的开发者为其开发插件,用户无需编写任何代码,仅需完成环境配置及插件安装,就可以正常使用 NoneBot。 需要注意的是,NoneBot 仅支持 **Python 3.9 以上版本** ## 特色 ### 异步优先 NoneBot 基于 Python [asyncio](https://docs.python.org/zh-cn/3/library/asyncio.html) / [trio](https://trio.readthedocs.io/en/stable/) 编写,并在异步机制的基础上进行了一定程度的同步函数兼容。 ### 完整的类型注解 NoneBot 参考 [PEP 484](https://www.python.org/dev/peps/pep-0484/) 等 PEP 完整实现了类型注解,通过 Pyright(Pylance) 检查。配合编辑器的类型推导功能,能将绝大多数的 Bug 杜绝在编辑器中([编辑器支持](./editor-support))。 ### 开箱即用 NoneBot 提供了使用便捷、具有交互式功能的命令行工具--`nb-cli`,使得用户初次接触 NoneBot 时更容易上手。使用方法请阅读本文档[指南](./quick-start.mdx)以及 [CLI 文档](https://cli.nonebot.dev/)。 ### 插件系统 插件系统是 NoneBot 的核心,通过它可以实现机器人的模块化以及功能扩展,便于维护和管理。 ### 依赖注入系统 NoneBot 采用了一套自行定义的依赖注入系统,可以让事件的处理过程更加的简洁、清晰,增加代码的可读性,减少代码冗余。 #### 什么是依赖注入 [**『依赖注入』**](https://zh.wikipedia.org/wiki/%E6%8E%A7%E5%88%B6%E5%8F%8D%E8%BD%AC)意思是,在编程中,有一种方法可以让你的代码声明它工作和使用所需要的东西,即**『依赖』**。 系统(在这里是指 NoneBot)将负责做任何需要的事情,为你的代码提供这些必要依赖(即**『注入』**依赖性) 这在你有以下情形的需求时非常有用: - 这部分代码拥有共享的逻辑(同样的代码逻辑多次重复) - 共享数据库以及网络请求连接会话 - 比如 `httpx.AsyncClient`、`aiohttp.ClientSession` 和 `sqlalchemy.Session` - 机器人用户权限检查以及认证 - 还有更多... 它在完成上述工作的同时,还能尽量减少代码的耦合和重复 ================================================ FILE: website/versioned_docs/version-2.4.2/advanced/adapter.md ================================================ --- sidebar_position: 1 description: 注册适配器与指定平台交互 options: menu: - category: advanced weight: 20 --- # 使用适配器 适配器 (Adapter) 是机器人与平台交互的核心桥梁,它负责在驱动器和机器人插件之间转换与传递消息。 ## 适配器功能与组成 适配器通常有两种功能,分别是**接收事件**和**调用平台接口**。其中,接收事件是指将驱动器收到的事件消息转换为 NoneBot 定义的事件模型,然后交由机器人插件处理;调用平台接口是指将机器人插件调用平台接口的数据转换为平台指定的格式,然后交由驱动器发送,并接收接口返回数据。 为了实现这两种功能,适配器通常由四个部分组成: - **Adapter**:负责转换事件和调用接口,正确创建 Bot 对象并注册到 NoneBot 中。 - **Bot**:负责存储平台机器人相关信息,并提供回复事件的方法。 - **Event**:负责定义事件内容,以及事件主体对象。 - **Message**:负责正确序列化消息,以便机器人插件处理。 ## 注册适配器 在使用适配器之前,我们需要先将适配器注册到驱动器中,这样适配器就可以通过驱动器接收事件和调用接口了。我们以 Console 适配器为例,来看看如何注册适配器: ```python {2,5} title=bot.py import nonebot from nonebot.adapters.console import Adapter driver = nonebot.get_driver() driver.register_adapter(Adapter) ``` 我们首先需要从适配器模块中导入所需要的适配器类,然后通过驱动器的 `register_adapter` 方法将适配器注册到驱动器中即可。如果我们需要多平台支持,可以多次调用 `register_adapter` 方法来注册多个适配器。 ## 获取已注册的适配器 NoneBot 提供了 `get_adapter` 方法来获取已注册的适配器,我们可以通过适配器的名称或类型来获取指定的适配器实例: ```python import nonebot from nonebot.adapters.console import Adapter adapters = nonebot.get_adapters() console_adapter = nonebot.get_adapter(Adapter) console_adapter = nonebot.get_adapter(Adapter.get_name()) ``` ## 获取 Bot 对象 当前所有适配器已连接的 Bot 对象可以通过 `get_bots` 方法获取,这是一个以机器人 ID 为键的字典: ```python import nonebot bots = nonebot.get_bots() ``` 我们也可以通过 `get_bot` 方法获取指定 ID 的 Bot 对象。如果省略 ID 参数,将会返回所有 Bot 中的第一个: ```python import nonebot bot = nonebot.get_bot("bot_id") ``` 如果需要获取指定适配器连接的 Bot 对象,我们可以通过适配器的 `bots` 属性获取,这也是一个以机器人 ID 为键的字典: ```python import nonebot from nonebot.adapters.console import Adapter console_adapter = nonebot.get_adapter(Adapter) bots = console_adapter.bots ``` Bot 对象都具有一个 `self_id` 属性,它是机器人的唯一 ID,由适配器填写,通常为机器人的帐号 ID 或者 APP ID。 ## 获取事件通用信息 适配器的所有事件模型均继承自 `Event` 基类,在[事件类型与重载](../appendices/overload.md)一节中,我们也提到了如何使用基类抽象方法来获取事件通用信息。基类能提供如下信息: ### 事件类型 事件类型通常为 `meta_event`、`message`、`notice`、`request`。 ```python type: str = event.get_type() ``` ### 事件名称 事件名称由适配器定义,通常用于日志记录。 ```python name: str = event.get_event_name() ``` ### 事件描述 事件描述由适配器定义,通常用于日志记录。 ```python description: str = event.get_event_description() ``` ### 事件日志字符串 事件日志字符串由事件名称和事件描述组成,用于日志记录。 ```python log: str = event.get_log_string() ``` ### 事件主体 ID 事件主体 ID 通常为机器人用户 ID。 ```python user_id: str = event.get_user_id() ``` ### 事件会话 ID 事件会话 ID 通常为机器人用户 ID 与群聊/频道 ID 组合而成。 ```python session_id: str = event.get_session_id() ``` ### 事件消息 如果事件包含消息,则可以通过该方法获取,否则会产生异常。 ```python message: Message = event.get_message() ``` ### 事件纯文本消息 通常为事件消息的纯文本内容,如果事件不包含消息,则会产生异常。 ```python text: str = event.get_plaintext() ``` ### 事件是否与机器人有关 由适配器实现的判断,通常将事件目标主体为机器人、消息中包含“@机器人”或以“机器人的昵称”开始视为与机器人有关。 ```python is_tome: bool = event.is_tome() ``` ## 更多 官方支持的适配器和社区贡献的适配器均可在[商店](/store/adapters)中查看。如果你想要开发自己的适配器,可以参考[开发文档](../developer/adapter-writing.md)。欢迎通过商店发布你的适配器。 ================================================ FILE: website/versioned_docs/version-2.4.2/advanced/dependency.mdx ================================================ --- sidebar_position: 6 description: 通过依赖注入获取上下文信息 options: menu: - category: advanced weight: 70 --- # 依赖注入 import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; 在事件处理流程中,事件响应器具有自己独立的上下文,例如:当前的事件、机器人等信息。在 NoneBot 中,这些信息通过依赖注入的方式提供给事件处理函数,可以让代码更加整洁可读、提升复用能力。 在了解如何使用依赖注入获取上下文信息之前,我们需要先了解两个概念: - `Dependent`:使用依赖注入的函数或其他任意可调用对象。如:事件处理函数、自定义的依赖函数等。 - `Dependency`:依赖注入的对象。如:当前事件、机器人等。 在之前的文档中,我们已经多次使用了依赖注入来获取事件信息。通过对函数参数依照一定规则填写类型注解,即可获得想要的上下文信息。任何一个事件处理函数在添加到事件处理流程时,都会根据一定规则提前将其解析成一个 `Dependent` 对象,方便运行时进行注入。如果遇到无法解析的参数,将会抛出 `ValueError("Unknown parameter")` 的异常。整个依赖注入系统可以分为两部分: - 参数解析 - 依据一定规则解析函数参数,识别 `Dependency` 依赖。 - 生成 `Dependent` 对象。 - 执行 - 根据已经解析的 `Dependency` 依赖,执行调用。 - 将所有 `Dependency` 的返回值根据参数名传入并调用 `Dependent` 。 :::danger 警告 在依赖注入中,类型注解是非常重要的,因为它不仅可以决定依赖注入的对象,还可以触发[重载机制](../appendices/overload.md#重载)。如果类型注解与实际获得数据类型不一致,将会跳过当前 `Dependent` 对象(即事件处理函数)。 ::: :::tip 提示 如果对于依赖注入的解析流程有疑问,可以调整[日志等级配置项](../appendices/config.mdx#log-level)为 `TRACE`,查看依赖解析日志。 ::: ## 同步支持 对于依赖注入系统中的 `Dependent` 或者 `Dependency` 对象,均支持同步类型的函数或可调用对象。例如: ```python {6,10} from nonebot import on_command from nonebot.params import Depends matcher = on_command("foo") def dependency() -> str: return "something" @matcher.handle() def _(result: str = Depends(dependency)): ... ``` ## 非依赖参数 在依赖注入解析中,任何无法解析的参数如果带有默认值,将会被视为非依赖参数。这些参数在依赖运行时将不会被注入而使用函数默认值。例如: ```python async def _(foo: str = "bar"): ... ``` ## 类型依赖注入 这一类的依赖注入仅需要在函数参数中添加对应的类型注解即可。 ### Bot 获取当前事件的 Bot 对象。 通过标注参数为 `Bot` 类型,或者一系列 `Bot` 类型,即可获取到当前事件的 Bot 对象。为兼容性考虑,如果参数名为 `bot` 且无类型注解,也会视为 Bot 依赖注入。 Bot 依赖注入支持重载(即:可以标注参数为子类型)且具有[重载优先检查权](../appendices/overload.md#重载)。 ```python from nonebot.adapters import Bot from nonebot.adapters.console import Bot as ConsoleBot from nonebot.adapters.onebot.v11 import Bot as OneBotV11Bot async def _(foo: Bot): ... async def _(foo: ConsoleBot | OneBotV11Bot): ... async def _(bot): ... # 兼容性处理 ``` ```python from typing import Union from nonebot.adapters import Bot from nonebot.adapters.console import Bot as ConsoleBot from nonebot.adapters.onebot.v11 import Bot as OneBotV11Bot async def _(foo: Bot): ... async def _(foo: Union[ConsoleBot, OneBotV11Bot]): ... async def _(bot): ... # 兼容性处理 ``` ### Event 获取当前事件。 通过标注参数为 `Event` 类型,或者一系列 `Event` 类型,即可获取到当前事件。为兼容性考虑,如果参数名为 `event` 且无类型注解,也会视为 Event 依赖注入。 Event 依赖注入支持重载(即:可以标注参数为子类型)且具有[重载优先检查权](../appendices/overload.md#重载)。 ```python from nonebot.adapters import Event from nonebot.adapters.onebot.v11 import PrivateMessageEvent, GroupMessageEvent async def _(foo: Event): ... async def _(foo: PrivateMessageEvent | GroupMessageEvent): ... async def _(event): ... # 兼容性处理 ``` ```python from typing import Union from nonebot.adapters import Event from nonebot.adapters.onebot.v11 import PrivateMessageEvent, GroupMessageEvent async def _(foo: Event): ... async def _(foo: Union[PrivateMessageEvent, GroupMessageEvent]): ... async def _(event): ... # 兼容性处理 ``` ### State 获取当前[会话状态](../appendices/session-state.md)。 通过标注参数为 `T_State` 类型,即可获取到当前会话状态。为兼容性考虑,如果参数名为 `state` 且无类型注解,也会视为 State 依赖注入。 ```python from nonebot.typing import T_State async def _(foo: T_State): ... ``` ### Matcher 获取当前事件响应器实例。常用于使用[事件响应器操作](../appendices/session-control.mdx)。 通过标注参数为 `Matcher` 类型,或者一系列 `Matcher` 类型,即可获取到当前事件。为兼容性考虑,如果参数名为 `matcher` 且无类型注解,也会视为 Matcher 依赖注入。 Matcher 依赖注入支持重载(即:可以标注参数为子类型)且具有[重载优先检查权](../appendices/overload.md#重载)。 ```python from nonebot.matcher import Matcher async def _(foo: Matcher): ... async def _(matcher): ... # 兼容性处理 ``` ### Exception 获取事件响应器运行中抛出的异常。该依赖注入目前仅在事件响应器运行后处理 Hook 中可用。 通过标注参数为异常类型,或者一系列异常类型,即可获取到事件响应器运行中抛出的异常。 ```python {5,8} from nonebot.message import run_postprocessor from nonebot.exception import ActionFailed, NetworkError @run_postprocessor async def _(e: Exception): ... @run_postprocessor async def _(e: ActionFailed | NetworkError): ... ``` ```python {6,9} from typing import Union from nonebot.message import run_postprocessor from nonebot.exception import ActionFailed, NetworkError @run_postprocessor async def _(e: Exception): ... @run_postprocessor async def _(e: Union[ActionFailed, NetworkError]): ... ``` ## 子依赖 在依赖注入系统中,我们可以定义一个子依赖,来执行自定义的操作,提高代码复用性以及处理性能。 ### 定义子依赖 子依赖使用 `Depends` 标记进行定义,其参数即依赖的函数或可调用对象,同样会被解析为 `Dependent` 对象,将会在依赖注入期间执行。我们来看一个例子: ```python {5,15} from typing import Annotated from nonebot import on_command from nonebot.adapters import Event from nonebot.params import Depends test = on_command("test") async def check(event: Event) -> Event: if event.get_user_id() in BLACKLIST: await test.finish() return event @test.handle() async def _(event: Annotated[Event, Depends(check)]): ... ``` ```python {3,13} from nonebot import on_command from nonebot.adapters import Event from nonebot.params import Depends test = on_command("test") async def check(event: Event) -> Event: if event.get_user_id() in BLACKLIST: await test.finish() return event @test.handle() async def _(event: Event = Depends(check)): ... ``` 在上面的代码中,我们使用 `Depends` 标记定义了一个子依赖 `check`。它判断事件主体用户是否在黑名单中,如果在,则直接结束事件处理流程。如果不在,则返回事件对象,以便事件处理函数可以继续执行。 通过将 `Depends` 包裹的子依赖作为参数的默认值,我们就可以在执行事件处理函数之前执行子依赖,并将其返回值作为参数传入事件处理函数。子依赖和普通的事件处理函数并没有区别,同样可以使用依赖注入,并且可以返回任何类型的值。但需要注意的是,如果事件处理函数参数的类型注解与子依赖返回值的类型**不一致**,将会触发[重载](../appendices/overload.md)而跳过当前事件处理函数。 特别的,我们可以为 `Dependent` 对象定义一系列前置子依赖,它们会在参数执行前被顺序执行,且返回值将会被忽略,例如: ```python {11} from nonebot import on_command from nonebot.adapters import Event from nonebot.params import Depends test = on_command("test") async def check(event: Event): if event.get_user_id() in BLACKLIST: await test.finish() @test.handle(parameterless=[Depends(check)]) async def _(): ... ``` ### 依赖缓存 NoneBot 在执行子依赖时,会将其返回值缓存起来。当我们在使用子依赖时,`Depends` 具有一个参数 `use_cache`,默认为 `True`。此时在事件处理流程中,多次使用同一个子依赖时,将会使用缓存中的结果而不会重复执行。这在很多情景中非常有用,例如: ```python {7} import random from typing import Annotated async def random_result() -> int: return random.randint(1, 100) async def _(x: Annotated[int, Depends(random_result)]): print(x) ``` ```python {6} import random async def random_result() -> int: return random.randint(1, 100) async def _(x: int = Depends(random_result)): print(x) ``` 此时,在同一事件处理流程中,这个随机函数的返回值将会保持一致。如果我们希望每次都重新执行子依赖,可以将 `use_cache` 设置为 `False`。 ```python {7} import random from typing import Annotated async def random_result() -> int: return random.randint(1, 100) async def _(x: Annotated[int, Depends(random_result, use_cache=False)]): print(x) ``` ```python {6} import random async def random_result() -> int: return random.randint(1, 100) async def _(x: int = Depends(random_result, use_cache=False)): print(x) ``` :::tip 提示 缓存的生命周期与当前接收到的事件相同。接收到事件后,子依赖在首次执行时缓存,在该事件处理完成后,缓存就会被清除。 ::: ### 类型转换与校验 在依赖注入系统中,我们可以对子依赖的返回值进行自动类型转换与校验。这个功能由 Pydantic 支持,因此我们通过参数类型注解自动使用 Pydantic 支持的类型转换。例如: ```python {6,9} from typing import Annotated from nonebot.params import Depends from nonebot.adapters import Event def get_user_id(event: Event) -> str: return event.get_user_id() async def _(user_id: Annotated[int, Depends(get_user_id, validate=True)]): print(user_id) ``` ```python {4,7} from nonebot.params import Depends from nonebot.adapters import Event def get_user_id(event: Event) -> str: return event.get_user_id() async def _(user_id: int = Depends(get_user_id, validate=True)): print(user_id) ``` 在进行类型自动转换的同时,Pydantic 还支持对数据进行更多的限制,如:大于、小于、长度等。使用方法如下: ```python {7,10} from typing import Annotated from pydantic import Field from nonebot.params import Depends from nonebot.adapters import Event def get_user_id(event: Event) -> str: return event.get_user_id() async def _(user_id: Annotated[int, Depends(get_user_id, validate=Field(gt=100))]): print(user_id) ``` ```python {5,8} from pydantic import Field from nonebot.params import Depends from nonebot.adapters import Event def get_user_id(event: Event) -> str: return event.get_user_id() async def _(user_id: int = Depends(get_user_id, validate=Field(gt=100))): print(user_id) ``` ### 类作为依赖 在前面的事例中,我们使用了函数作为子依赖。实际上,我们还可以使用类作为依赖。当我们在实例化一个类的时候,其实我们就在调用它,类本身也是一个可调用对象。例如: ```python {16} from typing import Annotated from dataclasses import dataclass from nonebot.params import Depends from nonebot.adapters import Event from nonebot.typing import T_State def get_context(state: T_State) -> dict: return state.setdefault("context", {}) @dataclass class ClassDependency: event: Event context: dict = Depends(get_context) async def _(data: Annotated[ClassDependency, Depends(ClassDependency)]): print(data.event, data.context) ``` ```python {15} from dataclasses import dataclass from nonebot.params import Depends from nonebot.adapters import Event from nonebot.typing import T_State def get_context(state: T_State) -> dict: return state.setdefault("context", {}) @dataclass class ClassDependency: event: Event context: dict = Depends(get_context) async def _(data: ClassDependency = Depends(ClassDependency)): print(data.event, data.context) ``` 可以看到,我们使用 `dataclass` 定义了一个类。由于这个类的 `__init__` 方法可以被依赖注入系统解析,因此,我们可以将其作为子依赖进行声明。特别地,对于类依赖,`Depends` 的参数可以为空,NoneBot 将会使用参数的类型注解进行解析与推断: ```python from typing import Annotated async def _(data: Annotated[ClassDependency, Depends()]): print(data.event, data.context) ``` ```python async def _(data: ClassDependency = Depends()): print(data.event, data.context) ``` ### 生成器作为依赖 NoneBot 的依赖注入支持依赖项在事件处理流程结束后进行一些额外的工作,比如数据库 session 或者网络 IO 的关闭,互斥锁的解锁等等。同时,由于[依赖缓存](#依赖缓存)的存在,我们可以通过这种方式来实现共享一个 session 等功能。 要实现上述功能,我们可以用生成器函数作为依赖项,我们用 `yield` 关键字取代 `return` 关键字,并在 `yield` 之后进行额外的工作。 我们可以看下述代码段, 使用 `httpx.AsyncClient` 异步网络 IO,并在事件处理流程中共用一个 client: ```python {15} from typing import Annotated from collections.abc import AsyncGenerator import httpx from nonebot.params import Depends async def get_client() -> AsyncGenerator[httpx.AsyncClient, None]: try: async with httpx.AsyncClient() as client: yield client finally: # 在这里进行额外的工作 @test.handle() async def _(x: Annotated[httpx.AsyncClient, Depends(get_client)]): resp = await x.get("https://nonebot.dev") ``` ```python {15} from collections.abc import AsyncGenerator import httpx from nonebot.params import Depends async def get_client() -> AsyncGenerator[httpx.AsyncClient, None]: try: async with httpx.AsyncClient() as client: yield client finally: # 在这里进行额外的工作 @test.handle() async def _(x: httpx.AsyncClient = Depends(get_client)): resp = await x.get("https://nonebot.dev") ``` :::caution 注意 生成器作为依赖时,其中只能进行一次 `yield`,否则将会触发异常。如果对此有疑问并想探究原因,可以参考 [contextmanager](https://docs.python.org/zh-cn/3/library/contextlib.html#contextlib.contextmanager) 和 [asynccontextmanager](https://docs.python.org/zh-cn/3/library/contextlib.html#contextlib.asynccontextmanager) 文档。事实上,NoneBot 内部就使用了这两个装饰器。 ::: ### 可调用对象作为依赖 在 Python 里,为类定义 `__call__` 方法就可以使得这个类的实例成为一个可调用对象。因此,我们也可以将定义了 `__call__` 方法的类的实例作为依赖。事实上,NoneBot 的[内置响应规则](./matcher.md#内置响应规则)就广泛使用了这种方式,以 `is_type` 规则为例: ```python from nonebot.adapters import Event class IsTypeRule: def __init__(self, *types: type[Event]): self.types = types async def __call__(self, event: Event) -> bool: return isinstance(event, self.types) ``` 我们在使用 `is_type` 时,即实例化了 `IsTypeRule` 类,然后将实例作为响应规则依赖项传入。 ## 其他依赖注入 这一类的依赖注入通常基于子依赖编写,为我们开发者提供更方便的途径获取上下文信息。 ### EventType 获取当前事件的类型。 ```python {4} from typing import Annotated from nonebot.params import EventType async def _(foo: Annotated[str, EventType()]): ... ``` ```python {3} from nonebot.params import EventType async def _(foo: str = EventType()): ... ``` ### EventMessage 获取当前事件的消息。 ```python {5} from typing import Annotated from nonebot.adapters import Message from nonebot.params import EventMessage async def _(foo: Annotated[Message, EventMessage()]): ... ``` ```python {4} from nonebot.adapters import Message from nonebot.params import EventMessage async def _(foo: Message = EventMessage()): ... ``` ### EventPlainText 获取当前事件的消息纯文本部分。 ```python {4} from typing import Annotated from nonebot.params import EventPlainText async def _(foo: Annotated[str, EventPlainText()]): ... ``` ```python {3} from nonebot.params import EventPlainText async def _(foo: str = EventPlainText()): ... ``` ### EventToMe 获取当前事件是否与机器人相关。 ```python {4} from typing import Annotated from nonebot.params import EventToMe async def _(foo: Annotated[bool, EventToMe()]): ... ``` ```python {3} from nonebot.params import EventToMe async def _(foo: bool = EventToMe()): ... ``` ### Command 获取当前命令型消息的元组形式命令名。 ```python {4} from typing import Annotated from nonebot.params import Command async def _(foo: Annotated[tuple[str, ...], Command()]): ... ``` ```python {4} from nonebot.params import Command async def _(foo: tuple[str, ...] = Command()): ... ``` :::tip 提示 命令详情只能在**触发命令型事件响应器时**获取。如果在事件处理后续流程中获取,则会获取到不同的值。 ::: ### RawCommand 获取当前命令型消息的文本形式命令名。 ```python {4} from typing import Annotated from nonebot.params import RawCommand async def _(foo: Annotated[str, RawCommand()]): ... ``` ```python {3} from nonebot.params import RawCommand async def _(foo: str = RawCommand()): ... ``` :::tip 提示 命令详情只能在**触发命令型事件响应器时**获取。如果在事件处理后续流程中获取,则会获取到不同的值。 ::: ### CommandArg 获取命令型消息命令后跟随的参数。 ```python {5} from typing import Annotated from nonebot.adapters import Message from nonebot.params import CommandArg async def _(foo: Annotated[Message, CommandArg()]): ... ``` ```python {4} from nonebot.adapters import Message from nonebot.params import CommandArg async def _(foo: Message = CommandArg()): ... ``` :::tip 提示 命令详情只能在**触发命令型事件响应器时**获取。如果在事件处理后续流程中获取,则会获取到不同的值。 ::: ### CommandStart 获取命令型消息命令前缀。 ```python {4} from typing import Annotated from nonebot.params import CommandStart async def _(foo: Annotated[str, CommandStart()]): ... ``` ```python {3} from nonebot.params import CommandStart async def _(foo: str = CommandStart()): ... ``` :::tip 提示 命令详情只能在**触发命令型事件响应器时**获取。如果在事件处理后续流程中获取,则会获取到不同的值。 ::: ### CommandWhitespace 获取命令型消息命令与参数间空白符。 ```python {4} from typing import Annotated from nonebot.params import CommandWhitespace async def _(foo: Annotated[str, CommandWhitespace()]): ... ``` ```python {3} from nonebot.params import CommandWhitespace async def _(foo: str = CommandWhitespace()): ... ``` :::tip 提示 命令详情只能在**触发命令型事件响应器时**获取。如果在事件处理后续流程中获取,则会获取到不同的值。 ::: ### ShellCommandArgv 获取 shell 命令解析前的参数列表,列表中可能包含文本字符串和富文本消息段(如:图片)。当词法解析出错的时候,返回值将为 `None`。通过重载机制即可处理两种不同的情况。 ```python {4} from typing import Annotated from nonebot import on_shell_command from nonebot.params import ShellCommandArgv matcher = on_shell_command("cmd") # 解析失败 @matcher.handle() async def _(foo: Annotated[None, ShellCommandArgv()]): ... # 解析成功 @matcher.handle() async def _(foo: Annotated[list[str | MessageSegment], ShellCommandArgv()]): ... ``` ```python {4} from nonebot import on_shell_command from nonebot.params import ShellCommandArgv matcher = on_shell_command("cmd") # 解析失败 @matcher.handle() async def _(foo: None = ShellCommandArgv()): ... # 解析成功 @matcher.handle() async def _(foo: list[str | MessageSegment] = ShellCommandArgv()): ... ``` ```python {4} from typing import Union, Annotated from nonebot import on_shell_command from nonebot.params import ShellCommandArgv matcher = on_shell_command("cmd") # 解析失败 @matcher.handle() async def _(foo: Annotated[None, ShellCommandArgv()]): ... # 解析成功 @matcher.handle() async def _(foo: Annotated[list[Union[str, MessageSegment]], ShellCommandArgv()]): ... ``` ```python {4} from typing import Union from nonebot import on_shell_command from nonebot.params import ShellCommandArgv matcher = on_shell_command("cmd") # 解析失败 @matcher.handle() async def _(foo: None = ShellCommandArgv()): ... # 解析成功 @matcher.handle() async def _(foo: list[Union[str, MessageSegment]] = ShellCommandArgv()): ... ``` ### ShellCommandArgs 获取 shell 命令解析后的参数 Namespace,支持 MessageSegment 富文本(如:图片)。 :::tip 提示 如果参数解析成功,则为 parser 返回的 Namespace;如果参数解析失败,则为 [`ParserExit`](../api/exception.md#ParserExit) 异常,并携带错误码与错误信息。在前置词法解析失败时,返回值也为 [`ParserExit`](../api/exception.md#ParserExit) 异常。通过重载机制即可处理两种不同的情况。 由于 `ArgumentParser` 在解析到 `--help` 参数时也会抛出异常,这种情况下错误码为 `0` 且错误信息即为帮助信息。 ::: ```python {14,22} from typing import Annotated from nonebot import on_shell_command from nonebot.exception import ParserExit from nonebot.params import ShellCommandArgs from nonebot.rule import Namespace, ArgumentParser parser = ArgumentParser("demo") # parser.add_argument ... matcher = on_shell_command("cmd", parser=parser) # 解析失败 @matcher.handle() async def _(foo: Annotated[ParserExit, ShellCommandArgs()]): if foo.status == 0: foo.message # help message else: foo.message # error message # 解析成功 @matcher.handle() async def _(foo: Annotated[Namespace, ShellCommandArgs()]): arg_dict = vars(foo) ``` ```python {12,20} from nonebot import on_shell_command from nonebot.exception import ParserExit from nonebot.params import ShellCommandArgs from nonebot.rule import Namespace, ArgumentParser parser = ArgumentParser("demo") # parser.add_argument ... matcher = on_shell_command("cmd", parser=parser) # 解析失败 @matcher.handle() async def _(foo: ParserExit = ShellCommandArgs()): if foo.status == 0: foo.message # help message else: foo.message # error message # 解析成功 @matcher.handle() async def _(foo: Namespace = ShellCommandArgs()): arg_dict = vars(foo) ``` ### RegexMatched 获取正则匹配结果的对象。 ```python {5} from re import Match from typing import Annotated from nonebot.params import RegexMatched async def _(foo: Annotated[Match[str], RegexMatched()]): ... ``` ```python {4} from re import Match from nonebot.params import RegexMatched async def _(foo: Match[str] = RegexMatched()): ... ``` ### RegexStr 获取正则匹配结果的文本。 ```python {4} from typing import Annotated from nonebot.params import RegexStr async def _(foo: Annotated[str, RegexStr()]): ... ``` ```python {3} from nonebot.params import RegexStr async def _(foo: str = RegexStr()): ... ``` ### RegexGroup 获取正则匹配结果的 group 元组。 ```python {4} from typing import Any, Annotated from nonebot.params import RegexGroup async def _(foo: Annotated[tuple[Any, ...], RegexGroup()]): ... ``` ```python {4} from typing import Any from nonebot.params import RegexGroup async def _(foo: tuple[Any, ...] = RegexGroup()): ... ``` ### RegexDict 获取正则匹配结果的 group 字典。 ```python {4} from typing import Any, Annotated from nonebot.params import RegexDict async def _(foo: Annotated[dict[str, Any], RegexDict()]): ... ``` ```python {4} from typing import Any from nonebot.params import RegexDict async def _(foo: dict[str, Any] = RegexDict()): ... ``` ### Startswith 获取触发响应器的消息前缀字符串。 ```python {4} from typing import Annotated from nonebot.params import Startswith async def _(foo: Annotated[str, Startswith()]): ... ``` ```python {3} from nonebot.params import Startswith async def _(foo: str = Startswith()): ... ``` ### Endswith 获取触发响应器的消息后缀字符串。 ```python {4} from typing import Annotated from nonebot.params import Endswith async def _(foo: Annotated[str, Endswith()]): ... ``` ```python {3} from nonebot.params import Endswith async def _(foo: str = Endswith()): ... ``` ### Fullmatch 获取触发响应器的消息字符串。 ```python {4} from typing import Annotated from nonebot.params import Fullmatch async def _(foo: Annotated[str, Fullmatch()]): ... ``` ```python {3} from nonebot.params import Fullmatch async def _(foo: str = Fullmatch()): ... ``` ### Keyword 获取触发响应器的关键字字符串。 ```python {4} from typing import Annotated from nonebot.params import Keyword async def _(foo: Annotated[str, Keyword()]): ... ``` ```python {3} from nonebot.params import Keyword async def _(foo: str = Keyword()): ... ``` ### Received 获取某次 `receive` 接收的事件。 ```python {7} from typing import Annotated from nonebot.adapters import Event from nonebot.params import Received @matcher.receive("id") async def _(foo: Annotated[Event, Received("id")]): ... ``` ```python {5} from nonebot.adapters import Event from nonebot.params import Received @matcher.receive("id") async def _(foo: Event = Received("id")): ... ``` ### LastReceived 获取最近一次 `receive` 接收的事件。 ```python {7} from typing import Annotated from nonebot.adapters import Event from nonebot.params import LastReceived @matcher.receive("any") async def _(foo: Annotated[Event, LastReceived()]): ... ``` ```python {5} from nonebot.adapters import Event from nonebot.params import LastReceived @matcher.receive("any") async def _(foo: Event = LastReceived()): ... ``` ### ReceivePromptResult 获取某次 `receive` 发送提示消息的结果。 ```python {6} from typing import Any, Annotated from nonebot.params import ReceivePromptResult @matcher.receive("id", prompt="prompt") async def _(result: Annotated[Any, ReceivePromptResult("id")]): ... ``` ```python {6} from typing import Any from nonebot.params import ReceivePromptResult @matcher.receive("id", prompt="prompt") async def _(result: Any = ReceivePromptResult("id")): ... ``` ### Arg 获取某次 `got` 接收的参数。如果 `Arg` 参数留空,则使用函数的参数名作为要获取的参数。 ```python {7,8} from typing import Annotated from nonebot.params import Arg from nonebot.adapters import Message @matcher.got("key") async def _(key: Annotated[Message, Arg()]): ... async def _(foo: Annotated[Message, Arg("key")]): ... ``` ```python {5,6} from nonebot.params import Arg from nonebot.adapters import Message @matcher.got("key") async def _(key: Message = Arg()): ... async def _(foo: Message = Arg("key")): ... ``` ### ArgStr 获取某次 `got` 接收的参数,并转换为字符串。如果 `Arg` 参数留空,则使用函数的参数名作为要获取的参数。 ```python {6,7} from typing import Annotated from nonebot.params import ArgStr @matcher.got("key") async def _(key: Annotated[str, ArgStr()]): ... async def _(foo: Annotated[str, ArgStr("key")]): ... ``` ```python {4,5} from nonebot.params import ArgStr @matcher.got("key") async def _(key: str = ArgStr()): ... async def _(foo: str = ArgStr("key")): ... ``` ### ArgPlainText 获取某次 `got` 接收的参数的纯文本部分。如果 `Arg` 参数留空,则使用函数的参数名作为要获取的参数。 ```python {6,7} from typing import Annotated from nonebot.params import ArgPlainText @matcher.got("key") async def _(key: Annotated[str, ArgPlainText()]): ... async def _(foo: Annotated[str, ArgPlainText("key")]): ... ``` ```python {4,5} from nonebot.params import ArgPlainText @matcher.got("key") async def _(key: str = ArgPlainText()): ... async def _(foo: str = ArgPlainText("key")): ... ``` ### ArgPromptResult 获取某次 `got` 发送提示消息的结果。如果 `Arg` 参数留空,则使用函数的参数名作为要获取的参数。 ```python {6,7} from typing import Any, Annotated from nonebot.params import ArgPromptResult @matcher.got("key", prompt="prompt") async def _(result: Annotated[Any, ArgPromptResult()]): ... async def _(result: Annotated[Any, ArgPromptResult("key")]): ... ``` ```python {6,7} from typing import Any from nonebot.params import ArgPromptResult @matcher.got("key", prompt="prompt") async def _(result: Any = ArgPromptResult()): ... async def _(result: Any = ArgPromptResult("key")): ... ``` ### PausePromptResult 获取最近一次 `pause` 发送提示消息的结果。 ```python {6} from typing import Any, Annotated from nonebot.params import PausePromptResult @matcher.handle() async def _(): await matcher.pause(prompt="prompt") @matcher.handle() async def _(result: Annotated[Any, PausePromptResult()]): ... ``` ```python {6} from typing import Any from nonebot.params import PausePromptResult @matcher.handle() async def _(): await matcher.pause(prompt="prompt") @matcher.handle() async def _(result: Any = PausePromptResult()): ... ``` ================================================ FILE: website/versioned_docs/version-2.4.2/advanced/driver.md ================================================ --- sidebar_position: 0 description: 选择合适的驱动器运行机器人 options: menu: - category: advanced weight: 10 --- # 选择驱动器 驱动器 (Driver) 是机器人运行的基石,它是机器人初始化的第一步,主要负责数据收发。 :::important 提示 驱动器的选择通常与机器人所使用的协议适配器相关,如果不知道该选择哪个驱动器,可以先阅读相关协议适配器文档说明。 ::: :::tip 提示 如何**安装**驱动器请参考[安装驱动器](../tutorial/store.mdx#安装驱动器)。 ::: ## 驱动器类型 驱动器类型大体上可以分为两种: - `Forward`:即客户端型驱动器,多用于使用 HTTP 轮询,连接 WebSocket 服务器等情形。 - `Reverse`:即服务端型驱动器,多用于使用 WebHook,接收 WebSocket 客户端连接等情形。 客户端型驱动器可以分为以下两种: 1. 异步发送 HTTP 请求,自定义 `HTTP Method`、`URL`、`Header`、`Body`、`Cookie`、`Proxy`、`Timeout` 等。 2. 异步建立 WebSocket 连接上下文,自定义 `WebSocket URL`、`Header`、`Cookie`、`Proxy`、`Timeout` 等。 服务端型驱动器目前有: 1. ASGI 应用框架,具有以下功能: - 协议适配器自定义 HTTP 上报地址以及对上报数据处理的回调函数。 - 协议适配器自定义 WebSocket 连接请求地址以及对 WebSocket 请求处理的回调函数。 - 用户可以向 ASGI 应用添加任何服务端相关功能,如:[添加自定义路由](./routing.md)。 ## 配置驱动器 驱动器的配置方法已经在[配置](../appendices/config.mdx)章节中简单进行了介绍,这里将详细介绍驱动器配置的格式。 NoneBot 中的客户端和服务端型驱动器可以相互配合使用,但服务端型驱动器**仅能选择一个**。所有驱动器模块都会包含一个 `Driver` 子类,即驱动器类,他可以作为驱动器单独运行。同时,客户端驱动器模块中还会提供一个 `Mixin` 子类,用于在与其他驱动器配合使用时加载。因此,驱动器配置格式采用特殊语法:`[:][+[:]]*`。 其中,`` 代表**驱动器模块路径**;`` 代表**驱动器类名**,默认为 `Driver`;`` 代表**驱动器混入类名**,默认为 `Mixin`。即,我们需要选择一个主要驱动器,然后在其基础上配合使用其他驱动器的功能。主要驱动器可以为客户端或服务端类型,但混入类驱动器只能为客户端类型。 特别的,为了简化内置驱动器模块路径,我们可以使用 `~` 符号作为内置驱动器模块路径的前缀,如 `~fastapi` 代表使用内置驱动器 `fastapi`。NoneBot 内置了多个驱动器适配,但需要安装额外依赖才能使用,具体请参考[安装驱动器](../tutorial/store.mdx#安装驱动器)。常见的驱动器配置如下: ```dotenv DRIVER=~fastapi DRIVER=~aiohttp DRIVER=~httpx+~websockets DRIVER=~fastapi+~httpx+~websockets ``` ## 获取驱动器 在 NoneBot 框架初始化完成后,我们就可以通过 `get_driver()` 方法获取全局驱动器实例: ```python from nonebot import get_driver driver = get_driver() ``` ## 内置驱动器 ### None **类型:**服务端驱动器 NoneBot 内置的空驱动器,不提供任何收发数据功能,可以在不需要外部网络连接时使用。 ```env DRIVER=~none ``` ### FastAPI(默认) **类型:**ASGI 服务端驱动器 > FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints. [FastAPI](https://fastapi.tiangolo.com/) 是一个易上手、高性能的异步 Web 框架,具有极佳的编写体验。 FastAPI 可以通过类型注解、依赖注入等方式实现输入参数校验、自动生成 API 文档等功能,也可以挂载其他 ASGI、WSGI 应用。 ```env DRIVER=~fastapi ``` #### FastAPI 配置项 ##### `fastapi_openapi_url` 类型:`str | None` 默认值:`None` 说明:`FastAPI` 提供的 `OpenAPI` JSON 定义地址,如果为 `None`,则不提供 `OpenAPI` JSON 定义。 ##### `fastapi_docs_url` 类型:`str | None` 默认值:`None` 说明:`FastAPI` 提供的 `Swagger` 文档地址,如果为 `None`,则不提供 `Swagger` 文档。 ##### `fastapi_redoc_url` 类型:`str | None` 默认值:`None` 说明:`FastAPI` 提供的 `ReDoc` 文档地址,如果为 `None`,则不提供 `ReDoc` 文档。 ##### `fastapi_include_adapter_schema` 类型:`bool` 默认值:`True` 说明:`FastAPI` 提供的 `OpenAPI` JSON 定义中是否包含适配器路由的 `Schema`。 ##### `fastapi_reload` :::caution 警告 不推荐开启该配置项,在 Windows 平台上开启该功能有可能会造成预料之外的影响!替代方案:使用 `nb-cli` 命令行工具以及参数 `--reload` 启动 NoneBot。 ```bash nb run --reload ``` 开启该功能后,在 uvicorn 运行时(FastAPI 提供的 ASGI 底层,即 reload 功能的实际来源),asyncio 使用的事件循环会被 uvicorn 从默认的 `ProactorEventLoop` 强制切换到 `SelectorEventLoop`。 > 相关信息参考 [uvicorn#529](https://github.com/encode/uvicorn/issues/529),[uvicorn#1070](https://github.com/encode/uvicorn/pull/1070),[uvicorn#1257](https://github.com/encode/uvicorn/pull/1257) 后者(`SelectorEventLoop`)在 Windows 平台的可使用性不如前者(`ProactorEventLoop`),包括但不限于 1. 不支持创建子进程 2. 最多只支持 512 个套接字 3. ... > 具体信息参考 [Python 文档](https://docs.python.org/zh-cn/3/library/asyncio-platforms.html#windows) 所以,一些使用了 asyncio 的库因此可能无法正常工作,如: 1. [playwright](https://playwright.dev/python/docs/library#incompatible-with-selectoreventloop-of-asyncio-on-windows) 如果在开启该功能后,原本**正常运行**的代码报错,且打印的异常堆栈信息和 asyncio 有关(异常一般为 `NotImplementedError`), 你可能就需要考虑相关库对事件循环的支持,以及是否启用该功能。 ::: 类型:`bool` 默认值:`False` 说明:是否开启 `uvicorn` 的 `reload` 功能,需要在机器人入口文件提供 ASGI 应用路径。 ```python title=bot.py app = nonebot.get_asgi() nonebot.run(app="bot:app") ``` ##### `fastapi_reload_dirs` 类型:`List[str] | None` 默认值:`None` 说明:重载监控文件夹列表,默认为 uvicorn 默认值 ##### `fastapi_reload_delay` 类型:`float | None` 默认值:`None` 说明:重载延迟,默认为 uvicorn 默认值 ##### `fastapi_reload_includes` 类型:`List[str] | None` 默认值:`None` 说明:要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值 ##### `fastapi_reload_excludes` 类型:`List[str] | None` 默认值:`None` 说明:不要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值 ##### `fastapi_extra` 类型:`Dist[str, Any]` 默认值:`{}` 说明:传递给 `FastAPI` 的其他参数 ### Quart **类型:**ASGI 服务端驱动器 > Quart is an asyncio reimplementation of the popular Flask microframework API. [Quart](https://quart.palletsprojects.com/) 是一个类 Flask 的异步版本,拥有与 Flask 非常相似的接口和使用方法。 ```env DRIVER=~quart ``` #### Quart 配置项 ##### `quart_reload` :::caution 警告 不推荐开启该配置项,在 Windows 平台上开启该功能有可能会造成预料之外的影响!替代方案:使用 `nb-cli` 命令行工具以及参数 `--reload` 启动 NoneBot。 ```bash nb run --reload ``` ::: 类型:`bool` 默认值:`False` 说明:是否开启 `uvicorn` 的 `reload` 功能,需要在机器人入口文件提供 ASGI 应用路径。 ```python title=bot.py app = nonebot.get_asgi() nonebot.run(app="bot:app") ``` ##### `quart_reload_dirs` 类型:`List[str] | None` 默认值:`None` 说明:重载监控文件夹列表,默认为 uvicorn 默认值 ##### `quart_reload_delay` 类型:`float | None` 默认值:`None` 说明:重载延迟,默认为 uvicorn 默认值 ##### `quart_reload_includes` 类型:`List[str] | None` 默认值:`None` 说明:要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值 ##### `quart_reload_excludes` 类型:`List[str] | None` 默认值:`None` 说明:不要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值 ##### `quart_extra` 类型:`Dist[str, Any]` 默认值:`{}` 说明:传递给 `Quart` 的其他参数 ### HTTPX **类型:**HTTP 客户端驱动器 :::caution 注意 本驱动器仅支持 HTTP 请求,不支持 WebSocket 连接请求。 ::: > [HTTPX](https://www.python-httpx.org/) is a fully featured HTTP client for Python 3, which provides sync and async APIs, and support for both HTTP/1.1 and HTTP/2. ```env DRIVER=~httpx ``` ### websockets **类型:**WebSocket 客户端驱动器 :::caution 注意 本驱动器仅支持 WebSocket 连接请求,不支持 HTTP 请求。 ::: > [websockets](https://websockets.readthedocs.io/) is a library for building WebSocket servers and clients in Python with a focus on correctness, simplicity, robustness, and performance. ```env DRIVER=~websockets ``` ### AIOHTTP **类型:**HTTP/WebSocket 客户端驱动器 > [AIOHTTP](https://docs.aiohttp.org/): Asynchronous HTTP Client/Server for asyncio and Python. ```env DRIVER=~aiohttp ``` ================================================ FILE: website/versioned_docs/version-2.4.2/advanced/matcher-provider.md ================================================ --- sidebar_position: 10 description: 自定义事件响应器存储 options: menu: - category: advanced weight: 110 --- # 事件响应器存储 事件响应器是 NoneBot 处理事件的核心,它们默认存储在一个字典中。在进入会话状态后,事件响应器将会转为临时响应器,作为最高优先级同样存储于该字典中。因此,事件响应器的存储类似于会话存储,它决定了整个 NoneBot 对事件的处理行为。 NoneBot 默认使用 Python 的字典将事件响应器存储于内存中,但是我们也可以自定义事件响应器存储,将事件响应器存储于其他地方,例如 Redis 等。这样我们就可以实现持久化、在多实例间共享会话状态等功能。 ## 编写存储提供者 事件响应器的存储提供者 `MatcherProvider` 抽象类继承自 `MutableMapping[int, list[type[Matcher]]]`,即以优先级为键,以事件响应器列表为值的映射。我们可以方便地进行逐优先级事件传播。 编写一个自定义的存储提供者,只需要继承并实现 `MatcherProvider` 抽象类: ```python from nonebot.matcher import MatcherProvider class CustomProvider(MatcherProvider): ... ``` ## 设置存储提供者 我们可以通过 `matchers.set_provider` 方法设置存储提供者: ```python {3} from nonebot.matcher import matchers matchers.set_provider(CustomProvider) assert isinstance(matchers.provider, CustomProvider) ``` ================================================ FILE: website/versioned_docs/version-2.4.2/advanced/matcher.md ================================================ --- sidebar_position: 5 description: 事件响应器组成与内置响应规则 options: menu: - category: advanced weight: 60 --- # 事件响应器进阶 在[指南](../tutorial/matcher.md)与[深入](../appendices/rule.md)中,我们已经介绍了事件响应器的基本用法以及响应规则、权限控制等功能。在这一节中,我们将介绍事件响应器的组成,内置的响应规则,与第三方响应规则拓展。 :::tip 提示 事件响应器允许继承,你可以通过直接继承 `Matcher` 类来创建一个新的事件响应器。 ::: ## 事件响应器组成 ### 事件响应器类型 事件响应器类型 `type` 即是该响应器所要响应的事件类型,只有在接收到的事件类型与该响应器的类型相同时,才会触发该响应器。如果类型为空字符串 `""`,则响应器将会响应所有类型的事件。事件响应器类型的检查在所有其他检查(权限控制、响应规则)之前进行。 NoneBot 内置了四种常用事件类型:`meta_event`、`message`、`notice`、`request`,分别对应元事件、消息、通知、请求。通常情况下,协议适配器会将事件合理地分类至这四种类型中。如果有其他类型的事件需要响应,可以自行定义新的类型。 ### 事件触发权限 事件触发权限 `permission` 是一个 `Permission` 对象,这在[权限控制](../appendices/permission.mdx)一节中已经介绍过。事件触发权限会在事件响应器的类型检查通过后进行检查,如果权限检查通过,则执行响应规则检查。 ### 事件响应规则 事件响应规则 `rule` 是一个 `Rule` 对象,这在[响应规则](../appendices/rule.md)一节中已经介绍过。事件响应器的响应规则会在事件响应器的权限检查通过后进行匹配,如果响应规则检查通过,则触发该响应器。 ### 响应优先级 响应优先级 `priority` 是一个正整数,用于指定响应器的优先级。响应器的优先级越小,越先被触发。如果响应器的优先级相同,则按照响应器的注册顺序进行触发。 ### 阻断 阻断 `block` 是一个布尔值,用于指定响应器是否阻断事件的传播。如果阻断为 `True`,则在该响应器被触发后,事件将不会再传播给其他下一优先级的响应器。 NoneBot 内置的事件响应器中,所有非 `command` 规则的 `message` 类型的事件响应器都会阻断事件传递,其他则不会。 在部分情况中,可以使用 [`stop_propagation`](../appendices/session-control.mdx#stop_propagation) 方法动态阻止事件传播,该方法需要 handler 在参数中获取 matcher 实例后调用方法。 ### 有效期 事件响应器的有效期分为 `temp` 和 `expire_time` 。`temp` 是一个布尔值,用于指定响应器是否为临时响应器。如果为 `True`,则该响应器在被触发后会被自动销毁。`expire_time` 是一个 `datetime` 对象,用于指定响应器的过期时间。如果 `expire_time` 不为 `None`,则在该时间点后,该响应器会被自动销毁。 ### 默认状态 事件响应器的默认状态 `default_state` 是一个 `dict` 对象,用于指定响应器的默认状态。在响应器被触发时,响应器将会初始化默认状态然后开始执行事件处理流程。 ## 基本辅助函数 NoneBot 为四种类型的事件响应器提供了五个基本的辅助函数: - `on`:创建任何类型的事件响应器。 - `on_metaevent`:创建元事件响应器。 - `on_message`:创建消息事件响应器。 - `on_request`:创建请求事件响应器。 - `on_notice`:创建通知事件响应器。 除了 `on` 函数具有一个 `type` 参数外,其余参数均相同: - `rule`:响应规则,可以是 `Rule` 对象或者 `RuleChecker` 函数。 - `permission`:事件触发权限,可以是 `Permission` 对象或者 `PermissionChecker` 函数。 - `handlers`:事件处理函数列表。 - `temp`:是否为临时响应器。 - `expire_time`:响应器的过期时间。 - `priority`:响应器的优先级。 - `block`:是否阻断事件传播。 - `state`:响应器的默认状态。 在消息类型的事件响应器的基础上,NoneBot 还内置了一些常用的响应规则,并结合为辅助函数来方便我们快速创建指定功能的响应器。下面我们逐个介绍。 ## 内置响应规则 :::tip 响应规则的使用方法可以参考 [深入 - 响应规则](../appendices/rule.md)。 ::: ### `startswith` `startswith` 响应规则用于匹配消息纯文本部分的开头是否与指定字符串(或一系列字符串)相同。可选参数 `ignorecase` 用于指定是否忽略大小写,默认为 `False`。 例如,我们可以创建一个匹配消息开头为 `!` 或者 `/` 的规则: ```python from nonebot.rule import startswith rule = startswith(("!", "/"), ignorecase=False) ``` 也可以直接使用辅助函数新建一个响应器: ```python from nonebot import on_startswith matcher = on_startswith(("!", "/"), ignorecase=False) ``` ### `endswith` `endswith` 响应规则用于匹配消息纯文本部分的结尾是否与指定字符串(或一系列字符串)相同。可选参数 `ignorecase` 用于指定是否忽略大小写,默认为 `False`。 例如,我们可以创建一个匹配消息结尾为 `.` 或者 `。` 的规则: ```python from nonebot.rule import endswith rule = endswith((".", "。"), ignorecase=False) ``` 也可以直接使用辅助函数新建一个响应器: ```python from nonebot import on_endswith matcher = on_endswith((".", "。"), ignorecase=False) ``` ### `fullmatch` `fullmatch` 响应规则用于匹配消息纯文本部分是否与指定字符串(或一系列字符串)完全相同。可选参数 `ignorecase` 用于指定是否忽略大小写,默认为 `False`。 例如,我们可以创建一个匹配消息为 `ping` 或者 `pong` 的规则: ```python from nonebot.rule import fullmatch rule = fullmatch(("ping", "pong"), ignorecase=False) ``` 也可以直接使用辅助函数新建一个响应器: ```python from nonebot import on_fullmatch matcher = on_fullmatch(("ping", "pong"), ignorecase=False) ``` ### `keyword` `keyword` 响应规则用于匹配消息纯文本部分是否包含指定字符串(或一系列字符串)。 例如,我们可以创建一个匹配消息中包含 `hello` 或者 `hi` 的规则: ```python from nonebot.rule import keyword rule = keyword("hello", "hi") ``` 也可以直接使用辅助函数新建一个响应器: ```python from nonebot import on_keyword matcher = on_keyword({"hello", "hi"}) ``` ### `command` `command` 是最常用的响应规则,它用于匹配消息是否为命令。它会根据配置中的 [Command Start 和 Command Separator](../appendices/config.mdx#command-start-和-command-separator) 来判断消息是否为命令。 例如,当我们配置了 `Command Start` 为 `/`,`Command Separator` 为 `.` 时: ```python from nonebot.rule import command # 匹配 "/help" 或者 "/帮助" 开头的消息 rule = command("help", "帮助") # 匹配 "/help.cmd" 开头的消息 rule = command(("help", "cmd")) ``` 也可以直接使用辅助函数新建一个响应器: ```python from nonebot import on_command matcher = on_command("help", aliases={"帮助"}) ``` 此外,`command` 响应规则默认允许消息命令与参数间不加空格,如果需要严格匹配命令与参数间的空白符,可以使用 `command` 函数的 `force_whitespace` 参数。`force_whitespace` 参数可以是 bool 类型或者具体的字符串,默认为 `False`。如果为 `True`,则命令与参数间必须有任意个数的空白符;如果为字符串,则命令与参数间必须有且与给定字符串一致的空白符。 ```python rule = command("help", force_whitespace=True) rule = command("help", force_whitespace=" ") ``` 命令解析后的结果可以通过 [`Command`](./dependency.mdx#command)、[`RawCommand`](./dependency.mdx#rawcommand)、[`CommandArg`](./dependency.mdx#commandarg)、[`CommandStart`](./dependency.mdx#commandstart)、[`CommandWhitespace`](./dependency.mdx#commandwhitespace) 依赖注入获取。 ### `shell_command` `shell_command` 响应规则用于匹配类 shell 命令形式的消息。它首先与 [`command`](#command) 响应规则一样进行命令匹配,如果匹配成功,则会进行进一步的参数解析。参数解析采用 `argparse` 标准库进行,在此基础上添加了消息序列 `Message` 支持。 例如,我们可以创建一个匹配 `/cmd` 命令并且带有 `-v` 选项与默认 `-h` 帮助选项的规则: ```python from nonebot.rule import shell_command, ArgumentParser parser = ArgumentParser() parser.add_argument("-v", "--verbose", action="store_true") rule = shell_command("cmd", parser=parser) ``` 更多关于 `argparse` 的使用方法请参考 [argparse 文档](https://docs.python.org/zh-cn/3/library/argparse.html)。我们也可以选择不提供 `parser` 参数,这样 `shell_command` 将不会解析参数,但会提供参数列表 `argv`。 直接使用辅助函数新建一个响应器: ```python from nonebot import on_shell_command from nonebot.rule import ArgumentParser parser = ArgumentParser() parser.add_argument("-v", "--verbose", action="store_true") matcher = on_shell_command("cmd", parser=parser) ``` 参数解析后的结果可以通过 [`ShellCommandArgv`](./dependency.mdx#shellcommandargv)、[`ShellCommandArgs`](./dependency.mdx#shellcommandargs) 依赖注入获取。 ### `regex` `regex` 响应规则用于匹配消息是否与指定正则表达式匹配。 :::tip 提示 正则表达式匹配使用 search 而非 match,如需从头匹配请使用 `r"^xxx"` 模式来确保匹配开头。 ::: 例如,我们可以创建一个匹配消息中包含字母并且忽略大小写的规则: ```python from nonebot.rule import regex rule = regex(r"[a-z]+", flags=re.IGNORECASE) ``` 也可以直接使用辅助函数新建一个响应器: ```python from nonebot import on_regex matcher = on_regex(r"[a-z]+", flags=re.IGNORECASE) ``` 正则匹配后的结果可以通过 [`RegexStr`](./dependency.mdx#regexstr)、[`RegexGroup`](./dependency.mdx#regexgroup)、[`RegexDict`](./dependency.mdx#regexdict) 依赖注入获取。 ### `to_me` `to_me` 响应规则用于匹配事件是否与机器人相关。 例如: ```python from nonebot.rule import to_me rule = to_me() ``` ### `is_type` `is_type` 响应规则用于匹配事件类型是否为指定类型(或者一系列类型)。 例如,我们可以创建一个匹配 OneBot v11 私聊和群聊消息事件的规则: ```python from nonebot.rule import is_type from nonebot.adapters.onebot.v11 import PrivateMessageEvent, GroupMessageEvent rule = is_type(PrivateMessageEvent, GroupMessageEvent) ``` ## 响应器组 为了更方便的管理一系列功能相近的响应器,NoneBot 提供了两种响应器组,它们可以帮助我们进行响应器的统一管理。 ### `CommandGroup` `CommandGroup` 可以用于管理一系列具有相同前置命令的子命令响应器。 例如,我们创建 `/cmd`、`/cmd.sub`、`/cmd.help` 三个命令,他们具有相同的优先级: ```python from nonebot import CommandGroup group = CommandGroup("cmd", priority=10) cmd = group.command(tuple()) sub_cmd = group.command("sub") help_cmd = group.command("help") ``` 命令别名 aliases 默认不会添加 `CommandGroup` 设定的前缀,如果需要为 aliases 添加前缀,可以添加 `prefix_aliases=True` 参数: ```python from nonebot import CommandGroup group = CommandGroup("cmd", prefix_aliases=True) cmd = group.command(tuple()) help_cmd = group.command("help", aliases={"帮助"}) ``` 这样就能成功匹配 `/cmd`、`/cmd.help`、`/cmd.帮助` 命令。如果未设置,将默认匹配 `/cmd`、`/cmd.help`、`/帮助` 命令。 ### `MatcherGroup` `MatcherGroup` 可以用于管理一系列具有相同属性的响应器。 例如,我们创建一个具有相同响应规则的响应器组: ```python from nonebot.rule import to_me from nonebot import MatcherGroup group = MatcherGroup(rule=to_me()) matcher1 = group.on_message() matcher2 = group.on_message() ``` ## 第三方响应规则 ### Alconna [`nonebot-plugin-alconna`](https://github.com/nonebot/plugin-alconna) 是一类提供了拓展响应规则的插件。 该插件使用 [Alconna](https://github.com/ArcletProject/Alconna) 作为命令解析器, 是一个简单、灵活、高效的命令参数解析器, 并且不局限于解析命令式字符串。 该插件提供了一类新的事件响应器辅助函数 `on_alconna`,以及 `AlconnaResult` 等依赖注入函数。 基于 `Alconna` 的特性,该插件同时提供了一系列便捷的消息段标注。 标注可用于在 `Alconna` 中匹配消息中除 text 外的其他消息段,也可用于快速创建各适配器下的消息段。所有标注位于 `nonebot_plugin_alconna.adapters` 中。 该插件同时通过提供 `UniMessage` (通用消息模型) 实现了**跨平台接收和发送消息**的功能。 详情请阅读最佳实践中的 [命令解析拓展](../best-practice/alconna/README.mdx) 章节。 ================================================ FILE: website/versioned_docs/version-2.4.2/advanced/plugin-info.md ================================================ --- sidebar_position: 2 description: 填写与获取插件相关的信息 options: menu: - category: advanced weight: 30 --- # 插件信息 NoneBot 是一个插件化的框架,可以通过加载插件来扩展功能。同时,我们也可以通过 NoneBot 的插件系统来获取相关信息,例如插件的名称、使用方法,用于收集帮助信息等。下面我们将介绍如何为插件添加元数据,以及如何获取插件信息。 ## 插件元数据 在 NoneBot 中,插件 [`Plugin`](../api/plugin/model.md#Plugin) 对象中存储了插件系统所需要的一系列信息。包括插件的索引名称、插件模块、插件中的事件响应器、插件父子关系等。通常,只有插件开发者才需要关心这些信息,而插件使用者或者机器人用户想要看到的是插件使用方法等帮助信息。因此,我们可以为插件添加插件元数据 `PluginMetadata`,它允许插件开发者为插件添加一些额外的信息。这些信息编写于插件模块的顶层,可以直接通过源码查看,或者通过 NoneBot 插件系统获取收集到的信息,通过其他方式发送给机器人用户等。 现在,假设我们有一个插件 `example`, 它的模块结构如下: ```tree {4-6} title=Project 📦 awesome-bot ├── 📂 awesome_bot │ └── 📂 plugins | └── 📂 example | ├── 📜 __init__.py | └── 📜 config.py ├── 📜 pyproject.toml └── 📜 README.md ``` 我们需要在插件顶层模块 `example/__init__.py` 中添加插件元数据,如下所示: ```python {1,5-12} title=example/__init__.py from nonebot.plugin import PluginMetadata from .config import Config __plugin_meta__ = PluginMetadata( name="示例插件", description="这是一个示例插件", usage="没什么用", type="application", config=Config, extra={}, ) ``` 我们可以看到,插件元数据 `PluginMetadata` 有三个基本属性:插件名称、插件描述、插件使用方法。除此之外,还有几个可选的属性(具体填写见[发布插件](../developer/plugin-publishing.mdx#插件元数据)章节): - `type`:插件类别,发布插件必填。当前有效类别有:`library`(为其他插件编写提供功能),`application`(向机器人用户提供功能); - `homepage`:插件项目主页,发布插件必填; - `config`:插件的[配置类](../appendices/config.mdx#插件配置),发布插件时如有配置类则必须填写; - `supported_adapters`:支持的适配器模块名集合,若插件只使用了 NoneBot 基本抽象,应显式填写 `None`; - `extra`:一个字典,可以用于存储任意信息。其他插件可以通过约定 `extra` 字典的键名来达成收集某些特殊信息的目的。 请注意,这里的**插件名称**是供使用者或机器人用户查看的人类可读名称,与插件索引名称无关。**插件索引名称(插件模块名称)**仅用于 NoneBot 插件系统**内部索引**。 ## 获取插件信息 NoneBot 提供了多种获取插件对象的方法,例如获取当前所有已导入的插件: ```python import nonebot plugins: set[Plugin] = nonebot.get_loaded_plugins() ``` 也可以通过插件索引名称获取插件对象: ```python import nonebot plugin: Plugin | None = nonebot.get_plugin("example") ``` 或者通过模块路径获取插件对象: ```python import nonebot plugin: Plugin | None = nonebot.get_plugin_by_module_name("awesome_bot.plugins.example") ``` 如果需要获取所有当前声明的插件名称(可能还未加载),可以使用 `get_available_plugin_names` 函数: ```python import nonebot plugin_names: set[str] = nonebot.get_available_plugin_names() ``` 插件对象 `Plugin` 中包含了多个属性: - `name`:插件索引名称 - `module`:插件模块 - `module_name`:插件模块路径 - `manager`:插件管理器 - `matcher`:插件中定义的事件响应器 - `parent_plugin`:插件的父插件 - `sub_plugins`:插件的子插件集合 - `metadata`:插件元数据 通过这些属性以及插件元数据,我们就可以收集所需要的插件信息了。 ================================================ FILE: website/versioned_docs/version-2.4.2/advanced/plugin-nesting.md ================================================ --- sidebar_position: 3 description: 编写与加载嵌套插件 options: menu: - category: advanced weight: 40 --- # 嵌套插件 NoneBot 支持嵌套插件,即一个插件可以包含其他插件。通过这种方式,我们可以将一个大型插件拆分成多个功能子插件,使得插件更加清晰、易于维护。我们可以直接在插件中使用 NoneBot 加载插件的方法来加载子插件。 ## 创建嵌套插件 我们可以在使用 `nb-cli` 命令[创建插件](../tutorial/create-plugin.md#创建插件)时,选择直接通过模板创建一个嵌套插件: ```bash $ nb plugin create [?] 插件名称: parent [?] 使用嵌套插件? (y/N) Y [?] 输出目录: awesome_bot/plugins ``` 或者使用 `nb plugin create --sub-plugin` 选项直接创建一个嵌套插件。 ## 已有插件 如果你已经有一个插件,想要在其中嵌套加载子插件,可以在插件的 `__init__.py` 中添加如下代码: ```python title=parent/__init__.py import nonebot from pathlib import Path sub_plugins = nonebot.load_plugins( str(Path(__file__).parent.joinpath("plugins").resolve()) ) ``` 这样,`parent` 插件就会加载 `parent/plugins` 目录下的所有插件。NoneBot 会正确识别这些插件的父子关系,你可以在 `parent` 的插件信息中看到这些子插件的信息,也可以在子插件信息中看到它们的父插件信息。 ================================================ FILE: website/versioned_docs/version-2.4.2/advanced/requiring.md ================================================ --- sidebar_position: 4 description: 使用其他插件提供的功能 options: menu: - category: advanced weight: 50 --- # 跨插件访问 NoneBot 插件化系统的设计使得插件之间可以功能独立、各司其职,我们可以更好地维护和扩展插件。但是,有时候我们可能需要在不同插件之间调用功能。NoneBot 生态中就有一类插件,它们专为其他插件提供功能支持,如:[定时任务插件](../best-practice/scheduler.md)、[数据存储插件](../best-practice/data-storing.md)等。这时候我们就需要在插件之间进行跨插件访问。 ## 插件跟踪 由于 NoneBot 插件系统通过 [Import Hooks](https://docs.python.org/3/reference/import.html#import-hooks) 的方式实现插件加载与跟踪管理,因此我们**不能**在 NoneBot 跟踪插件前进行模块 import,这会导致插件加载失败。即,我们不能在使用 NoneBot 提供的加载插件方法前,直接使用 `import` 语句导入插件。 对于在项目目录下的插件,我们通常直接使用 `load_from_toml` 等方法一次性加载所有插件。由于这些插件已经被声明,即便插件导入顺序不同,NoneBot 也能正确跟踪插件。此时,我们不需要对跨插件访问进行特殊处理。但当我们使用了外部插件,如果没有事先声明或加载插件,NoneBot 并不会将其当作插件进行跟踪,可能会出现意料之外的错误出现。 简单来说,我们必须在 `import` 外部插件之前,确保依赖的外部插件已经被声明或加载。 ## 插件依赖声明 NoneBot 提供了一种方法来确保我们依赖的插件已经被正确加载,即使用 `require` 函数。通过 `require` 函数,我们可以在当前插件中声明依赖的插件,NoneBot 会在加载当前插件时,检查依赖的插件是否已经被加载,如果没有,会尝试优先加载依赖的插件。 假设我们有一个插件 `a` 依赖于插件 `b`,我们可以在插件 `a` 中使用 `require` 函数声明其依赖于插件 `b`: ```python {3} title=a/__init__.py from nonebot import require require("b") from b import some_function ``` 其中,`require` 函数的参数为插件索引名称或者外部插件的模块名称。在完成依赖声明后,我们可以在插件 `a` 中直接导入插件 `b` 所提供的功能。 ================================================ FILE: website/versioned_docs/version-2.4.2/advanced/routing.md ================================================ --- sidebar_position: 9 description: 添加服务端路由规则 options: menu: - category: advanced weight: 100 --- # 添加路由 在[驱动器](./driver.md)一节中,我们了解了驱动器的两种类型。既然驱动器可以作为服务端运行,那么我们就可以向驱动器添加路由规则,从而实现自定义的 API 接口等功能。在添加路由规则时,我们需要注意驱动器的类型,详情可以参考[选择驱动器](./driver.md#配置驱动器)。 NoneBot 中,我们可以通过两种途径向 ASGI 驱动器添加路由规则: 1. 通过 NoneBot 的兼容层建立路由规则。 2. 直接向 ASGI 应用添加路由规则。 这两种途径各有优劣,前者可以在各种服务端型驱动器下运行,但并不能直接使用 ASGI 应用框架提供的特性与功能;后者直接使用 ASGI 应用,更自由、功能完整,但只能在特定类型驱动器下运行。 在向驱动器添加路由规则时,我们需要注意驱动器是否为服务端类型,我们可以通过以下方式判断: ```python from nonebot import get_driver from nonebot.drivers import ASGIMixin # highlight-next-line can_use = isinstance(get_driver(), ASGIMixin) ``` ## 通过兼容层添加路由 NoneBot 兼容层定义了两个数据类 `HTTPServerSetup` 和 `WebSocketServerSetup`,分别用于定义 HTTP 服务端和 WebSocket 服务端的路由规则。 ### HTTP 路由 `HTTPServerSetup` 具有四个属性: - `path`:路由路径,不支持特殊占位表达式。类型为 `URL`。 - `method`:请求方法。类型为 `str`。 - `name`:路由名称,不可重复。类型为 `str`。 - `handle_func`:路由处理函数。类型为 `Callable[[Request], Awaitable[Response]]`。 例如,我们添加一个 `/hello` 的路由,当请求方法为 `GET` 时,返回 `200 OK` 以及返回体信息: ```python from nonebot import get_driver from nonebot.drivers import URL, Request, Response, ASGIMixin, HTTPServerSetup async def hello(request: Request) -> Response: return Response(200, content="Hello, world!") if isinstance((driver := get_driver()), ASGIMixin): driver.setup_http_server( HTTPServerSetup( path=URL("/hello"), method="GET", name="hello", handle_func=hello, ) ) ``` 对于 `Request` 和 `Response` 的详细信息,可以参考 [API 文档](../api/drivers/index.md)。 ### WebSocket 路由 `WebSocketServerSetup` 具有三个属性: - `path`:路由路径,不支持特殊占位表达式。类型为 `URL`。 - `name`:路由名称,不可重复。类型为 `str`。 - `handle_func`:路由处理函数。类型为 `Callable[[WebSocket], Awaitable[Any]]`。 例如,我们添加一个 `/ws` 的路由,发送所有接收到的数据: ```python from nonebot import get_driver from nonebot.drivers import URL, ASGIMixin, WebSocket, WebSocketServerSetup async def ws_handler(ws: WebSocket): await ws.accept() try: while True: data = await ws.receive() await ws.send(data) except WebSocketClosed as e: # handle closed ... finally: with contextlib.suppress(Exception): await websocket.close() # do some cleanup if isinstance((driver := get_driver()), ASGIMixin): driver.setup_websocket_server( WebSocketServerSetup( path=URL("/ws"), name="ws", handle_func=ws_handler, ) ) ``` 对于 `WebSocket` 的详细信息,可以参考 [API 文档](../api/drivers/index.md)。 ## 使用 ASGI 应用添加路由 ### 获取 ASGI 应用 NoneBot 服务端类型的驱动器具有两个属性 `server_app` 和 `asgi`,分别对应驱动框架应用和 ASGI 应用。通常情况下,这两个应用是同一个对象。我们可以通过 `get_app()` 方法快速获取: ```python import nonebot app = nonebot.get_app() asgi = nonebot.get_asgi() ``` ### 添加路由规则 在获取到了 ASGI 应用后,我们就可以直接使用 ASGI 应用框架提供的功能来添加路由规则了。这里我们以 [FastAPI](./driver.md#fastapi默认) 为例,演示如何添加路由规则。 在下面的代码中,我们添加了一个 `GET` 类型的 `/api` 路由,具体方法参考 [FastAPI 文档](https://fastapi.tiangolo.com/)。 ```python import nonebot from fastapi import FastAPI app: FastAPI = nonebot.get_app() @app.get("/api") async def custom_api(): return {"message": "Hello, world!"} ``` ================================================ FILE: website/versioned_docs/version-2.4.2/advanced/runtime-hook.md ================================================ --- sidebar_position: 8 description: 在特定的生命周期中执行代码 options: menu: - category: advanced weight: 90 --- # 钩子函数 > [钩子编程](https://zh.wikipedia.org/wiki/%E9%92%A9%E5%AD%90%E7%BC%96%E7%A8%8B)(hooking),也称作“挂钩”,是计算机程序设计术语,指通过拦截软件模块间的函数调用、消息传递、事件传递来修改或扩展操作系统、应用程序或其他软件组件的行为的各种技术。处理被拦截的函数调用、事件、消息的代码,被称为钩子(hook)。 在 NoneBot 中有一系列预定义的钩子函数,可以分为两类:**全局钩子函数**和**事件处理钩子函数**,这些钩子函数可以用装饰器的形式来使用。 ## 全局钩子函数 全局钩子函数是指 NoneBot 针对其本身运行过程的钩子函数。 这些钩子函数是由驱动器来运行的,故需要先[获得全局驱动器](./driver.md#获取驱动器)。 ### 启动准备 这个钩子函数会在 NoneBot 启动时运行。很多时候,我们并不希望在模块被导入时就执行一些耗时操作,如:连接数据库,这时候我们可以在这个钩子函数中进行这些操作。 ```python from nonebot import get_driver driver = get_driver() @driver.on_startup async def do_something(): pass ``` ### 终止处理 这个钩子函数会在 NoneBot 终止时运行。我们可以在这个钩子函数中进行一些清理工作,如:关闭数据库连接。 ```python from nonebot import get_driver driver = get_driver() @driver.on_shutdown async def do_something(): pass ``` ### Bot 连接处理 这个钩子函数会在任何协议适配器连接 `Bot` 对象至 NoneBot 时运行。支持依赖注入,可以直接注入 `Bot` 对象。 ```python from nonebot import get_driver driver = get_driver() @driver.on_bot_connect async def do_something(bot: Bot): pass ``` ### Bot 断开处理 这个钩子函数会在 `Bot` 断开与 NoneBot 的连接时运行。支持依赖注入,可以直接注入 `Bot` 对象。 ```python from nonebot import get_driver driver = get_driver() @driver.on_bot_disconnect async def do_something(bot: Bot): pass ``` ## 事件处理钩子函数 这些钩子函数指的是影响 NoneBot 进行**事件处理**的函数, 这些函数可以跟普通的事件处理函数一样接受相应的参数。 ### 事件预处理 这个钩子函数会在 NoneBot 接收到新的事件时运行。支持依赖注入,可以注入 `Bot` 对象、事件、会话状态。在这个钩子函数内抛出 `nonebot.exception.IgnoredException` 会使 NoneBot 忽略该事件。 ```python from nonebot.exception import IgnoredException from nonebot.message import event_preprocessor @event_preprocessor async def do_something(event: Event): if not event.is_tome(): raise IgnoredException("some reason") ``` ### 事件后处理 这个钩子函数会在 NoneBot 处理事件完成后运行。支持依赖注入,可以注入 `Bot` 对象、事件、会话状态。 ```python from nonebot.message import event_postprocessor @event_postprocessor async def do_something(event: Event): pass ``` ### 运行预处理 这个钩子函数会在 NoneBot 运行事件响应器前运行。支持依赖注入,可以注入 `Bot` 对象、事件、事件响应器、会话状态。在这个钩子函数内抛出 `nonebot.exception.IgnoredException` 也会使 NoneBot 忽略本次运行。 ```python from nonebot.message import run_preprocessor from nonebot.exception import IgnoredException @run_preprocessor async def do_something(event: Event, matcher: Matcher): if not event.is_tome(): raise IgnoredException("some reason") ``` ### 运行后处理 这个钩子函数会在 NoneBot 运行事件响应器后运行。支持依赖注入,可以注入 `Bot` 对象、事件、事件响应器、会话状态、运行中产生的异常。 ```python from nonebot.message import run_postprocessor @run_postprocessor async def do_something(event: Event, matcher: Matcher, exception: Optional[Exception]): pass ``` ### 平台接口调用钩子 这个钩子函数会在 `Bot` 对象调用平台接口时运行。在这个钩子函数中,我们可以通过引起 `MockApiException` 异常来阻止 `Bot` 对象调用平台接口并返回指定的结果。 ```python from nonebot.adapters import Bot from nonebot.exception import MockApiException @Bot.on_calling_api async def handle_api_call(bot: Bot, api: str, data: Dict[str, Any]): if api == "send_msg": raise MockApiException(result={"message_id": 123}) ``` ### 平台接口调用后钩子 这个钩子函数会在 `Bot` 对象调用平台接口后运行。在这个钩子函数中,我们可以通过引起 `MockApiException` 异常来忽略平台接口返回的结果并返回指定的结果。 ```python from nonebot.adapters import Bot from nonebot.exception import MockApiException @Bot.on_called_api async def handle_api_result( bot: Bot, exception: Optional[Exception], api: str, data: Dict[str, Any], result: Any ): if not exception and api == "send_msg": raise MockApiException(result={**result, "message_id": 123}) ``` ================================================ FILE: website/versioned_docs/version-2.4.2/advanced/session-updating.md ================================================ --- sidebar_position: 7 description: 控制会话响应对象 options: menu: - category: advanced weight: 80 --- # 会话更新 在 NoneBot 中,在某个事件响应器对事件响应后,即是进入了会话状态,会话状态会持续到整个事件响应流程结束。会话过程中,机器人可以与用户进行多次交互。每次需要等待用户事件时,NoneBot 将会复制一个新的临时事件响应器,并更新该事件响应器使其响应当前会话主体的消息,这个过程称为会话更新。 会话更新分为两部分:**更新[事件响应器类型](./matcher.md#事件响应器类型)**和**更新[事件触发权限](./matcher.md#事件触发权限)**。 ## 更新事件响应器类型 通常情况下,与机器人用户进行的会话都是通过消息事件进行的,因此会话更新后的默认响应事件类型为 `message`。如果希望接收一个特定类型的消息,比如 `notice` 等,我们需要自定义响应事件类型更新函数。响应事件类型更新函数是一个 `Dependent`,可以使用依赖注入。 ```python {3-5} foo = on_message() @foo.type_updater async def _() -> str: return "notice" ``` 在注册了上述响应事件类型更新函数后,当我们需要等待用户事件时,将只会响应 `notice` 类型的事件。如果希望在会话过程中的不同阶段响应不同类型的事件,我们就需要使用更复杂的逻辑来更新响应事件类型(如:根据会话状态),这里将不再展示。 ## 更新事件触发权限 会话通常是由机器人与用户进行的一对一交互,因此会话更新后的默认触发权限为当前事件的会话 ID。这个会话 ID 由协议适配器生成,通常由用户 ID 和群 ID 等组成。如果希望实现更复杂的会话功能(如:多用户同时参与的会话),我们需要自定义触发权限更新函数。触发权限更新函数是一个 `Dependent`,可以使用依赖注入。 ```python {5-7} from nonebot.permission import User foo = on_message() @foo.permission_updater async def _(event: Event, matcher: Matcher) -> Permission: return Permission(User.from_event(event, perm=matcher.permission)) ``` 上述权限更新函数是默认的权限更新函数,它将会话的触发权限更新为当前事件的会话 ID。如果我们希望响应多个用户的消息,我们可以如下修改: ```python {5-7} from nonebot.permission import USER foo = on_message() @foo.permission_updater async def _(matcher: Matcher) -> Permission: return USER("session1", "session2", perm=matcher.permission) ``` 请注意,此处为全大写字母的 `USER` 权限,它可以匹配多个会话 ID。通过这种方式,我们可以实现多用户同时参与的会话。 我们已经了解了如何控制会话的更新,相信你已经能够实现更复杂的会话功能了,例如多人小游戏等等。欢迎将你的作品分享到[插件商店](/store/plugins)。 ================================================ FILE: website/versioned_docs/version-2.4.2/api/.gitkeep ================================================ ================================================ FILE: website/versioned_docs/version-2.4.2/api/adapters/_category_.json ================================================ { "position": 15 } ================================================ FILE: website/versioned_docs/version-2.4.2/api/adapters/index.md ================================================ --- mdx: format: md sidebar_position: 0 description: nonebot.adapters 模块 --- # nonebot.adapters 本模块定义了协议适配基类,各协议请继承以下基类。 使用 [Driver.register_adapter](../drivers/index.md#Driver-register-adapter) 注册适配器。 ## _abstract class_ `Adapter(driver, **kwargs)` {#Adapter} - **说明** 协议适配器基类。 通常,在 Adapter 中编写协议通信相关代码,如: 建立通信连接、处理接收与发送 data 等。 - **参数** - `driver` ([Driver](../drivers/index.md#Driver)): [Driver](../drivers/index.md#Driver) 实例 - `**kwargs` (Any): 其他由 [Driver.register_adapter](../drivers/index.md#Driver-register-adapter) 传入的额外参数 ### _instance-var_ `driver` {#Adapter-driver} - **类型:** [Driver](../drivers/index.md#Driver) - **说明:** 实例 ### _instance-var_ `bots` {#Adapter-bots} - **类型:** dict[str, [Bot](#Bot)] - **说明:** 本协议适配器已建立连接的 [Bot](#Bot) 实例 ### _abstract classmethod_ `get_name()` {#Adapter-get-name} - **说明:** 当前协议适配器的名称 - **参数** empty - **返回** - str ### _property_ `config` {#Adapter-config} - **类型:** [Config](../config.md#Config) - **说明:** 全局 NoneBot 配置 ### _method_ `bot_connect(bot)` {#Adapter-bot-connect} - **说明** 告知 NoneBot 建立了一个新的 [Bot](#Bot) 连接。 当有新的 [Bot](#Bot) 实例连接建立成功时调用。 - **参数** - `bot` ([Bot](#Bot)): [Bot](#Bot) 实例 - **返回** - None ### _method_ `bot_disconnect(bot)` {#Adapter-bot-disconnect} - **说明** 告知 NoneBot [Bot](#Bot) 连接已断开。 当有 [Bot](#Bot) 实例连接断开时调用。 - **参数** - `bot` ([Bot](#Bot)): [Bot](#Bot) 实例 - **返回** - None ### _method_ `setup_http_server(setup)` {#Adapter-setup-http-server} - **说明:** 设置一个 HTTP 服务器路由配置 - **参数** - `setup` ([HTTPServerSetup](../drivers/index.md#HTTPServerSetup)) - **返回** - untyped ### _method_ `setup_websocket_server(setup)` {#Adapter-setup-websocket-server} - **说明:** 设置一个 WebSocket 服务器路由配置 - **参数** - `setup` ([WebSocketServerSetup](../drivers/index.md#WebSocketServerSetup)) - **返回** - untyped ### _async method_ `request(setup)` {#Adapter-request} - **说明:** 进行一个 HTTP 客户端请求 - **参数** - `setup` ([Request](../drivers/index.md#Request)) - **返回** - [Response](../drivers/index.md#Response) ### _method_ `websocket(setup)` {#Adapter-websocket} - **说明:** 建立一个 WebSocket 客户端连接请求 - **参数** - `setup` ([Request](../drivers/index.md#Request)) - **返回** - AsyncGenerator[[WebSocket](../drivers/index.md#WebSocket), None] ### _method_ `on_ready(func)` {#Adapter-on-ready} - **参数** - `func` (LIFESPAN_FUNC) - **返回** - LIFESPAN_FUNC ## _abstract class_ `Bot(adapter, self_id)` {#Bot} - **说明** Bot 基类。 用于处理上报消息,并提供 API 调用接口。 - **参数** - `adapter` ([Adapter](#Adapter)): 协议适配器实例 - `self_id` (str): 机器人 ID ### _instance-var_ `adapter` {#Bot-adapter} - **类型:** [Adapter](#Adapter) - **说明:** 协议适配器实例 ### _instance-var_ `self_id` {#Bot-self-id} - **类型:** str - **说明:** 机器人 ID ### _property_ `type` {#Bot-type} - **类型:** str - **说明:** 协议适配器名称 ### _property_ `config` {#Bot-config} - **类型:** [Config](../config.md#Config) - **说明:** 全局 NoneBot 配置 ### _async method_ `call_api(api, **data)` {#Bot-call-api} - **说明:** 调用机器人 API 接口,可以通过该函数或直接通过 bot 属性进行调用 - **参数** - `api` (str): API 名称 - `**data` (Any): API 数据 - **返回** - Any - **用法** ```python await bot.call_api("send_msg", message="hello world") await bot.send_msg(message="hello world") ``` ### _abstract async method_ `send(event, message, **kwargs)` {#Bot-send} - **说明:** 调用机器人基础发送消息接口 - **参数** - `event` ([Event](#Event)): 上报事件 - `message` (str | [Message](#Message) | [MessageSegment](#MessageSegment)): 要发送的消息 - `**kwargs` (Any): 任意额外参数 - **返回** - Any ### _classmethod_ `on_calling_api(func)` {#Bot-on-calling-api} - **说明** 调用 api 预处理。 钩子函数参数: - bot: 当前 bot 对象 - api: 调用的 api 名称 - data: api 调用的参数字典 - **参数** - `func` ([T_CallingAPIHook](../typing.md#T-CallingAPIHook)) - **返回** - [T_CallingAPIHook](../typing.md#T-CallingAPIHook) ### _classmethod_ `on_called_api(func)` {#Bot-on-called-api} - **说明** 调用 api 后处理。 钩子函数参数: - bot: 当前 bot 对象 - exception: 调用 api 时发生的错误 - api: 调用的 api 名称 - data: api 调用的参数字典 - result: api 调用的返回 - **参数** - `func` ([T_CalledAPIHook](../typing.md#T-CalledAPIHook)) - **返回** - [T_CalledAPIHook](../typing.md#T-CalledAPIHook) ## _abstract class_ `Event()` {#Event} - **说明:** Event 基类。提供获取关键信息的方法,其余信息可直接获取。 - **参数** auto ### _abstract method_ `get_type()` {#Event-get-type} - **说明:** 获取事件类型的方法,类型通常为 NoneBot 内置的四种类型。 - **参数** empty - **返回** - str ### _abstract method_ `get_event_name()` {#Event-get-event-name} - **说明:** 获取事件名称的方法。 - **参数** empty - **返回** - str ### _abstract method_ `get_event_description()` {#Event-get-event-description} - **说明:** 获取事件描述的方法,通常为事件具体内容。 - **参数** empty - **返回** - str ### _method_ `get_log_string()` {#Event-get-log-string} - **说明** 获取事件日志信息的方法。 通常你不需要修改这个方法,只有当希望 NoneBot 隐藏该事件日志时, 可以抛出 `NoLogException` 异常。 - **参数** empty - **返回** - str - **异常** - NoLogException: 希望 NoneBot 隐藏该事件日志 ### _abstract method_ `get_user_id()` {#Event-get-user-id} - **说明:** 获取事件主体 id 的方法,通常是用户 id 。 - **参数** empty - **返回** - str ### _abstract method_ `get_session_id()` {#Event-get-session-id} - **说明:** 获取会话 id 的方法,用于判断当前事件属于哪一个会话, 通常是用户 id、群组 id 组合。 - **参数** empty - **返回** - str ### _abstract method_ `get_message()` {#Event-get-message} - **说明:** 获取事件消息内容的方法。 - **参数** empty - **返回** - [Message](#Message) ### _method_ `get_plaintext()` {#Event-get-plaintext} - **说明** 获取消息纯文本的方法。 通常不需要修改,默认通过 `get_message().extract_plain_text` 获取。 - **参数** empty - **返回** - str ### _abstract method_ `is_tome()` {#Event-is-tome} - **说明:** 获取事件是否与机器人有关的方法。 - **参数** empty - **返回** - bool ## _abstract class_ `Message()` {#Message} - **说明:** 消息序列 - **参数** - `message`: 消息内容 ### _classmethod_ `template(format_string)` {#Message-template} - **说明** 创建消息模板。 用法和 `str.format` 大致相同,支持以 `Message` 对象作为消息模板并输出消息对象。 并且提供了拓展的格式化控制符, 可以通过该消息类型的 `MessageSegment` 工厂方法创建消息。 - **参数** - `format_string` (str | TM): 格式化模板 - **返回** - [MessageTemplate](#MessageTemplate)[Self]: 消息格式化器 ### _abstract classmethod_ `get_segment_class()` {#Message-get-segment-class} - **说明:** 获取消息段类型 - **参数** empty - **返回** - type[TMS] ### _abstract staticmethod_ `_construct(msg)` {#Message--construct} - **说明:** 构造消息数组 - **参数** - `msg` (str) - **返回** - Iterable[TMS] ### _method_ `__getitem__(args)` {#Message---getitem--} - **重载** **1.** `(args) -> Self` - **参数** - `args` (str): 消息段类型 - **返回** - Self: 所有类型为 `args` 的消息段 **2.** `(args) -> TMS` - **参数** - `args` (tuple[str, int]): 消息段类型和索引 - **返回** - TMS: 类型为 `args[0]` 的消息段第 `args[1]` 个 **3.** `(args) -> Self` - **参数** - `args` (tuple[str, slice]): 消息段类型和切片 - **返回** - Self: 类型为 `args[0]` 的消息段切片 `args[1]` **4.** `(args) -> TMS` - **参数** - `args` (int): 索引 - **返回** - TMS: 第 `args` 个消息段 **5.** `(args) -> Self` - **参数** - `args` (slice): 切片 - **返回** - Self: 消息切片 `args` ### _method_ `__contains__(value)` {#Message---contains--} - **说明:** 检查消息段是否存在 - **参数** - `value` (TMS | str): 消息段或消息段类型 - **返回** - bool: 消息内是否存在给定消息段或给定类型的消息段 ### _method_ `has(value)` {#Message-has} - **说明:** 与 [`__contains__`](#Message---contains--) 相同 - **参数** - `value` (TMS | str) - **返回** - bool ### _method_ `index(value, *args)` {#Message-index} - **说明:** 索引消息段 - **参数** - `value` (TMS | str): 消息段或者消息段类型 - `*args` (SupportsIndex) - `arg`: start 与 end - **返回** - int: 索引 index - **异常** - ValueError: 消息段不存在 ### _method_ `get(type_, count=None)` {#Message-get} - **说明:** 获取指定类型的消息段 - **参数** - `type_` (str): 消息段类型 - `count` (int | None): 获取个数 - **返回** - Self: 构建的新消息 ### _method_ `count(value)` {#Message-count} - **说明:** 计算指定消息段的个数 - **参数** - `value` (TMS | str): 消息段或消息段类型 - **返回** - int: 个数 ### _method_ `only(value)` {#Message-only} - **说明:** 检查消息中是否仅包含指定消息段 - **参数** - `value` (TMS | str): 指定消息段或消息段类型 - **返回** - bool: 是否仅包含指定消息段 ### _method_ `append(obj)` {#Message-append} - **说明:** 添加一个消息段到消息数组末尾。 - **参数** - `obj` (str | TMS): 要添加的消息段 - **返回** - Self ### _method_ `extend(obj)` {#Message-extend} - **说明:** 拼接一个消息数组或多个消息段到消息数组末尾。 - **参数** - `obj` (Self | Iterable[TMS]): 要添加的消息数组 - **返回** - Self ### _method_ `join(iterable)` {#Message-join} - **说明:** 将多个消息连接并将自身作为分割 - **参数** - `iterable` (Iterable[TMS | Self]): 要连接的消息 - **返回** - Self: 连接后的消息 ### _method_ `copy()` {#Message-copy} - **说明:** 深拷贝消息 - **参数** empty - **返回** - Self ### _method_ `include(*types)` {#Message-include} - **说明:** 过滤消息 - **参数** - `*types` (str): 包含的消息段类型 - **返回** - Self: 新构造的消息 ### _method_ `exclude(*types)` {#Message-exclude} - **说明:** 过滤消息 - **参数** - `*types` (str): 不包含的消息段类型 - **返回** - Self: 新构造的消息 ### _method_ `extract_plain_text()` {#Message-extract-plain-text} - **说明:** 提取消息内纯文本消息 - **参数** empty - **返回** - str ## _abstract class_ `MessageSegment()` {#MessageSegment} - **说明:** 消息段基类 - **参数** auto ### _instance-var_ `type` {#MessageSegment-type} - **类型:** str - **说明:** 消息段类型 ### _class-var_ `data` {#MessageSegment-data} - **类型:** dict[str, Any] - **说明:** 消息段数据 ### _abstract classmethod_ `get_message_class()` {#MessageSegment-get-message-class} - **说明:** 获取消息数组类型 - **参数** empty - **返回** - type[TM] ### _abstract method_ `__str__()` {#MessageSegment---str--} - **说明:** 该消息段所代表的 str,在命令匹配部分使用 - **参数** empty - **返回** - str ### _method_ `__add__(other)` {#MessageSegment---add--} - **参数** - `other` (str | TMS | Iterable[TMS]) - **返回** - TM ### _method_ `get(key, default=None)` {#MessageSegment-get} - **参数** - `key` (str) - `default` (Any) - **返回** - untyped ### _method_ `keys()` {#MessageSegment-keys} - **参数** empty - **返回** - untyped ### _method_ `values()` {#MessageSegment-values} - **参数** empty - **返回** - untyped ### _method_ `items()` {#MessageSegment-items} - **参数** empty - **返回** - untyped ### _method_ `join(iterable)` {#MessageSegment-join} - **参数** - `iterable` (Iterable[TMS | TM]) - **返回** - TM ### _method_ `copy()` {#MessageSegment-copy} - **参数** empty - **返回** - Self ### _abstract method_ `is_text()` {#MessageSegment-is-text} - **说明:** 当前消息段是否为纯文本 - **参数** empty - **返回** - bool ## _class_ `MessageTemplate(template, factory=str, private_getattr=False)` {#MessageTemplate} - **说明:** 消息模板格式化实现类。 - **参数** - `template` (str | TM): 模板 - `factory` (type[str] | type[TM]): 消息类型工厂,默认为 `str` - `private_getattr` (bool): 是否允许在模板中访问私有属性,默认为 `False` ### _method_ `add_format_spec(spec, name=None)` {#MessageTemplate-add-format-spec} - **参数** - `spec` (FormatSpecFunc_T) - `name` (str | None) - **返回** - FormatSpecFunc_T ### _method_ `format(*args, **kwargs)` {#MessageTemplate-format} - **说明:** 根据传入参数和模板生成消息对象 - **参数** - `*args` - `**kwargs` - **返回** - TF ### _method_ `format_map(mapping)` {#MessageTemplate-format-map} - **说明:** 根据传入字典和模板生成消息对象, 在传入字段名不是有效标识符时有用 - **参数** - `mapping` (Mapping[str, Any]) - **返回** - TF ### _method_ `vformat(format_string, args, kwargs)` {#MessageTemplate-vformat} - **参数** - `format_string` (str) - `args` (Sequence[Any]) - `kwargs` (Mapping[str, Any]) - **返回** - TF ### _method_ `get_field(field_name, args, kwargs)` {#MessageTemplate-get-field} - **参数** - `field_name` (str) - `args` (Sequence[Any]) - `kwargs` (Mapping[str, Any]) - **返回** - tuple[Any, int | str] ### _method_ `format_field(value, format_spec)` {#MessageTemplate-format-field} - **参数** - `value` (Any) - `format_spec` (str) - **返回** - Any ================================================ FILE: website/versioned_docs/version-2.4.2/api/compat.md ================================================ --- mdx: format: md sidebar_position: 16 description: nonebot.compat 模块 --- # nonebot.compat 本模块为 Pydantic 版本兼容层模块 为兼容 Pydantic V1 与 V2 版本,定义了一系列兼容函数与类供使用。 ## _var_ `Required` {#Required} - **类型:** untyped - **说明:** Alias of Ellipsis for compatibility with pydantic v1 ## _library-attr_ `PydanticUndefined` {#PydanticUndefined} - **说明:** Pydantic Undefined object ## _library-attr_ `PydanticUndefinedType` {#PydanticUndefinedType} - **说明:** Pydantic Undefined type ## _var_ `DEFAULT_CONFIG` {#DEFAULT-CONFIG} - **类型:** untyped - **说明:** Default config for validations ## _class_ `FieldInfo(default=PydanticUndefined, **kwargs)` {#FieldInfo} - **说明:** FieldInfo class with extra property for compatibility with pydantic v1 - **参数** - `default` (Any) - `**kwargs` (Any) ### _property_ `extra` {#FieldInfo-extra} - **类型:** dict[str, Any] - **说明** Extra data that is not part of the standard pydantic fields. For compatibility with pydantic v1. ## _class_ `ModelField()` {#ModelField} - **说明:** ModelField class for compatibility with pydantic v1 - **参数** auto ### _instance-var_ `name` {#ModelField-name} - **类型:** str - **说明:** The name of the field. ### _instance-var_ `annotation` {#ModelField-annotation} - **类型:** Any - **说明:** The annotation of the field. ### _instance-var_ `field_info` {#ModelField-field-info} - **类型:** FieldInfo - **说明:** The FieldInfo of the field. ### _classmethod_ `construct(name, annotation, field_info=None)` {#ModelField-construct} - **说明:** Construct a ModelField from given infos. - **参数** - `name` (str) - `annotation` (Any) - `field_info` (FieldInfo | None) - **返回** - Self ### _method_ `get_default()` {#ModelField-get-default} - **说明:** Get the default value of the field. - **参数** empty - **返回** - Any ### _method_ `validate_value(value)` {#ModelField-validate-value} - **说明:** Validate the value pass to the field. - **参数** - `value` (Any) - **返回** - 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. - **参数** - `model` (type[BaseModel]) - **返回** - list[ModelField] ## _def_ `model_config(model)` {#model-config} - **说明:** Get config of a model. - **参数** - `model` (type[BaseModel]) - **返回** - Any ## _def_ `model_dump(model, include=None, exclude=None, by_alias=False, exclude_unset=False, exclude_defaults=False, exclude_none=False)` {#model-dump} - **参数** - `model` (BaseModel) - `include` (set[str] | None) - `exclude` (set[str] | None) - `by_alias` (bool) - `exclude_unset` (bool) - `exclude_defaults` (bool) - `exclude_none` (bool) - **返回** - dict[str, Any] ## _def_ `type_validate_python(type_, data)` {#type-validate-python} - **说明:** Validate data with given type. - **参数** - `type_` (type[T]) - `data` (Any) - **返回** - T ## _def_ `type_validate_json(type_, data)` {#type-validate-json} - **说明:** Validate JSON with given type. - **参数** - `type_` (type[T]) - `data` (str | bytes) - **返回** - T ## _def_ `custom_validation(class_)` {#custom-validation} - **说明:** Use pydantic v1 like validator generator in pydantic v2 - **参数** - `class_` (type[CVC]) - **返回** - type[CVC] ================================================ FILE: website/versioned_docs/version-2.4.2/api/config.md ================================================ --- mdx: format: md sidebar_position: 1 description: nonebot.config 模块 --- # nonebot.config 本模块定义了 NoneBot 本身运行所需的配置项。 NoneBot 使用 [`pydantic`](https://pydantic-docs.helpmanual.io/) 以及 [`python-dotenv`](https://saurabh-kumar.com/python-dotenv/) 来读取配置。 配置项需符合特殊格式或 json 序列化格式 详情见 [`pydantic Field Type`](https://pydantic-docs.helpmanual.io/usage/types/) 文档。 ## _class_ `Env(_env_file=ENV_FILE_SENTINEL, _env_file_encoding=None, _env_nested_delimiter=None, **values)` {#Env} - **说明** 运行环境配置。大小写不敏感。 将会从 **环境变量** > **dotenv 配置文件** 的优先级读取环境信息。 - **参数** - `_env_file` (DOTENV_TYPE | None) - `_env_file_encoding` (str | None) - `_env_nested_delimiter` (str | None) - `**values` (Any) ### _class-var_ `environment` {#Env-environment} - **类型:** str - **说明** 当前环境名。 NoneBot 将从 `.env.{environment}` 文件中加载配置。 ## _class_ `Config(_env_file=ENV_FILE_SENTINEL, _env_file_encoding=None, _env_nested_delimiter=None, **values)` {#Config} - **说明** NoneBot 主要配置。大小写不敏感。 除了 NoneBot 的配置项外,还可以自行添加配置项到 `.env.{environment}` 文件中。 这些配置将会在 json 反序列化后一起带入 `Config` 类中。 配置方法参考: [配置](https://nonebot.dev/docs/appendices/config) - **参数** - `_env_file` (DOTENV_TYPE | None) - `_env_file_encoding` (str | None) - `_env_nested_delimiter` (str | None) - `**values` (Any) ### _class-var_ `driver` {#Config-driver} - **类型:** str - **说明** NoneBot 运行所使用的 `Driver` 。继承自 [Driver](drivers/index.md#Driver) 。 配置格式为 `[:][+[:]]*`。 `~` 为 `nonebot.drivers.` 的缩写。 配置方法参考: [配置驱动器](https://nonebot.dev/docs/advanced/driver#%E9%85%8D%E7%BD%AE%E9%A9%B1%E5%8A%A8%E5%99%A8) ### _class-var_ `host` {#Config-host} - **类型:** IPvAnyAddress - **说明:** NoneBot [ReverseDriver](drivers/index.md#ReverseDriver) 服务端监听的 IP/主机名。 ### _class-var_ `port` {#Config-port} - **类型:** int - **说明:** NoneBot [ReverseDriver](drivers/index.md#ReverseDriver) 服务端监听的端口。 ### _class-var_ `log_level` {#Config-log-level} - **类型:** int | str - **说明** NoneBot 日志输出等级,可以为 `int` 类型等级或等级名称。 参考 [记录日志](https://nonebot.dev/docs/appendices/log),[loguru 日志等级](https://loguru.readthedocs.io/en/stable/api/logger.html#levels)。 :::tip 提示 日志等级名称应为大写,如 `INFO`。 ::: - **用法** ```conf LOG_LEVEL=25 LOG_LEVEL=INFO ``` ### _class-var_ `api_timeout` {#Config-api-timeout} - **类型:** float | None - **说明:** API 请求超时时间,单位: 秒。 ### _class-var_ `superusers` {#Config-superusers} - **类型:** set[str] - **说明:** 机器人超级用户。 - **用法** ```conf SUPERUSERS=["12345789"] ``` ### _class-var_ `nickname` {#Config-nickname} - **类型:** set[str] - **说明:** 机器人昵称。 ### _class-var_ `command_start` {#Config-command-start} - **类型:** set[str] - **说明** 命令的起始标记,用于判断一条消息是不是命令。 参考[命令响应规则](https://nonebot.dev/docs/advanced/matcher#command)。 - **用法** ```conf COMMAND_START=["/", ""] ``` ### _class-var_ `command_sep` {#Config-command-sep} - **类型:** set[str] - **说明** 命令的分隔标记,用于将文本形式的命令切分为元组(实际的命令名)。 参考[命令响应规则](https://nonebot.dev/docs/advanced/matcher#command)。 - **用法** ```conf COMMAND_SEP=["."] ``` ### _class-var_ `session_expire_timeout` {#Config-session-expire-timeout} - **类型:** timedelta - **说明:** 等待用户回复的超时时间。 - **用法** ```conf SESSION_EXPIRE_TIMEOUT=[-][DD]D[,][HH:MM:]SS[.ffffff] SESSION_EXPIRE_TIMEOUT=[±]P[DD]DT[HH]H[MM]M[SS]S # ISO 8601 ``` ================================================ FILE: website/versioned_docs/version-2.4.2/api/consts.md ================================================ --- mdx: format: md sidebar_position: 9 description: nonebot.consts 模块 --- # nonebot.consts 本模块包含了 NoneBot 事件处理过程中使用到的常量。 ## _var_ `RECEIVE_KEY` {#RECEIVE-KEY} - **类型:** Literal['\_receive\_{id}'] - **说明:** `receive` 存储 key ## _var_ `LAST_RECEIVE_KEY` {#LAST-RECEIVE-KEY} - **类型:** Literal['\_last\_receive'] - **说明:** `last_receive` 存储 key ## _var_ `ARG_KEY` {#ARG-KEY} - **类型:** Literal['{key}'] - **说明:** `arg` 存储 key ## _var_ `REJECT_TARGET` {#REJECT-TARGET} - **类型:** Literal['\_current\_target'] - **说明:** 当前 `reject` 目标存储 key ## _var_ `REJECT_CACHE_TARGET` {#REJECT-CACHE-TARGET} - **类型:** Literal['\_next\_target'] - **说明:** 下一个 `reject` 目标存储 key ## _var_ `PAUSE_PROMPT_RESULT_KEY` {#PAUSE-PROMPT-RESULT-KEY} - **类型:** Literal['\_pause\_result'] - **说明:** `pause` prompt 发送结果存储 key ## _var_ `REJECT_PROMPT_RESULT_KEY` {#REJECT-PROMPT-RESULT-KEY} - **类型:** Literal['\_reject\_{key}\_result'] - **说明:** `reject` prompt 发送结果存储 key ## _var_ `PREFIX_KEY` {#PREFIX-KEY} - **类型:** Literal['\_prefix'] - **说明:** 命令前缀存储 key ## _var_ `CMD_KEY` {#CMD-KEY} - **类型:** Literal['command'] - **说明:** 命令元组存储 key ## _var_ `RAW_CMD_KEY` {#RAW-CMD-KEY} - **类型:** Literal['raw\_command'] - **说明:** 命令文本存储 key ## _var_ `CMD_ARG_KEY` {#CMD-ARG-KEY} - **类型:** Literal['command\_arg'] - **说明:** 命令参数存储 key ## _var_ `CMD_START_KEY` {#CMD-START-KEY} - **类型:** Literal['command\_start'] - **说明:** 命令开头存储 key ## _var_ `CMD_WHITESPACE_KEY` {#CMD-WHITESPACE-KEY} - **类型:** Literal['command\_whitespace'] - **说明:** 命令与参数间空白符存储 key ## _var_ `SHELL_ARGS` {#SHELL-ARGS} - **类型:** Literal['\_args'] - **说明:** shell 命令 parse 后参数字典存储 key ## _var_ `SHELL_ARGV` {#SHELL-ARGV} - **类型:** Literal['\_argv'] - **说明:** shell 命令原始参数列表存储 key ## _var_ `REGEX_MATCHED` {#REGEX-MATCHED} - **类型:** Literal['\_matched'] - **说明:** 正则匹配结果存储 key ## _var_ `STARTSWITH_KEY` {#STARTSWITH-KEY} - **类型:** Literal['\_startswith'] - **说明:** 响应触发前缀 key ## _var_ `ENDSWITH_KEY` {#ENDSWITH-KEY} - **类型:** Literal['\_endswith'] - **说明:** 响应触发后缀 key ## _var_ `FULLMATCH_KEY` {#FULLMATCH-KEY} - **类型:** Literal['\_fullmatch'] - **说明:** 响应触发完整消息 key ## _var_ `KEYWORD_KEY` {#KEYWORD-KEY} - **类型:** Literal['\_keyword'] - **说明:** 响应触发关键字 key ================================================ FILE: website/versioned_docs/version-2.4.2/api/dependencies/_category_.json ================================================ { "position": 13 } ================================================ FILE: website/versioned_docs/version-2.4.2/api/dependencies/index.md ================================================ --- mdx: format: md sidebar_position: 0 description: nonebot.dependencies 模块 --- # nonebot.dependencies 本模块模块实现了依赖注入的定义与处理。 ## _abstract class_ `Param(*args, validate=False, **kwargs)` {#Param} - **说明** 依赖注入的基本单元 —— 参数。 继承自 `pydantic.fields.FieldInfo`,用于描述参数信息(不包括参数名)。 - **参数** - `*args` - `validate` (bool) - `**kwargs` (Any) ## _class_ `Dependent()` {#Dependent} - **说明:** 依赖注入容器 - **参数** - `call`: 依赖注入的可调用对象,可以是任何 Callable 对象 - `pre_checkers`: 依赖注入解析前的参数检查 - `params`: 具名参数列表 - `parameterless`: 匿名参数列表 - `allow_types`: 允许的参数类型 ### _staticmethod_ `parse_params(call, allow_types)` {#Dependent-parse-params} - **参数** - `call` (\_DependentCallable[R]) - `allow_types` (tuple[type[Param], ...]) - **返回** - tuple[[ModelField](../compat.md#ModelField), ...] ### _staticmethod_ `parse_parameterless(parameterless, allow_types)` {#Dependent-parse-parameterless} - **参数** - `parameterless` (tuple[Any, ...]) - `allow_types` (tuple[type[Param], ...]) - **返回** - tuple[Param, ...] ### _classmethod_ `parse(*, call, parameterless=None, allow_types)` {#Dependent-parse} - **参数** - `call` (\_DependentCallable[R]) - `parameterless` (Iterable[Any] | None) - `allow_types` (Iterable[type[Param]]) - **返回** - Dependent[R] ### _async method_ `check(**params)` {#Dependent-check} - **参数** - `**params` (Any) - **返回** - None ### _async method_ `solve(**params)` {#Dependent-solve} - **参数** - `**params` (Any) - **返回** - dict[str, Any] ================================================ FILE: website/versioned_docs/version-2.4.2/api/dependencies/utils.md ================================================ --- mdx: format: md sidebar_position: 1 description: nonebot.dependencies.utils 模块 --- # nonebot.dependencies.utils ## _def_ `get_typed_signature(call)` {#get-typed-signature} - **说明:** 获取可调用对象签名 - **参数** - `call` ((...) -> Any) - **返回** - inspect.Signature ## _def_ `get_typed_annotation(param, globalns)` {#get-typed-annotation} - **说明:** 获取参数的类型注解 - **参数** - `param` (inspect.Parameter) - `globalns` (dict[str, Any]) - **返回** - Any ## _def_ `check_field_type(field, value)` {#check-field-type} - **说明:** 检查字段类型是否匹配 - **参数** - `field` ([ModelField](../compat.md#ModelField)) - `value` (Any) - **返回** - Any ================================================ FILE: website/versioned_docs/version-2.4.2/api/drivers/_category_.json ================================================ { "position": 14 } ================================================ FILE: website/versioned_docs/version-2.4.2/api/drivers/aiohttp.md ================================================ --- mdx: format: md sidebar_position: 2 description: nonebot.drivers.aiohttp 模块 --- # nonebot.drivers.aiohttp [AIOHTTP](https://aiohttp.readthedocs.io/en/stable/) 驱动适配器。 ```bash nb driver install aiohttp # 或者 pip install nonebot2[aiohttp] ``` :::tip 提示 本驱动仅支持客户端连接 ::: ## _class_ `Session(params=None, headers=None, cookies=None, version=HTTPVersion.H11, timeout=None, proxy=None)` {#Session} - **参数** - `params` (QueryTypes) - `headers` (HeaderTypes) - `cookies` (CookieTypes) - `version` (str | [HTTPVersion](index.md#HTTPVersion)) - `timeout` (float | None) - `proxy` (str | None) ### _async method_ `request(setup)` {#Session-request} - **参数** - `setup` ([Request](index.md#Request)) - **返回** - [Response](index.md#Response) ### _async method_ `setup()` {#Session-setup} - **参数** empty - **返回** - None ### _async method_ `close()` {#Session-close} - **参数** empty - **返回** - None ## _class_ `Mixin()` {#Mixin} - **说明:** AIOHTTP Mixin - **参数** auto ### _async method_ `request(setup)` {#Mixin-request} - **参数** - `setup` ([Request](index.md#Request)) - **返回** - [Response](index.md#Response) ### _method_ `websocket(setup)` {#Mixin-websocket} - **参数** - `setup` ([Request](index.md#Request)) - **返回** - 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} - **参数** - `params` (QueryTypes) - `headers` (HeaderTypes) - `cookies` (CookieTypes) - `version` (str | [HTTPVersion](index.md#HTTPVersion)) - `timeout` (float | None) - `proxy` (str | None) - **返回** - Session ## _class_ `WebSocket(*, request, session, websocket)` {#WebSocket} - **说明:** AIOHTTP Websocket Wrapper - **参数** - `request` ([Request](index.md#Request)) - `session` (aiohttp.ClientSession) - `websocket` (aiohttp.ClientWebSocketResponse) ### _async method_ `accept()` {#WebSocket-accept} - **参数** empty - **返回** - untyped ### _async method_ `close(code=1000, reason="")` {#WebSocket-close} - **参数** - `code` (int) - `reason` (str) - **返回** - untyped ### _async method_ `receive()` {#WebSocket-receive} - **参数** empty - **返回** - str ### _async method_ `receive_text()` {#WebSocket-receive-text} - **参数** empty - **返回** - str ### _async method_ `receive_bytes()` {#WebSocket-receive-bytes} - **参数** empty - **返回** - bytes ### _async method_ `send_text(data)` {#WebSocket-send-text} - **参数** - `data` (str) - **返回** - None ### _async method_ `send_bytes(data)` {#WebSocket-send-bytes} - **参数** - `data` (bytes) - **返回** - None ## _class_ `Driver(env, config)` {#Driver} - **参数** - `env` ([Env](../config.md#Env)) - `config` ([Config](../config.md#Config)) ================================================ FILE: website/versioned_docs/version-2.4.2/api/drivers/fastapi.md ================================================ --- mdx: format: md sidebar_position: 1 description: nonebot.drivers.fastapi 模块 --- # nonebot.drivers.fastapi [FastAPI](https://fastapi.tiangolo.com/) 驱动适配 ```bash nb driver install fastapi # 或者 pip install nonebot2[fastapi] ``` :::tip 提示 本驱动仅支持服务端连接 ::: ## _class_ `Config()` {#Config} - **说明:** FastAPI 驱动框架设置,详情参考 FastAPI 文档 - **参数** auto ### _class-var_ `fastapi_openapi_url` {#Config-fastapi-openapi-url} - **类型:** str | None - **说明:** `openapi.json` 地址,默认为 `None` 即关闭 ### _class-var_ `fastapi_docs_url` {#Config-fastapi-docs-url} - **类型:** str | None - **说明:** `swagger` 地址,默认为 `None` 即关闭 ### _class-var_ `fastapi_redoc_url` {#Config-fastapi-redoc-url} - **类型:** str | None - **说明:** `redoc` 地址,默认为 `None` 即关闭 ### _class-var_ `fastapi_include_adapter_schema` {#Config-fastapi-include-adapter-schema} - **类型:** bool - **说明:** 是否包含适配器路由的 schema,默认为 `True` ### _class-var_ `fastapi_reload` {#Config-fastapi-reload} - **类型:** bool - **说明:** 开启/关闭冷重载 ### _class-var_ `fastapi_reload_dirs` {#Config-fastapi-reload-dirs} - **类型:** list[str] | None - **说明:** 重载监控文件夹列表,默认为 uvicorn 默认值 ### _class-var_ `fastapi_reload_delay` {#Config-fastapi-reload-delay} - **类型:** float - **说明:** 重载延迟,默认为 uvicorn 默认值 ### _class-var_ `fastapi_reload_includes` {#Config-fastapi-reload-includes} - **类型:** list[str] | None - **说明:** 要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值 ### _class-var_ `fastapi_reload_excludes` {#Config-fastapi-reload-excludes} - **类型:** list[str] | None - **说明:** 不要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值 ### _class-var_ `fastapi_extra` {#Config-fastapi-extra} - **类型:** dict[str, Any] - **说明:** 传递给 `FastAPI` 的其他参数。 ## _class_ `Driver(env, config)` {#Driver} - **说明:** FastAPI 驱动框架。 - **参数** - `env` ([Env](../config.md#Env)) - `config` (NoneBotConfig) ### _property_ `type` {#Driver-type} - **类型:** str - **说明:** 驱动名称: `fastapi` ### _property_ `server_app` {#Driver-server-app} - **类型:** FastAPI - **说明:** `FastAPI APP` 对象 ### _property_ `asgi` {#Driver-asgi} - **类型:** FastAPI - **说明:** `FastAPI APP` 对象 ### _property_ `logger` {#Driver-logger} - **类型:** logging.Logger - **说明:** fastapi 使用的 logger ### _method_ `setup_http_server(setup)` {#Driver-setup-http-server} - **参数** - `setup` ([HTTPServerSetup](index.md#HTTPServerSetup)) - **返回** - untyped ### _method_ `setup_websocket_server(setup)` {#Driver-setup-websocket-server} - **参数** - `setup` ([WebSocketServerSetup](index.md#WebSocketServerSetup)) - **返回** - None ### _method_ `run(host=None, port=None, *args, app=None, **kwargs)` {#Driver-run} - **说明:** 使用 `uvicorn` 启动 FastAPI - **参数** - `host` (str | None) - `port` (int | None) - `*args` - `app` (str | None) - `**kwargs` - **返回** - untyped ## _class_ `FastAPIWebSocket(*, request, websocket)` {#FastAPIWebSocket} - **说明:** FastAPI WebSocket Wrapper - **参数** - `request` (BaseRequest) - `websocket` ([WebSocket](index.md#WebSocket)) ### _async method_ `accept()` {#FastAPIWebSocket-accept} - **参数** empty - **返回** - None ### _async method_ `close(code=status.WS_1000_NORMAL_CLOSURE, reason="")` {#FastAPIWebSocket-close} - **参数** - `code` (int) - `reason` (str) - **返回** - None ### _async method_ `receive()` {#FastAPIWebSocket-receive} - **参数** empty - **返回** - str | bytes ### _async method_ `receive_text()` {#FastAPIWebSocket-receive-text} - **参数** empty - **返回** - str ### _async method_ `receive_bytes()` {#FastAPIWebSocket-receive-bytes} - **参数** empty - **返回** - bytes ### _async method_ `send_text(data)` {#FastAPIWebSocket-send-text} - **参数** - `data` (str) - **返回** - None ### _async method_ `send_bytes(data)` {#FastAPIWebSocket-send-bytes} - **参数** - `data` (bytes) - **返回** - None ================================================ FILE: website/versioned_docs/version-2.4.2/api/drivers/httpx.md ================================================ --- mdx: format: md sidebar_position: 3 description: nonebot.drivers.httpx 模块 --- # nonebot.drivers.httpx [HTTPX](https://www.python-httpx.org/) 驱动适配 ```bash nb driver install httpx # 或者 pip install nonebot2[httpx] ``` :::tip 提示 本驱动仅支持客户端 HTTP 连接 ::: ## _class_ `Session(params=None, headers=None, cookies=None, version=HTTPVersion.H11, timeout=None, proxy=None)` {#Session} - **参数** - `params` (QueryTypes) - `headers` (HeaderTypes) - `cookies` (CookieTypes) - `version` (str | [HTTPVersion](index.md#HTTPVersion)) - `timeout` (float | None) - `proxy` (str | None) ### _async method_ `request(setup)` {#Session-request} - **参数** - `setup` ([Request](index.md#Request)) - **返回** - [Response](index.md#Response) ### _async method_ `setup()` {#Session-setup} - **参数** empty - **返回** - None ### _async method_ `close()` {#Session-close} - **参数** empty - **返回** - None ## _class_ `Mixin()` {#Mixin} - **说明:** HTTPX Mixin - **参数** auto ### _async method_ `request(setup)` {#Mixin-request} - **参数** - `setup` ([Request](index.md#Request)) - **返回** - [Response](index.md#Response) ### _method_ `get_session(params=None, headers=None, cookies=None, version=HTTPVersion.H11, timeout=None, proxy=None)` {#Mixin-get-session} - **参数** - `params` (QueryTypes) - `headers` (HeaderTypes) - `cookies` (CookieTypes) - `version` (str | [HTTPVersion](index.md#HTTPVersion)) - `timeout` (float | None) - `proxy` (str | None) - **返回** - Session ## _class_ `Driver(env, config)` {#Driver} - **参数** - `env` ([Env](../config.md#Env)) - `config` ([Config](../config.md#Config)) ================================================ FILE: website/versioned_docs/version-2.4.2/api/drivers/index.md ================================================ --- mdx: format: md sidebar_position: 0 description: nonebot.drivers 模块 --- # nonebot.drivers 本模块定义了驱动适配器基类。 各驱动请继承以下基类。 ## _abstract class_ `ASGIMixin()` {#ASGIMixin} - **说明** ASGI 服务端基类。 将后端框架封装,以满足适配器使用。 - **参数** auto ### _abstract property_ `server_app` {#ASGIMixin-server-app} - **类型:** Any - **说明:** 驱动 APP 对象 ### _abstract property_ `asgi` {#ASGIMixin-asgi} - **类型:** Any - **说明:** 驱动 ASGI 对象 ### _abstract method_ `setup_http_server(setup)` {#ASGIMixin-setup-http-server} - **说明:** 设置一个 HTTP 服务器路由配置 - **参数** - `setup` ([HTTPServerSetup](#HTTPServerSetup)) - **返回** - None ### _abstract method_ `setup_websocket_server(setup)` {#ASGIMixin-setup-websocket-server} - **说明:** 设置一个 WebSocket 服务器路由配置 - **参数** - `setup` ([WebSocketServerSetup](#WebSocketServerSetup)) - **返回** - None ## _class_ `Cookies(cookies=None)` {#Cookies} - **参数** - `cookies` (CookieTypes) ### _method_ `set(name, value, domain="", path="/")` {#Cookies-set} - **参数** - `name` (str) - `value` (str) - `domain` (str) - `path` (str) - **返回** - None ### _method_ `get(name, default=None, domain=None, path=None)` {#Cookies-get} - **参数** - `name` (str) - `default` (str | None) - `domain` (str | None) - `path` (str | None) - **返回** - str | None ### _method_ `delete(name, domain=None, path=None)` {#Cookies-delete} - **参数** - `name` (str) - `domain` (str | None) - `path` (str | None) - **返回** - None ### _method_ `clear(domain=None, path=None)` {#Cookies-clear} - **参数** - `domain` (str | None) - `path` (str | None) - **返回** - None ### _method_ `update(cookies=None)` {#Cookies-update} - **参数** - `cookies` (CookieTypes) - **返回** - None ### _method_ `as_header(request)` {#Cookies-as-header} - **参数** - `request` (Request) - **返回** - dict[str, str] ## _abstract class_ `Driver(env, config)` {#Driver} - **说明** 驱动器基类。 驱动器控制框架的启动和停止,适配器的注册,以及机器人生命周期管理。 - **参数** - `env` ([Env](../config.md#Env)): 包含环境信息的 Env 对象 - `config` ([Config](../config.md#Config)): 包含配置信息的 Config 对象 ### _instance-var_ `env` {#Driver-env} - **类型:** str - **说明:** 环境名称 ### _instance-var_ `config` {#Driver-config} - **类型:** [Config](../config.md#Config) - **说明:** 全局配置对象 ### _property_ `bots` {#Driver-bots} - **类型:** dict[str, [Bot](../adapters/index.md#Bot)] - **说明:** 获取当前所有已连接的 Bot ### _method_ `register_adapter(adapter, **kwargs)` {#Driver-register-adapter} - **说明:** 注册一个协议适配器 - **参数** - `adapter` (type[[Adapter](../adapters/index.md#Adapter)]): 适配器类 - `**kwargs`: 其他传递给适配器的参数 - **返回** - None ### _abstract property_ `type` {#Driver-type} - **类型:** str - **说明:** 驱动类型名称 ### _abstract property_ `logger` {#Driver-logger} - **类型:** untyped - **说明:** 驱动专属 logger 日志记录器 ### _abstract method_ `run(*args, **kwargs)` {#Driver-run} - **说明:** 启动驱动框架 - **参数** - `*args` - `**kwargs` - **返回** - untyped ### _method_ `on_startup(func)` {#Driver-on-startup} - **说明:** 注册一个启动时执行的函数 - **参数** - `func` (LIFESPAN_FUNC) - **返回** - LIFESPAN_FUNC ### _method_ `on_shutdown(func)` {#Driver-on-shutdown} - **说明:** 注册一个停止时执行的函数 - **参数** - `func` (LIFESPAN_FUNC) - **返回** - LIFESPAN_FUNC ### _classmethod_ `on_bot_connect(func)` {#Driver-on-bot-connect} - **说明** 装饰一个函数使他在 bot 连接成功时执行。 钩子函数参数: - bot: 当前连接上的 Bot 对象 - **参数** - `func` ([T_BotConnectionHook](../typing.md#T-BotConnectionHook)) - **返回** - [T_BotConnectionHook](../typing.md#T-BotConnectionHook) ### _classmethod_ `on_bot_disconnect(func)` {#Driver-on-bot-disconnect} - **说明** 装饰一个函数使他在 bot 连接断开时执行。 钩子函数参数: - bot: 当前连接上的 Bot 对象 - **参数** - `func` ([T_BotDisconnectionHook](../typing.md#T-BotDisconnectionHook)) - **返回** - [T_BotDisconnectionHook](../typing.md#T-BotDisconnectionHook) ## _var_ `ForwardDriver` {#ForwardDriver} - **类型:** ForwardMixin - **说明** 支持客户端请求的驱动器。 **Deprecated**,请使用 [ForwardMixin](#ForwardMixin) 或其子类代替。 ## _abstract class_ `ForwardMixin()` {#ForwardMixin} - **说明:** 客户端混入基类。 - **参数** auto ## _abstract class_ `HTTPClientMixin()` {#HTTPClientMixin} - **说明:** HTTP 客户端混入基类。 - **参数** auto ### _abstract async method_ `request(setup)` {#HTTPClientMixin-request} - **说明:** 发送一个 HTTP 请求 - **参数** - `setup` ([Request](#Request)) - **返回** - [Response](#Response) ### _abstract method_ `get_session(params=None, headers=None, cookies=None, version=HTTPVersion.H11, timeout=None, proxy=None)` {#HTTPClientMixin-get-session} - **说明:** 获取一个 HTTP 会话 - **参数** - `params` (QueryTypes) - `headers` (HeaderTypes) - `cookies` (CookieTypes) - `version` (str | [HTTPVersion](#HTTPVersion)) - `timeout` (float | None) - `proxy` (str | None) - **返回** - HTTPClientSession ## _class_ `HTTPServerSetup()` {#HTTPServerSetup} - **说明:** HTTP 服务器路由配置。 - **参数** auto ## _enum_ `HTTPVersion` {#HTTPVersion} - **说明:** An enumeration. - **参数** auto - `H10: '1.0'` - `H11: '1.1'` - `H2: '2'` ## _abstract class_ `Mixin()` {#Mixin} - **说明:** 可与其他驱动器共用的混入基类。 - **参数** auto ### _abstract property_ `type` {#Mixin-type} - **类型:** str - **说明:** 混入驱动类型名称 ## _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} - **参数** - `method` (str | bytes) - `url` (URL | str | RawURL) - `params` (QueryTypes) - `headers` (HeaderTypes) - `cookies` (CookieTypes) - `content` (ContentTypes) - `data` (DataTypes) - `json` (Any) - `files` (FilesTypes) - `version` (str | HTTPVersion) - `timeout` (float | None) - `proxy` (str | None) ## _class_ `Response(status_code, *, headers=None, content=None, request=None)` {#Response} - **参数** - `status_code` (int) - `headers` (HeaderTypes) - `content` (ContentTypes) - `request` (Request | None) ## _var_ `ReverseDriver` {#ReverseDriver} - **类型:** ReverseMixin - **说明** 支持服务端请求的驱动器。 **Deprecated**,请使用 [ReverseMixin](#ReverseMixin) 或其子类代替。 ## _abstract class_ `ReverseMixin()` {#ReverseMixin} - **说明:** 服务端混入基类。 - **参数** auto ## _abstract class_ `WebSocket(*, request)` {#WebSocket} - **参数** - `request` (Request) ### _abstract property_ `closed` {#WebSocket-closed} - **类型:** bool - **说明:** 连接是否已经关闭 ### _abstract async method_ `accept()` {#WebSocket-accept} - **说明:** 接受 WebSocket 连接请求 - **参数** empty - **返回** - None ### _abstract async method_ `close(code=1000, reason="")` {#WebSocket-close} - **说明:** 关闭 WebSocket 连接请求 - **参数** - `code` (int) - `reason` (str) - **返回** - None ### _abstract async method_ `receive()` {#WebSocket-receive} - **说明:** 接收一条 WebSocket text/bytes 信息 - **参数** empty - **返回** - str | bytes ### _abstract async method_ `receive_text()` {#WebSocket-receive-text} - **说明:** 接收一条 WebSocket text 信息 - **参数** empty - **返回** - str ### _abstract async method_ `receive_bytes()` {#WebSocket-receive-bytes} - **说明:** 接收一条 WebSocket binary 信息 - **参数** empty - **返回** - bytes ### _async method_ `send(data)` {#WebSocket-send} - **说明:** 发送一条 WebSocket text/bytes 信息 - **参数** - `data` (str | bytes) - **返回** - None ### _abstract async method_ `send_text(data)` {#WebSocket-send-text} - **说明:** 发送一条 WebSocket text 信息 - **参数** - `data` (str) - **返回** - None ### _abstract async method_ `send_bytes(data)` {#WebSocket-send-bytes} - **说明:** 发送一条 WebSocket binary 信息 - **参数** - `data` (bytes) - **返回** - None ## _abstract class_ `WebSocketClientMixin()` {#WebSocketClientMixin} - **说明:** WebSocket 客户端混入基类。 - **参数** auto ### _abstract method_ `websocket(setup)` {#WebSocketClientMixin-websocket} - **说明:** 发起一个 WebSocket 连接 - **参数** - `setup` ([Request](#Request)) - **返回** - AsyncGenerator[[WebSocket](#WebSocket), None] ## _class_ `WebSocketServerSetup()` {#WebSocketServerSetup} - **说明:** WebSocket 服务器路由配置。 - **参数** auto ## _def_ `combine_driver(driver, *mixins)` {#combine-driver} - **说明:** 将一个驱动器和多个混入类合并。 - **重载** **1.** `(driver) -> type[D]` - **参数** - `driver` (type[D]) - **返回** - type[D] **2.** `(driver, __m, /, *mixins) -> type[CombinedDriver]` - **参数** - `driver` (type[D]) - `__m` (type[[Mixin](#Mixin)]) - `*mixins` (type[[Mixin](#Mixin)]) - **返回** - type[CombinedDriver] ================================================ FILE: website/versioned_docs/version-2.4.2/api/drivers/none.md ================================================ --- mdx: format: md sidebar_position: 6 description: nonebot.drivers.none 模块 --- # nonebot.drivers.none None 驱动适配 :::tip 提示 本驱动不支持任何服务器或客户端连接 ::: ## _class_ `Driver(env, config)` {#Driver} - **说明:** None 驱动框架 - **参数** - `env` ([Env](../config.md#Env)) - `config` ([Config](../config.md#Config)) ### _property_ `type` {#Driver-type} - **类型:** str - **说明:** 驱动名称: `none` ### _property_ `logger` {#Driver-logger} - **类型:** untyped - **说明:** none driver 使用的 logger ### _method_ `run(*args, **kwargs)` {#Driver-run} - **说明:** 启动 none driver - **参数** - `*args` - `**kwargs` - **返回** - untyped ### _method_ `exit(force=False)` {#Driver-exit} - **说明:** 退出 none driver - **参数** - `force` (bool): 强制退出 - **返回** - untyped ================================================ FILE: website/versioned_docs/version-2.4.2/api/drivers/quart.md ================================================ --- mdx: format: md sidebar_position: 5 description: nonebot.drivers.quart 模块 --- # nonebot.drivers.quart [Quart](https://pgjones.gitlab.io/quart/index.html) 驱动适配 ```bash nb driver install quart # 或者 pip install nonebot2[quart] ``` :::tip 提示 本驱动仅支持服务端连接 ::: ## _class_ `Config()` {#Config} - **说明:** Quart 驱动框架设置 - **参数** auto ### _class-var_ `quart_reload` {#Config-quart-reload} - **类型:** bool - **说明:** 开启/关闭冷重载 ### _class-var_ `quart_reload_dirs` {#Config-quart-reload-dirs} - **类型:** list[str] | None - **说明:** 重载监控文件夹列表,默认为 uvicorn 默认值 ### _class-var_ `quart_reload_delay` {#Config-quart-reload-delay} - **类型:** float - **说明:** 重载延迟,默认为 uvicorn 默认值 ### _class-var_ `quart_reload_includes` {#Config-quart-reload-includes} - **类型:** list[str] | None - **说明:** 要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值 ### _class-var_ `quart_reload_excludes` {#Config-quart-reload-excludes} - **类型:** list[str] | None - **说明:** 不要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值 ### _class-var_ `quart_extra` {#Config-quart-extra} - **类型:** dict[str, Any] - **说明:** 传递给 `Quart` 的其他参数。 ## _class_ `Driver(env, config)` {#Driver} - **说明:** Quart 驱动框架 - **参数** - `env` ([Env](../config.md#Env)) - `config` (NoneBotConfig) ### _property_ `type` {#Driver-type} - **类型:** str - **说明:** 驱动名称: `quart` ### _property_ `server_app` {#Driver-server-app} - **类型:** Quart - **说明:** `Quart` 对象 ### _property_ `asgi` {#Driver-asgi} - **类型:** untyped - **说明:** `Quart` 对象 ### _property_ `logger` {#Driver-logger} - **类型:** untyped - **说明:** Quart 使用的 logger ### _method_ `setup_http_server(setup)` {#Driver-setup-http-server} - **参数** - `setup` ([HTTPServerSetup](index.md#HTTPServerSetup)) - **返回** - untyped ### _method_ `setup_websocket_server(setup)` {#Driver-setup-websocket-server} - **参数** - `setup` ([WebSocketServerSetup](index.md#WebSocketServerSetup)) - **返回** - None ### _method_ `run(host=None, port=None, *args, app=None, **kwargs)` {#Driver-run} - **说明:** 使用 `uvicorn` 启动 Quart - **参数** - `host` (str | None) - `port` (int | None) - `*args` - `app` (str | None) - `**kwargs` - **返回** - untyped ## _class_ `WebSocket(*, request, websocket_ctx)` {#WebSocket} - **说明:** Quart WebSocket Wrapper - **参数** - `request` (BaseRequest) - `websocket_ctx` (WebsocketContext) ### _async method_ `accept()` {#WebSocket-accept} - **参数** empty - **返回** - untyped ### _async method_ `close(code=1000, reason="")` {#WebSocket-close} - **参数** - `code` (int) - `reason` (str) - **返回** - untyped ### _async method_ `receive()` {#WebSocket-receive} - **参数** empty - **返回** - str | bytes ### _async method_ `receive_text()` {#WebSocket-receive-text} - **参数** empty - **返回** - str ### _async method_ `receive_bytes()` {#WebSocket-receive-bytes} - **参数** empty - **返回** - bytes ### _async method_ `send_text(data)` {#WebSocket-send-text} - **参数** - `data` (str) - **返回** - untyped ### _async method_ `send_bytes(data)` {#WebSocket-send-bytes} - **参数** - `data` (bytes) - **返回** - untyped ================================================ FILE: website/versioned_docs/version-2.4.2/api/drivers/websockets.md ================================================ --- mdx: format: md sidebar_position: 4 description: nonebot.drivers.websockets 模块 --- # nonebot.drivers.websockets [websockets](https://websockets.readthedocs.io/) 驱动适配 ```bash nb driver install websockets # 或者 pip install nonebot2[websockets] ``` :::tip 提示 本驱动仅支持客户端 WebSocket 连接 ::: ## _def_ `catch_closed(func)` {#catch-closed} - **参数** - `func` ((P) -> CoroutineType[Any, Any, T]) - **返回** - (P) -> CoroutineType[Any, Any, T] ## _class_ `Mixin()` {#Mixin} - **说明:** Websockets Mixin - **参数** auto ### _method_ `websocket(setup)` {#Mixin-websocket} - **参数** - `setup` ([Request](index.md#Request)) - **返回** - AsyncGenerator[[WebSocket](index.md#WebSocket), None] ## _class_ `WebSocket(*, request, websocket)` {#WebSocket} - **说明:** Websockets WebSocket Wrapper - **参数** - `request` ([Request](index.md#Request)) - `websocket` (WebSocketClientProtocol) ### _async method_ `accept()` {#WebSocket-accept} - **参数** empty - **返回** - untyped ### _async method_ `close(code=1000, reason="")` {#WebSocket-close} - **参数** - `code` (int) - `reason` (str) - **返回** - untyped ### _async method_ `receive()` {#WebSocket-receive} - **参数** empty - **返回** - str | bytes ### _async method_ `receive_text()` {#WebSocket-receive-text} - **参数** empty - **返回** - str ### _async method_ `receive_bytes()` {#WebSocket-receive-bytes} - **参数** empty - **返回** - bytes ### _async method_ `send_text(data)` {#WebSocket-send-text} - **参数** - `data` (str) - **返回** - None ### _async method_ `send_bytes(data)` {#WebSocket-send-bytes} - **参数** - `data` (bytes) - **返回** - None ## _class_ `Driver(env, config)` {#Driver} - **参数** - `env` ([Env](../config.md#Env)) - `config` ([Config](../config.md#Config)) ================================================ FILE: website/versioned_docs/version-2.4.2/api/exception.md ================================================ --- mdx: format: md sidebar_position: 10 description: nonebot.exception 模块 --- # nonebot.exception 本模块包含了所有 NoneBot 运行时可能会抛出的异常。 这些异常并非所有需要用户处理,在 NoneBot 内部运行时被捕获,并进行对应操作。 ```bash NoneBotException ├── ParserExit ├── ProcessException | ├── IgnoredException | ├── SkippedException | | └── TypeMisMatch | ├── MockApiException | └── StopPropagation ├── MatcherException | ├── PausedException | ├── RejectedException | └── FinishedException ├── AdapterException | ├── NoLogException | ├── ApiNotAvailable | ├── NetworkError | └── ActionFailed └── DriverException └── WebSocketClosed ``` ## _class_ `NoneBotException()` {#NoneBotException} - **说明:** 所有 NoneBot 发生的异常基类。 - **参数** auto ## _class_ `ParserExit()` {#ParserExit} - **说明:** 处理消息失败时返回的异常。 - **参数** auto ## _class_ `ProcessException()` {#ProcessException} - **说明:** 事件处理过程中发生的异常基类。 - **参数** auto ## _class_ `IgnoredException()` {#IgnoredException} - **说明:** 指示 NoneBot 应该忽略该事件。可由 PreProcessor 抛出。 - **参数** - `reason`: 忽略事件的原因 ## _class_ `SkippedException()` {#SkippedException} - **说明** 指示 NoneBot 立即结束当前 `Dependent` 的运行。 例如,可以在 `Handler` 中通过 [Matcher.skip](matcher.md#Matcher-skip) 抛出。 - **参数** auto - **用法** ```python def always_skip(): Matcher.skip() @matcher.handle() async def handler(dependency = Depends(always_skip)): # never run ``` ## _class_ `TypeMisMatch()` {#TypeMisMatch} - **说明:** 当前 `Handler` 的参数类型不匹配。 - **参数** auto ## _class_ `MockApiException()` {#MockApiException} - **说明:** 指示 NoneBot 阻止本次 API 调用或修改本次调用返回值,并返回自定义内容。 可由 api hook 抛出。 - **参数** - `result`: 返回的内容 ## _class_ `StopPropagation()` {#StopPropagation} - **说明** 指示 NoneBot 终止事件向下层传播。 在 [Matcher.block](matcher.md#Matcher-block) 为 `True` 或使用 [Matcher.stop_propagation](matcher.md#Matcher-stop-propagation) 方法时抛出。 - **参数** auto - **用法** ```python matcher = on_notice(block=True) # 或者 @matcher.handle() async def handler(matcher: Matcher): matcher.stop_propagation() ``` ## _class_ `MatcherException()` {#MatcherException} - **说明:** 所有 Matcher 发生的异常基类。 - **参数** auto ## _class_ `PausedException()` {#PausedException} - **说明** 指示 NoneBot 结束当前 `Handler` 并等待下一条消息后继续下一个 `Handler`。 可用于用户输入新信息。 可以在 `Handler` 中通过 [Matcher.pause](matcher.md#Matcher-pause) 抛出。 - **参数** auto - **用法** ```python @matcher.handle() async def handler(): await matcher.pause("some message") ``` ## _class_ `RejectedException()` {#RejectedException} - **说明** 指示 NoneBot 结束当前 `Handler` 并等待下一条消息后重新运行当前 `Handler`。 可用于用户重新输入。 可以在 `Handler` 中通过 [Matcher.reject](matcher.md#Matcher-reject) 抛出。 - **参数** auto - **用法** ```python @matcher.handle() async def handler(): await matcher.reject("some message") ``` ## _class_ `FinishedException()` {#FinishedException} - **说明** 指示 NoneBot 结束当前 `Handler` 且后续 `Handler` 不再被运行。可用于结束用户会话。 可以在 `Handler` 中通过 [Matcher.finish](matcher.md#Matcher-finish) 抛出。 - **参数** auto - **用法** ```python @matcher.handle() async def handler(): await matcher.finish("some message") ``` ## _class_ `AdapterException()` {#AdapterException} - **说明:** 代表 `Adapter` 抛出的异常,所有的 `Adapter` 都要在内部继承自这个 `Exception`。 - **参数** - `adapter_name`: 标识 adapter ## _class_ `NoLogException()` {#NoLogException} - **说明** 指示 NoneBot 对当前 `Event` 进行处理但不显示 Log 信息。 可在 [Event.get_log_string](adapters/index.md#Event-get-log-string) 时抛出 - **参数** auto ## _class_ `ApiNotAvailable()` {#ApiNotAvailable} - **说明:** 在 API 连接不可用时抛出。 - **参数** auto ## _class_ `NetworkError()` {#NetworkError} - **说明:** 在网络出现问题时抛出, 如: API 请求地址不正确, API 请求无返回或返回状态非正常等。 - **参数** auto ## _class_ `ActionFailed()` {#ActionFailed} - **说明:** API 请求成功返回数据,但 API 操作失败。 - **参数** auto ## _class_ `DriverException()` {#DriverException} - **说明:** `Driver` 抛出的异常基类。 - **参数** auto ## _class_ `WebSocketClosed()` {#WebSocketClosed} - **说明:** WebSocket 连接已关闭。 - **参数** auto ================================================ FILE: website/versioned_docs/version-2.4.2/api/index.md ================================================ --- mdx: format: md sidebar_position: 0 description: nonebot 模块 --- # nonebot 本模块主要定义了 NoneBot 启动所需函数,供 bot 入口文件调用。 ## 快捷导入 为方便使用,本模块从子模块导入了部分内容,以下内容可以直接通过本模块导入: - `on` => [`on`](plugin/on.md#on) - `on_metaevent` => [`on_metaevent`](plugin/on.md#on-metaevent) - `on_message` => [`on_message`](plugin/on.md#on-message) - `on_notice` => [`on_notice`](plugin/on.md#on-notice) - `on_request` => [`on_request`](plugin/on.md#on-request) - `on_startswith` => [`on_startswith`](plugin/on.md#on-startswith) - `on_endswith` => [`on_endswith`](plugin/on.md#on-endswith) - `on_fullmatch` => [`on_fullmatch`](plugin/on.md#on-fullmatch) - `on_keyword` => [`on_keyword`](plugin/on.md#on-keyword) - `on_command` => [`on_command`](plugin/on.md#on-command) - `on_shell_command` => [`on_shell_command`](plugin/on.md#on-shell-command) - `on_regex` => [`on_regex`](plugin/on.md#on-regex) - `on_type` => [`on_type`](plugin/on.md#on-type) - `CommandGroup` => [`CommandGroup`](plugin/on.md#CommandGroup) - `Matchergroup` => [`MatcherGroup`](plugin/on.md#MatcherGroup) - `load_plugin` => [`load_plugin`](plugin/load.md#load-plugin) - `load_plugins` => [`load_plugins`](plugin/load.md#load-plugins) - `load_all_plugins` => [`load_all_plugins`](plugin/load.md#load-all-plugins) - `load_from_json` => [`load_from_json`](plugin/load.md#load-from-json) - `load_from_toml` => [`load_from_toml`](plugin/load.md#load-from-toml) - `load_builtin_plugin` => [`load_builtin_plugin`](plugin/load.md#load-builtin-plugin) - `load_builtin_plugins` => [`load_builtin_plugins`](plugin/load.md#load-builtin-plugins) - `get_plugin` => [`get_plugin`](plugin/index.md#get-plugin) - `get_plugin_by_module_name` => [`get_plugin_by_module_name`](plugin/index.md#get-plugin-by-module-name) - `get_loaded_plugins` => [`get_loaded_plugins`](plugin/index.md#get-loaded-plugins) - `get_available_plugin_names` => [`get_available_plugin_names`](plugin/index.md#get-available-plugin-names) - `get_plugin_config` => [`get_plugin_config`](plugin/index.md#get-plugin-config) - `require` => [`require`](plugin/load.md#require) ## _def_ `get_driver()` {#get-driver} - **说明** 获取全局 [Driver](drivers/index.md#Driver) 实例。 可用于在计划任务的回调等情形中获取当前 [Driver](drivers/index.md#Driver) 实例。 - **参数** empty - **返回** - [Driver](drivers/index.md#Driver): 全局 [Driver](drivers/index.md#Driver) 对象 - **异常** - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用) - **用法** ```python driver = nonebot.get_driver() ``` ## _def_ `get_adapter(name)` {#get-adapter} - **说明:** 获取已注册的 [Adapter](adapters/index.md#Adapter) 实例。 - **重载** **1.** `(name) -> Adapter` - **参数** - `name` (str): 适配器名称 - **返回** - [Adapter](adapters/index.md#Adapter): 指定名称的 [Adapter](adapters/index.md#Adapter) 对象 **2.** `(name) -> A` - **参数** - `name` (type[A]): 适配器类型 - **返回** - A: 指定类型的 [Adapter](adapters/index.md#Adapter) 对象 - **异常** - ValueError: 指定的 [Adapter](adapters/index.md#Adapter) 未注册 - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用) - **用法** ```python from nonebot.adapters.console import Adapter adapter = nonebot.get_adapter(Adapter) ``` ## _def_ `get_adapters()` {#get-adapters} - **说明:** 获取所有已注册的 [Adapter](adapters/index.md#Adapter) 实例。 - **参数** empty - **返回** - dict[str, [Adapter](adapters/index.md#Adapter)]: 所有 [Adapter](adapters/index.md#Adapter) 实例字典 - **异常** - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用) - **用法** ```python adapters = nonebot.get_adapters() ``` ## _def_ `get_app()` {#get-app} - **说明:** 获取全局 [ASGIMixin](drivers/index.md#ASGIMixin) 对应的 Server App 对象。 - **参数** empty - **返回** - Any: Server App 对象 - **异常** - AssertionError: 全局 Driver 对象不是 [ASGIMixin](drivers/index.md#ASGIMixin) 类型 - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用) - **用法** ```python app = nonebot.get_app() ``` ## _def_ `get_asgi()` {#get-asgi} - **说明:** 获取全局 [ASGIMixin](drivers/index.md#ASGIMixin) 对应的 [ASGI](https://asgi.readthedocs.io/) 对象。 - **参数** empty - **返回** - Any: ASGI 对象 - **异常** - AssertionError: 全局 Driver 对象不是 [ASGIMixin](drivers/index.md#ASGIMixin) 类型 - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用) - **用法** ```python asgi = nonebot.get_asgi() ``` ## _def_ `get_bot(self_id=None)` {#get-bot} - **说明** 获取一个连接到 NoneBot 的 [Bot](adapters/index.md#Bot) 对象。 当提供 `self_id` 时,此函数是 `get_bots()[self_id]` 的简写; 当不提供时,返回一个 [Bot](adapters/index.md#Bot)。 - **参数** - `self_id` (str | None): 用来识别 [Bot](adapters/index.md#Bot) 的 [Bot.self_id](adapters/index.md#Bot-self-id) 属性 - **返回** - [Bot](adapters/index.md#Bot): [Bot](adapters/index.md#Bot) 对象 - **异常** - KeyError: 对应 self_id 的 Bot 不存在 - ValueError: 没有传入 self_id 且没有 Bot 可用 - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用) - **用法** ```python assert nonebot.get_bot("12345") == nonebot.get_bots()["12345"] another_unspecified_bot = nonebot.get_bot() ``` ## _def_ `get_bots()` {#get-bots} - **说明:** 获取所有连接到 NoneBot 的 [Bot](adapters/index.md#Bot) 对象。 - **参数** empty - **返回** - dict[str, [Bot](adapters/index.md#Bot)]: 一个以 [Bot.self_id](adapters/index.md#Bot-self-id) 为键 [Bot](adapters/index.md#Bot) 对象为值的字典 - **异常** - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用) - **用法** ```python bots = nonebot.get_bots() ``` ## _def_ `init(*, _env_file=None, **kwargs)` {#init} - **说明** 初始化 NoneBot 以及 全局 [Driver](drivers/index.md#Driver) 对象。 NoneBot 将会从 .env 文件中读取环境信息,并使用相应的 env 文件配置。 也可以传入自定义的 `_env_file` 来指定 NoneBot 从该文件读取配置。 - **参数** - `_env_file` (DOTENV_TYPE | None): 配置文件名,默认从 `.env.{env_name}` 中读取配置 - `**kwargs` (Any): 任意变量,将会存储到 [Driver.config](drivers/index.md#Driver-config) 对象里 - **返回** - None - **用法** ```python nonebot.init(database=Database(...)) ``` ## _def_ `run(*args, **kwargs)` {#run} - **说明:** 启动 NoneBot,即运行全局 [Driver](drivers/index.md#Driver) 对象。 - **参数** - `*args` (Any): 传入 [Driver.run](drivers/index.md#Driver-run) 的位置参数 - `**kwargs` (Any): 传入 [Driver.run](drivers/index.md#Driver-run) 的命名参数 - **返回** - None - **用法** ```python nonebot.run(host="127.0.0.1", port=8080) ``` ================================================ FILE: website/versioned_docs/version-2.4.2/api/log.md ================================================ --- mdx: format: md sidebar_position: 7 description: nonebot.log 模块 --- # nonebot.log 本模块定义了 NoneBot 的日志记录 Logger。 NoneBot 使用 [`loguru`][loguru] 来记录日志信息。 自定义 logger 请参考 [自定义日志](https://nonebot.dev/docs/appendices/log) 以及 [`loguru`][loguru] 文档。 [loguru]: https://github.com/Delgan/loguru ## _var_ `logger` {#logger} - **类型:** Logger - **说明** NoneBot 日志记录器对象。 默认信息: - 格式: `[%(asctime)s %(name)s] %(levelname)s: %(message)s` - 等级: `INFO` ,根据 `config.log_level` 配置改变 - 输出: 输出至 stdout - **用法** ```python from nonebot.log import logger ``` ## _class_ `LoguruHandler()` {#LoguruHandler} - **说明:** logging 与 loguru 之间的桥梁,将 logging 的日志转发到 loguru。 - **参数** auto ### _method_ `emit(record)` {#LoguruHandler-emit} - **参数** - `record` (logging.LogRecord) - **返回** - untyped ## _def_ `default_filter(record)` {#default-filter} - **说明:** 默认的日志过滤器,根据 `config.log_level` 配置改变日志等级。 - **参数** - `record` (Record) - **返回** - untyped ## _var_ `default_format` {#default-format} - **类型:** str - **说明:** 默认日志格式 ================================================ FILE: website/versioned_docs/version-2.4.2/api/matcher.md ================================================ --- mdx: format: md sidebar_position: 3 description: nonebot.matcher 模块 --- # nonebot.matcher 本模块实现事件响应器的创建与运行,并提供一些快捷方法来帮助用户更好的与机器人进行对话。 ## _var_ `DEFAULT_PROVIDER_CLASS` {#DEFAULT-PROVIDER-CLASS} - **类型:** untyped - **说明:** 默认存储器类型 ## _class_ `Matcher()` {#Matcher} - **说明:** 事件响应器类 - **参数** empty ### _class-var_ `type` {#Matcher-type} - **类型:** ClassVar[str] - **说明:** 事件响应器类型 ### _class-var_ `rule` {#Matcher-rule} - **类型:** ClassVar[[Rule](rule.md#Rule)] - **说明:** 事件响应器匹配规则 ### _class-var_ `permission` {#Matcher-permission} - **类型:** ClassVar[[Permission](permission.md#Permission)] - **说明:** 事件响应器触发权限 ### _class-var_ `handlers` {#Matcher-handlers} - **类型:** ClassVar[list[[Dependent](dependencies/index.md#Dependent)[Any]]] - **说明:** 事件响应器拥有的事件处理函数列表 ### _class-var_ `priority` {#Matcher-priority} - **类型:** ClassVar[int] - **说明:** 事件响应器优先级 ### _class-var_ `block` {#Matcher-block} - **类型:** bool - **说明:** 事件响应器是否阻止事件传播 ### _class-var_ `temp` {#Matcher-temp} - **类型:** ClassVar[bool] - **说明:** 事件响应器是否为临时 ### _class-var_ `expire_time` {#Matcher-expire-time} - **类型:** ClassVar[datetime | None] - **说明:** 事件响应器过期时间点 ### _classmethod_ `new(type_="", rule=None, permission=None, handlers=None, temp=False, priority=1, block=False, *, plugin=None, module=None, source=None, expire_time=None, default_state=None, default_type_updater=None, default_permission_updater=None)` {#Matcher-new} - **说明:** 创建一个新的事件响应器,并存储至 `matchers <#matchers>`\_ - **参数** - `type_` (str): 事件响应器类型,与 `event.get_type()` 一致时触发,空字符串表示任意 - `rule` ([Rule](rule.md#Rule) | None): 匹配规则 - `permission` ([Permission](permission.md#Permission) | None): 权限 - `handlers` (list[[T\_Handler](typing.md#T-Handler) | [Dependent](dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器,即触发一次后删除 - `priority` (int): 响应优先级 - `block` (bool): 是否阻止事件向更低优先级的响应器传播 - `plugin` ([Plugin](plugin/model.md#Plugin) | None): **Deprecated.** 事件响应器所在插件 - `module` (ModuleType | None): **Deprecated.** 事件响应器所在模块 - `source` (MatcherSource | None): 事件响应器源代码上下文信息 - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `default_state` ([T_State](typing.md#T-State) | None): 默认状态 `state` - `default_type_updater` ([T_TypeUpdater](typing.md#T-TypeUpdater) | [Dependent](dependencies/index.md#Dependent)[str] | None): 默认事件类型更新函数 - `default_permission_updater` ([T_PermissionUpdater](typing.md#T-PermissionUpdater) | [Dependent](dependencies/index.md#Dependent)[[Permission](permission.md#Permission)] | None): 默认会话权限更新函数 - **返回** - type[Matcher]: 新的事件响应器类 ### _classmethod_ `destroy()` {#Matcher-destroy} - **说明:** 销毁当前的事件响应器 - **参数** empty - **返回** - None ### _classmethod_ `check_perm(bot, event, stack=None, dependency_cache=None)` {#Matcher-check-perm} - **说明:** 检查是否满足触发权限 - **参数** - `bot` ([Bot](adapters/index.md#Bot)): Bot 对象 - `event` ([Event](adapters/index.md#Event)): 上报事件 - `stack` (AsyncExitStack | None): 异步上下文栈 - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None): 依赖缓存 - **返回** - bool: 是否满足权限 ### _classmethod_ `check_rule(bot, event, state, stack=None, dependency_cache=None)` {#Matcher-check-rule} - **说明:** 检查是否满足匹配规则 - **参数** - `bot` ([Bot](adapters/index.md#Bot)): Bot 对象 - `event` ([Event](adapters/index.md#Event)): 上报事件 - `state` ([T_State](typing.md#T-State)): 当前状态 - `stack` (AsyncExitStack | None): 异步上下文栈 - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None): 依赖缓存 - **返回** - bool: 是否满足匹配规则 ### _classmethod_ `type_updater(func)` {#Matcher-type-updater} - **说明:** 装饰一个函数来更改当前事件响应器的默认响应事件类型更新函数 - **参数** - `func` ([T_TypeUpdater](typing.md#T-TypeUpdater)): 响应事件类型更新函数 - **返回** - [T_TypeUpdater](typing.md#T-TypeUpdater) ### _classmethod_ `permission_updater(func)` {#Matcher-permission-updater} - **说明:** 装饰一个函数来更改当前事件响应器的默认会话权限更新函数 - **参数** - `func` ([T_PermissionUpdater](typing.md#T-PermissionUpdater)): 会话权限更新函数 - **返回** - [T_PermissionUpdater](typing.md#T-PermissionUpdater) ### _classmethod_ `append_handler(handler, parameterless=None)` {#Matcher-append-handler} - **参数** - `handler` ([T_Handler](typing.md#T-Handler)) - `parameterless` (Iterable[Any] | None) - **返回** - [Dependent](dependencies/index.md#Dependent)[Any] ### _classmethod_ `handle(parameterless=None)` {#Matcher-handle} - **说明:** 装饰一个函数来向事件响应器直接添加一个处理函数 - **参数** - `parameterless` (Iterable[Any] | None): 非参数类型依赖列表 - **返回** - ([T_Handler](typing.md#T-Handler)) -> [T_Handler](typing.md#T-Handler) ### _classmethod_ `receive(id="", parameterless=None)` {#Matcher-receive} - **说明:** 装饰一个函数来指示 NoneBot 在接收用户新的一条消息后继续运行该函数 - **参数** - `id` (str): 消息 ID - `parameterless` (Iterable[Any] | None): 非参数类型依赖列表 - **返回** - ([T_Handler](typing.md#T-Handler)) -> [T_Handler](typing.md#T-Handler) ### _classmethod_ `got(key, prompt=None, parameterless=None)` {#Matcher-got} - **说明** 装饰一个函数来指示 NoneBot 获取一个参数 `key` 当要获取的 `key` 不存在时接收用户新的一条消息再运行该函数, 如果 `key` 已存在则直接继续运行 - **参数** - `key` (str): 参数名 - `prompt` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate) | None): 在参数不存在时向用户发送的消息 - `parameterless` (Iterable[Any] | None): 非参数类型依赖列表 - **返回** - ([T_Handler](typing.md#T-Handler)) -> [T_Handler](typing.md#T-Handler) ### _classmethod_ `send(message, **kwargs)` {#Matcher-send} - **说明:** 发送一条消息给当前交互用户 - **参数** - `message` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate)): 消息内容 - `**kwargs` (Any): [Bot.send](adapters/index.md#Bot-send) 的参数, 请参考对应 adapter 的 bot 对象 api - **返回** - Any ### _classmethod_ `finish(message=None, **kwargs)` {#Matcher-finish} - **说明:** 发送一条消息给当前交互用户并结束当前事件响应器 - **参数** - `message` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate) | None): 消息内容 - `**kwargs`: [Bot.send](adapters/index.md#Bot-send) 的参数, 请参考对应 adapter 的 bot 对象 api - **返回** - NoReturn ### _classmethod_ `pause(prompt=None, **kwargs)` {#Matcher-pause} - **说明:** 发送一条消息给当前交互用户并暂停事件响应器,在接收用户新的一条消息后继续下一个处理函数 - **参数** - `prompt` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate) | None): 消息内容 - `**kwargs`: [Bot.send](adapters/index.md#Bot-send) 的参数, 请参考对应 adapter 的 bot 对象 api - **返回** - NoReturn ### _classmethod_ `reject(prompt=None, **kwargs)` {#Matcher-reject} - **说明:** 最近使用 `got` / `receive` 接收的消息不符合预期, 发送一条消息给当前交互用户并将当前事件处理流程中断在当前位置,在接收用户新的一个事件后从头开始执行当前处理函数 - **参数** - `prompt` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate) | None): 消息内容 - `**kwargs`: [Bot.send](adapters/index.md#Bot-send) 的参数, 请参考对应 adapter 的 bot 对象 api - **返回** - NoReturn ### _classmethod_ `reject_arg(key, prompt=None, **kwargs)` {#Matcher-reject-arg} - **说明:** 最近使用 `got` 接收的消息不符合预期, 发送一条消息给当前交互用户并将当前事件处理流程中断在当前位置,在接收用户新的一条消息后从头开始执行当前处理函数 - **参数** - `key` (str): 参数名 - `prompt` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate) | None): 消息内容 - `**kwargs`: [Bot.send](adapters/index.md#Bot-send) 的参数, 请参考对应 adapter 的 bot 对象 api - **返回** - NoReturn ### _classmethod_ `reject_receive(id="", prompt=None, **kwargs)` {#Matcher-reject-receive} - **说明:** 最近使用 `receive` 接收的消息不符合预期, 发送一条消息给当前交互用户并将当前事件处理流程中断在当前位置,在接收用户新的一个事件后从头开始执行当前处理函数 - **参数** - `id` (str): 消息 id - `prompt` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate) | None): 消息内容 - `**kwargs`: [Bot.send](adapters/index.md#Bot-send) 的参数, 请参考对应 adapter 的 bot 对象 api - **返回** - NoReturn ### _classmethod_ `skip()` {#Matcher-skip} - **说明** 跳过当前事件处理函数,继续下一个处理函数 通常在事件处理函数的依赖中使用。 - **参数** empty - **返回** - NoReturn ### _method_ `get_receive(id, default=None)` {#Matcher-get-receive} - **说明** 获取一个 `receive` 事件 如果没有找到对应的事件,返回 `default` 值 - **重载** **1.** `(id) -> Event | None` - **参数** - `id` (str) - **返回** - [Event](adapters/index.md#Event) | None **2.** `(id, default) -> Event | T` - **参数** - `id` (str) - `default` (T) - **返回** - [Event](adapters/index.md#Event) | T ### _method_ `set_receive(id, event)` {#Matcher-set-receive} - **说明:** 设置一个 `receive` 事件 - **参数** - `id` (str) - `event` ([Event](adapters/index.md#Event)) - **返回** - None ### _method_ `get_last_receive(default=None)` {#Matcher-get-last-receive} - **说明** 获取最近一次 `receive` 事件 如果没有事件,返回 `default` 值 - **重载** **1.** `() -> Event | None` - **参数** empty - **返回** - [Event](adapters/index.md#Event) | None **2.** `(default) -> Event | T` - **参数** - `default` (T) - **返回** - [Event](adapters/index.md#Event) | T ### _method_ `get_arg(key, default=None)` {#Matcher-get-arg} - **说明** 获取一个 `got` 消息 如果没有找到对应的消息,返回 `default` 值 - **重载** **1.** `(key) -> Message | None` - **参数** - `key` (str) - **返回** - [Message](adapters/index.md#Message) | None **2.** `(key, default) -> Message | T` - **参数** - `key` (str) - `default` (T) - **返回** - [Message](adapters/index.md#Message) | T ### _method_ `set_arg(key, message)` {#Matcher-set-arg} - **说明:** 设置一个 `got` 消息 - **参数** - `key` (str) - `message` ([Message](adapters/index.md#Message)) - **返回** - None ### _method_ `set_target(target, cache=True)` {#Matcher-set-target} - **参数** - `target` (str) - `cache` (bool) - **返回** - None ### _method_ `get_target(default=None)` {#Matcher-get-target} - **重载** **1.** `() -> str | None` - **参数** empty - **返回** - str | None **2.** `(default) -> str | T` - **参数** - `default` (T) - **返回** - str | T ### _method_ `stop_propagation()` {#Matcher-stop-propagation} - **说明:** 阻止事件传播 - **参数** empty - **返回** - untyped ### _async method_ `update_type(bot, event, stack=None, dependency_cache=None)` {#Matcher-update-type} - **参数** - `bot` ([Bot](adapters/index.md#Bot)) - `event` ([Event](adapters/index.md#Event)) - `stack` (AsyncExitStack | None) - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None) - **返回** - str ### _async method_ `update_permission(bot, event, stack=None, dependency_cache=None)` {#Matcher-update-permission} - **参数** - `bot` ([Bot](adapters/index.md#Bot)) - `event` ([Event](adapters/index.md#Event)) - `stack` (AsyncExitStack | None) - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None) - **返回** - [Permission](permission.md#Permission) ### _async method_ `resolve_reject()` {#Matcher-resolve-reject} - **参数** empty - **返回** - untyped ### _method_ `ensure_context(bot, event)` {#Matcher-ensure-context} - **参数** - `bot` ([Bot](adapters/index.md#Bot)) - `event` ([Event](adapters/index.md#Event)) - **返回** - untyped ### _async method_ `simple_run(bot, event, state, stack=None, dependency_cache=None)` {#Matcher-simple-run} - **参数** - `bot` ([Bot](adapters/index.md#Bot)) - `event` ([Event](adapters/index.md#Event)) - `state` ([T_State](typing.md#T-State)) - `stack` (AsyncExitStack | None) - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None) - **返回** - untyped ### _async method_ `run(bot, event, state, stack=None, dependency_cache=None)` {#Matcher-run} - **参数** - `bot` ([Bot](adapters/index.md#Bot)) - `event` ([Event](adapters/index.md#Event)) - `state` ([T_State](typing.md#T-State)) - `stack` (AsyncExitStack | None) - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None) - **返回** - untyped ## _class_ `MatcherManager()` {#MatcherManager} - **说明** 事件响应器管理器 实现了常用字典操作,用于管理事件响应器。 - **参数** empty ### _method_ `keys()` {#MatcherManager-keys} - **参数** empty - **返回** - KeysView[int] ### _method_ `values()` {#MatcherManager-values} - **参数** empty - **返回** - ValuesView[list[type[[Matcher](#Matcher)]]] ### _method_ `items()` {#MatcherManager-items} - **参数** empty - **返回** - ItemsView[int, list[type[[Matcher](#Matcher)]]] ### _method_ `get(key, default=None)` {#MatcherManager-get} - **重载** **1.** `(key) -> list[type[Matcher]] | None` - **参数** - `key` (int) - **返回** - list[type[[Matcher](#Matcher)]] | None **2.** `(key, default) -> list[type[Matcher]] | T` - **参数** - `key` (int) - `default` (T) - **返回** - list[type[[Matcher](#Matcher)]] | T ### _method_ `pop(key)` {#MatcherManager-pop} - **参数** - `key` (int) - **返回** - list[type[[Matcher](#Matcher)]] ### _method_ `popitem()` {#MatcherManager-popitem} - **参数** empty - **返回** - tuple[int, list[type[[Matcher](#Matcher)]]] ### _method_ `clear()` {#MatcherManager-clear} - **参数** empty - **返回** - None ### _method_ `update(m, /)` {#MatcherManager-update} - **参数** - `m` (MutableMapping[int, list[type[[Matcher](#Matcher)]]]) - **返回** - None ### _method_ `setdefault(key, default)` {#MatcherManager-setdefault} - **参数** - `key` (int) - `default` (list[type[[Matcher](#Matcher)]]) - **返回** - list[type[[Matcher](#Matcher)]] ### _method_ `set_provider(provider_class)` {#MatcherManager-set-provider} - **说明:** 设置事件响应器存储器 - **参数** - `provider_class` (type[[MatcherProvider](#MatcherProvider)]): 事件响应器存储器类 - **返回** - None ## _abstract class_ `MatcherProvider(matchers)` {#MatcherProvider} - **说明:** 事件响应器存储器基类 - **参数** - `matchers` (Mapping[int, list[type[[Matcher](#Matcher)]]]): 当前存储器中已有的事件响应器 ## _var_ `matchers` {#matchers} - **类型:** untyped ================================================ FILE: website/versioned_docs/version-2.4.2/api/message.md ================================================ --- mdx: format: md sidebar_position: 2 description: nonebot.message 模块 --- # nonebot.message 本模块定义了事件处理主要流程。 NoneBot 内部处理并按优先级分发事件给所有事件响应器,提供了多个插槽以进行事件的预处理等。 ## _def_ `event_preprocessor(func)` {#event-preprocessor} - **说明** 事件预处理。 装饰一个函数,使它在每次接收到事件并分发给各响应器之前执行。 - **参数** - `func` ([T_EventPreProcessor](typing.md#T-EventPreProcessor)) - **返回** - [T_EventPreProcessor](typing.md#T-EventPreProcessor) ## _def_ `event_postprocessor(func)` {#event-postprocessor} - **说明** 事件后处理。 装饰一个函数,使它在每次接收到事件并分发给各响应器之后执行。 - **参数** - `func` ([T_EventPostProcessor](typing.md#T-EventPostProcessor)) - **返回** - [T_EventPostProcessor](typing.md#T-EventPostProcessor) ## _def_ `run_preprocessor(func)` {#run-preprocessor} - **说明** 运行预处理。 装饰一个函数,使它在每次事件响应器运行前执行。 - **参数** - `func` ([T_RunPreProcessor](typing.md#T-RunPreProcessor)) - **返回** - [T_RunPreProcessor](typing.md#T-RunPreProcessor) ## _def_ `run_postprocessor(func)` {#run-postprocessor} - **说明** 运行后处理。 装饰一个函数,使它在每次事件响应器运行后执行。 - **参数** - `func` ([T_RunPostProcessor](typing.md#T-RunPostProcessor)) - **返回** - [T_RunPostProcessor](typing.md#T-RunPostProcessor) ## _async def_ `check_and_run_matcher(Matcher, bot, event, state, stack=None, dependency_cache=None)` {#check-and-run-matcher} - **说明:** 检查并运行事件响应器。 - **参数** - `Matcher` (type[[Matcher](matcher.md#Matcher)]): 事件响应器 - `bot` ([Bot](adapters/index.md#Bot)): Bot 对象 - `event` ([Event](adapters/index.md#Event)): Event 对象 - `state` ([T_State](typing.md#T-State)): 会话状态 - `stack` (AsyncExitStack | None): 异步上下文栈 - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None): 依赖缓存 - **返回** - None ## _async def_ `handle_event(bot, event)` {#handle-event} - **说明:** 处理一个事件。调用该函数以实现分发事件。 - **参数** - `bot` ([Bot](adapters/index.md#Bot)): Bot 对象 - `event` ([Event](adapters/index.md#Event)): Event 对象 - **返回** - None - **用法** ```python driver.task_group.start_soon(handle_event, bot, event) ``` ================================================ FILE: website/versioned_docs/version-2.4.2/api/params.md ================================================ --- mdx: format: md sidebar_position: 4 description: nonebot.params 模块 --- # nonebot.params 本模块定义了依赖注入的各类参数。 ## _def_ `Arg(key=None)` {#Arg} - **说明:** Arg 参数消息 - **参数** - `key` (str | None) - **返回** - Any ## _class_ `ArgParam(*args, key, type, **kwargs)` {#ArgParam} - **说明** Arg 注入参数 本注入解析事件响应器操作 `got` 所获取的参数。 可以通过 `Arg`、`ArgStr`、`ArgPlainText` 等函数参数 `key` 指定获取的参数, 留空则会根据参数名称获取。 - **参数** - `*args` - `key` (str) - `type` (Literal['message', 'str', 'plaintext', 'prompt']) - `**kwargs` (Any) ## _def_ `ArgPlainText(key=None)` {#ArgPlainText} - **说明:** Arg 参数消息纯文本 - **参数** - `key` (str | None) - **返回** - str ## _def_ `ArgPromptResult(key=None)` {#ArgPromptResult} - **说明:** `arg` prompt 发送结果 - **参数** - `key` (str | None) - **返回** - Any ## _def_ `ArgStr(key=None)` {#ArgStr} - **说明:** Arg 参数消息文本 - **参数** - `key` (str | None) - **返回** - str ## _class_ `BotParam(*args, checker=None, **kwargs)` {#BotParam} - **说明** 注入参数。 本注入解析所有类型为且仅为 [Bot](adapters/index.md#Bot) 及其子类或 `None` 的参数。 为保证兼容性,本注入还会解析名为 `bot` 且没有类型注解的参数。 - **参数** - `*args` - `checker` ([ModelField](compat.md#ModelField) | None) - `**kwargs` (Any) ## _class_ `DefaultParam(*args, validate=False, **kwargs)` {#DefaultParam} - **说明** 默认值注入参数 本注入解析所有剩余未能解析且具有默认值的参数。 本注入参数应该具有最低优先级,因此应该在所有其他注入参数之后使用。 - **参数** - `*args` - `validate` (bool) - `**kwargs` (Any) ## _class_ `DependParam(*args, dependent, use_cache, **kwargs)` {#DependParam} - **说明** 子依赖注入参数。 本注入解析所有子依赖注入,然后将它们的返回值作为参数值传递给父依赖。 本注入应该具有最高优先级,因此应该在其他参数之前检查。 - **参数** - `*args` - `dependent` ([Dependent](dependencies/index.md#Dependent)[Any]) - `use_cache` (bool) - `**kwargs` (Any) ## _def_ `Depends(dependency=None, *, use_cache=True, validate=False)` {#Depends} - **说明:** 子依赖装饰器 - **参数** - `dependency` ([T_Handler](typing.md#T-Handler) | None): 依赖函数。默认为参数的类型注释。 - `use_cache` (bool): 是否使用缓存。默认为 `True`。 - `validate` (bool | PydanticFieldInfo): 是否使用 Pydantic 类型校验。默认为 `False`。 - **返回** - Any - **用法** ```python def depend_func() -> Any: return ... def depend_gen_func(): try: yield ... finally: ... async def handler( param_name: Any = Depends(depend_func), gen: Any = Depends(depend_gen_func), ): ... ``` ## _class_ `EventParam(*args, checker=None, **kwargs)` {#EventParam} - **说明** 注入参数 本注入解析所有类型为且仅为 [Event](adapters/index.md#Event) 及其子类或 `None` 的参数。 为保证兼容性,本注入还会解析名为 `event` 且没有类型注解的参数。 - **参数** - `*args` - `checker` ([ModelField](compat.md#ModelField) | None) - `**kwargs` (Any) ## _class_ `ExceptionParam(*args, validate=False, **kwargs)` {#ExceptionParam} - **说明** 的异常注入参数 本注入解析所有类型为 `Exception` 或 `None` 的参数。 为保证兼容性,本注入还会解析名为 `exception` 且没有类型注解的参数。 - **参数** - `*args` - `validate` (bool) - `**kwargs` (Any) ## _class_ `MatcherParam(*args, checker=None, **kwargs)` {#MatcherParam} - **说明** 事件响应器实例注入参数 本注入解析所有类型为且仅为 [Matcher](matcher.md#Matcher) 及其子类或 `None` 的参数。 为保证兼容性,本注入还会解析名为 `matcher` 且没有类型注解的参数。 - **参数** - `*args` - `checker` ([ModelField](compat.md#ModelField) | None) - `**kwargs` (Any) ## _class_ `StateParam(*args, validate=False, **kwargs)` {#StateParam} - **说明** 事件处理状态注入参数 本注入解析所有类型为 `T_State` 的参数。 为保证兼容性,本注入还会解析名为 `state` 且没有类型注解的参数。 - **参数** - `*args` - `validate` (bool) - `**kwargs` (Any) ## _def_ `EventType()` {#EventType} - **说明:** 类型参数 - **参数** empty - **返回** - str ## _def_ `EventMessage()` {#EventMessage} - **说明:** 消息参数 - **参数** empty - **返回** - Any ## _def_ `EventPlainText()` {#EventPlainText} - **说明:** 纯文本消息参数 - **参数** empty - **返回** - str ## _def_ `EventToMe()` {#EventToMe} - **说明:** `to_me` 参数 - **参数** empty - **返回** - bool ## _def_ `Command()` {#Command} - **说明:** 消息命令元组 - **参数** empty - **返回** - tuple[str, ...] ## _def_ `RawCommand()` {#RawCommand} - **说明:** 消息命令文本 - **参数** empty - **返回** - str ## _def_ `CommandArg()` {#CommandArg} - **说明:** 消息命令参数 - **参数** empty - **返回** - Any ## _def_ `CommandStart()` {#CommandStart} - **说明:** 消息命令开头 - **参数** empty - **返回** - str ## _def_ `CommandWhitespace()` {#CommandWhitespace} - **说明:** 消息命令与参数之间的空白 - **参数** empty - **返回** - str ## _def_ `ShellCommandArgs()` {#ShellCommandArgs} - **说明:** shell 命令解析后的参数字典 - **参数** empty - **返回** - Any ## _def_ `ShellCommandArgv()` {#ShellCommandArgv} - **说明:** shell 命令原始参数列表 - **参数** empty - **返回** - Any ## _def_ `RegexMatched()` {#RegexMatched} - **说明:** 正则匹配结果 - **参数** empty - **返回** - Match[str] ## _def_ `RegexStr(*groups)` {#RegexStr} - **说明:** 正则匹配结果文本 - **重载** **1.** `(group, /) -> str` - **参数** - `group` (Literal[0]) - **返回** - str **2.** `(group, /) -> str | Any` - **参数** - `group` (str | int) - **返回** - str | Any **3.** `(group1, group2, /, *groups) -> tuple[str | Any, ...]` - **参数** - `group1` (str | int) - `group2` (str | int) - `*groups` (str | int) - **返回** - tuple[str | Any, ...] ## _def_ `RegexGroup()` {#RegexGroup} - **说明:** 正则匹配结果 group 元组 - **参数** empty - **返回** - tuple[Any, ...] ## _def_ `RegexDict()` {#RegexDict} - **说明:** 正则匹配结果 group 字典 - **参数** empty - **返回** - dict[str, Any] ## _def_ `Startswith()` {#Startswith} - **说明:** 响应触发前缀 - **参数** empty - **返回** - str ## _def_ `Endswith()` {#Endswith} - **说明:** 响应触发后缀 - **参数** empty - **返回** - str ## _def_ `Fullmatch()` {#Fullmatch} - **说明:** 响应触发完整消息 - **参数** empty - **返回** - str ## _def_ `Keyword()` {#Keyword} - **说明:** 响应触发关键字 - **参数** empty - **返回** - str ## _def_ `Received(id=None, default=None)` {#Received} - **说明:** `receive` 事件参数 - **参数** - `id` (str | None) - `default` (Any) - **返回** - Any ## _def_ `LastReceived(default=None)` {#LastReceived} - **说明:** `last_receive` 事件参数 - **参数** - `default` (Any) - **返回** - Any ## _def_ `ReceivePromptResult(id=None)` {#ReceivePromptResult} - **说明:** `receive` prompt 发送结果 - **参数** - `id` (str | None) - **返回** - Any ## _def_ `PausePromptResult()` {#PausePromptResult} - **说明:** `pause` prompt 发送结果 - **参数** empty - **返回** - Any ================================================ FILE: website/versioned_docs/version-2.4.2/api/permission.md ================================================ --- mdx: format: md sidebar_position: 6 description: nonebot.permission 模块 --- # nonebot.permission 本模块是 [Matcher.permission](matcher.md#Matcher-permission) 的类型定义。 每个[事件响应器](matcher.md#Matcher) 拥有一个 [Permission](#Permission),其中是 `PermissionChecker` 的集合。 只要有一个 `PermissionChecker` 检查结果为 `True` 时就会继续运行。 ## _def_ `USER(*users, perm=None)` {#USER} - **说明** 匹配当前事件属于指定会话。 如果 `perm` 中仅有 `User` 类型的权限检查函数,则会去除原有检查函数的会话 ID 限制。 - **参数** - `*users` (str) - `perm` (Permission | None): 需要同时满足的权限 - `user`: 会话白名单 - **返回** - untyped ## _class_ `Permission(*checkers)` {#Permission} - **说明** 权限类。 当事件传递时,在 [Matcher](matcher.md#Matcher) 运行前进行检查。 - **参数** - `*checkers` ([T_PermissionChecker](typing.md#T-PermissionChecker) | [Dependent](dependencies/index.md#Dependent)[bool]): PermissionChecker - **用法** ```python Permission(async_function) | sync_function # 等价于 Permission(async_function, sync_function) ``` ### _instance-var_ `checkers` {#Permission-checkers} - **类型:** set[[Dependent](dependencies/index.md#Dependent)[bool]] - **说明:** 存储 `PermissionChecker` ### _async method_ `__call__(bot, event, stack=None, dependency_cache=None)` {#Permission---call--} - **说明:** 检查是否满足某个权限。 - **参数** - `bot` ([Bot](adapters/index.md#Bot)): Bot 对象 - `event` ([Event](adapters/index.md#Event)): Event 对象 - `stack` (AsyncExitStack | None): 异步上下文栈 - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None): 依赖缓存 - **返回** - bool ## _class_ `User(users, perm=None)` {#User} - **说明:** 检查当前事件是否属于指定会话。 - **参数** - `users` (tuple[str, ...]): 会话 ID 元组 - `perm` (Permission | None): 需同时满足的权限 ### _classmethod_ `from_event(event, perm=None)` {#User-from-event} - **说明** 从事件中获取会话 ID。 如果 `perm` 中仅有 `User` 类型的权限检查函数,则会去除原有的会话 ID 限制。 - **参数** - `event` ([Event](adapters/index.md#Event)): Event 对象 - `perm` (Permission | None): 需同时满足的权限 - **返回** - Self ### _classmethod_ `from_permission(*users, perm=None)` {#User-from-permission} - **说明** 指定会话与权限。 如果 `perm` 中仅有 `User` 类型的权限检查函数,则会去除原有的会话 ID 限制。 - **参数** - `*users` (str): 会话白名单 - `perm` (Permission | None): 需同时满足的权限 - **返回** - Self ## _class_ `Message()` {#Message} - **说明:** 检查是否为消息事件 - **参数** auto ## _class_ `Notice()` {#Notice} - **说明:** 检查是否为通知事件 - **参数** auto ## _class_ `Request()` {#Request} - **说明:** 检查是否为请求事件 - **参数** auto ## _class_ `MetaEvent()` {#MetaEvent} - **说明:** 检查是否为元事件 - **参数** auto ## _var_ `MESSAGE` {#MESSAGE} - **类型:** [Permission](#Permission) - **说明** 匹配任意 `message` 类型事件 仅在需要同时捕获不同类型事件时使用,优先使用 message type 的 Matcher。 ## _var_ `NOTICE` {#NOTICE} - **类型:** [Permission](#Permission) - **说明** 匹配任意 `notice` 类型事件 仅在需要同时捕获不同类型事件时使用,优先使用 notice type 的 Matcher。 ## _var_ `REQUEST` {#REQUEST} - **类型:** [Permission](#Permission) - **说明** 匹配任意 `request` 类型事件 仅在需要同时捕获不同类型事件时使用,优先使用 request type 的 Matcher。 ## _var_ `METAEVENT` {#METAEVENT} - **类型:** [Permission](#Permission) - **说明** 匹配任意 `meta_event` 类型事件 仅在需要同时捕获不同类型事件时使用,优先使用 meta_event type 的 Matcher。 ## _class_ `SuperUser()` {#SuperUser} - **说明:** 检查当前事件是否是消息事件且属于超级管理员 - **参数** auto ## _var_ `SUPERUSER` {#SUPERUSER} - **类型:** [Permission](#Permission) - **说明:** 匹配任意超级用户事件 ================================================ FILE: website/versioned_docs/version-2.4.2/api/plugin/_category_.json ================================================ { "position": 12 } ================================================ FILE: website/versioned_docs/version-2.4.2/api/plugin/index.md ================================================ --- mdx: format: md sidebar_position: 0 description: nonebot.plugin 模块 --- # nonebot.plugin 本模块为 NoneBot 插件开发提供便携的定义函数。 ## 快捷导入 为方便使用,本模块从子模块导入了部分内容,以下内容可以直接通过本模块导入: - `on` => [`on`](on.md#on) - `on_metaevent` => [`on_metaevent`](on.md#on-metaevent) - `on_message` => [`on_message`](on.md#on-message) - `on_notice` => [`on_notice`](on.md#on-notice) - `on_request` => [`on_request`](on.md#on-request) - `on_startswith` => [`on_startswith`](on.md#on-startswith) - `on_endswith` => [`on_endswith`](on.md#on-endswith) - `on_fullmatch` => [`on_fullmatch`](on.md#on-fullmatch) - `on_keyword` => [`on_keyword`](on.md#on-keyword) - `on_command` => [`on_command`](on.md#on-command) - `on_shell_command` => [`on_shell_command`](on.md#on-shell-command) - `on_regex` => [`on_regex`](on.md#on-regex) - `on_type` => [`on_type`](on.md#on-type) - `CommandGroup` => [`CommandGroup`](on.md#CommandGroup) - `Matchergroup` => [`MatcherGroup`](on.md#MatcherGroup) - `load_plugin` => [`load_plugin`](load.md#load-plugin) - `load_plugins` => [`load_plugins`](load.md#load-plugins) - `load_all_plugins` => [`load_all_plugins`](load.md#load-all-plugins) - `load_from_json` => [`load_from_json`](load.md#load-from-json) - `load_from_toml` => [`load_from_toml`](load.md#load-from-toml) - `load_builtin_plugin` => [`load_builtin_plugin`](load.md#load-builtin-plugin) - `load_builtin_plugins` => [`load_builtin_plugins`](load.md#load-builtin-plugins) - `require` => [`require`](load.md#require) - `PluginMetadata` => [`PluginMetadata`](model.md#PluginMetadata) ## _def_ `get_plugin(plugin_id)` {#get-plugin} - **说明** 获取已经导入的某个插件。 如果为 `load_plugins` 文件夹导入的插件,则为文件(夹)名。 如果为嵌套的子插件,标识符为 `父插件标识符:子插件文件(夹)名`。 - **参数** - `plugin_id` (str): 插件标识符,即 [Plugin.id\_](model.md#Plugin-id-)。 - **返回** - [Plugin](model.md#Plugin) | None ## _def_ `get_plugin_by_module_name(module_name)` {#get-plugin-by-module-name} - **说明** 通过模块名获取已经导入的某个插件。 如果提供的模块名为某个插件的子模块,同样会返回该插件。 - **参数** - `module_name` (str): 模块名,即 [Plugin.module_name](model.md#Plugin-module-name)。 - **返回** - [Plugin](model.md#Plugin) | None ## _def_ `get_loaded_plugins()` {#get-loaded-plugins} - **说明:** 获取当前已导入的所有插件。 - **参数** empty - **返回** - set[[Plugin](model.md#Plugin)] ## _def_ `get_available_plugin_names()` {#get-available-plugin-names} - **说明:** 获取当前所有可用的插件标识符(包含尚未加载的插件)。 - **参数** empty - **返回** - set[str] ## _def_ `get_plugin_config(config)` {#get-plugin-config} - **说明:** 从全局配置获取当前插件需要的配置项。 - **参数** - `config` (type[C]) - **返回** - C ================================================ FILE: website/versioned_docs/version-2.4.2/api/plugin/load.md ================================================ --- mdx: format: md sidebar_position: 1 description: nonebot.plugin.load 模块 --- # nonebot.plugin.load 本模块定义插件加载接口。 ## _def_ `load_plugin(module_path)` {#load-plugin} - **说明:** 加载单个插件,可以是本地插件或是通过 `pip` 安装的插件。 - **参数** - `module_path` (str | Path): 插件名称 `path.to.your.plugin` 或插件路径 `pathlib.Path(path/to/your/plugin)` - **返回** - [Plugin](model.md#Plugin) | None ## _def_ `load_plugins(*plugin_dir)` {#load-plugins} - **说明:** 导入文件夹下多个插件,以 `_` 开头的插件不会被导入! - **参数** - `*plugin_dir` (str): 文件夹路径 - **返回** - set[[Plugin](model.md#Plugin)] ## _def_ `load_all_plugins(module_path, plugin_dir)` {#load-all-plugins} - **说明:** 导入指定列表中的插件以及指定目录下多个插件,以 `_` 开头的插件不会被导入! - **参数** - `module_path` (Iterable[str]): 指定插件集合 - `plugin_dir` (Iterable[str]): 指定文件夹路径集合 - **返回** - set[[Plugin](model.md#Plugin)] ## _def_ `load_from_json(file_path, encoding="utf-8")` {#load-from-json} - **说明:** 导入指定 json 文件中的 `plugins` 以及 `plugin_dirs` 下多个插件。 以 `_` 开头的插件不会被导入! - **参数** - `file_path` (str): 指定 json 文件路径 - `encoding` (str): 指定 json 文件编码 - **返回** - set[[Plugin](model.md#Plugin)] - **用法** ```json title=plugins.json { "plugins": ["some_plugin"], "plugin_dirs": ["some_dir"] } ``` ```python nonebot.load_from_json("plugins.json") ``` ## _def_ `load_from_toml(file_path, encoding="utf-8")` {#load-from-toml} - **说明:** 导入指定 toml 文件 `[tool.nonebot]` 中的 `plugins` 以及 `plugin_dirs` 下多个插件。 以 `_` 开头的插件不会被导入! - **参数** - `file_path` (str): 指定 toml 文件路径 - `encoding` (str): 指定 toml 文件编码 - **返回** - set[[Plugin](model.md#Plugin)] - **用法** ```toml title=pyproject.toml [tool.nonebot] plugins = ["some_plugin"] plugin_dirs = ["some_dir"] ``` ```python nonebot.load_from_toml("pyproject.toml") ``` ## _def_ `load_builtin_plugin(name)` {#load-builtin-plugin} - **说明:** 导入 NoneBot 内置插件。 - **参数** - `name` (str): 插件名称 - **返回** - [Plugin](model.md#Plugin) | None ## _def_ `load_builtin_plugins(*plugins)` {#load-builtin-plugins} - **说明:** 导入多个 NoneBot 内置插件。 - **参数** - `*plugins` (str): 插件名称列表 - **返回** - set[[Plugin](model.md#Plugin)] ## _def_ `require(name)` {#require} - **说明:** 声明依赖插件。 - **参数** - `name` (str): 插件模块名或插件标识符,仅在已声明插件的情况下可使用标识符。 - **返回** - ModuleType - **异常** - RuntimeError: 插件无法加载 ## _def_ `inherit_supported_adapters(*names)` {#inherit-supported-adapters} - **说明** 获取已加载插件的适配器支持状态集合。 如果传入了多个插件名称,返回值会自动取交集。 - **参数** - `*names` (str): 插件名称列表。 - **返回** - set[str] | None - **异常** - RuntimeError: 插件未加载 - ValueError: 插件缺少元数据 ================================================ FILE: website/versioned_docs/version-2.4.2/api/plugin/manager.md ================================================ --- mdx: format: md sidebar_position: 5 description: nonebot.plugin.manager 模块 --- # nonebot.plugin.manager 本模块实现插件加载流程。 参考: [import hooks](https://docs.python.org/3/reference/import.html#import-hooks), [PEP302](https://www.python.org/dev/peps/pep-0302/) ## _class_ `PluginManager(plugins=None, search_path=None)` {#PluginManager} - **说明:** 插件管理器。 - **参数** - `plugins` (Iterable[str] | None): 独立插件模块名集合。 - `search_path` (Iterable[str] | None): 插件搜索路径(文件夹),相对于当前工作目录。 ### _property_ `third_party_plugins` {#PluginManager-third-party-plugins} - **类型:** set[str] - **说明:** 返回所有独立插件标识符。 ### _property_ `searched_plugins` {#PluginManager-searched-plugins} - **类型:** set[str] - **说明:** 返回已搜索到的插件标识符。 ### _property_ `available_plugins` {#PluginManager-available-plugins} - **类型:** set[str] - **说明:** 返回当前插件管理器中可用的插件标识符。 ### _property_ `controlled_modules` {#PluginManager-controlled-modules} - **类型:** dict[str, str] - **说明:** 返回当前插件管理器中控制的插件标识符与模块路径映射字典。 ### _method_ `load_plugin(name)` {#PluginManager-load-plugin} - **说明** 加载指定插件。 可以使用完整插件模块名或者插件标识符加载。 - **参数** - `name` (str): 插件名称或插件标识符。 - **返回** - [Plugin](model.md#Plugin) | None ### _method_ `load_all_plugins()` {#PluginManager-load-all-plugins} - **说明:** 加载所有可用插件。 - **参数** empty - **返回** - set[[Plugin](model.md#Plugin)] ## _class_ `PluginFinder()` {#PluginFinder} - **参数** auto ### _method_ `find_spec(fullname, path, target=None)` {#PluginFinder-find-spec} - **参数** - `fullname` (str) - `path` (Sequence[str] | None) - `target` (ModuleType | None) - **返回** - untyped ## _class_ `PluginLoader(manager, fullname, path)` {#PluginLoader} - **参数** - `manager` (PluginManager) - `fullname` (str) - `path` (str) ### _method_ `create_module(spec)` {#PluginLoader-create-module} - **参数** - `spec` - **返回** - ModuleType | None ### _method_ `exec_module(module)` {#PluginLoader-exec-module} - **参数** - `module` (ModuleType) - **返回** - None ================================================ FILE: website/versioned_docs/version-2.4.2/api/plugin/model.md ================================================ --- mdx: format: md sidebar_position: 3 description: nonebot.plugin.model 模块 --- # nonebot.plugin.model 本模块定义插件相关信息。 ## _class_ `PluginMetadata()` {#PluginMetadata} - **说明:** 插件元信息,由插件编写者提供 - **参数** auto ### _instance-var_ `name` {#PluginMetadata-name} - **类型:** str - **说明:** 插件名称 ### _instance-var_ `description` {#PluginMetadata-description} - **类型:** str - **说明:** 插件功能介绍 ### _instance-var_ `usage` {#PluginMetadata-usage} - **类型:** str - **说明:** 插件使用方法 ### _class-var_ `type` {#PluginMetadata-type} - **类型:** str | None - **说明:** 插件类型,用于商店分类 ### _class-var_ `homepage` {#PluginMetadata-homepage} - **类型:** str | None - **说明:** 插件主页 ### _class-var_ `config` {#PluginMetadata-config} - **类型:** type[BaseModel] | None - **说明:** 插件配置项 ### _class-var_ `supported_adapters` {#PluginMetadata-supported-adapters} - **类型:** set[str] | None - **说明** 插件支持的适配器模块路径 格式为 `[:]`,`~` 为 `nonebot.adapters.` 的缩写。 `None` 表示支持**所有适配器**。 ### _class-var_ `extra` {#PluginMetadata-extra} - **类型:** dict[Any, Any] - **说明:** 插件额外信息,可由插件编写者自由扩展定义 ### _method_ `get_supported_adapters()` {#PluginMetadata-get-supported-adapters} - **说明:** 获取当前已安装的插件支持适配器类列表 - **参数** empty - **返回** - set[type[[Adapter](../adapters/index.md#Adapter)]] | None ## _class_ `Plugin()` {#Plugin} - **说明:** 存储插件信息 - **参数** auto ### _instance-var_ `name` {#Plugin-name} - **类型:** str - **说明:** 插件名称,NoneBot 使用 文件/文件夹 名称作为插件名称 ### _instance-var_ `module` {#Plugin-module} - **类型:** ModuleType - **说明:** 插件模块对象 ### _instance-var_ `module_name` {#Plugin-module-name} - **类型:** str - **说明:** 点分割模块路径 ### _instance-var_ `manager` {#Plugin-manager} - **类型:** [PluginManager](manager.md#PluginManager) - **说明:** 导入该插件的插件管理器 ### _class-var_ `matcher` {#Plugin-matcher} - **类型:** set[type[[Matcher](../matcher.md#Matcher)]] - **说明:** 插件加载时定义的 `Matcher` ### _class-var_ `parent_plugin` {#Plugin-parent-plugin} - **类型:** Plugin | None - **说明:** 父插件 ### _class-var_ `sub_plugins` {#Plugin-sub-plugins} - **类型:** set[Plugin] - **说明:** 子插件集合 ### _property_ `id_` {#Plugin-id-} - **类型:** str - **说明:** 插件索引标识 ================================================ FILE: website/versioned_docs/version-2.4.2/api/plugin/on.md ================================================ --- mdx: format: md sidebar_position: 2 description: nonebot.plugin.on 模块 --- # nonebot.plugin.on 本模块定义事件响应器便携定义函数。 ## _def_ `store_matcher(matcher)` {#store-matcher} - **说明:** 存储一个事件响应器到插件。 - **参数** - `matcher` (type[[Matcher](../matcher.md#Matcher)]): 事件响应器 - **返回** - None ## _def_ `get_matcher_plugin(depth=...)` {#get-matcher-plugin} - **说明** 获取事件响应器定义所在插件。 **Deprecated**, 请使用 [get_matcher_source](#get-matcher-source) 获取信息。 - **参数** - `depth` (int): 调用栈深度 - **返回** - [Plugin](model.md#Plugin) | None ## _def_ `get_matcher_module(depth=...)` {#get-matcher-module} - **说明** 获取事件响应器定义所在模块。 **Deprecated**, 请使用 [get_matcher_source](#get-matcher-source) 获取信息。 - **参数** - `depth` (int): 调用栈深度 - **返回** - ModuleType | None ## _def_ `get_matcher_source(depth=...)` {#get-matcher-source} - **说明:** 获取事件响应器定义所在源码信息。 - **参数** - `depth` (int): 调用栈深度 - **返回** - MatcherSource | None ## _def_ `on(type="", rule=..., permission=..., *, handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on} - **说明:** 注册一个基础事件响应器,可自定义类型。 - **参数** - `type` (str): 事件响应器类型 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ## _def_ `on_metaevent(rule=..., permission=..., *, handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-metaevent} - **说明:** 注册一个元事件响应器。 - **参数** - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ## _def_ `on_message(rule=..., permission=..., *, handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-message} - **说明:** 注册一个消息事件响应器。 - **参数** - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ## _def_ `on_notice(rule=..., permission=..., *, handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-notice} - **说明:** 注册一个通知事件响应器。 - **参数** - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ## _def_ `on_request(rule=..., permission=..., *, handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-request} - **说明:** 注册一个请求事件响应器。 - **参数** - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ## _def_ `on_startswith(msg, rule=..., ignorecase=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-startswith} - **说明:** 注册一个消息事件响应器,并且当消息的**文本部分**以指定内容开头时响应。 - **参数** - `msg` (str | tuple[str, ...]): 指定消息开头内容 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `ignorecase` (bool): 是否忽略大小写 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ## _def_ `on_endswith(msg, rule=..., ignorecase=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-endswith} - **说明:** 注册一个消息事件响应器,并且当消息的**文本部分**以指定内容结尾时响应。 - **参数** - `msg` (str | tuple[str, ...]): 指定消息结尾内容 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `ignorecase` (bool): 是否忽略大小写 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ## _def_ `on_fullmatch(msg, rule=..., ignorecase=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-fullmatch} - **说明:** 注册一个消息事件响应器,并且当消息的**文本部分**与指定内容完全一致时响应。 - **参数** - `msg` (str | tuple[str, ...]): 指定消息全匹配内容 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `ignorecase` (bool): 是否忽略大小写 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ## _def_ `on_keyword(keywords, rule=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-keyword} - **说明:** 注册一个消息事件响应器,并且当消息纯文本部分包含关键词时响应。 - **参数** - `keywords` (set[str]): 关键词列表 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ## _def_ `on_command(cmd, rule=..., aliases=..., force_whitespace=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-command} - **说明** 注册一个消息事件响应器,并且当消息以指定命令开头时响应。 命令匹配规则参考: `命令形式匹配 `\_ - **参数** - `cmd` (str | tuple[str, ...]): 指定命令内容 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `aliases` (set[str | tuple[str, ...]] | None): 命令别名 - `force_whitespace` (str | bool | None): 是否强制命令后必须有指定空白符 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ## _def_ `on_shell_command(cmd, rule=..., aliases=..., parser=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-shell-command} - **说明** 注册一个支持 `shell_like` 解析参数的命令消息事件响应器。 与普通的 `on_command` 不同的是,在添加 `parser` 参数时, 响应器会自动处理消息。 可以通过 [ShellCommandArgv](../params.md#ShellCommandArgv) 获取原始参数列表, 通过 [ShellCommandArgs](../params.md#ShellCommandArgs) 获取解析后的参数字典。 - **参数** - `cmd` (str | tuple[str, ...]): 指定命令内容 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `aliases` (set[str | tuple[str, ...]] | None): 命令别名 - `parser` ([ArgumentParser](../rule.md#ArgumentParser) | None): `nonebot.rule.ArgumentParser` 对象 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ## _def_ `on_regex(pattern, flags=..., rule=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-regex} - **说明** 注册一个消息事件响应器,并且当消息匹配正则表达式时响应。 命令匹配规则参考: `正则匹配 `\_ - **参数** - `pattern` (str): 正则表达式 - `flags` (int | re.RegexFlag): 正则匹配标志 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ## _def_ `on_type(types, rule=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-type} - **说明:** 注册一个事件响应器,并且当事件为指定类型时响应。 - **参数** - `types` (type[[Event](../adapters/index.md#Event)] | tuple[type[[Event](../adapters/index.md#Event)], ...]): 事件类型 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ## _class_ `CommandGroup(cmd, prefix_aliases=..., *, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#CommandGroup} - **参数** - `cmd` (str | tuple[str, ...]) - `prefix_aliases` (bool) - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None) - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None) - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None) - `temp` (bool) - `expire_time` (datetime | timedelta | None) - `priority` (int) - `block` (bool) - `state` ([T_State](../typing.md#T-State) | None) ### _method_ `command(cmd, *, rule=..., aliases=..., force_whitespace=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#CommandGroup-command} - **说明:** 注册一个新的命令。新参数将会覆盖命令组默认值 - **参数** - `cmd` (str | tuple[str, ...]): 指定命令内容 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `aliases` (set[str | tuple[str, ...]] | None): 命令别名 - `force_whitespace` (str | bool | None): 是否强制命令后必须有指定空白符 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ### _method_ `shell_command(cmd, *, rule=..., aliases=..., parser=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#CommandGroup-shell-command} - **说明:** 注册一个新的 `shell_like` 命令。新参数将会覆盖命令组默认值 - **参数** - `cmd` (str | tuple[str, ...]): 指定命令内容 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `aliases` (set[str | tuple[str, ...]] | None): 命令别名 - `parser` ([ArgumentParser](../rule.md#ArgumentParser) | None): `nonebot.rule.ArgumentParser` 对象 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ## _class_ `MatcherGroup(*, type=..., rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup} - **参数** - `type` (str) - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None) - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None) - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None) - `temp` (bool) - `expire_time` (datetime | timedelta | None) - `priority` (int) - `block` (bool) - `state` ([T_State](../typing.md#T-State) | None) ### _method_ `on(*, type=..., rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on} - **说明:** 注册一个基础事件响应器,可自定义类型。 - **参数** - `type` (str): 事件响应器类型 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ### _method_ `on_metaevent(*, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-metaevent} - **说明:** 注册一个元事件响应器。 - **参数** - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ### _method_ `on_message(*, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-message} - **说明:** 注册一个消息事件响应器。 - **参数** - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ### _method_ `on_notice(*, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-notice} - **说明:** 注册一个通知事件响应器。 - **参数** - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ### _method_ `on_request(*, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-request} - **说明:** 注册一个请求事件响应器。 - **参数** - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ### _method_ `on_startswith(msg, *, ignorecase=..., rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-startswith} - **说明:** 注册一个消息事件响应器,并且当消息的**文本部分**以指定内容开头时响应。 - **参数** - `msg` (str | tuple[str, ...]): 指定消息开头内容 - `ignorecase` (bool): 是否忽略大小写 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ### _method_ `on_endswith(msg, *, ignorecase=..., rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-endswith} - **说明:** 注册一个消息事件响应器,并且当消息的**文本部分**以指定内容结尾时响应。 - **参数** - `msg` (str | tuple[str, ...]): 指定消息结尾内容 - `ignorecase` (bool): 是否忽略大小写 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ### _method_ `on_fullmatch(msg, *, ignorecase=..., rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-fullmatch} - **说明:** 注册一个消息事件响应器,并且当消息的**文本部分**与指定内容完全一致时响应。 - **参数** - `msg` (str | tuple[str, ...]): 指定消息全匹配内容 - `ignorecase` (bool): 是否忽略大小写 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ### _method_ `on_keyword(keywords, *, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-keyword} - **说明:** 注册一个消息事件响应器,并且当消息纯文本部分包含关键词时响应。 - **参数** - `keywords` (set[str]): 关键词列表 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ### _method_ `on_command(cmd, aliases=..., force_whitespace=..., *, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-command} - **说明** 注册一个消息事件响应器,并且当消息以指定命令开头时响应。 命令匹配规则参考: `命令形式匹配 `\_ - **参数** - `cmd` (str | tuple[str, ...]): 指定命令内容 - `aliases` (set[str | tuple[str, ...]] | None): 命令别名 - `force_whitespace` (str | bool | None): 是否强制命令后必须有指定空白符 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ### _method_ `on_shell_command(cmd, aliases=..., parser=..., *, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-shell-command} - **说明** 注册一个支持 `shell_like` 解析参数的命令消息事件响应器。 与普通的 `on_command` 不同的是,在添加 `parser` 参数时, 响应器会自动处理消息。 可以通过 [ShellCommandArgv](../params.md#ShellCommandArgv) 获取原始参数列表, 通过 [ShellCommandArgs](../params.md#ShellCommandArgs) 获取解析后的参数字典。 - **参数** - `cmd` (str | tuple[str, ...]): 指定命令内容 - `aliases` (set[str | tuple[str, ...]] | None): 命令别名 - `parser` ([ArgumentParser](../rule.md#ArgumentParser) | None): `nonebot.rule.ArgumentParser` 对象 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ### _method_ `on_regex(pattern, flags=..., *, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-regex} - **说明** 注册一个消息事件响应器,并且当消息匹配正则表达式时响应。 命令匹配规则参考: `正则匹配 `\_ - **参数** - `pattern` (str): 正则表达式 - `flags` (int | re.RegexFlag): 正则匹配标志 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ### _method_ `on_type(types, *, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-type} - **说明:** 注册一个事件响应器,并且当事件为指定类型时响应。 - **参数** - `types` (type[[Event](../adapters/index.md#Event)] | tuple[type[[Event](../adapters/index.md#Event)]]): 事件类型 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ================================================ FILE: website/versioned_docs/version-2.4.2/api/rule.md ================================================ --- mdx: format: md sidebar_position: 5 description: nonebot.rule 模块 --- # nonebot.rule 本模块是 [Matcher.rule](matcher.md#Matcher-rule) 的类型定义。 每个[事件响应器](matcher.md#Matcher)拥有一个 [Rule](#Rule),其中是 `RuleChecker` 的集合。 只有当所有 `RuleChecker` 检查结果为 `True` 时继续运行。 ## _class_ `Rule(*checkers)` {#Rule} - **说明** 规则类。 当事件传递时,在 [Matcher](matcher.md#Matcher) 运行前进行检查。 - **参数** - `*checkers` ([T_RuleChecker](typing.md#T-RuleChecker) | [Dependent](dependencies/index.md#Dependent)[bool]): RuleChecker - **用法** ```python Rule(async_function) & sync_function # 等价于 Rule(async_function, sync_function) ``` ### _instance-var_ `checkers` {#Rule-checkers} - **类型:** set[[Dependent](dependencies/index.md#Dependent)[bool]] - **说明:** 存储 `RuleChecker` ### _async method_ `__call__(bot, event, state, stack=None, dependency_cache=None)` {#Rule---call--} - **说明:** 检查是否符合所有规则 - **参数** - `bot` ([Bot](adapters/index.md#Bot)): Bot 对象 - `event` ([Event](adapters/index.md#Event)): Event 对象 - `state` ([T_State](typing.md#T-State)): 当前 State - `stack` (AsyncExitStack | None): 异步上下文栈 - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None): 依赖缓存 - **返回** - bool ## _class_ `CMD_RESULT()` {#CMD-RESULT} - **参数** auto ## _class_ `TRIE_VALUE()` {#TRIE-VALUE} - **说明:** TRIE_VALUE(command_start, command) - **参数** auto ## _class_ `StartswithRule(msg, ignorecase=False)` {#StartswithRule} - **说明:** 检查消息纯文本是否以指定字符串开头。 - **参数** - `msg` (tuple[str, ...]): 指定消息开头字符串元组 - `ignorecase` (bool): 是否忽略大小写 ## _def_ `startswith(msg, ignorecase=False)` {#startswith} - **说明:** 匹配消息纯文本开头。 - **参数** - `msg` (str | tuple[str, ...]): 指定消息开头字符串元组 - `ignorecase` (bool): 是否忽略大小写 - **返回** - [Rule](#Rule) ## _class_ `EndswithRule(msg, ignorecase=False)` {#EndswithRule} - **说明:** 检查消息纯文本是否以指定字符串结尾。 - **参数** - `msg` (tuple[str, ...]): 指定消息结尾字符串元组 - `ignorecase` (bool): 是否忽略大小写 ## _def_ `endswith(msg, ignorecase=False)` {#endswith} - **说明:** 匹配消息纯文本结尾。 - **参数** - `msg` (str | tuple[str, ...]): 指定消息开头字符串元组 - `ignorecase` (bool): 是否忽略大小写 - **返回** - [Rule](#Rule) ## _class_ `FullmatchRule(msg, ignorecase=False)` {#FullmatchRule} - **说明:** 检查消息纯文本是否与指定字符串全匹配。 - **参数** - `msg` (tuple[str, ...]): 指定消息全匹配字符串元组 - `ignorecase` (bool): 是否忽略大小写 ## _def_ `fullmatch(msg, ignorecase=False)` {#fullmatch} - **说明:** 完全匹配消息。 - **参数** - `msg` (str | tuple[str, ...]): 指定消息全匹配字符串元组 - `ignorecase` (bool): 是否忽略大小写 - **返回** - [Rule](#Rule) ## _class_ `KeywordsRule(*keywords)` {#KeywordsRule} - **说明:** 检查消息纯文本是否包含指定关键字。 - **参数** - `*keywords` (str): 指定关键字元组 ## _def_ `keyword(*keywords)` {#keyword} - **说明:** 匹配消息纯文本关键词。 - **参数** - `*keywords` (str): 指定关键字元组 - **返回** - [Rule](#Rule) ## _class_ `CommandRule(cmds, force_whitespace=None)` {#CommandRule} - **说明:** 检查消息是否为指定命令。 - **参数** - `cmds` (list[tuple[str, ...]]): 指定命令元组列表 - `force_whitespace` (str | bool | None): 是否强制命令后必须有指定空白符 ## _def_ `command(*cmds, force_whitespace=None)` {#command} - **说明** 匹配消息命令。 根据配置里提供的 [`command_start`](config.md#Config-command-start), [`command_sep`](config.md#Config-command-sep) 判断消息是否为命令。 可以通过 [Command](params.md#Command) 获取匹配成功的命令(例: `("test",)`), 通过 [RawCommand](params.md#RawCommand) 获取匹配成功的原始命令文本(例: `"/test"`), 通过 [CommandArg](params.md#CommandArg) 获取匹配成功的命令参数。 - **参数** - `*cmds` (str | tuple[str, ...]): 命令文本或命令元组 - `force_whitespace` (str | bool | None): 是否强制命令后必须有指定空白符 - **返回** - [Rule](#Rule) - **用法** 使用默认 `command_start`, `command_sep` 配置情况下: 命令 `("test",)` 可以匹配: `/test` 开头的消息 命令 `("test", "sub")` 可以匹配: `/test.sub` 开头的消息 :::tip 提示 命令内容与后续消息间无需空格! ::: ## _class_ `ArgumentParser()` {#ArgumentParser} - **说明** `shell_like` 命令参数解析器,解析出错时不会退出程序。 支持 [Message](adapters/index.md#Message) 富文本解析。 - **参数** auto - **用法** 用法与 `argparse.ArgumentParser` 相同, 参考文档: [argparse](https://docs.python.org/3/library/argparse.html) ### _method_ `parse_known_args(args=None, namespace=None)` {#ArgumentParser-parse-known-args} - **重载** **1.** `(args=None, namespace=None) -> tuple[Namespace, list[str | MessageSegment]]` - **参数** - `args` (Sequence[str | [MessageSegment](adapters/index.md#MessageSegment)] | None) - `namespace` (None) - **返回** - tuple[Namespace, list[str | [MessageSegment](adapters/index.md#MessageSegment)]] **2.** `(args, namespace) -> tuple[T, list[str | MessageSegment]]` - **参数** - `args` (Sequence[str | [MessageSegment](adapters/index.md#MessageSegment)] | None) - `namespace` (T) - **返回** - tuple[T, list[str | [MessageSegment](adapters/index.md#MessageSegment)]] **3.** `(*, namespace) -> tuple[T, list[str | MessageSegment]]` - **参数** - `namespace` (T) - **返回** - tuple[T, list[str | [MessageSegment](adapters/index.md#MessageSegment)]] ## _class_ `ShellCommandRule(cmds, parser)` {#ShellCommandRule} - **说明:** 检查消息是否为指定 shell 命令。 - **参数** - `cmds` (list[tuple[str, ...]]): 指定命令元组列表 - `parser` (ArgumentParser | None): 可选参数解析器 ## _def_ `shell_command(*cmds, parser=None)` {#shell-command} - **说明** 匹配 `shell_like` 形式的消息命令。 根据配置里提供的 [`command_start`](config.md#Config-command-start), [`command_sep`](config.md#Config-command-sep) 判断消息是否为命令。 可以通过 [Command](params.md#Command) 获取匹配成功的命令 (例: `("test",)`), 通过 [RawCommand](params.md#RawCommand) 获取匹配成功的原始命令文本 (例: `"/test"`), 通过 [ShellCommandArgv](params.md#ShellCommandArgv) 获取解析前的参数列表 (例: `["arg", "-h"]`), 通过 [ShellCommandArgs](params.md#ShellCommandArgs) 获取解析后的参数字典 (例: `{"arg": "arg", "h": True}`)。 :::caution 警告 如果参数解析失败,则通过 [ShellCommandArgs](params.md#ShellCommandArgs) 获取的将是 [ParserExit](exception.md#ParserExit) 异常。 ::: - **参数** - `*cmds` (str | tuple[str, ...]): 命令文本或命令元组 - `parser` (ArgumentParser | None): [ArgumentParser](#ArgumentParser) 对象 - **返回** - [Rule](#Rule) - **用法** 使用默认 `command_start`, `command_sep` 配置,更多示例参考 [argparse](https://docs.python.org/3/library/argparse.html) 标准库文档。 ```python from nonebot.rule import ArgumentParser parser = ArgumentParser() parser.add_argument("-a", action="store_true") rule = shell_command("ls", parser=parser) ``` :::tip 提示 命令内容与后续消息间无需空格! ::: ## _class_ `RegexRule(regex, flags=0)` {#RegexRule} - **说明:** 检查消息字符串是否符合指定正则表达式。 - **参数** - `regex` (str): 正则表达式 - `flags` (int): 正则表达式标记 ## _def_ `regex(regex, flags=0)` {#regex} - **说明** 匹配符合正则表达式的消息字符串。 可以通过 [RegexStr](params.md#RegexStr) 获取匹配成功的字符串, 通过 [RegexGroup](params.md#RegexGroup) 获取匹配成功的 group 元组, 通过 [RegexDict](params.md#RegexDict) 获取匹配成功的 group 字典。 - **参数** - `regex` (str): 正则表达式 - `flags` (int | re.RegexFlag): 正则表达式标记 - **返回** - [Rule](#Rule) :::tip 提示 正则表达式匹配使用 search 而非 match,如需从头匹配请使用 `r"^xxx"` 来确保匹配开头 ::: :::tip 提示 正则表达式匹配使用 `EventMessage` 的 `str` 字符串, 而非 `EventMessage` 的 `PlainText` 纯文本字符串 ::: ## _class_ `ToMeRule()` {#ToMeRule} - **说明:** 检查事件是否与机器人有关。 - **参数** auto ## _def_ `to_me()` {#to-me} - **说明:** 匹配与机器人有关的事件。 - **参数** empty - **返回** - [Rule](#Rule) ## _class_ `IsTypeRule(*types)` {#IsTypeRule} - **说明:** 检查事件类型是否为指定类型。 - **参数** - `*types` (type[[Event](adapters/index.md#Event)]) ## _def_ `is_type(*types)` {#is-type} - **说明:** 匹配事件类型。 - **参数** - `*types` (type[[Event](adapters/index.md#Event)]): 事件类型 - **返回** - [Rule](#Rule) ================================================ FILE: website/versioned_docs/version-2.4.2/api/typing.md ================================================ --- mdx: format: md sidebar_position: 11 description: nonebot.typing 模块 --- # nonebot.typing 本模块定义了 NoneBot 模块中共享的一些类型。 使用 Python 的 Type Hint 语法, 参考 [`PEP 484`](https://www.python.org/dev/peps/pep-0484/), [`PEP 526`](https://www.python.org/dev/peps/pep-0526/) 和 [`typing`](https://docs.python.org/3/library/typing.html)。 ## _def_ `overrides(InterfaceClass)` {#overrides} - **说明:** 标记一个方法为父类 interface 的 implement - **参数** - `InterfaceClass` (object) - **返回** - untyped ## _def_ `type_has_args(type_)` {#type-has-args} - **参数** - `type_` (type[Any]) - **返回** - bool ## _def_ `origin_is_union(origin)` {#origin-is-union} - **参数** - `origin` (type[Any] | None) - **返回** - bool ## _def_ `origin_is_literal(origin)` {#origin-is-literal} - **说明:** 判断是否是 Literal 类型 - **参数** - `origin` (type[Any] | None) - **返回** - bool ## _def_ `all_literal_values(type_)` {#all-literal-values} - **说明:** 获取 Literal 类型包含的所有值 - **参数** - `type_` (type[Any]) - **返回** - list[Any] ## _def_ `origin_is_annotated(origin)` {#origin-is-annotated} - **说明:** 判断是否是 Annotated 类型 - **参数** - `origin` (type[Any] | None) - **返回** - bool ## _def_ `is_none_type(type_)` {#is-none-type} - **说明:** 判断是否是 None 类型 - **参数** - `type_` (type[Any]) - **返回** - bool ## _def_ `evaluate_forwardref(ref, globalns, localns)` {#evaluate-forwardref} - **参数** - `ref` (ForwardRef) - `globalns` (dict[str, Any]) - `localns` (dict[str, Any]) - **返回** - Any ## _class_ `StateFlag()` {#StateFlag} - **参数** auto ## _var_ `T_State` {#T-State} - **类型:** dict[Any, Any] - **说明:** 事件处理状态 State 类型 ## _var_ `T_BotConnectionHook` {#T-BotConnectionHook} - **类型:** \_DependentCallable[Any] - **说明** Bot 连接建立时钩子函数 依赖参数: - DependParam: 子依赖参数 - BotParam: Bot 对象 - DefaultParam: 带有默认值的参数 ## _var_ `T_BotDisconnectionHook` {#T-BotDisconnectionHook} - **类型:** \_DependentCallable[Any] - **说明** Bot 连接断开时钩子函数 依赖参数: - DependParam: 子依赖参数 - BotParam: Bot 对象 - DefaultParam: 带有默认值的参数 ## _var_ `T_CallingAPIHook` {#T-CallingAPIHook} - **类型:** ([Bot](adapters/index.md#Bot), str, dict[str, Any]) -> Awaitable[Any] - **说明:** `bot.call_api` 钩子函数 ## _var_ `T_CalledAPIHook` {#T-CalledAPIHook} - **类型:** ([Bot](adapters/index.md#Bot), Exception | None, str, dict[str, Any], Any) -> Awaitable[Any] - **说明:** `bot.call_api` 后执行的函数,参数分别为 bot, exception, api, data, result ## _var_ `T_EventPreProcessor` {#T-EventPreProcessor} - **类型:** \_DependentCallable[Any] - **说明** 事件预处理函数 EventPreProcessor 类型 依赖参数: - DependParam: 子依赖参数 - BotParam: Bot 对象 - EventParam: Event 对象 - StateParam: State 对象 - DefaultParam: 带有默认值的参数 ## _var_ `T_EventPostProcessor` {#T-EventPostProcessor} - **类型:** \_DependentCallable[Any] - **说明** 事件后处理函数 EventPostProcessor 类型 依赖参数: - DependParam: 子依赖参数 - BotParam: Bot 对象 - EventParam: Event 对象 - StateParam: State 对象 - DefaultParam: 带有默认值的参数 ## _var_ `T_RunPreProcessor` {#T-RunPreProcessor} - **类型:** \_DependentCallable[Any] - **说明** 事件响应器运行前预处理函数 RunPreProcessor 类型 依赖参数: - DependParam: 子依赖参数 - BotParam: Bot 对象 - EventParam: Event 对象 - StateParam: State 对象 - MatcherParam: Matcher 对象 - DefaultParam: 带有默认值的参数 ## _var_ `T_RunPostProcessor` {#T-RunPostProcessor} - **类型:** \_DependentCallable[Any] - **说明** 事件响应器运行后后处理函数 RunPostProcessor 类型 依赖参数: - DependParam: 子依赖参数 - BotParam: Bot 对象 - EventParam: Event 对象 - StateParam: State 对象 - MatcherParam: Matcher 对象 - ExceptionParam: 异常对象(可能为 None) - DefaultParam: 带有默认值的参数 ## _var_ `T_RuleChecker` {#T-RuleChecker} - **类型:** \_DependentCallable[bool] - **说明** RuleChecker 即判断是否响应事件的处理函数。 依赖参数: - DependParam: 子依赖参数 - BotParam: Bot 对象 - EventParam: Event 对象 - StateParam: State 对象 - DefaultParam: 带有默认值的参数 ## _var_ `T_PermissionChecker` {#T-PermissionChecker} - **类型:** \_DependentCallable[bool] - **说明** PermissionChecker 即判断事件是否满足权限的处理函数。 依赖参数: - DependParam: 子依赖参数 - BotParam: Bot 对象 - EventParam: Event 对象 - DefaultParam: 带有默认值的参数 ## _var_ `T_Handler` {#T-Handler} - **类型:** \_DependentCallable[Any] - **说明:** Handler 处理函数。 ## _var_ `T_TypeUpdater` {#T-TypeUpdater} - **类型:** \_DependentCallable[str] - **说明** TypeUpdater 在 Matcher.pause, Matcher.reject 时被运行,用于更新响应的事件类型。 默认会更新为 `message`。 依赖参数: - DependParam: 子依赖参数 - BotParam: Bot 对象 - EventParam: Event 对象 - StateParam: State 对象 - MatcherParam: Matcher 对象 - DefaultParam: 带有默认值的参数 ## _var_ `T_PermissionUpdater` {#T-PermissionUpdater} - **类型:** \_DependentCallable[[Permission](permission.md#Permission)] - **说明** PermissionUpdater 在 Matcher.pause, Matcher.reject 时被运行,用于更新会话对象权限。 默认会更新为当前事件的触发对象。 依赖参数: - DependParam: 子依赖参数 - BotParam: Bot 对象 - EventParam: Event 对象 - StateParam: State 对象 - MatcherParam: Matcher 对象 - DefaultParam: 带有默认值的参数 ## _var_ `T_DependencyCache` {#T-DependencyCache} - **类型:** dict[\_DependentCallable[Any], DependencyCache] - **说明:** 依赖缓存, 用于存储依赖函数的返回值 ================================================ FILE: website/versioned_docs/version-2.4.2/api/utils.md ================================================ --- mdx: format: md sidebar_position: 8 description: nonebot.utils 模块 --- # nonebot.utils 本模块包含了 NoneBot 的一些工具函数 ## _def_ `escape_tag(s)` {#escape-tag} - **说明** 用于记录带颜色日志时转义 `` 类型特殊标签 参考: [loguru color 标签](https://loguru.readthedocs.io/en/stable/api/logger.html#color) - **参数** - `s` (str): 需要转义的字符串 - **返回** - str ## _def_ `deep_update(mapping, *updating_mappings)` {#deep-update} - **说明:** 深度更新合并字典 - **参数** - `mapping` (dict[K, Any]) - `*updating_mappings` (dict[K, Any]) - **返回** - dict[K, Any] ## _def_ `lenient_issubclass(cls, class_or_tuple)` {#lenient-issubclass} - **说明:** 检查 cls 是否是 class_or_tuple 中的一个类型子类并忽略类型错误。 - **参数** - `cls` (Any) - `class_or_tuple` (type[Any] | tuple[type[Any], ...]) - **返回** - bool ## _def_ `generic_check_issubclass(cls, class_or_tuple)` {#generic-check-issubclass} - **说明** 检查 cls 是否是 class_or_tuple 中的一个类型子类。 特别的: - 如果 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` (Any) - `class_or_tuple` (type[Any] | tuple[type[Any], ...]) - **返回** - bool ## _def_ `type_is_complex(type_)` {#type-is-complex} - **说明:** 检查 type\_ 是否是复杂类型 - **参数** - `type_` (type[Any]) - **返回** - bool ## _def_ `is_coroutine_callable(call)` {#is-coroutine-callable} - **说明:** 检查 call 是否是一个 callable 协程函数 - **参数** - `call` ((...) -> Any) - **返回** - bool ## _def_ `is_gen_callable(call)` {#is-gen-callable} - **说明:** 检查 call 是否是一个生成器函数 - **参数** - `call` ((...) -> Any) - **返回** - bool ## _def_ `is_async_gen_callable(call)` {#is-async-gen-callable} - **说明:** 检查 call 是否是一个异步生成器函数 - **参数** - `call` ((...) -> Any) - **返回** - bool ## _def_ `run_sync(call)` {#run-sync} - **说明:** 一个用于包装 sync function 为 async function 的装饰器 - **参数** - `call` ((P) -> R): 被装饰的同步函数 - **返回** - (P) -> Coroutine[None, None, R] ## _def_ `run_sync_ctx_manager(cm)` {#run-sync-ctx-manager} - **说明:** 一个用于包装 sync context manager 为 async context manager 的执行函数 - **参数** - `cm` (AbstractContextManager[T]) - **返回** - AsyncGenerator[T, None] ## _async def_ `run_coro_with_catch(coro, exc, return_on_err=None)` {#run-coro-with-catch} - **说明:** 运行协程并当遇到指定异常时返回指定值。 - **重载** **1.** `(coro, exc, return_on_err=None) -> T | None` - **参数** - `coro` (Coroutine[Any, Any, T]) - `exc` (tuple[type[Exception], ...]) - `return_on_err` (None) - **返回** - T | None **2.** `(coro, exc, return_on_err) -> T | R` - **参数** - `coro` (Coroutine[Any, Any, T]) - `exc` (tuple[type[Exception], ...]) - `return_on_err` (R) - **返回** - T | R - **参数** - `coro`: 要运行的协程 - `exc`: 要捕获的异常 - `return_on_err`: 当发生异常时返回的值 - **返回** 协程的返回值或发生异常时的指定值 ## _async def_ `run_coro_with_shield(coro)` {#run-coro-with-shield} - **说明:** 运行协程并在取消时屏蔽取消异常。 - **参数** - `coro` (Coroutine[Any, Any, T]): 要运行的协程 - **返回** - T: 协程的返回值 ## _def_ `flatten_exception_group(exc_group)` {#flatten-exception-group} - **参数** - `exc_group` (BaseExceptionGroup[E]) - **返回** - Generator[E, None, None] ## _def_ `get_name(obj)` {#get-name} - **说明:** 获取对象的名称 - **参数** - `obj` (Any) - **返回** - str ## _def_ `path_to_module_name(path)` {#path-to-module-name} - **说明:** 转换路径为模块名 - **参数** - `path` (Path) - **返回** - str ## _def_ `resolve_dot_notation(obj_str, default_attr, default_prefix=None)` {#resolve-dot-notation} - **说明:** 解析并导入点分表示法的对象 - **参数** - `obj_str` (str) - `default_attr` (str) - `default_prefix` (str | None) - **返回** - Any ## _class_ `classproperty(func)` {#classproperty} - **说明:** 类属性装饰器 - **参数** - `func` ((Any) -> T) ## _class_ `DataclassEncoder()` {#DataclassEncoder} - **说明:** 可以序列化 [Message](adapters/index.md#Message)(List[Dataclass]) 的 `JSONEncoder` - **参数** auto ### _method_ `default(o)` {#DataclassEncoder-default} - **参数** - `o` - **返回** - untyped ## _def_ `logger_wrapper(logger_name)` {#logger-wrapper} - **说明:** 用于打印 adapter 的日志。 - **参数** - `logger_name` (str): adapter 的名称 - **返回** - untyped: 日志记录函数 日志记录函数的参数: - level: 日志等级 - message: 日志信息 - exception: 异常信息 ================================================ FILE: website/versioned_docs/version-2.4.2/appendices/api-calling.mdx ================================================ --- sidebar_position: 4 description: 使用平台接口,完成更多功能 options: menu: - category: appendices weight: 50 --- # 使用平台接口 import Messenger from "@/components/Messenger"; 在 NoneBot 中,除了使用事件响应器操作发送文本消息外,我们还可以直接通过使用协议适配器提供的方法来使用平台特定的接口,完成发送特殊消息、获取信息等其他平台提供的功能。同时,在部分无法使用事件响应器的情况中,例如[定时任务](../best-practice/scheduler.md),我们也可以使用平台接口来完成需要的功能。 ## 发送平台特殊消息 在之前的章节中,我们介绍了如何向用户发送文本消息以及[如何处理平台消息](../tutorial/message.md),现在我们来向用户发送平台特殊消息。 :::caution 注意 在以下的示例中,我们将使用 `Console` 协议适配器来演示如何发送平台消息。在实际使用中,你需要确保你使用的**消息序列类型**与你所要发送的**平台类型**一致。 ::: ```python {4,7-17} title=weather/__init__.py import inspect from nonebot.adapters.console import MessageSegment @weather.got("location", prompt=MessageSegment.emoji("question") + "请输入地名") async def got_location(location: str = ArgPlainText()): result = await weather.send( MessageSegment.markdown( inspect.cleandoc( f""" # {location} - 今天 ⛅ 多云 20℃~24℃ """ ) ) ) ``` 在上面的示例中,我们使用了 `Console` 协议适配器提供的 `MessageSegment` 类来发送平台特定的消息 `emoji` 和 `markdown`。这两种消息可以显示在终端中,但是无法在其他平台上使用。在事件响应器操作中,我们可以使用 `str`、消息序列、消息段、消息模板四种类型来发送消息,但其中只有 `str` 和[纯文本形式的消息模板类型](../tutorial/message.md#使用消息模板)消息可以在所有平台上使用。 `send` 事件响应器操作实际上是由协议适配器通过调用平台 API 来实现的,通常会将 API 调用的结果作为返回值返回。 ## 调用平台 API 在 NoneBot 中,我们可以通过 `Bot` 对象来调用协议适配器支持的平台 API,来完成更多的功能。 ### 获取 Bot 在调用平台 API 之前,我们首先要获得 Bot 对象。有两种方式可以获得 Bot 对象。 在事件处理流程的上下文中,我们可以直接使用依赖注入 Bot 来获取: ```python {1,4} title=weather/__init__.py from nonebot.adapters import Bot @weather.got("location", prompt="请输入地名") async def got_location(bot: Bot, location: str = ArgPlainText()): ... ``` 依赖注入会确保你获得的 Bot 对象与类型注解的 Bot 类型一致。也就是说,如果你使用的是 Bot 基类,将会允许任何平台的 Bot 对象;如果你使用的是平台特定的 Bot 类型,将会只允许该平台的 Bot 对象,其他类型的 Bot 将会跳过这个事件处理函数。更多详情请参考[事件处理重载](./overload.md)。 在其他情况下,我们可以通过 NoneBot 提供的方法来获取 Bot 对象,这些方法将会在[使用适配器](../advanced/adapter.md#获取-bot-对象)中详细介绍: ```python {4,6} from nonebot import get_bot # 获取当前所有 Bot 中的第一个 bot = get_bot() # 获取指定 ID 的 Bot bot = get_bot("bot_id") ``` ### 调用 API 在获得 Bot 对象后,我们可以通过 Bot 的实例方法来调用平台 API: ```python {2,5} # 通过 bot.api_name(**kwargs) 的方法调用 API result = await bot.get_user_info(user_id=12345678) # 通过 bot.call_api(api_name, **kwargs) 的方法调用 API result = await bot.call_api("get_user_info", user_id=12345678) ``` :::caution 注意 实际可以使用的 API 以及参数取决于平台提供的接口以及协议适配器的实现,请参考协议适配器以及平台文档。 ::: 在了解了如何调用 API 后,我们可以来改进 `weather` 插件,使得消息发送后,调用 `Console` 接口响铃提醒机器人用户: ```python {4,18} title=weather/__init__.py from nonebot.adapters.console import Bot, MessageSegment @weather.got("location", prompt=MessageSegment.emoji("question") + "请输入地名") async def got_location(bot: Bot, location: str = ArgPlainText()): await weather.send( MessageSegment.markdown( inspect.cleandoc( f""" # {location} - 今天 ⛅ 多云 20℃~24℃ """ ) ) ) await bot.bell() ``` ================================================ FILE: website/versioned_docs/version-2.4.2/appendices/config.mdx ================================================ --- sidebar_position: 0 description: 读取用户配置来控制插件行为 options: menu: - category: appendices weight: 10 --- # 配置 import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; 配置是项目中非常重要的一部分,为了方便我们控制机器人的行为,NoneBot 提供了一套配置系统。下面我们将会补充[指南](../quick-start.mdx)中的天气插件,使其能够读取用户配置。在这之前,我们需要先了解一下配置系统,如果你已经了解了 NoneBot 中的配置方法,可以跳转到[编写插件配置](#插件配置)。 NoneBot 使用 [`pydantic`](https://docs.pydantic.dev/) 以及 [`python-dotenv`](https://saurabh-kumar.com/python-dotenv/) 来读取 dotenv 配置文件以及环境变量,从而控制机器人行为。配置文件需要符合 dotenv 格式,复杂数据类型需使用 JSON 格式或 [pydantic 支持格式](https://docs.pydantic.dev/usage/types/)填写。 NoneBot 内置的配置项列表及含义可以在[内置配置项](#内置配置项)中查看。 :::caution 注意 NoneBot 自 2.2.0 起兼容了 Pydantic v1 与 v2 版本,以下文档中 Pydantic 相关示例均采用 v2 版本用法。 如果在使用商店或其他第三方插件的过程中遇到 Pydantic 相关警告或报错,例如: ```python pydantic_core._pydantic_core.ValidationError: 1 validation error for Config Input should be a valid dictionary or instance of Config [type=model_type, input_value=Config(...), input_type=Config] ``` 请考虑降级 Pydantic 至 v1 版本: ```bash pip install --force-reinstall 'pydantic~=1.10' ``` ::: ## 配置项的加载 在 NoneBot 中,我们可以把配置途径分为 **直接传入**、**系统环境变量**、**dotenv 配置文件** 三种,其加载优先级依次由高到低。 ### 直接传入 在 NoneBot 初始化的过程中,可以通过 `nonebot.init()` 传入任意合法的 Python 变量,也可以在初始化完成后直接赋值。 通常,在初始化前的传参会在机器人的入口文件(如 `bot.py`)中进行,而初始化后的赋值可以在任何地方进行。 ```python {4,8,9} title=bot.py import nonebot # 初始化时 nonebot.init(custom_config1="config on init") # 初始化后 config = nonebot.get_driver().config config.custom_config1 = "changed after init" config.custom_config2 = "new config after init" ``` ### 系统环境变量 在 dotenv 配置文件中定义的配置项,也会在环境变量中进行寻找。如果在环境变量中发现同名配置项(大小写不敏感),将会覆盖 dotenv 中所填值。 例如,在 dotenv 配置文件中存在配置项 `custom_config`: ```dotenv CUSTOM_CONFIG=config in dotenv ``` 同时,设置环境变量: ```bash # windows cmd set CUSTOM_CONFIG 'config in environment variables' # windows powershell $Env:CUSTOM_CONFIG='config in environment variables' # linux/macOS export CUSTOM_CONFIG='config in environment variables' ``` 那最终 NoneBot 所读取的内容为环境变量中的内容,即 `config in environment variables`。 :::caution 注意 NoneBot 不会自发读取未被定义的配置项的环境变量,如果需要读取某一环境变量需要在 dotenv 配置文件中进行声明。 ::: ### dotenv 配置文件 dotenv 是一种便捷的跨平台配置通用模式,也是我们推荐的配置方式。 NoneBot 在启动时将会从系统环境变量或者 `.env` 文件中寻找配置项 `ENVIRONMENT` (大小写不敏感),默认值为 `prod`。这将决定 NoneBot 后续进一步加载环境配置的文件路径 `.env.{ENVIRONMENT}`。 #### 配置项解析 dotenv 文件中的配置值使用 JSON 进行解析。如果配置项值无法被解析,将作为**字符串**处理。例如: ```dotenv STRING_CONFIG=some string LIST_CONFIG=[1, 2, 3] DICT_CONFIG={"key": "value"} MULTILINE_CONFIG=' [ { "item_key": "item_value" } ] ' EMPTY_CONFIG= NULL_CONFIG ``` 将被解析为: ```python dotenv_config = { "string_config": "some string", "list_config": [1, 2, 3], "dict_config": {"key": "value"}, "multiline_config": [{"item_key": "item_value"}], "empty_config": "", "null_config": None } ``` 特别的,NoneBot 支持使用 `env_nested_delimiter` 配置嵌套字典,在层与层之间使用 `__` 分隔即可: ```dotenv DICT={"k1": "v1", "k2": null} DICT__K2=v2 DICT__K3=v3 DICT__INNER__K4=v4 ``` 将被解析为: ```python dotenv_config = { "dict": { "k1": "v1", "k2": "v2", "k3": "v3", "inner": { "k4": "v4" } } } ``` #### .env 文件 `.env` 文件是基础配置文件,该文件中的配置项在不同环境下都会被加载,但会被 `.env.{ENVIRONMENT}` 文件中的配置所**覆盖**。 我们可以在 `.env` 文件中写入当前的环境信息: ```dotenv ENVIRONMENT=dev COMMON_CONFIG=common config # 这个配置项在任何环境中都会被加载 ``` 这样,我们在启动 NoneBot 时就会从 `.env.dev` 文件中加载剩余配置项。 :::tip 提示 在生产环境中,可以通过设置环境变量 `ENVIRONMENT=prod` 来确保 NoneBot 读取正确的环境配置。 ::: #### .env.\{ENVIRONMENT\} 文件 `.env.{ENVIRONMENT}` 文件类似于预设,可以让我们在多套不同的配置方案中灵活切换,默认 NoneBot 会读取 `.env.prod` 配置。如果你使用了 `nb-cli` 创建 `simple` 项目,那么将含有两套预设配置:`.env.dev` 和 `.env.prod`。 在 NoneBot 初始化时,可以指定加载某个环境配置文件: ```python nonebot.init(_env_file=".env.dev") ``` 这将忽略在 `.env` 文件或环境变量中指定的 `ENVIRONMENT` 配置项。 ## 读取全局配置项 NoneBot 的全局配置对象可以通过 `driver` 获取,如: ```python import nonebot config = nonebot.get_driver().config ``` 如果我们需要获取某个配置项,可以直接通过 `config` 对象的属性访问: ```python superusers = config.superusers ``` 如果配置项不存在,将会抛出异常。 ## 插件配置 在一个涉及大量配置项的项目中,通过直接读取全局配置项的方式显然并不高效。同时,由于额外的全局配置项没有预先定义,开发时编辑器将无法提示字段与类型,并且运行时没有对配置项直接进行合法性检查。那么就需要一种方式来规范定义插件配置项。 在 NoneBot 中,我们使用强大高效的 `pydantic` 来定义配置模型,这个模型可以被用于配置的读取和类型检查等。例如在 `weather` 插件目录中新建 `config.py` 来定义一个模型: ```python title=weather/config.py from pydantic import BaseModel, field_validator class Config(BaseModel): weather_api_key: str weather_command_priority: int = 10 weather_plugin_enabled: bool = True @field_validator("weather_command_priority") @classmethod def check_priority(cls, v: int) -> int: if v >= 1: return v raise ValueError("weather command priority must greater than 1") ``` 在 `config.py` 中,我们定义了一个 `Config` 类,它继承自 `pydantic.BaseModel`,并定义了一些配置项。在 `Config` 类中,我们还定义了一个 `check_priority` 方法,它用于检查 `weather_command_priority` 配置项的合法性。更多关于 `pydantic` 的编写方式,可以参考 [pydantic 官方文档](https://docs.pydantic.dev/)。 在定义好配置模型后,我们可以在插件加载时通过配置模型获取插件配置: ```python {5,11} title=weather/__init__.py from nonebot import get_plugin_config from .config import Config plugin_config = get_plugin_config(Config) weather = on_command( "天气", rule=to_me(), aliases={"weather", "查天气"}, priority=plugin_config.weather_command_priority, block=True, ) ``` 然后,我们便可以从 `plugin_config` 中读取配置了,例如 `plugin_config.weather_api_key`。 这种方式可以简洁、高效地读取配置项,同时也可以设置默认值或者在运行时对配置项进行合法性检查,防止由于配置项导致的插件出错等情况出现。 :::tip 提示 发布插件应该为自身的事件响应器提供可配置的优先级,以便插件使用者可以自定义多个插件间的响应顺序。 ::: 由于插件配置项是从全局配置中读取的,通常我们需要在配置项名称前面添加前缀名,以防止配置项冲突。例如在上方的示例中,我们就添加了配置项前缀 `weather_`。但是这样会导致在使用配置项时过长的变量名,因此我们可以使用 `pydantic` 的 `alias` 或者通过配置 scope 来简化配置项名称。这里我们以 scope 配置为例: ```python title=weather/config.py from pydantic import BaseModel class ScopedConfig(BaseModel): api_key: str command_priority: int = 10 plugin_enabled: bool = True class Config(BaseModel): weather: ScopedConfig ``` ```python title=weather/__init__.py from nonebot import get_plugin_config from .config import Config plugin_config = get_plugin_config(Config).weather ``` 这样我们就可以省略插件配置项名称中的前缀 `weather_` 了。但需要注意的是,如果我们使用了 scope 配置,那么在配置文件中也需要使用 [`env_nested_delimiter` 格式](#配置项解析),例如: ```dotenv WEATHER__API_KEY=123456 WEATHER__COMMAND_PRIORITY=10 ``` ## 内置配置项 配置项 API 文档可以前往 [Config 类](../api/config.md#Config)查看。 ### Driver - **类型**: `str` - **默认值**: `"~fastapi"` NoneBot 运行所使用的驱动器。具体配置方法可以参考[安装驱动器](../tutorial/store.mdx#安装驱动器)和[选择驱动器](../advanced/driver.md)。 ```dotenv DRIVER=~fastapi+~httpx+~websockets ``` ```bash # windows cmd set DRIVER '~fastapi+~httpx+~websockets' # windows powershell $Env:DRIVER='~fastapi+~httpx+~websockets' # linux/macOS export DRIVER='~fastapi+~httpx+~websockets' ``` ```python title=bot.py import nonebot nonebot.init(driver="~fastapi+~httpx+~websockets") ``` ### Host - **类型**: `IPvAnyAddress` - **默认值**: `127.0.0.1` 当 NoneBot 作为服务端时,监听的 IP / 主机名。 ```dotenv HOST=127.0.0.1 ``` ```bash # windows cmd set HOST '127.0.0.1' # windows powershell $Env:HOST='127.0.0.1' # linux/macOS export HOST='127.0.0.1' ``` ```python title=bot.py import nonebot nonebot.init(host="127.0.0.1") ``` ### Port - **类型**: `int` (1 ~ 65535) - **默认值**: `8080` 当 NoneBot 作为服务端时,监听的端口。 ```dotenv PORT=8080 ``` ```bash # windows cmd set PORT '8080' # windows powershell $Env:PORT='8080' # linux/macOS export PORT='8080' ``` ```python title=bot.py import nonebot nonebot.init(port=8080) ``` ### Log Level - **类型**: `int | str` - **默认值**: `INFO` NoneBot 日志输出等级,可以为 `int` 类型等级或等级名称。具体等级对照表参考 [loguru 日志等级](https://loguru.readthedocs.io/en/stable/api/logger.html#levels)。 :::tip 提示 日志等级名称应为大写,如 `INFO`。 ::: ```dotenv LOG_LEVEL=DEBUG ``` ```bash # windows cmd set LOG_LEVEL 'DEBUG' # windows powershell $Env:LOG_LEVEL='DEBUG' # linux/macOS export LOG_LEVEL='DEBUG' ``` ```python title=bot.py import nonebot nonebot.init(log_level="DEBUG") ``` ### API Timeout - **类型**: `float | None` - **默认值**: `30.0` 调用平台接口的超时时间,单位为秒。`None` 表示不设置超时时间。 ```dotenv API_TIMEOUT=10.0 ``` ```bash # windows cmd set API_TIMEOUT '10.0' # windows powershell $Env:API_TIMEOUT='10.0' # linux/macOS export API_TIMEOUT='10.0' ``` ```python title=bot.py import nonebot nonebot.init(api_timeout=10.0) ``` ### SuperUsers - **类型**: `set[str]` - **默认值**: `set()` 机器人超级用户,可以使用权限 [`SUPERUSER`](../api/permission.md#SUPERUSER)。 ```dotenv SUPERUSERS=["123123123"] ``` ```bash # windows cmd set SUPERUSERS '["123123123"]' # windows powershell $Env:SUPERUSERS='["123123123"]' # linux/macOS export SUPERUSERS='["123123123"]' ``` ```python title=bot.py import nonebot nonebot.init(superusers={"123123123"}) ``` ### Nickname - **类型**: `set[str]` - **默认值**: `set()` 机器人昵称,通常协议适配器会根据用户是否 @bot 或者是否以机器人昵称开头来判断是否是向机器人发送的消息。 ```dotenv NICKNAME=["bot"] ``` ```bash # windows cmd set NICKNAME '["bot"]' # windows powershell $Env:NICKNAME='["bot"]' # linux/macOS export NICKNAME='["bot"]' ``` ```python title=bot.py import nonebot nonebot.init(nickname={"bot"}) ``` ### Command Start 和 Command Separator - **类型**: `set[str]` - **默认值**: - Command Start: `{"/"}` - Command Separator: `{"."}` 命令消息的起始符和分隔符。用于 [`command`](../advanced/matcher.md#command) 规则。 ```dotenv COMMAND_START=["/", ""] COMMAND_SEP=[".", " "] ``` ```bash # windows cmd set COMMAND_START '["/", ""]' set COMMAND_SEP '[".", " "]' # windows powershell $Env:COMMAND_START='["/", ""]' $Env:COMMAND_SEP='[".", " "]' # linux/macOS export COMMAND_START='["/", ""]' export COMMAND_SEP='[".", " "]' ``` ```python title=bot.py import nonebot nonebot.init(command_start={"/", ""}, command_sep={".", " "}) ``` ### Session Expire Timeout - **类型**: `timedelta` - **默认值**: `timedelta(minutes=2)` 用户会话超时时间,配置格式参考 [Datetime Types](https://docs.pydantic.dev/latest/api/standard_library_types/#datetimetimedelta)。 ```dotenv SESSION_EXPIRE_TIMEOUT=00:02:00 ``` ```bash # windows cmd set SESSION_EXPIRE_TIMEOUT '00:02:00' # windows powershell $Env:SESSION_EXPIRE_TIMEOUT='00:02:00' # linux/macOS export SESSION_EXPIRE_TIMEOUT='00:02:00' ``` ```python title=bot.py import nonebot nonebot.init(session_expire_timeout=120) ``` ================================================ FILE: website/versioned_docs/version-2.4.2/appendices/log.md ================================================ --- sidebar_position: 6 description: 记录与控制日志 options: menu: - category: appendices weight: 70 --- # 日志 无论是在开发还是在生产环境中,日志都是一个重要的功能,可以帮助我们了解运行状况、排查问题等。虽然我们可以使用 `print` 来将需要的信息输出到控制台,但是这种方式难以控制,而且不利于日志的归档、分析等。NoneBot 使用优秀的 [Loguru](https://loguru.readthedocs.io/) 库来进行日志记录。 ## 记录日志 我们可以从 NoneBot 中导入 `logger` 对象,然后使用 `logger` 对象的方法来记录日志。 ```python from nonebot import logger logger.trace("This is a trace message") logger.debug("This is a debug message") logger.info("This is an info message") logger.success("This is a success message") logger.warning("This is a warning message") logger.error("This is an error message") logger.critical("This is a critical message") ``` 我们仅需一行代码即可记录对应级别的日志。日志可以通过配置 [`LOG_LEVEL` 配置项](./config.mdx#log-level)来过滤输出等级,控制台中仅会输出大于等于 `LOG_LEVEL` 的日志。默认的 `LOG_LEVEL` 为 `INFO`,即只会输出 `INFO`、`SUCCESS`、`WARNING`、`ERROR`、`CRITICAL` 级别的日志。 如果需要记录 `Exception traceback` 日志,可以向 `logger` 添加 `exception` 选项: ```python {4} try: 1 / 0 except ZeroDivisionError: logger.opt(exception=True).error("ZeroDivisionError") ``` 如果需要输出彩色日志,可以向 `logger` 添加 `colors` 选项: ```python logger.opt(colors=True).warning("We got a BIG problem") ``` 更多日志记录方法请参考 [Loguru 文档](https://loguru.readthedocs.io/)。 ## 自定义日志输出 NoneBot 在启动时会添加一个默认的日志处理器,该处理器会将日志输出到**stdout**,并且根据 `LOG_LEVEL` 配置项过滤日志等级。 默认的日志格式为: ```text {time:MM-DD HH:mm:ss} [{level}] {name} | {message} ``` 我们可以从 `nonebot.log` 模块导入以使用 NoneBot 的默认格式和过滤器: ```python from nonebot.log import default_format, default_filter ``` 如果需要自定义日志格式,我们需要移除 NoneBot 默认的日志处理器并添加新的日志处理器。例如,在机器人入口文件中 `nonebot.init` 之前添加以下内容: ```python title=bot.py from nonebot.log import logger_id # 移除 NoneBot 默认的日志处理器 logger.remove(logger_id) # 添加新的日志处理器 logger.add( sys.stdout, level=0, diagnose=True, format="{time:MM-DD HH:mm:ss} [{level}] {name} | {message}", filter=default_filter ) ``` 如果想要输出日志到文件,我们可以使用 `logger.add` 方法添加文件处理器: ```python title=bot.py logger.add("error.log", level="ERROR", format=default_format, rotation="1 week") ``` 更多日志处理器的使用方法请参考 [Loguru 文档](https://loguru.readthedocs.io/)。 ## 重定向 logging 日志 `logging` 是 Python 标准库中的日志模块,NoneBot 提供了一个 logging handler 用于将 `logging` 日志重定向到 `loguru` 处理。 ```python from nonebot.log import LoguruHandler # root logger 添加 LoguruHandler logging.basicConfig(handlers=[LoguruHandler()]) # 或者为其他 logging.Logger 添加 LoguruHandler logger.addHandler(LoguruHandler()) ``` ================================================ FILE: website/versioned_docs/version-2.4.2/appendices/overload.md ================================================ --- sidebar_position: 7 description: 根据事件类型进行不同的处理 options: menu: - category: appendices weight: 80 --- # 事件类型与重载 在之前的示例中,我们已经了解了如何[获取事件信息](../tutorial/event-data.mdx)以及[使用平台接口](./api-calling.mdx)。但是,事件信息通常不仅仅包含消息这一个内容,还有其他平台提供的信息,例如消息发送时间、消息发送者等等。同时,在使用平台接口时,我们需要确保使用的**平台接口**与所要发送的**平台类型**一致,对不同类型的事件需要做出不同的处理。在本章节中,我们将介绍如何获取事件更多的信息以及根据事件类型进行不同的处理。 ## 事件类型 在 NoneBot 中,事件均是 `nonebot.adapters.Event` 基类的子类型,基类对一些必要的属性进行了抽象,子类型则根据不同的平台进行了实现。在[自定义权限](./permission.mdx#自定义权限)一节中,我们就使用了 `Event` 的抽象方法 `get_user_id` 来获取事件发送者 ID,这个方法由协议适配器进行了实现,返回机器人用户对应的平台 ID。更多的基类抽象方法可以在[使用适配器](../advanced/adapter.md#获取事件通用信息)中查看。 既然事件是基类的子类型,我们实际可以获得的信息通常多于基类抽象方法所提供的。如果我们不满足于基类能获得的信息,我们可以小小的修改一下事件处理函数的事件参数类型注解,使其变为子类型,这样我们就可以通过协议适配器定义的子类型来获取更多的信息。我们以 `Console` 协议适配器为例: ```python {4} title=weather/__init__.py from nonebot.adapters.console import MessageEvent @weather.got("location", prompt="请输入地名") async def got_location(event: MessageEvent, location: str = ArgPlainText()): await weather.finish(f"{event.time.strftime('%Y-%m-%d')} {location} 的天气是...") ``` 在上面的代码中,我们获取了 `Console` 协议适配器的消息事件提供的发送时间 `time` 属性。 :::caution 注意 如果**基类**就能满足你的需求,那么就**不要修改**事件参数类型注解,这样可以使你的代码更加**通用**,可以在更多平台上运行。如何根据不同平台事件类型进行不同的处理,我们将在[重载](#重载)一节中介绍。 ::: ## 重载 我们在编写机器人时,常常会遇到这样一个问题:如何对私聊和群聊消息进行不同的处理?如何对不同平台的事件进行不同的处理?针对这些问题,NoneBot 提供了一个便捷而高效的解决方案 ── 重载。简单来说,依赖函数会根据其参数的类型注解来决定是否执行,忽略不符合其参数类型注解的情况。这样,我们就可以通过修改事件参数类型注解来实现对不同事件的处理,或者修改 `Bot` 参数类型注解来实现使用不同平台的接口。我们以 `OneBot` 协议适配器为例: ```python {4,8} from nonebot.adapters.onebot.v11 import PrivateMessageEvent, GroupMessageEvent @matcher.handle() async def handle_private(event: PrivateMessageEvent): await matcher.finish("私聊消息") @matcher.handle() async def handle_group(event: GroupMessageEvent): await matcher.finish("群聊消息") ``` 这样,机器人用户就会在私聊和群聊中分别收到不同的回复。同样的,我们也可以通过修改 `Bot` 参数类型注解来实现使用不同平台的接口: ```python from nonebot.adapters.console import Bot as ConsoleBot from nonebot.adapters.onebot.v11 import Bot as OneBot @matcher.handle() async def handle_console(bot: ConsoleBot): await bot.bell() @matcher.handle() async def handle_onebot(bot: OneBot): await bot.send_group_message(group_id=123123, message="OneBot") ``` :::caution 注意 重载机制对所有的参数类型注解都有效,因此,依赖注入也可以使用这个特性来对不同的返回值进行处理。 但 Bot、Event 和 Matcher 三者的参数类型注解具有最高检查优先级,如果三者任一类型注解不匹配,那么其他依赖注入将不会执行(如:`Depends`)。 ::: :::tip 提示 如何更好地编写一个跨平台的插件,我们将在[最佳实践](../best-practice/multi-adapter.mdx)中介绍。 ::: ================================================ FILE: website/versioned_docs/version-2.4.2/appendices/permission.mdx ================================================ --- sidebar_position: 5 description: 控制事件响应器的权限 options: menu: - category: appendices weight: 60 --- # 权限控制 import Messenger from "@site/src/components/Messenger"; **权限控制**是机器人在实际应用中需要解决的重点问题之一,NoneBot 提供了灵活的权限控制机制 —— `Permission`。 类似于响应规则 `Rule`,`Permission` 是由非负整数个 `PermissionChecker` 所共同组成的**用于筛选事件**的对象。但需要特别说明的是,权限和响应规则有如下区别: 1. 权限检查**先于**响应规则检查 2. `Permission` 只需**其中一个** `PermissionChecker` 返回 `True` 时就会检查通过 3. 权限检查进行时,上下文中并不存在会话状态 `state` 4. `Rule` 仅在**初次触发**事件响应器时进行检查,在余下的会话中并不会限制事件;而 `Permission` 会**持续生效**,在连续对话中一直对事件主体加以限制。 ## 基础使用 通常情况下,`Permission` 更侧重于对于**触发事件的机器人用户**的筛选,例如由 NoneBot 自身提供的 `SUPERUSER` 权限,便是筛选出会话发起者是否为超级用户。它可以对输入的用户进行鉴别,如果符合要求则会被认为通过并返回 `True`,反之则返回 `False`。 简单来说,`Permission` 是一个用于筛选出符合要求的用户的机制,可以通过 `Permission` 精确的控制响应对象的覆盖范围,从而拒绝掉我们所不希望的事件。 例如,我们可以在 `weather` 插件中添加一个超级用户可用的指令: ```python {3,9} title=weather/__init__.py from typing import Tuple from nonebot.params import Command from nonebot.permission import SUPERUSER manage = on_command( ("天气", "启用"), rule=to_me(), aliases={("天气", "禁用")}, permission=SUPERUSER, ) @manage.handle() async def control(cmd: Tuple[str, str] = Command()): _, action = cmd if action == "启用": plugin_config.weather_plugin_enabled = True elif action == "禁用": plugin_config.weather_plugin_enabled = False await manage.finish(f"天气插件已{action}") ``` 如上方示例所示,在注册事件响应器时,我们设置了 `permission` 参数,那么这个事件处理器在触发事件前的检查阶段会对用户身份进行验证,如果不符合我们设置的条件(此处即为**超级用户**)则不会响应。此时,我们向机器人发送 `/天气.禁用` 指令,机器人不会有任何响应,因为我们还不是机器人的超级管理员。我们在 dotenv 文件中设置了 `SUPERUSERS` 配置项之后,机器人就会响应我们的指令了。 ```dotenv title=.env SUPERUSERS=["console_user"] ``` ## 自定义权限 与事件响应规则类似,`PermissionChecker` 也是一个返回值为 `bool` 类型的依赖函数,即 `PermissionChecker` 支持依赖注入。例如,我们可以限制用户的指令调用次数: ```python title=weather/__init__.py from nonebot.adapters import Event fake_db: Dict[str, int] = {} async def limit_permission(event: Event): count = fake_db.setdefault(event.get_user_id(), 100) if count > 0: fake_db[event.get_user_id()] -= 1 return True return False weather = on_command("天气", permission=limit_permission) ``` ## 权限组合 权限之间可以通过 `|` 运算符进行组合,使得任意一个权限检查返回 `True` 时通过。例如: ```python {4-6} perm1 = Permission(foo_checker) perm2 = Permission(bar_checker) perm = perm1 | perm2 perm = perm1 | bar_checker perm = foo_checker | perm2 ``` 同样的,我们也无需担心组合了一个 `None` 值,`Permission` 会自动忽略 `None` 值。 ```python assert (perm | None) is perm ``` ## 主动使用权限 除了在事件响应器中使用权限外,我们也可以主动使用权限来判断事件是否符合条件。例如: ```python {3} perm = Permission(some_checker) result: bool = await perm(bot, event) ``` 我们只需要传入 `Bot` 实例、事件,`Permission` 会并发调用所有 `PermissionChecker` 进行检查,并返回结果。 ================================================ FILE: website/versioned_docs/version-2.4.2/appendices/rule.md ================================================ --- sidebar_position: 1 description: 自定义响应规则 options: menu: - category: appendices weight: 20 --- # 响应规则 机器人在实际应用中,往往会接收到多种多样的事件类型,NoneBot 通过响应规则来控制事件的处理。 在[指南](../tutorial/matcher.md#为事件响应器添加参数)中,我们为 `weather` 命令添加了一个 `rule=to_me()` 参数,这个参数就是一个响应规则,确保只有在私聊或者 `@bot` 时才会响应。 响应规则是一个 `Rule` 对象,它由一系列的 `RuleChecker` 函数组成,每个 `RuleChecker` 函数都会检查事件是否符合条件,如果所有的检查都通过,则事件会被处理。 ## RuleChecker `RuleChecker` 是一个返回值为 `bool` 类型的依赖函数,即 `RuleChecker` 支持依赖注入。我们可以根据上一节中添加的[配置项](./config.mdx#插件配置),在 `weather` 插件目录中编写一个响应规则: ```python {7,8} title=weather/__init__.py from nonebot import get_plugin_config from .config import Config plugin_config = get_plugin_config(Config) async def is_enable() -> bool: return plugin_config.weather_plugin_enabled weather = on_command("天气", rule=is_enable) ``` 在上面的代码中,我们定义了一个函数 `is_enable`,它会检查配置项 `weather_plugin_enabled` 是否为 `True`。这个函数 `is_enable` 即为一个 `RuleChecker`。 ## Rule `Rule` 是若干个 `RuleChecker` 的集合,它会并发调用每个 `RuleChecker`,只有当所有 `RuleChecker` 检查通过时匹配成功。例如:我们可以组合两个 `RuleChecker`,一个用于检查插件是否启用,一个用于检查用户是否在黑名单中: ```python {10} from nonebot.rule import Rule from nonebot.adapters import Event async def is_enable() -> bool: return plugin_config.weather_plugin_enabled async def is_blacklisted(event: Event) -> bool: return event.get_user_id() not in BLACKLIST rule = Rule(is_enable, is_blacklisted) weather = on_command("天气", rule=rule) ``` ## 合并响应规则 在定义响应规则时,我们可以将规则进行细分,来更好地复用规则。而在使用时,我们需要合并多个规则。除了使用 `Rule` 对象来组合多个 `RuleChecker` 外,我们还可以对 `Rule` 对象进行合并。在原 `weather` 插件中,我们可以将 `rule=to_me()` 与 `rule=is_enable` 使用 `&` 运算符合并: ```python {13} title=weather/__init__.py from nonebot.rule import to_me from nonebot import get_plugin_config from .config import Config plugin_config = get_plugin_config(Config) async def is_enable() -> bool: return plugin_config.weather_plugin_enabled weather = on_command( "天气", rule=to_me() & is_enable, aliases={"weather", "查天气"}, priority=plugin_config.weather_command_priority, block=True, ) ``` 这样,`weather` 命令就只会在插件启用且在私聊或者 `@bot` 时才会响应。 合并响应规则可以有多种形式,例如: ```python {4-6} rule1 = Rule(foo_checker) rule2 = Rule(bar_checker) rule = rule1 & rule2 rule = rule1 & bar_checker rule = foo_checker & rule2 ``` 同时,我们也无需担心合并了一个 `None` 值,`Rule` 会忽略 `None` 值。 ```python assert (rule & None) is rule ``` ## 主动使用响应规则 除了在事件响应器中使用响应规则外,我们也可以主动使用响应规则来判断事件是否符合条件。例如: ```python {3} rule = Rule(some_checker) result: bool = await rule(bot, event, state) ``` 我们只需要传入 `Bot` 对象、事件和会话状态,`Rule` 会并发调用所有 `RuleChecker` 进行检查,并返回结果。 ## 内置响应规则 NoneBot 内置了一些常用的响应规则,可以直接通过事件响应器辅助函数或者自行合并其他规则使用。内置响应规则列表可以参考[事件响应器进阶](../advanced/matcher.md) ================================================ FILE: website/versioned_docs/version-2.4.2/appendices/session-control.mdx ================================================ --- sidebar_position: 2 description: 更灵活的会话控制 options: menu: - category: appendices weight: 30 --- # 会话控制 import Messenger from "@site/src/components/Messenger"; 在[指南](../tutorial/event-data.mdx#使用依赖注入)的 `weather` 插件中,我们使用依赖注入获取了机器人用户发送的地名参数,并根据地名参数进行相应的回复。但是,一问一答的对话模式仅仅适用于简单的对话场景,如果我们想要实现更复杂的对话模式,就需要使用会话控制。 ## 询问并获取用户输入 在 `weather` 插件中,我们对于用户未输入地名参数的情况直接回复了 `请输入地名` 并结束了事件流程。但是,这样用户体验并不好,需要重新输入指令和地名参数才能获取天气回复。我们现在来实现询问并获取用户地名参数的功能。 ### 询问用户 我们可以使用事件响应器操作中的 `got` 装饰器来表示当前事件处理流程需要询问并获取用户输入的消息: ```python {6} title=weather/__init__.py @weather.handle() async def handle_function(args: Message = CommandArg()): if location := args.extract_plain_text(): await weather.finish(f"今天{location}的天气是...") @weather.got("location", prompt="请输入地名") async def got_location(): ... ``` 在上面的代码中,我们使用 `got` 事件响应器操作来向用户发送 `prompt` 消息,并等待用户的回复。用户的回复消息将会被作为 `location` 参数存储于事件响应器状态中。 :::tip 提示 事件处理函数根据定义的顺序依次执行。 ::: ### 获取用户输入 在询问以及用户回复之后,我们就可以获取到我们需要的 `location` 参数了。我们使用 `ArgPlainText` 依赖注入来获取参数纯文本信息: ```python {9} title=weather/__init__.py from nonebot.params import ArgPlainText @weather.handle() async def handle_function(args: Message = CommandArg()): if location := args.extract_plain_text(): await weather.finish(f"今天{location}的天气是...") @weather.got("location", prompt="请输入地名") async def got_location(location: str = ArgPlainText()): await weather.finish(f"今天{location}的天气是...") ``` 在上面的代码中,我们在 `got_location` 函数中定义了一个依赖注入参数 `location`,他的值将会是用户回复的消息纯文本信息。获取到用户输入的地名参数后,我们就可以进行天气查询并回复了。 :::tip 提示 如果想要获取用户回复的消息对象 `Message` ,可以使用 `Arg` 依赖注入。 ::: ### 跳过询问 在上面的代码中,如果用户在输入天气指令时,同时提供了地名参数,我们直接回复了天气信息,这部分的逻辑是和询问用户地名参数之后的逻辑一致的。如果在复杂的业务场景下,我们希望这部分代码应该复用以减少代码冗余。我们可以使用事件响应器操作中的 `set_arg` 来主动设置一个参数: ```python {4,6} title=weather/__init__.py from nonebot.matcher import Matcher @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()): await weather.finish(f"今天{location}的天气是...") ``` 请注意,设置参数需要使用依赖注入来获取 `Matcher` 实例以确保上下文正确,且参数值应为 `Message` 对象。 在 `location` 参数被设置之后,`got` 事件响应器操作将不再会询问并等待用户的回复,而是直接进入 `got_location` 函数。 ## 请求重新输入 在实际的业务场景中,用户的输入很有可能并非是我们所期望的,而结束事件处理流程让用户重新发送指令也不是一个好的体验。这时我们可以使用 `reject` 事件响应器操作来请求用户重新输入: ```python {8,9} title=weather/__init__.py @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}的天气是...") ``` 在上面的代码中,我们在 `got_location` 函数中判断用户输入的地名是否在支持的城市列表中,如果不在,则使用 `reject` 事件响应器操作。操作将会向用户发送 `reject` 参数中的消息,并等待用户回复后,重新执行 `got_location` 函数。通过 `got` 和 `reject` 事件响应器操作,我们实现了类似于**循环**的执行方式。 `reject` 事件响应器操作与 `finish` 类似,NoneBot 会在向机器人用户发送消息内容后抛出 `RejectedException` 异常来暂停事件响应流程以等待用户输入。也就是说,在 `reject` 被执行后,后续的程序同样是不会被执行的。 ## 更多事件响应器操作 在之前的章节中,我们已经大致了解了五个事件响应器操作:`handle`、`got`、`finish`、`send` 和 `reject`。现在我们来完整地介绍一下这些操作。 事件响应器操作可以分为两大类:**交互操作**和**流程控制操作**。我们可以通过交互操作来与用户进行交互,而流程控制操作则可以用来控制事件处理流程的执行。 :::tip 提示 事件处理流程按照事件处理函数添加顺序执行,已经结束的事件处理函数不可能被恢复执行。 ::: ### handle `handle` 事件响应器操作是一个装饰器,用于向事件处理流程添加一个事件处理函数。 ```python @matcher.handle() async def handle_func(): ... ``` `handle` 装饰器支持嵌套操作,即一个事件处理函数可以被添加多次: ```python @matcher.handle() @matcher.handle() async def handle_func(): # 这个函数会被执行两次 ... ``` ### got `got` 事件响应器操作也是一个装饰器,它会在当前装饰的事件处理函数运行之前,中断当前事件处理流程,等待接收一个新的事件。它可以通过 `prompt` 参数来向用户发送询问消息,然后等待用户的回复消息,贴近对话形式会话。 `got` 装饰器接受一个参数 `key` 和一个可选参数 `prompt`。当会话状态中不存在 `key` 对应的消息时,会向用户发送 `prompt` 参数的消息,并等待用户回复。`prompt` 参数的类型和 [`send`](#send) 事件响应器操作的参数类型一致。 在事件处理函数中,可以通过依赖注入的方式来获取接收到的消息,参考:[`Arg`](../advanced/dependency.mdx#arg)、[`ArgStr`](../advanced/dependency.mdx#argstr)、[`ArgPlainText`](../advanced/dependency.mdx#argplaintext)。 ```python @matcher.got("key", prompt="请输入...") async def got_func(key: Message = Arg()): ... ``` `got` 装饰器支持与 `got` 和 `receive` 装饰器嵌套操作,即一个事件处理函数可以在接收多个事件或消息后执行: ```python @matcher.got("key1", prompt="请输入key1...") @matcher.got("key2", prompt="请输入key2...") @matcher.receive("key3") async def got_func(key1: Message = Arg(), key2: Message = Arg(), key3: Event = Received("key3")): ... ``` ### receive `receive` 事件响应器操作也是一个装饰器,它会在当前装饰的事件处理函数运行之前,中断当前事件处理流程,等待接收一个新的事件。与 `got` 不同的是,`receive` 不会向用户发送询问消息,并且等待一个用户事件。可以接收的事件类型取决于[会话更新](../advanced/session-updating.md)。 `receive` 装饰器接受一个可选参数 id,用于标识当前需要接收的事件,如果不指定,则默认为空 `""`。 在事件处理函数中,可以通过依赖注入的方式来获取接收到的事件,参考:[`Received`](../advanced/dependency.mdx#received)、[`LastReceived`](../advanced/dependency.mdx#lastreceived)。 ```python @matcher.receive("id") async def receive_func(event: Event = Received("id")): ... ``` `receive` 装饰器支持与 `got` 和 `receive` 装饰器嵌套操作,即一个事件处理函数可以在接收多个事件或消息后执行: ```python @matcher.receive("key1") @matcher.got("key2", prompt="请输入key2...") @matcher.got("key3", prompt="请输入key3...") async def receive_func(key1: Event = Received("key1"), key2: Message = Arg(), key3: Message = Arg()): ... ``` ### send `send` 事件响应器操作用于向用户回复一条消息。协议适配器会根据当前 event 选择回复的途径。 `send` 操作接受一个参数 message 和其他任何协议适配器接受的参数。message 参数类型可以是字符串、消息序列、消息段或者消息模板。消息模板将会使用会话状态字典进行渲染后发送。 这个操作等同于使用 `bot.send(event, message, **kwargs)`,但不需要自行传入 `event`。 ```python @matcher.handle() async def _(): await matcher.send("Hello world!") ``` ### finish 向用户回复一条消息(可选),并立即结束**整个处理流程**。 参数与 [`send`](#send) 相同。 ```python @matcher.handle() async def _(): await matcher.finish("Hello world!") # 下面的代码不会被执行 ``` ### pause 向用户回复一条消息(可选),立即结束**当前**事件处理函数,等待接收一个新的事件后进入**下一个**事件处理函数。 参数与 [`send`](#send) 相同。 ```python @matcher.handle() async def _(): if need_confirm: await matcher.pause("请在两分钟内确认执行") @matcher.handle() async def _(): ... ``` ### reject 向用户回复一条消息(可选),立即结束**当前**事件处理函数,等待接收一个新的事件后再次执行**当前**事件处理函数。 `reject` 可以用于拒绝当前 `receive` 接收的事件或 `got` 接收的参数。通常在用户回复不符合格式或标准需要重新输入,或者用于循环进行用户交互。 参数与 [`send`](#send) 相同。 ```python @matcher.got("arg") async def _(arg: str = ArgPlainText()): if not is_valid(arg): await matcher.reject("Invalid arg!") ``` ### reject_arg 向用户回复一条消息(可选),立即结束**当前**事件处理函数,等待接收一个新的消息后再次执行**当前**事件处理函数。 `reject_arg` 用于拒绝指定 `got` 接收的参数,通常在嵌套装饰器时使用。 `reject_arg` 操作接受一个 key 参数以及可选的 prompt 参数。prompt 参数与 [`send`](#send) 相同。 ```python @matcher.got("a") @matcher.got("b") async def _(a: str = ArgPlainText(), b: str = ArgPlainText()): if a not in b: await matcher.reject_arg("a", "Invalid a!") ``` ### reject_receive 向用户回复一条消息(可选),立即结束**当前**事件处理函数,等待接收一个新的事件后再次执行**当前**事件处理函数。 `reject_receive` 用于拒绝指定 `receive` 接收的事件,通常在嵌套装饰器时使用。 `reject_receive` 操作接受一个可选的 id 参数以及可选的 prompt 参数。id 参数默认为空 `""`,prompt 参数与 [`send`](#send) 相同。 ```python @matcher.receive("a") @matcher.receive("b") async def _(a: Event = Received("a"), b: Event = Received("b")): if a.get_user_id() != b.get_user_id(): await matcher.reject_receive("a") ``` ### skip 立即结束当前事件处理函数,进入下一个事件处理函数。 通常在依赖注入中使用,用于跳过当前事件处理函数的执行。 ```python from nonebot.params import Depends async def dependency(): matcher.skip() @matcher.handle() async def _(check=Depends(dependency)): # 这个函数不会被执行 ``` ### stop_propagation 阻止事件向更低优先级的事件响应器传播。 ```python from nonebot.matcher import Matcher @foo.handle() async def _(matcher: Matcher): matcher.stop_propagation() ``` :::caution 注意 `stop_propagation` 操作是实例方法,需要先通过依赖注入获取事件响应器实例再进行调用。 ::: ### get_arg 获取一个 `got` 接收的参数。 `get_arg` 操作接受一个 key 参数和一个可选的 default 参数。当参数不存在时,将返回 default 或 `None`。 ```python from nonebot.matcher import Matcher @matcher.handle() async def _(matcher: Matcher): key = matcher.get_arg("key", default=None) ``` ### set_arg 设置 / 覆盖一个 `got` 接收的参数。 `set_arg` 操作接受一个 key 参数和一个 value 参数。请注意,value 参数必须是消息序列对象,如需存储其他数据请使用[会话状态](./session-state.md)。 ```python from nonebot.matcher import Matcher @matcher.handle() async def _(matcher: Matcher): matcher.set_arg("key", Message("value")) ``` ### get_receive 获取一个 `receive` 接收的事件。 `get_receive` 操作接受一个 id 参数和一个可选的 default 参数。当事件不存在时,将返回 default 或 `None`。 ```python from nonebot.matcher import Matcher @matcher.handle() async def _(matcher: Matcher): event = matcher.get_receive("id", default=None) ``` ### get_last_receive 获取最近的一个 `receive` 接收的事件。 `get_last_receive` 操作接受一个可选的 default 参数。当事件不存在时,将返回 default 或 `None`。 ```python from nonebot.matcher import Matcher @matcher.handle() async def _(matcher: Matcher): event = matcher.get_last_receive(default=None) ``` ### set_receive 设置 / 覆盖一个 `receive` 接收的事件。 `set_receive` 操作接受一个 id 参数和一个 event 参数。请注意,event 参数必须是事件对象,如需存储其他数据请使用[会话状态](./session-state.md)。 ```python from nonebot.matcher import Matcher @matcher.handle() async def _(matcher: Matcher): matcher.set_receive("key", Event()) ``` ================================================ FILE: website/versioned_docs/version-2.4.2/appendices/session-state.md ================================================ --- sidebar_position: 3 description: 会话状态信息 options: menu: - category: appendices weight: 40 --- # 会话状态 在事件处理流程中,和用户交互的过程即是会话。在会话中,我们可能需要记录一些信息,例如用户的重试次数等等,以便在会话中的不同阶段进行判断和处理。这些信息都可以存储于会话状态中。 NoneBot 中的会话状态是一个字典,可以通过类型 `T_State` 来获取。字典内可以存储任意类型的数据,但是要注意的是,NoneBot 本身会在会话状态中存储一些信息,因此不要使用 [NoneBot 使用的键名](../api/consts.md)。 ```python from nonebot.typing import T_State @matcher.got("key", prompt="请输入密码") async def _(state: T_State, key: str = ArgPlainText()): if key != "some password": try_count = state.get("try_count", 1) if try_count >= 3: await matcher.finish("密码错误次数过多") else: state["try_count"] = try_count + 1 await matcher.reject("密码错误,请重新输入") await matcher.finish("密码正确") ``` 会话状态的生命周期与事件处理流程相同,在期间的任何一个事件处理函数都可以进行读写。 ```python from nonebot.typing import T_State @matcher.handle() async def _(state: T_State): state["key"] = "value" @matcher.handle() async def _(state: T_State): await matcher.finish(state["key"]) ``` 会话状态还可以用于发送动态消息,消息模板在发送时会使用会话状态字典进行渲染。消息模板的使用方法已经在[消息处理](../tutorial/message.md#使用消息模板)中介绍过,这里不再赘述。 ```python from nonebot.typing import T_State from nonebot.adapters import MessageTemplate @matcher.handle() async def _(state: T_State): state["username"] = "user" @matcher.got("password", prompt=MessageTemplate("请输入 {username} 的密码")) async def _(): await matcher.finish(MessageTemplate("密码为 {password}")) ``` ================================================ FILE: website/versioned_docs/version-2.4.2/appendices/whats-next.md ================================================ --- sidebar_position: 99 description: 下一步──进阶! --- # 下一步 至此,我们已经了解了 NoneBot 的大多数功能用法,相信你已经可以独自写出一个插件了。现在你可以选择: - 即刻开始插件编写! - 更深入地了解 NoneBot 的[更多功能和原理](../advanced/plugin-info.md)! ================================================ FILE: website/versioned_docs/version-2.4.2/best-practice/alconna/README.mdx ================================================ --- 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#安装插件)来了解并选择安装插件的方式。如: 在**项目目录**下执行以下命令: ```shell nb plugin install nonebot-plugin-alconna ``` ```shell pip install nonebot-plugin-alconna ``` ```shell pdm add nonebot-plugin-alconna ``` ## 导入插件 由于 `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` 来改写这个插件。
插件示例 ```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}的天气是...") ```
```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) ================================================ FILE: website/versioned_docs/version-2.4.2/best-practice/alconna/_category_.json ================================================ { "label": "Alconna 命令解析拓展", "position": 6 } ================================================ FILE: website/versioned_docs/version-2.4.2/best-practice/alconna/command.md ================================================ --- sidebar_position: 2 description: Alconna 基本介绍 --- # Alconna 本体 [`Alconna`](https://github.com/ArcletProject/Alconna) 隶属于 `ArcletProject`,是一个简单、灵活、高效的命令参数解析器, 并且不局限于解析命令式字符串。 我们通过一个例子来讲解 **Alconna** 的核心 —— `Args`, `Subcommand`, `Option`: ```python from arclet.alconna import Alconna, Args, Subcommand, Option alc = Alconna( "pip", Subcommand( "install", Args["package", str], Option("-r|--requirement", Args["file", str]), Option("-i|--index-url", Args["url", str]), ) ) res = alc.parse("pip install nonebot2 -i URL") print(res) # matched=True, header_match=(origin='pip' result='pip' matched=True groups={}), subcommands={'install': (value=Ellipsis args={'package': 'nonebot2'} options={'index-url': (value=None args={'url': 'URL'})} subcommands={})}, other_args={'package': 'nonebot2', 'url': 'URL'} print(res.all_matched_args) # {'package': 'nonebot2', 'url': 'URL'} ``` 这段代码通过`Alconna`创捷了一个接受主命令名为`pip`, 子命令为`install`且子命令接受一个 **Args** 参数`package`和二个 **Option** 参数`-r`和`-i`的命令参数解析器, 通过`parse`方法返回解析结果 **Arparma** 的实例。 ## 命令头 命令头是指命令的前缀 (Prefix) 与命令名 (Command) 的组合,例如 !help 中的 ! 与 help。 | 前缀 | 命令名 | 匹配内容 | 说明 | | :--------------------------: | :--------: | :---------------------------------------------------------: | :--------------: | | - | "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"]` | 元素头 | | [nepattern.NUMBER] | "bar" | `[123, "bar"]` 或 `[123.456, "bar"]` | 表达式头 | | [123, "foo"] | "bar" | `[123, "bar"]` 或 `"foobar"` 或 `["foo", "bar"]` | 混合头 | | [(int, "foo"), (456, "bar")] | "baz" | `[123, "foobaz"]` 或 `[456, "foobaz"]` 或 `[456, "barbaz"]` | 对头 | 对于无前缀的类型头,此时会将传入的值尝试转为 BasePattern,例如 `int` 会转为 `nepattern.INTEGER`。如此该命令头会匹配对应的类型, 例如 `int` 会匹配 `123` 或 `"456"`,但不会匹配 `"foo"`。解析后,Alconna 会将命令头匹配到的值转为对应的类型,例如 `int` 会将 `"123"` 转为 `123`。 :::tip **正则内容只在命令名上生效,前缀中的正则会被转义** ::: 除了通过传入 `re:xxx` 来使用正则表达式外,Alconna 还提供了一种更加简洁的方式来使用正则表达式,称为 Bracket Header: ```python from alconna import Alconna alc = Alconna(".rd{roll:int}") assert alc.parse(".rd123").header["roll"] == 123 ``` Bracket Header 类似 python 里的 f-string 写法,通过 `"{}"` 声明匹配类型 `"{}"` 中的内容为 "name:type or pat": - `"{}"`, `"{:}"` ⇔ `"(.+)"`, 占位符 - `"{foo}"` ⇔ `"(?P<foo>.+)"` - `"{:\d+}"` ⇔ `"(\d+)"` - `"{foo:int}"` ⇔ `"(?P<foo>\d+)"`,其中 `"int"` 部分若能转为 `BasePattern` 则读取里面的表达式 ## 参数声明(Args) `Args` 是用于声明命令参数的组件, 可以通过以下几种方式构造 **Args** : - `Args[key, var, default][key1, var1, default1][...]` - `Args[(key, var, default)]` - `Args.key[var, default]` 其中,key **一定**是字符串,而 var 一般为参数的类型,default 为具体的值或者 **arclet.alconna.args.Field** 其与函数签名类似,但是允许含有默认值的参数在前;同时支持 keyword-only 参数不依照构造顺序传入 (但是仍需要在非 keyword-only 参数之后)。 ### key `key` 的作用是用以标记解析出来的参数并存放于 **Arparma** 中,以方便用户调用。 其有三种为 Args 注解的标识符: `?`、`/`、 `!`, 标识符与 key 之间建议以 `;` 分隔: - `!` 标识符表示该处传入的参数应**不是**规定的类型,或**不在**指定的值中。 - `?` 标识符表示该参数为**可选**参数,会在无参数匹配时跳过。 - `/` 标识符表示该参数的类型注解需要隐藏。 另外,对于参数的注释也可以标记在 `key` 中,其与 key 或者标识符 以 `#` 分割: `foo#这是注释;?` 或 `foo?#这是注释` :::tip `Args` 中的 `key` 在实际命令中并不需要传入(keyword 参数除外): ```python from arclet.alconna import Alconna, Args alc = Alconna("test", Args["foo", str]) alc.parse("test --foo abc") # 错误 alc.parse("test abc") # 正确 ``` 若需要 `test --foo abc`,你应该使用 `Option`: ```python from arclet.alconna import Alconna, Args, Option alc = Alconna("test", Option("--foo", Args["foo", str])) ``` ::: ### var var 负责命令参数的**类型检查**与**类型转化** `Args` 的`var`表面上看需要传入一个 `type`,但实际上它需要的是一个 `nepattern.BasePattern` 的实例: ```python from arclet.alconna import Args from nepattern import BasePattern # 表示 foo 参数需要匹配一个 @number 样式的字符串 args = Args["foo", BasePattern("@\d+")] ``` `pip` 示例中可以传入 `str` 是因为 `str` 已经注册在了 `nepattern.global_patterns` 中,因此会替换为 `nepattern.global_patterns[str]` `nepattern.global_patterns`默认支持的类型有: - `str`: 匹配任意字符串 - `int`: 匹配整数 - `float`: 匹配浮点数 - `bool`: 匹配 `True` 与 `False` 以及他们小写形式 - `hex`: 匹配 `0x` 开头的十六进制字符串 - `url`: 匹配网址 - `email`: 匹配 `xxxx@xxx` 的字符串 - `ipv4`: 匹配 `xxx.xxx.xxx.xxx` 的字符串 - `list`: 匹配类似 `["foo","bar","baz"]` 的字符串 - `dict`: 匹配类似 `{"foo":"bar","baz":"qux"}` 的字符串 - `datetime`: 传入一个 `datetime` 支持的格式字符串,或时间戳 - `Any`: 匹配任意类型 - `AnyString`: 匹配任意类型,转为 `str` - `Number`: 匹配 `int` 与 `float`,转为 `int` 同时可以使用 typing 中的类型: - `Literal[X]`: 匹配其中的任意一个值 - `Union[X, Y]`: 匹配其中的任意一个类型 - `Optional[xxx]`: 会自动将默认值设为 `None`,并在解析失败时使用默认值 - `List[X]`: 匹配一个列表,其中的元素为 `X` 类型 - `Dict[X, Y]`: 匹配一个字典,其中的 key 为 `X` 类型,value 为 `Y` 类型 - ... :::tip 几类特殊的传入标记: - `"foo"`: 匹配字符串 "foo" (若没有某个 `BasePattern` 与之关联) - `RawStr("foo")`: 匹配字符串 "foo" (即使有 `BasePattern` 与之关联也不会被替换) - `"foo|bar|baz"`: 匹配 "foo" 或 "bar" 或 "baz" - `[foo, bar, Baz, ...]`: 匹配其中的任意一个值或类型 - `Callable[[X], Y]`: 匹配一个参数为 `X` 类型的值,并返回通过该函数调用得到的 `Y` 类型的值 - `"re:xxx"`: 匹配一个正则表达式 `xxx`,会返回 Match[0] - `"rep:xxx"`: 匹配一个正则表达式 `xxx`,会返回 `re.Match` 对象 - `{foo: bar, baz: qux}`: 匹配字典中的任意一个键, 并返回对应的值 (特殊的键 ... 会匹配任意的值) - ... **特别的**,你可以不传入 `var`,此时会使用 `key` 作为 `var`, 匹配 `key` 字符串。 ::: #### MultiVar 与 KeyWordVar `MultiVar` 是一个特殊的标注,用于告知解析器该参数可以接受多个值,类似于函数中的 `*args`,其构造方法形如 `MultiVar(str)`。 同样的还有 `KeyWordVar`,类似于函数中的 `*, name: type`,其构造方法形如 `KeyWordVar(str)`,用于告知解析器该参数为一个 keyword-only 参数。 :::tip `MultiVar` 与 `KeyWordVar` 组合时,代表该参数为一个可接受多个 key-value 的参数,类似于函数中的 `**kwargs`,其构造方法形如 `MultiVar(KeyWordVar(str))` `MultiVar` 与 `KeyWordVar` 也可以传入 `default` 参数,用于指定默认值 `MultiVar` 不能在 `KeyWordVar` 之后传入 ::: ### default `default` 传入的是该参数的默认值或者 `Field`,以携带对于该参数的更多信息。 默认情况下 (即不声明) `default` 的值为特殊值 `Empty`。这也意味着你可以将默认值设置为 `None` 表示默认值为空值。 `Field` 构造需要的参数说明如下: - default: 参数单元的默认值 - alias: 参数单元默认值的别名 - completion: 参数单元的补全说明生成函数 - unmatch_tips: 参数单元的错误提示生成函数,其接收一个表示匹配失败的元素的参数 - missing_tips: 参数单元的缺失提示生成函数 ## 选项与子命令(Option & Subcommand) `Option` 和 `Subcommand` 可以传入一组 `alias`,如 `Option("--foo|-F|--FOO|-f")`,`Subcommand("foo", alias=["F"])` 传入别名后,选项与子命令会选择其中长度最长的作为其名称。若传入为 "--foo|-f",则命令名称为 "--foo" :::tip 特别提醒!!! Option 的名字或别名**没有要求**必须在前面写上 `-` Option 与 Subcommand 的唯一区别在于 Subcommand 可以传入自己的 **Option** 与 **Subcommand** ::: 他们拥有如下共同参数: - `help_text`: 传入该组件的帮助信息 - `dest`: 被指定为解析完成时标注匹配结果的标识符,不传入时默认为选项或子命令的名称 (name) - `requires`: 一段指定顺序的字符串列表,作为唯一的前置序列与命令嵌套替换 对于命令 `test foo bar baz qux ` 来讲,因为`foo bar baz` 仅需要判断是否相等, 所以可以这么编写: ```python Alconna("test", Option("qux", Args.a[int], requires=["foo", "bar", "baz"])) ``` - `default`: 默认值,在该组件未被解析时使用使用该值替换。 特别的,使用 `OptionResult` 或 `SubcomanndResult` 可以设置包括参数字典在内的默认值: ```python from arclet.alconna import Option, OptionResult opt1 = Option("--foo", default=False) opt2 = Option("--foo", default=OptionResult(value=False, args={"bar": 1})) ``` ### Action `Option` 可以特别设置传入一类 `Action`,作为解析操作 `Action` 分为三类: - `store`: 无 Args 时, 仅存储一个值, 默认为 Ellipsis; 有 Args 时, 后续的解析结果会覆盖之前的值 - `append`: 无 Args 时, 将多个值存为列表, 默认为 Ellipsis; 有 Args 时, 每个解析结果会追加到列表中, 当存在默认值并且不为列表时, 会自动将默认值变成列表, 以保证追加的正确性 - `count`: 无 Args 时, 计数器加一; 有 Args 时, 表现与 STORE 相同, 当存在默认值并且不为数字时, 会自动将默认值变成 1, 以保证计数器的正确性。 `Alconna` 提供了预制的几类 `Action`: - `store`(默认),`store_value`,`store_true`,`store_false` - `append`,`append_value` - `count` ## 解析结果(Arparma) `Alconna.parse` 会返回由 **Arparma** 承载的解析结果 `Arparma` 有如下属性: - 调试类 - matched: 是否匹配成功 - error_data: 解析失败时剩余的数据 - error_info: 解析失败时的异常内容 - origin: 原始命令,可以类型标注 - 分析类 - header_match: 命令头部的解析结果,包括原始头部、解析后头部、解析结果与可能的正则匹配组 - main_args: 命令的主参数的解析结果 - options: 命令所有选项的解析结果 - subcommands: 命令所有子命令的解析结果 - 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` 键对应的值 ... ## 元数据(CommandMeta) `Alconna` 的元数据相当于其配置,拥有以下条目: - `description`: 命令的描述 - `usage`: 命令的用法 - `example`: 命令的使用样例 - `author`: 命令的作者 - `fuzzy_match`: 命令是否开启模糊匹配 - `fuzzy_threshold`: 模糊匹配阈值 - `raise_exception`: 命令是否抛出异常 - `hide`: 命令是否对 manager 隐藏 - `hide_shortcut`: 命令的快捷指令是否在 help 信息中隐藏 - `keep_crlf`: 命令解析时是否保留换行字符 - `compact`: 命令是否允许第一个参数紧随头部 - `strict`: 命令是否严格匹配,若为 False 则未知参数将作为名为 $extra 的参数 - `context_style`: 命令上下文插值的风格,None 为关闭,bracket 为 `{...}`,parentheses 为 `$(...)` - `extra`: 命令的自定义额外信息 元数据一定使用 `meta=...` 形式传入: ```python from arclet.alconna import Alconna, CommandMeta alc = Alconna(..., meta=CommandMeta("foo", example="bar")) ``` ## 命名空间配置 命名空间配置 (以下简称命名空间) 相当于 `Alconna` 的默认配置,其优先度低于 `CommandMeta`。 `Alconna` 默认使用 "Alconna" 命名空间。 命名空间有以下几个属性: - name: 命名空间名称 - prefixes: 默认前缀配置 - separators: 默认分隔符配置 - formatter_type: 默认格式化器类型 - fuzzy_match: 默认是否开启模糊匹配 - raise_exception: 默认是否抛出异常 - builtin_option_name: 默认的内置选项名称(--help, --shortcut, --comp) - disable_builtin_options: 默认禁用的内置选项(--help, --shortcut, --comp) - enable_message_cache: 默认是否启用消息缓存 - compact: 默认是否开启紧凑模式 - strict: 命令是否严格匹配 - context_style: 命令上下文插值的风格 - ... ### 新建命名空间并替换 ```python from arclet.alconna import Alconna, namespace, Namespace, Subcommand, Args, config ns = Namespace("foo", prefixes=["/"]) # 创建 "foo"命名空间配置, 它要求创建的Alconna的主命令前缀必须是/ alc = Alconna("pip", Subcommand("install", Args["package", str]), namespace=ns) # 在创建Alconna时候传入命名空间以替换默认命名空间 # 可以通过with方式创建命名空间 with namespace("bar") as np1: np1.prefixes = ["!"] # 以上下文管理器方式配置命名空间,此时配置会自动注入上下文内创建的命令 np1.formatter_type = ShellTextFormatter # 设置此命名空间下的命令的 formatter 默认为 ShellTextFormatter np1.builtin_option_name["help"] = {"帮助", "-h"} # 设置此命名空间下的命令的帮助选项名称 # 你还可以使用config来管理所有命名空间并切换至任意命名空间 config.namespaces["foo"] = ns # 将命名空间挂载到 config 上 alc = Alconna("pip", Subcommand("install", Args["package", str]), namespace=config.namespaces["foo"]) # 也是同样可以切换到"foo"命名空间 ``` ### 修改默认的命名空间 ```python from arclet.alconna import config, namespace, Namespace config.default_namespace.prefixes = [...] # 直接修改默认配置 np = Namespace("xxx", prefixes=[...]) config.default_namespace = np # 更换默认的命名空间 with namespace(config.default_namespace.name) as np: np.prefixes = [...] ``` ## 快捷指令 快捷命令可以做到标识一段命令, 并且传递参数给原命令 一般情况下你可以通过 `Alconna.shortcut` 进行快捷指令操作 (创建,删除) `shortcut` 的第一个参数为快捷指令名称,第二个参数为 `ShortcutArgs`,作为快捷指令的配置: ```python class ShortcutArgs(TypedDict): """快捷指令参数""" command: NotRequired[str] """快捷指令的命令""" args: NotRequired[list[Any]] """快捷指令的附带参数""" fuzzy: NotRequired[bool] """是否允许命令后随参数""" prefix: NotRequired[bool] """是否调用时保留指令前缀""" wrapper: NotRequired[ShortcutRegWrapper] """快捷指令的正则匹配结果的额外处理函数""" humanized: NotRequired[str] """快捷指令的人类可读描述""" ``` ### args的使用 ```python from arclet.alconna import Alconna, Args alc = Alconna("setu", Args["count", int]) alc.shortcut("涩图(\d+)张", {"args": ["{0}"]}) # 'Alconna::setu 的快捷指令: "涩图(\\d+)张" 添加成功' alc.parse("涩图3张").query("count") # 3 ``` ### command的使用 ```python from arclet.alconna import Alconna, Args alc = Alconna("eval", Args["content", str]) alc.shortcut("echo", {"command": "eval print(\\'{*}\\')"}) # 'Alconna::eval 的快捷指令: "echo" 添加成功' alc.shortcut("echo", delete=True) # 删除快捷指令 # 'Alconna::eval 的快捷指令: "echo" 删除成功' @alc.bind() # 绑定一个命令执行器, 若匹配成功则会传入参数, 自动执行命令执行器 def cb(content: str): eval(content, {}, {}) alc.parse('eval print(\\"hello world\\")') # hello world alc.parse("echo hello world!") # hello world! ``` 当 `fuzzy` 为 False 时,第一个例子中传入 `"涩图1张 abc"` 之类的快捷指令将视为解析失败 快捷指令允许三类特殊的 placeholder: - `{%X}`: 如 `setu {%0}`,表示此处填入快捷指令后随的第 X 个参数。 例如,若快捷指令为 `涩图`, 配置为 `{"command": "setu {%0}"}`, 则指令 `涩图 1` 相当于 `setu 1` - `{*}`: 表示此处填入所有后随参数,并且可以通过 `{*X}` 的方式指定组合参数之间的分隔符。 - `{X}`: 表示此处填入可能的正则匹配的组: - 若 `command` 中存在匹配组 `(xxx)`,则 `{X}` 表示第 X 个匹配组的内容 - 若 `command` 中存储匹配组 `(?P...)`, 则 `{X}` 表示 **名字** 为 X 的匹配结果 除此之外, 通过 **Alconna** 内置选项 `--shortcut` 可以动态操作快捷指令 例如: - `cmd --shortcut ` 来增加一个快捷指令 - `cmd --shortcut list` 来列出当前指令的所有快捷指令 - `cmd --shortcut delete key` 来删除一个快捷指令 ```python from arclet.alconna import Alconna, Args alc = Alconna("eval", Args["content", str]) alc.shortcut("echo", {"command": "eval print(\\'{*}\\')"}) alc.parse("eval --shortcut list") # 'echo' ``` ## 紧凑命令 `Alconna`, `Option` 与 `Subcommand` 可以设置 `compact=True` 使得解析命令时允许名称与后随参数之间没有分隔: ```python from arclet.alconna import Alconna, Option, CommandMeta, Args alc = Alconna("test", Args["foo", int], Option("BAR", Args["baz", str], compact=True), meta=CommandMeta(compact=True)) assert alc.parse("test123 BARabc").matched ``` 这使得我们可以实现如下命令: ```python from arclet.alconna import Alconna, Option, Args, append alc = Alconna("gcc", Option("--flag|-F", Args["content", str], action=append, compact=True)) print(alc.parse("gcc -Fabc -Fdef -Fxyz").query[list]("flag.content")) # ['abc', 'def', 'xyz'] ``` 当 `Option` 的 `action` 为 `count` 时,其自动支持 `compact` 特性: ```python from arclet.alconna import Alconna, Option, count alc = Alconna("pp", Option("--verbose|-v", action=count, default=0)) print(alc.parse("pp -vvv").query[int]("verbose.value")) # 3 ``` ## 模糊匹配 模糊匹配会应用在任意需要进行名称判断的地方,如 **命令名称**,**选项名称** 和 **参数名称** (如指定需要传入参数名称)。 ```python from arclet.alconna import Alconna, CommandMeta alc = Alconna("test_fuzzy", meta=CommandMeta(fuzzy_match=True)) alc.parse("test_fuzy") # test_fuzy is not matched. Do you mean "test_fuzzy"? ``` ## 半自动补全 半自动补全为用户提供了推荐后续输入的功能 补全默认通过 `--comp` 或 `-cp` 或 `?` 触发:(命名空间配置可修改名称) ```python from arclet.alconna import Alconna, Args, Option alc = Alconna("test", Args["abc", int]) + Option("foo") + Option("bar") alc.parse("test --comp") ''' output 以下是建议的输入: * * --help * -h * -sct * --shortcut * foo * bar ''' ``` ## Duplication **Duplication** 用来提供更好的自动补全,类似于 **ArgParse** 的 **Namespace** 普通情况下使用,需要利用到 **ArgsStub**、**OptionStub** 和 **SubcommandStub** 三个部分 以pip为例,其对应的 Duplication 应如下构造: ```python from arclet.alconna import Alconna, Args, Option, OptionResult, Duplication, SubcommandStub, Subcommand, count class MyDup(Duplication): verbose: OptionResult install: SubcommandStub alc = Alconna( "pip", Subcommand( "install", Args["package", str], Option("-r|--requirement", Args["file", str]), Option("-i|--index-url", Args["url", str]), ), Option("-v|--version"), Option("-v|--verbose", action=count), ) res = alc.parse("pip -v install ...") # 不使用duplication获得的提示较少 print(res.query("install")) # (value=Ellipsis args={'package': '...'} options={} subcommands={}) result = alc.parse("pip -v install ...", duplication=MyDup) print(result.install) # SubcommandStub(_origin=Subcommand('install', args=Args('package': str)), _value=Ellipsis, available=True, args=ArgsStub(_origin=Args('package': str), _value={'package': '...'}, available=True), dest='install', options=[OptionStub(_origin=Option('requirement', args=Args('file': str)), _value=None, available=False, args=ArgsStub(_origin=Args('file': str), _value={}, available=False), dest='requirement', aliases=['r', 'requirement'], name='requirement'), OptionStub(_origin=Option('index-url', args=Args('url': str)), _value=None, available=False, args=ArgsStub(_origin=Args('url': str), _value={}, available=False), dest='index-url', aliases=['index-url', 'i'], name='index-url')], subcommands=[], name='install') ``` **Duplication** 也可以如 **Namespace** 一样直接标明参数名称和类型: ```python from typing import Optional from arclet.alconna import Duplication class MyDup(Duplication): package: str file: Optional[str] = None url: Optional[str] = None ``` ## 上下文插值 当 `context_style` 条目被设置后,传入的命令中符合上下文插值的字段会被自动替换成当前上下文中的信息。 上下文可以在 `parse` 中传入: ```python from arclet.alconna import Alconna, Args, CommandMeta alc = Alconna("test", Args["foo", int], meta=CommandMeta(context_style="parentheses")) alc.parse("test $(bar)", {"bar": 123}) # {"foo": 123} ``` context_style 的值分两种: - `"bracket"`: 插值格式为 `{...}`,例如 `{foo}` - `"parentheses"`: 插值格式为 `$(...)`,例如 `$(bar)` ================================================ FILE: website/versioned_docs/version-2.4.2/best-practice/alconna/config.md ================================================ --- sidebar_position: 4 description: 配置项 --- # 配置项 ## alconna_auto_send_output - **类型**: `bool` - **默认值**: `False` 是否全局启用输出信息自动发送,不启用则会在触发特殊内置选项后仍然将解析结果传递至响应器。 ## alconna_use_command_start - **类型**: `bool` - **默认值**: `False` 是否读取 Nonebot 的配置项 `COMMAND_START` 来作为全局的 Alconna 命令前缀 ## alconna_auto_completion - **类型**: `bool` - **默认值**: `False` 是否全局启用命令自动补全,启用后会在参数缺失或触发 `--comp` 选项时自自动启用交互式补全。 ## alconna_use_origin - **类型**: `bool` - **默认值**: `False` 是否全局使用原始消息 (即未经过 to_me 等处理的),该选项会影响到 Alconna 的匹配行为。 ## alconna_use_command_sep - **类型**: `bool` - **默认值**: `False` 是否读取 Nonebot 的配置项 `COMMAND_SEP` 来作为全局的 Alconna 命令分隔符。 ## alconna_global_extensions - **类型**: `List[str]` - **默认值**: `[]` 全局加载的扩展,路径以 . 分隔,如 `foo.bar.baz:DemoExtension`。 ## alconna_context_style - **类型**: `Optional[Literal["bracket", "parentheses"]]` - **默认值**: `None` 全局命令上下文插值的风格,None 为关闭,bracket 为 `{...}`,parentheses 为 `$(...)`。 ## alconna_enable_saa_patch - **类型**: `bool` - **默认值**: `False` 是否启用 SAA 补丁。 ## alconna_apply_filehost - **类型**: `bool` - **默认值**: `False` 是否启用文件托管。 ## alconna_apply_fetch_targets - **类型**: `bool` - **默认值**: `False` 是否启动时拉取一次发送对象列表。 ================================================ FILE: website/versioned_docs/version-2.4.2/best-practice/alconna/matcher.mdx ================================================ --- 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 ") .option("writer", "--anonymous", {"id": 0}) .usage("book [-w | --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 ") .option("writer", "--anonymous", {"id": 0}) .usage("book [-w | --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("添加成功") ``` 此时,当用户输入 `添加教师` 时,会自动提示用户输入姓名,手机号和教师号,用户输入后会自动进入下一个提示: 补全会话配置如下: ```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 指令。 ================================================ FILE: website/versioned_docs/version-2.4.2/best-practice/alconna/uniseg.mdx ================================================ --- 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`: 通过提供的 `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) ... ``` 注意,`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) ``` 不仅如此,你还可以通过 `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 与消息发送对象的方法: 通过提供的 `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): ... ``` ```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) ``` `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 的处理方法。 ================================================ FILE: website/versioned_docs/version-2.4.2/best-practice/data-storing.md ================================================ --- sidebar_position: 1 description: 存储数据文件到本地 --- # 数据存储 在使用插件的过程中,难免会需要存储一些持久化数据,例如用户的个人信息、群组的信息等。除了使用数据库等第三方存储之外,还可以使用本地文件来自行管理数据。NoneBot 提供了 `nonebot-plugin-localstore` 插件,可用于获取正确的数据存储路径并写入数据。 ## 安装插件 在使用前请先安装 `nonebot-plugin-localstore` 插件至项目环境中,可参考[获取商店插件](../tutorial/store.mdx#安装插件)来了解并选择安装插件的方式。如: 在**项目目录**下执行以下命令: ```bash nb plugin install nonebot-plugin-localstore ``` ## 使用插件 `nonebot-plugin-localstore` 插件兼容 Windows、Linux 和 macOS 等操作系统,使用时无需关心操作系统的差异。同时插件提供 `nb-cli` 脚本,可以使用 `nb localstore` 命令来检查数据存储路径。 在使用本插件前同样需要使用 `require` 方法进行**加载**并**导入**需要使用的方法,可参考 [跨插件访问](../advanced/requiring.md) 一节进行了解,如: ```python from nonebot import require require("nonebot_plugin_localstore") import nonebot_plugin_localstore as store # 获取插件缓存目录 cache_dir = store.get_plugin_cache_dir() # 获取插件缓存文件 cache_file = store.get_plugin_cache_file("file_name") # 获取插件数据目录 data_dir = store.get_plugin_data_dir() # 获取插件数据文件 data_file = store.get_plugin_data_file("file_name") # 获取插件配置目录 config_dir = store.get_plugin_config_dir() # 获取插件配置文件 config_file = store.get_plugin_config_file("file_name") ``` :::danger 警告 在 Windows 和 macOS 系统下,插件的数据目录和配置目录是同一个目录,因此在使用时需要注意避免文件名冲突。 ::: 插件提供的方法均返回一个 `pathlib.Path` 路径,可以参考 [pathlib 文档](https://docs.python.org/zh-cn/3/library/pathlib.html)来了解如何使用。常用的方法有: ```python from pathlib import Path data_file = store.get_plugin_data_file("file_name") # 写入文件内容 data_file.write_text("Hello World!") # 读取文件内容 data = data_file.read_text() ``` :::note 提示 对于嵌套插件,子插件的存储目录将位于父插件存储目录下。 ::: ## 配置项 ### localstore_use_cwd 使用当前工作目录作为数据存储目录,以下数据目录配置项默认值将会对应变更 默认值:`False` ```dotenv LOCALSTORE_USE_CWD=true ``` ### localstore_cache_dir 自定义缓存目录 默认值: 当 `localstore_use_cwd` 为 `True` 时,缓存目录为 `/cache`,否则: - macOS: `~/Library/Caches/nonebot2` - Unix: `~/.cache/nonebot2` (XDG default) - Windows: `C:\Users\\AppData\Local\nonebot2\Cache` ```dotenv LOCALSTORE_CACHE_DIR=/tmp/cache ``` ### localstore_data_dir 自定义数据目录 默认值: 当 `localstore_use_cwd` 为 `True` 时,数据目录为 `/data`,否则: - macOS: `~/Library/Application Support/nonebot2` - Unix: `~/.local/share/nonebot2` or in $XDG_DATA_HOME, if defined - Win XP (not roaming): `C:\Documents and Settings\\Application Data\nonebot2` - Win 7 (not roaming): `C:\Users\\AppData\Local\nonebot2` ```dotenv LOCALSTORE_DATA_DIR=/tmp/data ``` ### localstore_config_dir 自定义配置目录 默认值: 当 `localstore_use_cwd` 为 `True` 时,配置目录为 `/config`,否则: - macOS: same as user_data_dir - Unix: `~/.config/nonebot2` - Win XP (roaming): `C:\Documents and Settings\\Local Settings\Application Data\nonebot2` - Win 7 (roaming): `C:\Users\\AppData\Roaming\nonebot2` ```dotenv LOCALSTORE_CONFIG_DIR=/tmp/config ``` ### localstore_plugin_cache_dir 自定义插件缓存目录 默认值:`{}` ```dotenv LOCALSTORE_PLUGIN_CACHE_DIR=' { "plugin_id": "/tmp/plugin_cache" } ' ``` ### localstore_plugin_data_dir 自定义插件数据目录 默认值:`{}` ```dotenv LOCALSTORE_PLUGIN_DATA_DIR=' { "plugin_id": "/tmp/plugin_data" } ' ``` ### localstore_plugin_config_dir 自定义插件配置目录 默认值:`{}` ```dotenv LOCALSTORE_PLUGIN_CONFIG_DIR=' { "plugin_id": "/tmp/plugin_config" } ' ``` ================================================ FILE: website/versioned_docs/version-2.4.2/best-practice/database/README.mdx ================================================ import TabItem from "@theme/TabItem"; import Tabs from "@theme/Tabs"; # 数据库 [`nonebot-plugin-orm`](https://github.com/nonebot/plugin-orm) 是 NoneBot 的数据库支持插件。 本插件基于 [SQLAlchemy](https://www.sqlalchemy.org/) 和 [Alembic](https://alembic.sqlalchemy.org/),提供了许多与 NoneBot 紧密集成的功能: - 多 Engine / Connection 支持 - Session 管理 - 关系模型管理、依赖注入支持 - 数据库迁移 ## 安装 ```shell nb plugin install nonebot-plugin-orm ``` ```shell pip install nonebot-plugin-orm ``` ```shell pdm add nonebot-plugin-orm ``` ## 数据库驱动和后端 本插件只提供了 ORM 功能,没有数据库后端,也没有直接连接数据库后端的能力。 所以你需要另行安装数据库驱动和数据库后端,并且配置数据库连接信息。 ### SQLite [SQLite](https://www.sqlite.org/) 是一个轻量级的嵌入式数据库,它的数据以单文件的形式存储在本地,不需要单独的数据库后端。 SQLite 非常适合用于开发环境和小型应用,但是不适合用于大型应用的生产环境。 虽然不需要另行安装数据库后端,但你仍然需要安装数据库驱动: ```shell pip install "nonebot-plugin-orm[sqlite]" ``` ```shell pdm add "nonebot-plugin-orm[sqlite]" ``` 默认情况下,数据库文件为 `/nonebot-plugin-orm/db.sqlite3`(数据目录由 [nonebot-plugin-localstore](../data-storing) 提供)。 或者,你可以通过配置 `SQLALCHEMY_DATABASE_URL` 来指定数据库文件路径: ```shell SQLALCHEMY_DATABASE_URL=sqlite+aiosqlite:///file_path ``` ### PostgreSQL [PostgreSQL](https://www.postgresql.org/) 是世界上最先进的开源关系数据库之一,对各种高级且广泛应用的功能有最好的支持,是中小型应用的首选数据库。 ```shell pip install nonebot-plugin-orm[postgresql] ``` ```shell pdm add nonebot-plugin-orm[postgresql] ``` ```shell SQLALCHEMY_DATABASE_URL=postgresql+psycopg://user:password@host:port/dbname[?key=value&key=value...] ``` ### MySQL / MariaDB [MySQL](https://www.mysql.com/) 和 [MariaDB](https://mariadb.com/) 是经典的开源关系数据库,适合用于中小型应用。 ```shell pip install nonebot-plugin-orm[mysql] ``` ```shell pdm add nonebot-plugin-orm[mysql] ``` ```shell SQLALCHEMY_DATABASE_URL=mysql+aiomysql://user:password@host:port/dbname[?key=value&key=value...] ``` ## 使用 本插件提供了数据库迁移功能(此功能依赖于 [nb-cli 脚手架](../../quick-start#安装脚手架))。 在安装了新的插件或机器人之后,你需要执行一次数据库迁移操作,将数据库同步至与机器人一致的状态: ```shell nb orm upgrade ``` 运行完毕后,可以检查一下: ```shell nb orm check ``` 如果输出是 `没有检测到新的升级操作`,那么恭喜你,数据库已经迁移完成了,你可以启动机器人了。 ================================================ FILE: website/versioned_docs/version-2.4.2/best-practice/database/_category_.json ================================================ { "label": "数据库", "position": 7 } ================================================ FILE: website/versioned_docs/version-2.4.2/best-practice/database/developer/README.md ================================================ # 开发者指南 开发者指南内容较多,故分为了一个示例以及数个专题。 阅读(并且最好跟随实践)示例后,你将会对使用 `nonebot-plugin-orm` 开发插件有一个基本的认识。 如果想要更深入地学习关于 [SQLAlchemy](https://www.sqlalchemy.org/) 和 [Alembic](https://alembic.sqlalchemy.org/) 的知识,或者在使用过程中遇到了问题,可以查阅专题以及其官方文档。 ## 示例 ### 模型定义 首先,我们需要设计存储的数据的结构。 例如天气插件,需要存储**什么地方 (`location`)** 的**天气是什么 (`weather`)**。 其中,一个地方只会有一种天气,而不同地方可能有相同的天气。 所以,我们可以设计出如下的模型: ```python title=weather/__init__.py showLineNumbers from nonebot_plugin_orm import Model from sqlalchemy.orm import Mapped, mapped_column class Weather(Model): location: Mapped[str] = mapped_column(primary_key=True) weather: Mapped[str] ``` 其中,`primary_key=True` 意味着此列 (`location`) 是主键,即内容是唯一的且非空的。 每一个模型必须有至少一个主键。 我们可以用以下代码检查模型生成的数据库模式是否正确: ```python from sqlalchemy.schema import CreateTable print(CreateTable(Weather.__table__)) ``` ```sql CREATE TABLE weather_weather ( location VARCHAR NOT NULL, weather VARCHAR NOT NULL, CONSTRAINT pk_weather_weather PRIMARY KEY (location) ) ``` 可以注意到表名是 `weather_weather` 而不是 `Weather` 或者 `weather`。 这是因为 `nonebot-plugin-orm` 会自动为模型生成一个表名,规则是:`<插件模块名>_<类名小写>`。 你也可以通过指定 `__tablename__` 属性来自定义表名: ```python {2} class Weather(Model): __tablename__ = "weather" ... ``` ```sql {1} CREATE TABLE weather ( ... ) ``` 但是,并不推荐你这么做,因为这可能会导致不同插件间的表名重复,引发冲突。 特别是当你会发布插件时,你并不知道其他插件会不会使用相同的表名。 ### 首次迁移 我们成功定义了模型,现在启动机器人试试吧: ```shell $ nb run 01-02 15:04:05 [SUCCESS] nonebot | NoneBot is initializing... 01-02 15:04:05 [ERROR] nonebot_plugin_orm | 启动检查失败 01-02 15:04:05 [ERROR] nonebot | Application startup failed. Exiting. Traceback (most recent call last): ... click.exceptions.UsageError: 检测到新的升级操作: [('add_table', Table('weather', MetaData(), Column('location', String(), table=, primary_key=True, nullable=False), Column('weather', String(), table=, nullable=False), schema=None))] ``` 咦,发生了什么? `nonebot-plugin-orm` 试图阻止我们启动机器人。 原来是我们定义了模型,但是数据库中并没有对应的表,这会导致插件不能正常运行。 所以,我们需要迁移数据库。 首先,我们需要创建一个迁移脚本: ```shell nb orm revision -m "first revision" --branch-label weather ``` 其中,`-m` 参数是迁移脚本的描述,`--branch-label` 参数是迁移脚本的分支,一般为插件模块名。 执行命令过后,出现了一个 `weather/migrations` 目录,其中有一个 `xxxxxxxxxxxx_first_revision.py` 文件: ```shell {4,5} weather ├── __init__.py ├── config.py └── migrations └── xxxxxxxxxxxx_first_revision.py ``` 这就是我们创建的迁移脚本,它记录了数据库模式的变化。 我们可以查看一下它的内容: ```python title=weather/migrations/xxxxxxxxxxxx_first_revision.py {25-33,39-41} showLineNumbers """first revision 迁移 ID: xxxxxxxxxxxx 父迁移: 创建时间: 2006-01-02 15:04:05.999999 """ from __future__ import annotations from collections.abc import Sequence import sqlalchemy as sa from alembic import op revision: str = "xxxxxxxxxxxx" down_revision: str | Sequence[str] | None = None branch_labels: str | Sequence[str] | None = ("weather",) depends_on: str | Sequence[str] | None = None def upgrade(name: str = "") -> None: if name: return # ### commands auto generated by Alembic - please adjust! ### op.create_table( "weather_weather", sa.Column("location", sa.String(), nullable=False), sa.Column("weather", sa.String(), nullable=False), sa.PrimaryKeyConstraint("location", name=op.f("pk_weather_weather")), info={"bind_key": "weather"}, ) # ### end Alembic commands ### def downgrade(name: str = "") -> None: if name: return # ### commands auto generated by Alembic - please adjust! ### op.drop_table("weather_weather") # ### end Alembic commands ### ``` 可以注意到脚本的主体部分(其余是模版代码,请勿修改)是: ```python # ### commands auto generated by Alembic - please adjust! ### op.create_table( # CREATE TABLE "weather_weather", # weather_weather sa.Column("location", sa.String(), nullable=False), # location VARCHAR NOT NULL, sa.Column("weather", sa.String(), nullable=False), # weather VARCHAR NOT NULL, sa.PrimaryKeyConstraint("location", name=op.f("pk_weather_weather")), # CONSTRAINT pk_weather_weather PRIMARY KEY (location) info={"bind_key": "weather"}, ) # ### end Alembic commands ### ``` ```python # ### commands auto generated by Alembic - please adjust! ### op.drop_table("weather_weather") # DROP TABLE weather_weather; # ### end Alembic commands ### ``` 虽然我们不是很懂这些代码的意思,但是可以注意到它们几乎与 SQL 语句 (DDL) 一一对应。 显然,它们是用来创建和删除表的。 我们还可以注意到,`upgrade()` 和 `downgrade()` 函数中的代码是**互逆**的。 也就是说,执行一次 `upgrade()` 函数,再执行一次 `downgrade()` 函数后,数据库的模式就会回到原来的状态。 这就是迁移脚本的作用:记录数据库模式的变化,以便我们在不同的环境中(例如开发环境和生产环境)**可复现地**、**可逆地**同步数据库模式,正如 git 对我们的代码做的事情那样。 对了,不要忘记还有一段注释:`commands auto generated by Alembic - please adjust!`。 它在提醒我们,这些代码是由 Alembic 自动生成的,我们应该检查它们,并且根据需要进行调整。 :::caution 注意 迁移脚本冗长且繁琐,我们一般不会手写它们,而是由 Alembic 自动生成。 一般情况下,Alembic 足够智能,可以正确地生成迁移脚本。 但是,在复杂或有歧义的情况下,我们可能需要手动调整迁移脚本。 所以,**永远要检查迁移脚本,并且在开发环境中测试!** **迁移脚本中任何一处错误都足以使数据付之东流!** ::: 确定迁移脚本正确后,我们就可以执行迁移脚本,将数据库模式同步到数据库中: ```shell nb orm upgrade ``` 现在,我们可以正常启动机器人了。 开发过程中,我们可能会频繁地修改模型,这意味着我们需要频繁地创建并执行迁移脚本,非常繁琐。 实际上,此时我们不在乎数据安全,只需要数据库模式与模型定义一致即可。 所以,我们可以关闭 `nonebot-plugin-orm` 的启动检查: ```shell title=.env.dev ALEMBIC_STARTUP_CHECK=false ``` 现在,每次启动机器人时,数据库模式会自动与模型定义同步,无需手动迁移。 ### 会话管理 我们已经成功定义了模型,并且迁移了数据库,现在可以开始使用数据库了……吗? 并不能,因为模型只是数据结构的定义,并不能通过它操作数据(如果你曾经使用过 [Tortoise ORM](https://tortoise.github.io/),可能会知道 `await Weather.get(location="上海")` 这样的面向对象编程。 但是 SQLAlchemy 不同,选择了命令式编程)。 我们需要使用**会话**操作数据: ```python title=weather/__init__.py {10,13} showLineNumbers from nonebot import on_command from nonebot.adapters import Message from nonebot.params import CommandArg from nonebot_plugin_orm import async_scoped_session weather = on_command("天气") @weather.handle() async def _(session: async_scoped_session, args: Message = CommandArg()): location = args.extract_plain_text() if wea := await session.get(Weather, location): await weather.finish(f"今天{location}的天气是{wea.weather}") await weather.finish(f"未查询到{location}的天气") ``` 我们通过 `session: async_scoped_session` 依赖注入获得了一个会话,然后使用 `await session.get(Weather, location)` 查询数据库。 `async_scoped_session` 是一个有作用域限制的会话,作用域为当前事件、当前事件响应器。 会话产生的模型实例(例如此处的 `wea := await session.get(Weather, location)`)作用域与会话相同。 :::caution 注意 此处提到的“会话”指的是 ORM 会话,而非 [NoneBot 会话](../../../appendices/session-control),两者的生命周期也是不同的(NoneBot 会话的生命周期中可能包含多个事件,不同的事件也会有不同的事件响应器)。 具体而言,就是不要将 ORM 会话和模型实例存储在 NoneBot 会话状态中: ```python {12} from nonebot.params import ArgPlainText from nonebot.typing import T_State @weather.got("location", prompt="请输入地名") async def _(state: T_State, session: async_scoped_session, location: str = ArgPlainText()): wea = await session.get(Weather, location) if not wea: await weather.finish(f"未查询到{location}的天气") state["weather"] = wea # 不要这么做,除非你知道自己在做什么 ``` 当然非要这么做也不是不可以: ```python {6} @weather.handle() async def _(state: T_State, session: async_scoped_session): # 通过 await session.merge(state["weather"]) 获得了此 ORM 会话中的相应模型实例, # 而非直接使用会话状态中的模型实例, # 因为先前的 ORM 会话已经关闭了。 wea = await session.merge(state["weather"]) await weather.finish(f"今天{state['location']}的天气是{wea.weather}") ``` ::: 当有数据更改时,我们需要提交事务,也要注意会话作用域问题: ```python title=weather/__init__.py {12,20} showLineNumbers from nonebot.params import Depends async def get_weather( session: async_scoped_session, args: Message = CommandArg() ) -> Weather: location = args.extract_plain_text() if not (wea := await session.get(Weather, location)): wea = Weather(location=location, weather="未知") session.add(wea) # await session.commit() # 不应该在其他地方提交事务 return wea @weather.handle() async def _(session: async_scoped_session, wea: Weather = Depends(get_weather)): await weather.send(f"今天的天气是{wea.weather}") await session.commit() # 而应该在事件响应器结束前提交事务 ``` 当然我们也可以获得一个新的会话,不过此时就要手动管理会话了: ```python title=weather/__init__.py {5-6} showLineNumbers from nonebot_plugin_orm import get_session async def get_weather(location: str) -> str: session = get_session() async with session.begin(): wea = await session.get(Weather, location) if not wea: wea = Weather(location=location, weather="未知") session.add(wea) return wea.weather @weather.handle() async def _(args: Message = CommandArg()): wea = await get_weather(args.extract_plain_text()) await weather.send(f"今天的天气是{wea}") ``` ### 依赖注入 在上面的示例中,我们都是通过会话获得数据的。 不过,我们也可以通过依赖注入获得数据: ```python title=weather/__init__.py {12-14} showLineNumbers from sqlalchemy import select from nonebot.params import Depends from nonebot_plugin_orm import SQLDepends def extract_arg_plain_text(args: Message = CommandArg()) -> str: return args.extract_plain_text() @weather.handle() async def _( wea: Weather = SQLDepends( select(Weather).where(Weather.location == Depends(extract_arg_plain_text)) ), ): await weather.send(f"今天的天气是{wea.weather}") ``` 其中,`SQLDepends` 是一个特殊的依赖注入,它会根据类型标注和 SQL 语句提供数据,SQL 语句中也可以有子依赖。 不同的类型标注也会获得不同形式的数据: ```python title=weather/__init__.py {5} showLineNumbers from collections.abc import Sequence @weather.handle() async def _( weas: Sequence[Weather] = SQLDepends( select(Weather).where(Weather.weather == Depends(extract_arg_plain_text)) ), ): await weather.send(f"今天的天气是{weas[0].weather}的城市有{','.join(wea.location for wea in weas)}") ``` 支持的类型标注请参见 [依赖注入](dependency)。 我们也可以像 [类作为依赖](../../../advanced/dependency#类作为依赖) 那样,在类属性中声明子依赖: ```python title=weather/__init__.py {5-6,10} showLineNumbers from collections.abc import Sequence class Weather(Model): location: Mapped[str] = mapped_column(primary_key=True) weather: Mapped[str] = Depends(extract_arg_plain_text) # weather: Annotated[Mapped[str], Depends(extract_arg_plain_text)] # Annotated 支持 @weather.handle() async def _(weas: Sequence[Weather]): await weather.send( f"今天的天气是{weas[0].weather}的城市有{','.join(wea.location for wea in weas)}" ) ``` ================================================ FILE: website/versioned_docs/version-2.4.2/best-practice/database/developer/_category_.json ================================================ { "label": "开发者指南", "position": 3 } ================================================ FILE: website/versioned_docs/version-2.4.2/best-practice/database/developer/dependency.md ================================================ --- sidebar_position: 3 description: 依赖注入 --- # 依赖注入 `nonebot-plugin-orm` 提供了强大且灵活的依赖注入,可以方便地帮助你获取数据库会话和查询数据。 ## 数据库会话 ### AsyncSession 新数据库会话,常用于有独立的数据库操作逻辑的插件。 ```python {13,26} from nonebot import on_message from nonebot.params import Depends from nonebot_plugin_orm import AsyncSession, Model, async_scoped_session from sqlalchemy.orm import Mapped, mapped_column message = on_message() class Message(Model): id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) async def get_message(session: AsyncSession) -> Message: # 等价于 session = get_session() async with session: msg = Message() session.add(msg) await session.commit() await session.refresh(msg) return msg @message.handle() async def _(session: async_scoped_session, msg: Message = Depends(get_message)): await session.rollback() # 无法回退 get_message() 中的更改 await message.send(str(msg.id)) # msg 被存储,msg.id 递增 ``` ### async_scoped_session 数据库作用域会话,常用于事件响应器和有与响应逻辑相关的数据库操作逻辑的插件。 ```python {13,26} from nonebot import on_message from nonebot.params import Depends from nonebot_plugin_orm import Model, async_scoped_session from sqlalchemy.orm import Mapped, mapped_column message = on_message() class Message(Model): id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) async def get_message(session: async_scoped_session) -> Message: # 等价于 session = get_scoped_session() msg = Message() session.add(msg) await session.flush() await session.refresh(msg) return msg @message.handle() async def _(session: async_scoped_session, msg: Message = Depends(get_message)): await session.rollback() # 可以回退 get_message() 中的更改 await message.send(str(msg.id)) # msg 没有被存储,msg.id 不变 ``` ## 查询数据 ### Model 支持类作为依赖。 ```python from typing import Annotated from nonebot.params import Depends from nonebot_plugin_orm import Model from sqlalchemy.orm import Mapped, mapped_column def get_id() -> int: ... class Message(Model): id: Annotated[Mapped[int], Depends(get_id)] = mapped_column( primary_key=True, autoincrement=True ) async def _(msg: Message): # 等价于 msg = ( # await (await session.stream(select(Message).where(Message.id == get_id()))) # .scalars() # .one_or_none() # ) ... ``` ### SQLDepends 参数为一个 SQL 语句,决定依赖注入的内容,SQL 语句中可以使用子依赖。 ```python {11-13} from nonebot.params import Depends from nonebot_plugin_orm import Model, SQLDepends from sqlalchemy import select def get_id() -> int: ... async def _( model: Model = SQLDepends(select(Model).where(Model.id == Depends(get_id))), ): ... ``` 参数可以是任意 SQL 语句,但不建议使用 `select` 以外的语句,因为语句可能没有返回值(`returning` 除外),而且代码不清晰。 ### 类型标注 类型标注决定依赖注入的数据结构,主要影响以下几个层面: - 迭代器(`session.execute()`)或异步迭代器(`session.stream()`) - 标量(`session.execute().scalars()`)或元组(`session.execute()`) - 一个(`session.execute().one_or_none()`,注意 `None` 时可能触发 [重载](../../../appendices/overload#重载))或全部(`session.execute()` / `session.execute().all()`) - 连续(`session().execute()`)或分块(`session.execute().partitions()`) 具体如下(可以使用父类型作为类型标注): - ```python async def _(rows_partitions: AsyncIterator[Sequence[Tuple[Model, ...]]]): # 等价于 rows_partitions = await (await session.stream(sql).partitions()) async for partition in rows_partitions: for row in partition: print(row[0], row[1], ...) ``` - ```python async def _(model_partitions: AsyncIterator[Sequence[Model]]): # 等价于 model_partitions = await (await session.stream(sql).scalars().partitions()) async for partition in model_partitions: for model in partition: print(model) ``` - ```python async def _(row_partitions: Iterator[Sequence[Tuple[Model, ...]]]): # 等价于 row_partitions = await session.execute(sql).partitions() for partition in rows_partitions: for row in partition: print(row[0], row[1], ...) ``` - ```python async def _(model_partitions: Iterator[Sequence[Model]]): # 等价于 model_partitions = await (await session.execute(sql).scalars().partitions()) for partition in model_partitions: for model in partition: print(model) ``` - ```python async def _(rows: sa_async.AsyncResult[Tuple[Model, ...]]): # 等价于 rows = await session.stream(sql) async for row in rows: print(row[0], row[1], ...) ``` - ```python async def _(models: sa_async.AsyncScalarResult[Model]): # 等价于 models = await session.stream(sql).scalars() async for model in models: print(model) ``` - ```python async def _(rows: sa.Result[Tuple[Model, ...]]): # 等价于 rows = await session.execute(sql) for row in rows: print(row[0], row[1], ...) ``` - ```python async def _(models: sa.ScalarResult[Model]): # 等价于 models = await session.execute(sql).scalars() for model in models: print(model) ``` - ```python async def _(rows: Sequence[Tuple[Model, ...]]): # 等价于 rows = await (await session.stream(sql).all()) for row in rows: print(row[0], row[1], ...) ``` - ```python async def _(models: Sequence[Model]): # 等价于 models = await (await session.stream(sql).scalars().all()) for model in models: print(model) ``` - ```python async def _(row: Tuple[Model, ...]): # 等价于 row = await (await session.stream(sql).one_or_none()) print(row[0], row[1], ...) ``` - ```python async def _(model: Model): # 等价于 model = await (await session.stream(sql).scalars().one_or_none()) print(model) ``` ================================================ FILE: website/versioned_docs/version-2.4.2/best-practice/database/developer/test.md ================================================ --- sidebar_position: 2 description: 测试 --- # 测试 百思不如一试,测试是发现问题的最佳方式。 不同的用户会有不同的配置,为了提高项目的兼容性,我们需要在不同数据库后端上测试。 手动进行大量的、重复的测试不可靠,也不现实,因此我们推荐使用 [GitHub Actions](https://github.com/features/actions) 进行自动化测试: ```yaml title=.github/workflows/test.yml {12-42,52-53} showLineNumbers name: Test on: push: branches: - main jobs: test: runs-on: ubuntu-latest strategy: matrix: db: - sqlite+aiosqlite:///db.sqlite3 - postgresql+psycopg://postgres:postgres@localhost:5432/postgres - mysql+aiomysql://mysql:mysql@localhost:3306/mymysql fail-fast: false env: SQLALCHEMY_DATABASE_URL: ${{ matrix.db }} services: postgresql: image: ${{ startsWith(matrix.db, 'postgresql') && 'postgres' || '' }} env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: postgres ports: - 5432:5432 mysql: image: ${{ startsWith(matrix.db, 'mysql') && 'mysql' || '' }} env: MYSQL_ROOT_PASSWORD: mysql MYSQL_USER: mysql MYSQL_PASSWORD: mysql MYSQL_DATABASE: mymysql ports: - 3306:3306 steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 - name: Install dependencies run: pip install -r requirements.txt - name: Run migrations run: pipx run nb-cli orm upgrade - name: Run tests run: pytest ``` 如果项目还需要考虑跨平台和跨 Python 版本兼容,测试矩阵中还需要增加这两个维度。 但是,我们没必要在所有平台和 Python 版本上运行所有数据库的测试,因为很显然,PostgreSQL 和 MySQL 这类独立的数据库后端不会受平台和 Python 影响,而且 Github Actions 的非 Linux 平台不支持运行独立服务: | | Python 3.9 | Python 3.10 | Python 3.11 | Python 3.12 | | ----------- | ---------- | ----------- | ----------- | --------------------------- | | **Linux** | SQLite | SQLite | SQLite | SQLite / PostgreSQL / MySQL | | **Windows** | SQLite | SQLite | SQLite | SQLite | | **macOS** | SQLite | SQLite | SQLite | SQLite | ```yaml title=.github/workflows/test.yml {12-24} showLineNumbers name: Test on: push: branches: - main jobs: test: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] python-version: ["3.9", "3.10", "3.11", "3.12"] db: ["sqlite+aiosqlite:///db.sqlite3"] include: - os: ubuntu-latest python-version: "3.12" db: postgresql+psycopg://postgres:postgres@localhost:5432/postgres - os: ubuntu-latest python-version: "3.12" db: mysql+aiomysql://mysql:mysql@localhost:3306/mymysql fail-fast: false env: SQLALCHEMY_DATABASE_URL: ${{ matrix.db }} services: postgresql: image: ${{ startsWith(matrix.db, 'postgresql') && 'postgres' || '' }} env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: postgres ports: - 5432:5432 mysql: image: ${{ startsWith(matrix.db, 'mysql') && 'mysql' || '' }} env: MYSQL_ROOT_PASSWORD: mysql MYSQL_USER: mysql MYSQL_PASSWORD: mysql MYSQL_DATABASE: mymysql ports: - 3306:3306 steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: pip install -r requirements.txt - name: Run migrations run: pipx run nb-cli orm upgrade - name: Run tests run: pytest ``` ================================================ FILE: website/versioned_docs/version-2.4.2/best-practice/database/user.md ================================================ --- sidebar_position: 2 description: 用户指南 --- # 用户指南 `nonebot-plugin-orm` 功能强大且复杂,使用上有一定难度。 不过,对于用户而言,只需要掌握部分功能即可。 :::caution 注意 请注意区分插件的项目名(如:`nonebot-plugin-wordcloud`)和模块名(如:`nonebot_plugin_wordcloud`)。`nonebot-plugin-orm` 中统一使用插件模块名。参见 [插件命名规范](../../developer/plugin-publishing#插件命名规范)。 ::: ## 示例 ### 创建新机器人 我们想要创建一个机器人,并安装 `nonebot-plugin-wordcloud` 插件,只需要执行以下命令: ```shell nb init # 初始化项目文件夹 pip install nonebot-plugin-orm[sqlite] # 安装 nonebot-plugin-orm,并附带 SQLite 支持 nb plugin install nonebot-plugin-wordcloud # 安装插件 # nb orm heads # 查看有什么插件使用到了数据库(可选) nb orm upgrade # 升级数据库 # nb orm check # 检查一下数据库模式是否与模型定义一致(可选) nb run # 启动机器人 ``` ### 卸载插件 我们已经安装了 `nonebot-plugin-wordcloud` 插件,但是现在想要卸载它,并且**删除它的数据**,只需要执行以下命令: ```shell nb plugin uninstall nonebot-plugin-wordcloud # 卸载插件 # nb orm heads # 查看有什么插件使用到了数据库。(可选) nb orm downgrade nonebot_plugin_wordcloud@base # 降级数据库,删除数据 # nb orm check # 检查一下数据库模式是否与模型定义一致(可选) ``` ## CLI 接下来,让我们了解下示例中出现的 CLI 命令的含义: ### heads 显示所有的分支头。一般一个分支对应一个插件。 ```shell nb orm heads ``` 输出格式为 `<迁移 ID> (<插件模块名>) (<头部类型>)`: ``` 46327b837dd8 (nonebot_plugin_chatrecorder) (head) 9492159f98f7 (nonebot_plugin_user) (head) 71a72119935f (nonebot_plugin_session_orm) (effective head) ade8cdca5470 (nonebot_plugin_wordcloud) (head) ``` ### upgrade 升级数据库。每次安装新的插件或更新插件版本后,都需要执行此命令。 ```shell nb orm upgrade <插件模块名>@<迁移 ID> ``` 其中,`<插件模块名>@<迁移 ID>` 是可选参数。如果不指定,则会将所有分支升级到最新版本,这也是最常见的用法: ```shell nb orm upgrade ``` ### downgrade 降级数据库。当需要回滚插件版本或删除插件时,可以执行此命令。 ```shell nb orm downgrade <插件模块名>@<迁移 ID> ``` 其中,`<迁移 ID>` 也可以是 `base`,即回滚到初始状态。常用于卸载插件后删除其数据: ```shell nb orm downgrade <插件模块名>@base ``` ### check 检查数据库模式是否与模型定义一致。机器人启动前会自动运行此命令(`ALEMBIC_STARTUP_CHECK=true` 时),并在检查失败时阻止启动。 ```shell nb orm check ``` ## 配置 ### sqlalchemy_database_url 默认数据库连接 URL。参见 [数据库驱动和后端](.#数据库驱动和后端) 和 [引擎配置 — SQLAlchemy 2.0 文档](https://docs.sqlalchemy.org/en/20/core/engines.html#database-urls)。 ```shell SQLALCHEMY_DATABASE_URL=dialect+driver://username:password@host:port/database ``` ### sqlalchemy_bind bind keys(一般为插件模块名)到数据库连接 URL、[`create_async_engine()`](https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html#sqlalchemy.ext.asyncio.create_async_engine) 参数字典或 [`AsyncEngine`](https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html#sqlalchemy.ext.asyncio.AsyncEngine) 实例的字典。 例如,我们想要让 `nonebot-plugin-wordcloud` 插件使用一个 SQLite 数据库,并开启 [Echo 选项](https://docs.sqlalchemy.org/en/20/core/engines.html#sqlalchemy.create_engine.params.echo) 便于 debug,而其他插件使用默认的 PostgreSQL 数据库,可以这样配置: ```shell SQLALCHEMY_BINDS='{ "": "postgresql+psycopg://scott:tiger@localhost/mydatabase", "nonebot_plugin_wordcloud": { "url": "sqlite+aiosqlite://", "echo": true } }' ``` ### sqlalchemy_engine_options [`create_async_engine()`](https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html#sqlalchemy.ext.asyncio.create_async_engine) 默认参数字典。 ```shell SQLALCHEMY_ENGINE_OPTIONS='{ "pool_size": 5, "max_overflow": 10, "pool_timeout": 30, "pool_recycle": 3600, "echo": true }' ``` ### sqlalchemy_echo 开启 [Echo 选项](https://docs.sqlalchemy.org/en/20/core/engines.html#sqlalchemy.create_engine.params.echo) 和 [Echo Pool 选项](https://docs.sqlalchemy.org/en/20/core/engines.html#sqlalchemy.create_engine.params.echo_pool) 便于 debug。 ```shell SQLALCHEMY_ECHO=true ``` :::caution 注意 以上配置之间有覆盖关系,遵循特殊优先于一般的原则,具体为 [`sqlalchemy_database_url`](#sqlalchemy_database_url) > [`sqlalchemy_bind`](#sqlalchemy_bind) > [`sqlalchemy_echo`](#sqlalchemy_echo) > [`sqlalchemy_engine_options`](#sqlalchemy_engine_options)。 但覆盖顺序并非显而易见,出于清晰考虑,请只配置必要的选项。 ::: ================================================ FILE: website/versioned_docs/version-2.4.2/best-practice/deployment.mdx ================================================ --- sidebar_position: 3 description: 部署你的机器人 --- # 部署 import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; 在编写完成各类插件后,我们需要长期运行机器人来使得用户能够正常使用。通常,我们会使用云服务器来部署机器人。 我们在开发插件时,机器人运行的环境称为开发环境;而在部署后,机器人运行的环境称为生产环境。与开发环境不同的是,在生产环境中,开发者通常不能随意地修改/添加/删除代码,开启或停止服务。 ## 部署前准备 ### 项目依赖管理 由于部署后的机器人运行在生产环境中,因此,为确保机器人能够正常运行,我们需要保证机器人的运行环境与开发环境一致。我们可以通过以下几种方式来进行依赖管理: [Poetry](https://python-poetry.org/) 是一个 Python 项目的依赖管理工具。它可以通过声明项目所依赖的库,为你管理(安装/更新)它们。Poetry 提供了一个 `poetry.lock` 文件,以确保可重复安装,并可以构建用于分发的项目。 Poetry 会在安装依赖时自动生成 `poetry.lock` 文件,在**项目目录**下执行以下命令: ```bash # 初始化 poetry 配置 poetry init # 添加项目依赖,这里以 nonebot2[fastapi] 为例 poetry add nonebot2[fastapi] ``` [PDM](https://pdm.fming.dev/) 是一个现代 Python 项目的依赖管理工具。它采用 [PEP621](https://www.python.org/dev/peps/pep-0621/) 标准,依赖解析快速;同时支持 [PEP582](https://www.python.org/dev/peps/pep-0582/) 和 [virtualenv](https://virtualenv.pypa.io/)。PDM 提供了一个 `pdm.lock` 文件,以确保可重复安装,并可以构建用于分发的项目。 PDM 会在安装依赖时自动生成 `pdm.lock` 文件,在**项目目录**下执行以下命令: ```bash # 初始化 pdm 配置 pdm init # 添加项目依赖,这里以 nonebot2[fastapi] 为例 pdm add nonebot2[fastapi] ``` [pip](https://pip.pypa.io/) 是 Python 包管理工具。他并不是一个依赖管理工具,为了尽可能保证环境的一致性,我们可以使用 `requirements.txt` 文件来声明依赖。 ```bash pip freeze > requirements.txt ``` ### 安装 Docker [Docker](https://www.docker.com/) 是一个应用容器引擎,可以让开发者打包应用以及依赖包到一个可移植的镜像中,然后发布到服务器上。 我们可以参考 [Docker 官方文档](https://docs.docker.com/get-docker/) 来安装 Docker 。 在 Linux 上,我们可以使用以下一键脚本来安装 Docker 以及 Docker Compose Plugin: ```bash curl -fsSL https://get.docker.com | sh -s -- --mirror Aliyun ``` 在 Windows/macOS 上,我们可以使用 [Docker Desktop](https://docs.docker.com/desktop/) 来安装 Docker 以及 Docker Compose Plugin。 ### 安装脚手架 Docker 插件 我们可以使用 [nb-cli-plugin-docker](https://github.com/nonebot/cli-plugin-docker) 来快速部署机器人。 插件可以帮助我们生成配置文件并构建 Docker 镜像,以及启动/停止/重启机器人。使用以下命令安装脚手架 Docker 插件: ```bash nb self install nb-cli-plugin-docker ``` ## Docker 部署 ### 快速部署 使用脚手架命令即可一键生成配置并部署: ```bash nb docker up ``` 当看到 `Running` 字样时,说明机器人已经启动成功。我们可以通过以下命令来查看机器人的运行日志: ```bash nb docker logs ``` ```bash docker compose logs ``` 如果需要停止机器人,我们可以使用以下命令: ```bash nb docker down ``` ```bash docker compose down ``` ### 自定义部署 在部分情况下,我们需要事先生成 Docker 配置文件,再到生产环境进行部署;或者自动生成的配置文件并不能满足复杂场景,需要根据实际需求手动修改配置文件。我们可以使用以下命令来生成基础配置文件: ```bash nb docker generate ``` nb-cli 将会在项目目录下生成 `docker-compose.yml` 和 `Dockerfile` 等配置文件。在 nb-cli 完成配置文件的生成后,我们可以根据部署环境的实际情况使用 nb-cli 或者 Docker Compose 来启动机器人。 我们可以参考 [Dockerfile 文件规范](https://docs.docker.com/engine/reference/builder/)和 [Compose 文件规范](https://docs.docker.com/compose/compose-file/)修改这两个文件。 修改完成后我们可以直接启动或者手动构建镜像: ```bash # 启动机器人 nb docker up # 手动构建镜像 nb docker build ``` ```bash # 启动机器人 docker compose up -d # 手动构建镜像 docker compose build ``` ### 持续集成 我们可以使用 GitHub Actions 来实现持续集成(CI),我们只需要在 GitHub 上发布 Release 即可自动构建镜像并推送至镜像仓库。 首先,我们需要在 [Docker Hub](https://hub.docker.com/) (或者其他平台,如:[GitHub Packages](https://github.com/features/packages)、[阿里云容器镜像服务](https://www.alibabacloud.com/zh/product/container-registry)等)上创建镜像仓库,用于存放镜像。 前往项目仓库的 `Settings` > `Secrets` > `actions` 栏目 `New Repository Secret` 添加构建所需的密钥: - `DOCKERHUB_USERNAME`: 你的 Docker Hub 用户名 - `DOCKERHUB_TOKEN`: 你的 Docker Hub PAT([创建方法](https://docs.docker.com/docker-hub/access-tokens/)) 将以下文件添加至**项目目录**下的 `.github/workflows/` 目录下,并将文件中高亮行中的仓库名称替换为你的仓库名称: ```yaml title=.github/workflows/build.yml name: Docker Hub Release on: push: tags: - "v*" jobs: docker: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 - name: Set up QEMU uses: docker/setup-qemu-action@v2 - name: Setup Docker uses: docker/setup-buildx-action@v2 - name: Login to DockerHub uses: docker/login-action@v2 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Generate Tags uses: docker/metadata-action@v4 id: metadata with: images: | # highlight-next-line {organization}/{repository} tags: | type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=sha type=raw,value=latest - name: Build and Publish uses: docker/build-push-action@v4 with: context: . push: true tags: ${{ steps.metadata.outputs.tags }} labels: ${{ steps.metadata.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max ``` ### 持续部署 在完成发布并构建镜像后,我们可以自动将镜像部署到服务器上。 前往项目仓库的 `Settings` > `Secrets` > `actions` 栏目 `New Repository Secret` 添加部署所需的密钥: - `DEPLOY_HOST`: 部署服务器的 SSH 地址 - `DEPLOY_USER`: 部署服务器用户名 - `DEPLOY_KEY`: 部署服务器私钥([创建方法](https://github.com/appleboy/ssh-action#setting-up-a-ssh-key)) - `DEPLOY_PATH`: 部署服务器上的项目路径 将以下文件添加至**项目目录**下的 `.github/workflows/` 目录下,在构建成功后触发部署: ```yaml title=.github/workflows/deploy.yml name: Deploy on: workflow_run: workflows: - Docker Hub Release types: - completed jobs: deploy: runs-on: ubuntu-latest if: ${{ github.event.workflow_run.conclusion == 'success' }} steps: - name: Start Deployment uses: bobheadxi/deployments@v1 id: deployment with: step: start token: ${{ secrets.GITHUB_TOKEN }} env: bot - name: Run Remote SSH Command uses: appleboy/ssh-action@master env: DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }} with: host: ${{ secrets.DEPLOY_HOST }} username: ${{ secrets.DEPLOY_USER }} key: ${{ secrets.DEPLOY_KEY }} envs: DEPLOY_PATH script: | cd $DEPLOY_PATH docker compose up -d --pull always - name: update deployment status uses: bobheadxi/deployments@v0.6.2 if: always() with: step: finish token: ${{ secrets.GITHUB_TOKEN }} status: ${{ job.status }} env: ${{ steps.deployment.outputs.env }} deployment_id: ${{ steps.deployment.outputs.deployment_id }} ``` 将上一部分的 `docker-compose.yml` 文件以及 `.env.prod` 配置文件添加至 `DEPLOY_PATH` 目录下,并修改 `docker-compose.yml` 文件中的镜像配置,替换为 Docker Hub 的仓库名称: ```diff - build: . + image: {organization}/{repository}:latest ``` ================================================ FILE: website/versioned_docs/version-2.4.2/best-practice/error-tracking.md ================================================ --- sidebar_position: 2 description: 使用 sentry 进行错误跟踪 --- # 错误跟踪 在应用实际运行过程中,可能会出现各种各样的错误。可能是由于代码逻辑错误,也可能是由于用户输入错误,甚至是由于第三方服务的错误。这些错误都会导致应用的运行出现问题,这时候就需要对错误进行跟踪,以便及时发现问题并进行修复。NoneBot 提供了 `nonebot-plugin-sentry` 插件,支持 [sentry](https://sentry.io/) 平台,可以方便地进行错误跟踪。 ## 安装插件 在使用前请先安装 `nonebot-plugin-sentry` 插件至项目环境中,可参考[获取商店插件](../tutorial/store.mdx#安装插件)来了解并选择安装插件的方式。如: 在**项目目录**下执行以下命令: ```bash nb plugin install nonebot-plugin-sentry ``` ## 使用插件 在安装完成之后,仅需要对插件进行简单的配置即可使用。 ### 获取 sentry DSN 前往 [sentry](https://sentry.io/) 平台,注册并创建一个新的项目,然后在项目设置中找到 `Client Keys (DSN)`,复制其中的 `DSN` 值。 ### 配置插件 :::caution 注意 错误跟踪通常在生产环境中使用,因此开发环境中 `sentry_dsn` 留空即会停用插件。 ::: 在项目 dotenv 配置文件中添加以下配置即可使用: ```dotenv SENTRY_DSN= ``` ## 配置项 配置项具体含义参考 [Sentry Docs](https://docs.sentry.io/platforms/python/configuration/options/)。 - `sentry_dsn: str` - `sentry_debug: bool = False` - `sentry_release: str | None = None` - `sentry_release: str | None = None` - `sentry_environment: str | None = nonebot env` - `sentry_server_name: str | None = None` - `sentry_sample_rate: float = 1.` - `sentry_max_breadcrumbs: int = 100` - `sentry_attach_stacktrace: bool = False` - `sentry_send_default_pii: bool = False` - `sentry_in_app_include: List[str] = Field(default_factory=list)` - `sentry_in_app_exclude: List[str] = Field(default_factory=list)` - `sentry_request_bodies: str = "medium"` - `sentry_with_locals: bool = True` - `sentry_ca_certs: str | None = None` - `sentry_before_send: Callable[[Any, Any], Any | None] | None = None` - `sentry_before_breadcrumb: Callable[[Any, Any], Any | None] | None = None` - `sentry_transport: Any | None = None` - `sentry_http_proxy: str | None = None` - `sentry_https_proxy: str | None = None` - `sentry_shutdown_timeout: int = 2` ================================================ FILE: website/versioned_docs/version-2.4.2/best-practice/htmlkit-render.md ================================================ --- sidebar_position: 8 description: 轻量化 HTML 绘图 --- # 轻量化 HTML 绘图 图片是机器人交互中不可或缺的一部分,对于信息展示的直观性、美观性有很大的作用。 基于 PIL 直接绘制图片具有良好的性能和存储开销,但是难以调试、维护过程式的绘图代码。 使用浏览器渲染类插件可以方便地绘制网页,且能够直接通过 JS 对网页效果进行编程,但是它占用的存储和内存空间相对可观。 NoneBot 提供的 `nonebot-plugin-htmlkit` 提供了另一种基于 HTML 和 CSS 语法的轻量化绘图选择:它基于 `litehtml` 解析库,无须安装额外的依赖即可使用,没有进程间通信带来的额外开销,且在支持 `webp` `avif` 等丰富图片格式的前提下,安装用的 wheel 文件大小仅有约 10 MB。 作为粗略的性能参考,在一台 Ryzen 7 9700X 的 Windows 电脑上,渲染 [PEP 7](https://peps.python.org/pep-0007/) 的 HTML 页面(分辨率为 800x5788,大小约 1.4MB,从本地文件系统读取 CSS)大约需要 100ms,每个渲染任务内存最高占用约为 40MB. ## 安装插件 在使用前请先安装 `nonebot-plugin-htmlkit` 插件至项目环境中,可参考[获取商店插件](../tutorial/store.mdx#安装插件)来了解并选择安装插件的方式。如: 在**项目目录**下执行以下命令: ```bash nb plugin install nonebot-plugin-htmlkit ``` `nonebot-plugin-htmlkit` 插件目前兼容以下系统架构: - Windows x64 - macOS arm64(M-系列芯片) - Linux x64 (非 Alpine 等 musl 系发行版) - Linux arm64 (非 Alpine 等 musl 系发行版) :::caution 访问网络内容 如果需要访问网络资源(如 http(s) 网页内容),NoneBot 需要客户端型驱动器(Forward)。内置的驱动器有 `~httpx` 与 `~aiohttp`。 详见[选择驱动器](../advanced/driver.md)。 ::: ## 使用插件 ### 加载插件 在使用本插件前同样需要使用 `require` 方法进行**加载**并**导入**需要使用的方法,可参考 [跨插件访问](../advanced/requiring.md) 一节进行了解,如: ```python from nonebot import require require("nonebot_plugin_htmlkit") from nonebot_plugin_htmlkit import html_to_pic, md_to_pic, template_to_pic, text_to_pic ``` 插件会自动使用[配置中的参数](#配置-fontconfig)初始化 `fontconfig` 以提供字体查找功能。 ### 渲染 API `nonebot-plugin-htmlkit` 主要提供以下**异步**渲染函数: #### html_to_pic ```python async def html_to_pic( html: str, *, base_url: str = "", dpi: float = 144.0, max_width: float = 800.0, device_height: float = 600.0, default_font_size: float = 12.0, font_name: str = "sans-serif", allow_refit: bool = True, image_format: Literal["png", "jpeg"] = "png", jpeg_quality: int = 100, lang: str = "zh", culture: str = "CN", img_fetch_fn: ImgFetchFn = combined_img_fetcher, css_fetch_fn: CSSFetchFn = combined_css_fetcher, urljoin_fn: Callable[[str, str], str] = urllib3.parse.urljoin, ) -> bytes: ... ``` 最核心的渲染函数。 `base_url` 和 `urljoin_fn` 控制着传入 `image_fetch_fn` 和 `css_fetch_fn` 回调的 url 内容。 `allow_refit` 如果为真,渲染时会自动缩小产出图片的宽度到最适合的宽度,否则必定产出 `max_width` 宽度的图片。 `max_width` 与 `device_height` 会在 `@media` 判断中被使用。 `img_fetch_fn` 预期为一个异步可调用对象(函数),接收图片 url 并返回对应 url 的 jpeg 或 png 二进制数据(`bytes`),可在拒绝加载时返回 `None`. `css_fetch_fn` 预期为一个异步可调用对象(函数),接收目标 CSS url 并返回对应 url 的 CSS 文本(`str`),可在拒绝加载时返回 `None`. 以下为辅助的封装函数,关键字参数若未特殊说明均与 `html_to_pic` 含义相同。 #### text_to_pic ```python async def text_to_pic( text: str, css_path: str = "", *, max_width: int = 500, allow_refit: bool = True, image_format: Literal["png", "jpeg"] = "png", jpeg_quality: int = 100, ) -> bytes: ... ``` 可用于渲染多行文本。 `text` 会被放置于 `
` 中,可据此编写 CSS 来改变文本表现。 #### md_to_pic ```python async def md_to_pic( md: str = "", md_path: str = "", css_path: str = "", *, max_width: int = 500, img_fetch_fn: ImgFetchFn = combined_img_fetcher, allow_refit: bool = True, image_format: Literal["png", "jpeg"] = "png", jpeg_quality: int = 100, ) -> bytes: ... ``` 可用于渲染 Markdown 文本。默认为 GitHub Markdown Light 风格,支持基于 `pygments` 的代码高亮。 `md` 和 `md_path` 二选一,前者设置时应为 Markdown 的文本,后者设置时应为指向 Markdown 文本文件的路径。 #### template_to_pic ```python async def template_to_pic( template_path: str | PathLike[str] | Sequence[str | PathLike[str]], template_name: str, templates: Mapping[Any, Any], filters: None | Mapping[str, Any] = None, *, max_width: int = 500, device_height: int = 600, base_url: str | None = None, img_fetch_fn: ImgFetchFn = combined_img_fetcher, css_fetch_fn: CSSFetchFn = combined_css_fetcher, allow_refit: bool = True, image_format: Literal["png", "jpeg"] = "png", jpeg_quality: int = 100, ) -> bytes: ... ``` 渲染 jinja2 模板。 `template_path` 为 jinja2 环境的路径,`template_name` 是环境中要加载模板的名字,`templates` 为传入模板的参数,`filters` 为过滤器名 -> 自定义过滤器的映射。 ### 控制外部资源获取 通过传入 `img_fetch_fn` 与 `css_fetch_fn`,我们可以在实际访问资源前进行审查,修改资源的来源,或是对 IO 操作进行缓存。 `img_fetch_fn` 预期为一个异步可调用对象(函数),接收图片 url 并返回对应 url 的 jpeg 或 png 二进制数据(`bytes`),可在拒绝加载时返回 `None`. `css_fetch_fn` 预期为一个异步可调用对象(函数),接收目标 CSS url 并返回对应 url 的 CSS 文本(`str`),可在拒绝加载时返回 `None`. 如果你想要禁用外部资源加载/只从文件系统加载/只从网络加载,可以使用 `none_fetcher` `filesystem_***_fetcher` `network_***_fetcher`。 默认的 fetcher 行为(对于 `file://` 从文件系统加载,其余从网络加载)位于 `combined_***_fetcher`,可以通过对其封装实现缓存等操作。 ## 配置项 ### 配置 fontconfig `htmlkit` 使用 `fontconfig` 查找字体,请参阅 [`fontconfig 用户手册`](https://fontconfig.pages.freedesktop.org/fontconfig/fontconfig-user) 了解环境变量的具体含义、如何通过编写配置文件修改字体配置等。 #### fontconfig_file - **类型**: `str | None` - **默认值**: `None` 覆盖默认的配置文件路径。 #### fontconfig_path - **类型**: `str | None` - **默认值**: `None` 覆盖默认的配置目录。 #### fontconfig_sysroot - **类型**: `str | None` - **默认值**: `None` 覆盖默认的 sysroot。 #### fc_debug - **类型**: `str | None` - **默认值**: `None` 设置 Fontconfig 的 debug 级别。 #### fc_dbg_match_filter - **类型**: `str | None` - **默认值**: `None` 当 `FC_DEBUG` 设置为 `MATCH2` 时,过滤 debug 输出。 #### fc_lang - **类型**: `str | None` - **默认值**: `None` 设置默认语言,否则从 `LOCALE` 环境变量获取。 #### fontconfig_use_mmap - **类型**: `str | None` - **默认值**: `None` 是否使用 `mmap(2)` 读取字体缓存。 ================================================ FILE: website/versioned_docs/version-2.4.2/best-practice/multi-adapter.mdx ================================================ --- sidebar_position: 4 description: 插件跨平台支持 --- # 插件跨平台支持 import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; 由于不同平台的事件与接口之间,存在着极大的差异性,NoneBot 通过[重载](../appendices/overload.md)的方式,使得插件可以在不同平台上正确响应。但为了减少跨平台的兼容性问题,我们应该尽可能的使用基类方法实现原生跨平台,而不是使用特定平台的方法。当基类方法无法满足需求时,我们可以使用依赖注入的方式,将特定平台的事件或机器人注入到事件处理函数中,实现针对特定平台的处理。 :::tip 提示 如果需要在多平台上**使用**跨平台插件,首先应该根据[注册适配器](../advanced/adapter.md#注册适配器)一节,为机器人注册各平台对应的适配器。 ::: ## 基于基类的跨平台 在[事件通用信息](../advanced/adapter.md#获取事件通用信息)中,我们了解了事件基类能够提供的通用信息。同时,[事件响应器操作](../appendices/session-control.mdx#更多事件响应器操作)也为我们提供了基本的用户交互方式。使用这些方法,可以让我们的插件运行在任何平台上。例如,一个简单的命令处理插件: ```python {5,11} from nonebot import on_command from nonebot.adapters import Event async def is_blacklisted(event: Event) -> bool: return event.get_user_id() not in BLACKLIST weather = on_command("天气", rule=is_blacklisted, priority=10, block=True) @weather.handle() async def handle_function(): await weather.finish("今天的天气是...") ``` 由于此插件仅使用了事件通用信息和事件响应器操作的纯文本交互方式,这些方法不使用特定平台的信息或接口,因此是原生跨平台的,并不需要额外处理。但在一些较为复杂的需求下,例如发送图片消息时,并非所有平台都具有统一的接口,因此基类便无能为力,我们需要引入特定平台的适配器了。 ## 基于重载的跨平台 重载是 NoneBot 跨平台操作的核心,在[事件类型与重载](../appendices/overload.md#重载)一节中,我们初步了解了如何通过类型注解来实现针对不同平台事件的处理方式。在[依赖注入](../advanced/dependency.mdx)一节中,我们又对依赖注入的使用方法进行了详细的介绍。结合这两节内容,我们可以实现更复杂的跨平台操作。 ### 处理近似事件 对于一系列**差异不大**的事件,我们往往具有相同的处理逻辑。这时,我们不希望将相同的逻辑编写两遍,而应该复用代码,以实现在同一个事件处理函数中处理多个近似事件。我们可以使用[事件重载](../advanced/dependency.mdx#event)的特性来实现这一功能。例如: ```python from nonebot import on_command from nonebot.adapters import Message from nonebot.params import CommandArg from nonebot.adapters.onebot.v11 import MessageEvent as OnebotV11MessageEvent from nonebot.adapters.onebot.v12 import MessageEvent as OnebotV12MessageEvent echo = on_command("echo", priority=10, block=True) @echo.handle() async def handle_function(event: OnebotV11MessageEvent | OnebotV12MessageEvent, args: Message = CommandArg()): await echo.finish(args) ``` ```python from typing import Union from nonebot import on_command from nonebot.adapters import Message from nonebot.params import CommandArg from nonebot.adapters.onebot.v11 import MessageEvent as OnebotV11MessageEvent from nonebot.adapters.onebot.v12 import MessageEvent as OnebotV12MessageEvent echo = on_command("echo", priority=10, block=True) @echo.handle() async def handle_function(event: Union[OnebotV11MessageEvent, OnebotV12MessageEvent], args: Message = CommandArg()): await echo.finish(args) ``` ### 在依赖注入中使用重载 NoneBot 依赖注入系统提供了自定义子依赖的方法,子依赖的类型同样会影响到事件处理函数的重载行为。例如: ```python from datetime import datetime from nonebot import on_command from nonebot.adapters.console import MessageEvent echo = on_command("echo", priority=10, block=True) def get_event_time(event: MessageEvent): return event.time # 处理控制台消息事件 @echo.handle() async def handle_function(time: datetime = Depends(get_event_time)): await echo.finish(time.strftime("%Y-%m-%d %H:%M:%S")) ``` 示例中 ,我们为 `handle_function` 事件处理函数注入了自定义的 `get_event_time` 子依赖,而此子依赖注入参数为 Console 适配器的 `MessageEvent`。因此 `handle_function` 仅会响应 Console 适配器的 `MessageEvent` ,而不能响应其他事件。 ### 处理多平台事件 不同平台的事件之间,往往存在着极大的差异性。为了满足我们插件的跨平台运行,通常我们需要抽离业务逻辑,以保证代码的复用性。一个合理的做法是,在事件响应器的处理流程中,首先先针对不同平台的事件分别进行处理,提取出核心业务逻辑所需要的信息;然后再将这些信息传递给业务逻辑处理函数;最后将业务逻辑的输出以各平台合适的方式返回给用户。也就是说,与平台绑定的处理部分应该与平台无关部分尽量分离。例如: ```python import inspect from nonebot import on_command from nonebot.typing import T_State from nonebot.matcher import Matcher from nonebot.adapters import Message from nonebot.params import CommandArg, ArgPlainText from nonebot.adapters.console import Bot as ConsoleBot from nonebot.adapters.onebot.v11 import Bot as OnebotBot from nonebot.adapters.console import MessageSegment as ConsoleMessageSegment weather = on_command("天气", priority=10, block=True) @weather.handle() async def handle_function(matcher: Matcher, args: Message = CommandArg()): if args.extract_plain_text(): matcher.set_arg("location", args) async def get_weather(state: T_State, location: str = ArgPlainText()): if location not in ["北京", "上海", "广州", "深圳"]: await weather.reject(f"你想查询的城市 {location} 暂不支持,请重新输入!") state["weather"] = "⛅ 多云 20℃~24℃" # 处理控制台询问 @weather.got( "location", prompt=ConsoleMessageSegment.emoji("question") + "请输入地名", parameterless=[Depends(get_weather)], ) async def handle_console(bot: ConsoleBot): pass # 处理 OneBot 询问 @weather.got( "location", prompt="请输入地名", parameterless=[Depends(get_weather)], ) async def handle_onebot(bot: OnebotBot): pass # 通过依赖注入或事件处理函数来进行业务逻辑处理 # 处理控制台回复 @weather.handle() async def handle_console_reply(bot: ConsoleBot, state: T_State, location: str = ArgPlainText()): await weather.send( ConsoleMessageSegment.markdown( inspect.cleandoc( f""" # {location} - 今天 {state['weather']} """ ) ) ) # 处理 OneBot 回复 @weather.handle() async def handle_onebot_reply(bot: OnebotBot, state: T_State, location: str = ArgPlainText()): await weather.send(f"今天{location}的天气是{state['weather']}") ``` :::tip 提示 NoneBot 社区中有一些插件,例如[all4one](https://github.com/nonepkg/nonebot-plugin-all4one)、[send-anything-anywhere](https://github.com/felinae98/nonebot-plugin-send-anything-anywhere),可以帮助你更好地处理跨平台功能,包括事件处理和消息发送等。 ::: ================================================ FILE: website/versioned_docs/version-2.4.2/best-practice/scheduler.md ================================================ --- sidebar_position: 0 description: 定时执行任务 --- # 定时任务 [APScheduler](https://apscheduler.readthedocs.io/en/3.x/) (Advanced Python Scheduler) 是一个 Python 第三方库,其强大的定时任务功能被广泛应用于各个场景。在 NoneBot 中,定时任务作为一个额外功能,依赖于基于 APScheduler 开发的 [`nonebot-plugin-apscheduler`](https://github.com/nonebot/plugin-apscheduler) 插件进行支持。 ## 安装插件 在使用前请先安装 `nonebot-plugin-apscheduler` 插件至项目环境中,可参考[获取商店插件](../tutorial/store.mdx#安装插件)来了解并选择安装插件的方式。如: 在**项目目录**下执行以下命令: ```bash nb plugin install nonebot-plugin-apscheduler ``` ## 使用插件 `nonebot-plugin-apscheduler` 本质上是对 [APScheduler](https://apscheduler.readthedocs.io/en/3.x/) 进行了封装以适用于 NoneBot 开发,因此其使用方式与 APScheduler 本身并无显著区别。在此我们会简要介绍其调用方法,更多的使用方面的功能请参考[APScheduler 官方文档](https://apscheduler.readthedocs.io/en/3.x/userguide.html)。 ### 导入调度器 由于 `nonebot_plugin_apscheduler` 作为插件,因此需要在使用前对其进行**加载**并**导入**其中的 `scheduler` 调度器来创建定时任务。使用 `require` 方法可轻松完成这一过程,可参考 [跨插件访问](../advanced/requiring.md) 一节进行了解。 ```python from nonebot import require require("nonebot_plugin_apscheduler") from nonebot_plugin_apscheduler import scheduler ``` ### 添加定时任务 在 [APScheduler 官方文档](https://apscheduler.readthedocs.io/en/3.x/userguide.html#adding-jobs) 中提供了以下两种直接添加任务的方式: ```python from nonebot import require require("nonebot_plugin_apscheduler") from nonebot_plugin_apscheduler import scheduler # 基于装饰器的方式 @scheduler.scheduled_job("cron", hour="*/2", id="job_0", args=[1], kwargs={arg2: 2}) async def run_every_2_hour(arg1: int, arg2: int): pass # 基于 add_job 方法的方式 def run_every_day(arg1: int, arg2: int): pass scheduler.add_job( run_every_day, "interval", days=1, id="job_1", args=[1], kwargs={arg2: 2} ) ``` :::caution 注意 由于 APScheduler 的定时任务并不是**由事件响应器所触发的事件**,因此其任务函数无法同[事件处理函数](../tutorial/handler.mdx#事件处理函数)一样通过[依赖注入](../tutorial/event-data.mdx#认识依赖注入)获取上下文信息,也无法通过事件响应器对象的方法进行任何操作,因此我们需要使用[调用平台 API](../appendices/api-calling.mdx#调用平台-api)的方式来获取信息或收发消息。 相对于事件处理依赖而言,编写定时任务更像是编写普通的函数,需要我们自行获取信息以及发送信息,请**不要**将事件处理依赖的特殊语法用于定时任务! ::: 关于 APScheduler 的更多使用方法,可以参考 [APScheduler 官方文档](https://apscheduler.readthedocs.io/en/3.x/index.html) 进行了解。 ### 配置项 #### apscheduler_autostart - **类型**: `bool` - **默认值**: `True` 是否自动启动 `scheduler` ,若不启动需要自行调用 `scheduler.start()`。 #### apscheduler_log_level - **类型**: `int` - **默认值**: `30` apscheduler 输出的日志等级 - `WARNING` = `30` (默认) - `INFO` = `20` - `DEBUG` = `10` (只有在开启 nonebot 的 debug 模式才会显示 debug 日志) #### apscheduler_config - **类型**: `dict` - **默认值**: `{ "apscheduler.timezone": "Asia/Shanghai" }` `apscheduler` 的相关配置。参考[配置调度器](https://apscheduler.readthedocs.io/en/latest/userguide.html#scheduler-config), [配置参数](https://apscheduler.readthedocs.io/en/latest/modules/schedulers/base.html#apscheduler.schedulers.base.BaseScheduler) 配置需要包含 `apscheduler.` 作为前缀,例如 `apscheduler.timezone`。 ================================================ FILE: website/versioned_docs/version-2.4.2/best-practice/testing/README.mdx ================================================ --- sidebar_position: 1 description: 使用 NoneBug 进行单元测试 slug: /best-practice/testing/ --- # 配置与测试事件响应器 import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; > 在计算机编程中,单元测试(Unit Testing)又称为模块测试,是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。 为了保证代码的正确运行,我们不仅需要对错误进行跟踪,还需要对代码进行正确性检验,也就是测试。NoneBot 提供了一个测试工具——NoneBug,它是一个 [pytest](https://docs.pytest.org/en/stable/) 插件,可以帮助我们便捷地进行单元测试。 :::tip 提示 建议在阅读本文档前先阅读 [pytest 官方文档](https://docs.pytest.org/en/stable/)来了解 pytest 的相关术语和基本用法。 ::: ## 安装 NoneBug 在**项目目录**下激活虚拟环境后运行以下命令安装 NoneBug: ```bash poetry add nonebug -G test ``` ```bash pdm add nonebug -dG test ``` ```bash pip install nonebug ``` 要运行 NoneBug 测试,还需要额外安装 pytest 异步插件 `pytest-asyncio` 或 `anyio` 以支持异步测试。文档中,我们以 `pytest-asyncio` 为例: ```bash poetry add pytest-asyncio -G test ``` ```bash pdm add pytest-asyncio -dG test ``` ```bash pip install pytest-asyncio ``` ## 配置测试 在开始测试之前,我们需要对测试进行一些配置,以正确启动我们的机器人。 首先我们需要配置 pytest-asyncio,在 `pyproject.toml` 的 pytest 配置部分添加: ```toml [tool.pytest.ini_options] asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "session" ``` 然后,我们在 `tests` 目录下新建 `conftest.py` 文件,添加以下内容: ```python title=tests/conftest.py import pytest import nonebot from pytest_asyncio import is_async_test # 导入适配器 from nonebot.adapters.console import Adapter as ConsoleAdapter def pytest_collection_modifyitems(items: list[pytest.Item]): pytest_asyncio_tests = (item for item in items if is_async_test(item)) session_scope_marker = pytest.mark.asyncio(loop_scope="session") for async_test in pytest_asyncio_tests: async_test.add_marker(session_scope_marker, append=False) @pytest.fixture(scope="session", autouse=True) async def after_nonebot_init(after_nonebot_init: None): # 加载适配器 driver = nonebot.get_driver() driver.register_adapter(ConsoleAdapter) # 加载插件 nonebot.load_from_toml("pyproject.toml") ``` 这样,我们就可以在测试中使用机器人的插件了。通常,我们不需要自行初始化 NoneBot,NoneBug 已经为我们运行了 `nonebot.init()`。如果需要自定义 NoneBot 初始化的参数,我们可以在 `conftest.py` 中添加 `pytest_configure` 钩子函数。例如,我们可以修改 NoneBot 配置环境为 `test` 并从环境变量中输入配置: ```python {4,6,8-10} title=tests/conftest.py import os import pytest from nonebug import NONEBOT_INIT_KWARGS os.environ["ENVIRONMENT"] = "test" def pytest_configure(config: pytest.Config): config.stash[NONEBOT_INIT_KWARGS] = {"secret": os.getenv("INPUT_SECRET")} ``` NoneBug 默认也会为我们管理 lifespan 的 startup 与 shutdown。如果不希望 NoneBug 管理 lifespan,你可以在 `pytest_configure` 里添加以下配置: ```python import pytest from nonebug import NONEBOT_START_LIFESPAN def pytest_configure(config: pytest.Config): config.stash[NONEBOT_START_LIFESPAN] = False ``` ## 编写插件测试 在配置完成插件加载后,我们就可以在测试中使用插件了。NoneBug 通过 pytest fixture `app` 提供各种测试方法,我们可以在测试中使用它来测试插件。现在,我们创建一个测试脚本来测试[深入指南](../../appendices/session-control.mdx)中编写的天气插件。首先,我们先要导入我们需要的模块:
插件示例 ```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}的天气是...") ```
```python {4,5,9,11-16} title=tests/test_weather.py from datetime import datetime import pytest from nonebug import App from nonebot.adapters.console import User, Message, MessageEvent @pytest.mark.asyncio async def test_weather(app: App): from awesome_bot.plugins.weather import weather event = MessageEvent( time=datetime.now(), self_id="test", message=Message("/天气 北京"), user=User(id="user"), ) ``` 在上面的代码中,我们引入了 NoneBug 的测试 `App` 对象,以及必要的适配器消息与事件定义等。在测试函数 `test_weather` 中,我们导入了要进行测试的事件响应器 `weather`。请注意,由于需要等待 NoneBot 初始化并加载插件完毕,插件内容必须在**测试函数内部**进行导入。然后,我们创建了一个 `MessageEvent` 事件对象,它模拟了一个用户发送了 `/天气 北京` 的消息。接下来,我们使用 `app.test_matcher` 方法来测试 `weather` 事件响应器: ```python {11-15} title=tests/test_weather.py @pytest.mark.asyncio async def test_weather(app: App): from awesome_bot.plugins.weather import weather event = MessageEvent( time=datetime.now(), self_id="test", message=Message("/天气 北京"), user=User(id="user"), ) async with app.test_matcher(weather) as ctx: bot = ctx.create_bot() ctx.receive_event(bot, event) ctx.should_call_send(event, "今天北京的天气是...", result=None) ctx.should_finished(weather) ``` 这里我们使用 `async with` 语句并通过参数指定要测试的事件响应器 `weather` 来进入测试上下文。在测试上下文中,我们可以使用 `ctx.create_bot` 方法创建一个虚拟的机器人实例,并使用 `ctx.receive_event` 方法来模拟机器人接收到消息事件。然后,我们就可以定义预期行为来测试机器人是否正确运行。在上面的代码中,我们使用 `ctx.should_call_send` 方法来断言机器人应该发送 `今天北京的天气是...` 这条消息,并且将发送函数的调用结果作为第三个参数返回给事件处理函数。如果断言失败,测试将会不通过。我们也可以使用 `ctx.should_finished` 方法来断言机器人应该结束会话。 为了测试更复杂的情况,我们可以为添加更多的测试用例。例如,我们可以测试用户输入了一个不支持的地名时机器人的反应: ```python {17-21,23-26} title=tests/test_weather.py def make_event(message: str = "") -> MessageEvent: return MessageEvent( time=datetime.now(), self_id="test", message=Message(message), user=User(id="user"), ) @pytest.mark.asyncio async def test_weather(app: App): from awesome_bot.plugins.weather import weather async with app.test_matcher(weather) as ctx: ... # 省略前面的测试用例 async with app.test_matcher(weather) as ctx: bot = ctx.create_bot() event = make_event("/天气 南京") ctx.receive_event(bot, event) ctx.should_call_send(event, "你想查询的城市 南京 暂不支持,请重新输入!", result=None) ctx.should_rejected(weather) event = make_event("北京") ctx.receive_event(bot, event) ctx.should_call_send(event, "今天北京的天气是...", result=None) ctx.should_finished(weather) ``` 在上面的代码中,我们使用 `ctx.should_rejected` 来断言机器人应该请求用户重新输入。然后,我们再次使用 `ctx.receive_event` 方法来模拟用户回复了 `北京`,并使用 `ctx.should_finished` 来断言机器人应该结束会话。 更多的 NoneBug 用法将在后续章节中介绍。 ================================================ FILE: website/versioned_docs/version-2.4.2/best-practice/testing/_category_.json ================================================ { "label": "单元测试", "position": 5 } ================================================ FILE: website/versioned_docs/version-2.4.2/best-practice/testing/behavior.mdx ================================================ --- sidebar_position: 2 description: 测试事件响应、平台接口调用和会话控制 --- # 测试事件响应与会话操作 import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; 在 NoneBot 接收到事件时,事件响应器根据优先级依次通过权限、响应规则来判断当前事件是否应该触发。事件响应流程中,机器人可能会通过 `send` 发送消息或者调用平台接口来执行预期的操作。因此,我们需要对这两种操作进行单元测试。 在上一节中,我们对单个事件响应器进行了简单测试。但是在实际场景中,机器人可能定义了多个事件响应器,由于优先级和响应规则的存在,预期的事件响应器可能并不会被触发。NoneBug 支持同时测试多个事件响应器,以此来测试机器人的整体行为。 ## 测试事件响应 NoneBug 提供了六种定义 `Rule` 和 `Permission` 预期行为的方法: - `should_pass_rule` - `should_not_pass_rule` - `should_ignore_rule` - `should_pass_permission` - `should_not_pass_permission` - `should_ignore_permission` :::tip 提示 事件响应器类型的检查属于 `Permission` 的一部分,因此可以通过 `should_pass_permission` 和 `should_not_pass_permission` 方法来断言事件响应器类型的检查。 ::: 下面我们根据插件示例来测试事件响应行为,我们首先定义两个事件响应器作为测试的对象: ```python title=example.py from nonebot import on_command def never_pass(): return False foo = on_command("foo") bar = on_command("bar", permission=never_pass) ``` 在这两个事件响应器中,`foo` 当收到 `/foo` 消息时会执行,而 `bar` 则不会执行。我们使用 NoneBug 来测试它们: ```python {21,22,28,29} title=tests/test_example.py from datetime import datetime import pytest from nonebug import App from nonebot.adapters.console import User, Message, MessageEvent def make_event(message: str = "") -> MessageEvent: return MessageEvent( time=datetime.now(), self_id="test", message=Message(message), user=User(id="user"), ) @pytest.mark.asyncio async def test_example(app: App): from awesome_bot.plugins.example import foo, bar async with app.test_matcher(foo) as ctx: bot = ctx.create_bot() event = make_event("/foo") ctx.receive_event(bot, event) ctx.should_pass_rule() ctx.should_pass_permission() async with app.test_matcher(bar) as ctx: bot = ctx.create_bot() event = make_event("/foo") ctx.receive_event(bot, event) ctx.should_not_pass_rule() ctx.should_not_pass_permission() ``` 在上面的代码中,我们分别对 `foo` 和 `bar` 事件响应器进行响应测试。我们使用 `ctx.should_pass_rule` 和 `ctx.should_pass_permission` 断言 `foo` 事件响应器应该被触发,使用 `ctx.should_not_pass_rule` 和 `ctx.should_not_pass_permission` 断言 `bar` 事件响应器应该被忽略。 ```python title=tests/test_example.py from datetime import datetime import pytest from nonebug import App from nonebot.adapters.console import User, Message, MessageEvent def make_event(message: str = "") -> MessageEvent: return MessageEvent( time=datetime.now(), self_id="test", message=Message(message), user=User(id="user"), ) @pytest.mark.asyncio async def test_example(app: App): from awesome_bot.plugins.example import foo, bar async with app.test_matcher() as ctx: bot = ctx.create_bot() event = make_event("/foo") ctx.receive_event(bot, event) ctx.should_pass_rule(foo) ctx.should_pass_permission(foo) ctx.should_not_pass_rule(bar) ctx.should_not_pass_permission(bar) ``` 在上面的代码中,我们对 `foo` 和 `bar` 事件响应器一起进行响应测试。我们使用 `ctx.should_pass_rule` 和 `ctx.should_pass_permission` 断言 `foo` 事件响应器应该被触发,使用 `ctx.should_not_pass_rule` 和 `ctx.should_not_pass_permission` 断言 `bar` 事件响应器应该被忽略。通过参数,我们可以指定断言的事件响应器。 当然,如果需要忽略某个事件响应器的响应规则和权限检查,强行进入响应流程,我们可以使用 `should_ignore_rule` 和 `should_ignore_permission` 方法: ```python {21,22} title=tests/test_example.py from datetime import datetime import pytest from nonebug import App from nonebot.adapters.console import User, Message, MessageEvent def make_event(message: str = "") -> MessageEvent: return MessageEvent( time=datetime.now(), self_id="test", message=Message(message), user=User(id="user"), ) @pytest.mark.asyncio async def test_example(app: App): from awesome_bot.plugins.example import foo, bar async with app.test_matcher(bar) as ctx: bot = ctx.create_bot() event = make_event("/foo") ctx.receive_event(bot, event) ctx.should_ignore_rule(bar) ctx.should_ignore_permission(bar) ``` 在忽略了响应规则和权限检查之后,就会进入 `bar` 事件响应器的响应流程。 ## 测试平台接口使用 上一节的示例插件测试中,我们已经尝试了测试插件对事件的消息回复。通常情况下,事件处理流程中对平台接口的使用会通过事件响应器操作或者调用平台 API 两种途径进行。针对这两种途径,NoneBug 分别提供了 `ctx.should_call_send` 和 `ctx.should_call_api` 方法来测试平台接口的使用情况。 1. `should_call_send` 定义事件响应器预期发送的消息,即通过[事件响应器操作 send](../../appendices/session-control.mdx#send)进行的操作。`should_call_send` 有四个参数: - `event`:回复的目标事件。 - `message`:预期的消息对象,可以是 `str`、`Message` 或 `MessageSegment`。 - `result`:send 的返回值,将会返回给插件。 - `bot`(可选):发送消息的 bot 对象。 - `**kwargs`:send 方法的额外参数。 2. `should_call_api` 定义事件响应器预期调用的平台 API 接口,即通过[调用平台 API](../../appendices/api-calling.mdx#调用平台-api)进行的操作。`should_call_api` 有四个参数: - `api`:API 名称。 - `data`:预期的请求数据。 - `result`:call_api 的返回值,将会返回给插件。 - `adapter`(可选):调用 API 的平台适配器对象。 - `**kwargs`:call_api 方法的额外参数。 下面是一个使用 `should_call_send` 和 `should_call_api` 方法的示例: 我们先定义一个测试插件,在响应流程中向用户发送一条消息并调用 `Console` 适配器的 `bell` API。 ```python {8,9} title=example.py from nonebot import on_command from nonebot.adapters.console import Bot foo = on_command("foo") @foo.handle() async def _(bot: Bot): await foo.send("message") await bot.bell() ``` 然后我们对该插件进行测试: ```python title=tests/test_example.py from datetime import datetime import pytest import nonebot from nonebug import App from nonebot.adapters.console import Bot, User, Adapter, Message, MessageEvent def make_event(message: str = "") -> MessageEvent: return MessageEvent( time=datetime.now(), self_id="test", message=Message(message), user=User(id="user"), ) @pytest.mark.asyncio async def test_example(app: App): from awesome_bot.plugins.example import foo async with app.test_matcher(foo) as ctx: # highlight-start adapter = nonebot.get_adapter(Adapter) bot = ctx.create_bot(base=Bot, adapter=adapter) # highlight-end event = make_event("/foo") ctx.receive_event(bot, event) # highlight-start ctx.should_call_send(event, "message", result=None, bot=bot) ctx.should_call_api("bell", {}, result=None, adapter=adapter) # highlight-end ``` 请注意,对于在依赖注入中使用了非基类对象的情况,我们需要在 `create_bot` 方法中指定 `base` 和 `adapter` 参数,确保不会因为重载功能而出现非预期情况。 ## 测试会话控制 在[会话控制](../../appendices/session-control.mdx)一节中,我们介绍了如何使用事件响应器操作来实现对用户的交互式会话。在上一节的示例插件测试中,我们其实已经使用了 `ctx.should_finished` 来断言会话结束。NoneBug 针对各种流程控制操作分别提供了相应的方法来定义预期的会话处理行为。它们分别是: - `should_finished`:断言会话结束,对应 `matcher.finish` 操作。 - `should_rejected`:断言会话等待用户输入并重新执行当前事件处理函数,对应 `matcher.reject` 系列操作。 - `should_paused`: 断言会话等待用户输入并执行下一个事件处理函数,对应 `matcher.pause` 操作。 我们仅需在测试用例中的正确位置调用这些方法,就可以断言会话的预期行为。例如: ```python title=example.py from nonebot import on_command from nonebot.typing import T_State foo = on_command("foo") @foo.got("key", prompt="请输入密码") async def _(state: T_State, key: str = ArgPlainText()): if key != "some password": try_count = state.get("try_count", 1) if try_count >= 3: await foo.finish("密码错误次数过多") else: state["try_count"] = try_count + 1 await foo.reject("密码错误,请重新输入") await foo.finish("密码正确") ``` ```python title=tests/test_example.py from datetime import datetime import pytest from nonebug import App from nonebot.adapters.console import User, Message, MessageEvent def make_event(message: str = "") -> MessageEvent: return MessageEvent( time=datetime.now(), self_id="test", message=Message(message), user=User(id="user"), ) @pytest.mark.asyncio async def test_example(app: App): from awesome_bot.plugins.example import foo async with app.test_matcher(foo) as ctx: bot = ctx.create_bot() event = make_event("/foo") ctx.receive_event(bot, event) ctx.should_call_send(event, "请输入密码", result=None) ctx.should_rejected(foo) event = make_event("wrong password") ctx.receive_event(bot, event) ctx.should_call_send(event, "密码错误,请重新输入", result=None) ctx.should_rejected(foo) event = make_event("wrong password") ctx.receive_event(bot, event) ctx.should_call_send(event, "密码错误,请重新输入", result=None) ctx.should_rejected(foo) event = make_event("wrong password") ctx.receive_event(bot, event) ctx.should_call_send(event, "密码错误次数过多", result=None) ctx.should_finished(foo) ``` ================================================ FILE: website/versioned_docs/version-2.4.2/best-practice/testing/mock-network.md ================================================ --- sidebar_position: 3 description: 模拟网络通信以进行测试 --- # 模拟网络通信 NoneBot 驱动器提供了多种方法来帮助适配器进行网络通信,主要包括客户端和服务端两种类型。模拟网络通信可以帮助我们更加接近实际机器人应用场景,进行更加真实的集成测试。同时,通过这种途径,我们还可以完成对适配器的测试。 NoneBot 中的网络通信主要包括以下几种: - HTTP 服务端(WebHook) - WebSocket 服务端 - HTTP 客户端 - WebSocket 客户端 下面我们将分别介绍如何使用 NoneBug 来模拟这几种通信方式。 ## 测试 HTTP 服务端 当 NoneBot 作为 ASGI 服务端应用时,我们可以定义一系列的路由来处理 HTTP 请求,适配器同样也可以通过定义路由来响应机器人相关的网络通信。下面假设我们使用了一个适配器 `fake` ,它定义了一个路由 `/fake/http` ,用于接收平台 WebHook 并处理。实际应用测试时,应将该路由地址替换为**真实适配器注册的路由地址**。 我们首先需要获取测试用模拟客户端: ```python {5,6} title=tests/test_http_server.py from nonebug import App @pytest.mark.asyncio async def test_http_server(app: App): async with app.test_server() as ctx: client = ctx.get_client() ``` 默认情况下,`app.test_server()` 会通过 `nonebot.get_asgi` 获取测试对象,我们也可以通过参数指定 ASGI 应用: ```python async with app.test_server(asgi=asgi_app) as ctx: ... ``` 获取到模拟客户端后,即可像 `requests`、`httpx` 等库类似的方法进行使用: ```python {3,11-14,16} title=tests/test_http_server.py import nonebot from nonebug import App from nonebot.adapters.fake import Adapter @pytest.mark.asyncio async def test_http_server(app: App): adapter = nonebot.get_adapter(Adapter) async with app.test_server() as ctx: client = ctx.get_client() response = await client.post("/fake/http", json={"bot_id": "fake"}) assert response.status_code == 200 assert response.json() == {"status": "success"} assert "fake" in nonebot.get_bots() adapter.bot_disconnect(nonebot.get_bot("fake")) ``` 在上面的测试中,我们向 `/fake/http` 发送了一个模拟 POST 请求,适配器将会对该请求进行处理,我们可以通过检查请求返回是否正确、Bot 对象是否创建等途径来验证机器人是否正确运行。在完成测试后,我们通常需要对 Bot 对象进行清理,以避免对其他测试产生影响。 ## 测试 WebSocket 服务端 当 NoneBot 作为 ASGI 服务端应用时,我们还可以定义一系列的路由来处理 WebSocket 通信。下面假设我们使用了一个适配器 `fake` ,它定义了一个路由 `/fake/ws` ,用于处理平台 WebSocket 连接信息。实际应用测试时,应将该路由地址替换为**真实适配器注册的路由地址**。 我们同样需要通过 `app.test_server()` 获取测试用模拟客户端,这里就不再赘述。在获取到模拟客户端后,我们可以通过 `client.websocket_connect` 方法来模拟 WebSocket 连接: ```python {3,11-15} title=tests/test_ws_server.py import nonebot from nonebug import App from nonebot.adapters.fake import Adapter @pytest.mark.asyncio async def test_ws_server(app: App): adapter = nonebot.get_adapter(Adapter) async with app.test_server() as ctx: client = ctx.get_client() async with client.websocket_connect("/fake/ws") as ws: await ws.send_json({"bot_id": "fake"}) response = await ws.receive_json() assert response == {"status": "success"} assert "fake" in nonebot.get_bots() ``` 在上面的测试中,我们向 `/fake/ws` 进行了 WebSocket 模拟通信,通过发送消息与机器人进行交互,然后检查机器人发送的信息是否正确。 ## 测试 HTTP 客户端 ~~暂不支持~~ ## 测试 WebSocket 客户端 ~~暂不支持~~ ================================================ FILE: website/versioned_docs/version-2.4.2/community/contact.md ================================================ --- sidebar-position: 0 description: 遇到问题如何获取帮助 --- # 参与讨论 如果在安装或者开发 NoneBot 过程中遇到了任何问题,或者有新奇的点子,欢迎参与我们的社区讨论: 1. 点击下方链接前往 GitHub,前往 Issues 页面,在 `New Issue` Template 中选择 `Question` NoneBot:[![NoneBot project link](https://img.shields.io/github/stars/nonebot/nonebot2?style=social)](https://github.com/nonebot/nonebot2) 2. 通过 QQ 群(点击下方链接直达) [![QQ Chat Group](https://img.shields.io/badge/QQ%E7%BE%A4-768887710-orange?style=social)](https://jq.qq.com/?_wv=1027&k=5OFifDh) 3. 通过 QQ 频道 [![QQ Channel](https://img.shields.io/badge/QQ%E9%A2%91%E9%81%93-NoneBot-orange?style=social)](https://qun.qq.com/qqweb/qunpro/share?_wv=3&_wwv=128&appChannel=share&inviteCode=7b4a3&appChannel=share&businessType=9&from=246610&biz=ka) 4. 通过 Discord 服务器(点击下方链接直达) [![Discord Server](https://discordapp.com/api/guilds/847819937858584596/widget.png?style=shield)](https://discord.gg/VKtE6Gdc4h) ================================================ FILE: website/versioned_docs/version-2.4.2/community/contributing.md ================================================ --- sidebar-position: 1 description: 如何为 NoneBot 贡献代码 --- # 贡献指南 ## Code of Conduct 请参阅 [Code of Conduct](https://github.com/nonebot/nonebot2/blob/master/CODE_OF_CONDUCT.md)。 ## 参与开发 请参阅 [Contributing](https://github.com/nonebot/nonebot2/blob/master/CONTRIBUTING.md)。 ## 鸣谢 感谢以下开发者对 NoneBot2 作出的贡献: ================================================ FILE: website/versioned_docs/version-2.4.2/developer/adapter-writing.md ================================================ --- sidebar_position: 1 description: 编写适配器对接新的平台 --- # 编写适配器 在编写适配器之前,我们需要先了解[适配器的功能与组成](../advanced/adapter#适配器功能与组成),适配器通常由 `Adapter`、`Bot`、`Event` 和 `Message` 四个部分组成,在编写适配器时,我们需要继承 NoneBot 中的基类,并根据实际平台来编写每个部分功能。 ## 组织结构 NoneBot 适配器项目通常以 `nonebot-adapter-{adapter-name}` 作为项目名,并以**命名空间包**的形式编写,即在 `nonebot/adapters/{adapter-name}` 目录中编写实际代码,例如: ```tree 📦 nonebot-adapter-{adapter-name} ├── 📂 nonebot │ ├── 📂 adapters │ │ ├── 📂 {adapter-name} │ │ │ ├── 📜 __init__.py │ │ │ ├── 📜 adapter.py │ │ │ ├── 📜 bot.py │ │ │ ├── 📜 config.py │ │ │ ├── 📜 event.py │ │ │ └── 📜 message.py ├── 📜 pyproject.toml └── 📜 README.md ``` :::tip 提示 上述的项目结构仅作推荐,不做强制要求,保证实际可用性即可。 ::: ### 使用 NB-CLI 创建项目 我们可以使用脚手架快速创建项目: ```shell nb adapter create ``` 按照指引,输入适配器名称以及存储位置,即可创建一个带有基本结构的适配器项目。 ## 组成部分 :::tip 提示 本章节的代码中提到的 `Adapter`、`Bot`、`Event` 和 `Message` 等,均为下文中适配器所编写的类,而非 NoneBot 中的基类。 ::: ### Log 适配器在处理时通常需要打印日志,但直接使用 NoneBot 的默认 `logger` 不方便区分适配器输出和其它日志。因此我们可以使用 NoneBot 提供的 `logger_wrapper` 方法,自定义一个 `log` 函数用于快捷打印适配器日志: ```python {3} title=log.py from nonebot.utils import logger_wrapper log = logger_wrapper("your_adapter_name") ``` 这个 `log` 函数会在默认 `logger` 中添加适配器名称前缀,它接收三个参数:日志等级、日志内容以及可选的异常,具体用法如下: ```python from .log import log log("DEBUG", "A DEBUG log.") log("INFO", "A INFO log.") try: ... except Exception as e: log("ERROR", "something error.", e) ``` ### Config 通常适配器需要一些配置项,例如平台连接密钥等。适配器的配置方法与[插件配置](../appendices/config#%E6%8F%92%E4%BB%B6%E9%85%8D%E7%BD%AE)类似,例如: ```python title=config.py from pydantic import BaseModel class Config(BaseModel): xxx_id: str xxx_token: str ``` 配置项的读取将在下方 [Adapter](#adapter) 中介绍。 ### Adapter Adapter 负责转换事件、调用接口,以及正确创建 Bot 对象并注册到 NoneBot 中。在编写平台相关内容之前,我们需要继承基类,并实现适配器的基本信息: ```python {9,11,14,18} title=adapter.py from typing import Any from typing_extensions import override from nonebot.drivers import Driver from nonebot import get_plugin_config from nonebot.adapters import Adapter as BaseAdapter from .config import Config class Adapter(BaseAdapter): @override def __init__(self, driver: Driver, **kwargs: Any): super().__init__(driver, **kwargs) # 读取适配器所需的配置项 self.adapter_config: Config = get_plugin_config(Config) @classmethod @override def get_name(cls) -> str: """适配器名称""" return "your_adapter_name" ``` #### 与平台交互 NoneBot 提供了多种 [Driver](../advanced/driver) 来帮助适配器进行网络通信,主要分为客户端和服务端两种类型。我们需要**根据平台文档和特性**选择合适的通信方式,并编写相关方法用于初始化适配器,与平台建立连接和进行交互: ##### 客户端通信方式 ```python {12,23,24} title=adapter.py import asyncio from typing_extensions import override from nonebot import get_plugin_config from nonebot.exception import WebSocketClosed from nonebot.drivers import Request, WebSocketClientMixin class Adapter(BaseAdapter): @override def __init__(self, driver: Driver, **kwargs: Any): super().__init__(driver, **kwargs) self.adapter_config: Config = get_plugin_config(Config) self.task: Optional[asyncio.Task] = None # 存储 ws 任务 self.setup() def setup(self) -> None: if not isinstance(self.driver, WebSocketClientMixin): # 判断用户配置的Driver类型是否符合适配器要求,不符合时应抛出异常 raise RuntimeError( f"Current driver {self.config.driver} doesn't support websocket client connections!" f"{self.get_name()} Adapter need a WebSocket Client Driver to work." ) # 在 NoneBot 启动和关闭时进行相关操作 self.driver.on_startup(self.startup) self.driver.on_shutdown(self.shutdown) async def startup(self) -> None: """定义启动时的操作,例如和平台建立连接""" self.task = asyncio.create_task(self._forward_ws()) # 建立 ws 连接 async def _forward_ws(self): request = Request( method="GET", url="your_platform_websocket_url", headers={"token": "..."}, # 鉴权请求头 ) while True: try: async with self.websocket(request) as ws: try: # 处理 websocket ... except WebSocketClosed as e: log( "ERROR", "WebSocket Closed", e, ) except Exception as e: log( "ERROR", "Error while process data from " "websocket platform_websocket_url. " "Trying to reconnect...", e, ) finally: # 这里要断开 Bot 连接 except Exception as e: # 尝试重连 log( "ERROR", "Error while setup websocket to " "platform_websocket_url. Trying to reconnect...", e, ) await asyncio.sleep(3) # 重连间隔 async def shutdown(self) -> None: """定义关闭时的操作,例如停止任务、断开连接""" # 断开 ws 连接 if self.task is not None and not self.task.done(): self.task.cancel() ``` ##### 服务端通信方式 ```python {30,38} title=adapter.py from nonebot import get_plugin_config from nonebot.drivers import ( Request, ASGIMixin, WebSocket, HTTPServerSetup, WebSocketServerSetup ) class Adapter(BaseAdapter): @override def __init__(self, driver: Driver, **kwargs: Any): super().__init__(driver, **kwargs) self.adapter_config: Config = get_plugin_config(Config) self.setup() def setup(self) -> None: if not isinstance(self.driver, ASGIMixin): raise RuntimeError( f"Current driver {self.config.driver} doesn't support asgi server!" f"{self.get_name()} Adapter need a asgi server driver to work." ) # 建立服务端路由 # HTTP Webhook 路由 http_setup = HTTPServerSetup( URL("your_webhook_url"), # 路由地址 "POST", # 接收的方法 "WEBHOOK name", # 路由名称 self._handle_http, # 处理函数 ) self.setup_http_server(http_setup) # 反向 Websocket 路由 ws_setup = WebSocketServerSetup( URL("your_websocket_url"), # 路由地址 "WebSocket name", # 路由名称 self._handle_ws, # 处理函数 ) self.setup_websocket_server(ws_setup) async def _handle_http(self, request: Request) -> Response: """HTTP 路由处理函数,只有一个类型为 Request 的参数,且返回值类型为 Response""" ... return Response( status_code=200, # 状态码 headers={"something": "something"}, # 响应头 content="xxx", # 响应内容 ) async def _handle_ws(self, websocket: WebSocket) -> Any: """WebSocket 路由处理函数,只有一个类型为 WebSocket 的参数""" ... ``` 更多通信交互方式可以参考以下适配器: - [OneBot](https://github.com/nonebot/adapter-onebot/blob/master/nonebot/adapters/onebot/v11/adapter.py) - `WebSocket 客户端`、`WebSocket 服务端`、`HTTP WEBHOOK`、`HTTP POST` - [QQ](https://github.com/nonebot/adapter-qq/blob/master/nonebot/adapters/qq/adapter.py) - `WebSocket 服务端`、`HTTP WEBHOOK` - [Telegram](https://github.com/nonebot/adapter-telegram/blob/beta/nonebot/adapters/telegram/adapter.py) - `HTTP WEBHOOK` #### 建立 Bot 连接 在与平台建立连接后,我们需要将 [Bot](#bot) 实例化,并调用适配器提供的的 `bot_connect` 方法告知 NoneBot 建立了 Bot 连接。在与平台断开连接或出现某些异常进行重连时,我们需要调用 `bot_disconnect` 方法告知 NoneBot 断开了 Bot 连接。 ```python {7,8,11} title=adapter.py from .bot import Bot class Adapter(BaseAdapter): def _handle_connect(self): bot_id = ... # 通过配置或者平台 API 等方式,获取到 Bot 的 ID bot = Bot(self, self_id=bot_id) # 实例化 Bot self.bot_connect(bot) # 建立 Bot 连接 def _handle_disconnect(self): self.bot_disconnect(bot) # 断开 Bot 连接 ``` #### 转换 Event 事件 在接收到来自平台的事件数据后,我们需要将其转为适配器的 [Event](#event),并调用 Bot 的 `handle_event` 方法来让 Bot 对事件进行处理: ```python title=adapter.py import asyncio from typing import Any, Dict from nonebot.compat import type_validate_python from .bot import Bot from .event import Event from .log import log class Adapter(BaseAdapter): @classmethod def payload_to_event(cls, payload: Dict[str, Any]) -> Event: """根据平台事件的特性,转换平台 payload 为具体 Event Event 模型继承自 pydantic.BaseModel,具体请参考 pydantic 文档 """ # 做一层异常处理,以应对平台事件数据的变更 try: return type_validate_python(your_event_class, payload) except Exception as e: # 无法正常解析为具体 Event 时,给出日志提示 log( "WARNING", f"Parse event error: {str(payload)}", ) # 也可以尝试转为基础 Event 进行处理 return type_validate_python(Event, payload) async def _forward(self, bot: Bot): payload: Dict[str, Any] # 接收到的事件数据 event = self.payload_to_event(payload) # 让 bot 对事件进行处理 asyncio.create_task(bot.handle_event(event)) ``` #### 调用平台 API 我们需要实现 `Adapter` 的 `_call_api` 方法,使开发者能够调用平台提供的 API。如果通过 WebSocket 通信可以通过 `send` 方法来发送数据,如果采用 HTTP 请求,则需要通过 NoneBot 提供的 `Request` 对象,调用 `driver` 的 `request` 方法来发送请求。 ```python {11} title=adapter.py from typing import Any from typing_extensions import override from nonebot.drivers import Request, WebSocket from .bot import Bot class Adapter(BaseAdapter): @override async def _call_api(self, bot: Bot, api: str, **data: Any) -> Any: log("DEBUG", f"Calling API {api}") # 给予日志提示 platform_data = your_handle_data_method(data) # 自行将数据转为平台所需要的格式 # 采用 HTTP 请求的方式,需要构造一个 Request 对象 request = Request( method="GET", # 请求方法 url=api, # 接口地址 headers=..., # 请求头,通常需要包含鉴权信息 params=platform_data, # 自行处理数据的传输形式 # json=platform_data, # data=platform_data, ) # 发送请求,返回结果 return await self.driver.request(request) # 采用 WebSocket 通信的方式,可以直接调用 send 方法发送数据 # 通过某种方式获取到 bot 对应的 websocket 对象 ws: WebSocket = your_get_websocket_method(bot.self_id) await ws.send_text(platform_data) # 发送 str 类型的数据 await ws.send_bytes(platform_data) # 发送 bytes 类型的数据 await ws.send(platform_data) # 是以上两种方式的合体 # 接收并返回结果,同样的,也有 str 和 bytes 的区别 return await ws.receive_text() return await ws.receive_bytes() return await ws.receive() ``` `调用平台 API` 实现方式具体可以参考以下适配器: Websocket: - [OneBot V11](https://github.com/nonebot/adapter-onebot/blob/54270edbbdb2a71332d744f90b1a3d7f4bf6463a/nonebot/adapters/onebot/v11/adapter.py#L167-L177) - [OneBot V12](https://github.com/nonebot/adapter-onebot/blob/54270edbbdb2a71332d744f90b1a3d7f4bf6463a/nonebot/adapters/onebot/v12/adapter.py#L204-L218) HTTP: - [OneBot V11](https://github.com/nonebot/adapter-onebot/blob/54270edbbdb2a71332d744f90b1a3d7f4bf6463a/nonebot/adapters/onebot/v11/adapter.py#L179-L215) - [OneBot V12](https://github.com/nonebot/adapter-onebot/blob/54270edbbdb2a71332d744f90b1a3d7f4bf6463a/nonebot/adapters/onebot/v12/adapter.py#L220-L266) - [QQ](https://github.com/nonebot/adapter-qq/blob/dc5d437e101f0e3db542de3300758a035ed7036e/nonebot/adapters/qq/adapter.py#L599-L605) - [Telegram](https://github.com/nonebot/adapter-telegram/blob/4a8633627e619245516767f5503dec2f58fe2193/nonebot/adapters/telegram/adapter.py#L148-L253) - [飞书](https://github.com/nonebot/adapter-feishu/blob/f8ab05e6d57a5e9013b944b0d019ca777725dfb0/nonebot/adapters/feishu/adapter.py#L201-L218) ### Bot Bot 是机器人开发者能够直接获取并使用的核心对象,负责存储平台机器人相关信息,并提供回复事件、调用 API 的上层方法。我们需要继承基类 `Bot`,并实现相关方法: ```python {20,25,34} title=bot.py from typing import TYPE_CHECKING, Any, Union from typing_extensions import override from nonebot.message import handle_event from nonebot.adapters import Bot as BaseBot from .event import Event from .message import Message, MessageSegment if TYPE_CHECKING: from .adapter import Adapter class Bot(BaseBot): """ your_adapter_name 协议 Bot 适配。 """ @override def __init__(self, adapter: Adapter, self_id: str, **kwargs: Any): super().__init__(adapter, self_id) self.adapter: Adapter = adapter # 一些有关 Bot 的信息也可以在此定义和存储 async def handle_event(self, event: Event): # 根据需要,对事件进行某些预处理,例如: # 检查事件是否和机器人有关操作,去除事件消息首尾的 @bot # 检查事件是否有回复消息,调用平台 API 获取原始消息的消息内容 ... # 调用 handle_event 让 NoneBot 对事件进行处理 await handle_event(self, event) @override async def send( self, event: Event, message: Union[str, Message, MessageSegment], **kwargs: Any, ) -> Any: # 根据平台实现 Bot 回复事件的方法 # 将消息处理为平台所需的格式后,调用发送消息接口进行发送,例如: data = message_to_platform_data(message) await self.send_message( data=data, ... ) ``` ### Event Event 是 NoneBot 中的事件主体对象,所有平台消息在进入处理流程前需要转换为 NoneBot 事件。我们需要继承基类 `Event`,并实现相关方法: ```python {5,8,13,18,23,28,33} title=event.py from typing_extensions import override from nonebot.compat import model_dump from nonebot.adapters import Event as BaseEvent class Event(BaseEvent): @override def get_event_name(self) -> str: # 返回事件的名称,用于日志打印 return "event name" @override def get_event_description(self) -> str: # 返回事件的描述,用于日志打印,请注意转义 loguru tag return escape_tag(repr(model_dump(self))) @override def get_message(self): # 获取事件消息的方法,根据事件具体实现,如果事件非消息类型事件,则抛出异常 raise ValueError("Event has no message!") @override def get_user_id(self) -> str: # 获取用户 ID 的方法,根据事件具体实现,如果事件没有用户 ID,则抛出异常 raise ValueError("Event has no context!") @override def get_session_id(self) -> str: # 获取事件会话 ID 的方法,根据事件具体实现,如果事件没有相关 ID,则抛出异常 raise ValueError("Event has no context!") @override def is_tome(self) -> bool: # 判断事件是否和机器人有关 return False ``` 然后根据平台消息的类型,编写各种不同的事件,并且注意要根据事件类型实现 `get_type` 方法,具体请参考[事件类型](../advanced/adapter#事件类型)。消息类型事件还应重写 `get_message` 和 `get_user_id` 等方法,例如: ```python {7,16,20,25,34,42} title=event.py from .message import Message class HeartbeatEvent(Event): """心跳时间,通常为元事件""" @override def get_type(self) -> str: return "meta_event" class MessageEvent(Event): """消息事件""" message_id: str user_id: str @override def get_type(self) -> str: return "message" @override def get_message(self) -> Message: # 返回事件消息对应的 NoneBot Message 对象 return self.message @override def get_user_id(self) -> str: return self.user_id class JoinRoomEvent(Event): """加入房间事件,通常为通知事件""" user_id: str room_id: str @override def get_type(self) -> str: return "notice" class ApplyAddFriendEvent(Event): """申请添加好友事件,通常为请求事件""" user_id: str @override def get_type(self) -> str: return "request" ``` ### Message Message 负责正确序列化消息,以便机器人插件处理。我们需要继承 `MessageSegment` 和 `Message` 两个类,并实现相关方法: ```python {9,12,17,22,27,30,36} title=message.py from typing import Type, Iterable from typing_extensions import override from nonebot.utils import escape_tag from nonebot.adapters import Message as BaseMessage from nonebot.adapters import MessageSegment as BaseMessageSegment class MessageSegment(BaseMessageSegment["Message"]): @classmethod @override def get_message_class(cls) -> Type["Message"]: # 返回适配器的 Message 类型本身 return Message @override def __str__(self) -> str: # 返回该消息段的纯文本表现形式,通常在日志中展示 return "text of MessageSegment" @override def is_text(self) -> bool: # 判断该消息段是否为纯文本 return self.type == "text" class Message(BaseMessage[MessageSegment]): @classmethod @override def get_segment_class(cls) -> Type[MessageSegment]: # 返回适配器的 MessageSegment 类型本身 return MessageSegment @staticmethod @override def _construct(msg: str) -> Iterable[MessageSegment]: # 实现从字符串中构造消息数组,如无字符串嵌入格式可直接返回文本类型 MessageSegment ... ``` 然后根据平台具体的消息类型,来实现各种 `MessageSegment` 消息段,具体可以参考以下适配器: - [OneBot V11](https://github.com/nonebot/adapter-onebot/blob/54270edbbdb2a71332d744f90b1a3d7f4bf6463a/nonebot/adapters/onebot/v11/message.py#L25-L259) - [QQ](https://github.com/nonebot/adapter-qq/blob/dc5d437e101f0e3db542de3300758a035ed7036e/nonebot/adapters/qq/message.py#L30-L520) - [Telegram](https://github.com/nonebot/adapter-telegram/blob/4a8633627e619245516767f5503dec2f58fe2193/nonebot/adapters/telegram/message.py#L13-L414) ## 适配器测试 关于适配器测试相关内容在这里不再展开,开发者可以根据需要进行合适的测试。这里为开发者提供几个常见问题的解决方法: 1. 在测试中无法导入 editable 模式安装的适配器代码。在 pytest 的 `conftest.py` 内添加如下代码: ```python title=tests/conftest.py from pathlib import Path import nonebot.adapters nonebot.adapters.__path__.append( # type: ignore str((Path(__file__).parent.parent / "nonebot" / "adapters").resolve()) ) ``` 2. 需要计算适配器测试覆盖率,请在 `pyproject.toml` 中添加 pytest 配置: ```toml title=pyproject.toml [tool.pytest.ini_options] addopts = "--cov nonebot/adapters/{adapter-name} --cov-report term-missing" ``` ## 后续工作 在完成适配器代码的编写后,如果想要将适配器发布到 NoneBot 商店,我们需要将适配器发布到 PyPI 中,然后前往[商店](/store/adapters)页面,切换到适配器页签,点击**发布适配器**按钮,填写适配器相关信息并提交。 另外建议编写适配器文档或者一些插件开发示例,以便其他开发者使用我们的适配器。 ================================================ FILE: website/versioned_docs/version-2.4.2/developer/plugin-publishing.mdx ================================================ --- sidebar_position: 0 description: 在商店发布自己的插件 --- # 发布插件 import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; NoneBot 为开发者提供了分享插件的官方商店。本指南囊括**从创建项目到发布到 PyPI,最终提交商店审核**的全过程。 :::warning 警告 如果你的插件只是满足自用需求,则完全可以选择**不发布插件**。发布插件**不是**使用插件的必要条件。 NoneBot 社区对于插件有一定质量要求,对于不符合要求的插件,社区成员将会要求修改,直至符合要求后才能通过审核;如果长期未更新修改,社区将会关闭当前请求,之后如需发布请重新提交发布插件请求。相应的要求会在本章节以下部分介绍。 ::: :::tip 提示 本章节仅包含插件发布流程指导,插件开发请查阅前述章节。 ::: ## 准备工作 ### 插件命名规范 NoneBot 插件使用下述命名规范: - 对于**项目名**,建议统一以 `nonebot-plugin-` 开头,之后为拟定的插件名字,词间用横杠 `-` 分隔; - **项目名**用于代码仓库名称、PyPI 包的发布名称等; - 本文使用 `nonebot-plugin-{your-plugin-name}` 为例。 - 对于**模块名**,建议与**项目名**一致,但词间用下划线 `_` 分隔,即统一以 `nonebot_plugin_` 开头,之后为拟定的插件名字; - **模块名**用于程序导入使用,应为插件文件(夹)的名称; - 本文使用 `nonebot_plugin_{your_plugin_name}` 为例。 ### 项目结构 :::tip 提示 本段所述的项目结构仅作推荐,不做强制要求。 ::: 插件程序本身结构可参考[插件结构](../tutorial/create-plugin.md#插件结构)一节,唯一区别在于,插件包可以直接处于项目顶层。 插件项目的一种组织结构如下: ```tree 📦 nonebot-plugin-{your-plugin-name} ├── 📂 nonebot_plugin_{your_plugin_name} │ ├── 📜 __init__.py │ └── 📜 config.py ├── 📜 pyproject.toml └── 📜 README.md ``` 功能开发可以在 `__init__.py` 中进行或在包内部创建其他模块并在 `__init__.py` 中导入。 ### 从项目模板开始 为降低新手门槛,我们提供三条清晰、完整、可复制的发布路径。 :::tip 提示 你只需选择一条与你习惯一致的路径,**完整跟随即可成功发布**。无需在不同工具间切换或猜测配置。 ::: NoneBot 生态目前有如下插件项目模板: - [RF-Tar-Railt/nonebot-plugin-template](https://github.com/RF-Tar-Railt/nonebot-plugin-template) 此路径使用 **PDM** 项目管理器,符合 PEP 621 标准,自动化程度高。 - [fllesser/nonebot-plugin-template](https://github.com/fllesser/nonebot-plugin-template) 此路径使用 **uv** 项目管理器和 **PoeThePoet** 任务运行器,构建速度快,适合追求效率的开发者。 - [A-kirami/nonebot-plugin-template](https://github.com/A-kirami/nonebot-plugin-template) 此路径使用 **Poetry** 项目管理器,适合熟悉传统 Python 生态的开发者。 #### 1. 创建项目 1. 访问上述三个模板之一。 2. 点击 **“Use this template”** → **“Create a new repository”**。 3. 仓库名称填写:`nonebot-plugin-{your-plugin-name}`(此部分以 `nonebot-plugin-weather` 为例)。 4. 点击 **“Create repository from template”**。 #### 2. 配置发布权限 1. 进入新仓库 → **Settings** → **Actions** → **General**。 2. 在 **Workflow permissions** 下,勾选 **“Read and write permissions”** → 点击 **Save**。 #### 3. 全局替换项目信息 在仓库中点击 **“Add file”** → **“Create new file”**,创建一个空文件 `LICENSE`,选择开源协议并提交(此操作会触发工作流)。 然后在本地克隆仓库,使用编辑器对以下内容进行**全局替换**: :::tip 提示 此部分以“天气插件”为例,实际的替换内容应该根据你所创建的插件名称等相应调整。 ::: | 原内容 | 替换为 | | ------------------------------ | ---------------------------------- | | `nonebot-plugin-template` | `nonebot-plugin-weather` | | `nonebot_plugin_template` | `nonebot_plugin_weather` | | `` | `天气查询` | | `` | `查询指定城市的实时天气与未来预报` | | `` | `你的GitHub用户名` | | `` | `你的邮箱` | #### 4. 安装依赖与开发 ```bash # 安装 PDM(若未安装) curl -sSL https://pdm-project.org/install-pdm.py | python3 - # 安装项目依赖(自动创建虚拟环境) pdm sync # 添加新依赖(如 httpx) pdm add httpx ``` ```bash # 安装 uv(Windows) powershell -c "irm https://astral.sh/uv/install.ps1 | iex" # 安装 uv(macOS/Linux) curl -LsSf https://astral.sh/uv/install.sh | sh # 安装所有依赖(含 dev) uv sync --all-groups -p 3.12 # 添加新依赖 uv add httpx ``` ```bash # 安装 Poetry(推荐方式) curl -sSL https://install.python-poetry.org | python3 - # 安装项目依赖 poetry install # 添加新依赖 poetry add httpx ``` #### 5. 更新版本并发布 [bump-my-version](https://github.com/callowayproject/bump-my-version) 是一个功能强大、可配置的 Python 项目版本更新工具,支持自动提交到 Git 等 VCS。 ```bash # 安装 bump-my-version pdm add --dev bump-my-version # 更新 patch 版本 pdm run bump patch # 推送 tag 触发发布 git push origin --tags ``` ```bash # 更新 patch 版本 uv run poe bump patch # 推送 tag 触发发布 git push origin --tags ``` ```bash # 安装 bump-my-version poetry add --dev bump-my-version # 更新 patch 版本 poetry run bump patch # 推送 tag 触发发布 git push origin --tags ``` 需要安装 PDM 插件 [pdm-bump](https://github.com/carstencodes/pdm-bump)。 ```bash # 安装 pdm-bump pdm self add pdm-bump # 更新 patch 版本 pdm bump patch # 推送 tag 触发发布 git push origin --tags ``` ```bash # 更新 patch 版本 uv version --bump patch # 创建相应提交与标签 git add pyproject.toml git commit -m "chore: release v0.1.1" # 替换为实际的版本号 git tag v0.1.1 # 替换为实际的版本号 # 推送 tag 触发发布 git push origin --tags ``` ```bash # 更新版本(自动提交并打标签) poetry version patch # 推送 tag 触发发布 git push origin --tags ``` 手动更新 `pyproject.toml` 中的 `version` 字段,然后推送 tag 触发发布工作流 ```bash git add pyproject.toml git commit -m "chore: release v0.1.1" # 替换为实际的版本号 git tag v0.1.1 # 替换为实际的版本号 git push origin --tags ``` 推送 `v*` 标签后,模板提供的 GitHub Actions 工作流将自动构建并发布到 PyPI。 #### 6. 发布到 [PyPI](https://pypi.org) 不同模板使用的发布方式可能不同,具体配置流程参考对应模板的详细使用指南。 根据选用的构建系统,在项目的 `pyproject.toml` 中填入必要信息后进行构建与发布。 :::tip 提示 不同构建工具的使用可能存在差别。本文仅以 [`pdm`](https://pdm-project.org/zh/latest/), [`poetry`](https://python-poetry.org/docs/), [`setuptools`](https://setuptools.pypa.io/en/latest/) 构建系统**本地构建与发布**为示例讲解,其余构建/管理工具等和自动化构建的用法请读者自行探索。 ::: ```bash poetry publish --build # 构建并发布 # 等效于以下两个命令 poetry build # 只构建 poetry publish # 只发布先前的构建 ``` ```bash pdm publish # 构建并发布 # 等效于以下两个命令 pdm build # 只构建 pdm publish --no-build # 只发布先前的构建 ``` ```bash pip install build twine # 安装通用构建与发布工具 python -m build --sdist --wheel . # 只构建 twine upload dist/* # 只发布先前的构建 ``` :::tip 提示 发布前建议自行测试构建包是否可用,避免遗漏代码文件或资源文件等问题。 ::: ## 基本要求 无论你选择哪条路径,以下内容**必须**完成,否则无法通过商店自动检查: ### 能够正确加载 插件包必须能够被 NoneBot 正确加载,在商店审核中会通过 **NoneFlow** 自动化加载测试进行。 #### 依赖其他插件 如果插件依赖其他插件提供的功能,则**必须**在代码中使用 `require()` 来引入该插件,然后才能 `import` 该插件提供的功能。具体细节参阅[跨插件访问](../advanced/requiring.md)。 使用示例如下: ```python title=nonebot_plugin_weather/__init__.py from nonebot import require require("nonebot_plugin_apscheduler") from nonebot_plugin_apscheduler import scheduler ``` #### 不能零配置加载的插件 如果插件需要必要配置项才能正常导入,则**必须**在商店提交表单中填写必要的配置项内容。 但一种更好的做法是,**将插件设计为零配置即可加载**(允许缺少必要配置项时插件仍能正常导入,但不执行需要相应配置项的功能),尤其是对于一些必要配置含有敏感信息(如密钥、Token、API Key 等)的插件。这样可以避免在商店提交表单时填写敏感信息的风险。 ### 插件元数据 插件包**必须**填写元数据才能通过 **NoneFlow** 自动化检查。 下面是一个示例: ```python title=nonebot_plugin_weather/__init__.py from nonebot.plugin import PluginMetadata from .config import Config __plugin_meta__ = PluginMetadata( # 基本信息(必填) name="天气查询", # 插件名称 description="查询指定城市的实时天气与未来预报", # 插件介绍 usage="发送【天气 城市名】获取天气信息", # 插件用法 # 发布额外信息 type="application", # 插件分类 # 发布必填,当前有效类型有:`library`(为其他插件编写提供功能),`application`(向机器人用户提供功能)。 homepage="https://github.com/你的用户名/nonebot-plugin-weather", # 发布必填。 config=Config, # 插件配置项类,如果有配置类则必须填写。 supported_adapters={"~onebot.v11"}, # 支持的适配器集合,其中 `~` 在此处代表前缀 `nonebot.adapters.`,其余适配器亦按此格式填写。 # 若插件只使用了 NoneBot 基本抽象,应显式填写 None,否则应该列出插件支持的适配器。 ) ``` :::caution 注意 `__plugin_meta__` 变量**必须**处于插件最外层(如 `__init__.py` 中),否则无法正常识别。 一般做法是在 `__init__.py` 中定义 `__plugin_meta__`。 ::: #### 继承其他插件支持的适配器 如果你的插件依赖于其他插件提供的支持功能,而其他插件可能支持更少的适配器,这时就应该使用 [inherit_supported_adapters()](../api/plugin/load#inherit-supported-adapters) 函数来继承其他插件支持的适配器。 示例用法如下: ```python title=nonebot_plugin_weather/__init__.py from nonebot import require from nonebot.plugin import PluginMetadata, inherit_supported_adapters from .config import Config require("nonebot_plugin_alconna") # 必须先 require 才能被 inherit_supported_adapters 处理 __plugin_meta__ = PluginMetadata( name="天气查询", description="查询指定城市的实时天气与未来预报", usage="发送【天气 城市名】获取天气信息", type="application", homepage="https://github.com/你的用户名/nonebot-plugin-weather", config=Config, supported_adapters=inherit_supported_adapters("nonebot_plugin_alconna"), # 继承 nonebot_plugin_alconna 插件的适配器支持列表 ) ``` ### 准备项目主页 通常可以使用 GitHub 项目页面作为项目主页,在 `README.md` 文件中编写插件介绍等内容。 内容大致包括: - 插件功能介绍; - 安装方法 - **必须**有 NB-CLI 方式安装 - 可选依赖可以给出其他安装方式 - **不得**使用旧式的 `bot.py` 配置 - 插件配置项(如 `Config` 类字段,若无可跳过) - 插件设置的触发规则(若无可跳过) - 插件的其它用法(按需编写) - 效果图、权限说明(按需编写) ## 质量要求 以下内容**强烈建议**完成,否则社区成员将会要求修改: ### 依赖管理原则 - **必须**包含 `nonebot2`。 - **必须**将插件直接使用的适配器加入依赖列表,如:使用 OneBot 适配器的插件应添加 `nonebot-adapter-onebot` 依赖; - **禁止**使用 `==` 锁定单一版本,使用 `>=` 或 `~=`。 - **禁止**添加 `nonebot`(V1)作为依赖。 - 所有在代码中 `import` 的第三方库,必须在 `pyproject.toml` 的 `dependencies` 中列出。 ### 避免误用同步操作 NoneBot 是一个异步框架,插件中**禁止**使用任何可能阻塞事件循环的同步操作,例如: - 同步 HTTP 请求(如 `requests` 库); **推荐**操作(以 `httpx` 为例): ```python import httpx async with httpx.AsyncClient() as client: response = await client.get("https://api.example.com/data") # 异步操作,不阻塞机器人 ``` **禁止**操作: ```python import requests requests.get("https://api.example.com/data") # 同步操作,会阻塞机器人 ``` - 其他可能长时间运行阻塞事件循环的操作。 ### 本地文件存储 如果插件需要在本地存储数据、配置或缓存文件,**必须**使用 [`nonebot-plugin-localstore`](https://github.com/nonebot/plugin-localstore) 管理,具体细节参阅[本地存储](../best-practice/data-storing.md)章节。 参考示例: ```python title=nonebot_plugin_weather/__init__.py from pathlib import Path from nonebot import require require("nonebot_plugin_localstore") import nonebot_plugin_localstore as store # 获取插件缓存文件(夹)路径 weather_cache_dir: Path = store.get_plugin_cache_dir() weather_cache_file: Path = store.get_plugin_cache_file("cache.json") # 获取插件配置文件(夹)路径 weather_config_dir: Path = store.get_plugin_config_dir() weather_config_file: Path = store.get_plugin_config_file("config.toml") # 获取插件数据文件(夹)路径 weather_data_dir: Path = store.get_plugin_data_dir() weather_data_file: Path = store.get_plugin_data_file("resource-index.json") ``` ## 商店审核 ### 提交申请 完成在 PyPI 的插件发布流程后,前往[商店](/store/plugins)页面,切换到插件页签,点击 **发布插件** 按钮。 在弹出的插件信息提交表单内,填入您所要发布的相应插件信息。请注意,如果插件需要必要配置项才能正常导入,请在“插件配置项”中填写必要的内容(请勿填写密钥等敏感信息)。 完成填写后,点击 **发布** 按钮,这将自动跳转 NoneBot 仓库内的“发布插件”页面。确认信息无误后点击页面下方的 `Submit new issue` 按钮进行最终提交即可。 ### 等待插件审核 插件发布 Issue 创建后,将会经过 **NoneFlow Bot** 的自动前置检查,以确保插件信息正确无误、插件能被正确加载。 :::tip 提示 若插件检查未通过或信息有误,**不必**关闭当前 Issue。只需更新插件并上传到 PyPI/修改信息后勾选插件测试勾选框即可重新触发插件检查。 ::: 之后,NoneBot 的维护者和一些志愿者会初步检查插件代码,帮助减少该插件的问题。 完成这些步骤后,您的插件将会被自动合并到[商店](/store/plugins),而您也将成为 [**NoneBot 贡献者**](https://github.com/nonebot/nonebot2/graphs/contributors)的一员。 ================================================ FILE: website/versioned_docs/version-2.4.2/editor-support.md ================================================ --- 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` 前往编辑。 ================================================ FILE: website/versioned_docs/version-2.4.2/ospp/2021.md ================================================ --- sidebar_position: 0 description: 开源软件供应链点亮计划 - 暑期 2021 mdx: format: md --- # 暑期 2021 **开源软件供应链点亮计划 - 暑期 2021** 是**中国科学院软件研究所**与 **openEuler 社区**共同举办的一项面向高校学生的暑期活动,旨在鼓励在校学生积极参与开源软件的开发维护,促进优秀开源软件社区的蓬勃发展。关于具体的活动规划、报名方式,请查看该活动的 [官网](https://summer.iscas.ac.cn/) 和 [帮助文档](https://summer.iscas.ac.cn/help/)。 NoneBot 社区有幸作为开源社区参与了本次活动,下面列出了目前我们已经发布的项目,欢迎感兴趣的同学在上面给出的活动官网报名,或通过 [contact@nonebot.dev](mailto:contact@nonebot.dev) 联系我们。 ## NoneBot v1 ### 更新 NoneBot v1 文档中的“指南”部分 由于 NoneBot v1 和 aiocqhttp 最初基于的 QQ 机器人平台不再提供服务,CQHTTP 接口也转型且改名为 OneBot 标准,目前 NoneBot v1 文档的“指南”部分和 aiocqhttp 文档有部分过时内容需要更新。我们希望将其中与旧的机器人平台相关的内容改为基于 go-cqhttp 或通用的 OneBot 表述,同时对 NoneBot v1 的 awesome-bot 示例做一次全面检查,修改其中可能已经不可用的部分。 **难度**:低 **导师**:[@cleoold](https://github.com/cleoold) **产出要求** - 修改“指南”文档和 aiocqhttp 文档中与旧的 QQ 机器人平台相关的部分 - 检查 awesome-bot 示例是否有已经过时/不可用的地方,并更新/修复 - 修改“图灵机器人”案例,使用其它 AI 聊天 API 提供商(需先做简单调研) **技术要求** - 熟悉 Python 编程语言及 asyncio 机制 - 了解 Git 基本用法 - 了解聊天机器人基本开发过程 - 了解 VuePress 更佳 ### NoneBot v1 API 文档自动生成 目前 NoneBot v1 的文档中“API”部分是手动编写的,在更新代码接口的同时需要手动更新文档,可能造成文档与代码不匹配,形成额外的维护成本。我们希望将 API 文档改为直接编写在 Python docstring 中,通过工具自动生成 API 文档。 **难度**:中 **导师**:[@cleoold](https://github.com/cleoold) **产出要求** - 调研市面上常见的 Python API 文档生成工具 - 在代码中补充 API 文档 - 编写或应用开源工具自动生成 API 文档 - 配置 GitHub Actions 或其它 CI 自动化构建和部署 API 文档 **技术要求** - 熟悉 Python 编程语言及 asyncio 和 Type Hints - 了解 Git 基本用法 - 了解 Sphinx 等文档生成工具更佳 - 了解 GitHub Actions 等 CI 工具更佳 ## NoneBot v2 ### NoneBot v2 自动化测试框架“NoneBug” 在聊天机器人的开发过程中,一套自动化的测试机制是非常重要的,特别是对于 NoneBot 2 这类为大型机器人开发而设计的项目来说,需要手动测试每一个边际条件是非常痛苦的。我们希望能够开发一款基于 NoneBot 2 插件机制的自动化测试框架,为 NoneBot 2 用户提供一套易用便捷、高度灵活的自动化测试框架。 **难度**:高 **导师**:[@yanyongyu](https://github.com/yanyongyu) **产出要求** - 调研现有的 Python 和其它语言集成测试框架 - 设计 NoneBug 的用户 API 和实现方式 - 实现 NoneBug 自动化测试框架 - 编写详细的使用文档 **技术要求** - 熟悉 Python 编程语言及 asyncio 和 Type Hints - 了解 Git 基本用法 - 了解 NoneBot v2 的基本原理和使用方式 - 了解主流的 Python 自动化测试框架 ### NoneBot v2 Telegram 适配器 目前 NoneBot v2 已支持 OneBot、Mirai HTTP API、钉钉协议,社区反馈有更多的平台需求,希望能在 NoneBot v2 获得更多的跨平台支持,提高机器人的便携性。同时,我们也希望随着新平台加入,提升现有 NoneBot v2 核心代码的平台通用性。Telegram 是一款较为广泛使用的安全即时聊天软件,同时其官方提供了丰富的聊天机器人 API,因此我们希望为 NoneBot v2 编写一个 Telegram 适配器来支持 Telegram 机器人的开发。 **难度**:中 **导师**:[@yanyongyu](https://github.com/yanyongyu) **产出要求** - 调研 Telegram Bot API 以及 WebHook 等官方接口 - 编写 Telegram 适配器并能够使用 - 代码遵守项目 Contributing 规范 **技术要求** - 熟悉 Python 编程语言及 asyncio 和 Type Hints - 了解 Git 基本用法 - 了解 Web 开发相关知识 - 了解 Sphinx 等文档生成工具更佳 ### NoneBot v2 飞书适配器 目前 NoneBot v2 已支持 OneBot、Mirai HTTP API、钉钉协议,社区反馈有更多的平台需求,希望能在 NoneBot v2 获得更多的跨平台支持,提高机器人的便携性。同时,我们也希望随着新平台加入,提升现有 NoneBot v2 核心代码的平台通用性。飞书是目前企业用户广泛使用的即时聊天和协作软件,其官方提供了丰富的聊天机器人 API,因此我们希望为 NoneBot v2 编写一个飞书适配器来支持飞书机器人的开发。 **难度**:中 **导师**:[@yanyongyu](https://github.com/yanyongyu) **产出要求** - 调研飞书机器人 API 以及 WebHook 等官方接口 - 编写飞书适配器并能够使用 - 代码遵守项目 Contributing 规范 **技术要求** - 熟悉 Python 编程语言及 asyncio 和 Type Hints - 了解 Git 基本用法 - 了解 Web 开发相关知识 - 了解 Sphinx 等文档生成工具更佳 ## OneBot ### 设计 OneBot v12 接口标准 目前的 OneBot 标准的 v11 版本仍然与 QQ 平台有较多耦合,我们希望在 v12 去掉与 QQ 耦合的历史包袱,形成一个通用的、可扩展的、易于使用的同时易于实现的聊天机器人接口标准。 **难度**:中 **导师**:[@richardchien](https://github.com/richardchien) **产出要求** - 调研各聊天机器人平台的官方/非官方接口特点 - 通用化 OneBot 核心 API,分离 QQ 特定的 API,去掉无用 API - 优化现有的通信、消息表示机制 - 补充 QQ 特定的缺失 API - 文档需符合风格指南 **技术要求** - 熟悉至少两个聊天平台的聊天机器人开发 - 了解 Git 基本用法 - 了解使用不同语言编写聊天机器人时的常用实践 - 对文档的优雅性与美观性有追求更佳 ### 实现 Rust 版 libonebot 目前最常用的 OneBot 实现包括 go-cqhttp、onebot-kotlin、node-onebot 等,这些实现都各自重复实现了 Web 通信、消息解析、配置读写等功能,当社区中的开发者想针对一个新的聊天平台实现 OneBot 时,他们往往同样需要再次实现类似逻辑。我们希望使用 Rust 编写一个 libonebot 模块,该模块实现所有 OneBot 实现所共享的功能,从而方便其他开发者们使用 Rust 快速编写具体的 OneBot 实现。同时,我们希望借此项目在聊天机器人社区中推广 Rust 编程语言。 > 注:这里的逻辑是 libonebot + 针对某聊天平台的对接代码 = 某聊天平台的 OneBot 实现,libonebot 要做的是让 OneBot 实现的开发者只需编写针对特定聊天平台的对接代码,而无需关心 OneBot 标准定义的通信方式、消息格式等。 **难度**:高 **导师**:[@richardchien](https://github.com/richardchien) **产出要求** - 实现所有 OneBot 实现所共享的功能,包括 Web 通信、消息解析、配置读写等 - 充分考虑同时兼容 OneBot v11 和 v12 接口 - 能够根据用户(OneBot 实现的开发者)所实现的接口自动实现类似 get_available_apis 等接口 - 编写详细的使用文档 - 如果可能,与 v12 设计项目联动,实现第一手 v12 支持 **技术要求** - 熟悉聊天机器人开发 - 熟悉 Rust Web 开发 ### 实现自选语言版 libonebot 目前最常用的 OneBot 实现包括 go-cqhttp、onebot-kotlin、node-onebot 等,这些实现都各自重复实现了 Web 通信、消息解析、配置读写等功能,当社区中的开发者想针对一个新的聊天平台实现 OneBot 时,他们往往同样需要再次实现类似逻辑。我们希望使用 Python、Go、Kotlin、Node、PHP、C#.NET 等主流语言(任选一个)编写 libonebot 模块,该模块实现所有 OneBot 实现所共享的功能,从而方便其他开发者们使用对应语言快速编写具体的 OneBot 实现。 > 注:这里的逻辑是 libonebot + 针对某聊天平台的对接代码 = 某聊天平台的 OneBot 实现,libonebot 要做的是让 OneBot 实现的开发者只需编写针对特定聊天平台的对接代码,而无需关心 OneBot 标准定义的通信方式、消息格式等。 **难度**:中 **导师**:[@richardchien](https://github.com/richardchien) **产出要求** - 实现所有 OneBot 实现所共享的功能,包括 Web 通信、消息解析、配置读写等 - 充分考虑同时兼容 OneBot v11 和 v12 接口 - 编写详细的使用文档 - 如果可能,实现更多附加特性,如根据用户(OneBot 实现的开发者)所实现的接口自动实现类似 get_available_apis 等接口、实现第一手 v12 支持等 **技术要求** - 熟悉聊天机器人开发 - 熟悉所选语言的 Web 开发 ================================================ FILE: website/versioned_docs/version-2.4.2/ospp/2022.md ================================================ --- sidebar_position: 1 description: 开源之夏 - 暑期 2022 mdx: format: md --- # 暑期 2022 **开源之夏 - 暑期 2022** 是由**开源软件供应链点亮计划**发起、由**中国科学院软件研究所**与 **openEuler 社区**主办的一项面向高校学生的暑期活动,类似 Google Summer of Code(GSoC),旨在鼓励在校学生积极参与开源软件的开发维护,促进优秀开源软件社区的蓬勃发展。关于具体的活动规划、报名方式,请查看该活动的 [官网](https://summer-ospp.ac.cn/) 和 [帮助文档](https://summer-ospp.ac.cn/help/)。 NoneBot 社区有幸作为开源社区 [参与](https://summer-ospp.ac.cn/#/org/orgdetail/e1fb5b8d-125a-4138-b756-25bd32c0a31a/) 了本次活动,下面列出了目前我们已经发布的项目,欢迎感兴趣的同学加入 QQ 群 [737131827](https://jq.qq.com/?_wv=1027&k=PEgyGeEu) 或通过 [contact@nonebot.dev](mailto:contact@nonebot.dev) 联系我们。 ## NoneBot2 命令行 CLI 交互体验升级 NoneBot2 为用户提供了命令行脚手架 ──`nb-cli`,辅助用户更好地上手项目以及进行开发。nb-cli 主要包括:创建项目、运行项目、安装与卸载插件、部署项目等功能。随着 NoneBot2 Beta 版本的发布,脚手架功能存在一定的定位不明确、功能体验不佳。本项目旨在重新设计 nb-cli 功能框架,完善功能,优化用户体验。 **难度**:进阶 **导师**:[@yanyongyu](https://github.com/yanyongyu) **产出要求** - 设计 nb-cli 功能框架 - 明确各功能模块 - 设计用户交互模式 - 完成 nb-cli 主要功能代码 - 项目管理 - 插件管理 - 其它 - 同步更新使用文档 **技术要求** - 熟悉 Python 命令行交互代码编写 - 熟悉 NoneBot2 框架功能 - 熟悉 NoneBot2 项目组织方式 **成果仓库** - - ## NoneBot2 命令行即时交互通信设计与实现 NoneBot2 在早期提供了基于网页的 nonebot-plugin-test 插件,无需平台适配接入即可对机器人进行测试,方便了开发者直观的感受机器人文本交互功能。我们希望提供一款基于命令行的适配器/驱动器,用于无平台适配接入、可以运行机器人的场景进行功能体验或测试。 **难度**:进阶 **导师**:[@mnixry](https://github.com/mnixry) **产出要求** - 设计命令行与 NoneBot2 通信模式 - 直接调用/HTTP/WebSocket - 设计命令行交互界面 - 实现相应适配器/驱动器 - 同步更新使用说明文档 **技术要求** - 熟悉 Python 命令行交互代码编写 - 熟悉 NoneBot2 框架功能 - 熟悉 NoneBot2 项目组织方式 **成果仓库** - ## NoneBot2 用户上手与深入教程设计 NoneBot2 为用户提供了详细的文档介绍,辅助用户更好的上手项目以及进行开发。文档分为基础与进阶两个部分。基础部分帮助新用户快速上手开发,主要包括:安装 NoneBot2、使用脚手架、创建配置项目、使用适配器、加载插件、定义消息事件、处理消息事件、调用平台 API 等。进阶部分向已经熟悉开发流程的用户介绍更多高级技巧,主要包括:NoneBot2 工作原理、定时任务、权限控制、钩子函数、跨插件访问、单元测试、发布插件等。目前文档对于用户而言过于费解,导致用户难以理解 NoneBot2 开发。本项目旨在优化文档内容,使其更加通俗易懂,不让文档成为用户上手的阻碍,同时完善进阶内容,让有更复杂需求的用户,同样能从文档中受益。 相关 issue: - - **难度**:进阶 **导师**:[@SK-415](https://github.com/SK-415) **产出要求** - 文档通俗易懂 - 附有适当的图片指引(如 asciinema) - 内容完整,由浅入深 - 适当的界面美化,合理分配布局 **技术要求** - 熟悉文档结构组织与语言表达 - 熟悉 NoneBot2 框架功能 - 熟悉 NoneBot2 项目组织方式 **成果仓库** - ================================================ FILE: website/versioned_docs/version-2.4.2/ospp/2023.md ================================================ --- sidebar_position: 2 description: 开源之夏 - 暑期 2023 mdx: format: md --- # 暑期 2023 **开源之夏 - 暑期 2023** 是由**开源软件供应链点亮计划**发起、由**中国科学院软件研究所**与 **openEuler 社区**主办的一项面向高校学生的暑期活动,类似 Google Summer of Code(GSoC),旨在鼓励在校学生积极参与开源软件的开发维护,促进优秀开源软件社区的蓬勃发展。关于具体的活动规划、报名方式,请查看该活动的 [官网](https://summer-ospp.ac.cn/) 和 [帮助文档](https://summer-ospp.ac.cn/help/)。 NoneBot 社区有幸作为开源社区 [参与](https://summer-ospp.ac.cn/org/orgdetail/e1fb5b8d-125a-4138-b756-25bd32c0a31a?lang=zh) 了本次活动,下面列出了目前我们已经发布的项目,欢迎感兴趣的同学通过 [contact@nonebot.dev](mailto:contact@nonebot.dev) 联系我们。 ## NoneBot 项目管理图形化面板 NoneBot 目前提供了开箱即用的命令行脚手架来帮助初次使用的用户更快的上手编写应用。但是,对于未有一定开发经验的用户,命令行的使用仍具有一定的困难。此外,其他项目如 koishi、vue 等,均可通过图形化界面的形式为用户提供更便捷的项目开发。因此,我们希望借助现有命令行脚手架的可扩展特性,提供一个项目管理面板服务,以网页的形式帮助用户开发 NoneBot 应用。 **难度**:进阶 **导师**:[@mnixry](https://github.com/mnixry) **产出要求** - 设计并实现项目管理面板相关功能 - 创建与管理项目 - 配置与运行项目 - NoneBot 插件管理 - 实现相应 nb-cli 插件提供面板服务 - 代码符合 NoneBot Contributing 规范 **技术要求** - 熟悉 nb-cli 相关功能 - 熟悉 NoneBot 框架功能 - 熟悉前后端相关实现方式 **成果仓库** - ## NoneBot Discord 适配器 NoneBot 作为一个跨平台聊天机器人框架,目前已有 OneBot、飞书、Telegram、QQ 频道等诸多平台的适配支持。作为众多用户期待的平台适配之一,我们希望借此机会接入 Discord 聊天机器人。 **难度**:进阶 **导师**:[@iyume](https://github.com/iyume) **产出要求** - 调研 Discord Bot 相关功能与接口 - 设计与编写 NoneBot Discord 适配器 - 代码符合 NoneBot Contributing 规范 **技术要求** - 熟悉 NoneBot 框架功能 - 熟悉 NoneBot 各模块职责与适配器编写 **成果仓库** - ## NoneBot 数据库支持插件 NoneBot 的插件系统为用户实现应用提供了极高的便捷性,但因此也增加了插件统一管理的难度。目前,我们发现许多用户发布的插件中存在文件存储结构化数据、数据存放散乱等现象,同时插件间也可能产生冲突。因此,我们希望提供一个统一的数据存储与管理方式,便于用户读写应用数据。 **难度**:进阶 **导师**:[@yanyongyu](https://github.com/yanyongyu) **产出要求** - 设计并实现 ORM 插件 - 提供关系模型定义功能 - 提供模型迁移与管理功能 - 能较好的支持 Python 类型检查与推导 - 编写相应的用户使用文档 - 代码符合 NoneBot Contributing 规范 **技术要求** - 熟悉 NoneBot 框架功能与插件编写 - 熟悉 SQLAlchemy 等 ORM 框架 - 熟悉 SQLAlchemy ORM - 熟悉 alembic 等迁移工具 - 熟悉 nb-cli 插件编写 **成果仓库** - ================================================ FILE: website/versioned_docs/version-2.4.2/ospp/2024.md ================================================ --- sidebar_position: 3 description: 开源之夏 - 暑期 2024 mdx: format: md --- # 暑期 2024 **开源之夏 - 暑期 2024** 是**中国科学院软件研究所**发起的**开源软件供应链点亮计划**系列暑期活动,旨在鼓励高校学生积极参与开源软件的开发维护,促进优秀开源软件社区的蓬勃发展。活动联合各大开源社区,针对重要开源软件的开发与维护提供项目开发任务,并向全球高校学生开放报名。关于具体的活动规划、报名方式,请查看该活动的 [官网](https://summer-ospp.ac.cn/) 和 [帮助文档](https://summer-ospp.ac.cn/help/)。 NoneBot 社区有幸作为开源社区 [参与](https://summer-ospp.ac.cn/org/orgdetail/e1fb5b8d-125a-4138-b756-25bd32c0a31a?lang=zh) 了本次活动,下面列出了目前我们已经发布的项目,欢迎感兴趣的同学通过 [contact@nonebot.dev](mailto:contact@nonebot.dev) 联系我们。 ## NonePress 官网组件库更新与优化 NoneBot 官网目前采用基于 TailwindCSS 自研的 NonePress 组件库及 Docusaurus 框架进行构建。由于相关依赖版本迭代迅速,目前官网组件库已产生了较大的版本落后。本项目希望在跟进框架新版本的基础上,对文档整体视觉体验进行重新设计,提升页面的无障碍访问性,基于 React Hydrate 特性实现完整的静态网站生成(SSG)以提升搜索引擎优化(SEO)水平。在解决以上问题的基础上,可对网页的开发以及生产构建性能做相应的优化提升,例如在生产构建使用自有的 webpack loader、替换现有的热重载逻辑以减少开发环境启动耗时等。 **难度**:进阶 **导师**:[@yanyongyu](https://github.com/yanyongyu) **产出要求** - 基于 Docusaurus v3 重构 NonePress 组件库及相关插件 - 升级相关依赖并重新打造 Docusaurus theme(布局与组件) - 根据需求实现/修改 Docusaurus 插件使得官网内容构建正常 - 能够提升页面渲染性能与 MDX 相关能力 - 升级官网采用新版组件库 - Algolia 索引与 SEO 正常 - 桌面端与移动端显示正常 - 优化官网开发与生产构建体验 - (可选)优化官网部分页面 - 优化官网过长的 changelog - 优化官网插件商店的展示细节 **技术要求** - 熟练掌握 TS、PostCSS、TSX、MDX等相关技术 - 掌握 React、Docusaurus、tailwind css 等框架 - 熟悉静态网站生成 SSG、SEO 优化与 Algolia 索引原理等 **成果仓库** - ## NoneFlow 社区自动化工作流管理优化 NoneFlow 在 NoneBot 社区中承担着重要的角色,它由 NoneBot 框架基于 GitHub APP 编写而成,能够自动化的完成许多复杂流程的处理,如:用户请求提交插件到商店时进行自动化检测,并在人工审核通过后自动存储至 registry;定时自动更新 registry 内插件信息,跟进插件新版本情况等。但是,在长期的使用中发现了一些问题和不足的地方,例如:项目本身结构复杂耦合,添加新自动化流程与维护现有流程困难;目前采用了 GitHub 用户名作为插件作者名,但已有不少插件作者改名;插件存储至 registry 并定时更新,缺少统计相关信息以帮助商店更好的展示当前插件状态;插件作者想要修改插件信息时无法便捷的找到操作方式等。本项目希望针对以上问题与不足的地方进行修复与优化,提升用户体验。 **难度**:进阶 **导师**:[@uy/sun](https://github.com/he0119) **产出要求** - 重构现有工作流处理结构 - 整合现有 Issue、Pull Request、Git 相关操作 - 提供用户修改信息的处理方式 - 正确处理 PR 的 Open、Close、Draft 状态 - 修复流程中存在的问题 - 插件作者名正确展示 - registry 定时更新中需要插件测试环境隔离 - 在 registry 定时更新的同时提供统计数据 **技术要求** - 掌握 GitHub APP 开发 - 熟悉 GitHub REST API、GraphQL 等 - 熟悉 GitHub APP 权限限制 - 熟悉 NoneBot 框架与 Python 相关技术 - 熟悉 Git、GitHub Action、GitHub 工作流 **成果仓库** - ## NoneBlockly 低代码框架开发 经过深入分析社区反馈,我们发现部分新手因不熟悉编程概念或框架本身而遇到问题。为了解决初学者在使用面向开发者的聊天机器人框架 NoneBot 时遇到的挑战,我们计划引入 Blockly 提供低代码编程支持。通过减少常见的编码错误和降低入门门槛,使框架对初学者更加友好,从而提升用户体验并有助于 NoneBot 生态的成长。本项目将基于 Blockly 实现 NoneBot 插件的低代码编写,使得用户能够快速搭建聊天机器人。 **难度**:进阶 **导师**:[@mnixry](https://github.com/mnixry) **产出要求** - 实现 NoneBlockly 低代码开发框架 - 能够基于 Alconna 编写跨平台插件 - 确保插件对 Python 和 NoneBot 版本的兼容性 - 支持对多种类型 NoneBot 事件的响应 - 支持对 NoneBot 消息对象的便捷操作 - 集成 localstore 文件存储、apscheduler 定时任务、网络请求等常用功能 - 对接 NB-CLI 脚手架,通过脚手架扩展使用低代码框架 **技术要求** - 掌握 Python 与 NoneBot 框架的使用 - 熟悉 NoneBot 插件的开发,包括事件响应与消息处理等 - 熟悉 NoneBot 生态组件(Alconna、localstore、apscheduler等)的使用 - 了解 NB-CLI 脚手架的扩展开发 - 熟悉 Blockly 低代码框架的使用和开发 **成果仓库** - ================================================ FILE: website/versioned_docs/version-2.4.2/ospp/2025.md ================================================ --- sidebar_position: 4 description: 开源之夏 - 暑期 2025 mdx: format: md --- # 暑期 2025 **开源之夏 - 暑期 2025** 是**中国科学院软件研究所**发起的**开源软件供应链点亮计划**系列暑期活动,旨在鼓励高校学生积极参与开源软件的开发维护,培养和发掘更多优秀的开发者,促进优秀开源软件社区的蓬勃发展,助力开源软件供应链建设。活动联合各大开源社区,针对重要开源软件的开发与维护提供项目开发任务,并向全球高校学生开放报名。关于具体的活动规划、报名方式,请查看该活动的 [官网](https://summer-ospp.ac.cn/) 和 [帮助文档](https://summer-ospp.ac.cn/help/)。 NoneBot 社区有幸作为开源社区 [参与](https://summer-ospp.ac.cn/org/orgdetail/e1fb5b8d-125a-4138-b756-25bd32c0a31a?lang=zh) 了本次活动,下面列出了目前我们已经发布的项目,欢迎感兴趣的同学通过 [contact@nonebot.dev](mailto:contact@nonebot.dev) 联系我们。 ## NoneBot HTML 图片渲染插件 文字与图片一直是聊天机器人的两大主流交互方式,而图片的渲染一直是用户开发应用的一大痛点。常见的方式包括 PIL 图片编辑、浏览器渲染 HTML 截图等。PIL 图片编辑依赖人工构建图片布局,容易出现自适应问题,且提升图片特效、美观程度需要极大的开发成本。浏览器渲染方案通过 HTML 与 CSS 能够轻松完成美观自适应能力强的布局,但其部署门槛较高,难以支撑较大规模调用量。而其他轻量化渲染引擎通常不具有完整 HTML/CSS 现代化标准实现,且未提供 Python Binding 直接使用。 本项目希望调研并实现一种高效、便捷的图片渲染方案。该方案需要在保障跨平台一致性、最大程度保证 HTML 与 CSS 现代化标准的前提下,低成本(资源消耗与吞吐量)将 HTML 渲染为对应图片。 **难度**:进阶 **导师**:[@MelodyKnit](https://github.com/MelodyKnit) **产出要求** - 调研 HTML/CSS 渲染引擎 - 调研 litehtml 等渲染引擎 标准支持能力与兼容性 - 基于渲染引擎实现 HTML 图片渲染插件 - 将渲染引擎通过 binding 等方式集成为 Python 模块 - 基于集成模块实现 HTML 图片渲染能力 - 编写插件使用文档 **技术要求** - 掌握 Python 及其异步编程 - 熟悉 NoneBot 框架及其插件编写 - 了解浏览器与 HTML 渲染原理 **成果仓库** - ## NB-CLI 命令行工具交互优化 NB-CLI 作为 NoneBot 生态的核心入门与管理工具,主要负责新手引导项目创建、项目运行以及插件管理几大功能。目前该脚手架工具仍存在几点缺陷: - 作为插件管理工具,由于存储数据的局限性,无法很好地展示用户项目当前安装插件状态,并进行卸载等操作; - 当前插件管理高度依赖云端 registry 提供插件信息,在离线情况下完全无法使用; - 由于插件信息繁多,工具未能向用户展示充分的信息,交互复杂 体验较差。 以上问题对用户使用 NB-CLI 管理项目插件造成了极大的阻碍。 本项目希望重点针对插件管理部分,重构工具插件管理模块,完善框架缺陷,并通过缓存等方式确保可用性。其次,调研同类工具方案与 TUI 等相关技术,优化信息展示能力、用户交互方式,提升工具整体交互体验。 **相关链接** - https://github.com/nonebot/nb-cli/issues/138 - https://github.com/nonebot/nb-cli/issues/140 **难度**:基础 **导师**:[@yanyongyu](https://github.com/yanyongyu) **产出要求** - 重构 NB-CLI 插件管理模块 - 优化项目插件信息存储方式,支持列出、卸载插件等操作 - 通过缓存 registry 数据等方式确保离线场景的可用性 - 提升 NB-CLI 交互体验 - 调研同类工具方案与 TUI 等相关技术 - 优化 registry 多字段信息展示能力 - 基于 TUI 等技术优化用户交互方式,提升整体交互体验 **技术要求** - 熟练掌握 Python 及其异步编程 - 熟悉 NoneBot 框架与 NB-CLI 使用方法 - 了解 TUI 等终端交互技术 **成果仓库** - ================================================ FILE: website/versioned_docs/version-2.4.2/quick-start.mdx ================================================ --- sidebar_position: 1 description: 尝试使用 NoneBot options: menu: - category: tutorial weight: 10 --- import Asciinema from "@site/src/components/Asciinema"; import Messenger from "@site/src/components/Messenger"; # 快速上手 :::caution 前提条件 - 请确保你的 Python 版本 >= 3.9 - **我们强烈建议使用虚拟环境进行开发**,如果没有使用虚拟环境,请确保已经卸载可能存在的 NoneBot v1!!! ```bash pip uninstall nonebot ``` ::: 在本章节中,我们将介绍如何使用脚手架来创建一个 NoneBot 简易项目。项目将基于 nb-cli 脚手架运行,并允许我们从商店安装插件。 ## 安装脚手架 确保你已经安装了 Python 3.9 及以上版本,然后在命令行中执行以下命令: 1. 安装 [pipx](https://pypa.github.io/pipx/) ```bash python -m pip install --user pipx python -m pipx ensurepath ``` 如果在此步骤的输出中出现了“open a new terminal”或者“re-login”字样,那么请关闭当前终端并重新打开一个新的终端。 2. 安装脚手架 ```bash pipx install nb-cli ``` 安装完成后,你可以在命令行使用 `nb` 命令来使用脚手架。如果出现无法找到命令的情况(例如出现“Command not found”字样),请参考 [pipx 文档](https://pypa.github.io/pipx/) 检查你的环境变量。 ## 创建项目 使用脚手架来创建一个项目: ```bash nb create ``` 这一指令将会执行创建项目的流程,你将会看到一些询问: 1. 项目模板 ```bash [?] 选择一个要使用的模板: bootstrap (初学者或用户) ``` 这里我们选择 `bootstrap` 模板,它是一个简单的项目模板,能够安装商店插件。如果你需要**自行编写插件**,这里请选择 `simple` 模板。 2. 项目名称 ```bash [?] 项目名称: awesome-bot ``` 这里我们以 `awesome-bot` 为例,作为项目名称。你可以根据自己的需要来命名。 3. 其他选项 请注意,多选项使用**空格**选中或取消,**回车**确认。 ```bash [?] 要使用哪些驱动器? FastAPI (FastAPI 驱动器) [?] 要使用哪些适配器? Console (基于终端的交互式适配器) [?] 立即安装依赖? (Y/n) Yes [?] 创建虚拟环境? (Y/n) Yes ``` 这里我们选择了创建虚拟环境,nb-cli 在之后的操作中将会自动使用这个虚拟环境。如果你不需要自动创建虚拟环境或者已经创建了其他虚拟环境,nb-cli 将会安装依赖至当前激活的 Python 虚拟环境。 4. 选择内置插件 ```bash [?] 要使用哪些内置插件? echo ``` 这里我们选择 `echo` 插件作为示例。这是一个简单的复读回显插件,可以用于测试你的机器人是否正常运行。 ## 运行项目 在项目创建完成后,你可以在**项目目录**中使用以下命令来运行项目: ```bash nb run ``` 你现在应该已经运行起来了你的第一个 NoneBot 项目了!请注意,生成的项目中使用了 `FastAPI` 驱动器和 `Console` 适配器,你之后可以自行修改配置或安装其他适配器。 ## 尝试使用 在项目运行起来后,`Console` 适配器会在你的终端启动交互模式,你可以直接在输入框中输入 `/echo hello world` 来测试你的机器人是否正常运行。 ================================================ FILE: website/versioned_docs/version-2.4.2/tutorial/application.md ================================================ --- sidebar_position: 0 description: 创建一个 NoneBot 项目 options: menu: - category: tutorial weight: 20 --- # 手动创建项目 在[快速上手](../quick-start.mdx)中,我们已经介绍了如何安装和使用 `nb-cli` 创建一个项目。在本章节中,我们将简要介绍如何在不使用 `nb-cli` 的方式创建一个机器人项目的**最小实例**并启动。如果你想要了解 NoneBot 的启动流程,也可以阅读本章节。 :::caution 警告 我们十分不推荐直接创建机器人项目,请优先考虑使用 nb-cli 进行项目创建。 ::: 一个机器人项目的**最小实例**中**至少**需要包含以下内容: - 入口文件:初始化并运行机器人的 Python 文件 - 配置文件:存储机器人启动所需的配置 - 插件:为机器人提供具体的功能 下面我们创建一个项目文件夹,来存放项目所需文件,以下步骤均在该文件夹中进行。 ## 安装依赖 在创建项目前,我们首先需要将项目所需依赖安装至环境中。 1. (可选)创建虚拟环境,以 venv 为例 ```bash python -m venv .venv --prompt nonebot2 # windows .venv\Scripts\activate # linux/macOS source .venv/bin/activate ``` 2. 安装 nonebot2 以及驱动器 ```bash pip install 'nonebot2[fastapi]' ``` 驱动器包名可以在 [驱动器商店](/store/drivers) 中找到。 3. 安装适配器 ```bash pip install nonebot-adapter-console ``` 适配器包名可以在 [适配器商店](/store/adapters) 中找到。 ## 创建配置文件 配置文件用于存放 NoneBot 运行所需要的配置项,使用 [`pydantic`](https://docs.pydantic.dev/) 以及 [`python-dotenv`](https://saurabh-kumar.com/python-dotenv/) 来读取配置。配置项需符合 dotenv 格式,复杂类型数据需使用 JSON 格式填写。具体可选配置方式以及配置项详情参考[配置](../appendices/config.mdx)。 在**项目文件夹**中创建一个 `.env` 文本文件,并写入以下内容: ```bash title=.env HOST=0.0.0.0 # 配置 NoneBot 监听的 IP / 主机名 PORT=8080 # 配置 NoneBot 监听的端口 COMMAND_START=["/"] # 配置命令起始字符 COMMAND_SEP=["."] # 配置命令分割字符 ``` ## 创建入口文件 入口文件( Entrypoint )顾名思义,是用来初始化并运行机器人的 Python 文件。入口文件需要完成框架的初始化、注册适配器、加载插件等工作。 :::tip 提示 如果你使用 `nb-cli` 创建项目,入口文件不会被创建,该文件功能会被 `nb run` 命令代替。 ::: 在**项目文件夹**中创建一个 `bot.py` 文件,并写入以下内容: ```python title=bot.py import nonebot from nonebot.adapters.console import Adapter as ConsoleAdapter # 避免重复命名 # 初始化 NoneBot nonebot.init() # 注册适配器 driver = nonebot.get_driver() driver.register_adapter(ConsoleAdapter) # 在这里加载插件 nonebot.load_builtin_plugins("echo") # 内置插件 # nonebot.load_plugin("thirdparty_plugin") # 第三方插件 # nonebot.load_plugins("awesome_bot/plugins") # 本地插件 if __name__ == "__main__": nonebot.run() ``` 我们暂时不需要了解其中内容的含义,这些将会在稍后的章节中逐一介绍。在创建完成以上文件并确认已安装所需适配器和插件后,即可运行机器人。 ## 运行机器人 在**项目文件夹**中,使用配置好环境的 Python 解释器运行入口文件(如果使用虚拟环境,请先激活虚拟环境): ```bash python bot.py ``` 如果你后续使用了 `nb-cli` ,你仍可以使用 `nb run` 命令来运行机器人,`nb-cli` 会自动检测入口文件 `bot.py` 是否存在并运行。同时,你也可以使用 `nb run --reload` 来自动检测代码的更改并自动重新运行入口文件。 ================================================ FILE: website/versioned_docs/version-2.4.2/tutorial/create-plugin.md ================================================ --- sidebar_position: 3 description: 创建并加载自定义插件 options: menu: - category: tutorial weight: 50 --- # 插件编写准备 在正式编写插件之前,我们需要先了解一下插件的概念。 ## 插件结构 在 NoneBot 中,插件即是 Python 的一个[模块(module)](https://docs.python.org/zh-cn/3/glossary.html#term-module)。NoneBot 会在导入时对这些模块做一些特殊的处理使得他们成为一个插件。插件间应尽量减少耦合,可以进行有限制的相互调用,NoneBot 能够正确解析插件间的依赖关系。 ### 单文件插件 一个普通的 `.py` 文件即可以作为一个插件,例如创建一个 `foo.py` 文件: ```tree title=Project 📂 plugins └── 📜 foo.py ``` 这个时候模块 `foo` 已经可以被称为一个插件了,尽管它还什么都没做。 ### 包插件 一个包含 `__init__.py` 的文件夹即是一个常规 Python [包 `package`](https://docs.python.org/zh-cn/3/glossary.html#term-regular-package),例如创建一个 `foo` 文件夹: ```tree title=Project 📂 plugins └── 📂 foo └── 📜 __init__.py ``` 这个时候包 `foo` 同样是一个合法的插件,插件内容可以在 `__init__.py` 文件中编写。 ## 创建插件 :::caution 注意 如果在之前的[快速上手](../quick-start.mdx)章节中已经使用 `bootstrap` 模板创建了项目,那么你需要做出如下修改: 1. 在项目目录中创建一个两层文件夹 `awesome_bot/plugins` ```tree title=Project 📦 awesome-bot ├── 📂 .venv ├── 📂 awesome_bot │ └── 📂 plugins ├── 📜 .env.prod ├── 📜 pyproject.toml └── 📜 README.md ``` 2. 修改 `pyproject.toml` 文件中的 `nonebot` 配置项,在 `plugin_dirs` 中添加 `awesome_bot/plugins` ```toml title=pyproject.toml [tool.nonebot] plugin_dirs = ["awesome_bot/plugins"] ``` ::: :::caution 注意 如果在之前的[创建项目](./application.md)章节中手动创建了相关文件,那么你需要做出如下修改: 1. 在项目目录中创建一个两层文件夹 `awesome_bot/plugins` ```tree title=Project 📦 awesome-bot ├── 📂 awesome_bot │ └── 📂 plugins └── 📜 bot.py ``` 2. 修改 `bot.py` 文件中的加载插件部分,取消注释或者添加如下代码 ```python title=bot.py # 在这里加载插件 nonebot.load_builtin_plugins("echo") # 内置插件 nonebot.load_plugins("awesome_bot/plugins") # 本地插件 ``` ::: 创建插件可以通过 `nb-cli` 命令从完整模板创建,也可以手动新建空白文件。通过以下命令创建一个名为 `weather` 的插件: ```bash $ nb plugin create [?] 插件名称: weather [?] 使用嵌套插件? (y/N) N [?] 请输入插件存储位置: awesome_bot/plugins ``` `nb-cli` 会在 `awesome_bot/plugins` 目录下创建一个名为 `weather` 的文件夹,其中包含的文件将在稍后章节中用到。 ```tree title=Project 📦 awesome-bot ├── 📂 .venv ├── 📂 awesome_bot │ └── 📂 plugins | └── 📂 weather | ├── 📜 __init__.py | └── 📜 config.py ├── 📜 .env.prod ├── 📜 pyproject.toml └── 📜 README.md ``` ## 加载插件 :::danger 警告 请勿在插件被加载前 `import` 插件模块,这会导致 NoneBot 无法将其转换为插件而出现意料之外的情况。 ::: 加载插件是在机器人入口文件中完成的,需要在框架初始化之后,运行之前进行。 请注意,加载的插件模块名称(插件文件名或文件夹名)**不能相同**,且每一个插件**只能被加载一次**,重复加载将会导致异常。 如果你使用 `nb-cli` 管理插件,那么你可以跳过这一节,`nb-cli` 将会自动处理加载。 如果你**使用自定义的入口文件** `bot.py`,那么你需要在 `bot.py` 中加载插件。 ```python {5} title=bot.py import nonebot nonebot.init() # 加载插件 nonebot.run() ``` 加载插件的方式有多种,但在底层的加载逻辑是一致的。以下是为加载插件提供的几种方式: ### `load_plugin` 通过点分割模块名称或使用 [`pathlib`](https://docs.python.org/zh-cn/3/library/pathlib.html) 的 `Path` 对象来加载插件。通常用于加载第三方插件或者项目插件。例如: ```python from pathlib import Path nonebot.load_plugin("path.to.your.plugin") # 加载第三方插件 nonebot.load_plugin(Path("./path/to/your/plugin.py")) # 加载项目插件 ``` :::caution 注意 请注意,本地插件的路径应该为相对机器人 **入口文件(通常为 bot.py)** 可导入的,例如在项目 `plugins` 目录下。 ::: ### `load_plugins` 加载传入插件目录中的所有插件,通常用于加载一系列本地编写的项目插件。例如: ```python nonebot.load_plugins("src/plugins", "path/to/your/plugins") ``` :::caution 注意 请注意,插件目录应该为相对机器人 **入口文件(通常为 bot.py)** 可导入的,例如在项目 `plugins` 目录下。 ::: ### `load_all_plugins` 这种加载方式是以上两种方式的混合,加载所有传入的插件模块名称,以及所有给定目录下的插件。例如: ```python nonebot.load_all_plugins(["path.to.your.plugin"], ["path/to/your/plugins"]) ``` ### `load_from_json` 通过 JSON 文件加载插件,是 [`load_all_plugins`](#load_all_plugins) 的 JSON 变种。通过读取 JSON 文件中的 `plugins` 字段和 `plugin_dirs` 字段进行加载。例如: ```json title=plugin_config.json { "plugins": ["path.to.your.plugin"], "plugin_dirs": ["path/to/your/plugins"] } ``` ```python nonebot.load_from_json("plugin_config.json", encoding="utf-8") ``` :::tip 提示 如果 JSON 配置文件中的字段无法满足你的需求,可以使用 [`load_all_plugins`](#load_all_plugins) 方法自行读取配置来加载插件。 ::: ### `load_from_toml` 通过 TOML 文件加载插件,是 [`load_all_plugins`](#load_all_plugins) 的 TOML 变种。通过读取 TOML 文件中的 `[tool.nonebot]` Table 中的 `plugin_dirs` Array 与 `[tool.nonebot.plugins]` Table 中的多个 Array 进行加载。例如: ```toml title=plugin_config.toml [tool.nonebot] plugin_dirs = ["path/to/your/plugins"] [tool.nonebot.plugins] "@local" = ["path.to.your.plugin"] # 本地插件等非插件商店来源的插件 "nonebot-plugin-someplugin" = ["nonebot_plugin_someplugin"] # 插件商店来源的插件 ``` ```python nonebot.load_from_toml("plugin_config.toml", encoding="utf-8") ``` :::tip 提示 如果 TOML 配置文件中的字段无法满足你的需求,可以使用 [`load_all_plugins`](#load_all_plugins) 方法自行读取配置来加载插件。 ::: ### `load_builtin_plugin` 加载一个内置插件,传入的插件名必须为 NoneBot 内置插件。该方法是 [`load_plugin`](#load_plugin) 的封装。例如: ```python nonebot.load_builtin_plugin("echo") ``` ### `load_builtin_plugins` 加载传入插件列表中的所有内置插件。例如: ```python nonebot.load_builtin_plugins("echo", "single_session") ``` ### 其他加载方式 有关其他插件加载的方式,可参考[跨插件访问](../advanced/requiring.md)和[嵌套插件](../advanced/plugin-nesting.md)。 ================================================ FILE: website/versioned_docs/version-2.4.2/tutorial/event-data.mdx ================================================ --- sidebar_position: 6 description: 通过依赖注入获取所需事件信息 options: menu: - category: tutorial weight: 80 --- # 获取事件信息 import Messenger from "@site/src/components/Messenger"; 在 NoneBot 事件处理流程中,获取事件信息并做出对应的操作是非常常见的场景。本章节中我们将介绍如何通过**依赖注入**获取事件信息。 ## 认识依赖注入 在事件处理流程中,事件响应器具有自己独立的上下文,例如:当前响应的事件、收到事件的机器人或者其他处理流程中新增的信息等。这些数据可以根据我们的需求,通过依赖注入的方式,在执行事件处理流程中注入到事件处理函数中。 相对于传统的信息获取方法,通过依赖注入获取信息的最大特色在于**按需获取**。如果该事件处理函数不需要任何额外信息即可运行,那么可以不进行依赖注入。如果事件处理函数需要额外的数据,可以通过依赖注入的方式灵活的标注出需要的依赖,在函数运行时便会被按需注入。 ## 使用依赖注入 使用依赖注入获取上下文信息的方法十分简单,我们仅需要在函数的参数中声明所需的依赖,并正确的将函数添加为事件处理依赖即可。在 NoneBot 中,我们可以直接使用 `nonebot.params` 模块中定义的参数类型来声明依赖。 例如,我们可以继续改进上一章节中的 `weather` 插件,使其可以获取到 `天气` 命令的地名参数,并根据地名返回天气信息。 ```python {9,11} title=weather/__init__.py from nonebot import on_command from nonebot.rule import to_me from nonebot.adapters import Message from nonebot.params import CommandArg weather = on_command("天气", rule=to_me(), aliases={"weather", "查天气"}, priority=10, block=True) @weather.handle() async def handle_function(args: Message = CommandArg()): # 提取参数纯文本作为地名,并判断是否有效 if location := args.extract_plain_text(): await weather.finish(f"今天{location}的天气是...") else: await weather.finish("请输入地名") ``` 如上方示例所示,我们使用了 `args` 作为注入参数名,注入的内容为 `CommandArg()`,也就是**消息命令后跟随的内容**。在这个示例中,我们获得的参数会被检查是否有效,对无效参数则会结束事件。 :::tip 提示 命令与参数之间可以不需要空格,`CommandArg()` 获取的信息为命令后跟随的内容并去除了头部空白符。例如:`/天气 上海` 消息的参数为 `上海`。 ::: :::tip 提示 `:=` 是 Python 3.8 引入的新语法 [Assignment Expressions](https://docs.python.org/zh-cn/3/reference/expressions.html#assignment-expressions),也称为海象表达式,可以在表达式中直接赋值。 ::: NoneBot 提供了多种依赖注入类型,可以获取不同的信息,具体内容可参考[依赖注入](../advanced/dependency.mdx)。 ================================================ FILE: website/versioned_docs/version-2.4.2/tutorial/fundamentals.md ================================================ --- sidebar_position: 1 description: NoneBot 机器人构成及基本使用 options: menu: - category: tutorial weight: 30 --- # 机器人的构成 了解机器人的基本构成有助于你更好地使用 NoneBot,本章节将介绍 NoneBot 中的基本组成部分,稍后的文档中将会使用到这些概念。 使用 NoneBot 框架搭建的机器人具有以下几个基本组成部分: 1. NoneBot 机器人框架主体:负责连接各个组成部分,提供基本的机器人功能 2. 驱动器 `Driver`:客户端/服务端的功能实现,负责接收和发送消息(通常为 HTTP 通信) 3. 适配器 `Adapter`:驱动器的上层,负责将**平台消息**与 NoneBot 事件/操作系统的消息格式相互转换 4. 插件 `Plugin`:机器人的功能实现,通常为负责处理事件并进行一系列的操作 除 NoneBot 机器人框架主体外,其他部分均可按需选择、互相搭配,但由于平台的兼容性问题,部分插件可能仅在某些特定平台上可用(这由插件编写者决定)。 在接下来的章节中,我们将重点介绍机器人功能实现,即插件 `Plugin` 部分。 ================================================ FILE: website/versioned_docs/version-2.4.2/tutorial/handler.mdx ================================================ --- sidebar_position: 5 description: 处理接收到的特定事件 options: menu: - category: tutorial weight: 70 --- # 事件处理 import Messenger from "@site/src/components/Messenger"; 在我们收到事件,并被某个事件响应器正确响应后,便正式开启了对于这个事件的**处理流程**。 ## 认识事件处理流程 就像我们在解决问题时需要遵循流程一样,处理一个事件也需要一套流程。在事件响应器对一个事件进行响应之后,会依次执行一系列的**事件处理依赖**(通常是函数)。简单来说,事件处理流程并不是一个函数、一个对象或一个方法,而是一整套由开发者设计的流程。 在这个流程中,我们**目前**只需要了解两个概念:函数形式的“事件处理依赖”(下称“事件处理函数”)和“事件响应器操作”。 ## 事件处理函数 在事件响应器中,事件处理流程可以由一个或多个“事件处理函数”组成,这些事件处理函数将会按照顺序依次对事件进行处理,直到全部执行完成或被中断。我们可以采用事件响应器的“事件处理函数装饰器”来添加这些“事件处理函数”。 顾名思义,“事件处理函数装饰器”是一个[装饰器(decorator)](https://docs.python.org/zh-cn/3/glossary.html#term-decorator),那么它的使用方法也同[函数定义](https://docs.python.org/zh-cn/3/reference/compound_stmts.html#function-definitions)中所展示的包装用法相同。例如: ```python {6-8} title=weather/__init__.py from nonebot import on_command from nonebot.rule import to_me weather = on_command("天气", rule=to_me(), aliases={"weather", "查天气"}, priority=10, block=True) @weather.handle() async def handle_function(): pass # do something here ``` 如上方示例所示,我们使用 `weather` 响应器的 `handle` 装饰器装饰了一个函数 `handle_function`。`handle_function` 函数会被添加到 `weather` 的事件处理流程中。在 `weather` 响应器被触发之后,将会依次调用 `weather` 响应器的事件处理函数,即 `handle_function` 来对事件进行处理。 ## 事件响应器操作 在事件处理流程中,我们可以使用事件响应器操作来进行一些交互或改变事件处理流程,例如向机器人用户发送消息或提前结束事件处理流程等。 事件响应器操作与事件处理函数装饰器类似,通常作为事件响应器 `Matcher` 的[类方法](https://docs.python.org/zh-cn/3/library/functions.html#classmethod)存在,因此事件响应器操作的调用方法也是 `Matcher.func()` 的形式。不过不同的是,事件响应器操作并不是装饰器,因此并不需要@进行标注。 ```python {8,9} title=weather/__init__.py from nonebot import on_command from nonebot.rule import to_me weather = on_command("天气", rule=to_me(), aliases={"weather", "查天气"}, priority=10, block=True) @weather.handle() async def handle_function(): # await weather.send("天气是...") await weather.finish("天气是...") ``` 如上方示例所示,我们使用 `weather` 响应器的 `finish` 操作方法向机器人用户回复了 `天气是...` 并结束了事件处理流程。效果如下: 值得注意的是,在执行 `finish` 方法时,NoneBot 会在向机器人用户发送消息内容后抛出 `FinishedException` 异常来结束事件响应流程。也就是说,在 `finish` 被执行后,后续的程序是不会被执行的。如果你需要回复机器人用户消息但不想事件处理流程结束,可以使用注释的部分中展示的 `send` 方法。 :::danger 警告 由于 `finish` 是通过抛出 `FinishedException` 异常来结束事件的,因此异常可能会被未加限制的 `try-except` 捕获,影响事件处理流程正确处理,导致无法正常结束此事件。请务必在异常捕获中指定错误类型或排除所有 [MatcherException](../api/exception.md#MatcherException) 类型的异常(如下所示),或将 `finish` 移出捕获范围进行使用。 ```python from nonebot.exception import MatcherException try: await weather.finish("天气是...") except MatcherException: raise except Exception as e: pass # do something here ``` ::: 目前 NoneBot 提供了多种事件响应器操作,其中包括用于机器人用户交互与流程控制两大类,进阶使用方法可以查看[会话控制](../appendices/session-control.mdx)。 ================================================ FILE: website/versioned_docs/version-2.4.2/tutorial/matcher.md ================================================ --- sidebar_position: 4 description: 响应接收到的特定事件 options: menu: - category: tutorial weight: 60 --- # 事件响应器 事件响应器(Matcher)是对接收到的事件进行响应的基本单元,所有的事件响应器都继承自 `Matcher` 基类。 在 NoneBot 中,事件响应器可以通过一系列特定的规则**筛选**出**具有某种特征的事件**,并按照**特定的流程**交由**预定义的事件处理依赖**进行处理。例如,在[快速上手](../quick-start.mdx)中,我们使用了内置插件 `echo` ,它定义的事件响应器能响应机器人用户发送的“/echo hello world”消息,提取“hello world”信息并作为回复消息发送。 ## 事件响应器辅助函数 NoneBot 中所有事件响应器均继承自 `Matcher` 基类,但直接使用 `Matcher.new()` 方法创建事件响应器过于繁琐且不能记录插件信息。因此,NoneBot 中提供了一系列“事件响应器辅助函数”(下称“辅助函数”)来辅助我们用**最简的方式**创建**带有不同规则预设**的事件响应器,提高代码可读性和书写效率。通常情况下,我们只需要使用辅助函数即可完成事件响应器的创建。 在 NoneBot 中,辅助函数以 `on()` 或 `on_()` 形式出现(例如 `on_command()`),调用后根据不同的参数返回一个 `Type[Matcher]` 类型的新事件响应器。 目前 NoneBot 提供了多种功能各异的辅助函数、具有共同命令名称前缀的命令组以及具有共同参数的响应器组,均可以从 `nonebot` 模块直接导入使用,具体内容参考[事件响应器进阶](../advanced/matcher.md)。 ## 创建事件响应器 在上一节[创建插件](./create-plugin.md#创建插件)中,我们创建了一个 `weather` 插件,现在我们来实现他的功能。 我们直接使用 `on_command()` 辅助函数来创建一个事件响应器: ```python {3} title=weather/__init__.py from nonebot import on_command weather = on_command("天气") ``` 这样,我们就获得一个名为 `weather` 的事件响应器了,这个事件响应器会对 `/天气` 开头的消息进行响应。 :::tip 提示 如果一条消息中包含“@机器人”或以“机器人的昵称”开始,例如 `@bot /天气` 时,协议适配器会将 `event.is_tome()` 判断为 `True` ,同时也会自动去除 `@bot`,即事件响应器收到的信息内容为 `/天气`,方便进行命令匹配。 ::: ### 为事件响应器添加参数 在辅助函数中,我们可以添加一些参数来对事件响应器进行更加精细的调整,例如事件响应器的优先级、匹配规则等。例如: ```python {4} title=weather/__init__.py from nonebot import on_command from nonebot.rule import to_me weather = on_command("天气", rule=to_me(), aliases={"weather", "查天气"}, priority=10, block=True) ``` 这样,我们就获得了一个可以响应 `天气`、`weather`、`查天气` 三个命令的响应规则,需要私聊或 `@bot` 时才会响应,优先级为 10(越小越优先),阻断事件向后续优先级传播的事件响应器了。这些内容的意义和使用方法将会在后续的章节中一一介绍。 :::tip 提示 需要注意的是,不同的辅助函数有不同的可选参数,在使用之前可以参考[事件响应器进阶 - 基本辅助函数](../advanced/matcher.md#基本辅助函数)或 [API 文档](../api/plugin/on.md#on)。 ::: ================================================ FILE: website/versioned_docs/version-2.4.2/tutorial/message.md ================================================ --- sidebar_position: 7 description: 处理消息序列与消息段 options: menu: - category: tutorial weight: 90 --- # 处理消息 在不同平台中,一条消息可能会有承载有各种不同的表现形式,它可能是一段纯文本、一张图片、一段语音、一篇富文本文章,也有可能是多种类型的组合等等。 在 NoneBot 中,为确保消息的正常处理与跨平台兼容性,采用了扁平化的消息序列形式,即 `Message` 对象。消息序列是 NoneBot 中的消息载体,无论是接收还是发送的消息,都采用消息序列的形式进行处理。 ## 认识消息类型 ### 消息序列 `Message` 在 NoneBot 中,消息序列 `Message` 的主要作用是用于表达“一串消息”。由于消息序列继承自 `List[MessageSegment]`,所以 `Message` 的本质是由若干消息段所组成的序列。因此,消息序列的使用方法与 `List` 有很多相似之处,例如切片、索引、拼接等。 在上一节的[使用依赖注入](./event-data.mdx#使用依赖注入)中,我们已经通过依赖注入 `CommandArg()` 获取了命令的参数,它的类型即是消息序列。我们使用了消息序列的 `extract_plain_text()` 方法来获取消息序列中的纯文本内容。 ### 消息段 `MessageSegment` 顾名思义,消息段 `MessageSegment` 是一段消息。由于消息序列的本质是由若干消息段所组成的序列,消息段可以被认为是构成消息序列的最小单位。简单来说,消息序列类似于一个自然段,而消息段则是组成自然段的一句话。同时,作为特殊消息载体的存在,绝大多数的平台都有着**独特的消息类型**,这些独特的内容均需要由对应的**协议适配器**所提供,以适应不同平台中的消息模式。**这也意味着,你需要导入对应的协议适配器中的消息序列和消息段后才能使用其特殊的工厂方法。** :::caution 注意 消息段的类型是由协议适配器提供的,因此你需要参考协议适配器的文档并导入对应的消息段后才能使用其特殊的消息类型。 在上一节的[使用依赖注入](./event-data.mdx#使用依赖注入)中,我们导入的为 `nonebot.adapters.Message` 抽象基类,因此我们无法使用平台特有的消息类型。仅能使用 `str` 作为纯文本消息回复。 ::: ## 使用消息序列 :::caution 注意 在以下的示例中,为了更好的理解多种类型的消息组成方式,我们将使用 `Console` 协议适配器来演示消息序列的使用方法。在实际使用中,你需要确保你使用的**消息序列类型**与你所要发送的**平台类型**一致。 ::: 通常情况下,适配器在接收到消息时,会将消息转换为消息序列,可以通过依赖注入 [`EventMessage`](../advanced/dependency.mdx#eventmessage),或者使用 `event.get_message()` 获取。 由于消息序列是 `List[MessageSegment]` 的子类,所以你总是可以用和操作 `List` 类似的方式来处理消息序列。例如: ```python >>> from nonebot.adapters.console import Message, MessageSegment >>> message = Message([ MessageSegment(type="text", data={"text":"hello"}), MessageSegment(type="markdown", data={"markup":"**world**"}), ]) >>> for segment in message: ... print(segment.type, segment.data) ... text {'text': 'hello'} markdown {'markup': '**world**'} >>> len(message) 2 ``` ### 构造消息序列 在使用事件响应器操作发送消息时,既可以使用 `str` 作为消息,也可以使用 `Message`、`MessageSegment` 或者 `MessageTemplate`。那么,我们就需要先构造一个消息序列。消息序列可以通过多种方式构造: #### 直接构造 `Message` 类可以直接实例化,支持 `str`、`MessageSegment`、`Iterable[MessageSegment]` 或适配器自定义类型的参数。 ```python from nonebot.adapters.console import Message, MessageSegment # str Message("Hello, world!") # MessageSegment Message(MessageSegment.text("Hello, world!")) # List[MessageSegment] Message([MessageSegment.text("Hello, world!")]) ``` #### 运算构造 `Message` 对象可以通过 `str`、`MessageSegment` 相加构造,详情请参考[拼接消息](#拼接消息)。 #### 从字典数组构造 `Message` 对象支持 Pydantic 自定义类型构造,可以使用 Pydantic 的 `TypeAdapter` 方法进行构造。 ```python from pydantic import TypeAdapter from nonebot.adapters.console import Message, MessageSegment # 由字典构造消息段 TypeAdapter(MessageSegment).validate_python( {"type": "text", "data": {"text": "text"}} ) == MessageSegment.text("text") # 由字典数组构造消息序列 TypeAdapter(Message).validate_python( [MessageSegment.text("text"), {"type": "text", "data": {"text": "text"}}], ) == Message([MessageSegment.text("text"), MessageSegment.text("text")]) ``` ### 获取消息纯文本 由于消息中存在各种类型的消息段,因此 `str(message)` 通常**不能得到消息的纯文本**,而是一个消息序列的字符串表示。 NoneBot 为消息段定义了一个方法 `is_text()` ,可以用于判断消息段是否为纯文本;也可以使用 `message.extract_plain_text()` 方法获取消息纯文本。 ```python from nonebot.adapters.console import Message, MessageSegment # 判断消息段是否为纯文本 MessageSegment.text("text").is_text() == True # 提取消息纯文本字符串 Message( [MessageSegment.text("text"), MessageSegment.markdown("**markup**")] ).extract_plain_text() == "text" ``` ### 遍历 消息序列继承自 `List[MessageSegment]` ,因此可以使用 `for` 循环遍历消息段。 ```python for segment in message: ... ``` ### 比较 消息和消息段都可以使用 `==` 或 `!=` 运算符比较是否相同。 ```python MessageSegment.text("text") != MessageSegment.text("foo") some_message == Message([MessageSegment.text("text")]) ``` ### 检查消息段 我们可以通过 `in` 运算符或消息序列的 `has` 方法来: ```python # 是否存在消息段 MessageSegment.text("text") in message # 是否存在指定类型的消息段 "text" in message ``` 我们还可以使用消息序列的 `only` 方法来检查消息中是否仅包含指定的消息段。 ```python # 是否都为指定消息段 message.only(MessageSegment.text("test")) # 是否仅包含指定类型的消息段 message.only("text") ``` ### 过滤、索引与切片 消息序列对列表的索引与切片进行了增强,在原有列表 `int` 索引与 `slice` 切片的基础上,支持 `type` 过滤索引与切片。 ```python from nonebot.adapters.console import Message, MessageSegment message = Message( [ MessageSegment.text("test"), MessageSegment.markdown("test2"), MessageSegment.markdown("test3"), MessageSegment.text("test4"), ] ) # 索引 message[0] == MessageSegment.text("test") # 切片 message[0:2] == Message( [MessageSegment.text("test"), MessageSegment.markdown("test2")] ) # 类型过滤 message["markdown"] == Message( [MessageSegment.markdown("test2"), MessageSegment.markdown("test3")] ) # 类型索引 message["markdown", 0] == MessageSegment.markdown("test2") # 类型切片 message["markdown", 0:2] == Message( [MessageSegment.markdown("test2"), MessageSegment.markdown("test3")] ) ``` 我们也可以通过消息序列的 `include`、`exclude` 方法进行类型过滤。 ```python message.include("text", "markdown") message.exclude("text") ``` 同样的,消息序列对列表的 `index`、`count` 方法也进行了增强,可以用于索引指定类型的消息段。 ```python # 指定类型首个消息段索引 message.index("markdown") == 1 # 指定类型消息段数量 message.count("markdown") == 2 ``` 此外,消息序列添加了一个 `get` 方法,可以用于获取指定类型指定个数的消息段。 ```python # 获取指定类型指定个数的消息段 message.get("markdown", 1) == Message([MessageSegment.markdown("test2")]) ``` ### 拼接消息 `str`、`Message`、`MessageSegment` 对象之间可以直接相加,相加均会返回一个新的 `Message` 对象。 ```python # 消息序列与消息段相加 Message([MessageSegment.text("text")]) + MessageSegment.text("text") # 消息序列与字符串相加 Message([MessageSegment.text("text")]) + "text" # 消息序列与消息序列相加 Message([MessageSegment.text("text")]) + Message([MessageSegment.text("text")]) # 字符串与消息序列相加 "text" + Message([MessageSegment.text("text")]) # 消息段与消息段相加 MessageSegment.text("text") + MessageSegment.text("text") # 消息段与字符串相加 MessageSegment.text("text") + "text" # 消息段与消息序列相加 MessageSegment.text("text") + Message([MessageSegment.text("text")]) # 字符串与消息段相加 "text" + MessageSegment.text("text") ``` 如果需要在当前消息序列后直接拼接新的消息段,可以使用 `Message.append`、`Message.extend` 方法,或者使用自加。 ```python msg = Message([MessageSegment.text("text")]) # 自加 msg += "text" msg += MessageSegment.text("text") msg += Message([MessageSegment.text("text")]) # 附加 msg.append("text") msg.append(MessageSegment.text("text")) # 扩展 msg.extend([MessageSegment.text("text")]) ``` 我们也可以通过消息段或消息序列的 `join` 方法来拼接一串消息: ```python seg = MessageSegment.text("text") msg = seg.join( [ MessageSegment.text("first"), Message( [ MessageSegment.text("second"), MessageSegment.text("third"), ] ) ] ) msg == Message( [ MessageSegment.text("first"), MessageSegment.text("text"), MessageSegment.text("second"), MessageSegment.text("third"), ] ) ``` ### 使用消息模板 为了提供安全可靠的跨平台模板字符,我们提供了一个消息模板功能来构建消息序列 它在以下常见场景中尤其有用: - 多行富文本编排(包含图片,文字以及表情等) - 客制化(由 Bot 最终用户提供消息模板时) 在事实上,它的用法和 `str.format` 极为相近,所以你在使用的时候,总是可以参考[Python 文档](https://docs.python.org/zh-cn/3/library/stdtypes.html#str.format)来达到你想要的效果,这里给出几个简单的例子。 默认情况下,消息模板采用 `str` 纯文本形式的格式化: ```python title=基础格式化用法 >>> from nonebot.adapters import MessageTemplate >>> MessageTemplate("{} {}").format("hello", "world") 'hello world' ``` 如果 `Message.template` 构建消息模板,那么消息模板将采用消息序列形式的格式化,此时的消息将会是平台特定的: :::caution 注意 使用 `Message.template` 构建消息模板时,应注意消息序列为平台适配器提供的类型,不能使用 `nonebot.adapters.Message` 基类作为模板构建。使用基类构建模板与使用 `str` 构建模板的效果是一样的,因此请使用上述的 `MessageTemplate` 类直接构建模板。: ::: ```python title=平台格式化用法 >>> from nonebot.adapters.console import Message, MessageSegment >>> Message.template("{} {}").format("hello", "world") Message( MessageSegment.text("hello"), MessageSegment.text(" "), MessageSegment.text("world") ) ``` 消息模板支持使用消息段进行格式化: ```python title=对消息段进行安全的拼接 >>> from nonebot.adapters.console import Message, MessageSegment >>> Message.template("{}{}").format(MessageSegment.markdown("**markup**"), "world") Message( MessageSegment(type='markdown', data={'markup': '**markup**'}), MessageSegment(type='text', data={'text': 'world'}) ) ``` 消息模板同样支持使用消息序列作为模板: ```python title=以消息对象作为模板 >>> from nonebot.adapters.console import Message, MessageSegment >>> Message.template( ... MessageSegment.text("{user_id}") + MessageSegment.emoji("tada") + ... MessageSegment.text("{message}") ... ).format_map({"user_id": 123456, "message": "hello world"}) Message( MessageSegment(type='text', data={'text': '123456'}), MessageSegment(type='emoji', data={'emoji': 'tada'}), MessageSegment(type='text', data={'text': 'hello world'}) ) ``` :::caution 注意 只有消息序列中的文本类型消息段才能被格式化,其他类型的消息段将会原样添加。 ::: 消息模板支持使用拓展控制符来控制消息段类型: ```python title=使用消息段的拓展控制符 >>> from nonebot.adapters.console import Message, MessageSegment >>> Message.template("{name:emoji}").format(name='tada') Message(MessageSegment(type='emoji', data={'name': 'tada'})) ``` ================================================ FILE: website/versioned_docs/version-2.4.2/tutorial/store.mdx ================================================ --- sidebar_position: 2 description: 从商店安装适配器和插件 options: menu: - category: tutorial weight: 40 --- # 获取商店内容 import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; import Asciinema from "@site/src/components/Asciinema"; :::tip 提示 如果你暂时没有获取商店内容的需求,可以跳过本章节。 ::: NoneBot 提供了一个[商店](/store/plugins),商店内容均由社区开发者贡献。你可以在商店中查找你需要的适配器和插件等,进行安装或者参考其文档等。 商店中每个内容的卡片都包含了其名称和简介等信息,点击**卡片右上角**链接图标即可跳转到其主页。 ## 安装插件 在商店插件页面中,点击你需要安装的插件下方的 `点击复制安装命令` 按钮,即可复制 `nb-cli` 命令。 请在你的**项目目录**下执行该命令。`nb-cli` 会自动安装插件并将其添加到加载列表中。 ```bash nb plugin install <插件名称> ``` ```bash $ nb plugin install [?] 想要安装的插件名称: <插件名称> ``` ```bash pip install <插件包名> ``` 插件包名可以在商店插件卡片中找到,或者使用 `nb-cli` 搜索插件显示的详情中找到。安装完成后,需要参考[加载插件章节](./create-plugin.md#加载插件)自行加载。 如果想要查看插件列表,可以使用以下命令 ```bash # 列出商店所有插件 nb plugin list # 搜索商店插件 nb plugin search [可选关键词] ``` 升级和卸载插件可以使用以下命令 ```bash nb plugin update <插件名称> nb plugin uninstall <插件名称> ``` ```bash $ nb plugin update [?] 想要安装的插件名称: <插件名称> $ nb plugin uninstall [?] 想要卸载的插件名称: <插件名称> ``` ```bash pip install --upgrade <插件包名> pip uninstall <插件包名> ``` 插件包名可以在商店插件卡片中找到,或者使用 `nb-cli` 搜索插件显示的详情中找到。卸载完成后,需要自行移除插件加载。 ## 安装适配器 安装适配器与安装插件类似,只是将命令换为 `nb adapter`,这里就不再赘述。 请在你的**项目目录**下执行该命令。`nb-cli` 会自动安装适配器并将其添加到注册列表中。 ```bash nb adapter install <适配器名称> ``` ```bash $ nb adapter install [?] 想要安装的适配器名称: <适配器名称> ``` ```bash pip install <适配器包名> ``` 适配器包名可以在商店适配器卡片中找到,或者使用 `nb-cli` 搜索适配器显示的详情中找到。安装完成后,需要参考[注册适配器章节](../advanced/adapter.md#注册适配器)自行注册。 如果想要查看适配器列表,可以使用以下命令 ```bash # 列出商店所有适配器 nb adapter list # 搜索商店适配器 nb adapter search [可选关键词] ``` 升级和卸载适配器可以使用以下命令 ```bash nb adapter update <适配器名称> nb adapter uninstall <适配器名称> ``` ```bash $ nb adapter update [?] 想要安装的适配器名称: <适配器名称> $ nb adapter uninstall [?] 想要卸载的适配器名称: <适配器名称> ``` ```bash pip install --upgrade <适配器包名> pip uninstall <适配器包名> ``` 适配器包名可以在商店适配器卡片中找到,或者使用 `nb-cli` 搜索适配器显示的详情中找到。卸载完成后,需要自行移除适配器加载。 ## 安装驱动器 安装驱动器与安装插件同样类似,只是将命令换为 `nb driver`,这里就不再赘述。 如果你使用了虚拟环境,请在你的**项目目录**下执行该命令,`nb-cli` 会自动安装驱动器到虚拟环境中。 请注意 `nb-cli` 并不会在安装驱动器后修改项目所使用的驱动器,请自行参考[配置方法](../appendices/config.mdx)章节以及 [`DRIVER` 配置项](../appendices/config.mdx#driver)修改驱动器。 ```bash nb driver install <驱动器名称> ``` ```bash $ nb driver install [?] 想要安装的驱动器名称: <驱动器名称> ``` ```bash pip install <驱动器包名> ``` 驱动器包名可以在商店驱动器卡片中找到,或者使用 `nb-cli` 搜索驱动器显示的详情中找到。 如果想要查看驱动器列表,可以使用以下命令 ```bash # 列出商店所有驱动器 nb driver list # 搜索商店驱动器 nb driver search [可选关键词] ``` 升级和卸载驱动器可以使用以下命令 ```bash nb driver update <驱动器名称> nb driver uninstall <驱动器名称> ``` ```bash $ nb driver update [?] 想要安装的驱动器名称: <驱动器名称> $ nb driver uninstall [?] 想要卸载的驱动器名称: <驱动器名称> ``` ```bash pip install --upgrade <驱动器包名> pip uninstall <驱动器包名> ``` 驱动器包名可以在商店驱动器卡片中找到,或者使用 `nb-cli` 搜索驱动器显示的详情中找到。卸载完成后,需要自行移除适配器加载。 ================================================ FILE: website/versioned_docs/version-2.4.3/README.md ================================================ --- sidebar_position: 0 id: index slug: / --- # 概览 NoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架(下称 NoneBot),它基于 Python 的类型注解和异步优先特性(兼容同步),能够为你的需求实现提供便捷灵活的支持。同时,NoneBot 拥有大量的开发者为其开发插件,用户无需编写任何代码,仅需完成环境配置及插件安装,就可以正常使用 NoneBot。 需要注意的是,NoneBot 仅支持 **Python 3.9 以上版本** ## 特色 ### 异步优先 NoneBot 基于 Python [asyncio](https://docs.python.org/zh-cn/3/library/asyncio.html) / [trio](https://trio.readthedocs.io/en/stable/) 编写,并在异步机制的基础上进行了一定程度的同步函数兼容。 ### 完整的类型注解 NoneBot 参考 [PEP 484](https://www.python.org/dev/peps/pep-0484/) 等 PEP 完整实现了类型注解,通过 Pyright(Pylance) 检查。配合编辑器的类型推导功能,能将绝大多数的 Bug 杜绝在编辑器中([编辑器支持](./editor-support))。 ### 开箱即用 NoneBot 提供了使用便捷、具有交互式功能的命令行工具--`nb-cli`,使得用户初次接触 NoneBot 时更容易上手。使用方法请阅读本文档[指南](./quick-start.mdx)以及 [CLI 文档](https://cli.nonebot.dev/)。 ### 插件系统 插件系统是 NoneBot 的核心,通过它可以实现机器人的模块化以及功能扩展,便于维护和管理。 ### 依赖注入系统 NoneBot 采用了一套自行定义的依赖注入系统,可以让事件的处理过程更加的简洁、清晰,增加代码的可读性,减少代码冗余。 #### 什么是依赖注入 [**『依赖注入』**](https://zh.wikipedia.org/wiki/%E6%8E%A7%E5%88%B6%E5%8F%8D%E8%BD%AC)意思是,在编程中,有一种方法可以让你的代码声明它工作和使用所需要的东西,即**『依赖』**。 系统(在这里是指 NoneBot)将负责做任何需要的事情,为你的代码提供这些必要依赖(即**『注入』**依赖性) 这在你有以下情形的需求时非常有用: - 这部分代码拥有共享的逻辑(同样的代码逻辑多次重复) - 共享数据库以及网络请求连接会话 - 比如 `httpx.AsyncClient`、`aiohttp.ClientSession` 和 `sqlalchemy.Session` - 机器人用户权限检查以及认证 - 还有更多... 它在完成上述工作的同时,还能尽量减少代码的耦合和重复 ================================================ FILE: website/versioned_docs/version-2.4.3/advanced/adapter.md ================================================ --- sidebar_position: 1 description: 注册适配器与指定平台交互 options: menu: - category: advanced weight: 20 --- # 使用适配器 适配器 (Adapter) 是机器人与平台交互的核心桥梁,它负责在驱动器和机器人插件之间转换与传递消息。 ## 适配器功能与组成 适配器通常有两种功能,分别是**接收事件**和**调用平台接口**。其中,接收事件是指将驱动器收到的事件消息转换为 NoneBot 定义的事件模型,然后交由机器人插件处理;调用平台接口是指将机器人插件调用平台接口的数据转换为平台指定的格式,然后交由驱动器发送,并接收接口返回数据。 为了实现这两种功能,适配器通常由四个部分组成: - **Adapter**:负责转换事件和调用接口,正确创建 Bot 对象并注册到 NoneBot 中。 - **Bot**:负责存储平台机器人相关信息,并提供回复事件的方法。 - **Event**:负责定义事件内容,以及事件主体对象。 - **Message**:负责正确序列化消息,以便机器人插件处理。 ## 注册适配器 在使用适配器之前,我们需要先将适配器注册到驱动器中,这样适配器就可以通过驱动器接收事件和调用接口了。我们以 Console 适配器为例,来看看如何注册适配器: ```python {2,5} title=bot.py import nonebot from nonebot.adapters.console import Adapter driver = nonebot.get_driver() driver.register_adapter(Adapter) ``` 我们首先需要从适配器模块中导入所需要的适配器类,然后通过驱动器的 `register_adapter` 方法将适配器注册到驱动器中即可。如果我们需要多平台支持,可以多次调用 `register_adapter` 方法来注册多个适配器。 ## 获取已注册的适配器 NoneBot 提供了 `get_adapter` 方法来获取已注册的适配器,我们可以通过适配器的名称或类型来获取指定的适配器实例: ```python import nonebot from nonebot.adapters.console import Adapter adapters = nonebot.get_adapters() console_adapter = nonebot.get_adapter(Adapter) console_adapter = nonebot.get_adapter(Adapter.get_name()) ``` ## 获取 Bot 对象 当前所有适配器已连接的 Bot 对象可以通过 `get_bots` 方法获取,这是一个以机器人 ID 为键的字典: ```python import nonebot bots = nonebot.get_bots() ``` 我们也可以通过 `get_bot` 方法获取指定 ID 的 Bot 对象。如果省略 ID 参数,将会返回所有 Bot 中的第一个: ```python import nonebot bot = nonebot.get_bot("bot_id") ``` 如果需要获取指定适配器连接的 Bot 对象,我们可以通过适配器的 `bots` 属性获取,这也是一个以机器人 ID 为键的字典: ```python import nonebot from nonebot.adapters.console import Adapter console_adapter = nonebot.get_adapter(Adapter) bots = console_adapter.bots ``` Bot 对象都具有一个 `self_id` 属性,它是机器人的唯一 ID,由适配器填写,通常为机器人的帐号 ID 或者 APP ID。 ## 获取事件通用信息 适配器的所有事件模型均继承自 `Event` 基类,在[事件类型与重载](../appendices/overload.md)一节中,我们也提到了如何使用基类抽象方法来获取事件通用信息。基类能提供如下信息: ### 事件类型 事件类型通常为 `meta_event`、`message`、`notice`、`request`。 ```python type: str = event.get_type() ``` ### 事件名称 事件名称由适配器定义,通常用于日志记录。 ```python name: str = event.get_event_name() ``` ### 事件描述 事件描述由适配器定义,通常用于日志记录。 ```python description: str = event.get_event_description() ``` ### 事件日志字符串 事件日志字符串由事件名称和事件描述组成,用于日志记录。 ```python log: str = event.get_log_string() ``` ### 事件主体 ID 事件主体 ID 通常为机器人用户 ID。 ```python user_id: str = event.get_user_id() ``` ### 事件会话 ID 事件会话 ID 通常为机器人用户 ID 与群聊/频道 ID 组合而成。 ```python session_id: str = event.get_session_id() ``` ### 事件消息 如果事件包含消息,则可以通过该方法获取,否则会产生异常。 ```python message: Message = event.get_message() ``` ### 事件纯文本消息 通常为事件消息的纯文本内容,如果事件不包含消息,则会产生异常。 ```python text: str = event.get_plaintext() ``` ### 事件是否与机器人有关 由适配器实现的判断,通常将事件目标主体为机器人、消息中包含“@机器人”或以“机器人的昵称”开始视为与机器人有关。 ```python is_tome: bool = event.is_tome() ``` ## 更多 官方支持的适配器和社区贡献的适配器均可在[商店](/store/adapters)中查看。如果你想要开发自己的适配器,可以参考[开发文档](../developer/adapter-writing.md)。欢迎通过商店发布你的适配器。 ================================================ FILE: website/versioned_docs/version-2.4.3/advanced/dependency.mdx ================================================ --- sidebar_position: 6 description: 通过依赖注入获取上下文信息 options: menu: - category: advanced weight: 70 --- # 依赖注入 import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; 在事件处理流程中,事件响应器具有自己独立的上下文,例如:当前的事件、机器人等信息。在 NoneBot 中,这些信息通过依赖注入的方式提供给事件处理函数,可以让代码更加整洁可读、提升复用能力。 在了解如何使用依赖注入获取上下文信息之前,我们需要先了解两个概念: - `Dependent`:使用依赖注入的函数或其他任意可调用对象。如:事件处理函数、自定义的依赖函数等。 - `Dependency`:依赖注入的对象。如:当前事件、机器人等。 在之前的文档中,我们已经多次使用了依赖注入来获取事件信息。通过对函数参数依照一定规则填写类型注解,即可获得想要的上下文信息。任何一个事件处理函数在添加到事件处理流程时,都会根据一定规则提前将其解析成一个 `Dependent` 对象,方便运行时进行注入。如果遇到无法解析的参数,将会抛出 `ValueError("Unknown parameter")` 的异常。整个依赖注入系统可以分为两部分: - 参数解析 - 依据一定规则解析函数参数,识别 `Dependency` 依赖。 - 生成 `Dependent` 对象。 - 执行 - 根据已经解析的 `Dependency` 依赖,执行调用。 - 将所有 `Dependency` 的返回值根据参数名传入并调用 `Dependent` 。 :::danger 警告 在依赖注入中,类型注解是非常重要的,因为它不仅可以决定依赖注入的对象,还可以触发[重载机制](../appendices/overload.md#重载)。如果类型注解与实际获得数据类型不一致,将会跳过当前 `Dependent` 对象(即事件处理函数)。 ::: :::tip 提示 如果对于依赖注入的解析流程有疑问,可以调整[日志等级配置项](../appendices/config.mdx#log-level)为 `TRACE`,查看依赖解析日志。 ::: ## 同步支持 对于依赖注入系统中的 `Dependent` 或者 `Dependency` 对象,均支持同步类型的函数或可调用对象。例如: ```python {6,10} from nonebot import on_command from nonebot.params import Depends matcher = on_command("foo") def dependency() -> str: return "something" @matcher.handle() def _(result: str = Depends(dependency)): ... ``` ## 非依赖参数 在依赖注入解析中,任何无法解析的参数如果带有默认值,将会被视为非依赖参数。这些参数在依赖运行时将不会被注入而使用函数默认值。例如: ```python async def _(foo: str = "bar"): ... ``` ## 类型依赖注入 这一类的依赖注入仅需要在函数参数中添加对应的类型注解即可。 ### Bot 获取当前事件的 Bot 对象。 通过标注参数为 `Bot` 类型,或者一系列 `Bot` 类型,即可获取到当前事件的 Bot 对象。为兼容性考虑,如果参数名为 `bot` 且无类型注解,也会视为 Bot 依赖注入。 Bot 依赖注入支持重载(即:可以标注参数为子类型)且具有[重载优先检查权](../appendices/overload.md#重载)。 ```python from nonebot.adapters import Bot from nonebot.adapters.console import Bot as ConsoleBot from nonebot.adapters.onebot.v11 import Bot as OneBotV11Bot async def _(foo: Bot): ... async def _(foo: ConsoleBot | OneBotV11Bot): ... async def _(bot): ... # 兼容性处理 ``` ```python from typing import Union from nonebot.adapters import Bot from nonebot.adapters.console import Bot as ConsoleBot from nonebot.adapters.onebot.v11 import Bot as OneBotV11Bot async def _(foo: Bot): ... async def _(foo: Union[ConsoleBot, OneBotV11Bot]): ... async def _(bot): ... # 兼容性处理 ``` ### Event 获取当前事件。 通过标注参数为 `Event` 类型,或者一系列 `Event` 类型,即可获取到当前事件。为兼容性考虑,如果参数名为 `event` 且无类型注解,也会视为 Event 依赖注入。 Event 依赖注入支持重载(即:可以标注参数为子类型)且具有[重载优先检查权](../appendices/overload.md#重载)。 ```python from nonebot.adapters import Event from nonebot.adapters.onebot.v11 import PrivateMessageEvent, GroupMessageEvent async def _(foo: Event): ... async def _(foo: PrivateMessageEvent | GroupMessageEvent): ... async def _(event): ... # 兼容性处理 ``` ```python from typing import Union from nonebot.adapters import Event from nonebot.adapters.onebot.v11 import PrivateMessageEvent, GroupMessageEvent async def _(foo: Event): ... async def _(foo: Union[PrivateMessageEvent, GroupMessageEvent]): ... async def _(event): ... # 兼容性处理 ``` ### State 获取当前[会话状态](../appendices/session-state.md)。 通过标注参数为 `T_State` 类型,即可获取到当前会话状态。为兼容性考虑,如果参数名为 `state` 且无类型注解,也会视为 State 依赖注入。 ```python from nonebot.typing import T_State async def _(foo: T_State): ... ``` ### Matcher 获取当前事件响应器实例。常用于使用[事件响应器操作](../appendices/session-control.mdx)。 通过标注参数为 `Matcher` 类型,或者一系列 `Matcher` 类型,即可获取到当前事件。为兼容性考虑,如果参数名为 `matcher` 且无类型注解,也会视为 Matcher 依赖注入。 Matcher 依赖注入支持重载(即:可以标注参数为子类型)且具有[重载优先检查权](../appendices/overload.md#重载)。 ```python from nonebot.matcher import Matcher async def _(foo: Matcher): ... async def _(matcher): ... # 兼容性处理 ``` ### Exception 获取事件响应器运行中抛出的异常。该依赖注入目前仅在事件响应器运行后处理 Hook 中可用。 通过标注参数为异常类型,或者一系列异常类型,即可获取到事件响应器运行中抛出的异常。 ```python {5,8} from nonebot.message import run_postprocessor from nonebot.exception import ActionFailed, NetworkError @run_postprocessor async def _(e: Exception): ... @run_postprocessor async def _(e: ActionFailed | NetworkError): ... ``` ```python {6,9} from typing import Union from nonebot.message import run_postprocessor from nonebot.exception import ActionFailed, NetworkError @run_postprocessor async def _(e: Exception): ... @run_postprocessor async def _(e: Union[ActionFailed, NetworkError]): ... ``` ## 子依赖 在依赖注入系统中,我们可以定义一个子依赖,来执行自定义的操作,提高代码复用性以及处理性能。 ### 定义子依赖 子依赖使用 `Depends` 标记进行定义,其参数即依赖的函数或可调用对象,同样会被解析为 `Dependent` 对象,将会在依赖注入期间执行。我们来看一个例子: ```python {5,15} from typing import Annotated from nonebot import on_command from nonebot.adapters import Event from nonebot.params import Depends test = on_command("test") async def check(event: Event) -> Event: if event.get_user_id() in BLACKLIST: await test.finish() return event @test.handle() async def _(event: Annotated[Event, Depends(check)]): ... ``` ```python {3,13} from nonebot import on_command from nonebot.adapters import Event from nonebot.params import Depends test = on_command("test") async def check(event: Event) -> Event: if event.get_user_id() in BLACKLIST: await test.finish() return event @test.handle() async def _(event: Event = Depends(check)): ... ``` 在上面的代码中,我们使用 `Depends` 标记定义了一个子依赖 `check`。它判断事件主体用户是否在黑名单中,如果在,则直接结束事件处理流程。如果不在,则返回事件对象,以便事件处理函数可以继续执行。 通过将 `Depends` 包裹的子依赖作为参数的默认值,我们就可以在执行事件处理函数之前执行子依赖,并将其返回值作为参数传入事件处理函数。子依赖和普通的事件处理函数并没有区别,同样可以使用依赖注入,并且可以返回任何类型的值。但需要注意的是,如果事件处理函数参数的类型注解与子依赖返回值的类型**不一致**,将会触发[重载](../appendices/overload.md)而跳过当前事件处理函数。 特别的,我们可以为 `Dependent` 对象定义一系列前置子依赖,它们会在参数执行前被顺序执行,且返回值将会被忽略,例如: ```python {11} from nonebot import on_command from nonebot.adapters import Event from nonebot.params import Depends test = on_command("test") async def check(event: Event): if event.get_user_id() in BLACKLIST: await test.finish() @test.handle(parameterless=[Depends(check)]) async def _(): ... ``` ### 依赖缓存 NoneBot 在执行子依赖时,会将其返回值缓存起来。当我们在使用子依赖时,`Depends` 具有一个参数 `use_cache`,默认为 `True`。此时在事件处理流程中,多次使用同一个子依赖时,将会使用缓存中的结果而不会重复执行。这在很多情景中非常有用,例如: ```python {7} import random from typing import Annotated async def random_result() -> int: return random.randint(1, 100) async def _(x: Annotated[int, Depends(random_result)]): print(x) ``` ```python {6} import random async def random_result() -> int: return random.randint(1, 100) async def _(x: int = Depends(random_result)): print(x) ``` 此时,在同一事件处理流程中,这个随机函数的返回值将会保持一致。如果我们希望每次都重新执行子依赖,可以将 `use_cache` 设置为 `False`。 ```python {7} import random from typing import Annotated async def random_result() -> int: return random.randint(1, 100) async def _(x: Annotated[int, Depends(random_result, use_cache=False)]): print(x) ``` ```python {6} import random async def random_result() -> int: return random.randint(1, 100) async def _(x: int = Depends(random_result, use_cache=False)): print(x) ``` :::tip 提示 缓存的生命周期与当前接收到的事件相同。接收到事件后,子依赖在首次执行时缓存,在该事件处理完成后,缓存就会被清除。 ::: ### 类型转换与校验 在依赖注入系统中,我们可以对子依赖的返回值进行自动类型转换与校验。这个功能由 Pydantic 支持,因此我们通过参数类型注解自动使用 Pydantic 支持的类型转换。例如: ```python {6,9} from typing import Annotated from nonebot.params import Depends from nonebot.adapters import Event def get_user_id(event: Event) -> str: return event.get_user_id() async def _(user_id: Annotated[int, Depends(get_user_id, validate=True)]): print(user_id) ``` ```python {4,7} from nonebot.params import Depends from nonebot.adapters import Event def get_user_id(event: Event) -> str: return event.get_user_id() async def _(user_id: int = Depends(get_user_id, validate=True)): print(user_id) ``` 在进行类型自动转换的同时,Pydantic 还支持对数据进行更多的限制,如:大于、小于、长度等。使用方法如下: ```python {7,10} from typing import Annotated from pydantic import Field from nonebot.params import Depends from nonebot.adapters import Event def get_user_id(event: Event) -> str: return event.get_user_id() async def _(user_id: Annotated[int, Depends(get_user_id, validate=Field(gt=100))]): print(user_id) ``` ```python {5,8} from pydantic import Field from nonebot.params import Depends from nonebot.adapters import Event def get_user_id(event: Event) -> str: return event.get_user_id() async def _(user_id: int = Depends(get_user_id, validate=Field(gt=100))): print(user_id) ``` ### 类作为依赖 在前面的事例中,我们使用了函数作为子依赖。实际上,我们还可以使用类作为依赖。当我们在实例化一个类的时候,其实我们就在调用它,类本身也是一个可调用对象。例如: ```python {16} from typing import Annotated from dataclasses import dataclass from nonebot.params import Depends from nonebot.adapters import Event from nonebot.typing import T_State def get_context(state: T_State) -> dict: return state.setdefault("context", {}) @dataclass class ClassDependency: event: Event context: dict = Depends(get_context) async def _(data: Annotated[ClassDependency, Depends(ClassDependency)]): print(data.event, data.context) ``` ```python {15} from dataclasses import dataclass from nonebot.params import Depends from nonebot.adapters import Event from nonebot.typing import T_State def get_context(state: T_State) -> dict: return state.setdefault("context", {}) @dataclass class ClassDependency: event: Event context: dict = Depends(get_context) async def _(data: ClassDependency = Depends(ClassDependency)): print(data.event, data.context) ``` 可以看到,我们使用 `dataclass` 定义了一个类。由于这个类的 `__init__` 方法可以被依赖注入系统解析,因此,我们可以将其作为子依赖进行声明。特别地,对于类依赖,`Depends` 的参数可以为空,NoneBot 将会使用参数的类型注解进行解析与推断: ```python from typing import Annotated async def _(data: Annotated[ClassDependency, Depends()]): print(data.event, data.context) ``` ```python async def _(data: ClassDependency = Depends()): print(data.event, data.context) ``` ### 生成器作为依赖 NoneBot 的依赖注入支持依赖项在事件处理流程结束后进行一些额外的工作,比如数据库 session 或者网络 IO 的关闭,互斥锁的解锁等等。同时,由于[依赖缓存](#依赖缓存)的存在,我们可以通过这种方式来实现共享一个 session 等功能。 要实现上述功能,我们可以用生成器函数作为依赖项,我们用 `yield` 关键字取代 `return` 关键字,并在 `yield` 之后进行额外的工作。 我们可以看下述代码段, 使用 `httpx.AsyncClient` 异步网络 IO,并在事件处理流程中共用一个 client: ```python {15} from typing import Annotated from collections.abc import AsyncGenerator import httpx from nonebot.params import Depends async def get_client() -> AsyncGenerator[httpx.AsyncClient, None]: try: async with httpx.AsyncClient() as client: yield client finally: # 在这里进行额外的工作 @test.handle() async def _(x: Annotated[httpx.AsyncClient, Depends(get_client)]): resp = await x.get("https://nonebot.dev") ``` ```python {15} from collections.abc import AsyncGenerator import httpx from nonebot.params import Depends async def get_client() -> AsyncGenerator[httpx.AsyncClient, None]: try: async with httpx.AsyncClient() as client: yield client finally: # 在这里进行额外的工作 @test.handle() async def _(x: httpx.AsyncClient = Depends(get_client)): resp = await x.get("https://nonebot.dev") ``` :::caution 注意 生成器作为依赖时,其中只能进行一次 `yield`,否则将会触发异常。如果对此有疑问并想探究原因,可以参考 [contextmanager](https://docs.python.org/zh-cn/3/library/contextlib.html#contextlib.contextmanager) 和 [asynccontextmanager](https://docs.python.org/zh-cn/3/library/contextlib.html#contextlib.asynccontextmanager) 文档。事实上,NoneBot 内部就使用了这两个装饰器。 ::: ### 可调用对象作为依赖 在 Python 里,为类定义 `__call__` 方法就可以使得这个类的实例成为一个可调用对象。因此,我们也可以将定义了 `__call__` 方法的类的实例作为依赖。事实上,NoneBot 的[内置响应规则](./matcher.md#内置响应规则)就广泛使用了这种方式,以 `is_type` 规则为例: ```python from nonebot.adapters import Event class IsTypeRule: def __init__(self, *types: type[Event]): self.types = types async def __call__(self, event: Event) -> bool: return isinstance(event, self.types) ``` 我们在使用 `is_type` 时,即实例化了 `IsTypeRule` 类,然后将实例作为响应规则依赖项传入。 ## 其他依赖注入 这一类的依赖注入通常基于子依赖编写,为我们开发者提供更方便的途径获取上下文信息。 ### EventType 获取当前事件的类型。 ```python {4} from typing import Annotated from nonebot.params import EventType async def _(foo: Annotated[str, EventType()]): ... ``` ```python {3} from nonebot.params import EventType async def _(foo: str = EventType()): ... ``` ### EventMessage 获取当前事件的消息。 ```python {5} from typing import Annotated from nonebot.adapters import Message from nonebot.params import EventMessage async def _(foo: Annotated[Message, EventMessage()]): ... ``` ```python {4} from nonebot.adapters import Message from nonebot.params import EventMessage async def _(foo: Message = EventMessage()): ... ``` ### EventPlainText 获取当前事件的消息纯文本部分。 ```python {4} from typing import Annotated from nonebot.params import EventPlainText async def _(foo: Annotated[str, EventPlainText()]): ... ``` ```python {3} from nonebot.params import EventPlainText async def _(foo: str = EventPlainText()): ... ``` ### EventToMe 获取当前事件是否与机器人相关。 ```python {4} from typing import Annotated from nonebot.params import EventToMe async def _(foo: Annotated[bool, EventToMe()]): ... ``` ```python {3} from nonebot.params import EventToMe async def _(foo: bool = EventToMe()): ... ``` ### Command 获取当前命令型消息的元组形式命令名。 ```python {4} from typing import Annotated from nonebot.params import Command async def _(foo: Annotated[tuple[str, ...], Command()]): ... ``` ```python {4} from nonebot.params import Command async def _(foo: tuple[str, ...] = Command()): ... ``` :::tip 提示 命令详情只能在**触发命令型事件响应器时**获取。如果在事件处理后续流程中获取,则会获取到不同的值。 ::: ### RawCommand 获取当前命令型消息的文本形式命令名。 ```python {4} from typing import Annotated from nonebot.params import RawCommand async def _(foo: Annotated[str, RawCommand()]): ... ``` ```python {3} from nonebot.params import RawCommand async def _(foo: str = RawCommand()): ... ``` :::tip 提示 命令详情只能在**触发命令型事件响应器时**获取。如果在事件处理后续流程中获取,则会获取到不同的值。 ::: ### CommandArg 获取命令型消息命令后跟随的参数。 ```python {5} from typing import Annotated from nonebot.adapters import Message from nonebot.params import CommandArg async def _(foo: Annotated[Message, CommandArg()]): ... ``` ```python {4} from nonebot.adapters import Message from nonebot.params import CommandArg async def _(foo: Message = CommandArg()): ... ``` :::tip 提示 命令详情只能在**触发命令型事件响应器时**获取。如果在事件处理后续流程中获取,则会获取到不同的值。 ::: ### CommandStart 获取命令型消息命令前缀。 ```python {4} from typing import Annotated from nonebot.params import CommandStart async def _(foo: Annotated[str, CommandStart()]): ... ``` ```python {3} from nonebot.params import CommandStart async def _(foo: str = CommandStart()): ... ``` :::tip 提示 命令详情只能在**触发命令型事件响应器时**获取。如果在事件处理后续流程中获取,则会获取到不同的值。 ::: ### CommandWhitespace 获取命令型消息命令与参数间空白符。 ```python {4} from typing import Annotated from nonebot.params import CommandWhitespace async def _(foo: Annotated[str, CommandWhitespace()]): ... ``` ```python {3} from nonebot.params import CommandWhitespace async def _(foo: str = CommandWhitespace()): ... ``` :::tip 提示 命令详情只能在**触发命令型事件响应器时**获取。如果在事件处理后续流程中获取,则会获取到不同的值。 ::: ### ShellCommandArgv 获取 shell 命令解析前的参数列表,列表中可能包含文本字符串和富文本消息段(如:图片)。当词法解析出错的时候,返回值将为 `None`。通过重载机制即可处理两种不同的情况。 ```python {4} from typing import Annotated from nonebot import on_shell_command from nonebot.params import ShellCommandArgv matcher = on_shell_command("cmd") # 解析失败 @matcher.handle() async def _(foo: Annotated[None, ShellCommandArgv()]): ... # 解析成功 @matcher.handle() async def _(foo: Annotated[list[str | MessageSegment], ShellCommandArgv()]): ... ``` ```python {4} from nonebot import on_shell_command from nonebot.params import ShellCommandArgv matcher = on_shell_command("cmd") # 解析失败 @matcher.handle() async def _(foo: None = ShellCommandArgv()): ... # 解析成功 @matcher.handle() async def _(foo: list[str | MessageSegment] = ShellCommandArgv()): ... ``` ```python {4} from typing import Union, Annotated from nonebot import on_shell_command from nonebot.params import ShellCommandArgv matcher = on_shell_command("cmd") # 解析失败 @matcher.handle() async def _(foo: Annotated[None, ShellCommandArgv()]): ... # 解析成功 @matcher.handle() async def _(foo: Annotated[list[Union[str, MessageSegment]], ShellCommandArgv()]): ... ``` ```python {4} from typing import Union from nonebot import on_shell_command from nonebot.params import ShellCommandArgv matcher = on_shell_command("cmd") # 解析失败 @matcher.handle() async def _(foo: None = ShellCommandArgv()): ... # 解析成功 @matcher.handle() async def _(foo: list[Union[str, MessageSegment]] = ShellCommandArgv()): ... ``` ### ShellCommandArgs 获取 shell 命令解析后的参数 Namespace,支持 MessageSegment 富文本(如:图片)。 :::tip 提示 如果参数解析成功,则为 parser 返回的 Namespace;如果参数解析失败,则为 [`ParserExit`](../api/exception.md#ParserExit) 异常,并携带错误码与错误信息。在前置词法解析失败时,返回值也为 [`ParserExit`](../api/exception.md#ParserExit) 异常。通过重载机制即可处理两种不同的情况。 由于 `ArgumentParser` 在解析到 `--help` 参数时也会抛出异常,这种情况下错误码为 `0` 且错误信息即为帮助信息。 ::: ```python {14,22} from typing import Annotated from nonebot import on_shell_command from nonebot.exception import ParserExit from nonebot.params import ShellCommandArgs from nonebot.rule import Namespace, ArgumentParser parser = ArgumentParser("demo") # parser.add_argument ... matcher = on_shell_command("cmd", parser=parser) # 解析失败 @matcher.handle() async def _(foo: Annotated[ParserExit, ShellCommandArgs()]): if foo.status == 0: foo.message # help message else: foo.message # error message # 解析成功 @matcher.handle() async def _(foo: Annotated[Namespace, ShellCommandArgs()]): arg_dict = vars(foo) ``` ```python {12,20} from nonebot import on_shell_command from nonebot.exception import ParserExit from nonebot.params import ShellCommandArgs from nonebot.rule import Namespace, ArgumentParser parser = ArgumentParser("demo") # parser.add_argument ... matcher = on_shell_command("cmd", parser=parser) # 解析失败 @matcher.handle() async def _(foo: ParserExit = ShellCommandArgs()): if foo.status == 0: foo.message # help message else: foo.message # error message # 解析成功 @matcher.handle() async def _(foo: Namespace = ShellCommandArgs()): arg_dict = vars(foo) ``` ### RegexMatched 获取正则匹配结果的对象。 ```python {5} from re import Match from typing import Annotated from nonebot.params import RegexMatched async def _(foo: Annotated[Match[str], RegexMatched()]): ... ``` ```python {4} from re import Match from nonebot.params import RegexMatched async def _(foo: Match[str] = RegexMatched()): ... ``` ### RegexStr 获取正则匹配结果的文本。 ```python {4} from typing import Annotated from nonebot.params import RegexStr async def _(foo: Annotated[str, RegexStr()]): ... ``` ```python {3} from nonebot.params import RegexStr async def _(foo: str = RegexStr()): ... ``` ### RegexGroup 获取正则匹配结果的 group 元组。 ```python {4} from typing import Any, Annotated from nonebot.params import RegexGroup async def _(foo: Annotated[tuple[Any, ...], RegexGroup()]): ... ``` ```python {4} from typing import Any from nonebot.params import RegexGroup async def _(foo: tuple[Any, ...] = RegexGroup()): ... ``` ### RegexDict 获取正则匹配结果的 group 字典。 ```python {4} from typing import Any, Annotated from nonebot.params import RegexDict async def _(foo: Annotated[dict[str, Any], RegexDict()]): ... ``` ```python {4} from typing import Any from nonebot.params import RegexDict async def _(foo: dict[str, Any] = RegexDict()): ... ``` ### Startswith 获取触发响应器的消息前缀字符串。 ```python {4} from typing import Annotated from nonebot.params import Startswith async def _(foo: Annotated[str, Startswith()]): ... ``` ```python {3} from nonebot.params import Startswith async def _(foo: str = Startswith()): ... ``` ### Endswith 获取触发响应器的消息后缀字符串。 ```python {4} from typing import Annotated from nonebot.params import Endswith async def _(foo: Annotated[str, Endswith()]): ... ``` ```python {3} from nonebot.params import Endswith async def _(foo: str = Endswith()): ... ``` ### Fullmatch 获取触发响应器的消息字符串。 ```python {4} from typing import Annotated from nonebot.params import Fullmatch async def _(foo: Annotated[str, Fullmatch()]): ... ``` ```python {3} from nonebot.params import Fullmatch async def _(foo: str = Fullmatch()): ... ``` ### Keyword 获取触发响应器的关键字字符串。 ```python {4} from typing import Annotated from nonebot.params import Keyword async def _(foo: Annotated[str, Keyword()]): ... ``` ```python {3} from nonebot.params import Keyword async def _(foo: str = Keyword()): ... ``` ### Received 获取某次 `receive` 接收的事件。 ```python {7} from typing import Annotated from nonebot.adapters import Event from nonebot.params import Received @matcher.receive("id") async def _(foo: Annotated[Event, Received("id")]): ... ``` ```python {5} from nonebot.adapters import Event from nonebot.params import Received @matcher.receive("id") async def _(foo: Event = Received("id")): ... ``` ### LastReceived 获取最近一次 `receive` 接收的事件。 ```python {7} from typing import Annotated from nonebot.adapters import Event from nonebot.params import LastReceived @matcher.receive("any") async def _(foo: Annotated[Event, LastReceived()]): ... ``` ```python {5} from nonebot.adapters import Event from nonebot.params import LastReceived @matcher.receive("any") async def _(foo: Event = LastReceived()): ... ``` ### ReceivePromptResult 获取某次 `receive` 发送提示消息的结果。 ```python {6} from typing import Any, Annotated from nonebot.params import ReceivePromptResult @matcher.receive("id", prompt="prompt") async def _(result: Annotated[Any, ReceivePromptResult("id")]): ... ``` ```python {6} from typing import Any from nonebot.params import ReceivePromptResult @matcher.receive("id", prompt="prompt") async def _(result: Any = ReceivePromptResult("id")): ... ``` ### Arg 获取某次 `got` 接收的参数。如果 `Arg` 参数留空,则使用函数的参数名作为要获取的参数。 ```python {7,8} from typing import Annotated from nonebot.params import Arg from nonebot.adapters import Message @matcher.got("key") async def _(key: Annotated[Message, Arg()]): ... async def _(foo: Annotated[Message, Arg("key")]): ... ``` ```python {5,6} from nonebot.params import Arg from nonebot.adapters import Message @matcher.got("key") async def _(key: Message = Arg()): ... async def _(foo: Message = Arg("key")): ... ``` ### ArgStr 获取某次 `got` 接收的参数,并转换为字符串。如果 `Arg` 参数留空,则使用函数的参数名作为要获取的参数。 ```python {6,7} from typing import Annotated from nonebot.params import ArgStr @matcher.got("key") async def _(key: Annotated[str, ArgStr()]): ... async def _(foo: Annotated[str, ArgStr("key")]): ... ``` ```python {4,5} from nonebot.params import ArgStr @matcher.got("key") async def _(key: str = ArgStr()): ... async def _(foo: str = ArgStr("key")): ... ``` ### ArgPlainText 获取某次 `got` 接收的参数的纯文本部分。如果 `Arg` 参数留空,则使用函数的参数名作为要获取的参数。 ```python {6,7} from typing import Annotated from nonebot.params import ArgPlainText @matcher.got("key") async def _(key: Annotated[str, ArgPlainText()]): ... async def _(foo: Annotated[str, ArgPlainText("key")]): ... ``` ```python {4,5} from nonebot.params import ArgPlainText @matcher.got("key") async def _(key: str = ArgPlainText()): ... async def _(foo: str = ArgPlainText("key")): ... ``` ### ArgPromptResult 获取某次 `got` 发送提示消息的结果。如果 `Arg` 参数留空,则使用函数的参数名作为要获取的参数。 ```python {6,7} from typing import Any, Annotated from nonebot.params import ArgPromptResult @matcher.got("key", prompt="prompt") async def _(result: Annotated[Any, ArgPromptResult()]): ... async def _(result: Annotated[Any, ArgPromptResult("key")]): ... ``` ```python {6,7} from typing import Any from nonebot.params import ArgPromptResult @matcher.got("key", prompt="prompt") async def _(result: Any = ArgPromptResult()): ... async def _(result: Any = ArgPromptResult("key")): ... ``` ### PausePromptResult 获取最近一次 `pause` 发送提示消息的结果。 ```python {6} from typing import Any, Annotated from nonebot.params import PausePromptResult @matcher.handle() async def _(): await matcher.pause(prompt="prompt") @matcher.handle() async def _(result: Annotated[Any, PausePromptResult()]): ... ``` ```python {6} from typing import Any from nonebot.params import PausePromptResult @matcher.handle() async def _(): await matcher.pause(prompt="prompt") @matcher.handle() async def _(result: Any = PausePromptResult()): ... ``` ================================================ FILE: website/versioned_docs/version-2.4.3/advanced/driver.md ================================================ --- sidebar_position: 0 description: 选择合适的驱动器运行机器人 options: menu: - category: advanced weight: 10 --- # 选择驱动器 驱动器 (Driver) 是机器人运行的基石,它是机器人初始化的第一步,主要负责数据收发。 :::important 提示 驱动器的选择通常与机器人所使用的协议适配器相关,如果不知道该选择哪个驱动器,可以先阅读相关协议适配器文档说明。 ::: :::tip 提示 如何**安装**驱动器请参考[安装驱动器](../tutorial/store.mdx#安装驱动器)。 ::: ## 驱动器类型 驱动器类型大体上可以分为两种: - `Forward`:即客户端型驱动器,多用于使用 HTTP 轮询,连接 WebSocket 服务器等情形。 - `Reverse`:即服务端型驱动器,多用于使用 WebHook,接收 WebSocket 客户端连接等情形。 客户端型驱动器可以分为以下两种: 1. 异步发送 HTTP 请求,自定义 `HTTP Method`、`URL`、`Header`、`Body`、`Cookie`、`Proxy`、`Timeout` 等。 2. 异步建立 WebSocket 连接上下文,自定义 `WebSocket URL`、`Header`、`Cookie`、`Proxy`、`Timeout` 等。 服务端型驱动器目前有: 1. ASGI 应用框架,具有以下功能: - 协议适配器自定义 HTTP 上报地址以及对上报数据处理的回调函数。 - 协议适配器自定义 WebSocket 连接请求地址以及对 WebSocket 请求处理的回调函数。 - 用户可以向 ASGI 应用添加任何服务端相关功能,如:[添加自定义路由](./routing.md)。 ## 配置驱动器 驱动器的配置方法已经在[配置](../appendices/config.mdx)章节中简单进行了介绍,这里将详细介绍驱动器配置的格式。 NoneBot 中的客户端和服务端型驱动器可以相互配合使用,但服务端型驱动器**仅能选择一个**。所有驱动器模块都会包含一个 `Driver` 子类,即驱动器类,他可以作为驱动器单独运行。同时,客户端驱动器模块中还会提供一个 `Mixin` 子类,用于在与其他驱动器配合使用时加载。因此,驱动器配置格式采用特殊语法:`[:][+[:]]*`。 其中,`` 代表**驱动器模块路径**;`` 代表**驱动器类名**,默认为 `Driver`;`` 代表**驱动器混入类名**,默认为 `Mixin`。即,我们需要选择一个主要驱动器,然后在其基础上配合使用其他驱动器的功能。主要驱动器可以为客户端或服务端类型,但混入类驱动器只能为客户端类型。 特别的,为了简化内置驱动器模块路径,我们可以使用 `~` 符号作为内置驱动器模块路径的前缀,如 `~fastapi` 代表使用内置驱动器 `fastapi`。NoneBot 内置了多个驱动器适配,但需要安装额外依赖才能使用,具体请参考[安装驱动器](../tutorial/store.mdx#安装驱动器)。常见的驱动器配置如下: ```dotenv DRIVER=~fastapi DRIVER=~aiohttp DRIVER=~httpx+~websockets DRIVER=~fastapi+~httpx+~websockets ``` ## 获取驱动器 在 NoneBot 框架初始化完成后,我们就可以通过 `get_driver()` 方法获取全局驱动器实例: ```python from nonebot import get_driver driver = get_driver() ``` ## 内置驱动器 ### None **类型:**服务端驱动器 NoneBot 内置的空驱动器,不提供任何收发数据功能,可以在不需要外部网络连接时使用。 ```env DRIVER=~none ``` ### FastAPI(默认) **类型:**ASGI 服务端驱动器 > FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints. [FastAPI](https://fastapi.tiangolo.com/) 是一个易上手、高性能的异步 Web 框架,具有极佳的编写体验。 FastAPI 可以通过类型注解、依赖注入等方式实现输入参数校验、自动生成 API 文档等功能,也可以挂载其他 ASGI、WSGI 应用。 ```env DRIVER=~fastapi ``` #### FastAPI 配置项 ##### `fastapi_openapi_url` 类型:`str | None` 默认值:`None` 说明:`FastAPI` 提供的 `OpenAPI` JSON 定义地址,如果为 `None`,则不提供 `OpenAPI` JSON 定义。 ##### `fastapi_docs_url` 类型:`str | None` 默认值:`None` 说明:`FastAPI` 提供的 `Swagger` 文档地址,如果为 `None`,则不提供 `Swagger` 文档。 ##### `fastapi_redoc_url` 类型:`str | None` 默认值:`None` 说明:`FastAPI` 提供的 `ReDoc` 文档地址,如果为 `None`,则不提供 `ReDoc` 文档。 ##### `fastapi_include_adapter_schema` 类型:`bool` 默认值:`True` 说明:`FastAPI` 提供的 `OpenAPI` JSON 定义中是否包含适配器路由的 `Schema`。 ##### `fastapi_reload` :::caution 警告 不推荐开启该配置项,在 Windows 平台上开启该功能有可能会造成预料之外的影响!替代方案:使用 `nb-cli` 命令行工具以及参数 `--reload` 启动 NoneBot。 ```bash nb run --reload ``` 开启该功能后,在 uvicorn 运行时(FastAPI 提供的 ASGI 底层,即 reload 功能的实际来源),asyncio 使用的事件循环会被 uvicorn 从默认的 `ProactorEventLoop` 强制切换到 `SelectorEventLoop`。 > 相关信息参考 [uvicorn#529](https://github.com/encode/uvicorn/issues/529),[uvicorn#1070](https://github.com/encode/uvicorn/pull/1070),[uvicorn#1257](https://github.com/encode/uvicorn/pull/1257) 后者(`SelectorEventLoop`)在 Windows 平台的可使用性不如前者(`ProactorEventLoop`),包括但不限于 1. 不支持创建子进程 2. 最多只支持 512 个套接字 3. ... > 具体信息参考 [Python 文档](https://docs.python.org/zh-cn/3/library/asyncio-platforms.html#windows) 所以,一些使用了 asyncio 的库因此可能无法正常工作,如: 1. [playwright](https://playwright.dev/python/docs/library#incompatible-with-selectoreventloop-of-asyncio-on-windows) 如果在开启该功能后,原本**正常运行**的代码报错,且打印的异常堆栈信息和 asyncio 有关(异常一般为 `NotImplementedError`), 你可能就需要考虑相关库对事件循环的支持,以及是否启用该功能。 ::: 类型:`bool` 默认值:`False` 说明:是否开启 `uvicorn` 的 `reload` 功能,需要在机器人入口文件提供 ASGI 应用路径。 ```python title=bot.py app = nonebot.get_asgi() nonebot.run(app="bot:app") ``` ##### `fastapi_reload_dirs` 类型:`List[str] | None` 默认值:`None` 说明:重载监控文件夹列表,默认为 uvicorn 默认值 ##### `fastapi_reload_delay` 类型:`float | None` 默认值:`None` 说明:重载延迟,默认为 uvicorn 默认值 ##### `fastapi_reload_includes` 类型:`List[str] | None` 默认值:`None` 说明:要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值 ##### `fastapi_reload_excludes` 类型:`List[str] | None` 默认值:`None` 说明:不要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值 ##### `fastapi_extra` 类型:`Dist[str, Any]` 默认值:`{}` 说明:传递给 `FastAPI` 的其他参数 ### Quart **类型:**ASGI 服务端驱动器 > Quart is an asyncio reimplementation of the popular Flask microframework API. [Quart](https://quart.palletsprojects.com/) 是一个类 Flask 的异步版本,拥有与 Flask 非常相似的接口和使用方法。 ```env DRIVER=~quart ``` #### Quart 配置项 ##### `quart_reload` :::caution 警告 不推荐开启该配置项,在 Windows 平台上开启该功能有可能会造成预料之外的影响!替代方案:使用 `nb-cli` 命令行工具以及参数 `--reload` 启动 NoneBot。 ```bash nb run --reload ``` ::: 类型:`bool` 默认值:`False` 说明:是否开启 `uvicorn` 的 `reload` 功能,需要在机器人入口文件提供 ASGI 应用路径。 ```python title=bot.py app = nonebot.get_asgi() nonebot.run(app="bot:app") ``` ##### `quart_reload_dirs` 类型:`List[str] | None` 默认值:`None` 说明:重载监控文件夹列表,默认为 uvicorn 默认值 ##### `quart_reload_delay` 类型:`float | None` 默认值:`None` 说明:重载延迟,默认为 uvicorn 默认值 ##### `quart_reload_includes` 类型:`List[str] | None` 默认值:`None` 说明:要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值 ##### `quart_reload_excludes` 类型:`List[str] | None` 默认值:`None` 说明:不要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值 ##### `quart_extra` 类型:`Dist[str, Any]` 默认值:`{}` 说明:传递给 `Quart` 的其他参数 ### HTTPX **类型:**HTTP 客户端驱动器 :::caution 注意 本驱动器仅支持 HTTP 请求,不支持 WebSocket 连接请求。 ::: > [HTTPX](https://www.python-httpx.org/) is a fully featured HTTP client for Python 3, which provides sync and async APIs, and support for both HTTP/1.1 and HTTP/2. ```env DRIVER=~httpx ``` ### websockets **类型:**WebSocket 客户端驱动器 :::caution 注意 本驱动器仅支持 WebSocket 连接请求,不支持 HTTP 请求。 ::: > [websockets](https://websockets.readthedocs.io/) is a library for building WebSocket servers and clients in Python with a focus on correctness, simplicity, robustness, and performance. ```env DRIVER=~websockets ``` ### AIOHTTP **类型:**HTTP/WebSocket 客户端驱动器 > [AIOHTTP](https://docs.aiohttp.org/): Asynchronous HTTP Client/Server for asyncio and Python. ```env DRIVER=~aiohttp ``` ================================================ FILE: website/versioned_docs/version-2.4.3/advanced/matcher-provider.md ================================================ --- sidebar_position: 10 description: 自定义事件响应器存储 options: menu: - category: advanced weight: 110 --- # 事件响应器存储 事件响应器是 NoneBot 处理事件的核心,它们默认存储在一个字典中。在进入会话状态后,事件响应器将会转为临时响应器,作为最高优先级同样存储于该字典中。因此,事件响应器的存储类似于会话存储,它决定了整个 NoneBot 对事件的处理行为。 NoneBot 默认使用 Python 的字典将事件响应器存储于内存中,但是我们也可以自定义事件响应器存储,将事件响应器存储于其他地方,例如 Redis 等。这样我们就可以实现持久化、在多实例间共享会话状态等功能。 ## 编写存储提供者 事件响应器的存储提供者 `MatcherProvider` 抽象类继承自 `MutableMapping[int, list[type[Matcher]]]`,即以优先级为键,以事件响应器列表为值的映射。我们可以方便地进行逐优先级事件传播。 编写一个自定义的存储提供者,只需要继承并实现 `MatcherProvider` 抽象类: ```python from nonebot.matcher import MatcherProvider class CustomProvider(MatcherProvider): ... ``` ## 设置存储提供者 我们可以通过 `matchers.set_provider` 方法设置存储提供者: ```python {3} from nonebot.matcher import matchers matchers.set_provider(CustomProvider) assert isinstance(matchers.provider, CustomProvider) ``` ================================================ FILE: website/versioned_docs/version-2.4.3/advanced/matcher.md ================================================ --- sidebar_position: 5 description: 事件响应器组成与内置响应规则 options: menu: - category: advanced weight: 60 --- # 事件响应器进阶 在[指南](../tutorial/matcher.md)与[深入](../appendices/rule.md)中,我们已经介绍了事件响应器的基本用法以及响应规则、权限控制等功能。在这一节中,我们将介绍事件响应器的组成,内置的响应规则,与第三方响应规则拓展。 :::tip 提示 事件响应器允许继承,你可以通过直接继承 `Matcher` 类来创建一个新的事件响应器。 ::: ## 事件响应器组成 ### 事件响应器类型 事件响应器类型 `type` 即是该响应器所要响应的事件类型,只有在接收到的事件类型与该响应器的类型相同时,才会触发该响应器。如果类型为空字符串 `""`,则响应器将会响应所有类型的事件。事件响应器类型的检查在所有其他检查(权限控制、响应规则)之前进行。 NoneBot 内置了四种常用事件类型:`meta_event`、`message`、`notice`、`request`,分别对应元事件、消息、通知、请求。通常情况下,协议适配器会将事件合理地分类至这四种类型中。如果有其他类型的事件需要响应,可以自行定义新的类型。 ### 事件触发权限 事件触发权限 `permission` 是一个 `Permission` 对象,这在[权限控制](../appendices/permission.mdx)一节中已经介绍过。事件触发权限会在事件响应器的类型检查通过后进行检查,如果权限检查通过,则执行响应规则检查。 ### 事件响应规则 事件响应规则 `rule` 是一个 `Rule` 对象,这在[响应规则](../appendices/rule.md)一节中已经介绍过。事件响应器的响应规则会在事件响应器的权限检查通过后进行匹配,如果响应规则检查通过,则触发该响应器。 ### 响应优先级 响应优先级 `priority` 是一个正整数,用于指定响应器的优先级。响应器的优先级越小,越先被触发。如果响应器的优先级相同,则按照响应器的注册顺序进行触发。 ### 阻断 阻断 `block` 是一个布尔值,用于指定响应器是否阻断事件的传播。如果阻断为 `True`,则在该响应器被触发后,事件将不会再传播给其他下一优先级的响应器。 NoneBot 内置的事件响应器中,所有非 `command` 规则的 `message` 类型的事件响应器都会阻断事件传递,其他则不会。 在部分情况中,可以使用 [`stop_propagation`](../appendices/session-control.mdx#stop_propagation) 方法动态阻止事件传播,该方法需要 handler 在参数中获取 matcher 实例后调用方法。 ### 有效期 事件响应器的有效期分为 `temp` 和 `expire_time` 。`temp` 是一个布尔值,用于指定响应器是否为临时响应器。如果为 `True`,则该响应器在被触发后会被自动销毁。`expire_time` 是一个 `datetime` 对象,用于指定响应器的过期时间。如果 `expire_time` 不为 `None`,则在该时间点后,该响应器会被自动销毁。 ### 默认状态 事件响应器的默认状态 `default_state` 是一个 `dict` 对象,用于指定响应器的默认状态。在响应器被触发时,响应器将会初始化默认状态然后开始执行事件处理流程。 ## 基本辅助函数 NoneBot 为四种类型的事件响应器提供了五个基本的辅助函数: - `on`:创建任何类型的事件响应器。 - `on_metaevent`:创建元事件响应器。 - `on_message`:创建消息事件响应器。 - `on_request`:创建请求事件响应器。 - `on_notice`:创建通知事件响应器。 除了 `on` 函数具有一个 `type` 参数外,其余参数均相同: - `rule`:响应规则,可以是 `Rule` 对象或者 `RuleChecker` 函数。 - `permission`:事件触发权限,可以是 `Permission` 对象或者 `PermissionChecker` 函数。 - `handlers`:事件处理函数列表。 - `temp`:是否为临时响应器。 - `expire_time`:响应器的过期时间。 - `priority`:响应器的优先级。 - `block`:是否阻断事件传播。 - `state`:响应器的默认状态。 在消息类型的事件响应器的基础上,NoneBot 还内置了一些常用的响应规则,并结合为辅助函数来方便我们快速创建指定功能的响应器。下面我们逐个介绍。 ## 内置响应规则 :::tip 响应规则的使用方法可以参考 [深入 - 响应规则](../appendices/rule.md)。 ::: ### `startswith` `startswith` 响应规则用于匹配消息纯文本部分的开头是否与指定字符串(或一系列字符串)相同。可选参数 `ignorecase` 用于指定是否忽略大小写,默认为 `False`。 例如,我们可以创建一个匹配消息开头为 `!` 或者 `/` 的规则: ```python from nonebot.rule import startswith rule = startswith(("!", "/"), ignorecase=False) ``` 也可以直接使用辅助函数新建一个响应器: ```python from nonebot import on_startswith matcher = on_startswith(("!", "/"), ignorecase=False) ``` ### `endswith` `endswith` 响应规则用于匹配消息纯文本部分的结尾是否与指定字符串(或一系列字符串)相同。可选参数 `ignorecase` 用于指定是否忽略大小写,默认为 `False`。 例如,我们可以创建一个匹配消息结尾为 `.` 或者 `。` 的规则: ```python from nonebot.rule import endswith rule = endswith((".", "。"), ignorecase=False) ``` 也可以直接使用辅助函数新建一个响应器: ```python from nonebot import on_endswith matcher = on_endswith((".", "。"), ignorecase=False) ``` ### `fullmatch` `fullmatch` 响应规则用于匹配消息纯文本部分是否与指定字符串(或一系列字符串)完全相同。可选参数 `ignorecase` 用于指定是否忽略大小写,默认为 `False`。 例如,我们可以创建一个匹配消息为 `ping` 或者 `pong` 的规则: ```python from nonebot.rule import fullmatch rule = fullmatch(("ping", "pong"), ignorecase=False) ``` 也可以直接使用辅助函数新建一个响应器: ```python from nonebot import on_fullmatch matcher = on_fullmatch(("ping", "pong"), ignorecase=False) ``` ### `keyword` `keyword` 响应规则用于匹配消息纯文本部分是否包含指定字符串(或一系列字符串)。 例如,我们可以创建一个匹配消息中包含 `hello` 或者 `hi` 的规则: ```python from nonebot.rule import keyword rule = keyword("hello", "hi") ``` 也可以直接使用辅助函数新建一个响应器: ```python from nonebot import on_keyword matcher = on_keyword({"hello", "hi"}) ``` ### `command` `command` 是最常用的响应规则,它用于匹配消息是否为命令。它会根据配置中的 [Command Start 和 Command Separator](../appendices/config.mdx#command-start-和-command-separator) 来判断消息是否为命令。 例如,当我们配置了 `Command Start` 为 `/`,`Command Separator` 为 `.` 时: ```python from nonebot.rule import command # 匹配 "/help" 或者 "/帮助" 开头的消息 rule = command("help", "帮助") # 匹配 "/help.cmd" 开头的消息 rule = command(("help", "cmd")) ``` 也可以直接使用辅助函数新建一个响应器: ```python from nonebot import on_command matcher = on_command("help", aliases={"帮助"}) ``` 此外,`command` 响应规则默认允许消息命令与参数间不加空格,如果需要严格匹配命令与参数间的空白符,可以使用 `command` 函数的 `force_whitespace` 参数。`force_whitespace` 参数可以是 bool 类型或者具体的字符串,默认为 `False`。如果为 `True`,则命令与参数间必须有任意个数的空白符;如果为字符串,则命令与参数间必须有且与给定字符串一致的空白符。 ```python rule = command("help", force_whitespace=True) rule = command("help", force_whitespace=" ") ``` 命令解析后的结果可以通过 [`Command`](./dependency.mdx#command)、[`RawCommand`](./dependency.mdx#rawcommand)、[`CommandArg`](./dependency.mdx#commandarg)、[`CommandStart`](./dependency.mdx#commandstart)、[`CommandWhitespace`](./dependency.mdx#commandwhitespace) 依赖注入获取。 ### `shell_command` `shell_command` 响应规则用于匹配类 shell 命令形式的消息。它首先与 [`command`](#command) 响应规则一样进行命令匹配,如果匹配成功,则会进行进一步的参数解析。参数解析采用 `argparse` 标准库进行,在此基础上添加了消息序列 `Message` 支持。 例如,我们可以创建一个匹配 `/cmd` 命令并且带有 `-v` 选项与默认 `-h` 帮助选项的规则: ```python from nonebot.rule import shell_command, ArgumentParser parser = ArgumentParser() parser.add_argument("-v", "--verbose", action="store_true") rule = shell_command("cmd", parser=parser) ``` 更多关于 `argparse` 的使用方法请参考 [argparse 文档](https://docs.python.org/zh-cn/3/library/argparse.html)。我们也可以选择不提供 `parser` 参数,这样 `shell_command` 将不会解析参数,但会提供参数列表 `argv`。 直接使用辅助函数新建一个响应器: ```python from nonebot import on_shell_command from nonebot.rule import ArgumentParser parser = ArgumentParser() parser.add_argument("-v", "--verbose", action="store_true") matcher = on_shell_command("cmd", parser=parser) ``` 参数解析后的结果可以通过 [`ShellCommandArgv`](./dependency.mdx#shellcommandargv)、[`ShellCommandArgs`](./dependency.mdx#shellcommandargs) 依赖注入获取。 ### `regex` `regex` 响应规则用于匹配消息是否与指定正则表达式匹配。 :::tip 提示 正则表达式匹配使用 search 而非 match,如需从头匹配请使用 `r"^xxx"` 模式来确保匹配开头。 ::: 例如,我们可以创建一个匹配消息中包含字母并且忽略大小写的规则: ```python from nonebot.rule import regex rule = regex(r"[a-z]+", flags=re.IGNORECASE) ``` 也可以直接使用辅助函数新建一个响应器: ```python from nonebot import on_regex matcher = on_regex(r"[a-z]+", flags=re.IGNORECASE) ``` 正则匹配后的结果可以通过 [`RegexStr`](./dependency.mdx#regexstr)、[`RegexGroup`](./dependency.mdx#regexgroup)、[`RegexDict`](./dependency.mdx#regexdict) 依赖注入获取。 ### `to_me` `to_me` 响应规则用于匹配事件是否与机器人相关。 例如: ```python from nonebot.rule import to_me rule = to_me() ``` ### `is_type` `is_type` 响应规则用于匹配事件类型是否为指定类型(或者一系列类型)。 例如,我们可以创建一个匹配 OneBot v11 私聊和群聊消息事件的规则: ```python from nonebot.rule import is_type from nonebot.adapters.onebot.v11 import PrivateMessageEvent, GroupMessageEvent rule = is_type(PrivateMessageEvent, GroupMessageEvent) ``` ## 响应器组 为了更方便的管理一系列功能相近的响应器,NoneBot 提供了两种响应器组,它们可以帮助我们进行响应器的统一管理。 ### `CommandGroup` `CommandGroup` 可以用于管理一系列具有相同前置命令的子命令响应器。 例如,我们创建 `/cmd`、`/cmd.sub`、`/cmd.help` 三个命令,他们具有相同的优先级: ```python from nonebot import CommandGroup group = CommandGroup("cmd", priority=10) cmd = group.command(tuple()) sub_cmd = group.command("sub") help_cmd = group.command("help") ``` 命令别名 aliases 默认不会添加 `CommandGroup` 设定的前缀,如果需要为 aliases 添加前缀,可以添加 `prefix_aliases=True` 参数: ```python from nonebot import CommandGroup group = CommandGroup("cmd", prefix_aliases=True) cmd = group.command(tuple()) help_cmd = group.command("help", aliases={"帮助"}) ``` 这样就能成功匹配 `/cmd`、`/cmd.help`、`/cmd.帮助` 命令。如果未设置,将默认匹配 `/cmd`、`/cmd.help`、`/帮助` 命令。 ### `MatcherGroup` `MatcherGroup` 可以用于管理一系列具有相同属性的响应器。 例如,我们创建一个具有相同响应规则的响应器组: ```python from nonebot.rule import to_me from nonebot import MatcherGroup group = MatcherGroup(rule=to_me()) matcher1 = group.on_message() matcher2 = group.on_message() ``` ## 第三方响应规则 ### Alconna [`nonebot-plugin-alconna`](https://github.com/nonebot/plugin-alconna) 是一类提供了拓展响应规则的插件。 该插件使用 [Alconna](https://github.com/ArcletProject/Alconna) 作为命令解析器, 是一个简单、灵活、高效的命令参数解析器, 并且不局限于解析命令式字符串。 该插件提供了一类新的事件响应器辅助函数 `on_alconna`,以及 `AlconnaResult` 等依赖注入函数。 基于 `Alconna` 的特性,该插件同时提供了一系列便捷的消息段标注。 标注可用于在 `Alconna` 中匹配消息中除 text 外的其他消息段,也可用于快速创建各适配器下的消息段。所有标注位于 `nonebot_plugin_alconna.adapters` 中。 该插件同时通过提供 `UniMessage` (通用消息模型) 实现了**跨平台接收和发送消息**的功能。 详情请阅读最佳实践中的 [命令解析拓展](../best-practice/alconna/README.mdx) 章节。 ================================================ FILE: website/versioned_docs/version-2.4.3/advanced/plugin-info.md ================================================ --- sidebar_position: 2 description: 填写与获取插件相关的信息 options: menu: - category: advanced weight: 30 --- # 插件信息 NoneBot 是一个插件化的框架,可以通过加载插件来扩展功能。同时,我们也可以通过 NoneBot 的插件系统来获取相关信息,例如插件的名称、使用方法,用于收集帮助信息等。下面我们将介绍如何为插件添加元数据,以及如何获取插件信息。 ## 插件元数据 在 NoneBot 中,插件 [`Plugin`](../api/plugin/model.md#Plugin) 对象中存储了插件系统所需要的一系列信息。包括插件的索引名称、插件模块、插件中的事件响应器、插件父子关系等。通常,只有插件开发者才需要关心这些信息,而插件使用者或者机器人用户想要看到的是插件使用方法等帮助信息。因此,我们可以为插件添加插件元数据 `PluginMetadata`,它允许插件开发者为插件添加一些额外的信息。这些信息编写于插件模块的顶层,可以直接通过源码查看,或者通过 NoneBot 插件系统获取收集到的信息,通过其他方式发送给机器人用户等。 现在,假设我们有一个插件 `example`, 它的模块结构如下: ```tree {4-6} title=Project 📦 awesome-bot ├── 📂 awesome_bot │ └── 📂 plugins | └── 📂 example | ├── 📜 __init__.py | └── 📜 config.py ├── 📜 pyproject.toml └── 📜 README.md ``` 我们需要在插件顶层模块 `example/__init__.py` 中添加插件元数据,如下所示: ```python {1,5-12} title=example/__init__.py from nonebot.plugin import PluginMetadata from .config import Config __plugin_meta__ = PluginMetadata( name="示例插件", description="这是一个示例插件", usage="没什么用", type="application", config=Config, extra={}, ) ``` 我们可以看到,插件元数据 `PluginMetadata` 有三个基本属性:插件名称、插件描述、插件使用方法。除此之外,还有几个可选的属性(具体填写见[发布插件](../developer/plugin-publishing.mdx#插件元数据)章节): - `type`:插件类别,发布插件必填。当前有效类别有:`library`(为其他插件编写提供功能),`application`(向机器人用户提供功能); - `homepage`:插件项目主页,发布插件必填; - `config`:插件的[配置类](../appendices/config.mdx#插件配置),发布插件时如有配置类则必须填写; - `supported_adapters`:支持的适配器模块名集合,若插件只使用了 NoneBot 基本抽象,应显式填写 `None`; - `extra`:一个字典,可以用于存储任意信息。其他插件可以通过约定 `extra` 字典的键名来达成收集某些特殊信息的目的。 请注意,这里的**插件名称**是供使用者或机器人用户查看的人类可读名称,与插件索引名称无关。**插件索引名称(插件模块名称)**仅用于 NoneBot 插件系统**内部索引**。 ## 获取插件信息 NoneBot 提供了多种获取插件对象的方法,例如获取当前所有已导入的插件: ```python import nonebot plugins: set[Plugin] = nonebot.get_loaded_plugins() ``` 也可以通过插件索引名称获取插件对象: ```python import nonebot plugin: Plugin | None = nonebot.get_plugin("example") ``` 或者通过模块路径获取插件对象: ```python import nonebot plugin: Plugin | None = nonebot.get_plugin_by_module_name("awesome_bot.plugins.example") ``` 如果需要获取所有当前声明的插件名称(可能还未加载),可以使用 `get_available_plugin_names` 函数: ```python import nonebot plugin_names: set[str] = nonebot.get_available_plugin_names() ``` 插件对象 `Plugin` 中包含了多个属性: - `name`:插件索引名称 - `module`:插件模块 - `module_name`:插件模块路径 - `manager`:插件管理器 - `matcher`:插件中定义的事件响应器 - `parent_plugin`:插件的父插件 - `sub_plugins`:插件的子插件集合 - `metadata`:插件元数据 通过这些属性以及插件元数据,我们就可以收集所需要的插件信息了。 ================================================ FILE: website/versioned_docs/version-2.4.3/advanced/plugin-nesting.md ================================================ --- sidebar_position: 3 description: 编写与加载嵌套插件 options: menu: - category: advanced weight: 40 --- # 嵌套插件 NoneBot 支持嵌套插件,即一个插件可以包含其他插件。通过这种方式,我们可以将一个大型插件拆分成多个功能子插件,使得插件更加清晰、易于维护。我们可以直接在插件中使用 NoneBot 加载插件的方法来加载子插件。 ## 创建嵌套插件 我们可以在使用 `nb-cli` 命令[创建插件](../tutorial/create-plugin.md#创建插件)时,选择直接通过模板创建一个嵌套插件: ```bash $ nb plugin create [?] 插件名称: parent [?] 使用嵌套插件? (y/N) Y [?] 输出目录: awesome_bot/plugins ``` 或者使用 `nb plugin create --sub-plugin` 选项直接创建一个嵌套插件。 ## 已有插件 如果你已经有一个插件,想要在其中嵌套加载子插件,可以在插件的 `__init__.py` 中添加如下代码: ```python title=parent/__init__.py import nonebot from pathlib import Path sub_plugins = nonebot.load_plugins( str(Path(__file__).parent.joinpath("plugins").resolve()) ) ``` 这样,`parent` 插件就会加载 `parent/plugins` 目录下的所有插件。NoneBot 会正确识别这些插件的父子关系,你可以在 `parent` 的插件信息中看到这些子插件的信息,也可以在子插件信息中看到它们的父插件信息。 ================================================ FILE: website/versioned_docs/version-2.4.3/advanced/requiring.md ================================================ --- sidebar_position: 4 description: 使用其他插件提供的功能 options: menu: - category: advanced weight: 50 --- # 跨插件访问 NoneBot 插件化系统的设计使得插件之间可以功能独立、各司其职,我们可以更好地维护和扩展插件。但是,有时候我们可能需要在不同插件之间调用功能。NoneBot 生态中就有一类插件,它们专为其他插件提供功能支持,如:[定时任务插件](../best-practice/scheduler.md)、[数据存储插件](../best-practice/data-storing.md)等。这时候我们就需要在插件之间进行跨插件访问。 ## 插件跟踪 由于 NoneBot 插件系统通过 [Import Hooks](https://docs.python.org/3/reference/import.html#import-hooks) 的方式实现插件加载与跟踪管理,因此我们**不能**在 NoneBot 跟踪插件前进行模块 import,这会导致插件加载失败。即,我们不能在使用 NoneBot 提供的加载插件方法前,直接使用 `import` 语句导入插件。 对于在项目目录下的插件,我们通常直接使用 `load_from_toml` 等方法一次性加载所有插件。由于这些插件已经被声明,即便插件导入顺序不同,NoneBot 也能正确跟踪插件。此时,我们不需要对跨插件访问进行特殊处理。但当我们使用了外部插件,如果没有事先声明或加载插件,NoneBot 并不会将其当作插件进行跟踪,可能会出现意料之外的错误出现。 简单来说,我们必须在 `import` 外部插件之前,确保依赖的外部插件已经被声明或加载。 ## 插件依赖声明 NoneBot 提供了一种方法来确保我们依赖的插件已经被正确加载,即使用 `require` 函数。通过 `require` 函数,我们可以在当前插件中声明依赖的插件,NoneBot 会在加载当前插件时,检查依赖的插件是否已经被加载,如果没有,会尝试优先加载依赖的插件。 假设我们有一个插件 `a` 依赖于插件 `b`,我们可以在插件 `a` 中使用 `require` 函数声明其依赖于插件 `b`: ```python {3} title=a/__init__.py from nonebot import require require("b") from b import some_function ``` 其中,`require` 函数的参数为插件索引名称或者外部插件的模块名称。在完成依赖声明后,我们可以在插件 `a` 中直接导入插件 `b` 所提供的功能。 ================================================ FILE: website/versioned_docs/version-2.4.3/advanced/routing.md ================================================ --- sidebar_position: 9 description: 添加服务端路由规则 options: menu: - category: advanced weight: 100 --- # 添加路由 在[驱动器](./driver.md)一节中,我们了解了驱动器的两种类型。既然驱动器可以作为服务端运行,那么我们就可以向驱动器添加路由规则,从而实现自定义的 API 接口等功能。在添加路由规则时,我们需要注意驱动器的类型,详情可以参考[选择驱动器](./driver.md#配置驱动器)。 NoneBot 中,我们可以通过两种途径向 ASGI 驱动器添加路由规则: 1. 通过 NoneBot 的兼容层建立路由规则。 2. 直接向 ASGI 应用添加路由规则。 这两种途径各有优劣,前者可以在各种服务端型驱动器下运行,但并不能直接使用 ASGI 应用框架提供的特性与功能;后者直接使用 ASGI 应用,更自由、功能完整,但只能在特定类型驱动器下运行。 在向驱动器添加路由规则时,我们需要注意驱动器是否为服务端类型,我们可以通过以下方式判断: ```python from nonebot import get_driver from nonebot.drivers import ASGIMixin # highlight-next-line can_use = isinstance(get_driver(), ASGIMixin) ``` ## 通过兼容层添加路由 NoneBot 兼容层定义了两个数据类 `HTTPServerSetup` 和 `WebSocketServerSetup`,分别用于定义 HTTP 服务端和 WebSocket 服务端的路由规则。 ### HTTP 路由 `HTTPServerSetup` 具有四个属性: - `path`:路由路径,不支持特殊占位表达式。类型为 `URL`。 - `method`:请求方法。类型为 `str`。 - `name`:路由名称,不可重复。类型为 `str`。 - `handle_func`:路由处理函数。类型为 `Callable[[Request], Awaitable[Response]]`。 例如,我们添加一个 `/hello` 的路由,当请求方法为 `GET` 时,返回 `200 OK` 以及返回体信息: ```python from nonebot import get_driver from nonebot.drivers import URL, Request, Response, ASGIMixin, HTTPServerSetup async def hello(request: Request) -> Response: return Response(200, content="Hello, world!") if isinstance((driver := get_driver()), ASGIMixin): driver.setup_http_server( HTTPServerSetup( path=URL("/hello"), method="GET", name="hello", handle_func=hello, ) ) ``` 对于 `Request` 和 `Response` 的详细信息,可以参考 [API 文档](../api/drivers/index.md)。 ### WebSocket 路由 `WebSocketServerSetup` 具有三个属性: - `path`:路由路径,不支持特殊占位表达式。类型为 `URL`。 - `name`:路由名称,不可重复。类型为 `str`。 - `handle_func`:路由处理函数。类型为 `Callable[[WebSocket], Awaitable[Any]]`。 例如,我们添加一个 `/ws` 的路由,发送所有接收到的数据: ```python from nonebot import get_driver from nonebot.drivers import URL, ASGIMixin, WebSocket, WebSocketServerSetup async def ws_handler(ws: WebSocket): await ws.accept() try: while True: data = await ws.receive() await ws.send(data) except WebSocketClosed as e: # handle closed ... finally: with contextlib.suppress(Exception): await websocket.close() # do some cleanup if isinstance((driver := get_driver()), ASGIMixin): driver.setup_websocket_server( WebSocketServerSetup( path=URL("/ws"), name="ws", handle_func=ws_handler, ) ) ``` 对于 `WebSocket` 的详细信息,可以参考 [API 文档](../api/drivers/index.md)。 ## 使用 ASGI 应用添加路由 ### 获取 ASGI 应用 NoneBot 服务端类型的驱动器具有两个属性 `server_app` 和 `asgi`,分别对应驱动框架应用和 ASGI 应用。通常情况下,这两个应用是同一个对象。我们可以通过 `get_app()` 方法快速获取: ```python import nonebot app = nonebot.get_app() asgi = nonebot.get_asgi() ``` ### 添加路由规则 在获取到了 ASGI 应用后,我们就可以直接使用 ASGI 应用框架提供的功能来添加路由规则了。这里我们以 [FastAPI](./driver.md#fastapi默认) 为例,演示如何添加路由规则。 在下面的代码中,我们添加了一个 `GET` 类型的 `/api` 路由,具体方法参考 [FastAPI 文档](https://fastapi.tiangolo.com/)。 ```python import nonebot from fastapi import FastAPI app: FastAPI = nonebot.get_app() @app.get("/api") async def custom_api(): return {"message": "Hello, world!"} ``` ================================================ FILE: website/versioned_docs/version-2.4.3/advanced/runtime-hook.md ================================================ --- sidebar_position: 8 description: 在特定的生命周期中执行代码 options: menu: - category: advanced weight: 90 --- # 钩子函数 > [钩子编程](https://zh.wikipedia.org/wiki/%E9%92%A9%E5%AD%90%E7%BC%96%E7%A8%8B)(hooking),也称作“挂钩”,是计算机程序设计术语,指通过拦截软件模块间的函数调用、消息传递、事件传递来修改或扩展操作系统、应用程序或其他软件组件的行为的各种技术。处理被拦截的函数调用、事件、消息的代码,被称为钩子(hook)。 在 NoneBot 中有一系列预定义的钩子函数,可以分为两类:**全局钩子函数**和**事件处理钩子函数**,这些钩子函数可以用装饰器的形式来使用。 ## 全局钩子函数 全局钩子函数是指 NoneBot 针对其本身运行过程的钩子函数。 这些钩子函数是由驱动器来运行的,故需要先[获得全局驱动器](./driver.md#获取驱动器)。 ### 启动准备 这个钩子函数会在 NoneBot 启动时运行。很多时候,我们并不希望在模块被导入时就执行一些耗时操作,如:连接数据库,这时候我们可以在这个钩子函数中进行这些操作。 ```python from nonebot import get_driver driver = get_driver() @driver.on_startup async def do_something(): pass ``` ### 终止处理 这个钩子函数会在 NoneBot 终止时运行。我们可以在这个钩子函数中进行一些清理工作,如:关闭数据库连接。 ```python from nonebot import get_driver driver = get_driver() @driver.on_shutdown async def do_something(): pass ``` ### Bot 连接处理 这个钩子函数会在任何协议适配器连接 `Bot` 对象至 NoneBot 时运行。支持依赖注入,可以直接注入 `Bot` 对象。 ```python from nonebot import get_driver driver = get_driver() @driver.on_bot_connect async def do_something(bot: Bot): pass ``` ### Bot 断开处理 这个钩子函数会在 `Bot` 断开与 NoneBot 的连接时运行。支持依赖注入,可以直接注入 `Bot` 对象。 ```python from nonebot import get_driver driver = get_driver() @driver.on_bot_disconnect async def do_something(bot: Bot): pass ``` ## 事件处理钩子函数 这些钩子函数指的是影响 NoneBot 进行**事件处理**的函数, 这些函数可以跟普通的事件处理函数一样接受相应的参数。 ### 事件预处理 这个钩子函数会在 NoneBot 接收到新的事件时运行。支持依赖注入,可以注入 `Bot` 对象、事件、会话状态。在这个钩子函数内抛出 `nonebot.exception.IgnoredException` 会使 NoneBot 忽略该事件。 ```python from nonebot.exception import IgnoredException from nonebot.message import event_preprocessor @event_preprocessor async def do_something(event: Event): if not event.is_tome(): raise IgnoredException("some reason") ``` ### 事件后处理 这个钩子函数会在 NoneBot 处理事件完成后运行。支持依赖注入,可以注入 `Bot` 对象、事件、会话状态。 ```python from nonebot.message import event_postprocessor @event_postprocessor async def do_something(event: Event): pass ``` ### 运行预处理 这个钩子函数会在 NoneBot 运行事件响应器前运行。支持依赖注入,可以注入 `Bot` 对象、事件、事件响应器、会话状态。在这个钩子函数内抛出 `nonebot.exception.IgnoredException` 也会使 NoneBot 忽略本次运行。 ```python from nonebot.message import run_preprocessor from nonebot.exception import IgnoredException @run_preprocessor async def do_something(event: Event, matcher: Matcher): if not event.is_tome(): raise IgnoredException("some reason") ``` ### 运行后处理 这个钩子函数会在 NoneBot 运行事件响应器后运行。支持依赖注入,可以注入 `Bot` 对象、事件、事件响应器、会话状态、运行中产生的异常。 ```python from nonebot.message import run_postprocessor @run_postprocessor async def do_something(event: Event, matcher: Matcher, exception: Optional[Exception]): pass ``` ### 平台接口调用钩子 这个钩子函数会在 `Bot` 对象调用平台接口时运行。在这个钩子函数中,我们可以通过引起 `MockApiException` 异常来阻止 `Bot` 对象调用平台接口并返回指定的结果。 ```python from nonebot.adapters import Bot from nonebot.exception import MockApiException @Bot.on_calling_api async def handle_api_call(bot: Bot, api: str, data: Dict[str, Any]): if api == "send_msg": raise MockApiException(result={"message_id": 123}) ``` ### 平台接口调用后钩子 这个钩子函数会在 `Bot` 对象调用平台接口后运行。在这个钩子函数中,我们可以通过引起 `MockApiException` 异常来忽略平台接口返回的结果并返回指定的结果。 ```python from nonebot.adapters import Bot from nonebot.exception import MockApiException @Bot.on_called_api async def handle_api_result( bot: Bot, exception: Optional[Exception], api: str, data: Dict[str, Any], result: Any ): if not exception and api == "send_msg": raise MockApiException(result={**result, "message_id": 123}) ``` ================================================ FILE: website/versioned_docs/version-2.4.3/advanced/session-updating.md ================================================ --- sidebar_position: 7 description: 控制会话响应对象 options: menu: - category: advanced weight: 80 --- # 会话更新 在 NoneBot 中,在某个事件响应器对事件响应后,即是进入了会话状态,会话状态会持续到整个事件响应流程结束。会话过程中,机器人可以与用户进行多次交互。每次需要等待用户事件时,NoneBot 将会复制一个新的临时事件响应器,并更新该事件响应器使其响应当前会话主体的消息,这个过程称为会话更新。 会话更新分为两部分:**更新[事件响应器类型](./matcher.md#事件响应器类型)**和**更新[事件触发权限](./matcher.md#事件触发权限)**。 ## 更新事件响应器类型 通常情况下,与机器人用户进行的会话都是通过消息事件进行的,因此会话更新后的默认响应事件类型为 `message`。如果希望接收一个特定类型的消息,比如 `notice` 等,我们需要自定义响应事件类型更新函数。响应事件类型更新函数是一个 `Dependent`,可以使用依赖注入。 ```python {3-5} foo = on_message() @foo.type_updater async def _() -> str: return "notice" ``` 在注册了上述响应事件类型更新函数后,当我们需要等待用户事件时,将只会响应 `notice` 类型的事件。如果希望在会话过程中的不同阶段响应不同类型的事件,我们就需要使用更复杂的逻辑来更新响应事件类型(如:根据会话状态),这里将不再展示。 ## 更新事件触发权限 会话通常是由机器人与用户进行的一对一交互,因此会话更新后的默认触发权限为当前事件的会话 ID。这个会话 ID 由协议适配器生成,通常由用户 ID 和群 ID 等组成。如果希望实现更复杂的会话功能(如:多用户同时参与的会话),我们需要自定义触发权限更新函数。触发权限更新函数是一个 `Dependent`,可以使用依赖注入。 ```python {5-7} from nonebot.permission import User foo = on_message() @foo.permission_updater async def _(event: Event, matcher: Matcher) -> Permission: return Permission(User.from_event(event, perm=matcher.permission)) ``` 上述权限更新函数是默认的权限更新函数,它将会话的触发权限更新为当前事件的会话 ID。如果我们希望响应多个用户的消息,我们可以如下修改: ```python {5-7} from nonebot.permission import USER foo = on_message() @foo.permission_updater async def _(matcher: Matcher) -> Permission: return USER("session1", "session2", perm=matcher.permission) ``` 请注意,此处为全大写字母的 `USER` 权限,它可以匹配多个会话 ID。通过这种方式,我们可以实现多用户同时参与的会话。 我们已经了解了如何控制会话的更新,相信你已经能够实现更复杂的会话功能了,例如多人小游戏等等。欢迎将你的作品分享到[插件商店](/store/plugins)。 ================================================ FILE: website/versioned_docs/version-2.4.3/api/.gitkeep ================================================ ================================================ FILE: website/versioned_docs/version-2.4.3/api/adapters/_category_.json ================================================ { "position": 15 } ================================================ FILE: website/versioned_docs/version-2.4.3/api/adapters/index.md ================================================ --- mdx: format: md sidebar_position: 0 description: nonebot.adapters 模块 --- # nonebot.adapters 本模块定义了协议适配基类,各协议请继承以下基类。 使用 [Driver.register_adapter](../drivers/index.md#Driver-register-adapter) 注册适配器。 ## _abstract class_ `Adapter(driver, **kwargs)` {#Adapter} - **说明** 协议适配器基类。 通常,在 Adapter 中编写协议通信相关代码,如: 建立通信连接、处理接收与发送 data 等。 - **参数** - `driver` ([Driver](../drivers/index.md#Driver)): [Driver](../drivers/index.md#Driver) 实例 - `**kwargs` (Any): 其他由 [Driver.register_adapter](../drivers/index.md#Driver-register-adapter) 传入的额外参数 ### _instance-var_ `driver` {#Adapter-driver} - **类型:** [Driver](../drivers/index.md#Driver) - **说明:** 实例 ### _instance-var_ `bots` {#Adapter-bots} - **类型:** dict[str, [Bot](#Bot)] - **说明:** 本协议适配器已建立连接的 [Bot](#Bot) 实例 ### _abstract classmethod_ `get_name()` {#Adapter-get-name} - **说明:** 当前协议适配器的名称 - **参数** empty - **返回** - str ### _property_ `config` {#Adapter-config} - **类型:** [Config](../config.md#Config) - **说明:** 全局 NoneBot 配置 ### _method_ `bot_connect(bot)` {#Adapter-bot-connect} - **说明** 告知 NoneBot 建立了一个新的 [Bot](#Bot) 连接。 当有新的 [Bot](#Bot) 实例连接建立成功时调用。 - **参数** - `bot` ([Bot](#Bot)): [Bot](#Bot) 实例 - **返回** - None ### _method_ `bot_disconnect(bot)` {#Adapter-bot-disconnect} - **说明** 告知 NoneBot [Bot](#Bot) 连接已断开。 当有 [Bot](#Bot) 实例连接断开时调用。 - **参数** - `bot` ([Bot](#Bot)): [Bot](#Bot) 实例 - **返回** - None ### _method_ `setup_http_server(setup)` {#Adapter-setup-http-server} - **说明:** 设置一个 HTTP 服务器路由配置 - **参数** - `setup` ([HTTPServerSetup](../drivers/index.md#HTTPServerSetup)) - **返回** - untyped ### _method_ `setup_websocket_server(setup)` {#Adapter-setup-websocket-server} - **说明:** 设置一个 WebSocket 服务器路由配置 - **参数** - `setup` ([WebSocketServerSetup](../drivers/index.md#WebSocketServerSetup)) - **返回** - untyped ### _async method_ `request(setup)` {#Adapter-request} - **说明:** 进行一个 HTTP 客户端请求 - **参数** - `setup` ([Request](../drivers/index.md#Request)) - **返回** - [Response](../drivers/index.md#Response) ### _method_ `websocket(setup)` {#Adapter-websocket} - **说明:** 建立一个 WebSocket 客户端连接请求 - **参数** - `setup` ([Request](../drivers/index.md#Request)) - **返回** - AsyncGenerator[[WebSocket](../drivers/index.md#WebSocket), None] ### _method_ `on_ready(func)` {#Adapter-on-ready} - **参数** - `func` (LIFESPAN_FUNC) - **返回** - LIFESPAN_FUNC ## _abstract class_ `Bot(adapter, self_id)` {#Bot} - **说明** Bot 基类。 用于处理上报消息,并提供 API 调用接口。 - **参数** - `adapter` ([Adapter](#Adapter)): 协议适配器实例 - `self_id` (str): 机器人 ID ### _instance-var_ `adapter` {#Bot-adapter} - **类型:** [Adapter](#Adapter) - **说明:** 协议适配器实例 ### _instance-var_ `self_id` {#Bot-self-id} - **类型:** str - **说明:** 机器人 ID ### _property_ `type` {#Bot-type} - **类型:** str - **说明:** 协议适配器名称 ### _property_ `config` {#Bot-config} - **类型:** [Config](../config.md#Config) - **说明:** 全局 NoneBot 配置 ### _async method_ `call_api(api, **data)` {#Bot-call-api} - **说明:** 调用机器人 API 接口,可以通过该函数或直接通过 bot 属性进行调用 - **参数** - `api` (str): API 名称 - `**data` (Any): API 数据 - **返回** - Any - **用法** ```python await bot.call_api("send_msg", message="hello world") await bot.send_msg(message="hello world") ``` ### _abstract async method_ `send(event, message, **kwargs)` {#Bot-send} - **说明:** 调用机器人基础发送消息接口 - **参数** - `event` ([Event](#Event)): 上报事件 - `message` (str | [Message](#Message) | [MessageSegment](#MessageSegment)): 要发送的消息 - `**kwargs` (Any): 任意额外参数 - **返回** - Any ### _classmethod_ `on_calling_api(func)` {#Bot-on-calling-api} - **说明** 调用 api 预处理。 钩子函数参数: - bot: 当前 bot 对象 - api: 调用的 api 名称 - data: api 调用的参数字典 - **参数** - `func` ([T_CallingAPIHook](../typing.md#T-CallingAPIHook)) - **返回** - [T_CallingAPIHook](../typing.md#T-CallingAPIHook) ### _classmethod_ `on_called_api(func)` {#Bot-on-called-api} - **说明** 调用 api 后处理。 钩子函数参数: - bot: 当前 bot 对象 - exception: 调用 api 时发生的错误 - api: 调用的 api 名称 - data: api 调用的参数字典 - result: api 调用的返回 - **参数** - `func` ([T_CalledAPIHook](../typing.md#T-CalledAPIHook)) - **返回** - [T_CalledAPIHook](../typing.md#T-CalledAPIHook) ## _abstract class_ `Event()` {#Event} - **说明:** Event 基类。提供获取关键信息的方法,其余信息可直接获取。 - **参数** auto ### _abstract method_ `get_type()` {#Event-get-type} - **说明:** 获取事件类型的方法,类型通常为 NoneBot 内置的四种类型。 - **参数** empty - **返回** - str ### _abstract method_ `get_event_name()` {#Event-get-event-name} - **说明:** 获取事件名称的方法。 - **参数** empty - **返回** - str ### _abstract method_ `get_event_description()` {#Event-get-event-description} - **说明:** 获取事件描述的方法,通常为事件具体内容。 - **参数** empty - **返回** - str ### _method_ `get_log_string()` {#Event-get-log-string} - **说明** 获取事件日志信息的方法。 通常你不需要修改这个方法,只有当希望 NoneBot 隐藏该事件日志时, 可以抛出 `NoLogException` 异常。 - **参数** empty - **返回** - str - **异常** - NoLogException: 希望 NoneBot 隐藏该事件日志 ### _abstract method_ `get_user_id()` {#Event-get-user-id} - **说明:** 获取事件主体 id 的方法,通常是用户 id 。 - **参数** empty - **返回** - str ### _abstract method_ `get_session_id()` {#Event-get-session-id} - **说明:** 获取会话 id 的方法,用于判断当前事件属于哪一个会话, 通常是用户 id、群组 id 组合。 - **参数** empty - **返回** - str ### _abstract method_ `get_message()` {#Event-get-message} - **说明:** 获取事件消息内容的方法。 - **参数** empty - **返回** - [Message](#Message) ### _method_ `get_plaintext()` {#Event-get-plaintext} - **说明** 获取消息纯文本的方法。 通常不需要修改,默认通过 `get_message().extract_plain_text` 获取。 - **参数** empty - **返回** - str ### _abstract method_ `is_tome()` {#Event-is-tome} - **说明:** 获取事件是否与机器人有关的方法。 - **参数** empty - **返回** - bool ## _abstract class_ `Message()` {#Message} - **说明:** 消息序列 - **参数** - `message`: 消息内容 ### _classmethod_ `template(format_string)` {#Message-template} - **说明** 创建消息模板。 用法和 `str.format` 大致相同,支持以 `Message` 对象作为消息模板并输出消息对象。 并且提供了拓展的格式化控制符, 可以通过该消息类型的 `MessageSegment` 工厂方法创建消息。 - **参数** - `format_string` (str | TM): 格式化模板 - **返回** - [MessageTemplate](#MessageTemplate)[Self]: 消息格式化器 ### _abstract classmethod_ `get_segment_class()` {#Message-get-segment-class} - **说明:** 获取消息段类型 - **参数** empty - **返回** - type[TMS] ### _abstract staticmethod_ `_construct(msg)` {#Message--construct} - **说明:** 构造消息数组 - **参数** - `msg` (str) - **返回** - Iterable[TMS] ### _method_ `__getitem__(args)` {#Message---getitem--} - **重载** **1.** `(args) -> Self` - **参数** - `args` (str): 消息段类型 - **返回** - Self: 所有类型为 `args` 的消息段 **2.** `(args) -> TMS` - **参数** - `args` (tuple[str, int]): 消息段类型和索引 - **返回** - TMS: 类型为 `args[0]` 的消息段第 `args[1]` 个 **3.** `(args) -> Self` - **参数** - `args` (tuple[str, slice]): 消息段类型和切片 - **返回** - Self: 类型为 `args[0]` 的消息段切片 `args[1]` **4.** `(args) -> TMS` - **参数** - `args` (int): 索引 - **返回** - TMS: 第 `args` 个消息段 **5.** `(args) -> Self` - **参数** - `args` (slice): 切片 - **返回** - Self: 消息切片 `args` ### _method_ `__contains__(value)` {#Message---contains--} - **说明:** 检查消息段是否存在 - **参数** - `value` (TMS | str): 消息段或消息段类型 - **返回** - bool: 消息内是否存在给定消息段或给定类型的消息段 ### _method_ `has(value)` {#Message-has} - **说明:** 与 [`__contains__`](#Message---contains--) 相同 - **参数** - `value` (TMS | str) - **返回** - bool ### _method_ `index(value, *args)` {#Message-index} - **说明:** 索引消息段 - **参数** - `value` (TMS | str): 消息段或者消息段类型 - `*args` (SupportsIndex) - `arg`: start 与 end - **返回** - int: 索引 index - **异常** - ValueError: 消息段不存在 ### _method_ `get(type_, count=None)` {#Message-get} - **说明:** 获取指定类型的消息段 - **参数** - `type_` (str): 消息段类型 - `count` (int | None): 获取个数 - **返回** - Self: 构建的新消息 ### _method_ `count(value)` {#Message-count} - **说明:** 计算指定消息段的个数 - **参数** - `value` (TMS | str): 消息段或消息段类型 - **返回** - int: 个数 ### _method_ `only(value)` {#Message-only} - **说明:** 检查消息中是否仅包含指定消息段 - **参数** - `value` (TMS | str): 指定消息段或消息段类型 - **返回** - bool: 是否仅包含指定消息段 ### _method_ `append(obj)` {#Message-append} - **说明:** 添加一个消息段到消息数组末尾。 - **参数** - `obj` (str | TMS): 要添加的消息段 - **返回** - Self ### _method_ `extend(obj)` {#Message-extend} - **说明:** 拼接一个消息数组或多个消息段到消息数组末尾。 - **参数** - `obj` (Self | Iterable[TMS]): 要添加的消息数组 - **返回** - Self ### _method_ `join(iterable)` {#Message-join} - **说明:** 将多个消息连接并将自身作为分割 - **参数** - `iterable` (Iterable[TMS | Self]): 要连接的消息 - **返回** - Self: 连接后的消息 ### _method_ `copy()` {#Message-copy} - **说明:** 深拷贝消息 - **参数** empty - **返回** - Self ### _method_ `include(*types)` {#Message-include} - **说明:** 过滤消息 - **参数** - `*types` (str): 包含的消息段类型 - **返回** - Self: 新构造的消息 ### _method_ `exclude(*types)` {#Message-exclude} - **说明:** 过滤消息 - **参数** - `*types` (str): 不包含的消息段类型 - **返回** - Self: 新构造的消息 ### _method_ `extract_plain_text()` {#Message-extract-plain-text} - **说明:** 提取消息内纯文本消息 - **参数** empty - **返回** - str ## _abstract class_ `MessageSegment()` {#MessageSegment} - **说明:** 消息段基类 - **参数** auto ### _instance-var_ `type` {#MessageSegment-type} - **类型:** str - **说明:** 消息段类型 ### _class-var_ `data` {#MessageSegment-data} - **类型:** dict[str, Any] - **说明:** 消息段数据 ### _abstract classmethod_ `get_message_class()` {#MessageSegment-get-message-class} - **说明:** 获取消息数组类型 - **参数** empty - **返回** - type[TM] ### _abstract method_ `__str__()` {#MessageSegment---str--} - **说明:** 该消息段所代表的 str,在命令匹配部分使用 - **参数** empty - **返回** - str ### _method_ `__add__(other)` {#MessageSegment---add--} - **参数** - `other` (str | Self | Iterable[Self]) - **返回** - TM ### _method_ `get(key, default=None)` {#MessageSegment-get} - **参数** - `key` (str) - `default` (Any) - **返回** - untyped ### _method_ `keys()` {#MessageSegment-keys} - **参数** empty - **返回** - untyped ### _method_ `values()` {#MessageSegment-values} - **参数** empty - **返回** - untyped ### _method_ `items()` {#MessageSegment-items} - **参数** empty - **返回** - untyped ### _method_ `join(iterable)` {#MessageSegment-join} - **参数** - `iterable` (Iterable[Self | TM]) - **返回** - TM ### _method_ `copy()` {#MessageSegment-copy} - **参数** empty - **返回** - Self ### _abstract method_ `is_text()` {#MessageSegment-is-text} - **说明:** 当前消息段是否为纯文本 - **参数** empty - **返回** - bool ## _class_ `MessageTemplate(template, factory=str, private_getattr=False)` {#MessageTemplate} - **说明:** 消息模板格式化实现类。 - **参数** - `template` (str | TM): 模板 - `factory` (type[str] | type[TM]): 消息类型工厂,默认为 `str` - `private_getattr` (bool): 是否允许在模板中访问私有属性,默认为 `False` ### _method_ `add_format_spec(spec, name=None)` {#MessageTemplate-add-format-spec} - **参数** - `spec` (FormatSpecFunc_T) - `name` (str | None) - **返回** - FormatSpecFunc_T ### _method_ `format(*args, **kwargs)` {#MessageTemplate-format} - **说明:** 根据传入参数和模板生成消息对象 - **参数** - `*args` - `**kwargs` - **返回** - TF ### _method_ `format_map(mapping)` {#MessageTemplate-format-map} - **说明:** 根据传入字典和模板生成消息对象, 在传入字段名不是有效标识符时有用 - **参数** - `mapping` (Mapping[str, Any]) - **返回** - TF ### _method_ `vformat(format_string, args, kwargs)` {#MessageTemplate-vformat} - **参数** - `format_string` (str) - `args` (Sequence[Any]) - `kwargs` (Mapping[str, Any]) - **返回** - TF ### _method_ `get_field(field_name, args, kwargs)` {#MessageTemplate-get-field} - **参数** - `field_name` (str) - `args` (Sequence[Any]) - `kwargs` (Mapping[str, Any]) - **返回** - tuple[Any, int | str] ### _method_ `format_field(value, format_spec)` {#MessageTemplate-format-field} - **参数** - `value` (Any) - `format_spec` (str) - **返回** - Any ================================================ FILE: website/versioned_docs/version-2.4.3/api/compat.md ================================================ --- mdx: format: md sidebar_position: 16 description: nonebot.compat 模块 --- # nonebot.compat 本模块为 Pydantic 版本兼容层模块 为兼容 Pydantic V1 与 V2 版本,定义了一系列兼容函数与类供使用。 ## _var_ `Required` {#Required} - **类型:** untyped - **说明:** Alias of Ellipsis for compatibility with pydantic v1 ## _library-attr_ `PydanticUndefined` {#PydanticUndefined} - **说明:** Pydantic Undefined object ## _library-attr_ `PydanticUndefinedType` {#PydanticUndefinedType} - **说明:** Pydantic Undefined type ## _var_ `DEFAULT_CONFIG` {#DEFAULT-CONFIG} - **类型:** untyped - **说明:** Default config for validations ## _class_ `FieldInfo(default=PydanticUndefined, **kwargs)` {#FieldInfo} - **说明:** FieldInfo class with extra property for compatibility with pydantic v1 - **参数** - `default` (Any) - `**kwargs` (Any) ### _property_ `extra` {#FieldInfo-extra} - **类型:** dict[str, Any] - **说明** Extra data that is not part of the standard pydantic fields. For compatibility with pydantic v1. ## _class_ `ModelField()` {#ModelField} - **说明:** ModelField class for compatibility with pydantic v1 - **参数** auto ### _instance-var_ `name` {#ModelField-name} - **类型:** str - **说明:** The name of the field. ### _instance-var_ `annotation` {#ModelField-annotation} - **类型:** Any - **说明:** The annotation of the field. ### _instance-var_ `field_info` {#ModelField-field-info} - **类型:** FieldInfo - **说明:** The FieldInfo of the field. ### _classmethod_ `construct(name, annotation, field_info=None)` {#ModelField-construct} - **说明:** Construct a ModelField from given infos. - **参数** - `name` (str) - `annotation` (Any) - `field_info` (FieldInfo | None) - **返回** - Self ### _method_ `get_default()` {#ModelField-get-default} - **说明:** Get the default value of the field. - **参数** empty - **返回** - Any ### _method_ `validate_value(value)` {#ModelField-validate-value} - **说明:** Validate the value pass to the field. - **参数** - `value` (Any) - **返回** - 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. - **参数** - `model` (type[BaseModel]) - **返回** - list[ModelField] ## _def_ `model_config(model)` {#model-config} - **说明:** Get config of a model. - **参数** - `model` (type[BaseModel]) - **返回** - Any ## _def_ `model_dump(model, include=None, exclude=None, by_alias=False, exclude_unset=False, exclude_defaults=False, exclude_none=False)` {#model-dump} - **参数** - `model` (BaseModel) - `include` (set[str] | None) - `exclude` (set[str] | None) - `by_alias` (bool) - `exclude_unset` (bool) - `exclude_defaults` (bool) - `exclude_none` (bool) - **返回** - dict[str, Any] ## _def_ `type_validate_python(type_, data)` {#type-validate-python} - **说明:** Validate data with given type. - **参数** - `type_` (type[T]) - `data` (Any) - **返回** - T ## _def_ `type_validate_json(type_, data)` {#type-validate-json} - **说明:** Validate JSON with given type. - **参数** - `type_` (type[T]) - `data` (str | bytes) - **返回** - T ## _def_ `custom_validation(class_)` {#custom-validation} - **说明:** Use pydantic v1 like validator generator in pydantic v2 - **参数** - `class_` (type[CVC]) - **返回** - type[CVC] ================================================ FILE: website/versioned_docs/version-2.4.3/api/config.md ================================================ --- mdx: format: md sidebar_position: 1 description: nonebot.config 模块 --- # nonebot.config 本模块定义了 NoneBot 本身运行所需的配置项。 NoneBot 使用 [`pydantic`](https://pydantic-docs.helpmanual.io/) 以及 [`python-dotenv`](https://saurabh-kumar.com/python-dotenv/) 来读取配置。 配置项需符合特殊格式或 json 序列化格式 详情见 [`pydantic Field Type`](https://pydantic-docs.helpmanual.io/usage/types/) 文档。 ## _class_ `Env(_env_file=ENV_FILE_SENTINEL, _env_file_encoding=None, _env_nested_delimiter=None, **values)` {#Env} - **说明** 运行环境配置。大小写不敏感。 将会从 **环境变量** > **dotenv 配置文件** 的优先级读取环境信息。 - **参数** - `_env_file` (DOTENV_TYPE | None) - `_env_file_encoding` (str | None) - `_env_nested_delimiter` (str | None) - `**values` (Any) ### _class-var_ `environment` {#Env-environment} - **类型:** str - **说明** 当前环境名。 NoneBot 将从 `.env.{environment}` 文件中加载配置。 ## _class_ `Config(_env_file=ENV_FILE_SENTINEL, _env_file_encoding=None, _env_nested_delimiter=None, **values)` {#Config} - **说明** NoneBot 主要配置。大小写不敏感。 除了 NoneBot 的配置项外,还可以自行添加配置项到 `.env.{environment}` 文件中。 这些配置将会在 json 反序列化后一起带入 `Config` 类中。 配置方法参考: [配置](https://nonebot.dev/docs/appendices/config) - **参数** - `_env_file` (DOTENV_TYPE | None) - `_env_file_encoding` (str | None) - `_env_nested_delimiter` (str | None) - `**values` (Any) ### _class-var_ `driver` {#Config-driver} - **类型:** str - **说明** NoneBot 运行所使用的 `Driver` 。继承自 [Driver](drivers/index.md#Driver) 。 配置格式为 `[:][+[:]]*`。 `~` 为 `nonebot.drivers.` 的缩写。 配置方法参考: [配置驱动器](https://nonebot.dev/docs/advanced/driver#%E9%85%8D%E7%BD%AE%E9%A9%B1%E5%8A%A8%E5%99%A8) ### _class-var_ `host` {#Config-host} - **类型:** IPvAnyAddress - **说明:** NoneBot [ReverseDriver](drivers/index.md#ReverseDriver) 服务端监听的 IP/主机名。 ### _class-var_ `port` {#Config-port} - **类型:** int - **说明:** NoneBot [ReverseDriver](drivers/index.md#ReverseDriver) 服务端监听的端口。 ### _class-var_ `log_level` {#Config-log-level} - **类型:** int | str - **说明** NoneBot 日志输出等级,可以为 `int` 类型等级或等级名称。 参考 [记录日志](https://nonebot.dev/docs/appendices/log),[loguru 日志等级](https://loguru.readthedocs.io/en/stable/api/logger.html#levels)。 :::tip 提示 日志等级名称应为大写,如 `INFO`。 ::: - **用法** ```conf LOG_LEVEL=25 LOG_LEVEL=INFO ``` ### _class-var_ `api_timeout` {#Config-api-timeout} - **类型:** float | None - **说明:** API 请求超时时间,单位: 秒。 ### _class-var_ `superusers` {#Config-superusers} - **类型:** set[str] - **说明:** 机器人超级用户。 - **用法** ```conf SUPERUSERS=["12345789"] ``` ### _class-var_ `nickname` {#Config-nickname} - **类型:** set[str] - **说明:** 机器人昵称。 ### _class-var_ `command_start` {#Config-command-start} - **类型:** set[str] - **说明** 命令的起始标记,用于判断一条消息是不是命令。 参考[命令响应规则](https://nonebot.dev/docs/advanced/matcher#command)。 - **用法** ```conf COMMAND_START=["/", ""] ``` ### _class-var_ `command_sep` {#Config-command-sep} - **类型:** set[str] - **说明** 命令的分隔标记,用于将文本形式的命令切分为元组(实际的命令名)。 参考[命令响应规则](https://nonebot.dev/docs/advanced/matcher#command)。 - **用法** ```conf COMMAND_SEP=["."] ``` ### _class-var_ `session_expire_timeout` {#Config-session-expire-timeout} - **类型:** timedelta - **说明:** 等待用户回复的超时时间。 - **用法** ```conf SESSION_EXPIRE_TIMEOUT=[-][DD]D[,][HH:MM:]SS[.ffffff] SESSION_EXPIRE_TIMEOUT=[±]P[DD]DT[HH]H[MM]M[SS]S # ISO 8601 ``` ================================================ FILE: website/versioned_docs/version-2.4.3/api/consts.md ================================================ --- mdx: format: md sidebar_position: 9 description: nonebot.consts 模块 --- # nonebot.consts 本模块包含了 NoneBot 事件处理过程中使用到的常量。 ## _var_ `RECEIVE_KEY` {#RECEIVE-KEY} - **类型:** Literal['\_receive\_{id}'] - **说明:** `receive` 存储 key ## _var_ `LAST_RECEIVE_KEY` {#LAST-RECEIVE-KEY} - **类型:** Literal['\_last\_receive'] - **说明:** `last_receive` 存储 key ## _var_ `ARG_KEY` {#ARG-KEY} - **类型:** Literal['{key}'] - **说明:** `arg` 存储 key ## _var_ `REJECT_TARGET` {#REJECT-TARGET} - **类型:** Literal['\_current\_target'] - **说明:** 当前 `reject` 目标存储 key ## _var_ `REJECT_CACHE_TARGET` {#REJECT-CACHE-TARGET} - **类型:** Literal['\_next\_target'] - **说明:** 下一个 `reject` 目标存储 key ## _var_ `PAUSE_PROMPT_RESULT_KEY` {#PAUSE-PROMPT-RESULT-KEY} - **类型:** Literal['\_pause\_result'] - **说明:** `pause` prompt 发送结果存储 key ## _var_ `REJECT_PROMPT_RESULT_KEY` {#REJECT-PROMPT-RESULT-KEY} - **类型:** Literal['\_reject\_{key}\_result'] - **说明:** `reject` prompt 发送结果存储 key ## _var_ `PREFIX_KEY` {#PREFIX-KEY} - **类型:** Literal['\_prefix'] - **说明:** 命令前缀存储 key ## _var_ `CMD_KEY` {#CMD-KEY} - **类型:** Literal['command'] - **说明:** 命令元组存储 key ## _var_ `RAW_CMD_KEY` {#RAW-CMD-KEY} - **类型:** Literal['raw\_command'] - **说明:** 命令文本存储 key ## _var_ `CMD_ARG_KEY` {#CMD-ARG-KEY} - **类型:** Literal['command\_arg'] - **说明:** 命令参数存储 key ## _var_ `CMD_START_KEY` {#CMD-START-KEY} - **类型:** Literal['command\_start'] - **说明:** 命令开头存储 key ## _var_ `CMD_WHITESPACE_KEY` {#CMD-WHITESPACE-KEY} - **类型:** Literal['command\_whitespace'] - **说明:** 命令与参数间空白符存储 key ## _var_ `SHELL_ARGS` {#SHELL-ARGS} - **类型:** Literal['\_args'] - **说明:** shell 命令 parse 后参数字典存储 key ## _var_ `SHELL_ARGV` {#SHELL-ARGV} - **类型:** Literal['\_argv'] - **说明:** shell 命令原始参数列表存储 key ## _var_ `REGEX_MATCHED` {#REGEX-MATCHED} - **类型:** Literal['\_matched'] - **说明:** 正则匹配结果存储 key ## _var_ `STARTSWITH_KEY` {#STARTSWITH-KEY} - **类型:** Literal['\_startswith'] - **说明:** 响应触发前缀 key ## _var_ `ENDSWITH_KEY` {#ENDSWITH-KEY} - **类型:** Literal['\_endswith'] - **说明:** 响应触发后缀 key ## _var_ `FULLMATCH_KEY` {#FULLMATCH-KEY} - **类型:** Literal['\_fullmatch'] - **说明:** 响应触发完整消息 key ## _var_ `KEYWORD_KEY` {#KEYWORD-KEY} - **类型:** Literal['\_keyword'] - **说明:** 响应触发关键字 key ================================================ FILE: website/versioned_docs/version-2.4.3/api/dependencies/_category_.json ================================================ { "position": 13 } ================================================ FILE: website/versioned_docs/version-2.4.3/api/dependencies/index.md ================================================ --- mdx: format: md sidebar_position: 0 description: nonebot.dependencies 模块 --- # nonebot.dependencies 本模块模块实现了依赖注入的定义与处理。 ## _abstract class_ `Param(*args, validate=False, **kwargs)` {#Param} - **说明** 依赖注入的基本单元 —— 参数。 继承自 `pydantic.fields.FieldInfo`,用于描述参数信息(不包括参数名)。 - **参数** - `*args` - `validate` (bool) - `**kwargs` (Any) ## _class_ `Dependent()` {#Dependent} - **说明:** 依赖注入容器 - **参数** - `call`: 依赖注入的可调用对象,可以是任何 Callable 对象 - `pre_checkers`: 依赖注入解析前的参数检查 - `params`: 具名参数列表 - `parameterless`: 匿名参数列表 - `allow_types`: 允许的参数类型 ### _staticmethod_ `parse_params(call, allow_types)` {#Dependent-parse-params} - **参数** - `call` (\_DependentCallable[R]) - `allow_types` (tuple[type[Param], ...]) - **返回** - tuple[[ModelField](../compat.md#ModelField), ...] ### _staticmethod_ `parse_parameterless(parameterless, allow_types)` {#Dependent-parse-parameterless} - **参数** - `parameterless` (tuple[Any, ...]) - `allow_types` (tuple[type[Param], ...]) - **返回** - tuple[Param, ...] ### _classmethod_ `parse(*, call, parameterless=None, allow_types)` {#Dependent-parse} - **参数** - `call` (\_DependentCallable[R]) - `parameterless` (Iterable[Any] | None) - `allow_types` (Iterable[type[Param]]) - **返回** - Dependent[R] ### _async method_ `check(**params)` {#Dependent-check} - **参数** - `**params` (Any) - **返回** - None ### _async method_ `solve(**params)` {#Dependent-solve} - **参数** - `**params` (Any) - **返回** - dict[str, Any] ================================================ FILE: website/versioned_docs/version-2.4.3/api/dependencies/utils.md ================================================ --- mdx: format: md sidebar_position: 1 description: nonebot.dependencies.utils 模块 --- # nonebot.dependencies.utils ## _def_ `get_typed_signature(call)` {#get-typed-signature} - **说明:** 获取可调用对象签名 - **参数** - `call` ((...) -> Any) - **返回** - inspect.Signature ## _def_ `get_typed_annotation(param, globalns)` {#get-typed-annotation} - **说明:** 获取参数的类型注解 - **参数** - `param` (inspect.Parameter) - `globalns` (dict[str, Any]) - **返回** - Any ## _def_ `check_field_type(field, value)` {#check-field-type} - **说明:** 检查字段类型是否匹配 - **参数** - `field` ([ModelField](../compat.md#ModelField)) - `value` (Any) - **返回** - Any ================================================ FILE: website/versioned_docs/version-2.4.3/api/drivers/_category_.json ================================================ { "position": 14 } ================================================ FILE: website/versioned_docs/version-2.4.3/api/drivers/aiohttp.md ================================================ --- mdx: format: md sidebar_position: 2 description: nonebot.drivers.aiohttp 模块 --- # nonebot.drivers.aiohttp [AIOHTTP](https://aiohttp.readthedocs.io/en/stable/) 驱动适配器。 ```bash nb driver install aiohttp # 或者 pip install nonebot2[aiohttp] ``` :::tip 提示 本驱动仅支持客户端连接 ::: ## _class_ `Session(params=None, headers=None, cookies=None, version=HTTPVersion.H11, timeout=None, proxy=None)` {#Session} - **参数** - `params` (QueryTypes) - `headers` (HeaderTypes) - `cookies` (CookieTypes) - `version` (str | [HTTPVersion](index.md#HTTPVersion)) - `timeout` (TimeoutTypes) - `proxy` (str | None) ### _async method_ `request(setup)` {#Session-request} - **参数** - `setup` ([Request](index.md#Request)) - **返回** - [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} - **参数** empty - **返回** - None ### _async method_ `close()` {#Session-close} - **参数** empty - **返回** - None ## _class_ `Mixin()` {#Mixin} - **说明:** AIOHTTP Mixin - **参数** auto ### _async method_ `request(setup)` {#Mixin-request} - **参数** - `setup` ([Request](index.md#Request)) - **返回** - [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} - **参数** - `setup` ([Request](index.md#Request)) - **返回** - 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} - **参数** - `params` (QueryTypes) - `headers` (HeaderTypes) - `cookies` (CookieTypes) - `version` (str | [HTTPVersion](index.md#HTTPVersion)) - `timeout` (TimeoutTypes) - `proxy` (str | None) - **返回** - Session ## _class_ `WebSocket(*, request, session, websocket)` {#WebSocket} - **说明:** AIOHTTP Websocket Wrapper - **参数** - `request` ([Request](index.md#Request)) - `session` (aiohttp.ClientSession) - `websocket` (aiohttp.ClientWebSocketResponse) ### _async method_ `accept()` {#WebSocket-accept} - **参数** empty - **返回** - untyped ### _async method_ `close(code=1000, reason="")` {#WebSocket-close} - **参数** - `code` (int) - `reason` (str) - **返回** - untyped ### _async method_ `receive()` {#WebSocket-receive} - **参数** empty - **返回** - str ### _async method_ `receive_text()` {#WebSocket-receive-text} - **参数** empty - **返回** - str ### _async method_ `receive_bytes()` {#WebSocket-receive-bytes} - **参数** empty - **返回** - bytes ### _async method_ `send_text(data)` {#WebSocket-send-text} - **参数** - `data` (str) - **返回** - None ### _async method_ `send_bytes(data)` {#WebSocket-send-bytes} - **参数** - `data` (bytes) - **返回** - None ## _class_ `Driver(env, config)` {#Driver} - **参数** - `env` ([Env](../config.md#Env)) - `config` ([Config](../config.md#Config)) ================================================ FILE: website/versioned_docs/version-2.4.3/api/drivers/fastapi.md ================================================ --- mdx: format: md sidebar_position: 1 description: nonebot.drivers.fastapi 模块 --- # nonebot.drivers.fastapi [FastAPI](https://fastapi.tiangolo.com/) 驱动适配 ```bash nb driver install fastapi # 或者 pip install nonebot2[fastapi] ``` :::tip 提示 本驱动仅支持服务端连接 ::: ## _class_ `Config()` {#Config} - **说明:** FastAPI 驱动框架设置,详情参考 FastAPI 文档 - **参数** auto ### _class-var_ `fastapi_openapi_url` {#Config-fastapi-openapi-url} - **类型:** str | None - **说明:** `openapi.json` 地址,默认为 `None` 即关闭 ### _class-var_ `fastapi_docs_url` {#Config-fastapi-docs-url} - **类型:** str | None - **说明:** `swagger` 地址,默认为 `None` 即关闭 ### _class-var_ `fastapi_redoc_url` {#Config-fastapi-redoc-url} - **类型:** str | None - **说明:** `redoc` 地址,默认为 `None` 即关闭 ### _class-var_ `fastapi_include_adapter_schema` {#Config-fastapi-include-adapter-schema} - **类型:** bool - **说明:** 是否包含适配器路由的 schema,默认为 `True` ### _class-var_ `fastapi_reload` {#Config-fastapi-reload} - **类型:** bool - **说明:** 开启/关闭冷重载 ### _class-var_ `fastapi_reload_dirs` {#Config-fastapi-reload-dirs} - **类型:** list[str] | None - **说明:** 重载监控文件夹列表,默认为 uvicorn 默认值 ### _class-var_ `fastapi_reload_delay` {#Config-fastapi-reload-delay} - **类型:** float - **说明:** 重载延迟,默认为 uvicorn 默认值 ### _class-var_ `fastapi_reload_includes` {#Config-fastapi-reload-includes} - **类型:** list[str] | None - **说明:** 要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值 ### _class-var_ `fastapi_reload_excludes` {#Config-fastapi-reload-excludes} - **类型:** list[str] | None - **说明:** 不要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值 ### _class-var_ `fastapi_extra` {#Config-fastapi-extra} - **类型:** dict[str, Any] - **说明:** 传递给 `FastAPI` 的其他参数。 ## _class_ `Driver(env, config)` {#Driver} - **说明:** FastAPI 驱动框架。 - **参数** - `env` ([Env](../config.md#Env)) - `config` (NoneBotConfig) ### _property_ `type` {#Driver-type} - **类型:** str - **说明:** 驱动名称: `fastapi` ### _property_ `server_app` {#Driver-server-app} - **类型:** FastAPI - **说明:** `FastAPI APP` 对象 ### _property_ `asgi` {#Driver-asgi} - **类型:** FastAPI - **说明:** `FastAPI APP` 对象 ### _property_ `logger` {#Driver-logger} - **类型:** logging.Logger - **说明:** fastapi 使用的 logger ### _method_ `setup_http_server(setup)` {#Driver-setup-http-server} - **参数** - `setup` ([HTTPServerSetup](index.md#HTTPServerSetup)) - **返回** - untyped ### _method_ `setup_websocket_server(setup)` {#Driver-setup-websocket-server} - **参数** - `setup` ([WebSocketServerSetup](index.md#WebSocketServerSetup)) - **返回** - None ### _method_ `run(host=None, port=None, *args, app=None, **kwargs)` {#Driver-run} - **说明:** 使用 `uvicorn` 启动 FastAPI - **参数** - `host` (str | None) - `port` (int | None) - `*args` - `app` (str | None) - `**kwargs` - **返回** - untyped ## _class_ `FastAPIWebSocket(*, request, websocket)` {#FastAPIWebSocket} - **说明:** FastAPI WebSocket Wrapper - **参数** - `request` (BaseRequest) - `websocket` ([WebSocket](index.md#WebSocket)) ### _async method_ `accept()` {#FastAPIWebSocket-accept} - **参数** empty - **返回** - None ### _async method_ `close(code=status.WS_1000_NORMAL_CLOSURE, reason="")` {#FastAPIWebSocket-close} - **参数** - `code` (int) - `reason` (str) - **返回** - None ### _async method_ `receive()` {#FastAPIWebSocket-receive} - **参数** empty - **返回** - str | bytes ### _async method_ `receive_text()` {#FastAPIWebSocket-receive-text} - **参数** empty - **返回** - str ### _async method_ `receive_bytes()` {#FastAPIWebSocket-receive-bytes} - **参数** empty - **返回** - bytes ### _async method_ `send_text(data)` {#FastAPIWebSocket-send-text} - **参数** - `data` (str) - **返回** - None ### _async method_ `send_bytes(data)` {#FastAPIWebSocket-send-bytes} - **参数** - `data` (bytes) - **返回** - None ================================================ FILE: website/versioned_docs/version-2.4.3/api/drivers/httpx.md ================================================ --- mdx: format: md sidebar_position: 3 description: nonebot.drivers.httpx 模块 --- # nonebot.drivers.httpx [HTTPX](https://www.python-httpx.org/) 驱动适配 ```bash nb driver install httpx # 或者 pip install nonebot2[httpx] ``` :::tip 提示 本驱动仅支持客户端 HTTP 连接 ::: ## _class_ `Session(params=None, headers=None, cookies=None, version=HTTPVersion.H11, timeout=None, proxy=None)` {#Session} - **参数** - `params` (QueryTypes) - `headers` (HeaderTypes) - `cookies` (CookieTypes) - `version` (str | [HTTPVersion](index.md#HTTPVersion)) - `timeout` (TimeoutTypes) - `proxy` (str | None) ### _async method_ `request(setup)` {#Session-request} - **参数** - `setup` ([Request](index.md#Request)) - **返回** - [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} - **参数** empty - **返回** - None ### _async method_ `close()` {#Session-close} - **参数** empty - **返回** - None ## _class_ `Mixin()` {#Mixin} - **说明:** HTTPX Mixin - **参数** auto ### _async method_ `request(setup)` {#Mixin-request} - **参数** - `setup` ([Request](index.md#Request)) - **返回** - [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} - **参数** - `params` (QueryTypes) - `headers` (HeaderTypes) - `cookies` (CookieTypes) - `version` (str | [HTTPVersion](index.md#HTTPVersion)) - `timeout` (TimeoutTypes) - `proxy` (str | None) - **返回** - Session ## _class_ `Driver(env, config)` {#Driver} - **参数** - `env` ([Env](../config.md#Env)) - `config` ([Config](../config.md#Config)) ================================================ FILE: website/versioned_docs/version-2.4.3/api/drivers/index.md ================================================ --- mdx: format: md sidebar_position: 0 description: nonebot.drivers 模块 --- # nonebot.drivers 本模块定义了驱动适配器基类。 各驱动请继承以下基类。 ## _abstract class_ `ASGIMixin()` {#ASGIMixin} - **说明** ASGI 服务端基类。 将后端框架封装,以满足适配器使用。 - **参数** auto ### _abstract property_ `server_app` {#ASGIMixin-server-app} - **类型:** Any - **说明:** 驱动 APP 对象 ### _abstract property_ `asgi` {#ASGIMixin-asgi} - **类型:** Any - **说明:** 驱动 ASGI 对象 ### _abstract method_ `setup_http_server(setup)` {#ASGIMixin-setup-http-server} - **说明:** 设置一个 HTTP 服务器路由配置 - **参数** - `setup` ([HTTPServerSetup](#HTTPServerSetup)) - **返回** - None ### _abstract method_ `setup_websocket_server(setup)` {#ASGIMixin-setup-websocket-server} - **说明:** 设置一个 WebSocket 服务器路由配置 - **参数** - `setup` ([WebSocketServerSetup](#WebSocketServerSetup)) - **返回** - None ## _class_ `Cookies(cookies=None)` {#Cookies} - **参数** - `cookies` (CookieTypes) ### _method_ `set(name, value, domain="", path="/")` {#Cookies-set} - **参数** - `name` (str) - `value` (str) - `domain` (str) - `path` (str) - **返回** - None ### _method_ `get(name, default=None, domain=None, path=None)` {#Cookies-get} - **参数** - `name` (str) - `default` (str | None) - `domain` (str | None) - `path` (str | None) - **返回** - str | None ### _method_ `delete(name, domain=None, path=None)` {#Cookies-delete} - **参数** - `name` (str) - `domain` (str | None) - `path` (str | None) - **返回** - None ### _method_ `clear(domain=None, path=None)` {#Cookies-clear} - **参数** - `domain` (str | None) - `path` (str | None) - **返回** - None ### _method_ `update(cookies=None)` {#Cookies-update} - **参数** - `cookies` (CookieTypes) - **返回** - None ### _method_ `as_header(request)` {#Cookies-as-header} - **参数** - `request` (Request) - **返回** - dict[str, str] ## _abstract class_ `Driver(env, config)` {#Driver} - **说明** 驱动器基类。 驱动器控制框架的启动和停止,适配器的注册,以及机器人生命周期管理。 - **参数** - `env` ([Env](../config.md#Env)): 包含环境信息的 Env 对象 - `config` ([Config](../config.md#Config)): 包含配置信息的 Config 对象 ### _instance-var_ `env` {#Driver-env} - **类型:** str - **说明:** 环境名称 ### _instance-var_ `config` {#Driver-config} - **类型:** [Config](../config.md#Config) - **说明:** 全局配置对象 ### _property_ `bots` {#Driver-bots} - **类型:** dict[str, [Bot](../adapters/index.md#Bot)] - **说明:** 获取当前所有已连接的 Bot ### _method_ `register_adapter(adapter, **kwargs)` {#Driver-register-adapter} - **说明:** 注册一个协议适配器 - **参数** - `adapter` (type[[Adapter](../adapters/index.md#Adapter)]): 适配器类 - `**kwargs`: 其他传递给适配器的参数 - **返回** - None ### _abstract property_ `type` {#Driver-type} - **类型:** str - **说明:** 驱动类型名称 ### _abstract property_ `logger` {#Driver-logger} - **类型:** untyped - **说明:** 驱动专属 logger 日志记录器 ### _abstract method_ `run(*args, **kwargs)` {#Driver-run} - **说明:** 启动驱动框架 - **参数** - `*args` - `**kwargs` - **返回** - untyped ### _method_ `on_startup(func)` {#Driver-on-startup} - **说明:** 注册一个启动时执行的函数 - **参数** - `func` (LIFESPAN_FUNC) - **返回** - LIFESPAN_FUNC ### _method_ `on_shutdown(func)` {#Driver-on-shutdown} - **说明:** 注册一个停止时执行的函数 - **参数** - `func` (LIFESPAN_FUNC) - **返回** - LIFESPAN_FUNC ### _classmethod_ `on_bot_connect(func)` {#Driver-on-bot-connect} - **说明** 装饰一个函数使他在 bot 连接成功时执行。 钩子函数参数: - bot: 当前连接上的 Bot 对象 - **参数** - `func` ([T_BotConnectionHook](../typing.md#T-BotConnectionHook)) - **返回** - [T_BotConnectionHook](../typing.md#T-BotConnectionHook) ### _classmethod_ `on_bot_disconnect(func)` {#Driver-on-bot-disconnect} - **说明** 装饰一个函数使他在 bot 连接断开时执行。 钩子函数参数: - bot: 当前连接上的 Bot 对象 - **参数** - `func` ([T_BotDisconnectionHook](../typing.md#T-BotDisconnectionHook)) - **返回** - [T_BotDisconnectionHook](../typing.md#T-BotDisconnectionHook) ## _var_ `ForwardDriver` {#ForwardDriver} - **类型:** ForwardMixin - **说明** 支持客户端请求的驱动器。 **Deprecated**,请使用 [ForwardMixin](#ForwardMixin) 或其子类代替。 ## _abstract class_ `ForwardMixin()` {#ForwardMixin} - **说明:** 客户端混入基类。 - **参数** auto ## _abstract class_ `HTTPClientMixin()` {#HTTPClientMixin} - **说明:** HTTP 客户端混入基类。 - **参数** auto ### _abstract async method_ `request(setup)` {#HTTPClientMixin-request} - **说明:** 发送一个 HTTP 请求 - **参数** - `setup` ([Request](#Request)) - **返回** - [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 会话 - **参数** - `params` (QueryTypes) - `headers` (HeaderTypes) - `cookies` (CookieTypes) - `version` (str | [HTTPVersion](#HTTPVersion)) - `timeout` (TimeoutTypes) - `proxy` (str | None) - **返回** - HTTPClientSession ## _class_ `HTTPServerSetup()` {#HTTPServerSetup} - **说明:** HTTP 服务器路由配置。 - **参数** auto ## _enum_ `HTTPVersion` {#HTTPVersion} - **参数** auto - `H10: '1.0'` - `H11: '1.1'` - `H2: '2'` ## _abstract class_ `Mixin()` {#Mixin} - **说明:** 可与其他驱动器共用的混入基类。 - **参数** auto ### _abstract property_ `type` {#Mixin-type} - **类型:** str - **说明:** 混入驱动类型名称 ## _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} - **参数** - `method` (str | bytes) - `url` (URL | str | RawURL) - `params` (QueryTypes) - `headers` (HeaderTypes) - `cookies` (CookieTypes) - `content` (ContentTypes) - `data` (DataTypes) - `json` (Any) - `files` (FilesTypes) - `version` (str | HTTPVersion) - `timeout` (TimeoutTypes) - `proxy` (str | None) ## _class_ `Response(status_code, *, headers=None, content=None, request=None)` {#Response} - **参数** - `status_code` (int) - `headers` (HeaderTypes) - `content` (ContentTypes) - `request` (Request | None) ## _var_ `ReverseDriver` {#ReverseDriver} - **类型:** ReverseMixin - **说明** 支持服务端请求的驱动器。 **Deprecated**,请使用 [ReverseMixin](#ReverseMixin) 或其子类代替。 ## _abstract class_ `ReverseMixin()` {#ReverseMixin} - **说明:** 服务端混入基类。 - **参数** auto ## _class_ `Timeout()` {#Timeout} - **说明:** Request 超时配置。 - **参数** auto ## _abstract class_ `WebSocket(*, request)` {#WebSocket} - **参数** - `request` (Request) ### _abstract property_ `closed` {#WebSocket-closed} - **类型:** bool - **说明:** 连接是否已经关闭 ### _abstract async method_ `accept()` {#WebSocket-accept} - **说明:** 接受 WebSocket 连接请求 - **参数** empty - **返回** - None ### _abstract async method_ `close(code=1000, reason="")` {#WebSocket-close} - **说明:** 关闭 WebSocket 连接请求 - **参数** - `code` (int) - `reason` (str) - **返回** - None ### _abstract async method_ `receive()` {#WebSocket-receive} - **说明:** 接收一条 WebSocket text/bytes 信息 - **参数** empty - **返回** - str | bytes ### _abstract async method_ `receive_text()` {#WebSocket-receive-text} - **说明:** 接收一条 WebSocket text 信息 - **参数** empty - **返回** - str ### _abstract async method_ `receive_bytes()` {#WebSocket-receive-bytes} - **说明:** 接收一条 WebSocket binary 信息 - **参数** empty - **返回** - bytes ### _async method_ `send(data)` {#WebSocket-send} - **说明:** 发送一条 WebSocket text/bytes 信息 - **参数** - `data` (str | bytes) - **返回** - None ### _abstract async method_ `send_text(data)` {#WebSocket-send-text} - **说明:** 发送一条 WebSocket text 信息 - **参数** - `data` (str) - **返回** - None ### _abstract async method_ `send_bytes(data)` {#WebSocket-send-bytes} - **说明:** 发送一条 WebSocket binary 信息 - **参数** - `data` (bytes) - **返回** - None ## _abstract class_ `WebSocketClientMixin()` {#WebSocketClientMixin} - **说明:** WebSocket 客户端混入基类。 - **参数** auto ### _abstract method_ `websocket(setup)` {#WebSocketClientMixin-websocket} - **说明:** 发起一个 WebSocket 连接 - **参数** - `setup` ([Request](#Request)) - **返回** - AsyncGenerator[[WebSocket](#WebSocket), None] ## _class_ `WebSocketServerSetup()` {#WebSocketServerSetup} - **说明:** WebSocket 服务器路由配置。 - **参数** auto ## _def_ `combine_driver(driver, *mixins)` {#combine-driver} - **说明:** 将一个驱动器和多个混入类合并。 - **重载** **1.** `(driver) -> type[D]` - **参数** - `driver` (type[D]) - **返回** - type[D] **2.** `(driver, __m, /, *mixins) -> type[CombinedDriver]` - **参数** - `driver` (type[D]) - `__m` (type[[Mixin](#Mixin)]) - `*mixins` (type[[Mixin](#Mixin)]) - **返回** - type[CombinedDriver] ================================================ FILE: website/versioned_docs/version-2.4.3/api/drivers/none.md ================================================ --- mdx: format: md sidebar_position: 6 description: nonebot.drivers.none 模块 --- # nonebot.drivers.none None 驱动适配 :::tip 提示 本驱动不支持任何服务器或客户端连接 ::: ## _class_ `Driver(env, config)` {#Driver} - **说明:** None 驱动框架 - **参数** - `env` ([Env](../config.md#Env)) - `config` ([Config](../config.md#Config)) ### _property_ `type` {#Driver-type} - **类型:** str - **说明:** 驱动名称: `none` ### _property_ `logger` {#Driver-logger} - **类型:** untyped - **说明:** none driver 使用的 logger ### _method_ `run(*args, **kwargs)` {#Driver-run} - **说明:** 启动 none driver - **参数** - `*args` - `**kwargs` - **返回** - untyped ### _method_ `exit(force=False)` {#Driver-exit} - **说明:** 退出 none driver - **参数** - `force` (bool): 强制退出 - **返回** - untyped ================================================ FILE: website/versioned_docs/version-2.4.3/api/drivers/quart.md ================================================ --- mdx: format: md sidebar_position: 5 description: nonebot.drivers.quart 模块 --- # nonebot.drivers.quart [Quart](https://pgjones.gitlab.io/quart/index.html) 驱动适配 ```bash nb driver install quart # 或者 pip install nonebot2[quart] ``` :::tip 提示 本驱动仅支持服务端连接 ::: ## _class_ `Config()` {#Config} - **说明:** Quart 驱动框架设置 - **参数** auto ### _class-var_ `quart_reload` {#Config-quart-reload} - **类型:** bool - **说明:** 开启/关闭冷重载 ### _class-var_ `quart_reload_dirs` {#Config-quart-reload-dirs} - **类型:** list[str] | None - **说明:** 重载监控文件夹列表,默认为 uvicorn 默认值 ### _class-var_ `quart_reload_delay` {#Config-quart-reload-delay} - **类型:** float - **说明:** 重载延迟,默认为 uvicorn 默认值 ### _class-var_ `quart_reload_includes` {#Config-quart-reload-includes} - **类型:** list[str] | None - **说明:** 要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值 ### _class-var_ `quart_reload_excludes` {#Config-quart-reload-excludes} - **类型:** list[str] | None - **说明:** 不要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值 ### _class-var_ `quart_extra` {#Config-quart-extra} - **类型:** dict[str, Any] - **说明:** 传递给 `Quart` 的其他参数。 ## _class_ `Driver(env, config)` {#Driver} - **说明:** Quart 驱动框架 - **参数** - `env` ([Env](../config.md#Env)) - `config` (NoneBotConfig) ### _property_ `type` {#Driver-type} - **类型:** str - **说明:** 驱动名称: `quart` ### _property_ `server_app` {#Driver-server-app} - **类型:** Quart - **说明:** `Quart` 对象 ### _property_ `asgi` {#Driver-asgi} - **类型:** untyped - **说明:** `Quart` 对象 ### _property_ `logger` {#Driver-logger} - **类型:** untyped - **说明:** Quart 使用的 logger ### _method_ `setup_http_server(setup)` {#Driver-setup-http-server} - **参数** - `setup` ([HTTPServerSetup](index.md#HTTPServerSetup)) - **返回** - untyped ### _method_ `setup_websocket_server(setup)` {#Driver-setup-websocket-server} - **参数** - `setup` ([WebSocketServerSetup](index.md#WebSocketServerSetup)) - **返回** - None ### _method_ `run(host=None, port=None, *args, app=None, **kwargs)` {#Driver-run} - **说明:** 使用 `uvicorn` 启动 Quart - **参数** - `host` (str | None) - `port` (int | None) - `*args` - `app` (str | None) - `**kwargs` - **返回** - untyped ## _class_ `WebSocket(*, request, websocket_ctx)` {#WebSocket} - **说明:** Quart WebSocket Wrapper - **参数** - `request` (BaseRequest) - `websocket_ctx` (WebsocketContext) ### _async method_ `accept()` {#WebSocket-accept} - **参数** empty - **返回** - untyped ### _async method_ `close(code=1000, reason="")` {#WebSocket-close} - **参数** - `code` (int) - `reason` (str) - **返回** - untyped ### _async method_ `receive()` {#WebSocket-receive} - **参数** empty - **返回** - str | bytes ### _async method_ `receive_text()` {#WebSocket-receive-text} - **参数** empty - **返回** - str ### _async method_ `receive_bytes()` {#WebSocket-receive-bytes} - **参数** empty - **返回** - bytes ### _async method_ `send_text(data)` {#WebSocket-send-text} - **参数** - `data` (str) - **返回** - untyped ### _async method_ `send_bytes(data)` {#WebSocket-send-bytes} - **参数** - `data` (bytes) - **返回** - untyped ================================================ FILE: website/versioned_docs/version-2.4.3/api/drivers/websockets.md ================================================ --- mdx: format: md sidebar_position: 4 description: nonebot.drivers.websockets 模块 --- # nonebot.drivers.websockets [websockets](https://websockets.readthedocs.io/) 驱动适配 ```bash nb driver install websockets # 或者 pip install nonebot2[websockets] ``` :::tip 提示 本驱动仅支持客户端 WebSocket 连接 ::: ## _def_ `catch_closed(func)` {#catch-closed} - **参数** - `func` ((P) -> CoroutineType[Any, Any, T]) - **返回** - (P) -> CoroutineType[Any, Any, T] ## _class_ `Mixin()` {#Mixin} - **说明:** Websockets Mixin - **参数** auto ### _method_ `websocket(setup)` {#Mixin-websocket} - **参数** - `setup` ([Request](index.md#Request)) - **返回** - AsyncGenerator[[WebSocket](index.md#WebSocket), None] ## _class_ `WebSocket(*, request, websocket)` {#WebSocket} - **说明:** Websockets WebSocket Wrapper - **参数** - `request` ([Request](index.md#Request)) - `websocket` (ClientConnection) ### _async method_ `accept()` {#WebSocket-accept} - **参数** empty - **返回** - untyped ### _async method_ `close(code=1000, reason="")` {#WebSocket-close} - **参数** - `code` (int) - `reason` (str) - **返回** - untyped ### _async method_ `receive()` {#WebSocket-receive} - **参数** empty - **返回** - str | bytes ### _async method_ `receive_text()` {#WebSocket-receive-text} - **参数** empty - **返回** - str ### _async method_ `receive_bytes()` {#WebSocket-receive-bytes} - **参数** empty - **返回** - bytes ### _async method_ `send_text(data)` {#WebSocket-send-text} - **参数** - `data` (str) - **返回** - None ### _async method_ `send_bytes(data)` {#WebSocket-send-bytes} - **参数** - `data` (bytes) - **返回** - None ## _class_ `Driver(env, config)` {#Driver} - **参数** - `env` ([Env](../config.md#Env)) - `config` ([Config](../config.md#Config)) ================================================ FILE: website/versioned_docs/version-2.4.3/api/exception.md ================================================ --- mdx: format: md sidebar_position: 10 description: nonebot.exception 模块 --- # nonebot.exception 本模块包含了所有 NoneBot 运行时可能会抛出的异常。 这些异常并非所有需要用户处理,在 NoneBot 内部运行时被捕获,并进行对应操作。 ```bash NoneBotException ├── ParserExit ├── ProcessException | ├── IgnoredException | ├── SkippedException | | └── TypeMisMatch | ├── MockApiException | └── StopPropagation ├── MatcherException | ├── PausedException | ├── RejectedException | └── FinishedException ├── AdapterException | ├── NoLogException | ├── ApiNotAvailable | ├── NetworkError | └── ActionFailed └── DriverException └── WebSocketClosed ``` ## _class_ `NoneBotException()` {#NoneBotException} - **说明:** 所有 NoneBot 发生的异常基类。 - **参数** auto ## _class_ `ParserExit()` {#ParserExit} - **说明:** 处理消息失败时返回的异常。 - **参数** auto ## _class_ `ProcessException()` {#ProcessException} - **说明:** 事件处理过程中发生的异常基类。 - **参数** auto ## _class_ `IgnoredException()` {#IgnoredException} - **说明:** 指示 NoneBot 应该忽略该事件。可由 PreProcessor 抛出。 - **参数** - `reason`: 忽略事件的原因 ## _class_ `SkippedException()` {#SkippedException} - **说明** 指示 NoneBot 立即结束当前 `Dependent` 的运行。 例如,可以在 `Handler` 中通过 [Matcher.skip](matcher.md#Matcher-skip) 抛出。 - **参数** auto - **用法** ```python def always_skip(): Matcher.skip() @matcher.handle() async def handler(dependency = Depends(always_skip)): # never run ``` ## _class_ `TypeMisMatch()` {#TypeMisMatch} - **说明:** 当前 `Handler` 的参数类型不匹配。 - **参数** auto ## _class_ `MockApiException()` {#MockApiException} - **说明:** 指示 NoneBot 阻止本次 API 调用或修改本次调用返回值,并返回自定义内容。 可由 api hook 抛出。 - **参数** - `result`: 返回的内容 ## _class_ `StopPropagation()` {#StopPropagation} - **说明** 指示 NoneBot 终止事件向下层传播。 在 [Matcher.block](matcher.md#Matcher-block) 为 `True` 或使用 [Matcher.stop_propagation](matcher.md#Matcher-stop-propagation) 方法时抛出。 - **参数** auto - **用法** ```python matcher = on_notice(block=True) # 或者 @matcher.handle() async def handler(matcher: Matcher): matcher.stop_propagation() ``` ## _class_ `MatcherException()` {#MatcherException} - **说明:** 所有 Matcher 发生的异常基类。 - **参数** auto ## _class_ `PausedException()` {#PausedException} - **说明** 指示 NoneBot 结束当前 `Handler` 并等待下一条消息后继续下一个 `Handler`。 可用于用户输入新信息。 可以在 `Handler` 中通过 [Matcher.pause](matcher.md#Matcher-pause) 抛出。 - **参数** auto - **用法** ```python @matcher.handle() async def handler(): await matcher.pause("some message") ``` ## _class_ `RejectedException()` {#RejectedException} - **说明** 指示 NoneBot 结束当前 `Handler` 并等待下一条消息后重新运行当前 `Handler`。 可用于用户重新输入。 可以在 `Handler` 中通过 [Matcher.reject](matcher.md#Matcher-reject) 抛出。 - **参数** auto - **用法** ```python @matcher.handle() async def handler(): await matcher.reject("some message") ``` ## _class_ `FinishedException()` {#FinishedException} - **说明** 指示 NoneBot 结束当前 `Handler` 且后续 `Handler` 不再被运行。可用于结束用户会话。 可以在 `Handler` 中通过 [Matcher.finish](matcher.md#Matcher-finish) 抛出。 - **参数** auto - **用法** ```python @matcher.handle() async def handler(): await matcher.finish("some message") ``` ## _class_ `AdapterException()` {#AdapterException} - **说明:** 代表 `Adapter` 抛出的异常,所有的 `Adapter` 都要在内部继承自这个 `Exception`。 - **参数** - `adapter_name`: 标识 adapter ## _class_ `NoLogException()` {#NoLogException} - **说明** 指示 NoneBot 对当前 `Event` 进行处理但不显示 Log 信息。 可在 [Event.get_log_string](adapters/index.md#Event-get-log-string) 时抛出 - **参数** auto ## _class_ `ApiNotAvailable()` {#ApiNotAvailable} - **说明:** 在 API 连接不可用时抛出。 - **参数** auto ## _class_ `NetworkError()` {#NetworkError} - **说明:** 在网络出现问题时抛出, 如: API 请求地址不正确, API 请求无返回或返回状态非正常等。 - **参数** auto ## _class_ `ActionFailed()` {#ActionFailed} - **说明:** API 请求成功返回数据,但 API 操作失败。 - **参数** auto ## _class_ `DriverException()` {#DriverException} - **说明:** `Driver` 抛出的异常基类。 - **参数** auto ## _class_ `WebSocketClosed()` {#WebSocketClosed} - **说明:** WebSocket 连接已关闭。 - **参数** auto ================================================ FILE: website/versioned_docs/version-2.4.3/api/index.md ================================================ --- mdx: format: md sidebar_position: 0 description: nonebot 模块 --- # nonebot 本模块主要定义了 NoneBot 启动所需函数,供 bot 入口文件调用。 ## 快捷导入 为方便使用,本模块从子模块导入了部分内容,以下内容可以直接通过本模块导入: - `on` => [`on`](plugin/on.md#on) - `on_metaevent` => [`on_metaevent`](plugin/on.md#on-metaevent) - `on_message` => [`on_message`](plugin/on.md#on-message) - `on_notice` => [`on_notice`](plugin/on.md#on-notice) - `on_request` => [`on_request`](plugin/on.md#on-request) - `on_startswith` => [`on_startswith`](plugin/on.md#on-startswith) - `on_endswith` => [`on_endswith`](plugin/on.md#on-endswith) - `on_fullmatch` => [`on_fullmatch`](plugin/on.md#on-fullmatch) - `on_keyword` => [`on_keyword`](plugin/on.md#on-keyword) - `on_command` => [`on_command`](plugin/on.md#on-command) - `on_shell_command` => [`on_shell_command`](plugin/on.md#on-shell-command) - `on_regex` => [`on_regex`](plugin/on.md#on-regex) - `on_type` => [`on_type`](plugin/on.md#on-type) - `CommandGroup` => [`CommandGroup`](plugin/on.md#CommandGroup) - `Matchergroup` => [`MatcherGroup`](plugin/on.md#MatcherGroup) - `load_plugin` => [`load_plugin`](plugin/load.md#load-plugin) - `load_plugins` => [`load_plugins`](plugin/load.md#load-plugins) - `load_all_plugins` => [`load_all_plugins`](plugin/load.md#load-all-plugins) - `load_from_json` => [`load_from_json`](plugin/load.md#load-from-json) - `load_from_toml` => [`load_from_toml`](plugin/load.md#load-from-toml) - `load_builtin_plugin` => [`load_builtin_plugin`](plugin/load.md#load-builtin-plugin) - `load_builtin_plugins` => [`load_builtin_plugins`](plugin/load.md#load-builtin-plugins) - `get_plugin` => [`get_plugin`](plugin/index.md#get-plugin) - `get_plugin_by_module_name` => [`get_plugin_by_module_name`](plugin/index.md#get-plugin-by-module-name) - `get_loaded_plugins` => [`get_loaded_plugins`](plugin/index.md#get-loaded-plugins) - `get_available_plugin_names` => [`get_available_plugin_names`](plugin/index.md#get-available-plugin-names) - `get_plugin_config` => [`get_plugin_config`](plugin/index.md#get-plugin-config) - `require` => [`require`](plugin/load.md#require) ## _def_ `get_driver()` {#get-driver} - **说明** 获取全局 [Driver](drivers/index.md#Driver) 实例。 可用于在计划任务的回调等情形中获取当前 [Driver](drivers/index.md#Driver) 实例。 - **参数** empty - **返回** - [Driver](drivers/index.md#Driver): 全局 [Driver](drivers/index.md#Driver) 对象 - **异常** - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用) - **用法** ```python driver = nonebot.get_driver() ``` ## _def_ `get_adapter(name)` {#get-adapter} - **说明:** 获取已注册的 [Adapter](adapters/index.md#Adapter) 实例。 - **重载** **1.** `(name) -> Adapter` - **参数** - `name` (str): 适配器名称 - **返回** - [Adapter](adapters/index.md#Adapter): 指定名称的 [Adapter](adapters/index.md#Adapter) 对象 **2.** `(name) -> A` - **参数** - `name` (type[A]): 适配器类型 - **返回** - A: 指定类型的 [Adapter](adapters/index.md#Adapter) 对象 - **异常** - ValueError: 指定的 [Adapter](adapters/index.md#Adapter) 未注册 - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用) - **用法** ```python from nonebot.adapters.console import Adapter adapter = nonebot.get_adapter(Adapter) ``` ## _def_ `get_adapters()` {#get-adapters} - **说明:** 获取所有已注册的 [Adapter](adapters/index.md#Adapter) 实例。 - **参数** empty - **返回** - dict[str, [Adapter](adapters/index.md#Adapter)]: 所有 [Adapter](adapters/index.md#Adapter) 实例字典 - **异常** - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用) - **用法** ```python adapters = nonebot.get_adapters() ``` ## _def_ `get_app()` {#get-app} - **说明:** 获取全局 [ASGIMixin](drivers/index.md#ASGIMixin) 对应的 Server App 对象。 - **参数** empty - **返回** - Any: Server App 对象 - **异常** - AssertionError: 全局 Driver 对象不是 [ASGIMixin](drivers/index.md#ASGIMixin) 类型 - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用) - **用法** ```python app = nonebot.get_app() ``` ## _def_ `get_asgi()` {#get-asgi} - **说明:** 获取全局 [ASGIMixin](drivers/index.md#ASGIMixin) 对应的 [ASGI](https://asgi.readthedocs.io/) 对象。 - **参数** empty - **返回** - Any: ASGI 对象 - **异常** - AssertionError: 全局 Driver 对象不是 [ASGIMixin](drivers/index.md#ASGIMixin) 类型 - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用) - **用法** ```python asgi = nonebot.get_asgi() ``` ## _def_ `get_bot(self_id=None)` {#get-bot} - **说明** 获取一个连接到 NoneBot 的 [Bot](adapters/index.md#Bot) 对象。 当提供 `self_id` 时,此函数是 `get_bots()[self_id]` 的简写; 当不提供时,返回一个 [Bot](adapters/index.md#Bot)。 - **参数** - `self_id` (str | None): 用来识别 [Bot](adapters/index.md#Bot) 的 [Bot.self_id](adapters/index.md#Bot-self-id) 属性 - **返回** - [Bot](adapters/index.md#Bot): [Bot](adapters/index.md#Bot) 对象 - **异常** - KeyError: 对应 self_id 的 Bot 不存在 - ValueError: 没有传入 self_id 且没有 Bot 可用 - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用) - **用法** ```python assert nonebot.get_bot("12345") == nonebot.get_bots()["12345"] another_unspecified_bot = nonebot.get_bot() ``` ## _def_ `get_bots()` {#get-bots} - **说明:** 获取所有连接到 NoneBot 的 [Bot](adapters/index.md#Bot) 对象。 - **参数** empty - **返回** - dict[str, [Bot](adapters/index.md#Bot)]: 一个以 [Bot.self_id](adapters/index.md#Bot-self-id) 为键 [Bot](adapters/index.md#Bot) 对象为值的字典 - **异常** - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用) - **用法** ```python bots = nonebot.get_bots() ``` ## _def_ `init(*, _env_file=None, **kwargs)` {#init} - **说明** 初始化 NoneBot 以及 全局 [Driver](drivers/index.md#Driver) 对象。 NoneBot 将会从 .env 文件中读取环境信息,并使用相应的 env 文件配置。 也可以传入自定义的 `_env_file` 来指定 NoneBot 从该文件读取配置。 - **参数** - `_env_file` (DOTENV_TYPE | None): 配置文件名,默认从 `.env.{env_name}` 中读取配置 - `**kwargs` (Any): 任意变量,将会存储到 [Driver.config](drivers/index.md#Driver-config) 对象里 - **返回** - None - **用法** ```python nonebot.init(database=Database(...)) ``` ## _def_ `run(*args, **kwargs)` {#run} - **说明:** 启动 NoneBot,即运行全局 [Driver](drivers/index.md#Driver) 对象。 - **参数** - `*args` (Any): 传入 [Driver.run](drivers/index.md#Driver-run) 的位置参数 - `**kwargs` (Any): 传入 [Driver.run](drivers/index.md#Driver-run) 的命名参数 - **返回** - None - **用法** ```python nonebot.run(host="127.0.0.1", port=8080) ``` ================================================ FILE: website/versioned_docs/version-2.4.3/api/log.md ================================================ --- mdx: format: md sidebar_position: 7 description: nonebot.log 模块 --- # nonebot.log 本模块定义了 NoneBot 的日志记录 Logger。 NoneBot 使用 [`loguru`][loguru] 来记录日志信息。 自定义 logger 请参考 [自定义日志](https://nonebot.dev/docs/appendices/log) 以及 [`loguru`][loguru] 文档。 [loguru]: https://github.com/Delgan/loguru ## _var_ `logger` {#logger} - **类型:** Logger - **说明** NoneBot 日志记录器对象。 默认信息: - 格式: `[%(asctime)s %(name)s] %(levelname)s: %(message)s` - 等级: `INFO` ,根据 `config.log_level` 配置改变 - 输出: 输出至 stdout - **用法** ```python from nonebot.log import logger ``` ## _class_ `LoguruHandler()` {#LoguruHandler} - **说明:** logging 与 loguru 之间的桥梁,将 logging 的日志转发到 loguru。 - **参数** auto ### _method_ `emit(record)` {#LoguruHandler-emit} - **参数** - `record` (logging.LogRecord) - **返回** - untyped ## _def_ `default_filter(record)` {#default-filter} - **说明:** 默认的日志过滤器,根据 `config.log_level` 配置改变日志等级。 - **参数** - `record` (Record) - **返回** - untyped ## _var_ `default_format` {#default-format} - **类型:** str - **说明:** 默认日志格式 ================================================ FILE: website/versioned_docs/version-2.4.3/api/matcher.md ================================================ --- mdx: format: md sidebar_position: 3 description: nonebot.matcher 模块 --- # nonebot.matcher 本模块实现事件响应器的创建与运行,并提供一些快捷方法来帮助用户更好的与机器人进行对话。 ## _var_ `DEFAULT_PROVIDER_CLASS` {#DEFAULT-PROVIDER-CLASS} - **类型:** untyped - **说明:** 默认存储器类型 ## _class_ `Matcher()` {#Matcher} - **说明:** 事件响应器类 - **参数** empty ### _class-var_ `type` {#Matcher-type} - **类型:** ClassVar[str] - **说明:** 事件响应器类型 ### _class-var_ `rule` {#Matcher-rule} - **类型:** ClassVar[[Rule](rule.md#Rule)] - **说明:** 事件响应器匹配规则 ### _class-var_ `permission` {#Matcher-permission} - **类型:** ClassVar[[Permission](permission.md#Permission)] - **说明:** 事件响应器触发权限 ### _class-var_ `handlers` {#Matcher-handlers} - **类型:** ClassVar[list[[Dependent](dependencies/index.md#Dependent)[Any]]] - **说明:** 事件响应器拥有的事件处理函数列表 ### _class-var_ `priority` {#Matcher-priority} - **类型:** ClassVar[int] - **说明:** 事件响应器优先级 ### _class-var_ `block` {#Matcher-block} - **类型:** bool - **说明:** 事件响应器是否阻止事件传播 ### _class-var_ `temp` {#Matcher-temp} - **类型:** ClassVar[bool] - **说明:** 事件响应器是否为临时 ### _class-var_ `expire_time` {#Matcher-expire-time} - **类型:** ClassVar[datetime | None] - **说明:** 事件响应器过期时间点 ### _classmethod_ `new(type_="", rule=None, permission=None, handlers=None, temp=False, priority=1, block=False, *, plugin=None, module=None, source=None, expire_time=None, default_state=None, default_type_updater=None, default_permission_updater=None)` {#Matcher-new} - **说明:** 创建一个新的事件响应器,并存储至 `matchers <#matchers>`\_ - **参数** - `type_` (str): 事件响应器类型,与 `event.get_type()` 一致时触发,空字符串表示任意 - `rule` ([Rule](rule.md#Rule) | None): 匹配规则 - `permission` ([Permission](permission.md#Permission) | None): 权限 - `handlers` (list[[T\_Handler](typing.md#T-Handler) | [Dependent](dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器,即触发一次后删除 - `priority` (int): 响应优先级 - `block` (bool): 是否阻止事件向更低优先级的响应器传播 - `plugin` ([Plugin](plugin/model.md#Plugin) | None): **Deprecated.** 事件响应器所在插件 - `module` (ModuleType | None): **Deprecated.** 事件响应器所在模块 - `source` (MatcherSource | None): 事件响应器源代码上下文信息 - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `default_state` ([T_State](typing.md#T-State) | None): 默认状态 `state` - `default_type_updater` ([T_TypeUpdater](typing.md#T-TypeUpdater) | [Dependent](dependencies/index.md#Dependent)[str] | None): 默认事件类型更新函数 - `default_permission_updater` ([T_PermissionUpdater](typing.md#T-PermissionUpdater) | [Dependent](dependencies/index.md#Dependent)[[Permission](permission.md#Permission)] | None): 默认会话权限更新函数 - **返回** - type[Matcher]: 新的事件响应器类 ### _classmethod_ `destroy()` {#Matcher-destroy} - **说明:** 销毁当前的事件响应器 - **参数** empty - **返回** - None ### _classmethod_ `check_perm(bot, event, stack=None, dependency_cache=None)` {#Matcher-check-perm} - **说明:** 检查是否满足触发权限 - **参数** - `bot` ([Bot](adapters/index.md#Bot)): Bot 对象 - `event` ([Event](adapters/index.md#Event)): 上报事件 - `stack` (AsyncExitStack | None): 异步上下文栈 - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None): 依赖缓存 - **返回** - bool: 是否满足权限 ### _classmethod_ `check_rule(bot, event, state, stack=None, dependency_cache=None)` {#Matcher-check-rule} - **说明:** 检查是否满足匹配规则 - **参数** - `bot` ([Bot](adapters/index.md#Bot)): Bot 对象 - `event` ([Event](adapters/index.md#Event)): 上报事件 - `state` ([T_State](typing.md#T-State)): 当前状态 - `stack` (AsyncExitStack | None): 异步上下文栈 - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None): 依赖缓存 - **返回** - bool: 是否满足匹配规则 ### _classmethod_ `type_updater(func)` {#Matcher-type-updater} - **说明:** 装饰一个函数来更改当前事件响应器的默认响应事件类型更新函数 - **参数** - `func` ([T_TypeUpdater](typing.md#T-TypeUpdater)): 响应事件类型更新函数 - **返回** - [T_TypeUpdater](typing.md#T-TypeUpdater) ### _classmethod_ `permission_updater(func)` {#Matcher-permission-updater} - **说明:** 装饰一个函数来更改当前事件响应器的默认会话权限更新函数 - **参数** - `func` ([T_PermissionUpdater](typing.md#T-PermissionUpdater)): 会话权限更新函数 - **返回** - [T_PermissionUpdater](typing.md#T-PermissionUpdater) ### _classmethod_ `append_handler(handler, parameterless=None)` {#Matcher-append-handler} - **参数** - `handler` ([T_Handler](typing.md#T-Handler)) - `parameterless` (Iterable[Any] | None) - **返回** - [Dependent](dependencies/index.md#Dependent)[Any] ### _classmethod_ `handle(parameterless=None)` {#Matcher-handle} - **说明:** 装饰一个函数来向事件响应器直接添加一个处理函数 - **参数** - `parameterless` (Iterable[Any] | None): 非参数类型依赖列表 - **返回** - ([T_Handler](typing.md#T-Handler)) -> [T_Handler](typing.md#T-Handler) ### _classmethod_ `receive(id="", parameterless=None)` {#Matcher-receive} - **说明:** 装饰一个函数来指示 NoneBot 在接收用户新的一条消息后继续运行该函数 - **参数** - `id` (str): 消息 ID - `parameterless` (Iterable[Any] | None): 非参数类型依赖列表 - **返回** - ([T_Handler](typing.md#T-Handler)) -> [T_Handler](typing.md#T-Handler) ### _classmethod_ `got(key, prompt=None, parameterless=None)` {#Matcher-got} - **说明** 装饰一个函数来指示 NoneBot 获取一个参数 `key` 当要获取的 `key` 不存在时接收用户新的一条消息再运行该函数, 如果 `key` 已存在则直接继续运行 - **参数** - `key` (str): 参数名 - `prompt` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate) | None): 在参数不存在时向用户发送的消息 - `parameterless` (Iterable[Any] | None): 非参数类型依赖列表 - **返回** - ([T_Handler](typing.md#T-Handler)) -> [T_Handler](typing.md#T-Handler) ### _classmethod_ `send(message, **kwargs)` {#Matcher-send} - **说明:** 发送一条消息给当前交互用户 - **参数** - `message` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate)): 消息内容 - `**kwargs` (Any): [Bot.send](adapters/index.md#Bot-send) 的参数, 请参考对应 adapter 的 bot 对象 api - **返回** - Any ### _classmethod_ `finish(message=None, **kwargs)` {#Matcher-finish} - **说明:** 发送一条消息给当前交互用户并结束当前事件响应器 - **参数** - `message` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate) | None): 消息内容 - `**kwargs`: [Bot.send](adapters/index.md#Bot-send) 的参数, 请参考对应 adapter 的 bot 对象 api - **返回** - NoReturn ### _classmethod_ `pause(prompt=None, **kwargs)` {#Matcher-pause} - **说明:** 发送一条消息给当前交互用户并暂停事件响应器,在接收用户新的一条消息后继续下一个处理函数 - **参数** - `prompt` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate) | None): 消息内容 - `**kwargs`: [Bot.send](adapters/index.md#Bot-send) 的参数, 请参考对应 adapter 的 bot 对象 api - **返回** - NoReturn ### _classmethod_ `reject(prompt=None, **kwargs)` {#Matcher-reject} - **说明:** 最近使用 `got` / `receive` 接收的消息不符合预期, 发送一条消息给当前交互用户并将当前事件处理流程中断在当前位置,在接收用户新的一个事件后从头开始执行当前处理函数 - **参数** - `prompt` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate) | None): 消息内容 - `**kwargs`: [Bot.send](adapters/index.md#Bot-send) 的参数, 请参考对应 adapter 的 bot 对象 api - **返回** - NoReturn ### _classmethod_ `reject_arg(key, prompt=None, **kwargs)` {#Matcher-reject-arg} - **说明:** 最近使用 `got` 接收的消息不符合预期, 发送一条消息给当前交互用户并将当前事件处理流程中断在当前位置,在接收用户新的一条消息后从头开始执行当前处理函数 - **参数** - `key` (str): 参数名 - `prompt` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate) | None): 消息内容 - `**kwargs`: [Bot.send](adapters/index.md#Bot-send) 的参数, 请参考对应 adapter 的 bot 对象 api - **返回** - NoReturn ### _classmethod_ `reject_receive(id="", prompt=None, **kwargs)` {#Matcher-reject-receive} - **说明:** 最近使用 `receive` 接收的消息不符合预期, 发送一条消息给当前交互用户并将当前事件处理流程中断在当前位置,在接收用户新的一个事件后从头开始执行当前处理函数 - **参数** - `id` (str): 消息 id - `prompt` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate) | None): 消息内容 - `**kwargs`: [Bot.send](adapters/index.md#Bot-send) 的参数, 请参考对应 adapter 的 bot 对象 api - **返回** - NoReturn ### _classmethod_ `skip()` {#Matcher-skip} - **说明** 跳过当前事件处理函数,继续下一个处理函数 通常在事件处理函数的依赖中使用。 - **参数** empty - **返回** - NoReturn ### _method_ `get_receive(id, default=None)` {#Matcher-get-receive} - **说明** 获取一个 `receive` 事件 如果没有找到对应的事件,返回 `default` 值 - **重载** **1.** `(id) -> Event | None` - **参数** - `id` (str) - **返回** - [Event](adapters/index.md#Event) | None **2.** `(id, default) -> Event | T` - **参数** - `id` (str) - `default` (T) - **返回** - [Event](adapters/index.md#Event) | T ### _method_ `set_receive(id, event)` {#Matcher-set-receive} - **说明:** 设置一个 `receive` 事件 - **参数** - `id` (str) - `event` ([Event](adapters/index.md#Event)) - **返回** - None ### _method_ `get_last_receive(default=None)` {#Matcher-get-last-receive} - **说明** 获取最近一次 `receive` 事件 如果没有事件,返回 `default` 值 - **重载** **1.** `() -> Event | None` - **参数** empty - **返回** - [Event](adapters/index.md#Event) | None **2.** `(default) -> Event | T` - **参数** - `default` (T) - **返回** - [Event](adapters/index.md#Event) | T ### _method_ `get_arg(key, default=None)` {#Matcher-get-arg} - **说明** 获取一个 `got` 消息 如果没有找到对应的消息,返回 `default` 值 - **重载** **1.** `(key) -> Message | None` - **参数** - `key` (str) - **返回** - [Message](adapters/index.md#Message) | None **2.** `(key, default) -> Message | T` - **参数** - `key` (str) - `default` (T) - **返回** - [Message](adapters/index.md#Message) | T ### _method_ `set_arg(key, message)` {#Matcher-set-arg} - **说明:** 设置一个 `got` 消息 - **参数** - `key` (str) - `message` ([Message](adapters/index.md#Message)) - **返回** - None ### _method_ `set_target(target, cache=True)` {#Matcher-set-target} - **参数** - `target` (str) - `cache` (bool) - **返回** - None ### _method_ `get_target(default=None)` {#Matcher-get-target} - **重载** **1.** `() -> str | None` - **参数** empty - **返回** - str | None **2.** `(default) -> str | T` - **参数** - `default` (T) - **返回** - str | T ### _method_ `stop_propagation()` {#Matcher-stop-propagation} - **说明:** 阻止事件传播 - **参数** empty - **返回** - untyped ### _async method_ `update_type(bot, event, stack=None, dependency_cache=None)` {#Matcher-update-type} - **参数** - `bot` ([Bot](adapters/index.md#Bot)) - `event` ([Event](adapters/index.md#Event)) - `stack` (AsyncExitStack | None) - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None) - **返回** - str ### _async method_ `update_permission(bot, event, stack=None, dependency_cache=None)` {#Matcher-update-permission} - **参数** - `bot` ([Bot](adapters/index.md#Bot)) - `event` ([Event](adapters/index.md#Event)) - `stack` (AsyncExitStack | None) - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None) - **返回** - [Permission](permission.md#Permission) ### _async method_ `resolve_reject()` {#Matcher-resolve-reject} - **参数** empty - **返回** - untyped ### _method_ `ensure_context(bot, event)` {#Matcher-ensure-context} - **参数** - `bot` ([Bot](adapters/index.md#Bot)) - `event` ([Event](adapters/index.md#Event)) - **返回** - untyped ### _async method_ `simple_run(bot, event, state, stack=None, dependency_cache=None)` {#Matcher-simple-run} - **参数** - `bot` ([Bot](adapters/index.md#Bot)) - `event` ([Event](adapters/index.md#Event)) - `state` ([T_State](typing.md#T-State)) - `stack` (AsyncExitStack | None) - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None) - **返回** - untyped ### _async method_ `run(bot, event, state, stack=None, dependency_cache=None)` {#Matcher-run} - **参数** - `bot` ([Bot](adapters/index.md#Bot)) - `event` ([Event](adapters/index.md#Event)) - `state` ([T_State](typing.md#T-State)) - `stack` (AsyncExitStack | None) - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None) - **返回** - untyped ## _class_ `MatcherManager()` {#MatcherManager} - **说明** 事件响应器管理器 实现了常用字典操作,用于管理事件响应器。 - **参数** empty ### _method_ `keys()` {#MatcherManager-keys} - **参数** empty - **返回** - KeysView[int] ### _method_ `values()` {#MatcherManager-values} - **参数** empty - **返回** - ValuesView[list[type[[Matcher](#Matcher)]]] ### _method_ `items()` {#MatcherManager-items} - **参数** empty - **返回** - ItemsView[int, list[type[[Matcher](#Matcher)]]] ### _method_ `get(key, default=None)` {#MatcherManager-get} - **重载** **1.** `(key) -> list[type[Matcher]] | None` - **参数** - `key` (int) - **返回** - list[type[[Matcher](#Matcher)]] | None **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) - `default` (T) - **返回** - list[type[[Matcher](#Matcher)]] | T ### _method_ `pop(key)` {#MatcherManager-pop} - **参数** - `key` (int) - **返回** - list[type[[Matcher](#Matcher)]] ### _method_ `popitem()` {#MatcherManager-popitem} - **参数** empty - **返回** - tuple[int, list[type[[Matcher](#Matcher)]]] ### _method_ `clear()` {#MatcherManager-clear} - **参数** empty - **返回** - None ### _method_ `update(m, /)` {#MatcherManager-update} - **参数** - `m` (MutableMapping[int, list[type[[Matcher](#Matcher)]]]) - **返回** - None ### _method_ `setdefault(key, default)` {#MatcherManager-setdefault} - **参数** - `key` (int) - `default` (list[type[[Matcher](#Matcher)]]) - **返回** - list[type[[Matcher](#Matcher)]] ### _method_ `set_provider(provider_class)` {#MatcherManager-set-provider} - **说明:** 设置事件响应器存储器 - **参数** - `provider_class` (type[[MatcherProvider](#MatcherProvider)]): 事件响应器存储器类 - **返回** - None ## _abstract class_ `MatcherProvider(matchers)` {#MatcherProvider} - **说明:** 事件响应器存储器基类 - **参数** - `matchers` (Mapping[int, list[type[[Matcher](#Matcher)]]]): 当前存储器中已有的事件响应器 ## _var_ `matchers` {#matchers} - **类型:** untyped ================================================ FILE: website/versioned_docs/version-2.4.3/api/message.md ================================================ --- mdx: format: md sidebar_position: 2 description: nonebot.message 模块 --- # nonebot.message 本模块定义了事件处理主要流程。 NoneBot 内部处理并按优先级分发事件给所有事件响应器,提供了多个插槽以进行事件的预处理等。 ## _def_ `event_preprocessor(func)` {#event-preprocessor} - **说明** 事件预处理。 装饰一个函数,使它在每次接收到事件并分发给各响应器之前执行。 - **参数** - `func` ([T_EventPreProcessor](typing.md#T-EventPreProcessor)) - **返回** - [T_EventPreProcessor](typing.md#T-EventPreProcessor) ## _def_ `event_postprocessor(func)` {#event-postprocessor} - **说明** 事件后处理。 装饰一个函数,使它在每次接收到事件并分发给各响应器之后执行。 - **参数** - `func` ([T_EventPostProcessor](typing.md#T-EventPostProcessor)) - **返回** - [T_EventPostProcessor](typing.md#T-EventPostProcessor) ## _def_ `run_preprocessor(func)` {#run-preprocessor} - **说明** 运行预处理。 装饰一个函数,使它在每次事件响应器运行前执行。 - **参数** - `func` ([T_RunPreProcessor](typing.md#T-RunPreProcessor)) - **返回** - [T_RunPreProcessor](typing.md#T-RunPreProcessor) ## _def_ `run_postprocessor(func)` {#run-postprocessor} - **说明** 运行后处理。 装饰一个函数,使它在每次事件响应器运行后执行。 - **参数** - `func` ([T_RunPostProcessor](typing.md#T-RunPostProcessor)) - **返回** - [T_RunPostProcessor](typing.md#T-RunPostProcessor) ## _async def_ `check_and_run_matcher(Matcher, bot, event, state, stack=None, dependency_cache=None)` {#check-and-run-matcher} - **说明:** 检查并运行事件响应器。 - **参数** - `Matcher` (type[[Matcher](matcher.md#Matcher)]): 事件响应器 - `bot` ([Bot](adapters/index.md#Bot)): Bot 对象 - `event` ([Event](adapters/index.md#Event)): Event 对象 - `state` ([T_State](typing.md#T-State)): 会话状态 - `stack` (AsyncExitStack | None): 异步上下文栈 - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None): 依赖缓存 - **返回** - None ## _async def_ `handle_event(bot, event)` {#handle-event} - **说明:** 处理一个事件。调用该函数以实现分发事件。 - **参数** - `bot` ([Bot](adapters/index.md#Bot)): Bot 对象 - `event` ([Event](adapters/index.md#Event)): Event 对象 - **返回** - None - **用法** ```python driver.task_group.start_soon(handle_event, bot, event) ``` ================================================ FILE: website/versioned_docs/version-2.4.3/api/params.md ================================================ --- mdx: format: md sidebar_position: 4 description: nonebot.params 模块 --- # nonebot.params 本模块定义了依赖注入的各类参数。 ## _def_ `Arg(key=None)` {#Arg} - **说明:** Arg 参数消息 - **参数** - `key` (str | None) - **返回** - Any ## _class_ `ArgParam(*args, key, type, **kwargs)` {#ArgParam} - **说明** Arg 注入参数 本注入解析事件响应器操作 `got` 所获取的参数。 可以通过 `Arg`、`ArgStr`、`ArgPlainText` 等函数参数 `key` 指定获取的参数, 留空则会根据参数名称获取。 - **参数** - `*args` - `key` (str) - `type` (Literal['message', 'str', 'plaintext', 'prompt']) - `**kwargs` (Any) ## _def_ `ArgPlainText(key=None)` {#ArgPlainText} - **说明:** Arg 参数消息纯文本 - **参数** - `key` (str | None) - **返回** - str ## _def_ `ArgPromptResult(key=None)` {#ArgPromptResult} - **说明:** `arg` prompt 发送结果 - **参数** - `key` (str | None) - **返回** - Any ## _def_ `ArgStr(key=None)` {#ArgStr} - **说明:** Arg 参数消息文本 - **参数** - `key` (str | None) - **返回** - str ## _class_ `BotParam(*args, checker=None, **kwargs)` {#BotParam} - **说明** 注入参数。 本注入解析所有类型为且仅为 [Bot](adapters/index.md#Bot) 及其子类或 `None` 的参数。 为保证兼容性,本注入还会解析名为 `bot` 且没有类型注解的参数。 - **参数** - `*args` - `checker` ([ModelField](compat.md#ModelField) | None) - `**kwargs` (Any) ## _class_ `DefaultParam(*args, validate=False, **kwargs)` {#DefaultParam} - **说明** 默认值注入参数 本注入解析所有剩余未能解析且具有默认值的参数。 本注入参数应该具有最低优先级,因此应该在所有其他注入参数之后使用。 - **参数** - `*args` - `validate` (bool) - `**kwargs` (Any) ## _class_ `DependParam(*args, dependent, use_cache, **kwargs)` {#DependParam} - **说明** 子依赖注入参数。 本注入解析所有子依赖注入,然后将它们的返回值作为参数值传递给父依赖。 本注入应该具有最高优先级,因此应该在其他参数之前检查。 - **参数** - `*args` - `dependent` ([Dependent](dependencies/index.md#Dependent)[Any]) - `use_cache` (bool) - `**kwargs` (Any) ## _def_ `Depends(dependency=None, *, use_cache=True, validate=False)` {#Depends} - **说明:** 子依赖装饰器 - **参数** - `dependency` ([T_Handler](typing.md#T-Handler) | None): 依赖函数。默认为参数的类型注释。 - `use_cache` (bool): 是否使用缓存。默认为 `True`。 - `validate` (bool | PydanticFieldInfo): 是否使用 Pydantic 类型校验。默认为 `False`。 - **返回** - Any - **用法** ```python def depend_func() -> Any: return ... def depend_gen_func(): try: yield ... finally: ... async def handler( param_name: Any = Depends(depend_func), gen: Any = Depends(depend_gen_func), ): ... ``` ## _class_ `EventParam(*args, checker=None, **kwargs)` {#EventParam} - **说明** 注入参数 本注入解析所有类型为且仅为 [Event](adapters/index.md#Event) 及其子类或 `None` 的参数。 为保证兼容性,本注入还会解析名为 `event` 且没有类型注解的参数。 - **参数** - `*args` - `checker` ([ModelField](compat.md#ModelField) | None) - `**kwargs` (Any) ## _class_ `ExceptionParam(*args, validate=False, **kwargs)` {#ExceptionParam} - **说明** 的异常注入参数 本注入解析所有类型为 `Exception` 或 `None` 的参数。 为保证兼容性,本注入还会解析名为 `exception` 且没有类型注解的参数。 - **参数** - `*args` - `validate` (bool) - `**kwargs` (Any) ## _class_ `MatcherParam(*args, checker=None, **kwargs)` {#MatcherParam} - **说明** 事件响应器实例注入参数 本注入解析所有类型为且仅为 [Matcher](matcher.md#Matcher) 及其子类或 `None` 的参数。 为保证兼容性,本注入还会解析名为 `matcher` 且没有类型注解的参数。 - **参数** - `*args` - `checker` ([ModelField](compat.md#ModelField) | None) - `**kwargs` (Any) ## _class_ `StateParam(*args, validate=False, **kwargs)` {#StateParam} - **说明** 事件处理状态注入参数 本注入解析所有类型为 `T_State` 的参数。 为保证兼容性,本注入还会解析名为 `state` 且没有类型注解的参数。 - **参数** - `*args` - `validate` (bool) - `**kwargs` (Any) ## _def_ `EventType()` {#EventType} - **说明:** 类型参数 - **参数** empty - **返回** - str ## _def_ `EventMessage()` {#EventMessage} - **说明:** 消息参数 - **参数** empty - **返回** - Any ## _def_ `EventPlainText()` {#EventPlainText} - **说明:** 纯文本消息参数 - **参数** empty - **返回** - str ## _def_ `EventToMe()` {#EventToMe} - **说明:** `to_me` 参数 - **参数** empty - **返回** - bool ## _def_ `Command()` {#Command} - **说明:** 消息命令元组 - **参数** empty - **返回** - tuple[str, ...] ## _def_ `RawCommand()` {#RawCommand} - **说明:** 消息命令文本 - **参数** empty - **返回** - str ## _def_ `CommandArg()` {#CommandArg} - **说明:** 消息命令参数 - **参数** empty - **返回** - Any ## _def_ `CommandStart()` {#CommandStart} - **说明:** 消息命令开头 - **参数** empty - **返回** - str ## _def_ `CommandWhitespace()` {#CommandWhitespace} - **说明:** 消息命令与参数之间的空白 - **参数** empty - **返回** - str ## _def_ `ShellCommandArgs()` {#ShellCommandArgs} - **说明:** shell 命令解析后的参数字典 - **参数** empty - **返回** - Any ## _def_ `ShellCommandArgv()` {#ShellCommandArgv} - **说明:** shell 命令原始参数列表 - **参数** empty - **返回** - Any ## _def_ `RegexMatched()` {#RegexMatched} - **说明:** 正则匹配结果 - **参数** empty - **返回** - Match[str] ## _def_ `RegexStr(*groups)` {#RegexStr} - **说明:** 正则匹配结果文本 - **重载** **1.** `(group, /) -> str` - **参数** - `group` (Literal[0]) - **返回** - str **2.** `(group, /) -> str | Any` - **参数** - `group` (str | int) - **返回** - str | Any **3.** `(group1, group2, /, *groups) -> tuple[str | Any, ...]` - **参数** - `group1` (str | int) - `group2` (str | int) - `*groups` (str | int) - **返回** - tuple[str | Any, ...] ## _def_ `RegexGroup()` {#RegexGroup} - **说明:** 正则匹配结果 group 元组 - **参数** empty - **返回** - tuple[Any, ...] ## _def_ `RegexDict()` {#RegexDict} - **说明:** 正则匹配结果 group 字典 - **参数** empty - **返回** - dict[str, Any] ## _def_ `Startswith()` {#Startswith} - **说明:** 响应触发前缀 - **参数** empty - **返回** - str ## _def_ `Endswith()` {#Endswith} - **说明:** 响应触发后缀 - **参数** empty - **返回** - str ## _def_ `Fullmatch()` {#Fullmatch} - **说明:** 响应触发完整消息 - **参数** empty - **返回** - str ## _def_ `Keyword()` {#Keyword} - **说明:** 响应触发关键字 - **参数** empty - **返回** - str ## _def_ `Received(id=None, default=None)` {#Received} - **说明:** `receive` 事件参数 - **参数** - `id` (str | None) - `default` (Any) - **返回** - Any ## _def_ `LastReceived(default=None)` {#LastReceived} - **说明:** `last_receive` 事件参数 - **参数** - `default` (Any) - **返回** - Any ## _def_ `ReceivePromptResult(id=None)` {#ReceivePromptResult} - **说明:** `receive` prompt 发送结果 - **参数** - `id` (str | None) - **返回** - Any ## _def_ `PausePromptResult()` {#PausePromptResult} - **说明:** `pause` prompt 发送结果 - **参数** empty - **返回** - Any ================================================ FILE: website/versioned_docs/version-2.4.3/api/permission.md ================================================ --- mdx: format: md sidebar_position: 6 description: nonebot.permission 模块 --- # nonebot.permission 本模块是 [Matcher.permission](matcher.md#Matcher-permission) 的类型定义。 每个[事件响应器](matcher.md#Matcher) 拥有一个 [Permission](#Permission),其中是 `PermissionChecker` 的集合。 只要有一个 `PermissionChecker` 检查结果为 `True` 时就会继续运行。 ## _def_ `USER(*users, perm=None)` {#USER} - **说明** 匹配当前事件属于指定会话。 如果 `perm` 中仅有 `User` 类型的权限检查函数,则会去除原有检查函数的会话 ID 限制。 - **参数** - `*users` (str) - `perm` (Permission | None): 需要同时满足的权限 - `user`: 会话白名单 - **返回** - untyped ## _class_ `Permission(*checkers)` {#Permission} - **说明** 权限类。 当事件传递时,在 [Matcher](matcher.md#Matcher) 运行前进行检查。 - **参数** - `*checkers` ([T_PermissionChecker](typing.md#T-PermissionChecker) | [Dependent](dependencies/index.md#Dependent)[bool]): PermissionChecker - **用法** ```python Permission(async_function) | sync_function # 等价于 Permission(async_function, sync_function) ``` ### _instance-var_ `checkers` {#Permission-checkers} - **类型:** set[[Dependent](dependencies/index.md#Dependent)[bool]] - **说明:** 存储 `PermissionChecker` ### _async method_ `__call__(bot, event, stack=None, dependency_cache=None)` {#Permission---call--} - **说明:** 检查是否满足某个权限。 - **参数** - `bot` ([Bot](adapters/index.md#Bot)): Bot 对象 - `event` ([Event](adapters/index.md#Event)): Event 对象 - `stack` (AsyncExitStack | None): 异步上下文栈 - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None): 依赖缓存 - **返回** - bool ## _class_ `User(users, perm=None)` {#User} - **说明:** 检查当前事件是否属于指定会话。 - **参数** - `users` (tuple[str, ...]): 会话 ID 元组 - `perm` (Permission | None): 需同时满足的权限 ### _classmethod_ `from_event(event, perm=None)` {#User-from-event} - **说明** 从事件中获取会话 ID。 如果 `perm` 中仅有 `User` 类型的权限检查函数,则会去除原有的会话 ID 限制。 - **参数** - `event` ([Event](adapters/index.md#Event)): Event 对象 - `perm` (Permission | None): 需同时满足的权限 - **返回** - Self ### _classmethod_ `from_permission(*users, perm=None)` {#User-from-permission} - **说明** 指定会话与权限。 如果 `perm` 中仅有 `User` 类型的权限检查函数,则会去除原有的会话 ID 限制。 - **参数** - `*users` (str): 会话白名单 - `perm` (Permission | None): 需同时满足的权限 - **返回** - Self ## _class_ `Message()` {#Message} - **说明:** 检查是否为消息事件 - **参数** auto ## _class_ `Notice()` {#Notice} - **说明:** 检查是否为通知事件 - **参数** auto ## _class_ `Request()` {#Request} - **说明:** 检查是否为请求事件 - **参数** auto ## _class_ `MetaEvent()` {#MetaEvent} - **说明:** 检查是否为元事件 - **参数** auto ## _var_ `MESSAGE` {#MESSAGE} - **类型:** [Permission](#Permission) - **说明** 匹配任意 `message` 类型事件 仅在需要同时捕获不同类型事件时使用,优先使用 message type 的 Matcher。 ## _var_ `NOTICE` {#NOTICE} - **类型:** [Permission](#Permission) - **说明** 匹配任意 `notice` 类型事件 仅在需要同时捕获不同类型事件时使用,优先使用 notice type 的 Matcher。 ## _var_ `REQUEST` {#REQUEST} - **类型:** [Permission](#Permission) - **说明** 匹配任意 `request` 类型事件 仅在需要同时捕获不同类型事件时使用,优先使用 request type 的 Matcher。 ## _var_ `METAEVENT` {#METAEVENT} - **类型:** [Permission](#Permission) - **说明** 匹配任意 `meta_event` 类型事件 仅在需要同时捕获不同类型事件时使用,优先使用 meta_event type 的 Matcher。 ## _class_ `SuperUser()` {#SuperUser} - **说明:** 检查当前事件是否是消息事件且属于超级管理员 - **参数** auto ## _var_ `SUPERUSER` {#SUPERUSER} - **类型:** [Permission](#Permission) - **说明:** 匹配任意超级用户事件 ================================================ FILE: website/versioned_docs/version-2.4.3/api/plugin/_category_.json ================================================ { "position": 12 } ================================================ FILE: website/versioned_docs/version-2.4.3/api/plugin/index.md ================================================ --- mdx: format: md sidebar_position: 0 description: nonebot.plugin 模块 --- # nonebot.plugin 本模块为 NoneBot 插件开发提供便携的定义函数。 ## 快捷导入 为方便使用,本模块从子模块导入了部分内容,以下内容可以直接通过本模块导入: - `on` => [`on`](on.md#on) - `on_metaevent` => [`on_metaevent`](on.md#on-metaevent) - `on_message` => [`on_message`](on.md#on-message) - `on_notice` => [`on_notice`](on.md#on-notice) - `on_request` => [`on_request`](on.md#on-request) - `on_startswith` => [`on_startswith`](on.md#on-startswith) - `on_endswith` => [`on_endswith`](on.md#on-endswith) - `on_fullmatch` => [`on_fullmatch`](on.md#on-fullmatch) - `on_keyword` => [`on_keyword`](on.md#on-keyword) - `on_command` => [`on_command`](on.md#on-command) - `on_shell_command` => [`on_shell_command`](on.md#on-shell-command) - `on_regex` => [`on_regex`](on.md#on-regex) - `on_type` => [`on_type`](on.md#on-type) - `CommandGroup` => [`CommandGroup`](on.md#CommandGroup) - `Matchergroup` => [`MatcherGroup`](on.md#MatcherGroup) - `load_plugin` => [`load_plugin`](load.md#load-plugin) - `load_plugins` => [`load_plugins`](load.md#load-plugins) - `load_all_plugins` => [`load_all_plugins`](load.md#load-all-plugins) - `load_from_json` => [`load_from_json`](load.md#load-from-json) - `load_from_toml` => [`load_from_toml`](load.md#load-from-toml) - `load_builtin_plugin` => [`load_builtin_plugin`](load.md#load-builtin-plugin) - `load_builtin_plugins` => [`load_builtin_plugins`](load.md#load-builtin-plugins) - `require` => [`require`](load.md#require) - `PluginMetadata` => [`PluginMetadata`](model.md#PluginMetadata) ## _def_ `get_plugin(plugin_id)` {#get-plugin} - **说明** 获取已经导入的某个插件。 如果为 `load_plugins` 文件夹导入的插件,则为文件(夹)名。 如果为嵌套的子插件,标识符为 `父插件标识符:子插件文件(夹)名`。 - **参数** - `plugin_id` (str): 插件标识符,即 [Plugin.id\_](model.md#Plugin-id-)。 - **返回** - [Plugin](model.md#Plugin) | None ## _def_ `get_plugin_by_module_name(module_name)` {#get-plugin-by-module-name} - **说明** 通过模块名获取已经导入的某个插件。 如果提供的模块名为某个插件的子模块,同样会返回该插件。 - **参数** - `module_name` (str): 模块名,即 [Plugin.module_name](model.md#Plugin-module-name)。 - **返回** - [Plugin](model.md#Plugin) | None ## _def_ `get_loaded_plugins()` {#get-loaded-plugins} - **说明:** 获取当前已导入的所有插件。 - **参数** empty - **返回** - set[[Plugin](model.md#Plugin)] ## _def_ `get_available_plugin_names()` {#get-available-plugin-names} - **说明:** 获取当前所有可用的插件标识符(包含尚未加载的插件)。 - **参数** empty - **返回** - set[str] ## _def_ `get_plugin_config(config)` {#get-plugin-config} - **说明:** 从全局配置获取当前插件需要的配置项。 - **参数** - `config` (type[C]) - **返回** - C ================================================ FILE: website/versioned_docs/version-2.4.3/api/plugin/load.md ================================================ --- mdx: format: md sidebar_position: 1 description: nonebot.plugin.load 模块 --- # nonebot.plugin.load 本模块定义插件加载接口。 ## _def_ `load_plugin(module_path)` {#load-plugin} - **说明:** 加载单个插件,可以是本地插件或是通过 `pip` 安装的插件。 - **参数** - `module_path` (str | Path): 插件名称 `path.to.your.plugin` 或插件路径 `pathlib.Path(path/to/your/plugin)` - **返回** - [Plugin](model.md#Plugin) | None ## _def_ `load_plugins(*plugin_dir)` {#load-plugins} - **说明:** 导入文件夹下多个插件,以 `_` 开头的插件不会被导入! - **参数** - `*plugin_dir` (str): 文件夹路径 - **返回** - set[[Plugin](model.md#Plugin)] ## _def_ `load_all_plugins(module_path, plugin_dir)` {#load-all-plugins} - **说明:** 导入指定列表中的插件以及指定目录下多个插件,以 `_` 开头的插件不会被导入! - **参数** - `module_path` (Iterable[str]): 指定插件集合 - `plugin_dir` (Iterable[str]): 指定文件夹路径集合 - **返回** - set[[Plugin](model.md#Plugin)] ## _def_ `load_from_json(file_path, encoding="utf-8")` {#load-from-json} - **说明:** 导入指定 json 文件中的 `plugins` 以及 `plugin_dirs` 下多个插件。 以 `_` 开头的插件不会被导入! - **参数** - `file_path` (str): 指定 json 文件路径 - `encoding` (str): 指定 json 文件编码 - **返回** - set[[Plugin](model.md#Plugin)] - **用法** ```json title=plugins.json { "plugins": ["some_plugin"], "plugin_dirs": ["some_dir"] } ``` ```python nonebot.load_from_json("plugins.json") ``` ## _def_ `load_from_toml(file_path, encoding="utf-8")` {#load-from-toml} - **说明:** 导入指定 toml 文件 `[tool.nonebot]` 中的 `plugins` 以及 `plugin_dirs` 下多个插件。 以 `_` 开头的插件不会被导入! - **参数** - `file_path` (str): 指定 toml 文件路径 - `encoding` (str): 指定 toml 文件编码 - **返回** - set[[Plugin](model.md#Plugin)] - **用法** ```toml title=pyproject.toml [tool.nonebot] plugins = ["some_plugin"] plugin_dirs = ["some_dir"] ``` ```python nonebot.load_from_toml("pyproject.toml") ``` ## _def_ `load_builtin_plugin(name)` {#load-builtin-plugin} - **说明:** 导入 NoneBot 内置插件。 - **参数** - `name` (str): 插件名称 - **返回** - [Plugin](model.md#Plugin) | None ## _def_ `load_builtin_plugins(*plugins)` {#load-builtin-plugins} - **说明:** 导入多个 NoneBot 内置插件。 - **参数** - `*plugins` (str): 插件名称列表 - **返回** - set[[Plugin](model.md#Plugin)] ## _def_ `require(name)` {#require} - **说明:** 声明依赖插件。 - **参数** - `name` (str): 插件模块名或插件标识符,仅在已声明插件的情况下可使用标识符。 - **返回** - ModuleType - **异常** - RuntimeError: 插件无法加载 ## _def_ `inherit_supported_adapters(*names)` {#inherit-supported-adapters} - **说明** 获取已加载插件的适配器支持状态集合。 如果传入了多个插件名称,返回值会自动取交集。 - **参数** - `*names` (str): 插件名称列表。 - **返回** - set[str] | None - **异常** - RuntimeError: 插件未加载 - ValueError: 插件缺少元数据 ================================================ FILE: website/versioned_docs/version-2.4.3/api/plugin/manager.md ================================================ --- mdx: format: md sidebar_position: 5 description: nonebot.plugin.manager 模块 --- # nonebot.plugin.manager 本模块实现插件加载流程。 参考: [import hooks](https://docs.python.org/3/reference/import.html#import-hooks), [PEP302](https://www.python.org/dev/peps/pep-0302/) ## _class_ `PluginManager(plugins=None, search_path=None)` {#PluginManager} - **说明:** 插件管理器。 - **参数** - `plugins` (Iterable[str] | None): 独立插件模块名集合。 - `search_path` (Iterable[str] | None): 插件搜索路径(文件夹),相对于当前工作目录。 ### _property_ `third_party_plugins` {#PluginManager-third-party-plugins} - **类型:** set[str] - **说明:** 返回所有独立插件标识符。 ### _property_ `searched_plugins` {#PluginManager-searched-plugins} - **类型:** set[str] - **说明:** 返回已搜索到的插件标识符。 ### _property_ `available_plugins` {#PluginManager-available-plugins} - **类型:** set[str] - **说明:** 返回当前插件管理器中可用的插件标识符。 ### _property_ `controlled_modules` {#PluginManager-controlled-modules} - **类型:** dict[str, str] - **说明:** 返回当前插件管理器中控制的插件标识符与模块路径映射字典。 ### _method_ `load_plugin(name)` {#PluginManager-load-plugin} - **说明** 加载指定插件。 可以使用完整插件模块名或者插件标识符加载。 - **参数** - `name` (str): 插件名称或插件标识符。 - **返回** - [Plugin](model.md#Plugin) | None ### _method_ `load_all_plugins()` {#PluginManager-load-all-plugins} - **说明:** 加载所有可用插件。 - **参数** empty - **返回** - set[[Plugin](model.md#Plugin)] ## _class_ `PluginFinder()` {#PluginFinder} - **参数** auto ### _method_ `find_spec(fullname, path, target=None)` {#PluginFinder-find-spec} - **参数** - `fullname` (str) - `path` (Sequence[str] | None) - `target` (ModuleType | None) - **返回** - untyped ## _class_ `PluginLoader(manager, fullname, path)` {#PluginLoader} - **参数** - `manager` (PluginManager) - `fullname` (str) - `path` (str) ### _method_ `create_module(spec)` {#PluginLoader-create-module} - **参数** - `spec` - **返回** - ModuleType | None ### _method_ `exec_module(module)` {#PluginLoader-exec-module} - **参数** - `module` (ModuleType) - **返回** - None ================================================ FILE: website/versioned_docs/version-2.4.3/api/plugin/model.md ================================================ --- mdx: format: md sidebar_position: 3 description: nonebot.plugin.model 模块 --- # nonebot.plugin.model 本模块定义插件相关信息。 ## _class_ `PluginMetadata()` {#PluginMetadata} - **说明:** 插件元信息,由插件编写者提供 - **参数** auto ### _instance-var_ `name` {#PluginMetadata-name} - **类型:** str - **说明:** 插件名称 ### _instance-var_ `description` {#PluginMetadata-description} - **类型:** str - **说明:** 插件功能介绍 ### _instance-var_ `usage` {#PluginMetadata-usage} - **类型:** str - **说明:** 插件使用方法 ### _class-var_ `type` {#PluginMetadata-type} - **类型:** str | None - **说明:** 插件类型,用于商店分类 ### _class-var_ `homepage` {#PluginMetadata-homepage} - **类型:** str | None - **说明:** 插件主页 ### _class-var_ `config` {#PluginMetadata-config} - **类型:** type[BaseModel] | None - **说明:** 插件配置项 ### _class-var_ `supported_adapters` {#PluginMetadata-supported-adapters} - **类型:** set[str] | None - **说明** 插件支持的适配器模块路径 格式为 `[:]`,`~` 为 `nonebot.adapters.` 的缩写。 `None` 表示支持**所有适配器**。 ### _class-var_ `extra` {#PluginMetadata-extra} - **类型:** dict[Any, Any] - **说明:** 插件额外信息,可由插件编写者自由扩展定义 ### _method_ `get_supported_adapters()` {#PluginMetadata-get-supported-adapters} - **说明:** 获取当前已安装的插件支持适配器类列表 - **参数** empty - **返回** - set[type[[Adapter](../adapters/index.md#Adapter)]] | None ## _class_ `Plugin()` {#Plugin} - **说明:** 存储插件信息 - **参数** auto ### _instance-var_ `name` {#Plugin-name} - **类型:** str - **说明:** 插件名称,NoneBot 使用 文件/文件夹 名称作为插件名称 ### _instance-var_ `module` {#Plugin-module} - **类型:** ModuleType - **说明:** 插件模块对象 ### _instance-var_ `module_name` {#Plugin-module-name} - **类型:** str - **说明:** 点分割模块路径 ### _instance-var_ `manager` {#Plugin-manager} - **类型:** [PluginManager](manager.md#PluginManager) - **说明:** 导入该插件的插件管理器 ### _class-var_ `matcher` {#Plugin-matcher} - **类型:** set[type[[Matcher](../matcher.md#Matcher)]] - **说明:** 插件加载时定义的 `Matcher` ### _class-var_ `parent_plugin` {#Plugin-parent-plugin} - **类型:** Plugin | None - **说明:** 父插件 ### _class-var_ `sub_plugins` {#Plugin-sub-plugins} - **类型:** set[Plugin] - **说明:** 子插件集合 ### _class-var_ `metadata` {#Plugin-metadata} - **类型:** PluginMetadata | None - **说明:** 插件元信息 ### _property_ `id_` {#Plugin-id-} - **类型:** str - **说明:** 插件索引标识 ================================================ FILE: website/versioned_docs/version-2.4.3/api/plugin/on.md ================================================ --- mdx: format: md sidebar_position: 2 description: nonebot.plugin.on 模块 --- # nonebot.plugin.on 本模块定义事件响应器便携定义函数。 ## _def_ `store_matcher(matcher)` {#store-matcher} - **说明:** 存储一个事件响应器到插件。 - **参数** - `matcher` (type[[Matcher](../matcher.md#Matcher)]): 事件响应器 - **返回** - None ## _def_ `get_matcher_plugin(depth=...)` {#get-matcher-plugin} - **说明** 获取事件响应器定义所在插件。 **Deprecated**, 请使用 [get_matcher_source](#get-matcher-source) 获取信息。 - **参数** - `depth` (int): 调用栈深度 - **返回** - [Plugin](model.md#Plugin) | None ## _def_ `get_matcher_module(depth=...)` {#get-matcher-module} - **说明** 获取事件响应器定义所在模块。 **Deprecated**, 请使用 [get_matcher_source](#get-matcher-source) 获取信息。 - **参数** - `depth` (int): 调用栈深度 - **返回** - ModuleType | None ## _def_ `get_matcher_source(depth=...)` {#get-matcher-source} - **说明:** 获取事件响应器定义所在源码信息。 - **参数** - `depth` (int): 调用栈深度 - **返回** - MatcherSource | None ## _def_ `on(type="", rule=..., permission=..., *, handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on} - **说明:** 注册一个基础事件响应器,可自定义类型。 - **参数** - `type` (str): 事件响应器类型 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ## _def_ `on_metaevent(rule=..., permission=..., *, handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-metaevent} - **说明:** 注册一个元事件响应器。 - **参数** - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ## _def_ `on_message(rule=..., permission=..., *, handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-message} - **说明:** 注册一个消息事件响应器。 - **参数** - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ## _def_ `on_notice(rule=..., permission=..., *, handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-notice} - **说明:** 注册一个通知事件响应器。 - **参数** - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ## _def_ `on_request(rule=..., permission=..., *, handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-request} - **说明:** 注册一个请求事件响应器。 - **参数** - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ## _def_ `on_startswith(msg, rule=..., ignorecase=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-startswith} - **说明:** 注册一个消息事件响应器,并且当消息的**文本部分**以指定内容开头时响应。 - **参数** - `msg` (str | tuple[str, ...]): 指定消息开头内容 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `ignorecase` (bool): 是否忽略大小写 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ## _def_ `on_endswith(msg, rule=..., ignorecase=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-endswith} - **说明:** 注册一个消息事件响应器,并且当消息的**文本部分**以指定内容结尾时响应。 - **参数** - `msg` (str | tuple[str, ...]): 指定消息结尾内容 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `ignorecase` (bool): 是否忽略大小写 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ## _def_ `on_fullmatch(msg, rule=..., ignorecase=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-fullmatch} - **说明:** 注册一个消息事件响应器,并且当消息的**文本部分**与指定内容完全一致时响应。 - **参数** - `msg` (str | tuple[str, ...]): 指定消息全匹配内容 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `ignorecase` (bool): 是否忽略大小写 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ## _def_ `on_keyword(keywords, rule=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-keyword} - **说明:** 注册一个消息事件响应器,并且当消息纯文本部分包含关键词时响应。 - **参数** - `keywords` (set[str]): 关键词列表 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ## _def_ `on_command(cmd, rule=..., aliases=..., force_whitespace=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-command} - **说明** 注册一个消息事件响应器,并且当消息以指定命令开头时响应。 命令匹配规则参考: `命令形式匹配 `\_ - **参数** - `cmd` (str | tuple[str, ...]): 指定命令内容 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `aliases` (set[str | tuple[str, ...]] | None): 命令别名 - `force_whitespace` (str | bool | None): 是否强制命令后必须有指定空白符 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ## _def_ `on_shell_command(cmd, rule=..., aliases=..., parser=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-shell-command} - **说明** 注册一个支持 `shell_like` 解析参数的命令消息事件响应器。 与普通的 `on_command` 不同的是,在添加 `parser` 参数时, 响应器会自动处理消息。 可以通过 [ShellCommandArgv](../params.md#ShellCommandArgv) 获取原始参数列表, 通过 [ShellCommandArgs](../params.md#ShellCommandArgs) 获取解析后的参数字典。 - **参数** - `cmd` (str | tuple[str, ...]): 指定命令内容 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `aliases` (set[str | tuple[str, ...]] | None): 命令别名 - `parser` ([ArgumentParser](../rule.md#ArgumentParser) | None): `nonebot.rule.ArgumentParser` 对象 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ## _def_ `on_regex(pattern, flags=..., rule=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-regex} - **说明** 注册一个消息事件响应器,并且当消息匹配正则表达式时响应。 命令匹配规则参考: `正则匹配 `\_ - **参数** - `pattern` (str): 正则表达式 - `flags` (int | re.RegexFlag): 正则匹配标志 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ## _def_ `on_type(types, rule=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-type} - **说明:** 注册一个事件响应器,并且当事件为指定类型时响应。 - **参数** - `types` (type[[Event](../adapters/index.md#Event)] | tuple[type[[Event](../adapters/index.md#Event)], ...]): 事件类型 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ## _class_ `CommandGroup(cmd, prefix_aliases=..., *, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#CommandGroup} - **参数** - `cmd` (str | tuple[str, ...]) - `prefix_aliases` (bool) - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None) - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None) - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None) - `temp` (bool) - `expire_time` (datetime | timedelta | None) - `priority` (int) - `block` (bool) - `state` ([T_State](../typing.md#T-State) | None) ### _method_ `command(cmd, *, rule=..., aliases=..., force_whitespace=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#CommandGroup-command} - **说明:** 注册一个新的命令。新参数将会覆盖命令组默认值 - **参数** - `cmd` (str | tuple[str, ...]): 指定命令内容 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `aliases` (set[str | tuple[str, ...]] | None): 命令别名 - `force_whitespace` (str | bool | None): 是否强制命令后必须有指定空白符 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ### _method_ `shell_command(cmd, *, rule=..., aliases=..., parser=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#CommandGroup-shell-command} - **说明:** 注册一个新的 `shell_like` 命令。新参数将会覆盖命令组默认值 - **参数** - `cmd` (str | tuple[str, ...]): 指定命令内容 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `aliases` (set[str | tuple[str, ...]] | None): 命令别名 - `parser` ([ArgumentParser](../rule.md#ArgumentParser) | None): `nonebot.rule.ArgumentParser` 对象 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ## _class_ `MatcherGroup(*, type=..., rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup} - **参数** - `type` (str) - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None) - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None) - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None) - `temp` (bool) - `expire_time` (datetime | timedelta | None) - `priority` (int) - `block` (bool) - `state` ([T_State](../typing.md#T-State) | None) ### _method_ `on(*, type=..., rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on} - **说明:** 注册一个基础事件响应器,可自定义类型。 - **参数** - `type` (str): 事件响应器类型 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ### _method_ `on_metaevent(*, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-metaevent} - **说明:** 注册一个元事件响应器。 - **参数** - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ### _method_ `on_message(*, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-message} - **说明:** 注册一个消息事件响应器。 - **参数** - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ### _method_ `on_notice(*, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-notice} - **说明:** 注册一个通知事件响应器。 - **参数** - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ### _method_ `on_request(*, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-request} - **说明:** 注册一个请求事件响应器。 - **参数** - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ### _method_ `on_startswith(msg, *, ignorecase=..., rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-startswith} - **说明:** 注册一个消息事件响应器,并且当消息的**文本部分**以指定内容开头时响应。 - **参数** - `msg` (str | tuple[str, ...]): 指定消息开头内容 - `ignorecase` (bool): 是否忽略大小写 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ### _method_ `on_endswith(msg, *, ignorecase=..., rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-endswith} - **说明:** 注册一个消息事件响应器,并且当消息的**文本部分**以指定内容结尾时响应。 - **参数** - `msg` (str | tuple[str, ...]): 指定消息结尾内容 - `ignorecase` (bool): 是否忽略大小写 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ### _method_ `on_fullmatch(msg, *, ignorecase=..., rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-fullmatch} - **说明:** 注册一个消息事件响应器,并且当消息的**文本部分**与指定内容完全一致时响应。 - **参数** - `msg` (str | tuple[str, ...]): 指定消息全匹配内容 - `ignorecase` (bool): 是否忽略大小写 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ### _method_ `on_keyword(keywords, *, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-keyword} - **说明:** 注册一个消息事件响应器,并且当消息纯文本部分包含关键词时响应。 - **参数** - `keywords` (set[str]): 关键词列表 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ### _method_ `on_command(cmd, aliases=..., force_whitespace=..., *, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-command} - **说明** 注册一个消息事件响应器,并且当消息以指定命令开头时响应。 命令匹配规则参考: `命令形式匹配 `\_ - **参数** - `cmd` (str | tuple[str, ...]): 指定命令内容 - `aliases` (set[str | tuple[str, ...]] | None): 命令别名 - `force_whitespace` (str | bool | None): 是否强制命令后必须有指定空白符 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ### _method_ `on_shell_command(cmd, aliases=..., parser=..., *, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-shell-command} - **说明** 注册一个支持 `shell_like` 解析参数的命令消息事件响应器。 与普通的 `on_command` 不同的是,在添加 `parser` 参数时, 响应器会自动处理消息。 可以通过 [ShellCommandArgv](../params.md#ShellCommandArgv) 获取原始参数列表, 通过 [ShellCommandArgs](../params.md#ShellCommandArgs) 获取解析后的参数字典。 - **参数** - `cmd` (str | tuple[str, ...]): 指定命令内容 - `aliases` (set[str | tuple[str, ...]] | None): 命令别名 - `parser` ([ArgumentParser](../rule.md#ArgumentParser) | None): `nonebot.rule.ArgumentParser` 对象 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ### _method_ `on_regex(pattern, flags=..., *, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-regex} - **说明** 注册一个消息事件响应器,并且当消息匹配正则表达式时响应。 命令匹配规则参考: `正则匹配 `\_ - **参数** - `pattern` (str): 正则表达式 - `flags` (int | re.RegexFlag): 正则匹配标志 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ### _method_ `on_type(types, *, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-type} - **说明:** 注册一个事件响应器,并且当事件为指定类型时响应。 - **参数** - `types` (type[[Event](../adapters/index.md#Event)] | tuple[type[[Event](../adapters/index.md#Event)]]): 事件类型 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ================================================ FILE: website/versioned_docs/version-2.4.3/api/rule.md ================================================ --- mdx: format: md sidebar_position: 5 description: nonebot.rule 模块 --- # nonebot.rule 本模块是 [Matcher.rule](matcher.md#Matcher-rule) 的类型定义。 每个[事件响应器](matcher.md#Matcher)拥有一个 [Rule](#Rule),其中是 `RuleChecker` 的集合。 只有当所有 `RuleChecker` 检查结果为 `True` 时继续运行。 ## _class_ `Rule(*checkers)` {#Rule} - **说明** 规则类。 当事件传递时,在 [Matcher](matcher.md#Matcher) 运行前进行检查。 - **参数** - `*checkers` ([T_RuleChecker](typing.md#T-RuleChecker) | [Dependent](dependencies/index.md#Dependent)[bool]): RuleChecker - **用法** ```python Rule(async_function) & sync_function # 等价于 Rule(async_function, sync_function) ``` ### _instance-var_ `checkers` {#Rule-checkers} - **类型:** set[[Dependent](dependencies/index.md#Dependent)[bool]] - **说明:** 存储 `RuleChecker` ### _async method_ `__call__(bot, event, state, stack=None, dependency_cache=None)` {#Rule---call--} - **说明:** 检查是否符合所有规则 - **参数** - `bot` ([Bot](adapters/index.md#Bot)): Bot 对象 - `event` ([Event](adapters/index.md#Event)): Event 对象 - `state` ([T_State](typing.md#T-State)): 当前 State - `stack` (AsyncExitStack | None): 异步上下文栈 - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None): 依赖缓存 - **返回** - bool ## _class_ `CMD_RESULT()` {#CMD-RESULT} - **参数** auto ## _class_ `TRIE_VALUE()` {#TRIE-VALUE} - **说明:** TRIE_VALUE(command_start, command) - **参数** auto ## _class_ `StartswithRule(msg, ignorecase=False)` {#StartswithRule} - **说明:** 检查消息纯文本是否以指定字符串开头。 - **参数** - `msg` (tuple[str, ...]): 指定消息开头字符串元组 - `ignorecase` (bool): 是否忽略大小写 ## _def_ `startswith(msg, ignorecase=False)` {#startswith} - **说明:** 匹配消息纯文本开头。 - **参数** - `msg` (str | tuple[str, ...]): 指定消息开头字符串元组 - `ignorecase` (bool): 是否忽略大小写 - **返回** - [Rule](#Rule) ## _class_ `EndswithRule(msg, ignorecase=False)` {#EndswithRule} - **说明:** 检查消息纯文本是否以指定字符串结尾。 - **参数** - `msg` (tuple[str, ...]): 指定消息结尾字符串元组 - `ignorecase` (bool): 是否忽略大小写 ## _def_ `endswith(msg, ignorecase=False)` {#endswith} - **说明:** 匹配消息纯文本结尾。 - **参数** - `msg` (str | tuple[str, ...]): 指定消息开头字符串元组 - `ignorecase` (bool): 是否忽略大小写 - **返回** - [Rule](#Rule) ## _class_ `FullmatchRule(msg, ignorecase=False)` {#FullmatchRule} - **说明:** 检查消息纯文本是否与指定字符串全匹配。 - **参数** - `msg` (tuple[str, ...]): 指定消息全匹配字符串元组 - `ignorecase` (bool): 是否忽略大小写 ## _def_ `fullmatch(msg, ignorecase=False)` {#fullmatch} - **说明:** 完全匹配消息。 - **参数** - `msg` (str | tuple[str, ...]): 指定消息全匹配字符串元组 - `ignorecase` (bool): 是否忽略大小写 - **返回** - [Rule](#Rule) ## _class_ `KeywordsRule(*keywords)` {#KeywordsRule} - **说明:** 检查消息纯文本是否包含指定关键字。 - **参数** - `*keywords` (str): 指定关键字元组 ## _def_ `keyword(*keywords)` {#keyword} - **说明:** 匹配消息纯文本关键词。 - **参数** - `*keywords` (str): 指定关键字元组 - **返回** - [Rule](#Rule) ## _class_ `CommandRule(cmds, force_whitespace=None)` {#CommandRule} - **说明:** 检查消息是否为指定命令。 - **参数** - `cmds` (list[tuple[str, ...]]): 指定命令元组列表 - `force_whitespace` (str | bool | None): 是否强制命令后必须有指定空白符 ## _def_ `command(*cmds, force_whitespace=None)` {#command} - **说明** 匹配消息命令。 根据配置里提供的 [`command_start`](config.md#Config-command-start), [`command_sep`](config.md#Config-command-sep) 判断消息是否为命令。 可以通过 [Command](params.md#Command) 获取匹配成功的命令(例: `("test",)`), 通过 [RawCommand](params.md#RawCommand) 获取匹配成功的原始命令文本(例: `"/test"`), 通过 [CommandArg](params.md#CommandArg) 获取匹配成功的命令参数。 - **参数** - `*cmds` (str | tuple[str, ...]): 命令文本或命令元组 - `force_whitespace` (str | bool | None): 是否强制命令后必须有指定空白符 - **返回** - [Rule](#Rule) - **用法** 使用默认 `command_start`, `command_sep` 配置情况下: 命令 `("test",)` 可以匹配: `/test` 开头的消息 命令 `("test", "sub")` 可以匹配: `/test.sub` 开头的消息 :::tip 提示 命令内容与后续消息间无需空格! ::: ## _class_ `ArgumentParser()` {#ArgumentParser} - **说明** `shell_like` 命令参数解析器,解析出错时不会退出程序。 支持 [Message](adapters/index.md#Message) 富文本解析。 - **参数** auto - **用法** 用法与 `argparse.ArgumentParser` 相同, 参考文档: [argparse](https://docs.python.org/3/library/argparse.html) ### _method_ `parse_known_args(args=None, namespace=None)` {#ArgumentParser-parse-known-args} - **重载** **1.** `(args=None, namespace=None) -> tuple[Namespace, list[str | MessageSegment]]` - **参数** - `args` (Sequence[str | [MessageSegment](adapters/index.md#MessageSegment)] | None) - `namespace` (None) - **返回** - tuple[Namespace, list[str | [MessageSegment](adapters/index.md#MessageSegment)]] **2.** `(args, namespace) -> tuple[T, list[str | MessageSegment]]` - **参数** - `args` (Sequence[str | [MessageSegment](adapters/index.md#MessageSegment)] | None) - `namespace` (T) - **返回** - tuple[T, list[str | [MessageSegment](adapters/index.md#MessageSegment)]] **3.** `(*, namespace) -> tuple[T, list[str | MessageSegment]]` - **参数** - `namespace` (T) - **返回** - tuple[T, list[str | [MessageSegment](adapters/index.md#MessageSegment)]] ## _class_ `ShellCommandRule(cmds, parser)` {#ShellCommandRule} - **说明:** 检查消息是否为指定 shell 命令。 - **参数** - `cmds` (list[tuple[str, ...]]): 指定命令元组列表 - `parser` (ArgumentParser | None): 可选参数解析器 ## _def_ `shell_command(*cmds, parser=None)` {#shell-command} - **说明** 匹配 `shell_like` 形式的消息命令。 根据配置里提供的 [`command_start`](config.md#Config-command-start), [`command_sep`](config.md#Config-command-sep) 判断消息是否为命令。 可以通过 [Command](params.md#Command) 获取匹配成功的命令 (例: `("test",)`), 通过 [RawCommand](params.md#RawCommand) 获取匹配成功的原始命令文本 (例: `"/test"`), 通过 [ShellCommandArgv](params.md#ShellCommandArgv) 获取解析前的参数列表 (例: `["arg", "-h"]`), 通过 [ShellCommandArgs](params.md#ShellCommandArgs) 获取解析后的参数字典 (例: `{"arg": "arg", "h": True}`)。 :::caution 警告 如果参数解析失败,则通过 [ShellCommandArgs](params.md#ShellCommandArgs) 获取的将是 [ParserExit](exception.md#ParserExit) 异常。 ::: - **参数** - `*cmds` (str | tuple[str, ...]): 命令文本或命令元组 - `parser` (ArgumentParser | None): [ArgumentParser](#ArgumentParser) 对象 - **返回** - [Rule](#Rule) - **用法** 使用默认 `command_start`, `command_sep` 配置,更多示例参考 [argparse](https://docs.python.org/3/library/argparse.html) 标准库文档。 ```python from nonebot.rule import ArgumentParser parser = ArgumentParser() parser.add_argument("-a", action="store_true") rule = shell_command("ls", parser=parser) ``` :::tip 提示 命令内容与后续消息间无需空格! ::: ## _class_ `RegexRule(regex, flags=0)` {#RegexRule} - **说明:** 检查消息字符串是否符合指定正则表达式。 - **参数** - `regex` (str): 正则表达式 - `flags` (int): 正则表达式标记 ## _def_ `regex(regex, flags=0)` {#regex} - **说明** 匹配符合正则表达式的消息字符串。 可以通过 [RegexStr](params.md#RegexStr) 获取匹配成功的字符串, 通过 [RegexGroup](params.md#RegexGroup) 获取匹配成功的 group 元组, 通过 [RegexDict](params.md#RegexDict) 获取匹配成功的 group 字典。 - **参数** - `regex` (str): 正则表达式 - `flags` (int | re.RegexFlag): 正则表达式标记 - **返回** - [Rule](#Rule) :::tip 提示 正则表达式匹配使用 search 而非 match,如需从头匹配请使用 `r"^xxx"` 来确保匹配开头 ::: :::tip 提示 正则表达式匹配使用 `EventMessage` 的 `str` 字符串, 而非 `EventMessage` 的 `PlainText` 纯文本字符串 ::: ## _class_ `ToMeRule()` {#ToMeRule} - **说明:** 检查事件是否与机器人有关。 - **参数** auto ## _def_ `to_me()` {#to-me} - **说明:** 匹配与机器人有关的事件。 - **参数** empty - **返回** - [Rule](#Rule) ## _class_ `IsTypeRule(*types)` {#IsTypeRule} - **说明:** 检查事件类型是否为指定类型。 - **参数** - `*types` (type[[Event](adapters/index.md#Event)]) ## _def_ `is_type(*types)` {#is-type} - **说明:** 匹配事件类型。 - **参数** - `*types` (type[[Event](adapters/index.md#Event)]): 事件类型 - **返回** - [Rule](#Rule) ================================================ FILE: website/versioned_docs/version-2.4.3/api/typing.md ================================================ --- mdx: format: md sidebar_position: 11 description: nonebot.typing 模块 --- # nonebot.typing 本模块定义了 NoneBot 模块中共享的一些类型。 使用 Python 的 Type Hint 语法, 参考 [`PEP 484`](https://www.python.org/dev/peps/pep-0484/), [`PEP 526`](https://www.python.org/dev/peps/pep-0526/) 和 [`typing`](https://docs.python.org/3/library/typing.html)。 ## _def_ `overrides(InterfaceClass)` {#overrides} - **说明:** 标记一个方法为父类 interface 的 implement - **参数** - `InterfaceClass` (object) - **返回** - untyped ## _def_ `type_has_args(type_)` {#type-has-args} - **参数** - `type_` (type[Any]) - **返回** - bool ## _def_ `origin_is_union(origin)` {#origin-is-union} - **参数** - `origin` (type[Any] | None) - **返回** - bool ## _def_ `origin_is_literal(origin)` {#origin-is-literal} - **说明:** 判断是否是 Literal 类型 - **参数** - `origin` (type[Any] | None) - **返回** - bool ## _def_ `all_literal_values(type_)` {#all-literal-values} - **说明:** 获取 Literal 类型包含的所有值 - **参数** - `type_` (type[Any]) - **返回** - list[Any] ## _def_ `origin_is_annotated(origin)` {#origin-is-annotated} - **说明:** 判断是否是 Annotated 类型 - **参数** - `origin` (type[Any] | None) - **返回** - bool ## _def_ `is_none_type(type_)` {#is-none-type} - **说明:** 判断是否是 None 类型 - **参数** - `type_` (type[Any]) - **返回** - bool ## _def_ `is_type_alias_type(type_)` {#is-type-alias-type} - **参数** - `type_` (type[Any]) - **返回** - bool ## _def_ `evaluate_forwardref(ref, globalns, localns)` {#evaluate-forwardref} - **参数** - `ref` (ForwardRef) - `globalns` (dict[str, Any]) - `localns` (dict[str, Any]) - **返回** - Any ## _class_ `StateFlag()` {#StateFlag} - **参数** auto ## _var_ `T_State` {#T-State} - **类型:** dict[Any, Any] - **说明:** 事件处理状态 State 类型 ## _var_ `T_BotConnectionHook` {#T-BotConnectionHook} - **类型:** \_DependentCallable[Any] - **说明** Bot 连接建立时钩子函数 依赖参数: - DependParam: 子依赖参数 - BotParam: Bot 对象 - DefaultParam: 带有默认值的参数 ## _var_ `T_BotDisconnectionHook` {#T-BotDisconnectionHook} - **类型:** \_DependentCallable[Any] - **说明** Bot 连接断开时钩子函数 依赖参数: - DependParam: 子依赖参数 - BotParam: Bot 对象 - DefaultParam: 带有默认值的参数 ## _var_ `T_CallingAPIHook` {#T-CallingAPIHook} - **类型:** ([Bot](adapters/index.md#Bot), str, dict[str, Any]) -> Awaitable[Any] - **说明:** `bot.call_api` 钩子函数 ## _var_ `T_CalledAPIHook` {#T-CalledAPIHook} - **类型:** ([Bot](adapters/index.md#Bot), Exception | None, str, dict[str, Any], Any) -> Awaitable[Any] - **说明:** `bot.call_api` 后执行的函数,参数分别为 bot, exception, api, data, result ## _var_ `T_EventPreProcessor` {#T-EventPreProcessor} - **类型:** \_DependentCallable[Any] - **说明** 事件预处理函数 EventPreProcessor 类型 依赖参数: - DependParam: 子依赖参数 - BotParam: Bot 对象 - EventParam: Event 对象 - StateParam: State 对象 - DefaultParam: 带有默认值的参数 ## _var_ `T_EventPostProcessor` {#T-EventPostProcessor} - **类型:** \_DependentCallable[Any] - **说明** 事件后处理函数 EventPostProcessor 类型 依赖参数: - DependParam: 子依赖参数 - BotParam: Bot 对象 - EventParam: Event 对象 - StateParam: State 对象 - DefaultParam: 带有默认值的参数 ## _var_ `T_RunPreProcessor` {#T-RunPreProcessor} - **类型:** \_DependentCallable[Any] - **说明** 事件响应器运行前预处理函数 RunPreProcessor 类型 依赖参数: - DependParam: 子依赖参数 - BotParam: Bot 对象 - EventParam: Event 对象 - StateParam: State 对象 - MatcherParam: Matcher 对象 - DefaultParam: 带有默认值的参数 ## _var_ `T_RunPostProcessor` {#T-RunPostProcessor} - **类型:** \_DependentCallable[Any] - **说明** 事件响应器运行后后处理函数 RunPostProcessor 类型 依赖参数: - DependParam: 子依赖参数 - BotParam: Bot 对象 - EventParam: Event 对象 - StateParam: State 对象 - MatcherParam: Matcher 对象 - ExceptionParam: 异常对象(可能为 None) - DefaultParam: 带有默认值的参数 ## _var_ `T_RuleChecker` {#T-RuleChecker} - **类型:** \_DependentCallable[bool] - **说明** RuleChecker 即判断是否响应事件的处理函数。 依赖参数: - DependParam: 子依赖参数 - BotParam: Bot 对象 - EventParam: Event 对象 - StateParam: State 对象 - DefaultParam: 带有默认值的参数 ## _var_ `T_PermissionChecker` {#T-PermissionChecker} - **类型:** \_DependentCallable[bool] - **说明** PermissionChecker 即判断事件是否满足权限的处理函数。 依赖参数: - DependParam: 子依赖参数 - BotParam: Bot 对象 - EventParam: Event 对象 - DefaultParam: 带有默认值的参数 ## _var_ `T_Handler` {#T-Handler} - **类型:** \_DependentCallable[Any] - **说明:** Handler 处理函数。 ## _var_ `T_TypeUpdater` {#T-TypeUpdater} - **类型:** \_DependentCallable[str] - **说明** TypeUpdater 在 Matcher.pause, Matcher.reject 时被运行,用于更新响应的事件类型。 默认会更新为 `message`。 依赖参数: - DependParam: 子依赖参数 - BotParam: Bot 对象 - EventParam: Event 对象 - StateParam: State 对象 - MatcherParam: Matcher 对象 - DefaultParam: 带有默认值的参数 ## _var_ `T_PermissionUpdater` {#T-PermissionUpdater} - **类型:** \_DependentCallable[[Permission](permission.md#Permission)] - **说明** PermissionUpdater 在 Matcher.pause, Matcher.reject 时被运行,用于更新会话对象权限。 默认会更新为当前事件的触发对象。 依赖参数: - DependParam: 子依赖参数 - BotParam: Bot 对象 - EventParam: Event 对象 - StateParam: State 对象 - MatcherParam: Matcher 对象 - DefaultParam: 带有默认值的参数 ## _var_ `T_DependencyCache` {#T-DependencyCache} - **类型:** dict[\_DependentCallable[Any], DependencyCache] - **说明:** 依赖缓存, 用于存储依赖函数的返回值 ================================================ FILE: website/versioned_docs/version-2.4.3/api/utils.md ================================================ --- mdx: format: md sidebar_position: 8 description: nonebot.utils 模块 --- # nonebot.utils 本模块包含了 NoneBot 的一些工具函数 ## _def_ `escape_tag(s)` {#escape-tag} - **说明** 用于记录带颜色日志时转义 `` 类型特殊标签 参考: [loguru color 标签](https://loguru.readthedocs.io/en/stable/api/logger.html#color) - **参数** - `s` (str): 需要转义的字符串 - **返回** - str ## _def_ `deep_update(mapping, *updating_mappings)` {#deep-update} - **说明:** 深度更新合并字典 - **参数** - `mapping` (dict[K, Any]) - `*updating_mappings` (dict[K, Any]) - **返回** - dict[K, Any] ## _def_ `lenient_issubclass(cls, class_or_tuple)` {#lenient-issubclass} - **说明:** 检查 cls 是否是 class_or_tuple 中的一个类型子类并忽略类型错误。 - **参数** - `cls` (Any) - `class_or_tuple` (type[Any] | tuple[type[Any], ...]) - **返回** - bool ## _def_ `generic_check_issubclass(cls, class_or_tuple)` {#generic-check-issubclass} - **说明** 检查 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.List`、`typing.Dict` 等泛型类型, 则会检查其原始类型是否是 class_or_tuple 中一个类型的子类。 - **参数** - `cls` (Any) - `class_or_tuple` (type[Any] | tuple[type[Any], ...]) - **返回** - bool ## _def_ `type_is_complex(type_)` {#type-is-complex} - **说明:** 检查 type\_ 是否是复杂类型 - **参数** - `type_` (type[Any]) - **返回** - bool ## _def_ `is_coroutine_callable(call)` {#is-coroutine-callable} - **说明:** 检查 call 是否是一个 callable 协程函数 - **参数** - `call` ((...) -> Any) - **返回** - bool ## _def_ `is_gen_callable(call)` {#is-gen-callable} - **说明:** 检查 call 是否是一个生成器函数 - **参数** - `call` ((...) -> Any) - **返回** - bool ## _def_ `is_async_gen_callable(call)` {#is-async-gen-callable} - **说明:** 检查 call 是否是一个异步生成器函数 - **参数** - `call` ((...) -> Any) - **返回** - bool ## _def_ `run_sync(call)` {#run-sync} - **说明:** 一个用于包装 sync function 为 async function 的装饰器 - **参数** - `call` ((P) -> R): 被装饰的同步函数 - **返回** - (P) -> Coroutine[None, None, R] ## _def_ `run_sync_ctx_manager(cm)` {#run-sync-ctx-manager} - **说明:** 一个用于包装 sync context manager 为 async context manager 的执行函数 - **参数** - `cm` (AbstractContextManager[T]) - **返回** - AsyncGenerator[T, None] ## _async def_ `run_coro_with_catch(coro, exc, return_on_err=None)` {#run-coro-with-catch} - **说明:** 运行协程并当遇到指定异常时返回指定值。 - **重载** **1.** `(coro, exc, return_on_err=None) -> T | None` - **参数** - `coro` (Coroutine[Any, Any, T]) - `exc` (tuple[type[Exception], ...]) - `return_on_err` (None) - **返回** - T | None **2.** `(coro, exc, return_on_err) -> T | R` - **参数** - `coro` (Coroutine[Any, Any, T]) - `exc` (tuple[type[Exception], ...]) - `return_on_err` (R) - **返回** - T | R - **参数** - `coro`: 要运行的协程 - `exc`: 要捕获的异常 - `return_on_err`: 当发生异常时返回的值 - **返回** 协程的返回值或发生异常时的指定值 ## _async def_ `run_coro_with_shield(coro)` {#run-coro-with-shield} - **说明:** 运行协程并在取消时屏蔽取消异常。 - **参数** - `coro` (Coroutine[Any, Any, T]): 要运行的协程 - **返回** - T: 协程的返回值 ## _def_ `flatten_exception_group(exc_group)` {#flatten-exception-group} - **参数** - `exc_group` (BaseExceptionGroup[E]) - **返回** - Generator[E, None, None] ## _def_ `get_name(obj)` {#get-name} - **说明:** 获取对象的名称 - **参数** - `obj` (Any) - **返回** - str ## _def_ `path_to_module_name(path)` {#path-to-module-name} - **说明:** 转换路径为模块名 - **参数** - `path` (Path) - **返回** - str ## _def_ `resolve_dot_notation(obj_str, default_attr, default_prefix=None)` {#resolve-dot-notation} - **说明:** 解析并导入点分表示法的对象 - **参数** - `obj_str` (str) - `default_attr` (str) - `default_prefix` (str | None) - **返回** - Any ## _class_ `classproperty(func)` {#classproperty} - **说明:** 类属性装饰器 - **参数** - `func` ((Any) -> T) ## _class_ `DataclassEncoder()` {#DataclassEncoder} - **说明:** 可以序列化 [Message](adapters/index.md#Message)(List[Dataclass]) 的 `JSONEncoder` - **参数** auto ### _method_ `default(o)` {#DataclassEncoder-default} - **参数** - `o` - **返回** - untyped ## _def_ `logger_wrapper(logger_name)` {#logger-wrapper} - **说明:** 用于打印 adapter 的日志。 - **参数** - `logger_name` (str): adapter 的名称 - **返回** - untyped: 日志记录函数 日志记录函数的参数: - level: 日志等级 - message: 日志信息 - exception: 异常信息 ================================================ FILE: website/versioned_docs/version-2.4.3/appendices/api-calling.mdx ================================================ --- sidebar_position: 4 description: 使用平台接口,完成更多功能 options: menu: - category: appendices weight: 50 --- # 使用平台接口 import Messenger from "@/components/Messenger"; 在 NoneBot 中,除了使用事件响应器操作发送文本消息外,我们还可以直接通过使用协议适配器提供的方法来使用平台特定的接口,完成发送特殊消息、获取信息等其他平台提供的功能。同时,在部分无法使用事件响应器的情况中,例如[定时任务](../best-practice/scheduler.md),我们也可以使用平台接口来完成需要的功能。 ## 发送平台特殊消息 在之前的章节中,我们介绍了如何向用户发送文本消息以及[如何处理平台消息](../tutorial/message.md),现在我们来向用户发送平台特殊消息。 :::caution 注意 在以下的示例中,我们将使用 `Console` 协议适配器来演示如何发送平台消息。在实际使用中,你需要确保你使用的**消息序列类型**与你所要发送的**平台类型**一致。 ::: ```python {4,7-17} title=weather/__init__.py import inspect from nonebot.adapters.console import MessageSegment @weather.got("location", prompt=MessageSegment.emoji("question") + "请输入地名") async def got_location(location: str = ArgPlainText()): result = await weather.send( MessageSegment.markdown( inspect.cleandoc( f""" # {location} - 今天 ⛅ 多云 20℃~24℃ """ ) ) ) ``` 在上面的示例中,我们使用了 `Console` 协议适配器提供的 `MessageSegment` 类来发送平台特定的消息 `emoji` 和 `markdown`。这两种消息可以显示在终端中,但是无法在其他平台上使用。在事件响应器操作中,我们可以使用 `str`、消息序列、消息段、消息模板四种类型来发送消息,但其中只有 `str` 和[纯文本形式的消息模板类型](../tutorial/message.md#使用消息模板)消息可以在所有平台上使用。 `send` 事件响应器操作实际上是由协议适配器通过调用平台 API 来实现的,通常会将 API 调用的结果作为返回值返回。 ## 调用平台 API 在 NoneBot 中,我们可以通过 `Bot` 对象来调用协议适配器支持的平台 API,来完成更多的功能。 ### 获取 Bot 在调用平台 API 之前,我们首先要获得 Bot 对象。有两种方式可以获得 Bot 对象。 在事件处理流程的上下文中,我们可以直接使用依赖注入 Bot 来获取: ```python {1,4} title=weather/__init__.py from nonebot.adapters import Bot @weather.got("location", prompt="请输入地名") async def got_location(bot: Bot, location: str = ArgPlainText()): ... ``` 依赖注入会确保你获得的 Bot 对象与类型注解的 Bot 类型一致。也就是说,如果你使用的是 Bot 基类,将会允许任何平台的 Bot 对象;如果你使用的是平台特定的 Bot 类型,将会只允许该平台的 Bot 对象,其他类型的 Bot 将会跳过这个事件处理函数。更多详情请参考[事件处理重载](./overload.md)。 在其他情况下,我们可以通过 NoneBot 提供的方法来获取 Bot 对象,这些方法将会在[使用适配器](../advanced/adapter.md#获取-bot-对象)中详细介绍: ```python {4,6} from nonebot import get_bot # 获取当前所有 Bot 中的第一个 bot = get_bot() # 获取指定 ID 的 Bot bot = get_bot("bot_id") ``` ### 调用 API 在获得 Bot 对象后,我们可以通过 Bot 的实例方法来调用平台 API: ```python {2,5} # 通过 bot.api_name(**kwargs) 的方法调用 API result = await bot.get_user_info(user_id=12345678) # 通过 bot.call_api(api_name, **kwargs) 的方法调用 API result = await bot.call_api("get_user_info", user_id=12345678) ``` :::caution 注意 实际可以使用的 API 以及参数取决于平台提供的接口以及协议适配器的实现,请参考协议适配器以及平台文档。 ::: 在了解了如何调用 API 后,我们可以来改进 `weather` 插件,使得消息发送后,调用 `Console` 接口响铃提醒机器人用户: ```python {4,18} title=weather/__init__.py from nonebot.adapters.console import Bot, MessageSegment @weather.got("location", prompt=MessageSegment.emoji("question") + "请输入地名") async def got_location(bot: Bot, location: str = ArgPlainText()): await weather.send( MessageSegment.markdown( inspect.cleandoc( f""" # {location} - 今天 ⛅ 多云 20℃~24℃ """ ) ) ) await bot.bell() ``` ================================================ FILE: website/versioned_docs/version-2.4.3/appendices/config.mdx ================================================ --- sidebar_position: 0 description: 读取用户配置来控制插件行为 options: menu: - category: appendices weight: 10 --- # 配置 import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; 配置是项目中非常重要的一部分,为了方便我们控制机器人的行为,NoneBot 提供了一套配置系统。下面我们将会补充[指南](../quick-start.mdx)中的天气插件,使其能够读取用户配置。在这之前,我们需要先了解一下配置系统,如果你已经了解了 NoneBot 中的配置方法,可以跳转到[编写插件配置](#插件配置)。 NoneBot 使用 [`pydantic`](https://docs.pydantic.dev/) 以及 [`python-dotenv`](https://saurabh-kumar.com/python-dotenv/) 来读取 dotenv 配置文件以及环境变量,从而控制机器人行为。配置文件需要符合 dotenv 格式,复杂数据类型需使用 JSON 格式或 [pydantic 支持格式](https://docs.pydantic.dev/usage/types/)填写。 NoneBot 内置的配置项列表及含义可以在[内置配置项](#内置配置项)中查看。 :::caution 注意 NoneBot 自 2.2.0 起兼容了 Pydantic v1 与 v2 版本,以下文档中 Pydantic 相关示例均采用 v2 版本用法。 如果在使用商店或其他第三方插件的过程中遇到 Pydantic 相关警告或报错,例如: ```python pydantic_core._pydantic_core.ValidationError: 1 validation error for Config Input should be a valid dictionary or instance of Config [type=model_type, input_value=Config(...), input_type=Config] ``` 请考虑降级 Pydantic 至 v1 版本: ```bash pip install --force-reinstall 'pydantic~=1.10' ``` ::: ## 配置项的加载 在 NoneBot 中,我们可以把配置途径分为 **直接传入**、**系统环境变量**、**dotenv 配置文件** 三种,其加载优先级依次由高到低。 ### 直接传入 在 NoneBot 初始化的过程中,可以通过 `nonebot.init()` 传入任意合法的 Python 变量,也可以在初始化完成后直接赋值。 通常,在初始化前的传参会在机器人的入口文件(如 `bot.py`)中进行,而初始化后的赋值可以在任何地方进行。 ```python {4,8,9} title=bot.py import nonebot # 初始化时 nonebot.init(custom_config1="config on init") # 初始化后 config = nonebot.get_driver().config config.custom_config1 = "changed after init" config.custom_config2 = "new config after init" ``` ### 系统环境变量 在 dotenv 配置文件中定义的配置项,也会在环境变量中进行寻找。如果在环境变量中发现同名配置项(大小写不敏感),将会覆盖 dotenv 中所填值。 例如,在 dotenv 配置文件中存在配置项 `custom_config`: ```dotenv CUSTOM_CONFIG=config in dotenv ``` 同时,设置环境变量: ```bash # windows cmd set CUSTOM_CONFIG 'config in environment variables' # windows powershell $Env:CUSTOM_CONFIG='config in environment variables' # linux/macOS export CUSTOM_CONFIG='config in environment variables' ``` 那最终 NoneBot 所读取的内容为环境变量中的内容,即 `config in environment variables`。 :::caution 注意 NoneBot 不会自发读取未被定义的配置项的环境变量,如果需要读取某一环境变量需要在 dotenv 配置文件中进行声明。 ::: ### dotenv 配置文件 dotenv 是一种便捷的跨平台配置通用模式,也是我们推荐的配置方式。 NoneBot 在启动时将会从系统环境变量或者 `.env` 文件中寻找配置项 `ENVIRONMENT` (大小写不敏感),默认值为 `prod`。这将决定 NoneBot 后续进一步加载环境配置的文件路径 `.env.{ENVIRONMENT}`。 #### 配置项解析 dotenv 文件中的配置值使用 JSON 进行解析。如果配置项值无法被解析,将作为**字符串**处理。例如: ```dotenv STRING_CONFIG=some string LIST_CONFIG=[1, 2, 3] DICT_CONFIG={"key": "value"} MULTILINE_CONFIG=' [ { "item_key": "item_value" } ] ' EMPTY_CONFIG= NULL_CONFIG ``` 将被解析为: ```python dotenv_config = { "string_config": "some string", "list_config": [1, 2, 3], "dict_config": {"key": "value"}, "multiline_config": [{"item_key": "item_value"}], "empty_config": "", "null_config": None } ``` 特别的,NoneBot 支持使用 `env_nested_delimiter` 配置嵌套字典,在层与层之间使用 `__` 分隔即可: ```dotenv DICT={"k1": "v1", "k2": null} DICT__K2=v2 DICT__K3=v3 DICT__INNER__K4=v4 ``` 将被解析为: ```python dotenv_config = { "dict": { "k1": "v1", "k2": "v2", "k3": "v3", "inner": { "k4": "v4" } } } ``` #### .env 文件 `.env` 文件是基础配置文件,该文件中的配置项在不同环境下都会被加载,但会被 `.env.{ENVIRONMENT}` 文件中的配置所**覆盖**。 我们可以在 `.env` 文件中写入当前的环境信息: ```dotenv ENVIRONMENT=dev COMMON_CONFIG=common config # 这个配置项在任何环境中都会被加载 ``` 这样,我们在启动 NoneBot 时就会从 `.env.dev` 文件中加载剩余配置项。 :::tip 提示 在生产环境中,可以通过设置环境变量 `ENVIRONMENT=prod` 来确保 NoneBot 读取正确的环境配置。 ::: #### .env.\{ENVIRONMENT\} 文件 `.env.{ENVIRONMENT}` 文件类似于预设,可以让我们在多套不同的配置方案中灵活切换,默认 NoneBot 会读取 `.env.prod` 配置。如果你使用了 `nb-cli` 创建 `simple` 项目,那么将含有两套预设配置:`.env.dev` 和 `.env.prod`。 在 NoneBot 初始化时,可以指定加载某个环境配置文件: ```python nonebot.init(_env_file=".env.dev") ``` 这将忽略在 `.env` 文件或环境变量中指定的 `ENVIRONMENT` 配置项。 ## 读取全局配置项 NoneBot 的全局配置对象可以通过 `driver` 获取,如: ```python import nonebot config = nonebot.get_driver().config ``` 如果我们需要获取某个配置项,可以直接通过 `config` 对象的属性访问: ```python superusers = config.superusers ``` 如果配置项不存在,将会抛出异常。 ## 插件配置 在一个涉及大量配置项的项目中,通过直接读取全局配置项的方式显然并不高效。同时,由于额外的全局配置项没有预先定义,开发时编辑器将无法提示字段与类型,并且运行时没有对配置项直接进行合法性检查。那么就需要一种方式来规范定义插件配置项。 在 NoneBot 中,我们使用强大高效的 `pydantic` 来定义配置模型,这个模型可以被用于配置的读取和类型检查等。例如在 `weather` 插件目录中新建 `config.py` 来定义一个模型: ```python title=weather/config.py from pydantic import BaseModel, field_validator class Config(BaseModel): weather_api_key: str weather_command_priority: int = 10 weather_plugin_enabled: bool = True @field_validator("weather_command_priority") @classmethod def check_priority(cls, v: int) -> int: if v >= 1: return v raise ValueError("weather command priority must greater than 1") ``` 在 `config.py` 中,我们定义了一个 `Config` 类,它继承自 `pydantic.BaseModel`,并定义了一些配置项。在 `Config` 类中,我们还定义了一个 `check_priority` 方法,它用于检查 `weather_command_priority` 配置项的合法性。更多关于 `pydantic` 的编写方式,可以参考 [pydantic 官方文档](https://docs.pydantic.dev/)。 在定义好配置模型后,我们可以在插件加载时通过配置模型获取插件配置: ```python {5,11} title=weather/__init__.py from nonebot import get_plugin_config from .config import Config plugin_config = get_plugin_config(Config) weather = on_command( "天气", rule=to_me(), aliases={"weather", "查天气"}, priority=plugin_config.weather_command_priority, block=True, ) ``` 然后,我们便可以从 `plugin_config` 中读取配置了,例如 `plugin_config.weather_api_key`。 这种方式可以简洁、高效地读取配置项,同时也可以设置默认值或者在运行时对配置项进行合法性检查,防止由于配置项导致的插件出错等情况出现。 :::tip 提示 发布插件应该为自身的事件响应器提供可配置的优先级,以便插件使用者可以自定义多个插件间的响应顺序。 ::: 由于插件配置项是从全局配置中读取的,通常我们需要在配置项名称前面添加前缀名,以防止配置项冲突。例如在上方的示例中,我们就添加了配置项前缀 `weather_`。但是这样会导致在使用配置项时过长的变量名,因此我们可以使用 `pydantic` 的 `alias` 或者通过配置 scope 来简化配置项名称。这里我们以 scope 配置为例: ```python title=weather/config.py from pydantic import BaseModel class ScopedConfig(BaseModel): api_key: str command_priority: int = 10 plugin_enabled: bool = True class Config(BaseModel): weather: ScopedConfig ``` ```python title=weather/__init__.py from nonebot import get_plugin_config from .config import Config plugin_config = get_plugin_config(Config).weather ``` 这样我们就可以省略插件配置项名称中的前缀 `weather_` 了。但需要注意的是,如果我们使用了 scope 配置,那么在配置文件中也需要使用 [`env_nested_delimiter` 格式](#配置项解析),例如: ```dotenv WEATHER__API_KEY=123456 WEATHER__COMMAND_PRIORITY=10 ``` ## 内置配置项 配置项 API 文档可以前往 [Config 类](../api/config.md#Config)查看。 ### Driver - **类型**: `str` - **默认值**: `"~fastapi"` NoneBot 运行所使用的驱动器。具体配置方法可以参考[安装驱动器](../tutorial/store.mdx#安装驱动器)和[选择驱动器](../advanced/driver.md)。 ```dotenv DRIVER=~fastapi+~httpx+~websockets ``` ```bash # windows cmd set DRIVER '~fastapi+~httpx+~websockets' # windows powershell $Env:DRIVER='~fastapi+~httpx+~websockets' # linux/macOS export DRIVER='~fastapi+~httpx+~websockets' ``` ```python title=bot.py import nonebot nonebot.init(driver="~fastapi+~httpx+~websockets") ``` ### Host - **类型**: `IPvAnyAddress` - **默认值**: `127.0.0.1` 当 NoneBot 作为服务端时,监听的 IP / 主机名。 ```dotenv HOST=127.0.0.1 ``` ```bash # windows cmd set HOST '127.0.0.1' # windows powershell $Env:HOST='127.0.0.1' # linux/macOS export HOST='127.0.0.1' ``` ```python title=bot.py import nonebot nonebot.init(host="127.0.0.1") ``` ### Port - **类型**: `int` (1 ~ 65535) - **默认值**: `8080` 当 NoneBot 作为服务端时,监听的端口。 ```dotenv PORT=8080 ``` ```bash # windows cmd set PORT '8080' # windows powershell $Env:PORT='8080' # linux/macOS export PORT='8080' ``` ```python title=bot.py import nonebot nonebot.init(port=8080) ``` ### Log Level - **类型**: `int | str` - **默认值**: `INFO` NoneBot 日志输出等级,可以为 `int` 类型等级或等级名称。具体等级对照表参考 [loguru 日志等级](https://loguru.readthedocs.io/en/stable/api/logger.html#levels)。 :::tip 提示 日志等级名称应为大写,如 `INFO`。 ::: ```dotenv LOG_LEVEL=DEBUG ``` ```bash # windows cmd set LOG_LEVEL 'DEBUG' # windows powershell $Env:LOG_LEVEL='DEBUG' # linux/macOS export LOG_LEVEL='DEBUG' ``` ```python title=bot.py import nonebot nonebot.init(log_level="DEBUG") ``` ### API Timeout - **类型**: `float | None` - **默认值**: `30.0` 调用平台接口的超时时间,单位为秒。`None` 表示不设置超时时间。 ```dotenv API_TIMEOUT=10.0 ``` ```bash # windows cmd set API_TIMEOUT '10.0' # windows powershell $Env:API_TIMEOUT='10.0' # linux/macOS export API_TIMEOUT='10.0' ``` ```python title=bot.py import nonebot nonebot.init(api_timeout=10.0) ``` ### SuperUsers - **类型**: `set[str]` - **默认值**: `set()` 机器人超级用户,可以使用权限 [`SUPERUSER`](../api/permission.md#SUPERUSER)。 ```dotenv SUPERUSERS=["123123123"] ``` ```bash # windows cmd set SUPERUSERS '["123123123"]' # windows powershell $Env:SUPERUSERS='["123123123"]' # linux/macOS export SUPERUSERS='["123123123"]' ``` ```python title=bot.py import nonebot nonebot.init(superusers={"123123123"}) ``` ### Nickname - **类型**: `set[str]` - **默认值**: `set()` 机器人昵称,通常协议适配器会根据用户是否 @bot 或者是否以机器人昵称开头来判断是否是向机器人发送的消息。 ```dotenv NICKNAME=["bot"] ``` ```bash # windows cmd set NICKNAME '["bot"]' # windows powershell $Env:NICKNAME='["bot"]' # linux/macOS export NICKNAME='["bot"]' ``` ```python title=bot.py import nonebot nonebot.init(nickname={"bot"}) ``` ### Command Start 和 Command Separator - **类型**: `set[str]` - **默认值**: - Command Start: `{"/"}` - Command Separator: `{"."}` 命令消息的起始符和分隔符。用于 [`command`](../advanced/matcher.md#command) 规则。 ```dotenv COMMAND_START=["/", ""] COMMAND_SEP=[".", " "] ``` ```bash # windows cmd set COMMAND_START '["/", ""]' set COMMAND_SEP '[".", " "]' # windows powershell $Env:COMMAND_START='["/", ""]' $Env:COMMAND_SEP='[".", " "]' # linux/macOS export COMMAND_START='["/", ""]' export COMMAND_SEP='[".", " "]' ``` ```python title=bot.py import nonebot nonebot.init(command_start={"/", ""}, command_sep={".", " "}) ``` ### Session Expire Timeout - **类型**: `timedelta` - **默认值**: `timedelta(minutes=2)` 用户会话超时时间,配置格式参考 [Datetime Types](https://docs.pydantic.dev/latest/api/standard_library_types/#datetimetimedelta)。 ```dotenv SESSION_EXPIRE_TIMEOUT=00:02:00 ``` ```bash # windows cmd set SESSION_EXPIRE_TIMEOUT '00:02:00' # windows powershell $Env:SESSION_EXPIRE_TIMEOUT='00:02:00' # linux/macOS export SESSION_EXPIRE_TIMEOUT='00:02:00' ``` ```python title=bot.py import nonebot nonebot.init(session_expire_timeout=120) ``` ================================================ FILE: website/versioned_docs/version-2.4.3/appendices/log.md ================================================ --- sidebar_position: 6 description: 记录与控制日志 options: menu: - category: appendices weight: 70 --- # 日志 无论是在开发还是在生产环境中,日志都是一个重要的功能,可以帮助我们了解运行状况、排查问题等。虽然我们可以使用 `print` 来将需要的信息输出到控制台,但是这种方式难以控制,而且不利于日志的归档、分析等。NoneBot 使用优秀的 [Loguru](https://loguru.readthedocs.io/) 库来进行日志记录。 ## 记录日志 我们可以从 NoneBot 中导入 `logger` 对象,然后使用 `logger` 对象的方法来记录日志。 ```python from nonebot import logger logger.trace("This is a trace message") logger.debug("This is a debug message") logger.info("This is an info message") logger.success("This is a success message") logger.warning("This is a warning message") logger.error("This is an error message") logger.critical("This is a critical message") ``` 我们仅需一行代码即可记录对应级别的日志。日志可以通过配置 [`LOG_LEVEL` 配置项](./config.mdx#log-level)来过滤输出等级,控制台中仅会输出大于等于 `LOG_LEVEL` 的日志。默认的 `LOG_LEVEL` 为 `INFO`,即只会输出 `INFO`、`SUCCESS`、`WARNING`、`ERROR`、`CRITICAL` 级别的日志。 如果需要记录 `Exception traceback` 日志,可以向 `logger` 添加 `exception` 选项: ```python {4} try: 1 / 0 except ZeroDivisionError: logger.opt(exception=True).error("ZeroDivisionError") ``` 如果需要输出彩色日志,可以向 `logger` 添加 `colors` 选项: ```python logger.opt(colors=True).warning("We got a BIG problem") ``` 更多日志记录方法请参考 [Loguru 文档](https://loguru.readthedocs.io/)。 ## 自定义日志输出 NoneBot 在启动时会添加一个默认的日志处理器,该处理器会将日志输出到**stdout**,并且根据 `LOG_LEVEL` 配置项过滤日志等级。 默认的日志格式为: ```text {time:MM-DD HH:mm:ss} [{level}] {name} | {message} ``` 我们可以从 `nonebot.log` 模块导入以使用 NoneBot 的默认格式和过滤器: ```python from nonebot.log import default_format, default_filter ``` 如果需要自定义日志格式,我们需要移除 NoneBot 默认的日志处理器并添加新的日志处理器。例如,在机器人入口文件中 `nonebot.init` 之前添加以下内容: ```python title=bot.py from nonebot.log import logger_id # 移除 NoneBot 默认的日志处理器 logger.remove(logger_id) # 添加新的日志处理器 logger.add( sys.stdout, level=0, diagnose=True, format="{time:MM-DD HH:mm:ss} [{level}] {name} | {message}", filter=default_filter ) ``` 如果想要输出日志到文件,我们可以使用 `logger.add` 方法添加文件处理器: ```python title=bot.py logger.add("error.log", level="ERROR", format=default_format, rotation="1 week") ``` 更多日志处理器的使用方法请参考 [Loguru 文档](https://loguru.readthedocs.io/)。 ## 重定向 logging 日志 `logging` 是 Python 标准库中的日志模块,NoneBot 提供了一个 logging handler 用于将 `logging` 日志重定向到 `loguru` 处理。 ```python from nonebot.log import LoguruHandler # root logger 添加 LoguruHandler logging.basicConfig(handlers=[LoguruHandler()]) # 或者为其他 logging.Logger 添加 LoguruHandler logger.addHandler(LoguruHandler()) ``` ================================================ FILE: website/versioned_docs/version-2.4.3/appendices/overload.md ================================================ --- sidebar_position: 7 description: 根据事件类型进行不同的处理 options: menu: - category: appendices weight: 80 --- # 事件类型与重载 在之前的示例中,我们已经了解了如何[获取事件信息](../tutorial/event-data.mdx)以及[使用平台接口](./api-calling.mdx)。但是,事件信息通常不仅仅包含消息这一个内容,还有其他平台提供的信息,例如消息发送时间、消息发送者等等。同时,在使用平台接口时,我们需要确保使用的**平台接口**与所要发送的**平台类型**一致,对不同类型的事件需要做出不同的处理。在本章节中,我们将介绍如何获取事件更多的信息以及根据事件类型进行不同的处理。 ## 事件类型 在 NoneBot 中,事件均是 `nonebot.adapters.Event` 基类的子类型,基类对一些必要的属性进行了抽象,子类型则根据不同的平台进行了实现。在[自定义权限](./permission.mdx#自定义权限)一节中,我们就使用了 `Event` 的抽象方法 `get_user_id` 来获取事件发送者 ID,这个方法由协议适配器进行了实现,返回机器人用户对应的平台 ID。更多的基类抽象方法可以在[使用适配器](../advanced/adapter.md#获取事件通用信息)中查看。 既然事件是基类的子类型,我们实际可以获得的信息通常多于基类抽象方法所提供的。如果我们不满足于基类能获得的信息,我们可以小小的修改一下事件处理函数的事件参数类型注解,使其变为子类型,这样我们就可以通过协议适配器定义的子类型来获取更多的信息。我们以 `Console` 协议适配器为例: ```python {4} title=weather/__init__.py from nonebot.adapters.console import MessageEvent @weather.got("location", prompt="请输入地名") async def got_location(event: MessageEvent, location: str = ArgPlainText()): await weather.finish(f"{event.time.strftime('%Y-%m-%d')} {location} 的天气是...") ``` 在上面的代码中,我们获取了 `Console` 协议适配器的消息事件提供的发送时间 `time` 属性。 :::caution 注意 如果**基类**就能满足你的需求,那么就**不要修改**事件参数类型注解,这样可以使你的代码更加**通用**,可以在更多平台上运行。如何根据不同平台事件类型进行不同的处理,我们将在[重载](#重载)一节中介绍。 ::: ## 重载 我们在编写机器人时,常常会遇到这样一个问题:如何对私聊和群聊消息进行不同的处理?如何对不同平台的事件进行不同的处理?针对这些问题,NoneBot 提供了一个便捷而高效的解决方案 ── 重载。简单来说,依赖函数会根据其参数的类型注解来决定是否执行,忽略不符合其参数类型注解的情况。这样,我们就可以通过修改事件参数类型注解来实现对不同事件的处理,或者修改 `Bot` 参数类型注解来实现使用不同平台的接口。我们以 `OneBot` 协议适配器为例: ```python {4,8} from nonebot.adapters.onebot.v11 import PrivateMessageEvent, GroupMessageEvent @matcher.handle() async def handle_private(event: PrivateMessageEvent): await matcher.finish("私聊消息") @matcher.handle() async def handle_group(event: GroupMessageEvent): await matcher.finish("群聊消息") ``` 这样,机器人用户就会在私聊和群聊中分别收到不同的回复。同样的,我们也可以通过修改 `Bot` 参数类型注解来实现使用不同平台的接口: ```python from nonebot.adapters.console import Bot as ConsoleBot from nonebot.adapters.onebot.v11 import Bot as OneBot @matcher.handle() async def handle_console(bot: ConsoleBot): await bot.bell() @matcher.handle() async def handle_onebot(bot: OneBot): await bot.send_group_message(group_id=123123, message="OneBot") ``` :::caution 注意 重载机制对所有的参数类型注解都有效,因此,依赖注入也可以使用这个特性来对不同的返回值进行处理。 但 Bot、Event 和 Matcher 三者的参数类型注解具有最高检查优先级,如果三者任一类型注解不匹配,那么其他依赖注入将不会执行(如:`Depends`)。 ::: :::tip 提示 如何更好地编写一个跨平台的插件,我们将在[最佳实践](../best-practice/multi-adapter.mdx)中介绍。 ::: ================================================ FILE: website/versioned_docs/version-2.4.3/appendices/permission.mdx ================================================ --- sidebar_position: 5 description: 控制事件响应器的权限 options: menu: - category: appendices weight: 60 --- # 权限控制 import Messenger from "@site/src/components/Messenger"; **权限控制**是机器人在实际应用中需要解决的重点问题之一,NoneBot 提供了灵活的权限控制机制 —— `Permission`。 类似于响应规则 `Rule`,`Permission` 是由非负整数个 `PermissionChecker` 所共同组成的**用于筛选事件**的对象。但需要特别说明的是,权限和响应规则有如下区别: 1. 权限检查**先于**响应规则检查 2. `Permission` 只需**其中一个** `PermissionChecker` 返回 `True` 时就会检查通过 3. 权限检查进行时,上下文中并不存在会话状态 `state` 4. `Rule` 仅在**初次触发**事件响应器时进行检查,在余下的会话中并不会限制事件;而 `Permission` 会**持续生效**,在连续对话中一直对事件主体加以限制。 ## 基础使用 通常情况下,`Permission` 更侧重于对于**触发事件的机器人用户**的筛选,例如由 NoneBot 自身提供的 `SUPERUSER` 权限,便是筛选出会话发起者是否为超级用户。它可以对输入的用户进行鉴别,如果符合要求则会被认为通过并返回 `True`,反之则返回 `False`。 简单来说,`Permission` 是一个用于筛选出符合要求的用户的机制,可以通过 `Permission` 精确的控制响应对象的覆盖范围,从而拒绝掉我们所不希望的事件。 例如,我们可以在 `weather` 插件中添加一个超级用户可用的指令: ```python {3,9} title=weather/__init__.py from typing import Tuple from nonebot.params import Command from nonebot.permission import SUPERUSER manage = on_command( ("天气", "启用"), rule=to_me(), aliases={("天气", "禁用")}, permission=SUPERUSER, ) @manage.handle() async def control(cmd: Tuple[str, str] = Command()): _, action = cmd if action == "启用": plugin_config.weather_plugin_enabled = True elif action == "禁用": plugin_config.weather_plugin_enabled = False await manage.finish(f"天气插件已{action}") ``` 如上方示例所示,在注册事件响应器时,我们设置了 `permission` 参数,那么这个事件处理器在触发事件前的检查阶段会对用户身份进行验证,如果不符合我们设置的条件(此处即为**超级用户**)则不会响应。此时,我们向机器人发送 `/天气.禁用` 指令,机器人不会有任何响应,因为我们还不是机器人的超级管理员。我们在 dotenv 文件中设置了 `SUPERUSERS` 配置项之后,机器人就会响应我们的指令了。 ```dotenv title=.env SUPERUSERS=["console_user"] ``` ## 自定义权限 与事件响应规则类似,`PermissionChecker` 也是一个返回值为 `bool` 类型的依赖函数,即 `PermissionChecker` 支持依赖注入。例如,我们可以限制用户的指令调用次数: ```python title=weather/__init__.py from nonebot.adapters import Event fake_db: Dict[str, int] = {} async def limit_permission(event: Event): count = fake_db.setdefault(event.get_user_id(), 100) if count > 0: fake_db[event.get_user_id()] -= 1 return True return False weather = on_command("天气", permission=limit_permission) ``` ## 权限组合 权限之间可以通过 `|` 运算符进行组合,使得任意一个权限检查返回 `True` 时通过。例如: ```python {4-6} perm1 = Permission(foo_checker) perm2 = Permission(bar_checker) perm = perm1 | perm2 perm = perm1 | bar_checker perm = foo_checker | perm2 ``` 同样的,我们也无需担心组合了一个 `None` 值,`Permission` 会自动忽略 `None` 值。 ```python assert (perm | None) is perm ``` ## 主动使用权限 除了在事件响应器中使用权限外,我们也可以主动使用权限来判断事件是否符合条件。例如: ```python {3} perm = Permission(some_checker) result: bool = await perm(bot, event) ``` 我们只需要传入 `Bot` 实例、事件,`Permission` 会并发调用所有 `PermissionChecker` 进行检查,并返回结果。 ================================================ FILE: website/versioned_docs/version-2.4.3/appendices/rule.md ================================================ --- sidebar_position: 1 description: 自定义响应规则 options: menu: - category: appendices weight: 20 --- # 响应规则 机器人在实际应用中,往往会接收到多种多样的事件类型,NoneBot 通过响应规则来控制事件的处理。 在[指南](../tutorial/matcher.md#为事件响应器添加参数)中,我们为 `weather` 命令添加了一个 `rule=to_me()` 参数,这个参数就是一个响应规则,确保只有在私聊或者 `@bot` 时才会响应。 响应规则是一个 `Rule` 对象,它由一系列的 `RuleChecker` 函数组成,每个 `RuleChecker` 函数都会检查事件是否符合条件,如果所有的检查都通过,则事件会被处理。 ## RuleChecker `RuleChecker` 是一个返回值为 `bool` 类型的依赖函数,即 `RuleChecker` 支持依赖注入。我们可以根据上一节中添加的[配置项](./config.mdx#插件配置),在 `weather` 插件目录中编写一个响应规则: ```python {7,8} title=weather/__init__.py from nonebot import get_plugin_config from .config import Config plugin_config = get_plugin_config(Config) async def is_enable() -> bool: return plugin_config.weather_plugin_enabled weather = on_command("天气", rule=is_enable) ``` 在上面的代码中,我们定义了一个函数 `is_enable`,它会检查配置项 `weather_plugin_enabled` 是否为 `True`。这个函数 `is_enable` 即为一个 `RuleChecker`。 ## Rule `Rule` 是若干个 `RuleChecker` 的集合,它会并发调用每个 `RuleChecker`,只有当所有 `RuleChecker` 检查通过时匹配成功。例如:我们可以组合两个 `RuleChecker`,一个用于检查插件是否启用,一个用于检查用户是否在黑名单中: ```python {10} from nonebot.rule import Rule from nonebot.adapters import Event async def is_enable() -> bool: return plugin_config.weather_plugin_enabled async def is_blacklisted(event: Event) -> bool: return event.get_user_id() not in BLACKLIST rule = Rule(is_enable, is_blacklisted) weather = on_command("天气", rule=rule) ``` ## 合并响应规则 在定义响应规则时,我们可以将规则进行细分,来更好地复用规则。而在使用时,我们需要合并多个规则。除了使用 `Rule` 对象来组合多个 `RuleChecker` 外,我们还可以对 `Rule` 对象进行合并。在原 `weather` 插件中,我们可以将 `rule=to_me()` 与 `rule=is_enable` 使用 `&` 运算符合并: ```python {13} title=weather/__init__.py from nonebot.rule import to_me from nonebot import get_plugin_config from .config import Config plugin_config = get_plugin_config(Config) async def is_enable() -> bool: return plugin_config.weather_plugin_enabled weather = on_command( "天气", rule=to_me() & is_enable, aliases={"weather", "查天气"}, priority=plugin_config.weather_command_priority, block=True, ) ``` 这样,`weather` 命令就只会在插件启用且在私聊或者 `@bot` 时才会响应。 合并响应规则可以有多种形式,例如: ```python {4-6} rule1 = Rule(foo_checker) rule2 = Rule(bar_checker) rule = rule1 & rule2 rule = rule1 & bar_checker rule = foo_checker & rule2 ``` 同时,我们也无需担心合并了一个 `None` 值,`Rule` 会忽略 `None` 值。 ```python assert (rule & None) is rule ``` ## 主动使用响应规则 除了在事件响应器中使用响应规则外,我们也可以主动使用响应规则来判断事件是否符合条件。例如: ```python {3} rule = Rule(some_checker) result: bool = await rule(bot, event, state) ``` 我们只需要传入 `Bot` 对象、事件和会话状态,`Rule` 会并发调用所有 `RuleChecker` 进行检查,并返回结果。 ## 内置响应规则 NoneBot 内置了一些常用的响应规则,可以直接通过事件响应器辅助函数或者自行合并其他规则使用。内置响应规则列表可以参考[事件响应器进阶](../advanced/matcher.md) ================================================ FILE: website/versioned_docs/version-2.4.3/appendices/session-control.mdx ================================================ --- sidebar_position: 2 description: 更灵活的会话控制 options: menu: - category: appendices weight: 30 --- # 会话控制 import Messenger from "@site/src/components/Messenger"; 在[指南](../tutorial/event-data.mdx#使用依赖注入)的 `weather` 插件中,我们使用依赖注入获取了机器人用户发送的地名参数,并根据地名参数进行相应的回复。但是,一问一答的对话模式仅仅适用于简单的对话场景,如果我们想要实现更复杂的对话模式,就需要使用会话控制。 ## 询问并获取用户输入 在 `weather` 插件中,我们对于用户未输入地名参数的情况直接回复了 `请输入地名` 并结束了事件流程。但是,这样用户体验并不好,需要重新输入指令和地名参数才能获取天气回复。我们现在来实现询问并获取用户地名参数的功能。 ### 询问用户 我们可以使用事件响应器操作中的 `got` 装饰器来表示当前事件处理流程需要询问并获取用户输入的消息: ```python {6} title=weather/__init__.py @weather.handle() async def handle_function(args: Message = CommandArg()): if location := args.extract_plain_text(): await weather.finish(f"今天{location}的天气是...") @weather.got("location", prompt="请输入地名") async def got_location(): ... ``` 在上面的代码中,我们使用 `got` 事件响应器操作来向用户发送 `prompt` 消息,并等待用户的回复。用户的回复消息将会被作为 `location` 参数存储于事件响应器状态中。 :::tip 提示 事件处理函数根据定义的顺序依次执行。 ::: ### 获取用户输入 在询问以及用户回复之后,我们就可以获取到我们需要的 `location` 参数了。我们使用 `ArgPlainText` 依赖注入来获取参数纯文本信息: ```python {9} title=weather/__init__.py from nonebot.params import ArgPlainText @weather.handle() async def handle_function(args: Message = CommandArg()): if location := args.extract_plain_text(): await weather.finish(f"今天{location}的天气是...") @weather.got("location", prompt="请输入地名") async def got_location(location: str = ArgPlainText()): await weather.finish(f"今天{location}的天气是...") ``` 在上面的代码中,我们在 `got_location` 函数中定义了一个依赖注入参数 `location`,他的值将会是用户回复的消息纯文本信息。获取到用户输入的地名参数后,我们就可以进行天气查询并回复了。 :::tip 提示 如果想要获取用户回复的消息对象 `Message` ,可以使用 `Arg` 依赖注入。 ::: ### 跳过询问 在上面的代码中,如果用户在输入天气指令时,同时提供了地名参数,我们直接回复了天气信息,这部分的逻辑是和询问用户地名参数之后的逻辑一致的。如果在复杂的业务场景下,我们希望这部分代码应该复用以减少代码冗余。我们可以使用事件响应器操作中的 `set_arg` 来主动设置一个参数: ```python {4,6} title=weather/__init__.py from nonebot.matcher import Matcher @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()): await weather.finish(f"今天{location}的天气是...") ``` 请注意,设置参数需要使用依赖注入来获取 `Matcher` 实例以确保上下文正确,且参数值应为 `Message` 对象。 在 `location` 参数被设置之后,`got` 事件响应器操作将不再会询问并等待用户的回复,而是直接进入 `got_location` 函数。 ## 请求重新输入 在实际的业务场景中,用户的输入很有可能并非是我们所期望的,而结束事件处理流程让用户重新发送指令也不是一个好的体验。这时我们可以使用 `reject` 事件响应器操作来请求用户重新输入: ```python {8,9} title=weather/__init__.py @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}的天气是...") ``` 在上面的代码中,我们在 `got_location` 函数中判断用户输入的地名是否在支持的城市列表中,如果不在,则使用 `reject` 事件响应器操作。操作将会向用户发送 `reject` 参数中的消息,并等待用户回复后,重新执行 `got_location` 函数。通过 `got` 和 `reject` 事件响应器操作,我们实现了类似于**循环**的执行方式。 `reject` 事件响应器操作与 `finish` 类似,NoneBot 会在向机器人用户发送消息内容后抛出 `RejectedException` 异常来暂停事件响应流程以等待用户输入。也就是说,在 `reject` 被执行后,后续的程序同样是不会被执行的。 ## 更多事件响应器操作 在之前的章节中,我们已经大致了解了五个事件响应器操作:`handle`、`got`、`finish`、`send` 和 `reject`。现在我们来完整地介绍一下这些操作。 事件响应器操作可以分为两大类:**交互操作**和**流程控制操作**。我们可以通过交互操作来与用户进行交互,而流程控制操作则可以用来控制事件处理流程的执行。 :::tip 提示 事件处理流程按照事件处理函数添加顺序执行,已经结束的事件处理函数不可能被恢复执行。 ::: ### handle `handle` 事件响应器操作是一个装饰器,用于向事件处理流程添加一个事件处理函数。 ```python @matcher.handle() async def handle_func(): ... ``` `handle` 装饰器支持嵌套操作,即一个事件处理函数可以被添加多次: ```python @matcher.handle() @matcher.handle() async def handle_func(): # 这个函数会被执行两次 ... ``` ### got `got` 事件响应器操作也是一个装饰器,它会在当前装饰的事件处理函数运行之前,中断当前事件处理流程,等待接收一个新的事件。它可以通过 `prompt` 参数来向用户发送询问消息,然后等待用户的回复消息,贴近对话形式会话。 `got` 装饰器接受一个参数 `key` 和一个可选参数 `prompt`。当会话状态中不存在 `key` 对应的消息时,会向用户发送 `prompt` 参数的消息,并等待用户回复。`prompt` 参数的类型和 [`send`](#send) 事件响应器操作的参数类型一致。 在事件处理函数中,可以通过依赖注入的方式来获取接收到的消息,参考:[`Arg`](../advanced/dependency.mdx#arg)、[`ArgStr`](../advanced/dependency.mdx#argstr)、[`ArgPlainText`](../advanced/dependency.mdx#argplaintext)。 ```python @matcher.got("key", prompt="请输入...") async def got_func(key: Message = Arg()): ... ``` `got` 装饰器支持与 `got` 和 `receive` 装饰器嵌套操作,即一个事件处理函数可以在接收多个事件或消息后执行: ```python @matcher.got("key1", prompt="请输入key1...") @matcher.got("key2", prompt="请输入key2...") @matcher.receive("key3") async def got_func(key1: Message = Arg(), key2: Message = Arg(), key3: Event = Received("key3")): ... ``` ### receive `receive` 事件响应器操作也是一个装饰器,它会在当前装饰的事件处理函数运行之前,中断当前事件处理流程,等待接收一个新的事件。与 `got` 不同的是,`receive` 不会向用户发送询问消息,并且等待一个用户事件。可以接收的事件类型取决于[会话更新](../advanced/session-updating.md)。 `receive` 装饰器接受一个可选参数 id,用于标识当前需要接收的事件,如果不指定,则默认为空 `""`。 在事件处理函数中,可以通过依赖注入的方式来获取接收到的事件,参考:[`Received`](../advanced/dependency.mdx#received)、[`LastReceived`](../advanced/dependency.mdx#lastreceived)。 ```python @matcher.receive("id") async def receive_func(event: Event = Received("id")): ... ``` `receive` 装饰器支持与 `got` 和 `receive` 装饰器嵌套操作,即一个事件处理函数可以在接收多个事件或消息后执行: ```python @matcher.receive("key1") @matcher.got("key2", prompt="请输入key2...") @matcher.got("key3", prompt="请输入key3...") async def receive_func(key1: Event = Received("key1"), key2: Message = Arg(), key3: Message = Arg()): ... ``` ### send `send` 事件响应器操作用于向用户回复一条消息。协议适配器会根据当前 event 选择回复的途径。 `send` 操作接受一个参数 message 和其他任何协议适配器接受的参数。message 参数类型可以是字符串、消息序列、消息段或者消息模板。消息模板将会使用会话状态字典进行渲染后发送。 这个操作等同于使用 `bot.send(event, message, **kwargs)`,但不需要自行传入 `event`。 ```python @matcher.handle() async def _(): await matcher.send("Hello world!") ``` ### finish 向用户回复一条消息(可选),并立即结束**整个处理流程**。 参数与 [`send`](#send) 相同。 ```python @matcher.handle() async def _(): await matcher.finish("Hello world!") # 下面的代码不会被执行 ``` ### pause 向用户回复一条消息(可选),立即结束**当前**事件处理函数,等待接收一个新的事件后进入**下一个**事件处理函数。 参数与 [`send`](#send) 相同。 ```python @matcher.handle() async def _(): if need_confirm: await matcher.pause("请在两分钟内确认执行") @matcher.handle() async def _(): ... ``` ### reject 向用户回复一条消息(可选),立即结束**当前**事件处理函数,等待接收一个新的事件后再次执行**当前**事件处理函数。 `reject` 可以用于拒绝当前 `receive` 接收的事件或 `got` 接收的参数。通常在用户回复不符合格式或标准需要重新输入,或者用于循环进行用户交互。 参数与 [`send`](#send) 相同。 ```python @matcher.got("arg") async def _(arg: str = ArgPlainText()): if not is_valid(arg): await matcher.reject("Invalid arg!") ``` ### reject_arg 向用户回复一条消息(可选),立即结束**当前**事件处理函数,等待接收一个新的消息后再次执行**当前**事件处理函数。 `reject_arg` 用于拒绝指定 `got` 接收的参数,通常在嵌套装饰器时使用。 `reject_arg` 操作接受一个 key 参数以及可选的 prompt 参数。prompt 参数与 [`send`](#send) 相同。 ```python @matcher.got("a") @matcher.got("b") async def _(a: str = ArgPlainText(), b: str = ArgPlainText()): if a not in b: await matcher.reject_arg("a", "Invalid a!") ``` ### reject_receive 向用户回复一条消息(可选),立即结束**当前**事件处理函数,等待接收一个新的事件后再次执行**当前**事件处理函数。 `reject_receive` 用于拒绝指定 `receive` 接收的事件,通常在嵌套装饰器时使用。 `reject_receive` 操作接受一个可选的 id 参数以及可选的 prompt 参数。id 参数默认为空 `""`,prompt 参数与 [`send`](#send) 相同。 ```python @matcher.receive("a") @matcher.receive("b") async def _(a: Event = Received("a"), b: Event = Received("b")): if a.get_user_id() != b.get_user_id(): await matcher.reject_receive("a") ``` ### skip 立即结束当前事件处理函数,进入下一个事件处理函数。 通常在依赖注入中使用,用于跳过当前事件处理函数的执行。 ```python from nonebot.params import Depends async def dependency(): matcher.skip() @matcher.handle() async def _(check=Depends(dependency)): # 这个函数不会被执行 ``` ### stop_propagation 阻止事件向更低优先级的事件响应器传播。 ```python from nonebot.matcher import Matcher @foo.handle() async def _(matcher: Matcher): matcher.stop_propagation() ``` :::caution 注意 `stop_propagation` 操作是实例方法,需要先通过依赖注入获取事件响应器实例再进行调用。 ::: ### get_arg 获取一个 `got` 接收的参数。 `get_arg` 操作接受一个 key 参数和一个可选的 default 参数。当参数不存在时,将返回 default 或 `None`。 ```python from nonebot.matcher import Matcher @matcher.handle() async def _(matcher: Matcher): key = matcher.get_arg("key", default=None) ``` ### set_arg 设置 / 覆盖一个 `got` 接收的参数。 `set_arg` 操作接受一个 key 参数和一个 value 参数。请注意,value 参数必须是消息序列对象,如需存储其他数据请使用[会话状态](./session-state.md)。 ```python from nonebot.matcher import Matcher @matcher.handle() async def _(matcher: Matcher): matcher.set_arg("key", Message("value")) ``` ### get_receive 获取一个 `receive` 接收的事件。 `get_receive` 操作接受一个 id 参数和一个可选的 default 参数。当事件不存在时,将返回 default 或 `None`。 ```python from nonebot.matcher import Matcher @matcher.handle() async def _(matcher: Matcher): event = matcher.get_receive("id", default=None) ``` ### get_last_receive 获取最近的一个 `receive` 接收的事件。 `get_last_receive` 操作接受一个可选的 default 参数。当事件不存在时,将返回 default 或 `None`。 ```python from nonebot.matcher import Matcher @matcher.handle() async def _(matcher: Matcher): event = matcher.get_last_receive(default=None) ``` ### set_receive 设置 / 覆盖一个 `receive` 接收的事件。 `set_receive` 操作接受一个 id 参数和一个 event 参数。请注意,event 参数必须是事件对象,如需存储其他数据请使用[会话状态](./session-state.md)。 ```python from nonebot.matcher import Matcher @matcher.handle() async def _(matcher: Matcher): matcher.set_receive("key", Event()) ``` ================================================ FILE: website/versioned_docs/version-2.4.3/appendices/session-state.md ================================================ --- sidebar_position: 3 description: 会话状态信息 options: menu: - category: appendices weight: 40 --- # 会话状态 在事件处理流程中,和用户交互的过程即是会话。在会话中,我们可能需要记录一些信息,例如用户的重试次数等等,以便在会话中的不同阶段进行判断和处理。这些信息都可以存储于会话状态中。 NoneBot 中的会话状态是一个字典,可以通过类型 `T_State` 来获取。字典内可以存储任意类型的数据,但是要注意的是,NoneBot 本身会在会话状态中存储一些信息,因此不要使用 [NoneBot 使用的键名](../api/consts.md)。 ```python from nonebot.typing import T_State @matcher.got("key", prompt="请输入密码") async def _(state: T_State, key: str = ArgPlainText()): if key != "some password": try_count = state.get("try_count", 1) if try_count >= 3: await matcher.finish("密码错误次数过多") else: state["try_count"] = try_count + 1 await matcher.reject("密码错误,请重新输入") await matcher.finish("密码正确") ``` 会话状态的生命周期与事件处理流程相同,在期间的任何一个事件处理函数都可以进行读写。 ```python from nonebot.typing import T_State @matcher.handle() async def _(state: T_State): state["key"] = "value" @matcher.handle() async def _(state: T_State): await matcher.finish(state["key"]) ``` 会话状态还可以用于发送动态消息,消息模板在发送时会使用会话状态字典进行渲染。消息模板的使用方法已经在[消息处理](../tutorial/message.md#使用消息模板)中介绍过,这里不再赘述。 ```python from nonebot.typing import T_State from nonebot.adapters import MessageTemplate @matcher.handle() async def _(state: T_State): state["username"] = "user" @matcher.got("password", prompt=MessageTemplate("请输入 {username} 的密码")) async def _(): await matcher.finish(MessageTemplate("密码为 {password}")) ``` ================================================ FILE: website/versioned_docs/version-2.4.3/appendices/whats-next.md ================================================ --- sidebar_position: 99 description: 下一步──进阶! --- # 下一步 至此,我们已经了解了 NoneBot 的大多数功能用法,相信你已经可以独自写出一个插件了。现在你可以选择: - 即刻开始插件编写! - 更深入地了解 NoneBot 的[更多功能和原理](../advanced/plugin-info.md)! ================================================ FILE: website/versioned_docs/version-2.4.3/best-practice/alconna/README.mdx ================================================ 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#安装插件)来了解并选择安装插件的方式。如: 在**项目目录**下执行以下命令: ```shell nb plugin install nonebot-plugin-alconna ``` ```shell pip install nonebot-plugin-alconna ``` ```shell pdm add nonebot-plugin-alconna ``` ## 导入插件 由于 `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` 来改写这个插件。
插件示例 ```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}的天气是...") ```
```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) ================================================ FILE: website/versioned_docs/version-2.4.3/best-practice/alconna/_category_.json ================================================ { "label": "命令解析拓展", "position": 6 } ================================================ FILE: website/versioned_docs/version-2.4.3/best-practice/alconna/builtins.mdx ================================================ --- 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` 插件能将用户发送的消息原样返回。 #### help `help` 插件能列出所有 Alconna 指令。同时还能查询某个指令对应的插件信息。 help 插件的帮助信息如下: ``` /help ## 注释 query: 选择某条命令的id或者名称查看具体帮助 显示所有命令帮助 用法: 可以使用 --hide 参数来显示隐藏命令,使用 -P 参数来显示命令所属插件名称 可用的子命令有: * 是否列出命令所属命名空间 -N│--namespace│命名空间 [target: str] ## 注释 target: 指定的命名空间 该子命令内可用的选项有: * 列出所有命名空间 --list 可用的选项有: * 查看指定页数的命令帮助 --page * 查看命令所属插件的信息 -P│插件信息│--plugin-info * 是否列出隐藏命令 隐藏│-H│--hide ``` #### lang `lang` 插件能切换 i18n 的语言设置。 lang 插件的帮助信息如下: ``` /lang i18n配置相关功能 可用的选项有: * 查看支持的语言列表 list [name: str] * 切换语言 switch [locale: str] ``` 其中 `list` 选项可以查找某一插件下的语言支持情况 (例如 `/lang list nonebot_plugin_alconna`)。 #### switch `switch` 插件能用来启用/禁用某个命令,其使用方法与 `help` 类似。 #### with `with` 插件能在当前会话中设置一个局部命令前缀,以便于有多个子命令的指令使用。 with 插件的帮助信息如下: ``` .with [name: str] with 指令 用法: 设置局部命令前缀 可用的选项有: * 设置可能的生效时间 --expire│expire * 取消当前前缀 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 的音乐分享卡片 ================================================ FILE: website/versioned_docs/version-2.4.3/best-practice/alconna/command.md ================================================ --- sidebar_position: 2 description: Alconna 基本介绍 --- # Alconna 本体 [`Alconna`](https://github.com/ArcletProject/Alconna) 隶属于 `ArcletProject`,是一个简单、灵活、高效的命令参数解析器, 并且不局限于解析命令式字符串。 我们先通过一个例子来讲解 **Alconna** 的核心 —— `Args`, `Subcommand`, `Option`: ```python from arclet.alconna import Alconna, Args, Subcommand, Option alc = Alconna( "pip", Subcommand( "install", Args["package", str], Option("-r|--requirement", Args["file", str]), Option("-i|--index-url", Args["url", str]), ) ) res = alc.parse("pip install nonebot2 -i URL") print(res) # matched=True, header_match=(origin='pip' result='pip' matched=True groups={}), subcommands={'install': (value=Ellipsis args={'package': 'nonebot2'} options={'index-url': (value=None args={'url': 'URL'})} subcommands={})}, other_args={'package': 'nonebot2', 'url': 'URL'} print(res.all_matched_args) # {'package': 'nonebot2', 'url': 'URL'} ``` 这段代码通过`Alconna`创捷了一个接受主命令名为`pip`, 子命令为`install`且子命令接受一个 **Args** 参数`package`和二个 **Option** 参数`-r`和`-i`的命令参数解析器, 通过`parse`方法返回解析结果 **Arparma** 的实例。 ## 命令头 命令头是指命令的前缀 (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", "bar"] | "baz" | `"foobaz"` 或 `"barbaz"` | 纯文字头 | | [int, bool] | "foo" | `[123, "foo"]` 或 `[False, "foo"]` | 类型头 | | [123, 4567] | "foo" | `[123, "foo"]` 或 `[4567, "foo"]` | 元素头 | | [nepattern.NUMBER] | "bar" | `[123, "bar"]` 或 `[123.456, "bar"]` | 表达式头 | | [123, "foo"] | "bar" | `[123, "bar"]` 或 `"foobar"` 或 `["foo", "bar"]` | 混合头 | | [(int, "foo"), (456, "bar")] | "baz" | `[123, "foobaz"]` 或 `[456, "foobaz"]` 或 `[456, "barbaz"]` | 对头 | 对于无前缀的类型头,此时会将传入的值尝试转为 BasePattern,例如 `int` 会转为 `nepattern.INTEGER`。如此该命令头会匹配对应的类型, 例如 `int` 会匹配 `123` 或 `"456"`,但不会匹配 `"foo"`。解析后,Alconna 会将命令头匹配到的值转为对应的类型,例如 `int` 会将 `"123"` 转为 `123`。 :::tip **正则内容只在命令名上生效,前缀中的正则会被转义** ::: 除了通过传入 `re:xxx` 来使用正则表达式外,Alconna 还提供了一种更加简洁的方式来使用正则表达式,称为 Bracket Header: ```python alc = Alconna(".rd{roll:int}") assert alc.parse(".rd123").header["roll"] == 123 ``` Bracket Header 类似 python 里的 f-string 写法,通过 `"{}"` 声明匹配类型 `"{}"` 中的内容为 "name:type or pat": - `"{}"`, `"{:}"` ⇔ `"(.+)"`, 占位符 - `"{foo}"` ⇔ `"(?P<foo>.+)"` - `"{:\d+}"` ⇔ `"(\d+)"` - `"{foo:int}"` ⇔ `"(?P<foo>\d+)"`,其中 `"int"` 部分若能转为 `BasePattern` 则读取里面的表达式 ## 参数声明(Args) `Args` 是用于声明命令参数的组件, 可以通过以下几种方式构造 **Args** : - `Args[key, var, default][key1, var1, default1][...]` - `Args[(key, var, default)]` - `Args.key[var, default]` 其中,key **一定**是字符串,而 var 一般为参数的类型,default 为具体的值或者 **arclet.alconna.args.Field** 其与函数签名类似,但是允许含有默认值的参数在前;同时支持 keyword-only 参数不依照构造顺序传入 (但是仍需要在非 keyword-only 参数之后)。 ### key `key` 的作用是用以标记解析出来的参数并存放于 **Arparma** 中,以方便用户调用。 其有三种为 Args 注解的标识符: `?`、`/`、 `!`, 标识符与 key 之间建议以 `;` 分隔: - `!` 标识符表示该处传入的参数应**不是**规定的类型,或**不在**指定的值中。 - `?` 标识符表示该参数为**可选**参数,会在无参数匹配时跳过。 - `/` 标识符表示该参数的类型注解需要隐藏。 另外,对于参数的注释也可以标记在 `key` 中,其与 key 或者标识符 以 `#` 分割: `foo#这是注释;?` 或 `foo?#这是注释` :::tip `Args` 中的 `key` 在实际命令中并不需要传入(keyword 参数除外): ```python from arclet.alconna import Alconna, Args alc = Alconna("test", Args["foo", str]) alc.parse("test --foo abc") # 错误 alc.parse("test abc") # 正确 ``` 若需要 `test --foo abc`,你应该使用 `Option`: ```python from arclet.alconna import Alconna, Args, Option alc = Alconna("test", Option("--foo", Args["foo", str])) ``` ::: ### var var 负责命令参数的**类型检查**与**类型转化** `Args` 的`var`表面上看需要传入一个 `type`,但实际上它需要的是一个 `nepattern.BasePattern` 的实例: ```python from arclet.alconna import Args from nepattern import BasePattern # 表示 foo 参数需要匹配一个 @number 样式的字符串 args = Args["foo", BasePattern("@\d+")] ``` `pip` 示例中可以传入 `str` 是因为 `str` 已经注册在了 `nepattern.global_patterns` 中,因此会替换为 `nepattern.global_patterns[str]` `nepattern.global_patterns`默认支持的类型有: - `str`: 匹配任意字符串 - `int`: 匹配整数 - `float`: 匹配浮点数 - `bool`: 匹配 `True` 与 `False` 以及他们小写形式 - `hex`: 匹配 `0x` 开头的十六进制字符串 - `url`: 匹配网址 - `email`: 匹配 `xxxx@xxx` 的字符串 - `ipv4`: 匹配 `xxx.xxx.xxx.xxx` 的字符串 - `list`: 匹配类似 `["foo","bar","baz"]` 的字符串 - `dict`: 匹配类似 `{"foo":"bar","baz":"qux"}` 的字符串 - `datetime`: 传入一个 `datetime` 支持的格式字符串,或时间戳 - `Any`: 匹配任意类型 - `AnyString`: 匹配任意类型,转为 `str` - `Number`: 匹配 `int` 与 `float`,转为 `int` 同时可以使用 typing 中的类型: - `Literal[X]`: 匹配其中的任意一个值 - `Union[X, Y]`: 匹配其中的任意一个类型 - `Optional[xxx]`: 会自动将默认值设为 `None`,并在解析失败时使用默认值 - `List[X]`: 匹配一个列表,其中的元素为 `X` 类型 - `Dict[X, Y]`: 匹配一个字典,其中的 key 为 `X` 类型,value 为 `Y` 类型 - ... :::tip 几类特殊的传入标记: - `"foo"`: 匹配字符串 "foo" (若没有某个 `BasePattern` 与之关联) - `RawStr("foo")`: 匹配字符串 "foo" (即使有 `BasePattern` 与之关联也不会被替换) - `"foo|bar|baz"`: 匹配 "foo" 或 "bar" 或 "baz" - `[foo, bar, Baz, ...]`: 匹配其中的任意一个值或类型 - `Callable[[X], Y]`: 匹配一个参数为 `X` 类型的值,并返回通过该函数调用得到的 `Y` 类型的值 - `"re:xxx"`: 匹配一个正则表达式 `xxx`,会返回 Match[0] - `"rep:xxx"`: 匹配一个正则表达式 `xxx`,会返回 `re.Match` 对象 - `{foo: bar, baz: qux}`: 匹配字典中的任意一个键, 并返回对应的值 (特殊的键 ... 会匹配任意的值) - ... **特别的**,你可以不传入 `var`,此时会使用 `key` 作为 `var`, 匹配 `key` 字符串。 ::: #### MultiVar 与 KeyWordVar `MultiVar` 是一个特殊的标注,用于告知解析器该参数可以接受多个值,类似于函数中的 `*args`,其构造方法形如 `MultiVar(str)`。 同样的还有 `KeyWordVar`,类似于函数中的 `*, name: type`,其构造方法形如 `KeyWordVar(str)`,用于告知解析器该参数为一个 keyword-only 参数。 :::tip `MultiVar` 与 `KeyWordVar` 组合时,代表该参数为一个可接受多个 key-value 的参数,类似于函数中的 `**kwargs`,其构造方法形如 `MultiVar(KeyWordVar(str))` `MultiVar` 与 `KeyWordVar` 也可以传入 `default` 参数,用于指定默认值 `MultiVar` 不能在 `KeyWordVar` 之后传入 ::: #### AllParam `AllParam` 是一个特殊的标注,用于告知解析器该参数接收命令中在此位置之后的所有参数并**结束解析**,可以认为是**泛匹配参数**。 `AllParam` 可直接使用 (`Args["xxx", AllParam]`), 也可以传入指定的接收类型 (`Args["xxx", AllParam(str)]`)。 :::tip 在 `nonebot_plugin_alconna` 下,`AllParam` 的返回值为 [`UniMessage`](./uniseg/message.mdx) ::: ### default `default` 传入的是该参数的默认值或者 `Field`,以携带对于该参数的更多信息。 默认情况下 (即不声明) `default` 的值为特殊值 `Empty`。这也意味着你可以将默认值设置为 `None` 表示默认值为空值。 `Field` 构造需要的参数说明如下: - default: 参数单元的默认值 - alias: 参数单元默认值的别名 - completion: 参数单元的补全说明生成函数 - unmatch_tips: 参数单元的错误提示生成函数,其接收一个表示匹配失败的元素的参数 - missing_tips: 参数单元的缺失提示生成函数 ## 选项与子命令(Option & Subcommand) `Option` 和 `Subcommand` 可以传入一组 `alias`,如 `Option("--foo|-F|--FOO|-f")`,`Subcommand("foo", alias=["F"])` 传入别名后,选项与子命令会选择其中长度最长的作为其名称。若传入为 "--foo|-f",则命令名称为 "--foo" :::tip 特别提醒!!! Option 的名字或别名**没有要求**必须在前面写上 `-` Option 与 Subcommand 的唯一区别在于 Subcommand 可以传入自己的 **Option** 与 **Subcommand** ::: 他们拥有如下共同参数: - `help_text`: 传入该组件的帮助信息 - `dest`: 被指定为解析完成时标注匹配结果的标识符,不传入时默认为选项或子命令的名称 (name) - `requires`: 一段指定顺序的字符串列表,作为唯一的前置序列与命令嵌套替换 对于命令 `test foo bar baz qux ` 来讲,因为`foo bar baz` 仅需要判断是否相等, 所以可以这么编写: ```python Alconna("test", Option("qux", Args.a[int], requires=["foo", "bar", "baz"])) ``` - `default`: 默认值,在该组件未被解析时使用使用该值替换。 特别的,使用 `OptionResult` 或 `SubcomanndResult` 可以设置包括参数字典在内的默认值: ```python from arclet.alconna import Option, OptionResult opt1 = Option("--foo", default=False) opt2 = Option("--foo", default=OptionResult(value=False, args={"bar": 1})) ``` ### Action `Option` 可以特别设置传入一类 `Action`,作为解析操作 `Action` 分为三类: - `store`: 无 Args 时, 仅存储一个值, 默认为 Ellipsis; 有 Args 时, 后续的解析结果会覆盖之前的值 - `append`: 无 Args 时, 将多个值存为列表, 默认为 Ellipsis; 有 Args 时, 每个解析结果会追加到列表中, 当存在默认值并且不为列表时, 会自动将默认值变成列表, 以保证追加的正确性 - `count`: 无 Args 时, 计数器加一; 有 Args 时, 表现与 STORE 相同, 当存在默认值并且不为数字时, 会自动将默认值变成 1, 以保证计数器的正确性。 `Alconna` 提供了预制的几类 `Action`: - `store`(默认),`store_value`,`store_true`,`store_false` - `append`,`append_value` - `count` ## 解析结果 `Alconna.parse` 会返回由 **Arparma** 承载的解析结果 `Arparma` 有如下属性: - 调试类 - matched: 是否匹配成功 - error_data: 解析失败时剩余的数据 - error_info: 解析失败时的异常内容 - origin: 原始命令,可以类型标注 - 分析类 - header_match: 命令头部的解析结果,包括原始头部、解析后头部、解析结果与可能的正则匹配组 - main_args: 命令的主参数的解析结果 - options: 命令所有选项的解析结果 - subcommands: 命令所有子命令的解析结果 - other_args: 除主参数外的其他解析结果 - all_matched_args: 所有 Args 的解析结果 ### 路径查询 `Arparma` 同时提供了便捷的查询方法 `query[type]()`,会根据传入的 `path` 查找参数并返回 `path` 支持如下: - `main_args`, `options`, ...: 返回对应的属性 - `args`: 返回 all_matched_args - `args.`: 返回 all_matched_args 中 `key` 键对应的值 - `main_args.`: 返回主命令的解析参数字典中 `key` 键对应的值 - ``: 返回选项/子命令 `node` 的解析结果 (OptionResult | SubcommandResult) - `.value`: 返回选项/子命令 `node` 的解析值 - `.args`: 返回选项/子命令 `node` 的解析参数字典 - `.`, `.args.`: 返回选项/子命令 `node` 的参数字典中 `key` 键对应的值 以及: - `options.`: 返回选项 `opt` 的解析结果 (OptionResult) - `options..value`: 返回选项 `opt` 的解析值 - `options..args`: 返回选项 `opt` 的解析参数字典 - `options..`, `options..args.`: 返回选项 `opt` 的参数字典中 `key` 键对应的值 - `subcommands.`: 返回子命令 `subcmd` 的解析结果 (SubcommandResult) - `subcommands..value`: 返回子命令 `subcmd` 的解析值 - `subcommands..args`: 返回子命令 `subcmd` 的解析参数字典 - `subcommands..`, `subcommands..args.`: 返回子命令 `subcmd` 的参数字典中 `key` 键对应的值 ## 元数据(CommandMeta) `Alconna` 的元数据相当于其配置,拥有以下条目: - `description`: 命令的描述 - `usage`: 命令的用法 - `example`: 命令的使用样例 - `author`: 命令的作者 - `fuzzy_match`: 命令是否开启模糊匹配 - `fuzzy_threshold`: 模糊匹配阈值 - `raise_exception`: 命令是否抛出异常 - `hide`: 命令是否对 manager 隐藏 - `hide_shortcut`: 命令的快捷指令是否在 help 信息中隐藏 - `keep_crlf`: 命令解析时是否保留换行字符 - `compact`: 命令是否允许第一个参数紧随头部 - `strict`: 命令是否严格匹配,若为 False 则未知参数将作为名为 $extra 的参数 - `context_style`: 命令上下文插值的风格,None 为关闭,bracket 为 `{...}`,parentheses 为 `$(...)` - `extra`: 命令的自定义额外信息 元数据一定使用 `meta=...` 形式传入: ```python from arclet.alconna import Alconna, CommandMeta alc = Alconna(..., meta=CommandMeta("foo", example="bar")) ``` ## 命名空间配置 命名空间配置 (以下简称命名空间) 相当于 `Alconna` 的默认配置,其优先度低于 `CommandMeta`。 `Alconna` 默认使用 "Alconna" 命名空间。 命名空间有以下几个属性: - name: 命名空间名称 - prefixes: 默认前缀配置 - separators: 默认分隔符配置 - formatter_type: 默认格式化器类型 - fuzzy_match: 默认是否开启模糊匹配 - raise_exception: 默认是否抛出异常 - builtin_option_name: 默认的内置选项名称(--help, --shortcut, --comp) - disable_builtin_options: 默认禁用的内置选项(--help, --shortcut, --comp) - enable_message_cache: 默认是否启用消息缓存 - compact: 默认是否开启紧凑模式 - strict: 命令是否严格匹配 - context_style: 命令上下文插值的风格 - ... ### 新建命名空间并替换 ```python from arclet.alconna import Alconna, namespace, Namespace, Subcommand, Args, config ns = Namespace("foo", prefixes=["/"]) # 创建 "foo"命名空间配置, 它要求创建的Alconna的主命令前缀必须是/ alc = Alconna("pip", Subcommand("install", Args["package", str]), namespace=ns) # 在创建Alconna时候传入命名空间以替换默认命名空间 # 可以通过with方式创建命名空间 with namespace("bar") as np1: np1.prefixes = ["!"] # 以上下文管理器方式配置命名空间,此时配置会自动注入上下文内创建的命令 np1.formatter_type = ShellTextFormatter # 设置此命名空间下的命令的 formatter 默认为 ShellTextFormatter np1.builtin_option_name["help"] = {"帮助", "-h"} # 设置此命名空间下的命令的帮助选项名称 # 你还可以使用config来管理所有命名空间并切换至任意命名空间 config.namespaces["foo"] = ns # 将命名空间挂载到 config 上 alc = Alconna("pip", Subcommand("install", Args["package", str]), namespace=config.namespaces["foo"]) # 也是同样可以切换到"foo"命名空间 ``` ### 修改默认的命名空间 ```python from arclet.alconna import config, namespace, Namespace config.default_namespace.prefixes = [...] # 直接修改默认配置 np = Namespace("xxx", prefixes=[...]) config.default_namespace = np # 更换默认的命名空间 with namespace(config.default_namespace.name) as np: np.prefixes = [...] ``` ## 快捷指令 快捷命令可以做到标识一段命令, 并且传递参数给原命令 一般情况下你可以通过 `Alconna.shortcut` 进行快捷指令操作 (创建,删除) `shortcut` 的第一个参数为快捷指令名称,第二个参数为 `ShortcutArgs`,作为快捷指令的配置: ```python class ShortcutArgs(TypedDict): """快捷指令参数""" command: NotRequired[str] """快捷指令的命令""" args: NotRequired[list[Any]] """快捷指令的附带参数""" fuzzy: NotRequired[bool] """是否允许命令后随参数""" prefix: NotRequired[bool] """是否调用时保留指令前缀""" wrapper: NotRequired[ShortcutRegWrapper] """快捷指令的正则匹配结果的额外处理函数""" humanized: NotRequired[str] """快捷指令的人类可读描述""" ``` ### args的使用 ```python from arclet.alconna import Alconna, Args alc = Alconna("setu", Args["count", int]) alc.shortcut("涩图(\d+)张", {"args": ["{0}"]}) # 'Alconna::setu 的快捷指令: "涩图(\\d+)张" 添加成功' alc.parse("涩图3张").query("count") # 3 ``` ### command的使用 ```python from arclet.alconna import Alconna, Args alc = Alconna("eval", Args["content", str]) alc.shortcut("echo", {"command": "eval print(\\'{*}\\')"}) # 'Alconna::eval 的快捷指令: "echo" 添加成功' alc.shortcut("echo", delete=True) # 删除快捷指令 # 'Alconna::eval 的快捷指令: "echo" 删除成功' @alc.bind() # 绑定一个命令执行器, 若匹配成功则会传入参数, 自动执行命令执行器 def cb(content: str): eval(content, {}, {}) alc.parse('eval print(\\"hello world\\")') # hello world alc.parse("echo hello world!") # hello world! ``` 当 `fuzzy` 为 False 时,第一个例子中传入 `"涩图1张 abc"` 之类的快捷指令将视为解析失败 快捷指令允许三类特殊的 placeholder: - `{%X}`: 如 `setu {%0}`,表示此处填入快捷指令后随的第 X 个参数。 例如,若快捷指令为 `涩图`, 配置为 `{"command": "setu {%0}"}`, 则指令 `涩图 1` 相当于 `setu 1` - `{*}`: 表示此处填入所有后随参数,并且可以通过 `{*X}` 的方式指定组合参数之间的分隔符。 - `{X}`: 表示此处填入可能的正则匹配的组: - 若 `command` 中存在匹配组 `(xxx)`,则 `{X}` 表示第 X 个匹配组的内容 - 若 `command` 中存储匹配组 `(?P...)`, 则 `{X}` 表示 **名字** 为 X 的匹配结果 除此之外, 通过 **Alconna** 内置选项 `--shortcut` 可以动态操作快捷指令 例如: - `cmd --shortcut ` 来增加一个快捷指令 - `cmd --shortcut list` 来列出当前指令的所有快捷指令 - `cmd --shortcut delete key` 来删除一个快捷指令 ```python from arclet.alconna import Alconna, Args alc = Alconna("eval", Args["content", str]) alc.shortcut("echo", {"command": "eval print(\\'{*}\\')"}) alc.parse("eval --shortcut list") # 'echo' ``` ## 紧凑命令 `Alconna`, `Option` 与 `Subcommand` 可以设置 `compact=True` 使得解析命令时允许名称与后随参数之间没有分隔: ```python from arclet.alconna import Alconna, Option, CommandMeta, Args alc = Alconna("test", Args["foo", int], Option("BAR", Args["baz", str], compact=True), meta=CommandMeta(compact=True)) assert alc.parse("test123 BARabc").matched ``` 这使得我们可以实现如下命令: ```python from arclet.alconna import Alconna, Option, Args, append alc = Alconna("gcc", Option("--flag|-F", Args["content", str], action=append, compact=True)) print(alc.parse("gcc -Fabc -Fdef -Fxyz").query[list]("flag.content")) # ['abc', 'def', 'xyz'] ``` 当 `Option` 的 `action` 为 `count` 时,其自动支持 `compact` 特性: ```python from arclet.alconna import Alconna, Option, count alc = Alconna("pp", Option("--verbose|-v", action=count, default=0)) print(alc.parse("pp -vvv").query[int]("verbose.value")) # 3 ``` ## 模糊匹配 模糊匹配会应用在任意需要进行名称判断的地方,如 **命令名称**,**选项名称** 和 **参数名称** (如指定需要传入参数名称)。 ```python from arclet.alconna import Alconna, CommandMeta alc = Alconna("test_fuzzy", meta=CommandMeta(fuzzy_match=True)) alc.parse("test_fuzy") # test_fuzy is not matched. Do you mean "test_fuzzy"? ``` ## 半自动补全 半自动补全为用户提供了推荐后续输入的功能 补全默认通过 `--comp` 或 `-cp` 或 `?` 触发:(命名空间配置可修改名称) ```python from arclet.alconna import Alconna, Args, Option alc = Alconna("test", Args["abc", int]) + Option("foo") + Option("bar") alc.parse("test --comp") ''' output 以下是建议的输入: * * --help * -h * -sct * --shortcut * foo * bar ''' ``` ## Duplication **Duplication** 用来提供更好的自动补全,类似于 **ArgParse** 的 **Namespace** 普通情况下使用,需要利用到 **ArgsStub**、**OptionStub** 和 **SubcommandStub** 三个部分 以pip为例,其对应的 Duplication 应如下构造: ```python from arclet.alconna import Alconna, Args, Option, OptionResult, Duplication, SubcommandStub, Subcommand, count class MyDup(Duplication): verbose: OptionResult install: SubcommandStub alc = Alconna( "pip", Subcommand( "install", Args["package", str], Option("-r|--requirement", Args["file", str]), Option("-i|--index-url", Args["url", str]), ), Option("-v|--version"), Option("-v|--verbose", action=count), ) res = alc.parse("pip -v install ...") # 不使用duplication获得的提示较少 print(res.query("install")) # (value=Ellipsis args={'package': '...'} options={} subcommands={}) result = alc.parse("pip -v install ...", duplication=MyDup) print(result.install) # SubcommandStub(_origin=Subcommand('install', args=Args('package': str)), _value=Ellipsis, available=True, args=ArgsStub(_origin=Args('package': str), _value={'package': '...'}, available=True), dest='install', options=[OptionStub(_origin=Option('requirement', args=Args('file': str)), _value=None, available=False, args=ArgsStub(_origin=Args('file': str), _value={}, available=False), dest='requirement', aliases=['r', 'requirement'], name='requirement'), OptionStub(_origin=Option('index-url', args=Args('url': str)), _value=None, available=False, args=ArgsStub(_origin=Args('url': str), _value={}, available=False), dest='index-url', aliases=['index-url', 'i'], name='index-url')], subcommands=[], name='install') ``` **Duplication** 也可以如 **Namespace** 一样直接标明参数名称和类型: ```python from typing import Optional from arclet.alconna import Duplication class MyDup(Duplication): package: str file: Optional[str] = None url: Optional[str] = None ``` ## 上下文插值 当 `context_style` 条目被设置后,传入的命令中符合上下文插值的字段会被自动替换成当前上下文中的信息。 上下文可以在 `parse` 中传入: ```python from arclet.alconna import Alconna, Args, CommandMeta alc = Alconna("test", Args["foo", int], meta=CommandMeta(context_style="parentheses")) alc.parse("test $(bar)", {"bar": 123}) # {"foo": 123} ``` context_style 的值分两种: - `"bracket"`: 插值格式为 `{...}`,例如 `{foo}` - `"parentheses"`: 插值格式为 `$(...)`,例如 `$(bar)` ================================================ FILE: website/versioned_docs/version-2.4.3/best-practice/alconna/config.md ================================================ --- sidebar_position: 4 description: 配置项 --- # 配置项 ## alconna_auto_send_output - **类型**: `bool | None` - **默认值**: `None` 是否全局启用输出信息自动发送,不启用则会在触发特殊内置选项后仍然将解析结果传递至响应器。 ## alconna_use_command_start - **类型**: `bool` - **默认值**: `False` 是否读取 Nonebot 的配置项 `COMMAND_START` 来作为全局的 Alconna 命令前缀 ## alconna_global_completion - **类型**: [`CompConfig | None`](./matcher.mdx#补全会话) - **默认值**: `None` 全局的补全会话配置 (不代表全局启用补全会话)。 ## alconna_use_origin - **类型**: `bool` - **默认值**: `False` 是否全局使用原始消息 (即未经过 to_me 等处理的),该选项会影响到 Alconna 的匹配行为。 ## alconna_use_command_sep - **类型**: `bool` - **默认值**: `False` 是否读取 Nonebot 的配置项 `COMMAND_SEP` 来作为全局的 Alconna 命令分隔符。 ## alconna_global_extensions - **类型**: `list[str]` - **默认值**: `[]` 全局加载的扩展,其读取路径以 . 分隔,如 `foo.bar.baz:DemoExtension`。 对于内置扩展,路径为 `nonebot_plugin_alconna.builtins.extensions` 下的模块名,如 `ReplyMergeExtension`,可以使用 `@` 来缩写路径, 如 `@reply:ReplyMergeExtension`。 ## alconna_context_style - **类型**: `Optional[Literal["bracket", "parentheses"]]` - **默认值**: `None` 全局命令上下文插值的风格,None 为关闭,bracket 为 `{...}`,parentheses 为 `$(...)`。 ## alconna_enable_saa_patch - **类型**: `bool` - **默认值**: `False` 是否启用 SAA 补丁。 ## alconna_apply_filehost - **类型**: `bool` - **默认值**: `False` 是否启用文件托管。 ## alconna_apply_fetch_targets - **类型**: `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 自身发送的消息。 ================================================ FILE: website/versioned_docs/version-2.4.3/best-practice/alconna/matcher.mdx ================================================ --- 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("添加成功") ``` ## 声明 `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`:命令的源命令 - `: Match[]`:上述的匹配项,使用 `key` 作为查询路径 - `xxx: Query[] = Query(, default)`:上述的查询项,必需声明默认值以设置查询路径 `path` - 当用来查询选项/子命令是否存在时,可不写 `Query[]` - `xxx: Extension`:当前 `AlconnaMatcher` 使用的指定类型的匹配扩展 - `xxx: SelectedExtensions`:当前 `AlconnaMatcher` 使用的所有可用的匹配扩展 - `: `: 其他情况 - 当 `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. ```yaml title="zh-CN.yml" # 中文语言文件 demo: command: role-group: add: 添加 {name} 成功! ``` ```yaml title="en-US.yml" # 英文语言文件 demo: command: role-group: add: Add {name} successfully! ``` ```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("添加成功") ``` 此时,当用户输入 `添加教师` 时,会自动提示用户输入姓名,手机号和教师号,用户输入后会自动进入下一个提示: 补全会话配置如下: ```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] """进行补全会话时是否阻塞响应器""" ``` ================================================ FILE: website/versioned_docs/version-2.4.3/best-practice/alconna/shortcut.md ================================================ --- 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 ") .option("writer", "--anonymous", {"id": 0}) .usage("book [-w | --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 ") .option("writer", "--anonymous", {"id": 0}) .usage("book [-w | --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 " - name: writer opt: "--anonymous" default: id: 1 usage: book [-w | --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") ``` ================================================ FILE: website/versioned_docs/version-2.4.3/best-practice/alconna/uniseg/README.md ================================================ # 通用消息组件 `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) ``` ================================================ FILE: website/versioned_docs/version-2.4.3/best-practice/alconna/uniseg/_category_.json ================================================ { "label": "通用消息组件", "position": 5 } ================================================ FILE: website/versioned_docs/version-2.4.3/best-practice/alconna/uniseg/message.mdx ================================================ --- sidebar_position: 3 description: 消息序列 --- import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; # 通用消息序列 `uniseg` 提供了一个类似于 `Message` 的 `UniMessage` 类型,其元素为[通用消息段](./segment.md)。 你可以用如下方式获取 `UniMessage`: 通过提供的 `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) ... ``` 注意,`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) ``` ## 发送消息 你还可以通过 `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 = (attrs: dict, children: Segment[]) => T; type Transformer = boolean | Fragment | Render; ``` ### 字符串操作 类似于 `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 格式。 ================================================ FILE: website/versioned_docs/version-2.4.3/best-practice/alconna/uniseg/segment.md ================================================ --- 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 的处理方法。 ================================================ FILE: website/versioned_docs/version-2.4.3/best-practice/alconna/uniseg/utils.mdx ================================================ --- sidebar_position: 4 description: 辅助模型 --- import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; # 辅助功能 `uniseg` 模块同时提供了多种方法以通用消息操作。 :::note 这些方法中与 `event`, `bot` 相关的参数都会尝试从上下文中获取对象。 ::: ## 消息事件 ID 消息事件 ID 是用来标识当前消息事件的唯一 ID,通常用于回复/撤回/编辑/表态当前消息。 通过提供的 `MessageId` 或 `MsgId` 依赖注入器来获取消息事件 id。 ```python from nonebot_plugin_alconna.uniseg import MsgId matcher = on_xxx(...) @matcher.handle() asycn def _(msg_id: MsgId): ... ``` ```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) ``` :::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] """额外信息,用于适配器扩展""" ``` 通过提供的 `MessageTarget` 或 `MsgTarget` 依赖注入器来获取消息发送对象。 ```python from nonebot_plugin_alconna.uniseg import MsgTarget matcher = on_xxx(...) @matcher.handle() asycn def _(target: MsgTarget): ... ``` ```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) ``` 主动构造一个发送对象时,则需要如下参数: - `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()) ``` ================================================ FILE: website/versioned_docs/version-2.4.3/best-practice/data-storing.md ================================================ --- sidebar_position: 1 description: 存储数据文件到本地 --- # 数据存储 在使用插件的过程中,难免会需要存储一些持久化数据,例如用户的个人信息、群组的信息等。除了使用数据库等第三方存储之外,还可以使用本地文件来自行管理数据。NoneBot 提供了 `nonebot-plugin-localstore` 插件,可用于获取正确的数据存储路径并写入数据。 ## 安装插件 在使用前请先安装 `nonebot-plugin-localstore` 插件至项目环境中,可参考[获取商店插件](../tutorial/store.mdx#安装插件)来了解并选择安装插件的方式。如: 在**项目目录**下执行以下命令: ```bash nb plugin install nonebot-plugin-localstore ``` ## 使用插件 `nonebot-plugin-localstore` 插件兼容 Windows、Linux 和 macOS 等操作系统,使用时无需关心操作系统的差异。同时插件提供 `nb-cli` 脚本,可以使用 `nb localstore` 命令来检查数据存储路径。 在使用本插件前同样需要使用 `require` 方法进行**加载**并**导入**需要使用的方法,可参考 [跨插件访问](../advanced/requiring.md) 一节进行了解,如: ```python from nonebot import require require("nonebot_plugin_localstore") import nonebot_plugin_localstore as store # 获取插件缓存目录 cache_dir = store.get_plugin_cache_dir() # 获取插件缓存文件 cache_file = store.get_plugin_cache_file("file_name") # 获取插件数据目录 data_dir = store.get_plugin_data_dir() # 获取插件数据文件 data_file = store.get_plugin_data_file("file_name") # 获取插件配置目录 config_dir = store.get_plugin_config_dir() # 获取插件配置文件 config_file = store.get_plugin_config_file("file_name") ``` :::danger 警告 在 Windows 和 macOS 系统下,插件的数据目录和配置目录是同一个目录,因此在使用时需要注意避免文件名冲突。 ::: 插件提供的方法均返回一个 `pathlib.Path` 路径,可以参考 [pathlib 文档](https://docs.python.org/zh-cn/3/library/pathlib.html)来了解如何使用。常用的方法有: ```python from pathlib import Path data_file = store.get_plugin_data_file("file_name") # 写入文件内容 data_file.write_text("Hello World!") # 读取文件内容 data = data_file.read_text() ``` :::note 提示 对于嵌套插件,子插件的存储目录将位于父插件存储目录下。 ::: ## 配置项 ### localstore_use_cwd 使用当前工作目录作为数据存储目录,以下数据目录配置项默认值将会对应变更 默认值:`False` ```dotenv LOCALSTORE_USE_CWD=true ``` ### localstore_cache_dir 自定义缓存目录 默认值: 当 `localstore_use_cwd` 为 `True` 时,缓存目录为 `/cache`,否则: - macOS: `~/Library/Caches/nonebot2` - Unix: `~/.cache/nonebot2` (XDG default) - Windows: `C:\Users\\AppData\Local\nonebot2\Cache` ```dotenv LOCALSTORE_CACHE_DIR=/tmp/cache ``` ### localstore_data_dir 自定义数据目录 默认值: 当 `localstore_use_cwd` 为 `True` 时,数据目录为 `/data`,否则: - macOS: `~/Library/Application Support/nonebot2` - Unix: `~/.local/share/nonebot2` or in $XDG_DATA_HOME, if defined - Win XP (not roaming): `C:\Documents and Settings\\Application Data\nonebot2` - Win 7 (not roaming): `C:\Users\\AppData\Local\nonebot2` ```dotenv LOCALSTORE_DATA_DIR=/tmp/data ``` ### localstore_config_dir 自定义配置目录 默认值: 当 `localstore_use_cwd` 为 `True` 时,配置目录为 `/config`,否则: - macOS: same as user_data_dir - Unix: `~/.config/nonebot2` - Win XP (roaming): `C:\Documents and Settings\\Local Settings\Application Data\nonebot2` - Win 7 (roaming): `C:\Users\\AppData\Roaming\nonebot2` ```dotenv LOCALSTORE_CONFIG_DIR=/tmp/config ``` ### localstore_plugin_cache_dir 自定义插件缓存目录 默认值:`{}` ```dotenv LOCALSTORE_PLUGIN_CACHE_DIR=' { "plugin_id": "/tmp/plugin_cache" } ' ``` ### localstore_plugin_data_dir 自定义插件数据目录 默认值:`{}` ```dotenv LOCALSTORE_PLUGIN_DATA_DIR=' { "plugin_id": "/tmp/plugin_data" } ' ``` ### localstore_plugin_config_dir 自定义插件配置目录 默认值:`{}` ```dotenv LOCALSTORE_PLUGIN_CONFIG_DIR=' { "plugin_id": "/tmp/plugin_config" } ' ``` ================================================ FILE: website/versioned_docs/version-2.4.3/best-practice/database/README.mdx ================================================ import TabItem from "@theme/TabItem"; import Tabs from "@theme/Tabs"; # 数据库 [`nonebot-plugin-orm`](https://github.com/nonebot/plugin-orm) 是 NoneBot 的数据库支持插件。 本插件基于 [SQLAlchemy](https://www.sqlalchemy.org/) 和 [Alembic](https://alembic.sqlalchemy.org/),提供了许多与 NoneBot 紧密集成的功能: - 多 Engine / Connection 支持 - Session 管理 - 关系模型管理、依赖注入支持 - 数据库迁移 ## 安装 ```shell nb plugin install nonebot-plugin-orm ``` ```shell pip install nonebot-plugin-orm ``` ```shell pdm add nonebot-plugin-orm ``` ## 数据库驱动和后端 本插件只提供了 ORM 功能,没有数据库后端,也没有直接连接数据库后端的能力。 所以你需要另行安装数据库驱动和数据库后端,并且配置数据库连接信息。 ### SQLite [SQLite](https://www.sqlite.org/) 是一个轻量级的嵌入式数据库,它的数据以单文件的形式存储在本地,不需要单独的数据库后端。 SQLite 非常适合用于开发环境和小型应用,但是不适合用于大型应用的生产环境。 虽然不需要另行安装数据库后端,但你仍然需要安装数据库驱动: ```shell pip install "nonebot-plugin-orm[sqlite]" ``` ```shell pdm add "nonebot-plugin-orm[sqlite]" ``` 默认情况下,数据库文件为 `/nonebot-plugin-orm/db.sqlite3`(数据目录由 [nonebot-plugin-localstore](../data-storing) 提供)。 或者,你可以通过配置 `SQLALCHEMY_DATABASE_URL` 来指定数据库文件路径: ```shell SQLALCHEMY_DATABASE_URL=sqlite+aiosqlite:///file_path ``` ### PostgreSQL [PostgreSQL](https://www.postgresql.org/) 是世界上最先进的开源关系数据库之一,对各种高级且广泛应用的功能有最好的支持,是中小型应用的首选数据库。 ```shell pip install nonebot-plugin-orm[postgresql] ``` ```shell pdm add nonebot-plugin-orm[postgresql] ``` ```shell SQLALCHEMY_DATABASE_URL=postgresql+psycopg://user:password@host:port/dbname[?key=value&key=value...] ``` ### MySQL / MariaDB [MySQL](https://www.mysql.com/) 和 [MariaDB](https://mariadb.com/) 是经典的开源关系数据库,适合用于中小型应用。 ```shell pip install nonebot-plugin-orm[mysql] ``` ```shell pdm add nonebot-plugin-orm[mysql] ``` ```shell SQLALCHEMY_DATABASE_URL=mysql+aiomysql://user:password@host:port/dbname[?key=value&key=value...] ``` ## 使用 本插件提供了数据库迁移功能(此功能依赖于 [nb-cli 脚手架](../../quick-start#安装脚手架))。 在安装了新的插件或机器人之后,你需要执行一次数据库迁移操作,将数据库同步至与机器人一致的状态: ```shell nb orm upgrade ``` 运行完毕后,可以检查一下: ```shell nb orm check ``` 如果输出是 `没有检测到新的升级操作`,那么恭喜你,数据库已经迁移完成了,你可以启动机器人了。 ================================================ FILE: website/versioned_docs/version-2.4.3/best-practice/database/_category_.json ================================================ { "label": "数据库", "position": 7 } ================================================ FILE: website/versioned_docs/version-2.4.3/best-practice/database/developer/README.md ================================================ # 开发者指南 开发者指南内容较多,故分为了一个示例以及数个专题。 阅读(并且最好跟随实践)示例后,你将会对使用 `nonebot-plugin-orm` 开发插件有一个基本的认识。 如果想要更深入地学习关于 [SQLAlchemy](https://www.sqlalchemy.org/) 和 [Alembic](https://alembic.sqlalchemy.org/) 的知识,或者在使用过程中遇到了问题,可以查阅专题以及其官方文档。 ## 示例 ### 模型定义 首先,我们需要设计存储的数据的结构。 例如天气插件,需要存储**什么地方 (`location`)** 的**天气是什么 (`weather`)**。 其中,一个地方只会有一种天气,而不同地方可能有相同的天气。 所以,我们可以设计出如下的模型: ```python title=weather/__init__.py showLineNumbers from nonebot_plugin_orm import Model from sqlalchemy.orm import Mapped, mapped_column class Weather(Model): location: Mapped[str] = mapped_column(primary_key=True) weather: Mapped[str] ``` 其中,`primary_key=True` 意味着此列 (`location`) 是主键,即内容是唯一的且非空的。 每一个模型必须有至少一个主键。 我们可以用以下代码检查模型生成的数据库模式是否正确: ```python from sqlalchemy.schema import CreateTable print(CreateTable(Weather.__table__)) ``` ```sql CREATE TABLE weather_weather ( location VARCHAR NOT NULL, weather VARCHAR NOT NULL, CONSTRAINT pk_weather_weather PRIMARY KEY (location) ) ``` 可以注意到表名是 `weather_weather` 而不是 `Weather` 或者 `weather`。 这是因为 `nonebot-plugin-orm` 会自动为模型生成一个表名,规则是:`<插件模块名>_<类名小写>`。 你也可以通过指定 `__tablename__` 属性来自定义表名: ```python {2} class Weather(Model): __tablename__ = "weather" ... ``` ```sql {1} CREATE TABLE weather ( ... ) ``` 但是,并不推荐你这么做,因为这可能会导致不同插件间的表名重复,引发冲突。 特别是当你会发布插件时,你并不知道其他插件会不会使用相同的表名。 ### 首次迁移 我们成功定义了模型,现在启动机器人试试吧: ```shell $ nb run 01-02 15:04:05 [SUCCESS] nonebot | NoneBot is initializing... 01-02 15:04:05 [ERROR] nonebot_plugin_orm | 启动检查失败 01-02 15:04:05 [ERROR] nonebot | Application startup failed. Exiting. Traceback (most recent call last): ... click.exceptions.UsageError: 检测到新的升级操作: [('add_table', Table('weather', MetaData(), Column('location', String(), table=, primary_key=True, nullable=False), Column('weather', String(), table=, nullable=False), schema=None))] ``` 咦,发生了什么? `nonebot-plugin-orm` 试图阻止我们启动机器人。 原来是我们定义了模型,但是数据库中并没有对应的表,这会导致插件不能正常运行。 所以,我们需要迁移数据库。 首先,我们需要创建一个迁移脚本: ```shell nb orm revision -m "first revision" --branch-label weather ``` 其中,`-m` 参数是迁移脚本的描述,`--branch-label` 参数是迁移脚本的分支,一般为插件模块名。 执行命令过后,出现了一个 `weather/migrations` 目录,其中有一个 `xxxxxxxxxxxx_first_revision.py` 文件: ```shell {4,5} weather ├── __init__.py ├── config.py └── migrations └── xxxxxxxxxxxx_first_revision.py ``` 这就是我们创建的迁移脚本,它记录了数据库模式的变化。 我们可以查看一下它的内容: ```python title=weather/migrations/xxxxxxxxxxxx_first_revision.py {25-33,39-41} showLineNumbers """first revision 迁移 ID: xxxxxxxxxxxx 父迁移: 创建时间: 2006-01-02 15:04:05.999999 """ from __future__ import annotations from collections.abc import Sequence import sqlalchemy as sa from alembic import op revision: str = "xxxxxxxxxxxx" down_revision: str | Sequence[str] | None = None branch_labels: str | Sequence[str] | None = ("weather",) depends_on: str | Sequence[str] | None = None def upgrade(name: str = "") -> None: if name: return # ### commands auto generated by Alembic - please adjust! ### op.create_table( "weather_weather", sa.Column("location", sa.String(), nullable=False), sa.Column("weather", sa.String(), nullable=False), sa.PrimaryKeyConstraint("location", name=op.f("pk_weather_weather")), info={"bind_key": "weather"}, ) # ### end Alembic commands ### def downgrade(name: str = "") -> None: if name: return # ### commands auto generated by Alembic - please adjust! ### op.drop_table("weather_weather") # ### end Alembic commands ### ``` 可以注意到脚本的主体部分(其余是模版代码,请勿修改)是: ```python # ### commands auto generated by Alembic - please adjust! ### op.create_table( # CREATE TABLE "weather_weather", # weather_weather sa.Column("location", sa.String(), nullable=False), # location VARCHAR NOT NULL, sa.Column("weather", sa.String(), nullable=False), # weather VARCHAR NOT NULL, sa.PrimaryKeyConstraint("location", name=op.f("pk_weather_weather")), # CONSTRAINT pk_weather_weather PRIMARY KEY (location) info={"bind_key": "weather"}, ) # ### end Alembic commands ### ``` ```python # ### commands auto generated by Alembic - please adjust! ### op.drop_table("weather_weather") # DROP TABLE weather_weather; # ### end Alembic commands ### ``` 虽然我们不是很懂这些代码的意思,但是可以注意到它们几乎与 SQL 语句 (DDL) 一一对应。 显然,它们是用来创建和删除表的。 我们还可以注意到,`upgrade()` 和 `downgrade()` 函数中的代码是**互逆**的。 也就是说,执行一次 `upgrade()` 函数,再执行一次 `downgrade()` 函数后,数据库的模式就会回到原来的状态。 这就是迁移脚本的作用:记录数据库模式的变化,以便我们在不同的环境中(例如开发环境和生产环境)**可复现地**、**可逆地**同步数据库模式,正如 git 对我们的代码做的事情那样。 对了,不要忘记还有一段注释:`commands auto generated by Alembic - please adjust!`。 它在提醒我们,这些代码是由 Alembic 自动生成的,我们应该检查它们,并且根据需要进行调整。 :::caution 注意 迁移脚本冗长且繁琐,我们一般不会手写它们,而是由 Alembic 自动生成。 一般情况下,Alembic 足够智能,可以正确地生成迁移脚本。 但是,在复杂或有歧义的情况下,我们可能需要手动调整迁移脚本。 所以,**永远要检查迁移脚本,并且在开发环境中测试!** **迁移脚本中任何一处错误都足以使数据付之东流!** ::: 确定迁移脚本正确后,我们就可以执行迁移脚本,将数据库模式同步到数据库中: ```shell nb orm upgrade ``` 现在,我们可以正常启动机器人了。 开发过程中,我们可能会频繁地修改模型,这意味着我们需要频繁地创建并执行迁移脚本,非常繁琐。 实际上,此时我们不在乎数据安全,只需要数据库模式与模型定义一致即可。 所以,我们可以关闭 `nonebot-plugin-orm` 的启动检查: ```shell title=.env.dev ALEMBIC_STARTUP_CHECK=false ``` 现在,每次启动机器人时,数据库模式会自动与模型定义同步,无需手动迁移。 ### 会话管理 我们已经成功定义了模型,并且迁移了数据库,现在可以开始使用数据库了……吗? 并不能,因为模型只是数据结构的定义,并不能通过它操作数据(如果你曾经使用过 [Tortoise ORM](https://tortoise.github.io/),可能会知道 `await Weather.get(location="上海")` 这样的面向对象编程。 但是 SQLAlchemy 不同,选择了命令式编程)。 我们需要使用**会话**操作数据: ```python title=weather/__init__.py {10,13} showLineNumbers from nonebot import on_command from nonebot.adapters import Message from nonebot.params import CommandArg from nonebot_plugin_orm import async_scoped_session weather = on_command("天气") @weather.handle() async def _(session: async_scoped_session, args: Message = CommandArg()): location = args.extract_plain_text() if wea := await session.get(Weather, location): await weather.finish(f"今天{location}的天气是{wea.weather}") await weather.finish(f"未查询到{location}的天气") ``` 我们通过 `session: async_scoped_session` 依赖注入获得了一个会话,然后使用 `await session.get(Weather, location)` 查询数据库。 `async_scoped_session` 是一个有作用域限制的会话,作用域为当前事件、当前事件响应器。 会话产生的模型实例(例如此处的 `wea := await session.get(Weather, location)`)作用域与会话相同。 :::caution 注意 此处提到的“会话”指的是 ORM 会话,而非 [NoneBot 会话](../../../appendices/session-control),两者的生命周期也是不同的(NoneBot 会话的生命周期中可能包含多个事件,不同的事件也会有不同的事件响应器)。 具体而言,就是不要将 ORM 会话和模型实例存储在 NoneBot 会话状态中: ```python {12} from nonebot.params import ArgPlainText from nonebot.typing import T_State @weather.got("location", prompt="请输入地名") async def _(state: T_State, session: async_scoped_session, location: str = ArgPlainText()): wea = await session.get(Weather, location) if not wea: await weather.finish(f"未查询到{location}的天气") state["weather"] = wea # 不要这么做,除非你知道自己在做什么 ``` 当然非要这么做也不是不可以: ```python {6} @weather.handle() async def _(state: T_State, session: async_scoped_session): # 通过 await session.merge(state["weather"]) 获得了此 ORM 会话中的相应模型实例, # 而非直接使用会话状态中的模型实例, # 因为先前的 ORM 会话已经关闭了。 wea = await session.merge(state["weather"]) await weather.finish(f"今天{state['location']}的天气是{wea.weather}") ``` ::: 当有数据更改时,我们需要提交事务,也要注意会话作用域问题: ```python title=weather/__init__.py {12,20} showLineNumbers from nonebot.params import Depends async def get_weather( session: async_scoped_session, args: Message = CommandArg() ) -> Weather: location = args.extract_plain_text() if not (wea := await session.get(Weather, location)): wea = Weather(location=location, weather="未知") session.add(wea) # await session.commit() # 不应该在其他地方提交事务 return wea @weather.handle() async def _(session: async_scoped_session, wea: Weather = Depends(get_weather)): await weather.send(f"今天的天气是{wea.weather}") await session.commit() # 而应该在事件响应器结束前提交事务 ``` 当然我们也可以获得一个新的会话,不过此时就要手动管理会话了: ```python title=weather/__init__.py {5-6} showLineNumbers from nonebot_plugin_orm import get_session async def get_weather(location: str) -> str: session = get_session() async with session.begin(): wea = await session.get(Weather, location) if not wea: wea = Weather(location=location, weather="未知") session.add(wea) return wea.weather @weather.handle() async def _(args: Message = CommandArg()): wea = await get_weather(args.extract_plain_text()) await weather.send(f"今天的天气是{wea}") ``` ### 依赖注入 在上面的示例中,我们都是通过会话获得数据的。 不过,我们也可以通过依赖注入获得数据: ```python title=weather/__init__.py {12-14} showLineNumbers from sqlalchemy import select from nonebot.params import Depends from nonebot_plugin_orm import SQLDepends def extract_arg_plain_text(args: Message = CommandArg()) -> str: return args.extract_plain_text() @weather.handle() async def _( wea: Weather = SQLDepends( select(Weather).where(Weather.location == Depends(extract_arg_plain_text)) ), ): await weather.send(f"今天的天气是{wea.weather}") ``` 其中,`SQLDepends` 是一个特殊的依赖注入,它会根据类型标注和 SQL 语句提供数据,SQL 语句中也可以有子依赖。 不同的类型标注也会获得不同形式的数据: ```python title=weather/__init__.py {5} showLineNumbers from collections.abc import Sequence @weather.handle() async def _( weas: Sequence[Weather] = SQLDepends( select(Weather).where(Weather.weather == Depends(extract_arg_plain_text)) ), ): await weather.send(f"今天的天气是{weas[0].weather}的城市有{','.join(wea.location for wea in weas)}") ``` 支持的类型标注请参见 [依赖注入](dependency)。 我们也可以像 [类作为依赖](../../../advanced/dependency#类作为依赖) 那样,在类属性中声明子依赖: ```python title=weather/__init__.py {5-6,10} showLineNumbers from collections.abc import Sequence class Weather(Model): location: Mapped[str] = mapped_column(primary_key=True) weather: Mapped[str] = Depends(extract_arg_plain_text) # weather: Annotated[Mapped[str], Depends(extract_arg_plain_text)] # Annotated 支持 @weather.handle() async def _(weas: Sequence[Weather]): await weather.send( f"今天的天气是{weas[0].weather}的城市有{','.join(wea.location for wea in weas)}" ) ``` ================================================ FILE: website/versioned_docs/version-2.4.3/best-practice/database/developer/_category_.json ================================================ { "label": "开发者指南", "position": 3 } ================================================ FILE: website/versioned_docs/version-2.4.3/best-practice/database/developer/dependency.md ================================================ --- sidebar_position: 3 description: 依赖注入 --- # 依赖注入 `nonebot-plugin-orm` 提供了强大且灵活的依赖注入,可以方便地帮助你获取数据库会话和查询数据。 ## 数据库会话 ### AsyncSession 新数据库会话,常用于有独立的数据库操作逻辑的插件。 ```python {13,26} from nonebot import on_message from nonebot.params import Depends from nonebot_plugin_orm import AsyncSession, Model, async_scoped_session from sqlalchemy.orm import Mapped, mapped_column message = on_message() class Message(Model): id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) async def get_message(session: AsyncSession) -> Message: # 等价于 session = get_session() async with session: msg = Message() session.add(msg) await session.commit() await session.refresh(msg) return msg @message.handle() async def _(session: async_scoped_session, msg: Message = Depends(get_message)): await session.rollback() # 无法回退 get_message() 中的更改 await message.send(str(msg.id)) # msg 被存储,msg.id 递增 ``` ### async_scoped_session 数据库作用域会话,常用于事件响应器和有与响应逻辑相关的数据库操作逻辑的插件。 ```python {13,26} from nonebot import on_message from nonebot.params import Depends from nonebot_plugin_orm import Model, async_scoped_session from sqlalchemy.orm import Mapped, mapped_column message = on_message() class Message(Model): id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) async def get_message(session: async_scoped_session) -> Message: # 等价于 session = get_scoped_session() msg = Message() session.add(msg) await session.flush() await session.refresh(msg) return msg @message.handle() async def _(session: async_scoped_session, msg: Message = Depends(get_message)): await session.rollback() # 可以回退 get_message() 中的更改 await message.send(str(msg.id)) # msg 没有被存储,msg.id 不变 ``` ## 查询数据 ### Model 支持类作为依赖。 ```python from typing import Annotated from nonebot.params import Depends from nonebot_plugin_orm import Model from sqlalchemy.orm import Mapped, mapped_column def get_id() -> int: ... class Message(Model): id: Annotated[Mapped[int], Depends(get_id)] = mapped_column( primary_key=True, autoincrement=True ) async def _(msg: Message): # 等价于 msg = ( # await (await session.stream(select(Message).where(Message.id == get_id()))) # .scalars() # .one_or_none() # ) ... ``` ### SQLDepends 参数为一个 SQL 语句,决定依赖注入的内容,SQL 语句中可以使用子依赖。 ```python {11-13} from nonebot.params import Depends from nonebot_plugin_orm import Model, SQLDepends from sqlalchemy import select def get_id() -> int: ... async def _( model: Model = SQLDepends(select(Model).where(Model.id == Depends(get_id))), ): ... ``` 参数可以是任意 SQL 语句,但不建议使用 `select` 以外的语句,因为语句可能没有返回值(`returning` 除外),而且代码不清晰。 ### 类型标注 类型标注决定依赖注入的数据结构,主要影响以下几个层面: - 迭代器(`session.execute()`)或异步迭代器(`session.stream()`) - 标量(`session.execute().scalars()`)或元组(`session.execute()`) - 一个(`session.execute().one_or_none()`,注意 `None` 时可能触发 [重载](../../../appendices/overload#重载))或全部(`session.execute()` / `session.execute().all()`) - 连续(`session().execute()`)或分块(`session.execute().partitions()`) 具体如下(可以使用父类型作为类型标注): - ```python async def _(rows_partitions: AsyncIterator[Sequence[Tuple[Model, ...]]]): # 等价于 rows_partitions = await (await session.stream(sql).partitions()) async for partition in rows_partitions: for row in partition: print(row[0], row[1], ...) ``` - ```python async def _(model_partitions: AsyncIterator[Sequence[Model]]): # 等价于 model_partitions = await (await session.stream(sql).scalars().partitions()) async for partition in model_partitions: for model in partition: print(model) ``` - ```python async def _(row_partitions: Iterator[Sequence[Tuple[Model, ...]]]): # 等价于 row_partitions = await session.execute(sql).partitions() for partition in rows_partitions: for row in partition: print(row[0], row[1], ...) ``` - ```python async def _(model_partitions: Iterator[Sequence[Model]]): # 等价于 model_partitions = await (await session.execute(sql).scalars().partitions()) for partition in model_partitions: for model in partition: print(model) ``` - ```python async def _(rows: sa_async.AsyncResult[Tuple[Model, ...]]): # 等价于 rows = await session.stream(sql) async for row in rows: print(row[0], row[1], ...) ``` - ```python async def _(models: sa_async.AsyncScalarResult[Model]): # 等价于 models = await session.stream(sql).scalars() async for model in models: print(model) ``` - ```python async def _(rows: sa.Result[Tuple[Model, ...]]): # 等价于 rows = await session.execute(sql) for row in rows: print(row[0], row[1], ...) ``` - ```python async def _(models: sa.ScalarResult[Model]): # 等价于 models = await session.execute(sql).scalars() for model in models: print(model) ``` - ```python async def _(rows: Sequence[Tuple[Model, ...]]): # 等价于 rows = await (await session.stream(sql).all()) for row in rows: print(row[0], row[1], ...) ``` - ```python async def _(models: Sequence[Model]): # 等价于 models = await (await session.stream(sql).scalars().all()) for model in models: print(model) ``` - ```python async def _(row: Tuple[Model, ...]): # 等价于 row = await (await session.stream(sql).one_or_none()) print(row[0], row[1], ...) ``` - ```python async def _(model: Model): # 等价于 model = await (await session.stream(sql).scalars().one_or_none()) print(model) ``` ================================================ FILE: website/versioned_docs/version-2.4.3/best-practice/database/developer/test.md ================================================ --- sidebar_position: 2 description: 测试 --- # 测试 百思不如一试,测试是发现问题的最佳方式。 不同的用户会有不同的配置,为了提高项目的兼容性,我们需要在不同数据库后端上测试。 手动进行大量的、重复的测试不可靠,也不现实,因此我们推荐使用 [GitHub Actions](https://github.com/features/actions) 进行自动化测试: ```yaml title=.github/workflows/test.yml {12-42,52-53} showLineNumbers name: Test on: push: branches: - main jobs: test: runs-on: ubuntu-latest strategy: matrix: db: - sqlite+aiosqlite:///db.sqlite3 - postgresql+psycopg://postgres:postgres@localhost:5432/postgres - mysql+aiomysql://mysql:mysql@localhost:3306/mymysql fail-fast: false env: SQLALCHEMY_DATABASE_URL: ${{ matrix.db }} services: postgresql: image: ${{ startsWith(matrix.db, 'postgresql') && 'postgres' || '' }} env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: postgres ports: - 5432:5432 mysql: image: ${{ startsWith(matrix.db, 'mysql') && 'mysql' || '' }} env: MYSQL_ROOT_PASSWORD: mysql MYSQL_USER: mysql MYSQL_PASSWORD: mysql MYSQL_DATABASE: mymysql ports: - 3306:3306 steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 - name: Install dependencies run: pip install -r requirements.txt - name: Run migrations run: pipx run nb-cli orm upgrade - name: Run tests run: pytest ``` 如果项目还需要考虑跨平台和跨 Python 版本兼容,测试矩阵中还需要增加这两个维度。 但是,我们没必要在所有平台和 Python 版本上运行所有数据库的测试,因为很显然,PostgreSQL 和 MySQL 这类独立的数据库后端不会受平台和 Python 影响,而且 Github Actions 的非 Linux 平台不支持运行独立服务: | | Python 3.9 | Python 3.10 | Python 3.11 | Python 3.12 | | ----------- | ---------- | ----------- | ----------- | --------------------------- | | **Linux** | SQLite | SQLite | SQLite | SQLite / PostgreSQL / MySQL | | **Windows** | SQLite | SQLite | SQLite | SQLite | | **macOS** | SQLite | SQLite | SQLite | SQLite | ```yaml title=.github/workflows/test.yml {12-24} showLineNumbers name: Test on: push: branches: - main jobs: test: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] python-version: ["3.9", "3.10", "3.11", "3.12"] db: ["sqlite+aiosqlite:///db.sqlite3"] include: - os: ubuntu-latest python-version: "3.12" db: postgresql+psycopg://postgres:postgres@localhost:5432/postgres - os: ubuntu-latest python-version: "3.12" db: mysql+aiomysql://mysql:mysql@localhost:3306/mymysql fail-fast: false env: SQLALCHEMY_DATABASE_URL: ${{ matrix.db }} services: postgresql: image: ${{ startsWith(matrix.db, 'postgresql') && 'postgres' || '' }} env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: postgres ports: - 5432:5432 mysql: image: ${{ startsWith(matrix.db, 'mysql') && 'mysql' || '' }} env: MYSQL_ROOT_PASSWORD: mysql MYSQL_USER: mysql MYSQL_PASSWORD: mysql MYSQL_DATABASE: mymysql ports: - 3306:3306 steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: pip install -r requirements.txt - name: Run migrations run: pipx run nb-cli orm upgrade - name: Run tests run: pytest ``` ================================================ FILE: website/versioned_docs/version-2.4.3/best-practice/database/user.md ================================================ --- sidebar_position: 2 description: 用户指南 --- # 用户指南 `nonebot-plugin-orm` 功能强大且复杂,使用上有一定难度。 不过,对于用户而言,只需要掌握部分功能即可。 :::caution 注意 请注意区分插件的项目名(如:`nonebot-plugin-wordcloud`)和模块名(如:`nonebot_plugin_wordcloud`)。`nonebot-plugin-orm` 中统一使用插件模块名。参见 [插件命名规范](../../developer/plugin-publishing#插件命名规范)。 ::: ## 示例 ### 创建新机器人 我们想要创建一个机器人,并安装 `nonebot-plugin-wordcloud` 插件,只需要执行以下命令: ```shell nb init # 初始化项目文件夹 pip install nonebot-plugin-orm[sqlite] # 安装 nonebot-plugin-orm,并附带 SQLite 支持 nb plugin install nonebot-plugin-wordcloud # 安装插件 # nb orm heads # 查看有什么插件使用到了数据库(可选) nb orm upgrade # 升级数据库 # nb orm check # 检查一下数据库模式是否与模型定义一致(可选) nb run # 启动机器人 ``` ### 卸载插件 我们已经安装了 `nonebot-plugin-wordcloud` 插件,但是现在想要卸载它,并且**删除它的数据**,只需要执行以下命令: ```shell nb plugin uninstall nonebot-plugin-wordcloud # 卸载插件 # nb orm heads # 查看有什么插件使用到了数据库。(可选) nb orm downgrade nonebot_plugin_wordcloud@base # 降级数据库,删除数据 # nb orm check # 检查一下数据库模式是否与模型定义一致(可选) ``` ## CLI 接下来,让我们了解下示例中出现的 CLI 命令的含义: ### heads 显示所有的分支头。一般一个分支对应一个插件。 ```shell nb orm heads ``` 输出格式为 `<迁移 ID> (<插件模块名>) (<头部类型>)`: ``` 46327b837dd8 (nonebot_plugin_chatrecorder) (head) 9492159f98f7 (nonebot_plugin_user) (head) 71a72119935f (nonebot_plugin_session_orm) (effective head) ade8cdca5470 (nonebot_plugin_wordcloud) (head) ``` ### upgrade 升级数据库。每次安装新的插件或更新插件版本后,都需要执行此命令。 ```shell nb orm upgrade <插件模块名>@<迁移 ID> ``` 其中,`<插件模块名>@<迁移 ID>` 是可选参数。如果不指定,则会将所有分支升级到最新版本,这也是最常见的用法: ```shell nb orm upgrade ``` ### downgrade 降级数据库。当需要回滚插件版本或删除插件时,可以执行此命令。 ```shell nb orm downgrade <插件模块名>@<迁移 ID> ``` 其中,`<迁移 ID>` 也可以是 `base`,即回滚到初始状态。常用于卸载插件后删除其数据: ```shell nb orm downgrade <插件模块名>@base ``` ### check 检查数据库模式是否与模型定义一致。机器人启动前会自动运行此命令(`ALEMBIC_STARTUP_CHECK=true` 时),并在检查失败时阻止启动。 ```shell nb orm check ``` ## 配置 ### sqlalchemy_database_url 默认数据库连接 URL。参见 [数据库驱动和后端](.#数据库驱动和后端) 和 [引擎配置 — SQLAlchemy 2.0 文档](https://docs.sqlalchemy.org/en/20/core/engines.html#database-urls)。 ```shell SQLALCHEMY_DATABASE_URL=dialect+driver://username:password@host:port/database ``` ### sqlalchemy_bind bind keys(一般为插件模块名)到数据库连接 URL、[`create_async_engine()`](https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html#sqlalchemy.ext.asyncio.create_async_engine) 参数字典或 [`AsyncEngine`](https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html#sqlalchemy.ext.asyncio.AsyncEngine) 实例的字典。 例如,我们想要让 `nonebot-plugin-wordcloud` 插件使用一个 SQLite 数据库,并开启 [Echo 选项](https://docs.sqlalchemy.org/en/20/core/engines.html#sqlalchemy.create_engine.params.echo) 便于 debug,而其他插件使用默认的 PostgreSQL 数据库,可以这样配置: ```shell SQLALCHEMY_BINDS='{ "": "postgresql+psycopg://scott:tiger@localhost/mydatabase", "nonebot_plugin_wordcloud": { "url": "sqlite+aiosqlite://", "echo": true } }' ``` ### sqlalchemy_engine_options [`create_async_engine()`](https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html#sqlalchemy.ext.asyncio.create_async_engine) 默认参数字典。 ```shell SQLALCHEMY_ENGINE_OPTIONS='{ "pool_size": 5, "max_overflow": 10, "pool_timeout": 30, "pool_recycle": 3600, "echo": true }' ``` ### sqlalchemy_echo 开启 [Echo 选项](https://docs.sqlalchemy.org/en/20/core/engines.html#sqlalchemy.create_engine.params.echo) 和 [Echo Pool 选项](https://docs.sqlalchemy.org/en/20/core/engines.html#sqlalchemy.create_engine.params.echo_pool) 便于 debug。 ```shell SQLALCHEMY_ECHO=true ``` :::caution 注意 以上配置之间有覆盖关系,遵循特殊优先于一般的原则,具体为 [`sqlalchemy_database_url`](#sqlalchemy_database_url) > [`sqlalchemy_bind`](#sqlalchemy_bind) > [`sqlalchemy_echo`](#sqlalchemy_echo) > [`sqlalchemy_engine_options`](#sqlalchemy_engine_options)。 但覆盖顺序并非显而易见,出于清晰考虑,请只配置必要的选项。 ::: ================================================ FILE: website/versioned_docs/version-2.4.3/best-practice/deployment.mdx ================================================ --- sidebar_position: 3 description: 部署你的机器人 --- # 部署 import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; 在编写完成各类插件后,我们需要长期运行机器人来使得用户能够正常使用。通常,我们会使用云服务器来部署机器人。 我们在开发插件时,机器人运行的环境称为开发环境;而在部署后,机器人运行的环境称为生产环境。与开发环境不同的是,在生产环境中,开发者通常不能随意地修改/添加/删除代码,开启或停止服务。 ## 部署前准备 ### 项目依赖管理 由于部署后的机器人运行在生产环境中,因此,为确保机器人能够正常运行,我们需要保证机器人的运行环境与开发环境一致。我们可以通过以下几种方式来进行依赖管理: [Poetry](https://python-poetry.org/) 是一个 Python 项目的依赖管理工具。它可以通过声明项目所依赖的库,为你管理(安装/更新)它们。Poetry 提供了一个 `poetry.lock` 文件,以确保可重复安装,并可以构建用于分发的项目。 Poetry 会在安装依赖时自动生成 `poetry.lock` 文件,在**项目目录**下执行以下命令: ```bash # 初始化 poetry 配置 poetry init # 添加项目依赖,这里以 nonebot2[fastapi] 为例 poetry add nonebot2[fastapi] ``` [PDM](https://pdm.fming.dev/) 是一个现代 Python 项目的依赖管理工具。它采用 [PEP621](https://www.python.org/dev/peps/pep-0621/) 标准,依赖解析快速;同时支持 [PEP582](https://www.python.org/dev/peps/pep-0582/) 和 [virtualenv](https://virtualenv.pypa.io/)。PDM 提供了一个 `pdm.lock` 文件,以确保可重复安装,并可以构建用于分发的项目。 PDM 会在安装依赖时自动生成 `pdm.lock` 文件,在**项目目录**下执行以下命令: ```bash # 初始化 pdm 配置 pdm init # 添加项目依赖,这里以 nonebot2[fastapi] 为例 pdm add nonebot2[fastapi] ``` [pip](https://pip.pypa.io/) 是 Python 包管理工具。他并不是一个依赖管理工具,为了尽可能保证环境的一致性,我们可以使用 `requirements.txt` 文件来声明依赖。 ```bash pip freeze > requirements.txt ``` ### 安装 Docker [Docker](https://www.docker.com/) 是一个应用容器引擎,可以让开发者打包应用以及依赖包到一个可移植的镜像中,然后发布到服务器上。 我们可以参考 [Docker 官方文档](https://docs.docker.com/get-docker/) 来安装 Docker 。 在 Linux 上,我们可以使用以下一键脚本来安装 Docker 以及 Docker Compose Plugin: ```bash curl -fsSL https://get.docker.com | sh -s -- --mirror Aliyun ``` 在 Windows/macOS 上,我们可以使用 [Docker Desktop](https://docs.docker.com/desktop/) 来安装 Docker 以及 Docker Compose Plugin。 ### 安装脚手架 Docker 插件 我们可以使用 [nb-cli-plugin-docker](https://github.com/nonebot/cli-plugin-docker) 来快速部署机器人。 插件可以帮助我们生成配置文件并构建 Docker 镜像,以及启动/停止/重启机器人。使用以下命令安装脚手架 Docker 插件: ```bash nb self install nb-cli-plugin-docker ``` ## Docker 部署 ### 快速部署 使用脚手架命令即可一键生成配置并部署: ```bash nb docker up ``` 当看到 `Running` 字样时,说明机器人已经启动成功。我们可以通过以下命令来查看机器人的运行日志: ```bash nb docker logs ``` ```bash docker compose logs ``` 如果需要停止机器人,我们可以使用以下命令: ```bash nb docker down ``` ```bash docker compose down ``` ### 自定义部署 在部分情况下,我们需要事先生成 Docker 配置文件,再到生产环境进行部署;或者自动生成的配置文件并不能满足复杂场景,需要根据实际需求手动修改配置文件。我们可以使用以下命令来生成基础配置文件: ```bash nb docker generate ``` nb-cli 将会在项目目录下生成 `docker-compose.yml` 和 `Dockerfile` 等配置文件。在 nb-cli 完成配置文件的生成后,我们可以根据部署环境的实际情况使用 nb-cli 或者 Docker Compose 来启动机器人。 我们可以参考 [Dockerfile 文件规范](https://docs.docker.com/engine/reference/builder/)和 [Compose 文件规范](https://docs.docker.com/compose/compose-file/)修改这两个文件。 修改完成后我们可以直接启动或者手动构建镜像: ```bash # 启动机器人 nb docker up # 手动构建镜像 nb docker build ``` ```bash # 启动机器人 docker compose up -d # 手动构建镜像 docker compose build ``` ### 持续集成 我们可以使用 GitHub Actions 来实现持续集成(CI),我们只需要在 GitHub 上发布 Release 即可自动构建镜像并推送至镜像仓库。 首先,我们需要在 [Docker Hub](https://hub.docker.com/) (或者其他平台,如:[GitHub Packages](https://github.com/features/packages)、[阿里云容器镜像服务](https://www.alibabacloud.com/zh/product/container-registry)等)上创建镜像仓库,用于存放镜像。 前往项目仓库的 `Settings` > `Secrets` > `actions` 栏目 `New Repository Secret` 添加构建所需的密钥: - `DOCKERHUB_USERNAME`: 你的 Docker Hub 用户名 - `DOCKERHUB_TOKEN`: 你的 Docker Hub PAT([创建方法](https://docs.docker.com/docker-hub/access-tokens/)) 将以下文件添加至**项目目录**下的 `.github/workflows/` 目录下,并将文件中高亮行中的仓库名称替换为你的仓库名称: ```yaml title=.github/workflows/build.yml name: Docker Hub Release on: push: tags: - "v*" jobs: docker: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 - name: Set up QEMU uses: docker/setup-qemu-action@v2 - name: Setup Docker uses: docker/setup-buildx-action@v2 - name: Login to DockerHub uses: docker/login-action@v2 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Generate Tags uses: docker/metadata-action@v4 id: metadata with: images: | # highlight-next-line {organization}/{repository} tags: | type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=sha type=raw,value=latest - name: Build and Publish uses: docker/build-push-action@v4 with: context: . push: true tags: ${{ steps.metadata.outputs.tags }} labels: ${{ steps.metadata.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max ``` ### 持续部署 在完成发布并构建镜像后,我们可以自动将镜像部署到服务器上。 前往项目仓库的 `Settings` > `Secrets` > `actions` 栏目 `New Repository Secret` 添加部署所需的密钥: - `DEPLOY_HOST`: 部署服务器的 SSH 地址 - `DEPLOY_USER`: 部署服务器用户名 - `DEPLOY_KEY`: 部署服务器私钥([创建方法](https://github.com/appleboy/ssh-action#setting-up-a-ssh-key)) - `DEPLOY_PATH`: 部署服务器上的项目路径 将以下文件添加至**项目目录**下的 `.github/workflows/` 目录下,在构建成功后触发部署: ```yaml title=.github/workflows/deploy.yml name: Deploy on: workflow_run: workflows: - Docker Hub Release types: - completed jobs: deploy: runs-on: ubuntu-latest if: ${{ github.event.workflow_run.conclusion == 'success' }} steps: - name: Start Deployment uses: bobheadxi/deployments@v1 id: deployment with: step: start token: ${{ secrets.GITHUB_TOKEN }} env: bot - name: Run Remote SSH Command uses: appleboy/ssh-action@master env: DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }} with: host: ${{ secrets.DEPLOY_HOST }} username: ${{ secrets.DEPLOY_USER }} key: ${{ secrets.DEPLOY_KEY }} envs: DEPLOY_PATH script: | cd $DEPLOY_PATH docker compose up -d --pull always - name: update deployment status uses: bobheadxi/deployments@v0.6.2 if: always() with: step: finish token: ${{ secrets.GITHUB_TOKEN }} status: ${{ job.status }} env: ${{ steps.deployment.outputs.env }} deployment_id: ${{ steps.deployment.outputs.deployment_id }} ``` 将上一部分的 `docker-compose.yml` 文件以及 `.env.prod` 配置文件添加至 `DEPLOY_PATH` 目录下,并修改 `docker-compose.yml` 文件中的镜像配置,替换为 Docker Hub 的仓库名称: ```diff - build: . + image: {organization}/{repository}:latest ``` ================================================ FILE: website/versioned_docs/version-2.4.3/best-practice/error-tracking.md ================================================ --- sidebar_position: 2 description: 使用 sentry 进行错误跟踪 --- # 错误跟踪 在应用实际运行过程中,可能会出现各种各样的错误。可能是由于代码逻辑错误,也可能是由于用户输入错误,甚至是由于第三方服务的错误。这些错误都会导致应用的运行出现问题,这时候就需要对错误进行跟踪,以便及时发现问题并进行修复。NoneBot 提供了 `nonebot-plugin-sentry` 插件,支持 [sentry](https://sentry.io/) 平台,可以方便地进行错误跟踪。 ## 安装插件 在使用前请先安装 `nonebot-plugin-sentry` 插件至项目环境中,可参考[获取商店插件](../tutorial/store.mdx#安装插件)来了解并选择安装插件的方式。如: 在**项目目录**下执行以下命令: ```bash nb plugin install nonebot-plugin-sentry ``` ## 使用插件 在安装完成之后,仅需要对插件进行简单的配置即可使用。 ### 获取 sentry DSN 前往 [sentry](https://sentry.io/) 平台,注册并创建一个新的项目,然后在项目设置中找到 `Client Keys (DSN)`,复制其中的 `DSN` 值。 ### 配置插件 :::caution 注意 错误跟踪通常在生产环境中使用,因此开发环境中 `sentry_dsn` 留空即会停用插件。 ::: 在项目 dotenv 配置文件中添加以下配置即可使用: ```dotenv SENTRY_DSN= ``` ## 配置项 配置项具体含义参考 [Sentry Docs](https://docs.sentry.io/platforms/python/configuration/options/)。 - `sentry_dsn: str` - `sentry_debug: bool = False` - `sentry_release: str | None = None` - `sentry_release: str | None = None` - `sentry_environment: str | None = nonebot env` - `sentry_server_name: str | None = None` - `sentry_sample_rate: float = 1.` - `sentry_max_breadcrumbs: int = 100` - `sentry_attach_stacktrace: bool = False` - `sentry_send_default_pii: bool = False` - `sentry_in_app_include: List[str] = Field(default_factory=list)` - `sentry_in_app_exclude: List[str] = Field(default_factory=list)` - `sentry_request_bodies: str = "medium"` - `sentry_with_locals: bool = True` - `sentry_ca_certs: str | None = None` - `sentry_before_send: Callable[[Any, Any], Any | None] | None = None` - `sentry_before_breadcrumb: Callable[[Any, Any], Any | None] | None = None` - `sentry_transport: Any | None = None` - `sentry_http_proxy: str | None = None` - `sentry_https_proxy: str | None = None` - `sentry_shutdown_timeout: int = 2` ================================================ FILE: website/versioned_docs/version-2.4.3/best-practice/htmlkit-render.md ================================================ --- sidebar_position: 8 description: 轻量化 HTML 绘图 --- # 轻量化 HTML 绘图 图片是机器人交互中不可或缺的一部分,对于信息展示的直观性、美观性有很大的作用。 基于 PIL 直接绘制图片具有良好的性能和存储开销,但是难以调试、维护过程式的绘图代码。 使用浏览器渲染类插件可以方便地绘制网页,且能够直接通过 JS 对网页效果进行编程,但是它占用的存储和内存空间相对可观。 NoneBot 提供的 `nonebot-plugin-htmlkit` 提供了另一种基于 HTML 和 CSS 语法的轻量化绘图选择:它基于 `litehtml` 解析库,无须安装额外的依赖即可使用,没有进程间通信带来的额外开销,且在支持 `webp` `avif` 等丰富图片格式的前提下,安装用的 wheel 文件大小仅有约 10 MB。 作为粗略的性能参考,在一台 Ryzen 7 9700X 的 Windows 电脑上,渲染 [PEP 7](https://peps.python.org/pep-0007/) 的 HTML 页面(分辨率为 800x5788,大小约 1.4MB,从本地文件系统读取 CSS)大约需要 100ms,每个渲染任务内存最高占用约为 40MB. ## 安装插件 在使用前请先安装 `nonebot-plugin-htmlkit` 插件至项目环境中,可参考[获取商店插件](../tutorial/store.mdx#安装插件)来了解并选择安装插件的方式。如: 在**项目目录**下执行以下命令: ```bash nb plugin install nonebot-plugin-htmlkit ``` `nonebot-plugin-htmlkit` 插件目前兼容以下系统架构: - Windows x64 - macOS arm64(M-系列芯片) - Linux x64 (非 Alpine 等 musl 系发行版) - Linux arm64 (非 Alpine 等 musl 系发行版) :::caution 访问网络内容 如果需要访问网络资源(如 http(s) 网页内容),NoneBot 需要客户端型驱动器(Forward)。内置的驱动器有 `~httpx` 与 `~aiohttp`。 详见[选择驱动器](../advanced/driver.md)。 ::: ## 使用插件 ### 加载插件 在使用本插件前同样需要使用 `require` 方法进行**加载**并**导入**需要使用的方法,可参考 [跨插件访问](../advanced/requiring.md) 一节进行了解,如: ```python from nonebot import require require("nonebot_plugin_htmlkit") from nonebot_plugin_htmlkit import html_to_pic, md_to_pic, template_to_pic, text_to_pic ``` 插件会自动使用[配置中的参数](#配置-fontconfig)初始化 `fontconfig` 以提供字体查找功能。 ### 渲染 API `nonebot-plugin-htmlkit` 主要提供以下**异步**渲染函数: #### html_to_pic ```python async def html_to_pic( html: str, *, base_url: str = "", dpi: float = 144.0, max_width: float = 800.0, device_height: float = 600.0, default_font_size: float = 12.0, font_name: str = "sans-serif", allow_refit: bool = True, image_format: Literal["png", "jpeg"] = "png", jpeg_quality: int = 100, lang: str = "zh", culture: str = "CN", img_fetch_fn: ImgFetchFn = combined_img_fetcher, css_fetch_fn: CSSFetchFn = combined_css_fetcher, urljoin_fn: Callable[[str, str], str] = urllib3.parse.urljoin, ) -> bytes: ... ``` 最核心的渲染函数。 `base_url` 和 `urljoin_fn` 控制着传入 `image_fetch_fn` 和 `css_fetch_fn` 回调的 url 内容。 `allow_refit` 如果为真,渲染时会自动缩小产出图片的宽度到最适合的宽度,否则必定产出 `max_width` 宽度的图片。 `max_width` 与 `device_height` 会在 `@media` 判断中被使用。 `img_fetch_fn` 预期为一个异步可调用对象(函数),接收图片 url 并返回对应 url 的 jpeg 或 png 二进制数据(`bytes`),可在拒绝加载时返回 `None`. `css_fetch_fn` 预期为一个异步可调用对象(函数),接收目标 CSS url 并返回对应 url 的 CSS 文本(`str`),可在拒绝加载时返回 `None`. 以下为辅助的封装函数,关键字参数若未特殊说明均与 `html_to_pic` 含义相同。 #### text_to_pic ```python async def text_to_pic( text: str, css_path: str = "", *, max_width: int = 500, allow_refit: bool = True, image_format: Literal["png", "jpeg"] = "png", jpeg_quality: int = 100, ) -> bytes: ... ``` 可用于渲染多行文本。 `text` 会被放置于 `
` 中,可据此编写 CSS 来改变文本表现。 #### md_to_pic ```python async def md_to_pic( md: str = "", md_path: str = "", css_path: str = "", *, max_width: int = 500, img_fetch_fn: ImgFetchFn = combined_img_fetcher, allow_refit: bool = True, image_format: Literal["png", "jpeg"] = "png", jpeg_quality: int = 100, ) -> bytes: ... ``` 可用于渲染 Markdown 文本。默认为 GitHub Markdown Light 风格,支持基于 `pygments` 的代码高亮。 `md` 和 `md_path` 二选一,前者设置时应为 Markdown 的文本,后者设置时应为指向 Markdown 文本文件的路径。 #### template_to_pic ```python async def template_to_pic( template_path: str | PathLike[str] | Sequence[str | PathLike[str]], template_name: str, templates: Mapping[Any, Any], filters: None | Mapping[str, Any] = None, *, max_width: int = 500, device_height: int = 600, base_url: str | None = None, img_fetch_fn: ImgFetchFn = combined_img_fetcher, css_fetch_fn: CSSFetchFn = combined_css_fetcher, allow_refit: bool = True, image_format: Literal["png", "jpeg"] = "png", jpeg_quality: int = 100, ) -> bytes: ... ``` 渲染 jinja2 模板。 `template_path` 为 jinja2 环境的路径,`template_name` 是环境中要加载模板的名字,`templates` 为传入模板的参数,`filters` 为过滤器名 -> 自定义过滤器的映射。 ### 控制外部资源获取 通过传入 `img_fetch_fn` 与 `css_fetch_fn`,我们可以在实际访问资源前进行审查,修改资源的来源,或是对 IO 操作进行缓存。 `img_fetch_fn` 预期为一个异步可调用对象(函数),接收图片 url 并返回对应 url 的 jpeg 或 png 二进制数据(`bytes`),可在拒绝加载时返回 `None`. `css_fetch_fn` 预期为一个异步可调用对象(函数),接收目标 CSS url 并返回对应 url 的 CSS 文本(`str`),可在拒绝加载时返回 `None`. 如果你想要禁用外部资源加载/只从文件系统加载/只从网络加载,可以使用 `none_fetcher` `filesystem_***_fetcher` `network_***_fetcher`。 默认的 fetcher 行为(对于 `file://` 从文件系统加载,其余从网络加载)位于 `combined_***_fetcher`,可以通过对其封装实现缓存等操作。 ## 配置项 ### 配置 fontconfig `htmlkit` 使用 `fontconfig` 查找字体,请参阅 [`fontconfig 用户手册`](https://fontconfig.pages.freedesktop.org/fontconfig/fontconfig-user) 了解环境变量的具体含义、如何通过编写配置文件修改字体配置等。 #### fontconfig_file - **类型**: `str | None` - **默认值**: `None` 覆盖默认的配置文件路径。 #### fontconfig_path - **类型**: `str | None` - **默认值**: `None` 覆盖默认的配置目录。 #### fontconfig_sysroot - **类型**: `str | None` - **默认值**: `None` 覆盖默认的 sysroot。 #### fc_debug - **类型**: `str | None` - **默认值**: `None` 设置 Fontconfig 的 debug 级别。 #### fc_dbg_match_filter - **类型**: `str | None` - **默认值**: `None` 当 `FC_DEBUG` 设置为 `MATCH2` 时,过滤 debug 输出。 #### fc_lang - **类型**: `str | None` - **默认值**: `None` 设置默认语言,否则从 `LOCALE` 环境变量获取。 #### fontconfig_use_mmap - **类型**: `str | None` - **默认值**: `None` 是否使用 `mmap(2)` 读取字体缓存。 ================================================ FILE: website/versioned_docs/version-2.4.3/best-practice/multi-adapter.mdx ================================================ --- sidebar_position: 4 description: 插件跨平台支持 --- # 插件跨平台支持 import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; ## 使用 NoneBot 本身 由于不同平台的事件与接口之间,存在着极大的差异性,NoneBot 通过[重载](../appendices/overload.md)的方式,使得插件可以在不同平台上正确响应。但为了减少跨平台的兼容性问题,我们应该尽可能的使用基类方法实现原生跨平台,而不是使用特定平台的方法。当基类方法无法满足需求时,我们可以使用依赖注入的方式,将特定平台的事件或机器人注入到事件处理函数中,实现针对特定平台的处理。 :::tip 提示 如果需要在多平台上**使用**跨平台插件,首先应该根据[注册适配器](../advanced/adapter.md#注册适配器)一节,为机器人注册各平台对应的适配器。 ::: ### 基于基类的跨平台 在[事件通用信息](../advanced/adapter.md#获取事件通用信息)中,我们了解了事件基类能够提供的通用信息。同时,[事件响应器操作](../appendices/session-control.mdx#更多事件响应器操作)也为我们提供了基本的用户交互方式。使用这些方法,可以让我们的插件运行在任何平台上。例如,一个简单的命令处理插件: ```python {5,11} from nonebot import on_command from nonebot.adapters import Event async def is_blacklisted(event: Event) -> bool: return event.get_user_id() not in BLACKLIST weather = on_command("天气", rule=is_blacklisted, priority=10, block=True) @weather.handle() async def handle_function(): await weather.finish("今天的天气是...") ``` 由于此插件仅使用了事件通用信息和事件响应器操作的纯文本交互方式,这些方法不使用特定平台的信息或接口,因此是原生跨平台的,并不需要额外处理。但在一些较为复杂的需求下,例如发送图片消息时,并非所有平台都具有统一的接口,因此基类便无能为力,我们需要引入特定平台的适配器了。 ### 基于重载的跨平台 重载是 NoneBot 跨平台操作的核心,在[事件类型与重载](../appendices/overload.md#重载)一节中,我们初步了解了如何通过类型注解来实现针对不同平台事件的处理方式。在[依赖注入](../advanced/dependency.mdx)一节中,我们又对依赖注入的使用方法进行了详细的介绍。结合这两节内容,我们可以实现更复杂的跨平台操作。 #### 处理近似事件 对于一系列**差异不大**的事件,我们往往具有相同的处理逻辑。这时,我们不希望将相同的逻辑编写两遍,而应该复用代码,以实现在同一个事件处理函数中处理多个近似事件。我们可以使用[事件重载](../advanced/dependency.mdx#event)的特性来实现这一功能。例如: ```python from nonebot import on_command from nonebot.adapters import Message from nonebot.params import CommandArg from nonebot.adapters.onebot.v11 import MessageEvent as OnebotV11MessageEvent from nonebot.adapters.onebot.v12 import MessageEvent as OnebotV12MessageEvent echo = on_command("echo", priority=10, block=True) @echo.handle() async def handle_function(event: OnebotV11MessageEvent | OnebotV12MessageEvent, args: Message = CommandArg()): await echo.finish(args) ``` ```python from typing import Union from nonebot import on_command from nonebot.adapters import Message from nonebot.params import CommandArg from nonebot.adapters.onebot.v11 import MessageEvent as OnebotV11MessageEvent from nonebot.adapters.onebot.v12 import MessageEvent as OnebotV12MessageEvent echo = on_command("echo", priority=10, block=True) @echo.handle() async def handle_function(event: Union[OnebotV11MessageEvent, OnebotV12MessageEvent], args: Message = CommandArg()): await echo.finish(args) ``` #### 在依赖注入中使用重载 NoneBot 依赖注入系统提供了自定义子依赖的方法,子依赖的类型同样会影响到事件处理函数的重载行为。例如: ```python from datetime import datetime from nonebot import on_command from nonebot.adapters.console import MessageEvent echo = on_command("echo", priority=10, block=True) def get_event_time(event: MessageEvent): return event.time # 处理控制台消息事件 @echo.handle() async def handle_function(time: datetime = Depends(get_event_time)): await echo.finish(time.strftime("%Y-%m-%d %H:%M:%S")) ``` 示例中 ,我们为 `handle_function` 事件处理函数注入了自定义的 `get_event_time` 子依赖,而此子依赖注入参数为 Console 适配器的 `MessageEvent`。因此 `handle_function` 仅会响应 Console 适配器的 `MessageEvent` ,而不能响应其他事件。 #### 处理多平台事件 不同平台的事件之间,往往存在着极大的差异性。为了满足我们插件的跨平台运行,通常我们需要抽离业务逻辑,以保证代码的复用性。一个合理的做法是,在事件响应器的处理流程中,首先先针对不同平台的事件分别进行处理,提取出核心业务逻辑所需要的信息;然后再将这些信息传递给业务逻辑处理函数;最后将业务逻辑的输出以各平台合适的方式返回给用户。也就是说,与平台绑定的处理部分应该与平台无关部分尽量分离。例如: ```python import inspect from nonebot import on_command from nonebot.typing import T_State from nonebot.matcher import Matcher from nonebot.adapters import Message from nonebot.params import CommandArg, ArgPlainText from nonebot.adapters.console import Bot as ConsoleBot from nonebot.adapters.onebot.v11 import Bot as OnebotBot from nonebot.adapters.console import MessageSegment as ConsoleMessageSegment weather = on_command("天气", priority=10, block=True) @weather.handle() async def handle_function(matcher: Matcher, args: Message = CommandArg()): if args.extract_plain_text(): matcher.set_arg("location", args) async def get_weather(state: T_State, location: str = ArgPlainText()): if location not in ["北京", "上海", "广州", "深圳"]: await weather.reject(f"你想查询的城市 {location} 暂不支持,请重新输入!") state["weather"] = "⛅ 多云 20℃~24℃" # 处理控制台询问 @weather.got( "location", prompt=ConsoleMessageSegment.emoji("question") + "请输入地名", parameterless=[Depends(get_weather)], ) async def handle_console(bot: ConsoleBot): pass # 处理 OneBot 询问 @weather.got( "location", prompt="请输入地名", parameterless=[Depends(get_weather)], ) async def handle_onebot(bot: OnebotBot): pass # 通过依赖注入或事件处理函数来进行业务逻辑处理 # 处理控制台回复 @weather.handle() async def handle_console_reply(bot: ConsoleBot, state: T_State, location: str = ArgPlainText()): await weather.send( ConsoleMessageSegment.markdown( inspect.cleandoc( f""" # {location} - 今天 {state['weather']} """ ) ) ) # 处理 OneBot 回复 @weather.handle() async def handle_onebot_reply(bot: OnebotBot, state: T_State, location: str = ArgPlainText()): await weather.send(f"今天{location}的天气是{state['weather']}") ``` ## 使用插件 得益于众多开发者为 NoneBot 社区做出的贡献,我们可以通过一系列插件来完成跨平台插件的开发。 这些插件可以分为三类: ### 事件处理 - [all4one](https://github.com/nonepkg/nonebot-plugin-all4one): 将不同平台的事件转为符合 OneBot V12 协议的插件 - 支持的适配器: OneBot V11/V12, Discord, QQ, Telegram ### 消息处理 - [alconna](https://github.com/nonebot/plugin-alconna): 对几乎所有适配器中消息的收发、撤回、编辑、表态的统一插件 - 支持的适配器: OneBot V11/V12, Telegram, Feishu, Github, QQ, Ding, Console, Kaiheila, Mirai, NtChat, Minecraft, Discord, Satori, Red, Dodo, Kritor, Tailchat, Mail, WXMP, Heybox, Gewechat - [send-anything-anywhere](https://github.com/felinae98/nonebot-plugin-send-anything-anywhere): 帮助处理不同适配器消息的适配和发送的插件 - 支持的适配器: OneBot V11/V12, Kaiheila, Telegram, Feishu, Red, DoDo, Satori, QQ, Discord ### 会话信息提取 - [uninfo](https://github.com/RF-Tar-Railt/nonebot-plugin-uninfo): 多平台的会话信息(用户、群组、频道)获取插件 - 支持的适配器: OneBot V11/V12, Telegram, Feishu, QQ, Console, Kaiheila, Mirai, Minecraft, Discord, Satori, Dodo, Kritor, Mail, WXMP, Gewechat - [session](https://github.com/noneplugin/nonebot-plugin-session): 会话信息提取与会话 id 定义插件 - 支持的适配器: OneBot V11/V12, Console, Kaiheila, Telegram, Feishu, Red, DoDo, Satori, QQ, Discord - [userinfo](https://github.com/noneplugin/nonebot-plugin-userinfo): 用户信息获取插件 - 支持的适配器: OneBot V11/V12, Console, Kaiheila, Telegram, Feishu, Red, DoDo, Satori, QQ, Discord ================================================ FILE: website/versioned_docs/version-2.4.3/best-practice/scheduler.md ================================================ --- sidebar_position: 0 description: 定时执行任务 --- # 定时任务 [APScheduler](https://apscheduler.readthedocs.io/en/3.x/) (Advanced Python Scheduler) 是一个 Python 第三方库,其强大的定时任务功能被广泛应用于各个场景。在 NoneBot 中,定时任务作为一个额外功能,依赖于基于 APScheduler 开发的 [`nonebot-plugin-apscheduler`](https://github.com/nonebot/plugin-apscheduler) 插件进行支持。 ## 安装插件 在使用前请先安装 `nonebot-plugin-apscheduler` 插件至项目环境中,可参考[获取商店插件](../tutorial/store.mdx#安装插件)来了解并选择安装插件的方式。如: 在**项目目录**下执行以下命令: ```bash nb plugin install nonebot-plugin-apscheduler ``` ## 使用插件 `nonebot-plugin-apscheduler` 本质上是对 [APScheduler](https://apscheduler.readthedocs.io/en/3.x/) 进行了封装以适用于 NoneBot 开发,因此其使用方式与 APScheduler 本身并无显著区别。在此我们会简要介绍其调用方法,更多的使用方面的功能请参考[APScheduler 官方文档](https://apscheduler.readthedocs.io/en/3.x/userguide.html)。 ### 导入调度器 由于 `nonebot_plugin_apscheduler` 作为插件,因此需要在使用前对其进行**加载**并**导入**其中的 `scheduler` 调度器来创建定时任务。使用 `require` 方法可轻松完成这一过程,可参考 [跨插件访问](../advanced/requiring.md) 一节进行了解。 ```python from nonebot import require require("nonebot_plugin_apscheduler") from nonebot_plugin_apscheduler import scheduler ``` ### 添加定时任务 在 [APScheduler 官方文档](https://apscheduler.readthedocs.io/en/3.x/userguide.html#adding-jobs) 中提供了以下两种直接添加任务的方式: ```python from nonebot import require require("nonebot_plugin_apscheduler") from nonebot_plugin_apscheduler import scheduler # 基于装饰器的方式 @scheduler.scheduled_job("cron", hour="*/2", id="job_0", args=[1], kwargs={arg2: 2}) async def run_every_2_hour(arg1: int, arg2: int): pass # 基于 add_job 方法的方式 def run_every_day(arg1: int, arg2: int): pass scheduler.add_job( run_every_day, "interval", days=1, id="job_1", args=[1], kwargs={arg2: 2} ) ``` :::caution 注意 由于 APScheduler 的定时任务并不是**由事件响应器所触发的事件**,因此其任务函数无法同[事件处理函数](../tutorial/handler.mdx#事件处理函数)一样通过[依赖注入](../tutorial/event-data.mdx#认识依赖注入)获取上下文信息,也无法通过事件响应器对象的方法进行任何操作,因此我们需要使用[调用平台 API](../appendices/api-calling.mdx#调用平台-api)的方式来获取信息或收发消息。 相对于事件处理依赖而言,编写定时任务更像是编写普通的函数,需要我们自行获取信息以及发送信息,请**不要**将事件处理依赖的特殊语法用于定时任务! ::: 关于 APScheduler 的更多使用方法,可以参考 [APScheduler 官方文档](https://apscheduler.readthedocs.io/en/3.x/index.html) 进行了解。 ### 配置项 #### apscheduler_autostart - **类型**: `bool` - **默认值**: `True` 是否自动启动 `scheduler` ,若不启动需要自行调用 `scheduler.start()`。 #### apscheduler_log_level - **类型**: `int` - **默认值**: `30` apscheduler 输出的日志等级 - `WARNING` = `30` (默认) - `INFO` = `20` - `DEBUG` = `10` (只有在开启 nonebot 的 debug 模式才会显示 debug 日志) #### apscheduler_config - **类型**: `dict` - **默认值**: `{ "apscheduler.timezone": "Asia/Shanghai" }` `apscheduler` 的相关配置。参考[配置调度器](https://apscheduler.readthedocs.io/en/latest/userguide.html#scheduler-config), [配置参数](https://apscheduler.readthedocs.io/en/latest/modules/schedulers/base.html#apscheduler.schedulers.base.BaseScheduler) 配置需要包含 `apscheduler.` 作为前缀,例如 `apscheduler.timezone`。 ================================================ FILE: website/versioned_docs/version-2.4.3/best-practice/testing/README.mdx ================================================ --- sidebar_position: 1 description: 使用 NoneBug 进行单元测试 slug: /best-practice/testing/ --- # 配置与测试事件响应器 import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; > 在计算机编程中,单元测试(Unit Testing)又称为模块测试,是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。 为了保证代码的正确运行,我们不仅需要对错误进行跟踪,还需要对代码进行正确性检验,也就是测试。NoneBot 提供了一个测试工具——NoneBug,它是一个 [pytest](https://docs.pytest.org/en/stable/) 插件,可以帮助我们便捷地进行单元测试。 :::tip 提示 建议在阅读本文档前先阅读 [pytest 官方文档](https://docs.pytest.org/en/stable/)来了解 pytest 的相关术语和基本用法。 ::: ## 安装 NoneBug 在**项目目录**下激活虚拟环境后运行以下命令安装 NoneBug: ```bash poetry add nonebug -G test ``` ```bash pdm add nonebug -dG test ``` ```bash pip install nonebug ``` 要运行 NoneBug 测试,还需要额外安装 pytest 异步插件 `pytest-asyncio` 或 `anyio` 以支持异步测试。文档中,我们以 `pytest-asyncio` 为例: ```bash poetry add pytest-asyncio -G test ``` ```bash pdm add pytest-asyncio -dG test ``` ```bash pip install pytest-asyncio ``` ## 配置测试 在开始测试之前,我们需要对测试进行一些配置,以正确启动我们的机器人。 首先我们需要配置 pytest-asyncio,在 `pyproject.toml` 的 pytest 配置部分添加: ```toml [tool.pytest.ini_options] asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "session" ``` 然后,我们在 `tests` 目录下新建 `conftest.py` 文件,添加以下内容: ```python title=tests/conftest.py import pytest import nonebot from pytest_asyncio import is_async_test # 导入适配器 from nonebot.adapters.console import Adapter as ConsoleAdapter def pytest_collection_modifyitems(items: list[pytest.Item]): pytest_asyncio_tests = (item for item in items if is_async_test(item)) session_scope_marker = pytest.mark.asyncio(loop_scope="session") for async_test in pytest_asyncio_tests: async_test.add_marker(session_scope_marker, append=False) @pytest.fixture(scope="session", autouse=True) async def after_nonebot_init(after_nonebot_init: None): # 加载适配器 driver = nonebot.get_driver() driver.register_adapter(ConsoleAdapter) # 加载插件 nonebot.load_from_toml("pyproject.toml") ``` 这样,我们就可以在测试中使用机器人的插件了。通常,我们不需要自行初始化 NoneBot,NoneBug 已经为我们运行了 `nonebot.init()`。如果需要自定义 NoneBot 初始化的参数,我们可以在 `conftest.py` 中添加 `pytest_configure` 钩子函数。例如,我们可以修改 NoneBot 配置环境为 `test` 并从环境变量中输入配置: ```python {4,6,8-10} title=tests/conftest.py import os import pytest from nonebug import NONEBOT_INIT_KWARGS os.environ["ENVIRONMENT"] = "test" def pytest_configure(config: pytest.Config): config.stash[NONEBOT_INIT_KWARGS] = {"secret": os.getenv("INPUT_SECRET")} ``` NoneBug 默认也会为我们管理 lifespan 的 startup 与 shutdown。如果不希望 NoneBug 管理 lifespan,你可以在 `pytest_configure` 里添加以下配置: ```python import pytest from nonebug import NONEBOT_START_LIFESPAN def pytest_configure(config: pytest.Config): config.stash[NONEBOT_START_LIFESPAN] = False ``` ## 编写插件测试 在配置完成插件加载后,我们就可以在测试中使用插件了。NoneBug 通过 pytest fixture `app` 提供各种测试方法,我们可以在测试中使用它来测试插件。现在,我们创建一个测试脚本来测试[深入指南](../../appendices/session-control.mdx)中编写的天气插件。首先,我们先要导入我们需要的模块:
插件示例 ```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}的天气是...") ```
```python {4,5,9,11-16} title=tests/test_weather.py from datetime import datetime import pytest from nonebug import App from nonebot.adapters.console import User, Message, MessageEvent @pytest.mark.asyncio async def test_weather(app: App): from awesome_bot.plugins.weather import weather event = MessageEvent( time=datetime.now(), self_id="test", message=Message("/天气 北京"), user=User(id="user"), ) ``` 在上面的代码中,我们引入了 NoneBug 的测试 `App` 对象,以及必要的适配器消息与事件定义等。在测试函数 `test_weather` 中,我们导入了要进行测试的事件响应器 `weather`。请注意,由于需要等待 NoneBot 初始化并加载插件完毕,插件内容必须在**测试函数内部**进行导入。然后,我们创建了一个 `MessageEvent` 事件对象,它模拟了一个用户发送了 `/天气 北京` 的消息。接下来,我们使用 `app.test_matcher` 方法来测试 `weather` 事件响应器: ```python {11-15} title=tests/test_weather.py @pytest.mark.asyncio async def test_weather(app: App): from awesome_bot.plugins.weather import weather event = MessageEvent( time=datetime.now(), self_id="test", message=Message("/天气 北京"), user=User(id="user"), ) async with app.test_matcher(weather) as ctx: bot = ctx.create_bot() ctx.receive_event(bot, event) ctx.should_call_send(event, "今天北京的天气是...", result=None) ctx.should_finished(weather) ``` 这里我们使用 `async with` 语句并通过参数指定要测试的事件响应器 `weather` 来进入测试上下文。在测试上下文中,我们可以使用 `ctx.create_bot` 方法创建一个虚拟的机器人实例,并使用 `ctx.receive_event` 方法来模拟机器人接收到消息事件。然后,我们就可以定义预期行为来测试机器人是否正确运行。在上面的代码中,我们使用 `ctx.should_call_send` 方法来断言机器人应该发送 `今天北京的天气是...` 这条消息,并且将发送函数的调用结果作为第三个参数返回给事件处理函数。如果断言失败,测试将会不通过。我们也可以使用 `ctx.should_finished` 方法来断言机器人应该结束会话。 为了测试更复杂的情况,我们可以为添加更多的测试用例。例如,我们可以测试用户输入了一个不支持的地名时机器人的反应: ```python {17-21,23-26} title=tests/test_weather.py def make_event(message: str = "") -> MessageEvent: return MessageEvent( time=datetime.now(), self_id="test", message=Message(message), user=User(id="user"), ) @pytest.mark.asyncio async def test_weather(app: App): from awesome_bot.plugins.weather import weather async with app.test_matcher(weather) as ctx: ... # 省略前面的测试用例 async with app.test_matcher(weather) as ctx: bot = ctx.create_bot() event = make_event("/天气 南京") ctx.receive_event(bot, event) ctx.should_call_send(event, "你想查询的城市 南京 暂不支持,请重新输入!", result=None) ctx.should_rejected(weather) event = make_event("北京") ctx.receive_event(bot, event) ctx.should_call_send(event, "今天北京的天气是...", result=None) ctx.should_finished(weather) ``` 在上面的代码中,我们使用 `ctx.should_rejected` 来断言机器人应该请求用户重新输入。然后,我们再次使用 `ctx.receive_event` 方法来模拟用户回复了 `北京`,并使用 `ctx.should_finished` 来断言机器人应该结束会话。 更多的 NoneBug 用法将在后续章节中介绍。 ================================================ FILE: website/versioned_docs/version-2.4.3/best-practice/testing/_category_.json ================================================ { "label": "单元测试", "position": 5 } ================================================ FILE: website/versioned_docs/version-2.4.3/best-practice/testing/behavior.mdx ================================================ --- sidebar_position: 2 description: 测试事件响应、平台接口调用和会话控制 --- # 测试事件响应与会话操作 import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; 在 NoneBot 接收到事件时,事件响应器根据优先级依次通过权限、响应规则来判断当前事件是否应该触发。事件响应流程中,机器人可能会通过 `send` 发送消息或者调用平台接口来执行预期的操作。因此,我们需要对这两种操作进行单元测试。 在上一节中,我们对单个事件响应器进行了简单测试。但是在实际场景中,机器人可能定义了多个事件响应器,由于优先级和响应规则的存在,预期的事件响应器可能并不会被触发。NoneBug 支持同时测试多个事件响应器,以此来测试机器人的整体行为。 ## 测试事件响应 NoneBug 提供了六种定义 `Rule` 和 `Permission` 预期行为的方法: - `should_pass_rule` - `should_not_pass_rule` - `should_ignore_rule` - `should_pass_permission` - `should_not_pass_permission` - `should_ignore_permission` :::tip 提示 事件响应器类型的检查属于 `Permission` 的一部分,因此可以通过 `should_pass_permission` 和 `should_not_pass_permission` 方法来断言事件响应器类型的检查。 ::: 下面我们根据插件示例来测试事件响应行为,我们首先定义两个事件响应器作为测试的对象: ```python title=example.py from nonebot import on_command def never_pass(): return False foo = on_command("foo") bar = on_command("bar", permission=never_pass) ``` 在这两个事件响应器中,`foo` 当收到 `/foo` 消息时会执行,而 `bar` 则不会执行。我们使用 NoneBug 来测试它们: ```python {21,22,28,29} title=tests/test_example.py from datetime import datetime import pytest from nonebug import App from nonebot.adapters.console import User, Message, MessageEvent def make_event(message: str = "") -> MessageEvent: return MessageEvent( time=datetime.now(), self_id="test", message=Message(message), user=User(id="user"), ) @pytest.mark.asyncio async def test_example(app: App): from awesome_bot.plugins.example import foo, bar async with app.test_matcher(foo) as ctx: bot = ctx.create_bot() event = make_event("/foo") ctx.receive_event(bot, event) ctx.should_pass_rule() ctx.should_pass_permission() async with app.test_matcher(bar) as ctx: bot = ctx.create_bot() event = make_event("/foo") ctx.receive_event(bot, event) ctx.should_not_pass_rule() ctx.should_not_pass_permission() ``` 在上面的代码中,我们分别对 `foo` 和 `bar` 事件响应器进行响应测试。我们使用 `ctx.should_pass_rule` 和 `ctx.should_pass_permission` 断言 `foo` 事件响应器应该被触发,使用 `ctx.should_not_pass_rule` 和 `ctx.should_not_pass_permission` 断言 `bar` 事件响应器应该被忽略。 ```python title=tests/test_example.py from datetime import datetime import pytest from nonebug import App from nonebot.adapters.console import User, Message, MessageEvent def make_event(message: str = "") -> MessageEvent: return MessageEvent( time=datetime.now(), self_id="test", message=Message(message), user=User(id="user"), ) @pytest.mark.asyncio async def test_example(app: App): from awesome_bot.plugins.example import foo, bar async with app.test_matcher() as ctx: bot = ctx.create_bot() event = make_event("/foo") ctx.receive_event(bot, event) ctx.should_pass_rule(foo) ctx.should_pass_permission(foo) ctx.should_not_pass_rule(bar) ctx.should_not_pass_permission(bar) ``` 在上面的代码中,我们对 `foo` 和 `bar` 事件响应器一起进行响应测试。我们使用 `ctx.should_pass_rule` 和 `ctx.should_pass_permission` 断言 `foo` 事件响应器应该被触发,使用 `ctx.should_not_pass_rule` 和 `ctx.should_not_pass_permission` 断言 `bar` 事件响应器应该被忽略。通过参数,我们可以指定断言的事件响应器。 当然,如果需要忽略某个事件响应器的响应规则和权限检查,强行进入响应流程,我们可以使用 `should_ignore_rule` 和 `should_ignore_permission` 方法: ```python {21,22} title=tests/test_example.py from datetime import datetime import pytest from nonebug import App from nonebot.adapters.console import User, Message, MessageEvent def make_event(message: str = "") -> MessageEvent: return MessageEvent( time=datetime.now(), self_id="test", message=Message(message), user=User(id="user"), ) @pytest.mark.asyncio async def test_example(app: App): from awesome_bot.plugins.example import foo, bar async with app.test_matcher(bar) as ctx: bot = ctx.create_bot() event = make_event("/foo") ctx.receive_event(bot, event) ctx.should_ignore_rule(bar) ctx.should_ignore_permission(bar) ``` 在忽略了响应规则和权限检查之后,就会进入 `bar` 事件响应器的响应流程。 ## 测试平台接口使用 上一节的示例插件测试中,我们已经尝试了测试插件对事件的消息回复。通常情况下,事件处理流程中对平台接口的使用会通过事件响应器操作或者调用平台 API 两种途径进行。针对这两种途径,NoneBug 分别提供了 `ctx.should_call_send` 和 `ctx.should_call_api` 方法来测试平台接口的使用情况。 1. `should_call_send` 定义事件响应器预期发送的消息,即通过[事件响应器操作 send](../../appendices/session-control.mdx#send)进行的操作。`should_call_send` 有四个参数: - `event`:回复的目标事件。 - `message`:预期的消息对象,可以是 `str`、`Message` 或 `MessageSegment`。 - `result`:send 的返回值,将会返回给插件。 - `bot`(可选):发送消息的 bot 对象。 - `**kwargs`:send 方法的额外参数。 2. `should_call_api` 定义事件响应器预期调用的平台 API 接口,即通过[调用平台 API](../../appendices/api-calling.mdx#调用平台-api)进行的操作。`should_call_api` 有四个参数: - `api`:API 名称。 - `data`:预期的请求数据。 - `result`:call_api 的返回值,将会返回给插件。 - `adapter`(可选):调用 API 的平台适配器对象。 - `**kwargs`:call_api 方法的额外参数。 下面是一个使用 `should_call_send` 和 `should_call_api` 方法的示例: 我们先定义一个测试插件,在响应流程中向用户发送一条消息并调用 `Console` 适配器的 `bell` API。 ```python {8,9} title=example.py from nonebot import on_command from nonebot.adapters.console import Bot foo = on_command("foo") @foo.handle() async def _(bot: Bot): await foo.send("message") await bot.bell() ``` 然后我们对该插件进行测试: ```python title=tests/test_example.py from datetime import datetime import pytest import nonebot from nonebug import App from nonebot.adapters.console import Bot, User, Adapter, Message, MessageEvent def make_event(message: str = "") -> MessageEvent: return MessageEvent( time=datetime.now(), self_id="test", message=Message(message), user=User(id="user"), ) @pytest.mark.asyncio async def test_example(app: App): from awesome_bot.plugins.example import foo async with app.test_matcher(foo) as ctx: # highlight-start adapter = nonebot.get_adapter(Adapter) bot = ctx.create_bot(base=Bot, adapter=adapter) # highlight-end event = make_event("/foo") ctx.receive_event(bot, event) # highlight-start ctx.should_call_send(event, "message", result=None, bot=bot) ctx.should_call_api("bell", {}, result=None, adapter=adapter) # highlight-end ``` 请注意,对于在依赖注入中使用了非基类对象的情况,我们需要在 `create_bot` 方法中指定 `base` 和 `adapter` 参数,确保不会因为重载功能而出现非预期情况。 ## 测试会话控制 在[会话控制](../../appendices/session-control.mdx)一节中,我们介绍了如何使用事件响应器操作来实现对用户的交互式会话。在上一节的示例插件测试中,我们其实已经使用了 `ctx.should_finished` 来断言会话结束。NoneBug 针对各种流程控制操作分别提供了相应的方法来定义预期的会话处理行为。它们分别是: - `should_finished`:断言会话结束,对应 `matcher.finish` 操作。 - `should_rejected`:断言会话等待用户输入并重新执行当前事件处理函数,对应 `matcher.reject` 系列操作。 - `should_paused`: 断言会话等待用户输入并执行下一个事件处理函数,对应 `matcher.pause` 操作。 我们仅需在测试用例中的正确位置调用这些方法,就可以断言会话的预期行为。例如: ```python title=example.py from nonebot import on_command from nonebot.typing import T_State foo = on_command("foo") @foo.got("key", prompt="请输入密码") async def _(state: T_State, key: str = ArgPlainText()): if key != "some password": try_count = state.get("try_count", 1) if try_count >= 3: await foo.finish("密码错误次数过多") else: state["try_count"] = try_count + 1 await foo.reject("密码错误,请重新输入") await foo.finish("密码正确") ``` ```python title=tests/test_example.py from datetime import datetime import pytest from nonebug import App from nonebot.adapters.console import User, Message, MessageEvent def make_event(message: str = "") -> MessageEvent: return MessageEvent( time=datetime.now(), self_id="test", message=Message(message), user=User(id="user"), ) @pytest.mark.asyncio async def test_example(app: App): from awesome_bot.plugins.example import foo async with app.test_matcher(foo) as ctx: bot = ctx.create_bot() event = make_event("/foo") ctx.receive_event(bot, event) ctx.should_call_send(event, "请输入密码", result=None) ctx.should_rejected(foo) event = make_event("wrong password") ctx.receive_event(bot, event) ctx.should_call_send(event, "密码错误,请重新输入", result=None) ctx.should_rejected(foo) event = make_event("wrong password") ctx.receive_event(bot, event) ctx.should_call_send(event, "密码错误,请重新输入", result=None) ctx.should_rejected(foo) event = make_event("wrong password") ctx.receive_event(bot, event) ctx.should_call_send(event, "密码错误次数过多", result=None) ctx.should_finished(foo) ``` ================================================ FILE: website/versioned_docs/version-2.4.3/best-practice/testing/mock-network.md ================================================ --- sidebar_position: 3 description: 模拟网络通信以进行测试 --- # 模拟网络通信 NoneBot 驱动器提供了多种方法来帮助适配器进行网络通信,主要包括客户端和服务端两种类型。模拟网络通信可以帮助我们更加接近实际机器人应用场景,进行更加真实的集成测试。同时,通过这种途径,我们还可以完成对适配器的测试。 NoneBot 中的网络通信主要包括以下几种: - HTTP 服务端(WebHook) - WebSocket 服务端 - HTTP 客户端 - WebSocket 客户端 下面我们将分别介绍如何使用 NoneBug 来模拟这几种通信方式。 ## 测试 HTTP 服务端 当 NoneBot 作为 ASGI 服务端应用时,我们可以定义一系列的路由来处理 HTTP 请求,适配器同样也可以通过定义路由来响应机器人相关的网络通信。下面假设我们使用了一个适配器 `fake` ,它定义了一个路由 `/fake/http` ,用于接收平台 WebHook 并处理。实际应用测试时,应将该路由地址替换为**真实适配器注册的路由地址**。 我们首先需要获取测试用模拟客户端: ```python {5,6} title=tests/test_http_server.py from nonebug import App @pytest.mark.asyncio async def test_http_server(app: App): async with app.test_server() as ctx: client = ctx.get_client() ``` 默认情况下,`app.test_server()` 会通过 `nonebot.get_asgi` 获取测试对象,我们也可以通过参数指定 ASGI 应用: ```python async with app.test_server(asgi=asgi_app) as ctx: ... ``` 获取到模拟客户端后,即可像 `requests`、`httpx` 等库类似的方法进行使用: ```python {3,11-14,16} title=tests/test_http_server.py import nonebot from nonebug import App from nonebot.adapters.fake import Adapter @pytest.mark.asyncio async def test_http_server(app: App): adapter = nonebot.get_adapter(Adapter) async with app.test_server() as ctx: client = ctx.get_client() response = await client.post("/fake/http", json={"bot_id": "fake"}) assert response.status_code == 200 assert response.json() == {"status": "success"} assert "fake" in nonebot.get_bots() adapter.bot_disconnect(nonebot.get_bot("fake")) ``` 在上面的测试中,我们向 `/fake/http` 发送了一个模拟 POST 请求,适配器将会对该请求进行处理,我们可以通过检查请求返回是否正确、Bot 对象是否创建等途径来验证机器人是否正确运行。在完成测试后,我们通常需要对 Bot 对象进行清理,以避免对其他测试产生影响。 ## 测试 WebSocket 服务端 当 NoneBot 作为 ASGI 服务端应用时,我们还可以定义一系列的路由来处理 WebSocket 通信。下面假设我们使用了一个适配器 `fake` ,它定义了一个路由 `/fake/ws` ,用于处理平台 WebSocket 连接信息。实际应用测试时,应将该路由地址替换为**真实适配器注册的路由地址**。 我们同样需要通过 `app.test_server()` 获取测试用模拟客户端,这里就不再赘述。在获取到模拟客户端后,我们可以通过 `client.websocket_connect` 方法来模拟 WebSocket 连接: ```python {3,11-15} title=tests/test_ws_server.py import nonebot from nonebug import App from nonebot.adapters.fake import Adapter @pytest.mark.asyncio async def test_ws_server(app: App): adapter = nonebot.get_adapter(Adapter) async with app.test_server() as ctx: client = ctx.get_client() async with client.websocket_connect("/fake/ws") as ws: await ws.send_json({"bot_id": "fake"}) response = await ws.receive_json() assert response == {"status": "success"} assert "fake" in nonebot.get_bots() ``` 在上面的测试中,我们向 `/fake/ws` 进行了 WebSocket 模拟通信,通过发送消息与机器人进行交互,然后检查机器人发送的信息是否正确。 ## 测试 HTTP 客户端 ~~暂不支持~~ ## 测试 WebSocket 客户端 ~~暂不支持~~ ================================================ FILE: website/versioned_docs/version-2.4.3/community/contact.md ================================================ --- sidebar-position: 0 description: 遇到问题如何获取帮助 --- # 参与讨论 如果在安装或者开发 NoneBot 过程中遇到了任何问题,或者有新奇的点子,欢迎参与我们的社区讨论: 1. 点击下方链接前往 GitHub,前往 Issues 页面,在 `New Issue` Template 中选择 `Question` NoneBot:[![NoneBot project link](https://img.shields.io/github/stars/nonebot/nonebot2?style=social)](https://github.com/nonebot/nonebot2) 2. 通过 QQ 群(点击下方链接直达) [![QQ Chat Group](https://img.shields.io/badge/QQ%E7%BE%A4-768887710-orange?style=social)](https://jq.qq.com/?_wv=1027&k=5OFifDh) 3. 通过 QQ 频道 [![QQ Channel](https://img.shields.io/badge/QQ%E9%A2%91%E9%81%93-NoneBot-orange?style=social)](https://qun.qq.com/qqweb/qunpro/share?_wv=3&_wwv=128&appChannel=share&inviteCode=7b4a3&appChannel=share&businessType=9&from=246610&biz=ka) 4. 通过 Discord 服务器(点击下方链接直达) [![Discord Server](https://discordapp.com/api/guilds/847819937858584596/widget.png?style=shield)](https://discord.gg/VKtE6Gdc4h) ================================================ FILE: website/versioned_docs/version-2.4.3/community/contributing.md ================================================ --- sidebar-position: 1 description: 如何为 NoneBot 贡献代码 --- # 贡献指南 ## Code of Conduct 请参阅 [Code of Conduct](https://github.com/nonebot/nonebot2/blob/master/CODE_OF_CONDUCT.md)。 ## 参与开发 请参阅 [Contributing](https://github.com/nonebot/nonebot2/blob/master/CONTRIBUTING.md)。 ## 鸣谢 感谢以下开发者对 NoneBot2 作出的贡献: ================================================ FILE: website/versioned_docs/version-2.4.3/developer/adapter-writing.md ================================================ --- sidebar_position: 1 description: 编写适配器对接新的平台 --- # 编写适配器 在编写适配器之前,我们需要先了解[适配器的功能与组成](../advanced/adapter#适配器功能与组成),适配器通常由 `Adapter`、`Bot`、`Event` 和 `Message` 四个部分组成,在编写适配器时,我们需要继承 NoneBot 中的基类,并根据实际平台来编写每个部分功能。 ## 组织结构 NoneBot 适配器项目通常以 `nonebot-adapter-{adapter-name}` 作为项目名,并以**命名空间包**的形式编写,即在 `nonebot/adapters/{adapter-name}` 目录中编写实际代码,例如: ```tree 📦 nonebot-adapter-{adapter-name} ├── 📂 nonebot │ ├── 📂 adapters │ │ ├── 📂 {adapter-name} │ │ │ ├── 📜 __init__.py │ │ │ ├── 📜 adapter.py │ │ │ ├── 📜 bot.py │ │ │ ├── 📜 config.py │ │ │ ├── 📜 event.py │ │ │ └── 📜 message.py ├── 📜 pyproject.toml └── 📜 README.md ``` :::tip 提示 上述的项目结构仅作推荐,不做强制要求,保证实际可用性即可。 ::: ### 使用 NB-CLI 创建项目 我们可以使用脚手架快速创建项目: ```shell nb adapter create ``` 按照指引,输入适配器名称以及存储位置,即可创建一个带有基本结构的适配器项目。 ## 组成部分 :::tip 提示 本章节的代码中提到的 `Adapter`、`Bot`、`Event` 和 `Message` 等,均为下文中适配器所编写的类,而非 NoneBot 中的基类。 ::: ### Log 适配器在处理时通常需要打印日志,但直接使用 NoneBot 的默认 `logger` 不方便区分适配器输出和其它日志。因此我们可以使用 NoneBot 提供的 `logger_wrapper` 方法,自定义一个 `log` 函数用于快捷打印适配器日志: ```python {3} title=log.py from nonebot.utils import logger_wrapper log = logger_wrapper("your_adapter_name") ``` 这个 `log` 函数会在默认 `logger` 中添加适配器名称前缀,它接收三个参数:日志等级、日志内容以及可选的异常,具体用法如下: ```python from .log import log log("DEBUG", "A DEBUG log.") log("INFO", "A INFO log.") try: ... except Exception as e: log("ERROR", "something error.", e) ``` ### Config 通常适配器需要一些配置项,例如平台连接密钥等。适配器的配置方法与[插件配置](../appendices/config#%E6%8F%92%E4%BB%B6%E9%85%8D%E7%BD%AE)类似,例如: ```python title=config.py from pydantic import BaseModel class Config(BaseModel): xxx_id: str xxx_token: str ``` 配置项的读取将在下方 [Adapter](#adapter) 中介绍。 ### Adapter Adapter 负责转换事件、调用接口,以及正确创建 Bot 对象并注册到 NoneBot 中。在编写平台相关内容之前,我们需要继承基类,并实现适配器的基本信息: ```python {9,11,14,18} title=adapter.py from typing import Any from typing_extensions import override from nonebot.drivers import Driver from nonebot import get_plugin_config from nonebot.adapters import Adapter as BaseAdapter from .config import Config class Adapter(BaseAdapter): @override def __init__(self, driver: Driver, **kwargs: Any): super().__init__(driver, **kwargs) # 读取适配器所需的配置项 self.adapter_config: Config = get_plugin_config(Config) @classmethod @override def get_name(cls) -> str: """适配器名称""" return "your_adapter_name" ``` #### 与平台交互 NoneBot 提供了多种 [Driver](../advanced/driver) 来帮助适配器进行网络通信,主要分为客户端和服务端两种类型。我们需要**根据平台文档和特性**选择合适的通信方式,并编写相关方法用于初始化适配器,与平台建立连接和进行交互: ##### 客户端通信方式 ```python {12,23,24} title=adapter.py import asyncio from typing_extensions import override from nonebot import get_plugin_config from nonebot.exception import WebSocketClosed from nonebot.drivers import Request, WebSocketClientMixin class Adapter(BaseAdapter): @override def __init__(self, driver: Driver, **kwargs: Any): super().__init__(driver, **kwargs) self.adapter_config: Config = get_plugin_config(Config) self.task: Optional[asyncio.Task] = None # 存储 ws 任务 self.setup() def setup(self) -> None: if not isinstance(self.driver, WebSocketClientMixin): # 判断用户配置的Driver类型是否符合适配器要求,不符合时应抛出异常 raise RuntimeError( f"Current driver {self.config.driver} doesn't support websocket client connections!" f"{self.get_name()} Adapter need a WebSocket Client Driver to work." ) # 在 NoneBot 启动和关闭时进行相关操作 self.driver.on_startup(self.startup) self.driver.on_shutdown(self.shutdown) async def startup(self) -> None: """定义启动时的操作,例如和平台建立连接""" self.task = asyncio.create_task(self._forward_ws()) # 建立 ws 连接 async def _forward_ws(self): request = Request( method="GET", url="your_platform_websocket_url", headers={"token": "..."}, # 鉴权请求头 ) while True: try: async with self.websocket(request) as ws: try: # 处理 websocket ... except WebSocketClosed as e: log( "ERROR", "WebSocket Closed", e, ) except Exception as e: log( "ERROR", "Error while process data from " "websocket platform_websocket_url. " "Trying to reconnect...", e, ) finally: # 这里要断开 Bot 连接 except Exception as e: # 尝试重连 log( "ERROR", "Error while setup websocket to " "platform_websocket_url. Trying to reconnect...", e, ) await asyncio.sleep(3) # 重连间隔 async def shutdown(self) -> None: """定义关闭时的操作,例如停止任务、断开连接""" # 断开 ws 连接 if self.task is not None and not self.task.done(): self.task.cancel() ``` ##### 服务端通信方式 ```python {30,38} title=adapter.py from nonebot import get_plugin_config from nonebot.drivers import ( Request, ASGIMixin, WebSocket, HTTPServerSetup, WebSocketServerSetup ) class Adapter(BaseAdapter): @override def __init__(self, driver: Driver, **kwargs: Any): super().__init__(driver, **kwargs) self.adapter_config: Config = get_plugin_config(Config) self.setup() def setup(self) -> None: if not isinstance(self.driver, ASGIMixin): raise RuntimeError( f"Current driver {self.config.driver} doesn't support asgi server!" f"{self.get_name()} Adapter need a asgi server driver to work." ) # 建立服务端路由 # HTTP Webhook 路由 http_setup = HTTPServerSetup( URL("your_webhook_url"), # 路由地址 "POST", # 接收的方法 "WEBHOOK name", # 路由名称 self._handle_http, # 处理函数 ) self.setup_http_server(http_setup) # 反向 Websocket 路由 ws_setup = WebSocketServerSetup( URL("your_websocket_url"), # 路由地址 "WebSocket name", # 路由名称 self._handle_ws, # 处理函数 ) self.setup_websocket_server(ws_setup) async def _handle_http(self, request: Request) -> Response: """HTTP 路由处理函数,只有一个类型为 Request 的参数,且返回值类型为 Response""" ... return Response( status_code=200, # 状态码 headers={"something": "something"}, # 响应头 content="xxx", # 响应内容 ) async def _handle_ws(self, websocket: WebSocket) -> Any: """WebSocket 路由处理函数,只有一个类型为 WebSocket 的参数""" ... ``` 更多通信交互方式可以参考以下适配器: - [OneBot](https://github.com/nonebot/adapter-onebot/blob/master/nonebot/adapters/onebot/v11/adapter.py) - `WebSocket 客户端`、`WebSocket 服务端`、`HTTP WEBHOOK`、`HTTP POST` - [QQ](https://github.com/nonebot/adapter-qq/blob/master/nonebot/adapters/qq/adapter.py) - `WebSocket 服务端`、`HTTP WEBHOOK` - [Telegram](https://github.com/nonebot/adapter-telegram/blob/beta/nonebot/adapters/telegram/adapter.py) - `HTTP WEBHOOK` #### 建立 Bot 连接 在与平台建立连接后,我们需要将 [Bot](#bot) 实例化,并调用适配器提供的的 `bot_connect` 方法告知 NoneBot 建立了 Bot 连接。在与平台断开连接或出现某些异常进行重连时,我们需要调用 `bot_disconnect` 方法告知 NoneBot 断开了 Bot 连接。 ```python {7,8,11} title=adapter.py from .bot import Bot class Adapter(BaseAdapter): def _handle_connect(self): bot_id = ... # 通过配置或者平台 API 等方式,获取到 Bot 的 ID bot = Bot(self, self_id=bot_id) # 实例化 Bot self.bot_connect(bot) # 建立 Bot 连接 def _handle_disconnect(self): self.bot_disconnect(bot) # 断开 Bot 连接 ``` #### 转换 Event 事件 在接收到来自平台的事件数据后,我们需要将其转为适配器的 [Event](#event),并调用 Bot 的 `handle_event` 方法来让 Bot 对事件进行处理: ```python title=adapter.py import asyncio from typing import Any, Dict from nonebot.compat import type_validate_python from .bot import Bot from .event import Event from .log import log class Adapter(BaseAdapter): @classmethod def payload_to_event(cls, payload: Dict[str, Any]) -> Event: """根据平台事件的特性,转换平台 payload 为具体 Event Event 模型继承自 pydantic.BaseModel,具体请参考 pydantic 文档 """ # 做一层异常处理,以应对平台事件数据的变更 try: return type_validate_python(your_event_class, payload) except Exception as e: # 无法正常解析为具体 Event 时,给出日志提示 log( "WARNING", f"Parse event error: {str(payload)}", ) # 也可以尝试转为基础 Event 进行处理 return type_validate_python(Event, payload) async def _forward(self, bot: Bot): payload: Dict[str, Any] # 接收到的事件数据 event = self.payload_to_event(payload) # 让 bot 对事件进行处理 asyncio.create_task(bot.handle_event(event)) ``` #### 调用平台 API 我们需要实现 `Adapter` 的 `_call_api` 方法,使开发者能够调用平台提供的 API。如果通过 WebSocket 通信可以通过 `send` 方法来发送数据,如果采用 HTTP 请求,则需要通过 NoneBot 提供的 `Request` 对象,调用 `driver` 的 `request` 方法来发送请求。 ```python {11} title=adapter.py from typing import Any from typing_extensions import override from nonebot.drivers import Request, WebSocket from .bot import Bot class Adapter(BaseAdapter): @override async def _call_api(self, bot: Bot, api: str, **data: Any) -> Any: log("DEBUG", f"Calling API {api}") # 给予日志提示 platform_data = your_handle_data_method(data) # 自行将数据转为平台所需要的格式 # 采用 HTTP 请求的方式,需要构造一个 Request 对象 request = Request( method="GET", # 请求方法 url=api, # 接口地址 headers=..., # 请求头,通常需要包含鉴权信息 params=platform_data, # 自行处理数据的传输形式 # json=platform_data, # data=platform_data, ) # 发送请求,返回结果 return await self.driver.request(request) # 采用 WebSocket 通信的方式,可以直接调用 send 方法发送数据 # 通过某种方式获取到 bot 对应的 websocket 对象 ws: WebSocket = your_get_websocket_method(bot.self_id) await ws.send_text(platform_data) # 发送 str 类型的数据 await ws.send_bytes(platform_data) # 发送 bytes 类型的数据 await ws.send(platform_data) # 是以上两种方式的合体 # 接收并返回结果,同样的,也有 str 和 bytes 的区别 return await ws.receive_text() return await ws.receive_bytes() return await ws.receive() ``` `调用平台 API` 实现方式具体可以参考以下适配器: Websocket: - [OneBot V11](https://github.com/nonebot/adapter-onebot/blob/54270edbbdb2a71332d744f90b1a3d7f4bf6463a/nonebot/adapters/onebot/v11/adapter.py#L167-L177) - [OneBot V12](https://github.com/nonebot/adapter-onebot/blob/54270edbbdb2a71332d744f90b1a3d7f4bf6463a/nonebot/adapters/onebot/v12/adapter.py#L204-L218) HTTP: - [OneBot V11](https://github.com/nonebot/adapter-onebot/blob/54270edbbdb2a71332d744f90b1a3d7f4bf6463a/nonebot/adapters/onebot/v11/adapter.py#L179-L215) - [OneBot V12](https://github.com/nonebot/adapter-onebot/blob/54270edbbdb2a71332d744f90b1a3d7f4bf6463a/nonebot/adapters/onebot/v12/adapter.py#L220-L266) - [QQ](https://github.com/nonebot/adapter-qq/blob/dc5d437e101f0e3db542de3300758a035ed7036e/nonebot/adapters/qq/adapter.py#L599-L605) - [Telegram](https://github.com/nonebot/adapter-telegram/blob/4a8633627e619245516767f5503dec2f58fe2193/nonebot/adapters/telegram/adapter.py#L148-L253) - [飞书](https://github.com/nonebot/adapter-feishu/blob/f8ab05e6d57a5e9013b944b0d019ca777725dfb0/nonebot/adapters/feishu/adapter.py#L201-L218) ### Bot Bot 是机器人开发者能够直接获取并使用的核心对象,负责存储平台机器人相关信息,并提供回复事件、调用 API 的上层方法。我们需要继承基类 `Bot`,并实现相关方法: ```python {20,25,34} title=bot.py from typing import TYPE_CHECKING, Any, Union from typing_extensions import override from nonebot.message import handle_event from nonebot.adapters import Bot as BaseBot from .event import Event from .message import Message, MessageSegment if TYPE_CHECKING: from .adapter import Adapter class Bot(BaseBot): """ your_adapter_name 协议 Bot 适配。 """ @override def __init__(self, adapter: Adapter, self_id: str, **kwargs: Any): super().__init__(adapter, self_id) self.adapter: Adapter = adapter # 一些有关 Bot 的信息也可以在此定义和存储 async def handle_event(self, event: Event): # 根据需要,对事件进行某些预处理,例如: # 检查事件是否和机器人有关操作,去除事件消息首尾的 @bot # 检查事件是否有回复消息,调用平台 API 获取原始消息的消息内容 ... # 调用 handle_event 让 NoneBot 对事件进行处理 await handle_event(self, event) @override async def send( self, event: Event, message: Union[str, Message, MessageSegment], **kwargs: Any, ) -> Any: # 根据平台实现 Bot 回复事件的方法 # 将消息处理为平台所需的格式后,调用发送消息接口进行发送,例如: data = message_to_platform_data(message) await self.send_message( data=data, ... ) ``` ### Event Event 是 NoneBot 中的事件主体对象,所有平台消息在进入处理流程前需要转换为 NoneBot 事件。我们需要继承基类 `Event`,并实现相关方法: ```python {5,8,13,18,23,28,33} title=event.py from typing_extensions import override from nonebot.compat import model_dump from nonebot.adapters import Event as BaseEvent class Event(BaseEvent): @override def get_event_name(self) -> str: # 返回事件的名称,用于日志打印 return "event name" @override def get_event_description(self) -> str: # 返回事件的描述,用于日志打印,请注意转义 loguru tag return escape_tag(repr(model_dump(self))) @override def get_message(self): # 获取事件消息的方法,根据事件具体实现,如果事件非消息类型事件,则抛出异常 raise ValueError("Event has no message!") @override def get_user_id(self) -> str: # 获取用户 ID 的方法,根据事件具体实现,如果事件没有用户 ID,则抛出异常 raise ValueError("Event has no context!") @override def get_session_id(self) -> str: # 获取事件会话 ID 的方法,根据事件具体实现,如果事件没有相关 ID,则抛出异常 raise ValueError("Event has no context!") @override def is_tome(self) -> bool: # 判断事件是否和机器人有关 return False ``` 然后根据平台消息的类型,编写各种不同的事件,并且注意要根据事件类型实现 `get_type` 方法,具体请参考[事件类型](../advanced/adapter#事件类型)。消息类型事件还应重写 `get_message` 和 `get_user_id` 等方法,例如: ```python {7,16,20,25,34,42} title=event.py from .message import Message class HeartbeatEvent(Event): """心跳时间,通常为元事件""" @override def get_type(self) -> str: return "meta_event" class MessageEvent(Event): """消息事件""" message_id: str user_id: str @override def get_type(self) -> str: return "message" @override def get_message(self) -> Message: # 返回事件消息对应的 NoneBot Message 对象 return self.message @override def get_user_id(self) -> str: return self.user_id class JoinRoomEvent(Event): """加入房间事件,通常为通知事件""" user_id: str room_id: str @override def get_type(self) -> str: return "notice" class ApplyAddFriendEvent(Event): """申请添加好友事件,通常为请求事件""" user_id: str @override def get_type(self) -> str: return "request" ``` ### Message Message 负责正确序列化消息,以便机器人插件处理。我们需要继承 `MessageSegment` 和 `Message` 两个类,并实现相关方法: ```python {9,12,17,22,27,30,36} title=message.py from typing import Type, Iterable from typing_extensions import override from nonebot.utils import escape_tag from nonebot.adapters import Message as BaseMessage from nonebot.adapters import MessageSegment as BaseMessageSegment class MessageSegment(BaseMessageSegment["Message"]): @classmethod @override def get_message_class(cls) -> Type["Message"]: # 返回适配器的 Message 类型本身 return Message @override def __str__(self) -> str: # 返回该消息段的纯文本表现形式,通常在日志中展示 return "text of MessageSegment" @override def is_text(self) -> bool: # 判断该消息段是否为纯文本 return self.type == "text" class Message(BaseMessage[MessageSegment]): @classmethod @override def get_segment_class(cls) -> Type[MessageSegment]: # 返回适配器的 MessageSegment 类型本身 return MessageSegment @staticmethod @override def _construct(msg: str) -> Iterable[MessageSegment]: # 实现从字符串中构造消息数组,如无字符串嵌入格式可直接返回文本类型 MessageSegment ... ``` 然后根据平台具体的消息类型,来实现各种 `MessageSegment` 消息段,具体可以参考以下适配器: - [OneBot V11](https://github.com/nonebot/adapter-onebot/blob/54270edbbdb2a71332d744f90b1a3d7f4bf6463a/nonebot/adapters/onebot/v11/message.py#L25-L259) - [QQ](https://github.com/nonebot/adapter-qq/blob/dc5d437e101f0e3db542de3300758a035ed7036e/nonebot/adapters/qq/message.py#L30-L520) - [Telegram](https://github.com/nonebot/adapter-telegram/blob/4a8633627e619245516767f5503dec2f58fe2193/nonebot/adapters/telegram/message.py#L13-L414) ## 适配器测试 关于适配器测试相关内容在这里不再展开,开发者可以根据需要进行合适的测试。这里为开发者提供几个常见问题的解决方法: 1. 在测试中无法导入 editable 模式安装的适配器代码。在 pytest 的 `conftest.py` 内添加如下代码: ```python title=tests/conftest.py from pathlib import Path import nonebot.adapters nonebot.adapters.__path__.append( # type: ignore str((Path(__file__).parent.parent / "nonebot" / "adapters").resolve()) ) ``` 2. 需要计算适配器测试覆盖率,请在 `pyproject.toml` 中添加 pytest 配置: ```toml title=pyproject.toml [tool.pytest.ini_options] addopts = "--cov nonebot/adapters/{adapter-name} --cov-report term-missing" ``` ## 后续工作 在完成适配器代码的编写后,如果想要将适配器发布到 NoneBot 商店,我们需要将适配器发布到 PyPI 中,然后前往[商店](/store/adapters)页面,切换到适配器页签,点击**发布适配器**按钮,填写适配器相关信息并提交。 另外建议编写适配器文档或者一些插件开发示例,以便其他开发者使用我们的适配器。 ================================================ FILE: website/versioned_docs/version-2.4.3/developer/plugin-publishing.mdx ================================================ --- sidebar_position: 0 description: 在商店发布自己的插件 --- # 发布插件 import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; NoneBot 为开发者提供了分享插件的官方商店。本指南囊括**从创建项目到发布到 PyPI,最终提交商店审核**的全过程。 :::warning 警告 如果你的插件只是满足自用需求,则完全可以选择**不发布插件**。发布插件**不是**使用插件的必要条件。 NoneBot 社区对于插件有一定质量要求,对于不符合要求的插件,社区成员将会要求修改,直至符合要求后才能通过审核;如果长期未更新修改,社区将会关闭当前请求,之后如需发布请重新提交发布插件请求。相应的要求会在本章节以下部分介绍。 ::: :::tip 提示 本章节仅包含插件发布流程指导,插件开发请查阅前述章节。 ::: ## 准备工作 ### 插件命名规范 NoneBot 插件使用下述命名规范: - 对于**项目名**,建议统一以 `nonebot-plugin-` 开头,之后为拟定的插件名字,词间用横杠 `-` 分隔; - **项目名**用于代码仓库名称、PyPI 包的发布名称等; - 本文使用 `nonebot-plugin-{your-plugin-name}` 为例。 - 对于**模块名**,建议与**项目名**一致,但词间用下划线 `_` 分隔,即统一以 `nonebot_plugin_` 开头,之后为拟定的插件名字; - **模块名**用于程序导入使用,应为插件文件(夹)的名称; - 本文使用 `nonebot_plugin_{your_plugin_name}` 为例。 ### 项目结构 :::tip 提示 本段所述的项目结构仅作推荐,不做强制要求。 ::: 插件程序本身结构可参考[插件结构](../tutorial/create-plugin.md#插件结构)一节,唯一区别在于,插件包可以直接处于项目顶层。 插件项目的一种组织结构如下: ```tree 📦 nonebot-plugin-{your-plugin-name} ├── 📂 nonebot_plugin_{your_plugin_name} │ ├── 📜 __init__.py │ └── 📜 config.py ├── 📜 pyproject.toml └── 📜 README.md ``` 功能开发可以在 `__init__.py` 中进行或在包内部创建其他模块并在 `__init__.py` 中导入。 ### 从项目模板开始 为降低新手门槛,我们提供三条清晰、完整、可复制的发布路径。 :::tip 提示 你只需选择一条与你习惯一致的路径,**完整跟随即可成功发布**。无需在不同工具间切换或猜测配置。 ::: NoneBot 生态目前有如下插件项目模板: - [RF-Tar-Railt/nonebot-plugin-template](https://github.com/RF-Tar-Railt/nonebot-plugin-template) 此路径使用 **PDM** 项目管理器,符合 PEP 621 标准,自动化程度高。 - [fllesser/nonebot-plugin-template](https://github.com/fllesser/nonebot-plugin-template) 此路径使用 **uv** 项目管理器和 **PoeThePoet** 任务运行器,构建速度快,适合追求效率的开发者。 - [A-kirami/nonebot-plugin-template](https://github.com/A-kirami/nonebot-plugin-template) 此路径使用 **Poetry** 项目管理器,适合熟悉传统 Python 生态的开发者。 #### 1. 创建项目 1. 访问上述三个模板之一。 2. 点击 **“Use this template”** → **“Create a new repository”**。 3. 仓库名称填写:`nonebot-plugin-{your-plugin-name}`(此部分以 `nonebot-plugin-weather` 为例)。 4. 点击 **“Create repository from template”**。 #### 2. 配置发布权限 1. 进入新仓库 → **Settings** → **Actions** → **General**。 2. 在 **Workflow permissions** 下,勾选 **“Read and write permissions”** → 点击 **Save**。 #### 3. 全局替换项目信息 在仓库中点击 **“Add file”** → **“Create new file”**,创建一个空文件 `LICENSE`,选择开源协议并提交(此操作会触发工作流)。 然后在本地克隆仓库,使用编辑器对以下内容进行**全局替换**: :::tip 提示 此部分以“天气插件”为例,实际的替换内容应该根据你所创建的插件名称等相应调整。 ::: | 原内容 | 替换为 | | ------------------------------ | ---------------------------------- | | `nonebot-plugin-template` | `nonebot-plugin-weather` | | `nonebot_plugin_template` | `nonebot_plugin_weather` | | `` | `天气查询` | | `` | `查询指定城市的实时天气与未来预报` | | `` | `你的GitHub用户名` | | `` | `你的邮箱` | #### 4. 安装依赖与开发 ```bash # 安装 PDM(若未安装) curl -sSL https://pdm-project.org/install-pdm.py | python3 - # 安装项目依赖(自动创建虚拟环境) pdm sync # 添加新依赖(如 httpx) pdm add httpx ``` ```bash # 安装 uv(Windows) powershell -c "irm https://astral.sh/uv/install.ps1 | iex" # 安装 uv(macOS/Linux) curl -LsSf https://astral.sh/uv/install.sh | sh # 安装所有依赖(含 dev) uv sync --all-groups -p 3.12 # 添加新依赖 uv add httpx ``` ```bash # 安装 Poetry(推荐方式) curl -sSL https://install.python-poetry.org | python3 - # 安装项目依赖 poetry install # 添加新依赖 poetry add httpx ``` #### 5. 更新版本并发布 [bump-my-version](https://github.com/callowayproject/bump-my-version) 是一个功能强大、可配置的 Python 项目版本更新工具,支持自动提交到 Git 等 VCS。 ```bash # 安装 bump-my-version pdm add --dev bump-my-version # 更新 patch 版本 pdm run bump patch # 推送 tag 触发发布 git push origin --tags ``` ```bash # 更新 patch 版本 uv run poe bump patch # 推送 tag 触发发布 git push origin --tags ``` ```bash # 安装 bump-my-version poetry add --dev bump-my-version # 更新 patch 版本 poetry run bump patch # 推送 tag 触发发布 git push origin --tags ``` 需要安装 PDM 插件 [pdm-bump](https://github.com/carstencodes/pdm-bump)。 ```bash # 安装 pdm-bump pdm self add pdm-bump # 更新 patch 版本 pdm bump patch # 推送 tag 触发发布 git push origin --tags ``` ```bash # 更新 patch 版本 uv version --bump patch # 创建相应提交与标签 git add pyproject.toml git commit -m "chore: release v0.1.1" # 替换为实际的版本号 git tag v0.1.1 # 替换为实际的版本号 # 推送 tag 触发发布 git push origin --tags ``` ```bash # 更新版本(自动提交并打标签) poetry version patch # 推送 tag 触发发布 git push origin --tags ``` 手动更新 `pyproject.toml` 中的 `version` 字段,然后推送 tag 触发发布工作流 ```bash git add pyproject.toml git commit -m "chore: release v0.1.1" # 替换为实际的版本号 git tag v0.1.1 # 替换为实际的版本号 git push origin --tags ``` 推送 `v*` 标签后,模板提供的 GitHub Actions 工作流将自动构建并发布到 PyPI。 #### 6. 发布到 [PyPI](https://pypi.org) 不同模板使用的发布方式可能不同,具体配置流程参考对应模板的详细使用指南。 根据选用的构建系统,在项目的 `pyproject.toml` 中填入必要信息后进行构建与发布。 :::tip 提示 不同构建工具的使用可能存在差别。本文仅以 [`pdm`](https://pdm-project.org/zh/latest/), [`poetry`](https://python-poetry.org/docs/), [`setuptools`](https://setuptools.pypa.io/en/latest/) 构建系统**本地构建与发布**为示例讲解,其余构建/管理工具等和自动化构建的用法请读者自行探索。 ::: ```bash poetry publish --build # 构建并发布 # 等效于以下两个命令 poetry build # 只构建 poetry publish # 只发布先前的构建 ``` ```bash pdm publish # 构建并发布 # 等效于以下两个命令 pdm build # 只构建 pdm publish --no-build # 只发布先前的构建 ``` ```bash pip install build twine # 安装通用构建与发布工具 python -m build --sdist --wheel . # 只构建 twine upload dist/* # 只发布先前的构建 ``` :::tip 提示 发布前建议自行测试构建包是否可用,避免遗漏代码文件或资源文件等问题。 ::: ## 基本要求 无论你选择哪条路径,以下内容**必须**完成,否则无法通过商店自动检查: ### 能够正确加载 插件包必须能够被 NoneBot 正确加载,在商店审核中会通过 **NoneFlow** 自动化加载测试进行。 #### 依赖其他插件 如果插件依赖其他插件提供的功能,则**必须**在代码中使用 `require()` 来引入该插件,然后才能 `import` 该插件提供的功能。具体细节参阅[跨插件访问](../advanced/requiring.md)。 使用示例如下: ```python title=nonebot_plugin_weather/__init__.py from nonebot import require require("nonebot_plugin_apscheduler") from nonebot_plugin_apscheduler import scheduler ``` #### 不能零配置加载的插件 如果插件需要必要配置项才能正常导入,则**必须**在商店提交表单中填写必要的配置项内容。 但一种更好的做法是,**将插件设计为零配置即可加载**(允许缺少必要配置项时插件仍能正常导入,但不执行需要相应配置项的功能),尤其是对于一些必要配置含有敏感信息(如密钥、Token、API Key 等)的插件。这样可以避免在商店提交表单时填写敏感信息的风险。 ### 插件元数据 插件包**必须**填写元数据才能通过 **NoneFlow** 自动化检查。 下面是一个示例: ```python title=nonebot_plugin_weather/__init__.py from nonebot.plugin import PluginMetadata from .config import Config __plugin_meta__ = PluginMetadata( # 基本信息(必填) name="天气查询", # 插件名称 description="查询指定城市的实时天气与未来预报", # 插件介绍 usage="发送【天气 城市名】获取天气信息", # 插件用法 # 发布额外信息 type="application", # 插件分类 # 发布必填,当前有效类型有:`library`(为其他插件编写提供功能),`application`(向机器人用户提供功能)。 homepage="https://github.com/你的用户名/nonebot-plugin-weather", # 发布必填。 config=Config, # 插件配置项类,如果有配置类则必须填写。 supported_adapters={"~onebot.v11"}, # 支持的适配器集合,其中 `~` 在此处代表前缀 `nonebot.adapters.`,其余适配器亦按此格式填写。 # 若插件只使用了 NoneBot 基本抽象,应显式填写 None,否则应该列出插件支持的适配器。 ) ``` :::caution 注意 `__plugin_meta__` 变量**必须**处于插件最外层(如 `__init__.py` 中),否则无法正常识别。 一般做法是在 `__init__.py` 中定义 `__plugin_meta__`。 ::: #### 继承其他插件支持的适配器 如果你的插件依赖于其他插件提供的支持功能,而其他插件可能支持更少的适配器,这时就应该使用 [inherit_supported_adapters()](../api/plugin/load#inherit-supported-adapters) 函数来继承其他插件支持的适配器。 示例用法如下: ```python title=nonebot_plugin_weather/__init__.py from nonebot import require from nonebot.plugin import PluginMetadata, inherit_supported_adapters from .config import Config require("nonebot_plugin_alconna") # 必须先 require 才能被 inherit_supported_adapters 处理 __plugin_meta__ = PluginMetadata( name="天气查询", description="查询指定城市的实时天气与未来预报", usage="发送【天气 城市名】获取天气信息", type="application", homepage="https://github.com/你的用户名/nonebot-plugin-weather", config=Config, supported_adapters=inherit_supported_adapters("nonebot_plugin_alconna"), # 继承 nonebot_plugin_alconna 插件的适配器支持列表 ) ``` ### 准备项目主页 通常可以使用 GitHub 项目页面作为项目主页,在 `README.md` 文件中编写插件介绍等内容。 内容大致包括: - 插件功能介绍; - 安装方法 - **必须**有 NB-CLI 方式安装 - 可选依赖可以给出其他安装方式 - **不得**使用旧式的 `bot.py` 配置 - 插件配置项(如 `Config` 类字段,若无可跳过) - 插件设置的触发规则(若无可跳过) - 插件的其它用法(按需编写) - 效果图、权限说明(按需编写) ## 质量要求 以下内容**强烈建议**完成,否则社区成员将会要求修改: ### 依赖管理原则 - **必须**包含 `nonebot2`。 - **必须**将插件直接使用的适配器加入依赖列表,如:使用 OneBot 适配器的插件应添加 `nonebot-adapter-onebot` 依赖; - **禁止**使用 `==` 锁定单一版本,使用 `>=` 或 `~=`。 - **禁止**添加 `nonebot`(V1)作为依赖。 - 所有在代码中 `import` 的第三方库,必须在 `pyproject.toml` 的 `dependencies` 中列出。 ### 避免误用同步操作 NoneBot 是一个异步框架,插件中**禁止**使用任何可能阻塞事件循环的同步操作,例如: - 同步 HTTP 请求(如 `requests` 库); **推荐**操作(以 `httpx` 为例): ```python import httpx async with httpx.AsyncClient() as client: response = await client.get("https://api.example.com/data") # 异步操作,不阻塞机器人 ``` **禁止**操作: ```python import requests requests.get("https://api.example.com/data") # 同步操作,会阻塞机器人 ``` - 其他可能长时间运行阻塞事件循环的操作。 ### 本地文件存储 如果插件需要在本地存储数据、配置或缓存文件,**必须**使用 [`nonebot-plugin-localstore`](https://github.com/nonebot/plugin-localstore) 管理,具体细节参阅[本地存储](../best-practice/data-storing.md)章节。 参考示例: ```python title=nonebot_plugin_weather/__init__.py from pathlib import Path from nonebot import require require("nonebot_plugin_localstore") import nonebot_plugin_localstore as store # 获取插件缓存文件(夹)路径 weather_cache_dir: Path = store.get_plugin_cache_dir() weather_cache_file: Path = store.get_plugin_cache_file("cache.json") # 获取插件配置文件(夹)路径 weather_config_dir: Path = store.get_plugin_config_dir() weather_config_file: Path = store.get_plugin_config_file("config.toml") # 获取插件数据文件(夹)路径 weather_data_dir: Path = store.get_plugin_data_dir() weather_data_file: Path = store.get_plugin_data_file("resource-index.json") ``` ## 商店审核 ### 提交申请 完成在 PyPI 的插件发布流程后,前往[商店](/store/plugins)页面,切换到插件页签,点击 **发布插件** 按钮。 在弹出的插件信息提交表单内,填入您所要发布的相应插件信息。请注意,如果插件需要必要配置项才能正常导入,请在“插件配置项”中填写必要的内容(请勿填写密钥等敏感信息)。 完成填写后,点击 **发布** 按钮,这将自动跳转 NoneBot 仓库内的“发布插件”页面。确认信息无误后点击页面下方的 `Submit new issue` 按钮进行最终提交即可。 ### 等待插件审核 插件发布 Issue 创建后,将会经过 **NoneFlow Bot** 的自动前置检查,以确保插件信息正确无误、插件能被正确加载。 :::tip 提示 若插件检查未通过或信息有误,**不必**关闭当前 Issue。只需更新插件并上传到 PyPI/修改信息后勾选插件测试勾选框即可重新触发插件检查。 ::: 之后,NoneBot 的维护者和一些志愿者会初步检查插件代码,帮助减少该插件的问题。 完成这些步骤后,您的插件将会被自动合并到[商店](/store/plugins),而您也将成为 [**NoneBot 贡献者**](https://github.com/nonebot/nonebot2/graphs/contributors)的一员。 ================================================ FILE: website/versioned_docs/version-2.4.3/editor-support.md ================================================ --- 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` 前往编辑。 ================================================ FILE: website/versioned_docs/version-2.4.3/ospp/2021.md ================================================ --- sidebar_position: 0 description: 开源软件供应链点亮计划 - 暑期 2021 mdx: format: md --- # 暑期 2021 **开源软件供应链点亮计划 - 暑期 2021** 是**中国科学院软件研究所**与 **openEuler 社区**共同举办的一项面向高校学生的暑期活动,旨在鼓励在校学生积极参与开源软件的开发维护,促进优秀开源软件社区的蓬勃发展。关于具体的活动规划、报名方式,请查看该活动的 [官网](https://summer.iscas.ac.cn/) 和 [帮助文档](https://summer.iscas.ac.cn/help/)。 NoneBot 社区有幸作为开源社区参与了本次活动,下面列出了目前我们已经发布的项目,欢迎感兴趣的同学在上面给出的活动官网报名,或通过 [contact@nonebot.dev](mailto:contact@nonebot.dev) 联系我们。 ## NoneBot v1 ### 更新 NoneBot v1 文档中的“指南”部分 由于 NoneBot v1 和 aiocqhttp 最初基于的 QQ 机器人平台不再提供服务,CQHTTP 接口也转型且改名为 OneBot 标准,目前 NoneBot v1 文档的“指南”部分和 aiocqhttp 文档有部分过时内容需要更新。我们希望将其中与旧的机器人平台相关的内容改为基于 go-cqhttp 或通用的 OneBot 表述,同时对 NoneBot v1 的 awesome-bot 示例做一次全面检查,修改其中可能已经不可用的部分。 **难度**:低 **导师**:[@cleoold](https://github.com/cleoold) **产出要求** - 修改“指南”文档和 aiocqhttp 文档中与旧的 QQ 机器人平台相关的部分 - 检查 awesome-bot 示例是否有已经过时/不可用的地方,并更新/修复 - 修改“图灵机器人”案例,使用其它 AI 聊天 API 提供商(需先做简单调研) **技术要求** - 熟悉 Python 编程语言及 asyncio 机制 - 了解 Git 基本用法 - 了解聊天机器人基本开发过程 - 了解 VuePress 更佳 ### NoneBot v1 API 文档自动生成 目前 NoneBot v1 的文档中“API”部分是手动编写的,在更新代码接口的同时需要手动更新文档,可能造成文档与代码不匹配,形成额外的维护成本。我们希望将 API 文档改为直接编写在 Python docstring 中,通过工具自动生成 API 文档。 **难度**:中 **导师**:[@cleoold](https://github.com/cleoold) **产出要求** - 调研市面上常见的 Python API 文档生成工具 - 在代码中补充 API 文档 - 编写或应用开源工具自动生成 API 文档 - 配置 GitHub Actions 或其它 CI 自动化构建和部署 API 文档 **技术要求** - 熟悉 Python 编程语言及 asyncio 和 Type Hints - 了解 Git 基本用法 - 了解 Sphinx 等文档生成工具更佳 - 了解 GitHub Actions 等 CI 工具更佳 ## NoneBot v2 ### NoneBot v2 自动化测试框架“NoneBug” 在聊天机器人的开发过程中,一套自动化的测试机制是非常重要的,特别是对于 NoneBot 2 这类为大型机器人开发而设计的项目来说,需要手动测试每一个边际条件是非常痛苦的。我们希望能够开发一款基于 NoneBot 2 插件机制的自动化测试框架,为 NoneBot 2 用户提供一套易用便捷、高度灵活的自动化测试框架。 **难度**:高 **导师**:[@yanyongyu](https://github.com/yanyongyu) **产出要求** - 调研现有的 Python 和其它语言集成测试框架 - 设计 NoneBug 的用户 API 和实现方式 - 实现 NoneBug 自动化测试框架 - 编写详细的使用文档 **技术要求** - 熟悉 Python 编程语言及 asyncio 和 Type Hints - 了解 Git 基本用法 - 了解 NoneBot v2 的基本原理和使用方式 - 了解主流的 Python 自动化测试框架 ### NoneBot v2 Telegram 适配器 目前 NoneBot v2 已支持 OneBot、Mirai HTTP API、钉钉协议,社区反馈有更多的平台需求,希望能在 NoneBot v2 获得更多的跨平台支持,提高机器人的便携性。同时,我们也希望随着新平台加入,提升现有 NoneBot v2 核心代码的平台通用性。Telegram 是一款较为广泛使用的安全即时聊天软件,同时其官方提供了丰富的聊天机器人 API,因此我们希望为 NoneBot v2 编写一个 Telegram 适配器来支持 Telegram 机器人的开发。 **难度**:中 **导师**:[@yanyongyu](https://github.com/yanyongyu) **产出要求** - 调研 Telegram Bot API 以及 WebHook 等官方接口 - 编写 Telegram 适配器并能够使用 - 代码遵守项目 Contributing 规范 **技术要求** - 熟悉 Python 编程语言及 asyncio 和 Type Hints - 了解 Git 基本用法 - 了解 Web 开发相关知识 - 了解 Sphinx 等文档生成工具更佳 ### NoneBot v2 飞书适配器 目前 NoneBot v2 已支持 OneBot、Mirai HTTP API、钉钉协议,社区反馈有更多的平台需求,希望能在 NoneBot v2 获得更多的跨平台支持,提高机器人的便携性。同时,我们也希望随着新平台加入,提升现有 NoneBot v2 核心代码的平台通用性。飞书是目前企业用户广泛使用的即时聊天和协作软件,其官方提供了丰富的聊天机器人 API,因此我们希望为 NoneBot v2 编写一个飞书适配器来支持飞书机器人的开发。 **难度**:中 **导师**:[@yanyongyu](https://github.com/yanyongyu) **产出要求** - 调研飞书机器人 API 以及 WebHook 等官方接口 - 编写飞书适配器并能够使用 - 代码遵守项目 Contributing 规范 **技术要求** - 熟悉 Python 编程语言及 asyncio 和 Type Hints - 了解 Git 基本用法 - 了解 Web 开发相关知识 - 了解 Sphinx 等文档生成工具更佳 ## OneBot ### 设计 OneBot v12 接口标准 目前的 OneBot 标准的 v11 版本仍然与 QQ 平台有较多耦合,我们希望在 v12 去掉与 QQ 耦合的历史包袱,形成一个通用的、可扩展的、易于使用的同时易于实现的聊天机器人接口标准。 **难度**:中 **导师**:[@richardchien](https://github.com/richardchien) **产出要求** - 调研各聊天机器人平台的官方/非官方接口特点 - 通用化 OneBot 核心 API,分离 QQ 特定的 API,去掉无用 API - 优化现有的通信、消息表示机制 - 补充 QQ 特定的缺失 API - 文档需符合风格指南 **技术要求** - 熟悉至少两个聊天平台的聊天机器人开发 - 了解 Git 基本用法 - 了解使用不同语言编写聊天机器人时的常用实践 - 对文档的优雅性与美观性有追求更佳 ### 实现 Rust 版 libonebot 目前最常用的 OneBot 实现包括 go-cqhttp、onebot-kotlin、node-onebot 等,这些实现都各自重复实现了 Web 通信、消息解析、配置读写等功能,当社区中的开发者想针对一个新的聊天平台实现 OneBot 时,他们往往同样需要再次实现类似逻辑。我们希望使用 Rust 编写一个 libonebot 模块,该模块实现所有 OneBot 实现所共享的功能,从而方便其他开发者们使用 Rust 快速编写具体的 OneBot 实现。同时,我们希望借此项目在聊天机器人社区中推广 Rust 编程语言。 > 注:这里的逻辑是 libonebot + 针对某聊天平台的对接代码 = 某聊天平台的 OneBot 实现,libonebot 要做的是让 OneBot 实现的开发者只需编写针对特定聊天平台的对接代码,而无需关心 OneBot 标准定义的通信方式、消息格式等。 **难度**:高 **导师**:[@richardchien](https://github.com/richardchien) **产出要求** - 实现所有 OneBot 实现所共享的功能,包括 Web 通信、消息解析、配置读写等 - 充分考虑同时兼容 OneBot v11 和 v12 接口 - 能够根据用户(OneBot 实现的开发者)所实现的接口自动实现类似 get_available_apis 等接口 - 编写详细的使用文档 - 如果可能,与 v12 设计项目联动,实现第一手 v12 支持 **技术要求** - 熟悉聊天机器人开发 - 熟悉 Rust Web 开发 ### 实现自选语言版 libonebot 目前最常用的 OneBot 实现包括 go-cqhttp、onebot-kotlin、node-onebot 等,这些实现都各自重复实现了 Web 通信、消息解析、配置读写等功能,当社区中的开发者想针对一个新的聊天平台实现 OneBot 时,他们往往同样需要再次实现类似逻辑。我们希望使用 Python、Go、Kotlin、Node、PHP、C#.NET 等主流语言(任选一个)编写 libonebot 模块,该模块实现所有 OneBot 实现所共享的功能,从而方便其他开发者们使用对应语言快速编写具体的 OneBot 实现。 > 注:这里的逻辑是 libonebot + 针对某聊天平台的对接代码 = 某聊天平台的 OneBot 实现,libonebot 要做的是让 OneBot 实现的开发者只需编写针对特定聊天平台的对接代码,而无需关心 OneBot 标准定义的通信方式、消息格式等。 **难度**:中 **导师**:[@richardchien](https://github.com/richardchien) **产出要求** - 实现所有 OneBot 实现所共享的功能,包括 Web 通信、消息解析、配置读写等 - 充分考虑同时兼容 OneBot v11 和 v12 接口 - 编写详细的使用文档 - 如果可能,实现更多附加特性,如根据用户(OneBot 实现的开发者)所实现的接口自动实现类似 get_available_apis 等接口、实现第一手 v12 支持等 **技术要求** - 熟悉聊天机器人开发 - 熟悉所选语言的 Web 开发 ================================================ FILE: website/versioned_docs/version-2.4.3/ospp/2022.md ================================================ --- sidebar_position: 1 description: 开源之夏 - 暑期 2022 mdx: format: md --- # 暑期 2022 **开源之夏 - 暑期 2022** 是由**开源软件供应链点亮计划**发起、由**中国科学院软件研究所**与 **openEuler 社区**主办的一项面向高校学生的暑期活动,类似 Google Summer of Code(GSoC),旨在鼓励在校学生积极参与开源软件的开发维护,促进优秀开源软件社区的蓬勃发展。关于具体的活动规划、报名方式,请查看该活动的 [官网](https://summer-ospp.ac.cn/) 和 [帮助文档](https://summer-ospp.ac.cn/help/)。 NoneBot 社区有幸作为开源社区 [参与](https://summer-ospp.ac.cn/#/org/orgdetail/e1fb5b8d-125a-4138-b756-25bd32c0a31a/) 了本次活动,下面列出了目前我们已经发布的项目,欢迎感兴趣的同学加入 QQ 群 [737131827](https://jq.qq.com/?_wv=1027&k=PEgyGeEu) 或通过 [contact@nonebot.dev](mailto:contact@nonebot.dev) 联系我们。 ## NoneBot2 命令行 CLI 交互体验升级 NoneBot2 为用户提供了命令行脚手架 ──`nb-cli`,辅助用户更好地上手项目以及进行开发。nb-cli 主要包括:创建项目、运行项目、安装与卸载插件、部署项目等功能。随着 NoneBot2 Beta 版本的发布,脚手架功能存在一定的定位不明确、功能体验不佳。本项目旨在重新设计 nb-cli 功能框架,完善功能,优化用户体验。 **难度**:进阶 **导师**:[@yanyongyu](https://github.com/yanyongyu) **产出要求** - 设计 nb-cli 功能框架 - 明确各功能模块 - 设计用户交互模式 - 完成 nb-cli 主要功能代码 - 项目管理 - 插件管理 - 其它 - 同步更新使用文档 **技术要求** - 熟悉 Python 命令行交互代码编写 - 熟悉 NoneBot2 框架功能 - 熟悉 NoneBot2 项目组织方式 **成果仓库** - - ## NoneBot2 命令行即时交互通信设计与实现 NoneBot2 在早期提供了基于网页的 nonebot-plugin-test 插件,无需平台适配接入即可对机器人进行测试,方便了开发者直观的感受机器人文本交互功能。我们希望提供一款基于命令行的适配器/驱动器,用于无平台适配接入、可以运行机器人的场景进行功能体验或测试。 **难度**:进阶 **导师**:[@mnixry](https://github.com/mnixry) **产出要求** - 设计命令行与 NoneBot2 通信模式 - 直接调用/HTTP/WebSocket - 设计命令行交互界面 - 实现相应适配器/驱动器 - 同步更新使用说明文档 **技术要求** - 熟悉 Python 命令行交互代码编写 - 熟悉 NoneBot2 框架功能 - 熟悉 NoneBot2 项目组织方式 **成果仓库** - ## NoneBot2 用户上手与深入教程设计 NoneBot2 为用户提供了详细的文档介绍,辅助用户更好的上手项目以及进行开发。文档分为基础与进阶两个部分。基础部分帮助新用户快速上手开发,主要包括:安装 NoneBot2、使用脚手架、创建配置项目、使用适配器、加载插件、定义消息事件、处理消息事件、调用平台 API 等。进阶部分向已经熟悉开发流程的用户介绍更多高级技巧,主要包括:NoneBot2 工作原理、定时任务、权限控制、钩子函数、跨插件访问、单元测试、发布插件等。目前文档对于用户而言过于费解,导致用户难以理解 NoneBot2 开发。本项目旨在优化文档内容,使其更加通俗易懂,不让文档成为用户上手的阻碍,同时完善进阶内容,让有更复杂需求的用户,同样能从文档中受益。 相关 issue: - - **难度**:进阶 **导师**:[@SK-415](https://github.com/SK-415) **产出要求** - 文档通俗易懂 - 附有适当的图片指引(如 asciinema) - 内容完整,由浅入深 - 适当的界面美化,合理分配布局 **技术要求** - 熟悉文档结构组织与语言表达 - 熟悉 NoneBot2 框架功能 - 熟悉 NoneBot2 项目组织方式 **成果仓库** - ================================================ FILE: website/versioned_docs/version-2.4.3/ospp/2023.md ================================================ --- sidebar_position: 2 description: 开源之夏 - 暑期 2023 mdx: format: md --- # 暑期 2023 **开源之夏 - 暑期 2023** 是由**开源软件供应链点亮计划**发起、由**中国科学院软件研究所**与 **openEuler 社区**主办的一项面向高校学生的暑期活动,类似 Google Summer of Code(GSoC),旨在鼓励在校学生积极参与开源软件的开发维护,促进优秀开源软件社区的蓬勃发展。关于具体的活动规划、报名方式,请查看该活动的 [官网](https://summer-ospp.ac.cn/) 和 [帮助文档](https://summer-ospp.ac.cn/help/)。 NoneBot 社区有幸作为开源社区 [参与](https://summer-ospp.ac.cn/org/orgdetail/e1fb5b8d-125a-4138-b756-25bd32c0a31a?lang=zh) 了本次活动,下面列出了目前我们已经发布的项目,欢迎感兴趣的同学通过 [contact@nonebot.dev](mailto:contact@nonebot.dev) 联系我们。 ## NoneBot 项目管理图形化面板 NoneBot 目前提供了开箱即用的命令行脚手架来帮助初次使用的用户更快的上手编写应用。但是,对于未有一定开发经验的用户,命令行的使用仍具有一定的困难。此外,其他项目如 koishi、vue 等,均可通过图形化界面的形式为用户提供更便捷的项目开发。因此,我们希望借助现有命令行脚手架的可扩展特性,提供一个项目管理面板服务,以网页的形式帮助用户开发 NoneBot 应用。 **难度**:进阶 **导师**:[@mnixry](https://github.com/mnixry) **产出要求** - 设计并实现项目管理面板相关功能 - 创建与管理项目 - 配置与运行项目 - NoneBot 插件管理 - 实现相应 nb-cli 插件提供面板服务 - 代码符合 NoneBot Contributing 规范 **技术要求** - 熟悉 nb-cli 相关功能 - 熟悉 NoneBot 框架功能 - 熟悉前后端相关实现方式 **成果仓库** - ## NoneBot Discord 适配器 NoneBot 作为一个跨平台聊天机器人框架,目前已有 OneBot、飞书、Telegram、QQ 频道等诸多平台的适配支持。作为众多用户期待的平台适配之一,我们希望借此机会接入 Discord 聊天机器人。 **难度**:进阶 **导师**:[@iyume](https://github.com/iyume) **产出要求** - 调研 Discord Bot 相关功能与接口 - 设计与编写 NoneBot Discord 适配器 - 代码符合 NoneBot Contributing 规范 **技术要求** - 熟悉 NoneBot 框架功能 - 熟悉 NoneBot 各模块职责与适配器编写 **成果仓库** - ## NoneBot 数据库支持插件 NoneBot 的插件系统为用户实现应用提供了极高的便捷性,但因此也增加了插件统一管理的难度。目前,我们发现许多用户发布的插件中存在文件存储结构化数据、数据存放散乱等现象,同时插件间也可能产生冲突。因此,我们希望提供一个统一的数据存储与管理方式,便于用户读写应用数据。 **难度**:进阶 **导师**:[@yanyongyu](https://github.com/yanyongyu) **产出要求** - 设计并实现 ORM 插件 - 提供关系模型定义功能 - 提供模型迁移与管理功能 - 能较好的支持 Python 类型检查与推导 - 编写相应的用户使用文档 - 代码符合 NoneBot Contributing 规范 **技术要求** - 熟悉 NoneBot 框架功能与插件编写 - 熟悉 SQLAlchemy 等 ORM 框架 - 熟悉 SQLAlchemy ORM - 熟悉 alembic 等迁移工具 - 熟悉 nb-cli 插件编写 **成果仓库** - ================================================ FILE: website/versioned_docs/version-2.4.3/ospp/2024.md ================================================ --- sidebar_position: 3 description: 开源之夏 - 暑期 2024 mdx: format: md --- # 暑期 2024 **开源之夏 - 暑期 2024** 是**中国科学院软件研究所**发起的**开源软件供应链点亮计划**系列暑期活动,旨在鼓励高校学生积极参与开源软件的开发维护,促进优秀开源软件社区的蓬勃发展。活动联合各大开源社区,针对重要开源软件的开发与维护提供项目开发任务,并向全球高校学生开放报名。关于具体的活动规划、报名方式,请查看该活动的 [官网](https://summer-ospp.ac.cn/) 和 [帮助文档](https://summer-ospp.ac.cn/help/)。 NoneBot 社区有幸作为开源社区 [参与](https://summer-ospp.ac.cn/org/orgdetail/e1fb5b8d-125a-4138-b756-25bd32c0a31a?lang=zh) 了本次活动,下面列出了目前我们已经发布的项目,欢迎感兴趣的同学通过 [contact@nonebot.dev](mailto:contact@nonebot.dev) 联系我们。 ## NonePress 官网组件库更新与优化 NoneBot 官网目前采用基于 TailwindCSS 自研的 NonePress 组件库及 Docusaurus 框架进行构建。由于相关依赖版本迭代迅速,目前官网组件库已产生了较大的版本落后。本项目希望在跟进框架新版本的基础上,对文档整体视觉体验进行重新设计,提升页面的无障碍访问性,基于 React Hydrate 特性实现完整的静态网站生成(SSG)以提升搜索引擎优化(SEO)水平。在解决以上问题的基础上,可对网页的开发以及生产构建性能做相应的优化提升,例如在生产构建使用自有的 webpack loader、替换现有的热重载逻辑以减少开发环境启动耗时等。 **难度**:进阶 **导师**:[@yanyongyu](https://github.com/yanyongyu) **产出要求** - 基于 Docusaurus v3 重构 NonePress 组件库及相关插件 - 升级相关依赖并重新打造 Docusaurus theme(布局与组件) - 根据需求实现/修改 Docusaurus 插件使得官网内容构建正常 - 能够提升页面渲染性能与 MDX 相关能力 - 升级官网采用新版组件库 - Algolia 索引与 SEO 正常 - 桌面端与移动端显示正常 - 优化官网开发与生产构建体验 - (可选)优化官网部分页面 - 优化官网过长的 changelog - 优化官网插件商店的展示细节 **技术要求** - 熟练掌握 TS、PostCSS、TSX、MDX等相关技术 - 掌握 React、Docusaurus、tailwind css 等框架 - 熟悉静态网站生成 SSG、SEO 优化与 Algolia 索引原理等 **成果仓库** - ## NoneFlow 社区自动化工作流管理优化 NoneFlow 在 NoneBot 社区中承担着重要的角色,它由 NoneBot 框架基于 GitHub APP 编写而成,能够自动化的完成许多复杂流程的处理,如:用户请求提交插件到商店时进行自动化检测,并在人工审核通过后自动存储至 registry;定时自动更新 registry 内插件信息,跟进插件新版本情况等。但是,在长期的使用中发现了一些问题和不足的地方,例如:项目本身结构复杂耦合,添加新自动化流程与维护现有流程困难;目前采用了 GitHub 用户名作为插件作者名,但已有不少插件作者改名;插件存储至 registry 并定时更新,缺少统计相关信息以帮助商店更好的展示当前插件状态;插件作者想要修改插件信息时无法便捷的找到操作方式等。本项目希望针对以上问题与不足的地方进行修复与优化,提升用户体验。 **难度**:进阶 **导师**:[@uy/sun](https://github.com/he0119) **产出要求** - 重构现有工作流处理结构 - 整合现有 Issue、Pull Request、Git 相关操作 - 提供用户修改信息的处理方式 - 正确处理 PR 的 Open、Close、Draft 状态 - 修复流程中存在的问题 - 插件作者名正确展示 - registry 定时更新中需要插件测试环境隔离 - 在 registry 定时更新的同时提供统计数据 **技术要求** - 掌握 GitHub APP 开发 - 熟悉 GitHub REST API、GraphQL 等 - 熟悉 GitHub APP 权限限制 - 熟悉 NoneBot 框架与 Python 相关技术 - 熟悉 Git、GitHub Action、GitHub 工作流 **成果仓库** - ## NoneBlockly 低代码框架开发 经过深入分析社区反馈,我们发现部分新手因不熟悉编程概念或框架本身而遇到问题。为了解决初学者在使用面向开发者的聊天机器人框架 NoneBot 时遇到的挑战,我们计划引入 Blockly 提供低代码编程支持。通过减少常见的编码错误和降低入门门槛,使框架对初学者更加友好,从而提升用户体验并有助于 NoneBot 生态的成长。本项目将基于 Blockly 实现 NoneBot 插件的低代码编写,使得用户能够快速搭建聊天机器人。 **难度**:进阶 **导师**:[@mnixry](https://github.com/mnixry) **产出要求** - 实现 NoneBlockly 低代码开发框架 - 能够基于 Alconna 编写跨平台插件 - 确保插件对 Python 和 NoneBot 版本的兼容性 - 支持对多种类型 NoneBot 事件的响应 - 支持对 NoneBot 消息对象的便捷操作 - 集成 localstore 文件存储、apscheduler 定时任务、网络请求等常用功能 - 对接 NB-CLI 脚手架,通过脚手架扩展使用低代码框架 **技术要求** - 掌握 Python 与 NoneBot 框架的使用 - 熟悉 NoneBot 插件的开发,包括事件响应与消息处理等 - 熟悉 NoneBot 生态组件(Alconna、localstore、apscheduler等)的使用 - 了解 NB-CLI 脚手架的扩展开发 - 熟悉 Blockly 低代码框架的使用和开发 **成果仓库** - ================================================ FILE: website/versioned_docs/version-2.4.3/ospp/2025.md ================================================ --- sidebar_position: 4 description: 开源之夏 - 暑期 2025 mdx: format: md --- # 暑期 2025 **开源之夏 - 暑期 2025** 是**中国科学院软件研究所**发起的**开源软件供应链点亮计划**系列暑期活动,旨在鼓励高校学生积极参与开源软件的开发维护,培养和发掘更多优秀的开发者,促进优秀开源软件社区的蓬勃发展,助力开源软件供应链建设。活动联合各大开源社区,针对重要开源软件的开发与维护提供项目开发任务,并向全球高校学生开放报名。关于具体的活动规划、报名方式,请查看该活动的 [官网](https://summer-ospp.ac.cn/) 和 [帮助文档](https://summer-ospp.ac.cn/help/)。 NoneBot 社区有幸作为开源社区 [参与](https://summer-ospp.ac.cn/org/orgdetail/e1fb5b8d-125a-4138-b756-25bd32c0a31a?lang=zh) 了本次活动,下面列出了目前我们已经发布的项目,欢迎感兴趣的同学通过 [contact@nonebot.dev](mailto:contact@nonebot.dev) 联系我们。 ## NoneBot HTML 图片渲染插件 文字与图片一直是聊天机器人的两大主流交互方式,而图片的渲染一直是用户开发应用的一大痛点。常见的方式包括 PIL 图片编辑、浏览器渲染 HTML 截图等。PIL 图片编辑依赖人工构建图片布局,容易出现自适应问题,且提升图片特效、美观程度需要极大的开发成本。浏览器渲染方案通过 HTML 与 CSS 能够轻松完成美观自适应能力强的布局,但其部署门槛较高,难以支撑较大规模调用量。而其他轻量化渲染引擎通常不具有完整 HTML/CSS 现代化标准实现,且未提供 Python Binding 直接使用。 本项目希望调研并实现一种高效、便捷的图片渲染方案。该方案需要在保障跨平台一致性、最大程度保证 HTML 与 CSS 现代化标准的前提下,低成本(资源消耗与吞吐量)将 HTML 渲染为对应图片。 **难度**:进阶 **导师**:[@MelodyKnit](https://github.com/MelodyKnit) **产出要求** - 调研 HTML/CSS 渲染引擎 - 调研 litehtml 等渲染引擎 标准支持能力与兼容性 - 基于渲染引擎实现 HTML 图片渲染插件 - 将渲染引擎通过 binding 等方式集成为 Python 模块 - 基于集成模块实现 HTML 图片渲染能力 - 编写插件使用文档 **技术要求** - 掌握 Python 及其异步编程 - 熟悉 NoneBot 框架及其插件编写 - 了解浏览器与 HTML 渲染原理 **成果仓库** - ## NB-CLI 命令行工具交互优化 NB-CLI 作为 NoneBot 生态的核心入门与管理工具,主要负责新手引导项目创建、项目运行以及插件管理几大功能。目前该脚手架工具仍存在几点缺陷: - 作为插件管理工具,由于存储数据的局限性,无法很好地展示用户项目当前安装插件状态,并进行卸载等操作; - 当前插件管理高度依赖云端 registry 提供插件信息,在离线情况下完全无法使用; - 由于插件信息繁多,工具未能向用户展示充分的信息,交互复杂 体验较差。 以上问题对用户使用 NB-CLI 管理项目插件造成了极大的阻碍。 本项目希望重点针对插件管理部分,重构工具插件管理模块,完善框架缺陷,并通过缓存等方式确保可用性。其次,调研同类工具方案与 TUI 等相关技术,优化信息展示能力、用户交互方式,提升工具整体交互体验。 **相关链接** - https://github.com/nonebot/nb-cli/issues/138 - https://github.com/nonebot/nb-cli/issues/140 **难度**:基础 **导师**:[@yanyongyu](https://github.com/yanyongyu) **产出要求** - 重构 NB-CLI 插件管理模块 - 优化项目插件信息存储方式,支持列出、卸载插件等操作 - 通过缓存 registry 数据等方式确保离线场景的可用性 - 提升 NB-CLI 交互体验 - 调研同类工具方案与 TUI 等相关技术 - 优化 registry 多字段信息展示能力 - 基于 TUI 等技术优化用户交互方式,提升整体交互体验 **技术要求** - 熟练掌握 Python 及其异步编程 - 熟悉 NoneBot 框架与 NB-CLI 使用方法 - 了解 TUI 等终端交互技术 **成果仓库** - ================================================ FILE: website/versioned_docs/version-2.4.3/quick-start.mdx ================================================ --- sidebar_position: 1 description: 尝试使用 NoneBot options: menu: - category: tutorial weight: 10 --- import Asciinema from "@site/src/components/Asciinema"; import Messenger from "@site/src/components/Messenger"; # 快速上手 :::caution 前提条件 - 请确保你的 Python 版本 >= 3.9 - **我们强烈建议使用虚拟环境进行开发**,如果没有使用虚拟环境,请确保已经卸载可能存在的 NoneBot v1!!! ```bash pip uninstall nonebot ``` ::: 在本章节中,我们将介绍如何使用脚手架来创建一个 NoneBot 简易项目。项目将基于 nb-cli 脚手架运行,并允许我们从商店安装插件。 ## 安装脚手架 确保你已经安装了 Python 3.9 及以上版本,然后在命令行中执行以下命令: 1. 安装 [pipx](https://pypa.github.io/pipx/) ```bash python -m pip install --user pipx python -m pipx ensurepath ``` 如果在此步骤的输出中出现了“open a new terminal”或者“re-login”字样,那么请关闭当前终端并重新打开一个新的终端。 2. 安装脚手架 ```bash pipx install nb-cli ``` 安装完成后,你可以在命令行使用 `nb` 命令来使用脚手架。如果出现无法找到命令的情况(例如出现“Command not found”字样),请参考 [pipx 文档](https://pypa.github.io/pipx/) 检查你的环境变量。 ## 创建项目 使用脚手架来创建一个项目: ```bash nb create ``` 这一指令将会执行创建项目的流程,你将会看到一些询问: 1. 项目模板 ```bash [?] 选择一个要使用的模板: bootstrap (初学者或用户) ``` 这里我们选择 `bootstrap` 模板,它是一个简单的项目模板,能够安装商店插件。如果你需要**自行编写插件**,这里请选择 `simple` 模板。 2. 项目名称 ```bash [?] 项目名称: awesome-bot ``` 这里我们以 `awesome-bot` 为例,作为项目名称。你可以根据自己的需要来命名。 3. 其他选项 请注意,多选项使用**空格**选中或取消,**回车**确认。 ```bash [?] 要使用哪些驱动器? FastAPI (FastAPI 驱动器) [?] 要使用哪些适配器? Console (基于终端的交互式适配器) [?] 立即安装依赖? (Y/n) Yes [?] 创建虚拟环境? (Y/n) Yes ``` 这里我们选择了创建虚拟环境,nb-cli 在之后的操作中将会自动使用这个虚拟环境。如果你不需要自动创建虚拟环境或者已经创建了其他虚拟环境,nb-cli 将会安装依赖至当前激活的 Python 虚拟环境。 4. 选择内置插件 ```bash [?] 要使用哪些内置插件? echo ``` 这里我们选择 `echo` 插件作为示例。这是一个简单的复读回显插件,可以用于测试你的机器人是否正常运行。 ## 运行项目 在项目创建完成后,你可以在**项目目录**中使用以下命令来运行项目: ```bash nb run ``` 你现在应该已经运行起来了你的第一个 NoneBot 项目了!请注意,生成的项目中使用了 `FastAPI` 驱动器和 `Console` 适配器,你之后可以自行修改配置或安装其他适配器。 ## 尝试使用 在项目运行起来后,`Console` 适配器会在你的终端启动交互模式,你可以直接在输入框中输入 `/echo hello world` 来测试你的机器人是否正常运行。 ================================================ FILE: website/versioned_docs/version-2.4.3/tutorial/application.md ================================================ --- sidebar_position: 0 description: 创建一个 NoneBot 项目 options: menu: - category: tutorial weight: 20 --- # 手动创建项目 在[快速上手](../quick-start.mdx)中,我们已经介绍了如何安装和使用 `nb-cli` 创建一个项目。在本章节中,我们将简要介绍如何在不使用 `nb-cli` 的方式创建一个机器人项目的**最小实例**并启动。如果你想要了解 NoneBot 的启动流程,也可以阅读本章节。 :::caution 警告 我们十分不推荐直接创建机器人项目,请优先考虑使用 nb-cli 进行项目创建。 ::: 一个机器人项目的**最小实例**中**至少**需要包含以下内容: - 入口文件:初始化并运行机器人的 Python 文件 - 配置文件:存储机器人启动所需的配置 - 插件:为机器人提供具体的功能 下面我们创建一个项目文件夹,来存放项目所需文件,以下步骤均在该文件夹中进行。 ## 安装依赖 在创建项目前,我们首先需要将项目所需依赖安装至环境中。 1. (可选)创建虚拟环境,以 venv 为例 ```bash python -m venv .venv --prompt nonebot2 # windows .venv\Scripts\activate # linux/macOS source .venv/bin/activate ``` 2. 安装 nonebot2 以及驱动器 ```bash pip install 'nonebot2[fastapi]' ``` 驱动器包名可以在 [驱动器商店](/store/drivers) 中找到。 3. 安装适配器 ```bash pip install nonebot-adapter-console ``` 适配器包名可以在 [适配器商店](/store/adapters) 中找到。 ## 创建配置文件 配置文件用于存放 NoneBot 运行所需要的配置项,使用 [`pydantic`](https://docs.pydantic.dev/) 以及 [`python-dotenv`](https://saurabh-kumar.com/python-dotenv/) 来读取配置。配置项需符合 dotenv 格式,复杂类型数据需使用 JSON 格式填写。具体可选配置方式以及配置项详情参考[配置](../appendices/config.mdx)。 在**项目文件夹**中创建一个 `.env` 文本文件,并写入以下内容: ```bash title=.env HOST=0.0.0.0 # 配置 NoneBot 监听的 IP / 主机名 PORT=8080 # 配置 NoneBot 监听的端口 COMMAND_START=["/"] # 配置命令起始字符 COMMAND_SEP=["."] # 配置命令分割字符 ``` ## 创建入口文件 入口文件( Entrypoint )顾名思义,是用来初始化并运行机器人的 Python 文件。入口文件需要完成框架的初始化、注册适配器、加载插件等工作。 :::tip 提示 如果你使用 `nb-cli` 创建项目,入口文件不会被创建,该文件功能会被 `nb run` 命令代替。 ::: 在**项目文件夹**中创建一个 `bot.py` 文件,并写入以下内容: ```python title=bot.py import nonebot from nonebot.adapters.console import Adapter as ConsoleAdapter # 避免重复命名 # 初始化 NoneBot nonebot.init() # 注册适配器 driver = nonebot.get_driver() driver.register_adapter(ConsoleAdapter) # 在这里加载插件 nonebot.load_builtin_plugins("echo") # 内置插件 # nonebot.load_plugin("thirdparty_plugin") # 第三方插件 # nonebot.load_plugins("awesome_bot/plugins") # 本地插件 if __name__ == "__main__": nonebot.run() ``` 我们暂时不需要了解其中内容的含义,这些将会在稍后的章节中逐一介绍。在创建完成以上文件并确认已安装所需适配器和插件后,即可运行机器人。 ## 运行机器人 在**项目文件夹**中,使用配置好环境的 Python 解释器运行入口文件(如果使用虚拟环境,请先激活虚拟环境): ```bash python bot.py ``` 如果你后续使用了 `nb-cli` ,你仍可以使用 `nb run` 命令来运行机器人,`nb-cli` 会自动检测入口文件 `bot.py` 是否存在并运行。同时,你也可以使用 `nb run --reload` 来自动检测代码的更改并自动重新运行入口文件。 ================================================ FILE: website/versioned_docs/version-2.4.3/tutorial/create-plugin.md ================================================ --- sidebar_position: 3 description: 创建并加载自定义插件 options: menu: - category: tutorial weight: 50 --- # 插件编写准备 在正式编写插件之前,我们需要先了解一下插件的概念。 ## 插件结构 在 NoneBot 中,插件即是 Python 的一个[模块(module)](https://docs.python.org/zh-cn/3/glossary.html#term-module)。NoneBot 会在导入时对这些模块做一些特殊的处理使得他们成为一个插件。插件间应尽量减少耦合,可以进行有限制的相互调用,NoneBot 能够正确解析插件间的依赖关系。 ### 单文件插件 一个普通的 `.py` 文件即可以作为一个插件,例如创建一个 `foo.py` 文件: ```tree title=Project 📂 plugins └── 📜 foo.py ``` 这个时候模块 `foo` 已经可以被称为一个插件了,尽管它还什么都没做。 ### 包插件 一个包含 `__init__.py` 的文件夹即是一个常规 Python [包 `package`](https://docs.python.org/zh-cn/3/glossary.html#term-regular-package),例如创建一个 `foo` 文件夹: ```tree title=Project 📂 plugins └── 📂 foo └── 📜 __init__.py ``` 这个时候包 `foo` 同样是一个合法的插件,插件内容可以在 `__init__.py` 文件中编写。 ## 创建插件 :::caution 注意 如果在之前的[快速上手](../quick-start.mdx)章节中已经使用 `bootstrap` 模板创建了项目,那么你需要做出如下修改: 1. 在项目目录中创建一个两层文件夹 `awesome_bot/plugins` ```tree title=Project 📦 awesome-bot ├── 📂 .venv ├── 📂 awesome_bot │ └── 📂 plugins ├── 📜 .env.prod ├── 📜 pyproject.toml └── 📜 README.md ``` 2. 修改 `pyproject.toml` 文件中的 `nonebot` 配置项,在 `plugin_dirs` 中添加 `awesome_bot/plugins` ```toml title=pyproject.toml [tool.nonebot] plugin_dirs = ["awesome_bot/plugins"] ``` ::: :::caution 注意 如果在之前的[创建项目](./application.md)章节中手动创建了相关文件,那么你需要做出如下修改: 1. 在项目目录中创建一个两层文件夹 `awesome_bot/plugins` ```tree title=Project 📦 awesome-bot ├── 📂 awesome_bot │ └── 📂 plugins └── 📜 bot.py ``` 2. 修改 `bot.py` 文件中的加载插件部分,取消注释或者添加如下代码 ```python title=bot.py # 在这里加载插件 nonebot.load_builtin_plugins("echo") # 内置插件 nonebot.load_plugins("awesome_bot/plugins") # 本地插件 ``` ::: 创建插件可以通过 `nb-cli` 命令从完整模板创建,也可以手动新建空白文件。通过以下命令创建一个名为 `weather` 的插件: ```bash $ nb plugin create [?] 插件名称: weather [?] 使用嵌套插件? (y/N) N [?] 请输入插件存储位置: awesome_bot/plugins ``` `nb-cli` 会在 `awesome_bot/plugins` 目录下创建一个名为 `weather` 的文件夹,其中包含的文件将在稍后章节中用到。 ```tree title=Project 📦 awesome-bot ├── 📂 .venv ├── 📂 awesome_bot │ └── 📂 plugins | └── 📂 weather | ├── 📜 __init__.py | └── 📜 config.py ├── 📜 .env.prod ├── 📜 pyproject.toml └── 📜 README.md ``` ## 加载插件 :::danger 警告 请勿在插件被加载前 `import` 插件模块,这会导致 NoneBot 无法将其转换为插件而出现意料之外的情况。 ::: 加载插件是在机器人入口文件中完成的,需要在框架初始化之后,运行之前进行。 请注意,加载的插件模块名称(插件文件名或文件夹名)**不能相同**,且每一个插件**只能被加载一次**,重复加载将会导致异常。 如果你使用 `nb-cli` 管理插件,那么你可以跳过这一节,`nb-cli` 将会自动处理加载。 如果你**使用自定义的入口文件** `bot.py`,那么你需要在 `bot.py` 中加载插件。 ```python {5} title=bot.py import nonebot nonebot.init() # 加载插件 nonebot.run() ``` 加载插件的方式有多种,但在底层的加载逻辑是一致的。以下是为加载插件提供的几种方式: ### `load_plugin` 通过点分割模块名称或使用 [`pathlib`](https://docs.python.org/zh-cn/3/library/pathlib.html) 的 `Path` 对象来加载插件。通常用于加载第三方插件或者项目插件。例如: ```python from pathlib import Path nonebot.load_plugin("path.to.your.plugin") # 加载第三方插件 nonebot.load_plugin(Path("./path/to/your/plugin.py")) # 加载项目插件 ``` :::caution 注意 请注意,本地插件的路径应该为相对机器人 **入口文件(通常为 bot.py)** 可导入的,例如在项目 `plugins` 目录下。 ::: ### `load_plugins` 加载传入插件目录中的所有插件,通常用于加载一系列本地编写的项目插件。例如: ```python nonebot.load_plugins("src/plugins", "path/to/your/plugins") ``` :::caution 注意 请注意,插件目录应该为相对机器人 **入口文件(通常为 bot.py)** 可导入的,例如在项目 `plugins` 目录下。 ::: ### `load_all_plugins` 这种加载方式是以上两种方式的混合,加载所有传入的插件模块名称,以及所有给定目录下的插件。例如: ```python nonebot.load_all_plugins(["path.to.your.plugin"], ["path/to/your/plugins"]) ``` ### `load_from_json` 通过 JSON 文件加载插件,是 [`load_all_plugins`](#load_all_plugins) 的 JSON 变种。通过读取 JSON 文件中的 `plugins` 字段和 `plugin_dirs` 字段进行加载。例如: ```json title=plugin_config.json { "plugins": ["path.to.your.plugin"], "plugin_dirs": ["path/to/your/plugins"] } ``` ```python nonebot.load_from_json("plugin_config.json", encoding="utf-8") ``` :::tip 提示 如果 JSON 配置文件中的字段无法满足你的需求,可以使用 [`load_all_plugins`](#load_all_plugins) 方法自行读取配置来加载插件。 ::: ### `load_from_toml` 通过 TOML 文件加载插件,是 [`load_all_plugins`](#load_all_plugins) 的 TOML 变种。通过读取 TOML 文件中的 `[tool.nonebot]` Table 中的 `plugin_dirs` Array 与 `[tool.nonebot.plugins]` Table 中的多个 Array 进行加载。例如: ```toml title=plugin_config.toml [tool.nonebot] plugin_dirs = ["path/to/your/plugins"] [tool.nonebot.plugins] "@local" = ["path.to.your.plugin"] # 本地插件等非插件商店来源的插件 "nonebot-plugin-someplugin" = ["nonebot_plugin_someplugin"] # 插件商店来源的插件 ``` ```python nonebot.load_from_toml("plugin_config.toml", encoding="utf-8") ``` :::tip 提示 如果 TOML 配置文件中的字段无法满足你的需求,可以使用 [`load_all_plugins`](#load_all_plugins) 方法自行读取配置来加载插件。 ::: ### `load_builtin_plugin` 加载一个内置插件,传入的插件名必须为 NoneBot 内置插件。该方法是 [`load_plugin`](#load_plugin) 的封装。例如: ```python nonebot.load_builtin_plugin("echo") ``` ### `load_builtin_plugins` 加载传入插件列表中的所有内置插件。例如: ```python nonebot.load_builtin_plugins("echo", "single_session") ``` ### 其他加载方式 有关其他插件加载的方式,可参考[跨插件访问](../advanced/requiring.md)和[嵌套插件](../advanced/plugin-nesting.md)。 ================================================ FILE: website/versioned_docs/version-2.4.3/tutorial/event-data.mdx ================================================ --- sidebar_position: 6 description: 通过依赖注入获取所需事件信息 options: menu: - category: tutorial weight: 80 --- # 获取事件信息 import Messenger from "@site/src/components/Messenger"; 在 NoneBot 事件处理流程中,获取事件信息并做出对应的操作是非常常见的场景。本章节中我们将介绍如何通过**依赖注入**获取事件信息。 ## 认识依赖注入 在事件处理流程中,事件响应器具有自己独立的上下文,例如:当前响应的事件、收到事件的机器人或者其他处理流程中新增的信息等。这些数据可以根据我们的需求,通过依赖注入的方式,在执行事件处理流程中注入到事件处理函数中。 相对于传统的信息获取方法,通过依赖注入获取信息的最大特色在于**按需获取**。如果该事件处理函数不需要任何额外信息即可运行,那么可以不进行依赖注入。如果事件处理函数需要额外的数据,可以通过依赖注入的方式灵活的标注出需要的依赖,在函数运行时便会被按需注入。 ## 使用依赖注入 使用依赖注入获取上下文信息的方法十分简单,我们仅需要在函数的参数中声明所需的依赖,并正确的将函数添加为事件处理依赖即可。在 NoneBot 中,我们可以直接使用 `nonebot.params` 模块中定义的参数类型来声明依赖。 例如,我们可以继续改进上一章节中的 `weather` 插件,使其可以获取到 `天气` 命令的地名参数,并根据地名返回天气信息。 ```python {9,11} title=weather/__init__.py from nonebot import on_command from nonebot.rule import to_me from nonebot.adapters import Message from nonebot.params import CommandArg weather = on_command("天气", rule=to_me(), aliases={"weather", "查天气"}, priority=10, block=True) @weather.handle() async def handle_function(args: Message = CommandArg()): # 提取参数纯文本作为地名,并判断是否有效 if location := args.extract_plain_text(): await weather.finish(f"今天{location}的天气是...") else: await weather.finish("请输入地名") ``` 如上方示例所示,我们使用了 `args` 作为注入参数名,注入的内容为 `CommandArg()`,也就是**消息命令后跟随的内容**。在这个示例中,我们获得的参数会被检查是否有效,对无效参数则会结束事件。 :::tip 提示 命令与参数之间可以不需要空格,`CommandArg()` 获取的信息为命令后跟随的内容并去除了头部空白符。例如:`/天气 上海` 消息的参数为 `上海`。 ::: :::tip 提示 `:=` 是 Python 3.8 引入的新语法 [Assignment Expressions](https://docs.python.org/zh-cn/3/reference/expressions.html#assignment-expressions),也称为海象表达式,可以在表达式中直接赋值。 ::: NoneBot 提供了多种依赖注入类型,可以获取不同的信息,具体内容可参考[依赖注入](../advanced/dependency.mdx)。 ================================================ FILE: website/versioned_docs/version-2.4.3/tutorial/fundamentals.md ================================================ --- sidebar_position: 1 description: NoneBot 机器人构成及基本使用 options: menu: - category: tutorial weight: 30 --- # 机器人的构成 了解机器人的基本构成有助于你更好地使用 NoneBot,本章节将介绍 NoneBot 中的基本组成部分,稍后的文档中将会使用到这些概念。 使用 NoneBot 框架搭建的机器人具有以下几个基本组成部分: 1. NoneBot 机器人框架主体:负责连接各个组成部分,提供基本的机器人功能 2. 驱动器 `Driver`:客户端/服务端的功能实现,负责接收和发送消息(通常为 HTTP 通信) 3. 适配器 `Adapter`:驱动器的上层,负责将**平台消息**与 NoneBot 事件/操作系统的消息格式相互转换 4. 插件 `Plugin`:机器人的功能实现,通常为负责处理事件并进行一系列的操作 除 NoneBot 机器人框架主体外,其他部分均可按需选择、互相搭配,但由于平台的兼容性问题,部分插件可能仅在某些特定平台上可用(这由插件编写者决定)。 在接下来的章节中,我们将重点介绍机器人功能实现,即插件 `Plugin` 部分。 ================================================ FILE: website/versioned_docs/version-2.4.3/tutorial/handler.mdx ================================================ --- sidebar_position: 5 description: 处理接收到的特定事件 options: menu: - category: tutorial weight: 70 --- # 事件处理 import Messenger from "@site/src/components/Messenger"; 在我们收到事件,并被某个事件响应器正确响应后,便正式开启了对于这个事件的**处理流程**。 ## 认识事件处理流程 就像我们在解决问题时需要遵循流程一样,处理一个事件也需要一套流程。在事件响应器对一个事件进行响应之后,会依次执行一系列的**事件处理依赖**(通常是函数)。简单来说,事件处理流程并不是一个函数、一个对象或一个方法,而是一整套由开发者设计的流程。 在这个流程中,我们**目前**只需要了解两个概念:函数形式的“事件处理依赖”(下称“事件处理函数”)和“事件响应器操作”。 ## 事件处理函数 在事件响应器中,事件处理流程可以由一个或多个“事件处理函数”组成,这些事件处理函数将会按照顺序依次对事件进行处理,直到全部执行完成或被中断。我们可以采用事件响应器的“事件处理函数装饰器”来添加这些“事件处理函数”。 顾名思义,“事件处理函数装饰器”是一个[装饰器(decorator)](https://docs.python.org/zh-cn/3/glossary.html#term-decorator),那么它的使用方法也同[函数定义](https://docs.python.org/zh-cn/3/reference/compound_stmts.html#function-definitions)中所展示的包装用法相同。例如: ```python {6-8} title=weather/__init__.py from nonebot import on_command from nonebot.rule import to_me weather = on_command("天气", rule=to_me(), aliases={"weather", "查天气"}, priority=10, block=True) @weather.handle() async def handle_function(): pass # do something here ``` 如上方示例所示,我们使用 `weather` 响应器的 `handle` 装饰器装饰了一个函数 `handle_function`。`handle_function` 函数会被添加到 `weather` 的事件处理流程中。在 `weather` 响应器被触发之后,将会依次调用 `weather` 响应器的事件处理函数,即 `handle_function` 来对事件进行处理。 ## 事件响应器操作 在事件处理流程中,我们可以使用事件响应器操作来进行一些交互或改变事件处理流程,例如向机器人用户发送消息或提前结束事件处理流程等。 事件响应器操作与事件处理函数装饰器类似,通常作为事件响应器 `Matcher` 的[类方法](https://docs.python.org/zh-cn/3/library/functions.html#classmethod)存在,因此事件响应器操作的调用方法也是 `Matcher.func()` 的形式。不过不同的是,事件响应器操作并不是装饰器,因此并不需要@进行标注。 ```python {8,9} title=weather/__init__.py from nonebot import on_command from nonebot.rule import to_me weather = on_command("天气", rule=to_me(), aliases={"weather", "查天气"}, priority=10, block=True) @weather.handle() async def handle_function(): # await weather.send("天气是...") await weather.finish("天气是...") ``` 如上方示例所示,我们使用 `weather` 响应器的 `finish` 操作方法向机器人用户回复了 `天气是...` 并结束了事件处理流程。效果如下: 值得注意的是,在执行 `finish` 方法时,NoneBot 会在向机器人用户发送消息内容后抛出 `FinishedException` 异常来结束事件响应流程。也就是说,在 `finish` 被执行后,后续的程序是不会被执行的。如果你需要回复机器人用户消息但不想事件处理流程结束,可以使用注释的部分中展示的 `send` 方法。 :::danger 警告 由于 `finish` 是通过抛出 `FinishedException` 异常来结束事件的,因此异常可能会被未加限制的 `try-except` 捕获,影响事件处理流程正确处理,导致无法正常结束此事件。请务必在异常捕获中指定错误类型或排除所有 [MatcherException](../api/exception.md#MatcherException) 类型的异常(如下所示),或将 `finish` 移出捕获范围进行使用。 ```python from nonebot.exception import MatcherException try: await weather.finish("天气是...") except MatcherException: raise except Exception as e: pass # do something here ``` ::: 目前 NoneBot 提供了多种事件响应器操作,其中包括用于机器人用户交互与流程控制两大类,进阶使用方法可以查看[会话控制](../appendices/session-control.mdx)。 ================================================ FILE: website/versioned_docs/version-2.4.3/tutorial/matcher.md ================================================ --- sidebar_position: 4 description: 响应接收到的特定事件 options: menu: - category: tutorial weight: 60 --- # 事件响应器 事件响应器(Matcher)是对接收到的事件进行响应的基本单元,所有的事件响应器都继承自 `Matcher` 基类。 在 NoneBot 中,事件响应器可以通过一系列特定的规则**筛选**出**具有某种特征的事件**,并按照**特定的流程**交由**预定义的事件处理依赖**进行处理。例如,在[快速上手](../quick-start.mdx)中,我们使用了内置插件 `echo` ,它定义的事件响应器能响应机器人用户发送的“/echo hello world”消息,提取“hello world”信息并作为回复消息发送。 ## 事件响应器辅助函数 NoneBot 中所有事件响应器均继承自 `Matcher` 基类,但直接使用 `Matcher.new()` 方法创建事件响应器过于繁琐且不能记录插件信息。因此,NoneBot 中提供了一系列“事件响应器辅助函数”(下称“辅助函数”)来辅助我们用**最简的方式**创建**带有不同规则预设**的事件响应器,提高代码可读性和书写效率。通常情况下,我们只需要使用辅助函数即可完成事件响应器的创建。 在 NoneBot 中,辅助函数以 `on()` 或 `on_()` 形式出现(例如 `on_command()`),调用后根据不同的参数返回一个 `Type[Matcher]` 类型的新事件响应器。 目前 NoneBot 提供了多种功能各异的辅助函数、具有共同命令名称前缀的命令组以及具有共同参数的响应器组,均可以从 `nonebot` 模块直接导入使用,具体内容参考[事件响应器进阶](../advanced/matcher.md)。 ## 创建事件响应器 在上一节[创建插件](./create-plugin.md#创建插件)中,我们创建了一个 `weather` 插件,现在我们来实现他的功能。 我们直接使用 `on_command()` 辅助函数来创建一个事件响应器: ```python {3} title=weather/__init__.py from nonebot import on_command weather = on_command("天气") ``` 这样,我们就获得一个名为 `weather` 的事件响应器了,这个事件响应器会对 `/天气` 开头的消息进行响应。 :::tip 提示 如果一条消息中包含“@机器人”或以“机器人的昵称”开始,例如 `@bot /天气` 时,协议适配器会将 `event.is_tome()` 判断为 `True` ,同时也会自动去除 `@bot`,即事件响应器收到的信息内容为 `/天气`,方便进行命令匹配。 ::: ### 为事件响应器添加参数 在辅助函数中,我们可以添加一些参数来对事件响应器进行更加精细的调整,例如事件响应器的优先级、匹配规则等。例如: ```python {4} title=weather/__init__.py from nonebot import on_command from nonebot.rule import to_me weather = on_command("天气", rule=to_me(), aliases={"weather", "查天气"}, priority=10, block=True) ``` 这样,我们就获得了一个可以响应 `天气`、`weather`、`查天气` 三个命令的响应规则,需要私聊或 `@bot` 时才会响应,优先级为 10(越小越优先),阻断事件向后续优先级传播的事件响应器了。这些内容的意义和使用方法将会在后续的章节中一一介绍。 :::tip 提示 需要注意的是,不同的辅助函数有不同的可选参数,在使用之前可以参考[事件响应器进阶 - 基本辅助函数](../advanced/matcher.md#基本辅助函数)或 [API 文档](../api/plugin/on.md#on)。 ::: ================================================ FILE: website/versioned_docs/version-2.4.3/tutorial/message.md ================================================ --- sidebar_position: 7 description: 处理消息序列与消息段 options: menu: - category: tutorial weight: 90 --- # 处理消息 在不同平台中,一条消息可能会有承载有各种不同的表现形式,它可能是一段纯文本、一张图片、一段语音、一篇富文本文章,也有可能是多种类型的组合等等。 在 NoneBot 中,为确保消息的正常处理与跨平台兼容性,采用了扁平化的消息序列形式,即 `Message` 对象。消息序列是 NoneBot 中的消息载体,无论是接收还是发送的消息,都采用消息序列的形式进行处理。 ## 认识消息类型 ### 消息序列 `Message` 在 NoneBot 中,消息序列 `Message` 的主要作用是用于表达“一串消息”。由于消息序列继承自 `List[MessageSegment]`,所以 `Message` 的本质是由若干消息段所组成的序列。因此,消息序列的使用方法与 `List` 有很多相似之处,例如切片、索引、拼接等。 在上一节的[使用依赖注入](./event-data.mdx#使用依赖注入)中,我们已经通过依赖注入 `CommandArg()` 获取了命令的参数,它的类型即是消息序列。我们使用了消息序列的 `extract_plain_text()` 方法来获取消息序列中的纯文本内容。 ### 消息段 `MessageSegment` 顾名思义,消息段 `MessageSegment` 是一段消息。由于消息序列的本质是由若干消息段所组成的序列,消息段可以被认为是构成消息序列的最小单位。简单来说,消息序列类似于一个自然段,而消息段则是组成自然段的一句话。同时,作为特殊消息载体的存在,绝大多数的平台都有着**独特的消息类型**,这些独特的内容均需要由对应的**协议适配器**所提供,以适应不同平台中的消息模式。**这也意味着,你需要导入对应的协议适配器中的消息序列和消息段后才能使用其特殊的工厂方法。** :::caution 注意 消息段的类型是由协议适配器提供的,因此你需要参考协议适配器的文档并导入对应的消息段后才能使用其特殊的消息类型。 在上一节的[使用依赖注入](./event-data.mdx#使用依赖注入)中,我们导入的为 `nonebot.adapters.Message` 抽象基类,因此我们无法使用平台特有的消息类型。仅能使用 `str` 作为纯文本消息回复。 ::: ## 使用消息序列 :::caution 注意 在以下的示例中,为了更好的理解多种类型的消息组成方式,我们将使用 `Console` 协议适配器来演示消息序列的使用方法。在实际使用中,你需要确保你使用的**消息序列类型**与你所要发送的**平台类型**一致。 ::: 通常情况下,适配器在接收到消息时,会将消息转换为消息序列,可以通过依赖注入 [`EventMessage`](../advanced/dependency.mdx#eventmessage),或者使用 `event.get_message()` 获取。 由于消息序列是 `List[MessageSegment]` 的子类,所以你总是可以用和操作 `List` 类似的方式来处理消息序列。例如: ```python >>> from nonebot.adapters.console import Message, MessageSegment >>> message = Message([ MessageSegment(type="text", data={"text":"hello"}), MessageSegment(type="markdown", data={"markup":"**world**"}), ]) >>> for segment in message: ... print(segment.type, segment.data) ... text {'text': 'hello'} markdown {'markup': '**world**'} >>> len(message) 2 ``` ### 构造消息序列 在使用事件响应器操作发送消息时,既可以使用 `str` 作为消息,也可以使用 `Message`、`MessageSegment` 或者 `MessageTemplate`。那么,我们就需要先构造一个消息序列。消息序列可以通过多种方式构造: #### 直接构造 `Message` 类可以直接实例化,支持 `str`、`MessageSegment`、`Iterable[MessageSegment]` 或适配器自定义类型的参数。 ```python from nonebot.adapters.console import Message, MessageSegment # str Message("Hello, world!") # MessageSegment Message(MessageSegment.text("Hello, world!")) # List[MessageSegment] Message([MessageSegment.text("Hello, world!")]) ``` #### 运算构造 `Message` 对象可以通过 `str`、`MessageSegment` 相加构造,详情请参考[拼接消息](#拼接消息)。 #### 从字典数组构造 `Message` 对象支持 Pydantic 自定义类型构造,可以使用 Pydantic 的 `TypeAdapter` 方法进行构造。 ```python from pydantic import TypeAdapter from nonebot.adapters.console import Message, MessageSegment # 由字典构造消息段 TypeAdapter(MessageSegment).validate_python( {"type": "text", "data": {"text": "text"}} ) == MessageSegment.text("text") # 由字典数组构造消息序列 TypeAdapter(Message).validate_python( [MessageSegment.text("text"), {"type": "text", "data": {"text": "text"}}], ) == Message([MessageSegment.text("text"), MessageSegment.text("text")]) ``` ### 获取消息纯文本 由于消息中存在各种类型的消息段,因此 `str(message)` 通常**不能得到消息的纯文本**,而是一个消息序列的字符串表示。 NoneBot 为消息段定义了一个方法 `is_text()` ,可以用于判断消息段是否为纯文本;也可以使用 `message.extract_plain_text()` 方法获取消息纯文本。 ```python from nonebot.adapters.console import Message, MessageSegment # 判断消息段是否为纯文本 MessageSegment.text("text").is_text() == True # 提取消息纯文本字符串 Message( [MessageSegment.text("text"), MessageSegment.markdown("**markup**")] ).extract_plain_text() == "text" ``` ### 遍历 消息序列继承自 `List[MessageSegment]` ,因此可以使用 `for` 循环遍历消息段。 ```python for segment in message: ... ``` ### 比较 消息和消息段都可以使用 `==` 或 `!=` 运算符比较是否相同。 ```python MessageSegment.text("text") != MessageSegment.text("foo") some_message == Message([MessageSegment.text("text")]) ``` ### 检查消息段 我们可以通过 `in` 运算符或消息序列的 `has` 方法来: ```python # 是否存在消息段 MessageSegment.text("text") in message # 是否存在指定类型的消息段 "text" in message ``` 我们还可以使用消息序列的 `only` 方法来检查消息中是否仅包含指定的消息段。 ```python # 是否都为指定消息段 message.only(MessageSegment.text("test")) # 是否仅包含指定类型的消息段 message.only("text") ``` ### 过滤、索引与切片 消息序列对列表的索引与切片进行了增强,在原有列表 `int` 索引与 `slice` 切片的基础上,支持 `type` 过滤索引与切片。 ```python from nonebot.adapters.console import Message, MessageSegment message = Message( [ MessageSegment.text("test"), MessageSegment.markdown("test2"), MessageSegment.markdown("test3"), MessageSegment.text("test4"), ] ) # 索引 message[0] == MessageSegment.text("test") # 切片 message[0:2] == Message( [MessageSegment.text("test"), MessageSegment.markdown("test2")] ) # 类型过滤 message["markdown"] == Message( [MessageSegment.markdown("test2"), MessageSegment.markdown("test3")] ) # 类型索引 message["markdown", 0] == MessageSegment.markdown("test2") # 类型切片 message["markdown", 0:2] == Message( [MessageSegment.markdown("test2"), MessageSegment.markdown("test3")] ) ``` 我们也可以通过消息序列的 `include`、`exclude` 方法进行类型过滤。 ```python message.include("text", "markdown") message.exclude("text") ``` 同样的,消息序列对列表的 `index`、`count` 方法也进行了增强,可以用于索引指定类型的消息段。 ```python # 指定类型首个消息段索引 message.index("markdown") == 1 # 指定类型消息段数量 message.count("markdown") == 2 ``` 此外,消息序列添加了一个 `get` 方法,可以用于获取指定类型指定个数的消息段。 ```python # 获取指定类型指定个数的消息段 message.get("markdown", 1) == Message([MessageSegment.markdown("test2")]) ``` ### 拼接消息 `str`、`Message`、`MessageSegment` 对象之间可以直接相加,相加均会返回一个新的 `Message` 对象。 ```python # 消息序列与消息段相加 Message([MessageSegment.text("text")]) + MessageSegment.text("text") # 消息序列与字符串相加 Message([MessageSegment.text("text")]) + "text" # 消息序列与消息序列相加 Message([MessageSegment.text("text")]) + Message([MessageSegment.text("text")]) # 字符串与消息序列相加 "text" + Message([MessageSegment.text("text")]) # 消息段与消息段相加 MessageSegment.text("text") + MessageSegment.text("text") # 消息段与字符串相加 MessageSegment.text("text") + "text" # 消息段与消息序列相加 MessageSegment.text("text") + Message([MessageSegment.text("text")]) # 字符串与消息段相加 "text" + MessageSegment.text("text") ``` 如果需要在当前消息序列后直接拼接新的消息段,可以使用 `Message.append`、`Message.extend` 方法,或者使用自加。 ```python msg = Message([MessageSegment.text("text")]) # 自加 msg += "text" msg += MessageSegment.text("text") msg += Message([MessageSegment.text("text")]) # 附加 msg.append("text") msg.append(MessageSegment.text("text")) # 扩展 msg.extend([MessageSegment.text("text")]) ``` 我们也可以通过消息段或消息序列的 `join` 方法来拼接一串消息: ```python seg = MessageSegment.text("text") msg = seg.join( [ MessageSegment.text("first"), Message( [ MessageSegment.text("second"), MessageSegment.text("third"), ] ) ] ) msg == Message( [ MessageSegment.text("first"), MessageSegment.text("text"), MessageSegment.text("second"), MessageSegment.text("third"), ] ) ``` ### 使用消息模板 为了提供安全可靠的跨平台模板字符,我们提供了一个消息模板功能来构建消息序列 它在以下常见场景中尤其有用: - 多行富文本编排(包含图片,文字以及表情等) - 客制化(由 Bot 最终用户提供消息模板时) 在事实上,它的用法和 `str.format` 极为相近,所以你在使用的时候,总是可以参考[Python 文档](https://docs.python.org/zh-cn/3/library/stdtypes.html#str.format)来达到你想要的效果,这里给出几个简单的例子。 默认情况下,消息模板采用 `str` 纯文本形式的格式化: ```python title=基础格式化用法 >>> from nonebot.adapters import MessageTemplate >>> MessageTemplate("{} {}").format("hello", "world") 'hello world' ``` 如果 `Message.template` 构建消息模板,那么消息模板将采用消息序列形式的格式化,此时的消息将会是平台特定的: :::caution 注意 使用 `Message.template` 构建消息模板时,应注意消息序列为平台适配器提供的类型,不能使用 `nonebot.adapters.Message` 基类作为模板构建。使用基类构建模板与使用 `str` 构建模板的效果是一样的,因此请使用上述的 `MessageTemplate` 类直接构建模板。: ::: ```python title=平台格式化用法 >>> from nonebot.adapters.console import Message, MessageSegment >>> Message.template("{} {}").format("hello", "world") Message( MessageSegment.text("hello"), MessageSegment.text(" "), MessageSegment.text("world") ) ``` 消息模板支持使用消息段进行格式化: ```python title=对消息段进行安全的拼接 >>> from nonebot.adapters.console import Message, MessageSegment >>> Message.template("{}{}").format(MessageSegment.markdown("**markup**"), "world") Message( MessageSegment(type='markdown', data={'markup': '**markup**'}), MessageSegment(type='text', data={'text': 'world'}) ) ``` 消息模板同样支持使用消息序列作为模板: ```python title=以消息对象作为模板 >>> from nonebot.adapters.console import Message, MessageSegment >>> Message.template( ... MessageSegment.text("{user_id}") + MessageSegment.emoji("tada") + ... MessageSegment.text("{message}") ... ).format_map({"user_id": 123456, "message": "hello world"}) Message( MessageSegment(type='text', data={'text': '123456'}), MessageSegment(type='emoji', data={'emoji': 'tada'}), MessageSegment(type='text', data={'text': 'hello world'}) ) ``` :::caution 注意 只有消息序列中的文本类型消息段才能被格式化,其他类型的消息段将会原样添加。 ::: 消息模板支持使用拓展控制符来控制消息段类型: ```python title=使用消息段的拓展控制符 >>> from nonebot.adapters.console import Message, MessageSegment >>> Message.template("{name:emoji}").format(name='tada') Message(MessageSegment(type='emoji', data={'name': 'tada'})) ``` ================================================ FILE: website/versioned_docs/version-2.4.3/tutorial/store.mdx ================================================ --- sidebar_position: 2 description: 从商店安装适配器和插件 options: menu: - category: tutorial weight: 40 --- # 获取商店内容 import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; import Asciinema from "@site/src/components/Asciinema"; :::tip 提示 如果你暂时没有获取商店内容的需求,可以跳过本章节。 ::: NoneBot 提供了一个[商店](/store/plugins),商店内容均由社区开发者贡献。你可以在商店中查找你需要的适配器和插件等,进行安装或者参考其文档等。 商店中每个内容的卡片都包含了其名称和简介等信息,点击**卡片右上角**链接图标即可跳转到其主页。 ## 安装插件 在商店插件页面中,点击你需要安装的插件下方的 `点击复制安装命令` 按钮,即可复制 `nb-cli` 命令。 请在你的**项目目录**下执行该命令。`nb-cli` 会自动安装插件并将其添加到加载列表中。 ```bash nb plugin install <插件名称> ``` ```bash $ nb plugin install [?] 想要安装的插件名称: <插件名称> ``` ```bash pip install <插件包名> ``` 插件包名可以在商店插件卡片中找到,或者使用 `nb-cli` 搜索插件显示的详情中找到。安装完成后,需要参考[加载插件章节](./create-plugin.md#加载插件)自行加载。 如果想要查看插件列表,可以使用以下命令 ```bash # 列出商店所有插件 nb plugin list # 搜索商店插件 nb plugin search [可选关键词] ``` 升级和卸载插件可以使用以下命令 ```bash nb plugin update <插件名称> nb plugin uninstall <插件名称> ``` ```bash $ nb plugin update [?] 想要安装的插件名称: <插件名称> $ nb plugin uninstall [?] 想要卸载的插件名称: <插件名称> ``` ```bash pip install --upgrade <插件包名> pip uninstall <插件包名> ``` 插件包名可以在商店插件卡片中找到,或者使用 `nb-cli` 搜索插件显示的详情中找到。卸载完成后,需要自行移除插件加载。 ## 安装适配器 安装适配器与安装插件类似,只是将命令换为 `nb adapter`,这里就不再赘述。 请在你的**项目目录**下执行该命令。`nb-cli` 会自动安装适配器并将其添加到注册列表中。 ```bash nb adapter install <适配器名称> ``` ```bash $ nb adapter install [?] 想要安装的适配器名称: <适配器名称> ``` ```bash pip install <适配器包名> ``` 适配器包名可以在商店适配器卡片中找到,或者使用 `nb-cli` 搜索适配器显示的详情中找到。安装完成后,需要参考[注册适配器章节](../advanced/adapter.md#注册适配器)自行注册。 如果想要查看适配器列表,可以使用以下命令 ```bash # 列出商店所有适配器 nb adapter list # 搜索商店适配器 nb adapter search [可选关键词] ``` 升级和卸载适配器可以使用以下命令 ```bash nb adapter update <适配器名称> nb adapter uninstall <适配器名称> ``` ```bash $ nb adapter update [?] 想要安装的适配器名称: <适配器名称> $ nb adapter uninstall [?] 想要卸载的适配器名称: <适配器名称> ``` ```bash pip install --upgrade <适配器包名> pip uninstall <适配器包名> ``` 适配器包名可以在商店适配器卡片中找到,或者使用 `nb-cli` 搜索适配器显示的详情中找到。卸载完成后,需要自行移除适配器加载。 ## 安装驱动器 安装驱动器与安装插件同样类似,只是将命令换为 `nb driver`,这里就不再赘述。 如果你使用了虚拟环境,请在你的**项目目录**下执行该命令,`nb-cli` 会自动安装驱动器到虚拟环境中。 请注意 `nb-cli` 并不会在安装驱动器后修改项目所使用的驱动器,请自行参考[配置方法](../appendices/config.mdx)章节以及 [`DRIVER` 配置项](../appendices/config.mdx#driver)修改驱动器。 ```bash nb driver install <驱动器名称> ``` ```bash $ nb driver install [?] 想要安装的驱动器名称: <驱动器名称> ``` ```bash pip install <驱动器包名> ``` 驱动器包名可以在商店驱动器卡片中找到,或者使用 `nb-cli` 搜索驱动器显示的详情中找到。 如果想要查看驱动器列表,可以使用以下命令 ```bash # 列出商店所有驱动器 nb driver list # 搜索商店驱动器 nb driver search [可选关键词] ``` 升级和卸载驱动器可以使用以下命令 ```bash nb driver update <驱动器名称> nb driver uninstall <驱动器名称> ``` ```bash $ nb driver update [?] 想要安装的驱动器名称: <驱动器名称> $ nb driver uninstall [?] 想要卸载的驱动器名称: <驱动器名称> ``` ```bash pip install --upgrade <驱动器包名> pip uninstall <驱动器包名> ``` 驱动器包名可以在商店驱动器卡片中找到,或者使用 `nb-cli` 搜索驱动器显示的详情中找到。卸载完成后,需要自行移除适配器加载。 ================================================ FILE: website/versioned_docs/version-2.4.4/README.md ================================================ --- sidebar_position: 0 id: index slug: / --- # 概览 NoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架(下称 NoneBot),它基于 Python 的类型注解和异步优先特性(兼容同步),能够为你的需求实现提供便捷灵活的支持。同时,NoneBot 拥有大量的开发者为其开发插件,用户无需编写任何代码,仅需完成环境配置及插件安装,就可以正常使用 NoneBot。 需要注意的是,NoneBot 仅支持 **Python 3.9 以上版本** ## 特色 ### 异步优先 NoneBot 基于 Python [asyncio](https://docs.python.org/zh-cn/3/library/asyncio.html) / [trio](https://trio.readthedocs.io/en/stable/) 编写,并在异步机制的基础上进行了一定程度的同步函数兼容。 ### 完整的类型注解 NoneBot 参考 [PEP 484](https://www.python.org/dev/peps/pep-0484/) 等 PEP 完整实现了类型注解,通过 Pyright(Pylance) 检查。配合编辑器的类型推导功能,能将绝大多数的 Bug 杜绝在编辑器中([编辑器支持](./editor-support))。 ### 开箱即用 NoneBot 提供了使用便捷、具有交互式功能的命令行工具--`nb-cli`,使得用户初次接触 NoneBot 时更容易上手。使用方法请阅读本文档[指南](./quick-start.mdx)以及 [CLI 文档](https://cli.nonebot.dev/)。 ### 插件系统 插件系统是 NoneBot 的核心,通过它可以实现机器人的模块化以及功能扩展,便于维护和管理。 ### 依赖注入系统 NoneBot 采用了一套自行定义的依赖注入系统,可以让事件的处理过程更加的简洁、清晰,增加代码的可读性,减少代码冗余。 #### 什么是依赖注入 [**『依赖注入』**](https://zh.wikipedia.org/wiki/%E6%8E%A7%E5%88%B6%E5%8F%8D%E8%BD%AC)意思是,在编程中,有一种方法可以让你的代码声明它工作和使用所需要的东西,即**『依赖』**。 系统(在这里是指 NoneBot)将负责做任何需要的事情,为你的代码提供这些必要依赖(即**『注入』**依赖性) 这在你有以下情形的需求时非常有用: - 这部分代码拥有共享的逻辑(同样的代码逻辑多次重复) - 共享数据库以及网络请求连接会话 - 比如 `httpx.AsyncClient`、`aiohttp.ClientSession` 和 `sqlalchemy.Session` - 机器人用户权限检查以及认证 - 还有更多... 它在完成上述工作的同时,还能尽量减少代码的耦合和重复 ================================================ FILE: website/versioned_docs/version-2.4.4/advanced/adapter.md ================================================ --- sidebar_position: 1 description: 注册适配器与指定平台交互 options: menu: - category: advanced weight: 20 --- # 使用适配器 适配器 (Adapter) 是机器人与平台交互的核心桥梁,它负责在驱动器和机器人插件之间转换与传递消息。 ## 适配器功能与组成 适配器通常有两种功能,分别是**接收事件**和**调用平台接口**。其中,接收事件是指将驱动器收到的事件消息转换为 NoneBot 定义的事件模型,然后交由机器人插件处理;调用平台接口是指将机器人插件调用平台接口的数据转换为平台指定的格式,然后交由驱动器发送,并接收接口返回数据。 为了实现这两种功能,适配器通常由四个部分组成: - **Adapter**:负责转换事件和调用接口,正确创建 Bot 对象并注册到 NoneBot 中。 - **Bot**:负责存储平台机器人相关信息,并提供回复事件的方法。 - **Event**:负责定义事件内容,以及事件主体对象。 - **Message**:负责正确序列化消息,以便机器人插件处理。 ## 注册适配器 在使用适配器之前,我们需要先将适配器注册到驱动器中,这样适配器就可以通过驱动器接收事件和调用接口了。我们以 Console 适配器为例,来看看如何注册适配器: ```python {2,5} title=bot.py import nonebot from nonebot.adapters.console import Adapter driver = nonebot.get_driver() driver.register_adapter(Adapter) ``` 我们首先需要从适配器模块中导入所需要的适配器类,然后通过驱动器的 `register_adapter` 方法将适配器注册到驱动器中即可。如果我们需要多平台支持,可以多次调用 `register_adapter` 方法来注册多个适配器。 ## 获取已注册的适配器 NoneBot 提供了 `get_adapter` 方法来获取已注册的适配器,我们可以通过适配器的名称或类型来获取指定的适配器实例: ```python import nonebot from nonebot.adapters.console import Adapter adapters = nonebot.get_adapters() console_adapter = nonebot.get_adapter(Adapter) console_adapter = nonebot.get_adapter(Adapter.get_name()) ``` ## 获取 Bot 对象 当前所有适配器已连接的 Bot 对象可以通过 `get_bots` 方法获取,这是一个以机器人 ID 为键的字典: ```python import nonebot bots = nonebot.get_bots() ``` 我们也可以通过 `get_bot` 方法获取指定 ID 的 Bot 对象。如果省略 ID 参数,将会返回所有 Bot 中的第一个: ```python import nonebot bot = nonebot.get_bot("bot_id") ``` 如果需要获取指定适配器连接的 Bot 对象,我们可以通过适配器的 `bots` 属性获取,这也是一个以机器人 ID 为键的字典: ```python import nonebot from nonebot.adapters.console import Adapter console_adapter = nonebot.get_adapter(Adapter) bots = console_adapter.bots ``` Bot 对象都具有一个 `self_id` 属性,它是机器人的唯一 ID,由适配器填写,通常为机器人的帐号 ID 或者 APP ID。 ## 获取事件通用信息 适配器的所有事件模型均继承自 `Event` 基类,在[事件类型与重载](../appendices/overload.md)一节中,我们也提到了如何使用基类抽象方法来获取事件通用信息。基类能提供如下信息: ### 事件类型 事件类型通常为 `meta_event`、`message`、`notice`、`request`。 ```python type: str = event.get_type() ``` ### 事件名称 事件名称由适配器定义,通常用于日志记录。 ```python name: str = event.get_event_name() ``` ### 事件描述 事件描述由适配器定义,通常用于日志记录。 ```python description: str = event.get_event_description() ``` ### 事件日志字符串 事件日志字符串由事件名称和事件描述组成,用于日志记录。 ```python log: str = event.get_log_string() ``` ### 事件主体 ID 事件主体 ID 通常为机器人用户 ID。 ```python user_id: str = event.get_user_id() ``` ### 事件会话 ID 事件会话 ID 通常为机器人用户 ID 与群聊/频道 ID 组合而成。 ```python session_id: str = event.get_session_id() ``` ### 事件消息 如果事件包含消息,则可以通过该方法获取,否则会产生异常。 ```python message: Message = event.get_message() ``` ### 事件纯文本消息 通常为事件消息的纯文本内容,如果事件不包含消息,则会产生异常。 ```python text: str = event.get_plaintext() ``` ### 事件是否与机器人有关 由适配器实现的判断,通常将事件目标主体为机器人、消息中包含“@机器人”或以“机器人的昵称”开始视为与机器人有关。 ```python is_tome: bool = event.is_tome() ``` ## 更多 官方支持的适配器和社区贡献的适配器均可在[商店](/store/adapters)中查看。如果你想要开发自己的适配器,可以参考[开发文档](../developer/adapter-writing.md)。欢迎通过商店发布你的适配器。 ================================================ FILE: website/versioned_docs/version-2.4.4/advanced/dependency.mdx ================================================ --- sidebar_position: 6 description: 通过依赖注入获取上下文信息 options: menu: - category: advanced weight: 70 --- # 依赖注入 import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; 在事件处理流程中,事件响应器具有自己独立的上下文,例如:当前的事件、机器人等信息。在 NoneBot 中,这些信息通过依赖注入的方式提供给事件处理函数,可以让代码更加整洁可读、提升复用能力。 在了解如何使用依赖注入获取上下文信息之前,我们需要先了解两个概念: - `Dependent`:使用依赖注入的函数或其他任意可调用对象。如:事件处理函数、自定义的依赖函数等。 - `Dependency`:依赖注入的对象。如:当前事件、机器人等。 在之前的文档中,我们已经多次使用了依赖注入来获取事件信息。通过对函数参数依照一定规则填写类型注解,即可获得想要的上下文信息。任何一个事件处理函数在添加到事件处理流程时,都会根据一定规则提前将其解析成一个 `Dependent` 对象,方便运行时进行注入。如果遇到无法解析的参数,将会抛出 `ValueError("Unknown parameter")` 的异常。整个依赖注入系统可以分为两部分: - 参数解析 - 依据一定规则解析函数参数,识别 `Dependency` 依赖。 - 生成 `Dependent` 对象。 - 执行 - 根据已经解析的 `Dependency` 依赖,执行调用。 - 将所有 `Dependency` 的返回值根据参数名传入并调用 `Dependent` 。 :::danger 警告 在依赖注入中,类型注解是非常重要的,因为它不仅可以决定依赖注入的对象,还可以触发[重载机制](../appendices/overload.md#重载)。如果类型注解与实际获得数据类型不一致,将会跳过当前 `Dependent` 对象(即事件处理函数)。 ::: :::tip 提示 如果对于依赖注入的解析流程有疑问,可以调整[日志等级配置项](../appendices/config.mdx#log-level)为 `TRACE`,查看依赖解析日志。 ::: ## 同步支持 对于依赖注入系统中的 `Dependent` 或者 `Dependency` 对象,均支持同步类型的函数或可调用对象。例如: ```python {6,10} from nonebot import on_command from nonebot.params import Depends matcher = on_command("foo") def dependency() -> str: return "something" @matcher.handle() def _(result: str = Depends(dependency)): ... ``` ## 非依赖参数 在依赖注入解析中,任何无法解析的参数如果带有默认值,将会被视为非依赖参数。这些参数在依赖运行时将不会被注入而使用函数默认值。例如: ```python async def _(foo: str = "bar"): ... ``` ## 类型依赖注入 这一类的依赖注入仅需要在函数参数中添加对应的类型注解即可。 ### Bot 获取当前事件的 Bot 对象。 通过标注参数为 `Bot` 类型,或者一系列 `Bot` 类型,即可获取到当前事件的 Bot 对象。为兼容性考虑,如果参数名为 `bot` 且无类型注解,也会视为 Bot 依赖注入。 Bot 依赖注入支持重载(即:可以标注参数为子类型)且具有[重载优先检查权](../appendices/overload.md#重载)。 ```python from nonebot.adapters import Bot from nonebot.adapters.console import Bot as ConsoleBot from nonebot.adapters.onebot.v11 import Bot as OneBotV11Bot async def _(foo: Bot): ... async def _(foo: ConsoleBot | OneBotV11Bot): ... async def _(bot): ... # 兼容性处理 ``` ```python from typing import Union from nonebot.adapters import Bot from nonebot.adapters.console import Bot as ConsoleBot from nonebot.adapters.onebot.v11 import Bot as OneBotV11Bot async def _(foo: Bot): ... async def _(foo: Union[ConsoleBot, OneBotV11Bot]): ... async def _(bot): ... # 兼容性处理 ``` ### Event 获取当前事件。 通过标注参数为 `Event` 类型,或者一系列 `Event` 类型,即可获取到当前事件。为兼容性考虑,如果参数名为 `event` 且无类型注解,也会视为 Event 依赖注入。 Event 依赖注入支持重载(即:可以标注参数为子类型)且具有[重载优先检查权](../appendices/overload.md#重载)。 ```python from nonebot.adapters import Event from nonebot.adapters.onebot.v11 import PrivateMessageEvent, GroupMessageEvent async def _(foo: Event): ... async def _(foo: PrivateMessageEvent | GroupMessageEvent): ... async def _(event): ... # 兼容性处理 ``` ```python from typing import Union from nonebot.adapters import Event from nonebot.adapters.onebot.v11 import PrivateMessageEvent, GroupMessageEvent async def _(foo: Event): ... async def _(foo: Union[PrivateMessageEvent, GroupMessageEvent]): ... async def _(event): ... # 兼容性处理 ``` ### State 获取当前[会话状态](../appendices/session-state.md)。 通过标注参数为 `T_State` 类型,即可获取到当前会话状态。为兼容性考虑,如果参数名为 `state` 且无类型注解,也会视为 State 依赖注入。 ```python from nonebot.typing import T_State async def _(foo: T_State): ... ``` ### Matcher 获取当前事件响应器实例。常用于使用[事件响应器操作](../appendices/session-control.mdx)。 通过标注参数为 `Matcher` 类型,或者一系列 `Matcher` 类型,即可获取到当前事件。为兼容性考虑,如果参数名为 `matcher` 且无类型注解,也会视为 Matcher 依赖注入。 Matcher 依赖注入支持重载(即:可以标注参数为子类型)且具有[重载优先检查权](../appendices/overload.md#重载)。 ```python from nonebot.matcher import Matcher async def _(foo: Matcher): ... async def _(matcher): ... # 兼容性处理 ``` ### Exception 获取事件响应器运行中抛出的异常。该依赖注入目前仅在事件响应器运行后处理 Hook 中可用。 通过标注参数为异常类型,或者一系列异常类型,即可获取到事件响应器运行中抛出的异常。 ```python {5,8} from nonebot.message import run_postprocessor from nonebot.exception import ActionFailed, NetworkError @run_postprocessor async def _(e: Exception): ... @run_postprocessor async def _(e: ActionFailed | NetworkError): ... ``` ```python {6,9} from typing import Union from nonebot.message import run_postprocessor from nonebot.exception import ActionFailed, NetworkError @run_postprocessor async def _(e: Exception): ... @run_postprocessor async def _(e: Union[ActionFailed, NetworkError]): ... ``` ## 子依赖 在依赖注入系统中,我们可以定义一个子依赖,来执行自定义的操作,提高代码复用性以及处理性能。 ### 定义子依赖 子依赖使用 `Depends` 标记进行定义,其参数即依赖的函数或可调用对象,同样会被解析为 `Dependent` 对象,将会在依赖注入期间执行。我们来看一个例子: ```python {5,15} from typing import Annotated from nonebot import on_command from nonebot.adapters import Event from nonebot.params import Depends test = on_command("test") async def check(event: Event) -> Event: if event.get_user_id() in BLACKLIST: await test.finish() return event @test.handle() async def _(event: Annotated[Event, Depends(check)]): ... ``` ```python {3,13} from nonebot import on_command from nonebot.adapters import Event from nonebot.params import Depends test = on_command("test") async def check(event: Event) -> Event: if event.get_user_id() in BLACKLIST: await test.finish() return event @test.handle() async def _(event: Event = Depends(check)): ... ``` 在上面的代码中,我们使用 `Depends` 标记定义了一个子依赖 `check`。它判断事件主体用户是否在黑名单中,如果在,则直接结束事件处理流程。如果不在,则返回事件对象,以便事件处理函数可以继续执行。 通过将 `Depends` 包裹的子依赖作为参数的默认值,我们就可以在执行事件处理函数之前执行子依赖,并将其返回值作为参数传入事件处理函数。子依赖和普通的事件处理函数并没有区别,同样可以使用依赖注入,并且可以返回任何类型的值。但需要注意的是,如果事件处理函数参数的类型注解与子依赖返回值的类型**不一致**,将会触发[重载](../appendices/overload.md)而跳过当前事件处理函数。 特别的,我们可以为 `Dependent` 对象定义一系列前置子依赖,它们会在参数执行前被顺序执行,且返回值将会被忽略,例如: ```python {11} from nonebot import on_command from nonebot.adapters import Event from nonebot.params import Depends test = on_command("test") async def check(event: Event): if event.get_user_id() in BLACKLIST: await test.finish() @test.handle(parameterless=[Depends(check)]) async def _(): ... ``` ### 依赖缓存 NoneBot 在执行子依赖时,会将其返回值缓存起来。当我们在使用子依赖时,`Depends` 具有一个参数 `use_cache`,默认为 `True`。此时在事件处理流程中,多次使用同一个子依赖时,将会使用缓存中的结果而不会重复执行。这在很多情景中非常有用,例如: ```python {7} import random from typing import Annotated async def random_result() -> int: return random.randint(1, 100) async def _(x: Annotated[int, Depends(random_result)]): print(x) ``` ```python {6} import random async def random_result() -> int: return random.randint(1, 100) async def _(x: int = Depends(random_result)): print(x) ``` 此时,在同一事件处理流程中,这个随机函数的返回值将会保持一致。如果我们希望每次都重新执行子依赖,可以将 `use_cache` 设置为 `False`。 ```python {7} import random from typing import Annotated async def random_result() -> int: return random.randint(1, 100) async def _(x: Annotated[int, Depends(random_result, use_cache=False)]): print(x) ``` ```python {6} import random async def random_result() -> int: return random.randint(1, 100) async def _(x: int = Depends(random_result, use_cache=False)): print(x) ``` :::tip 提示 缓存的生命周期与当前接收到的事件相同。接收到事件后,子依赖在首次执行时缓存,在该事件处理完成后,缓存就会被清除。 ::: ### 类型转换与校验 在依赖注入系统中,我们可以对子依赖的返回值进行自动类型转换与校验。这个功能由 Pydantic 支持,因此我们通过参数类型注解自动使用 Pydantic 支持的类型转换。例如: ```python {6,9} from typing import Annotated from nonebot.params import Depends from nonebot.adapters import Event def get_user_id(event: Event) -> str: return event.get_user_id() async def _(user_id: Annotated[int, Depends(get_user_id, validate=True)]): print(user_id) ``` ```python {4,7} from nonebot.params import Depends from nonebot.adapters import Event def get_user_id(event: Event) -> str: return event.get_user_id() async def _(user_id: int = Depends(get_user_id, validate=True)): print(user_id) ``` 在进行类型自动转换的同时,Pydantic 还支持对数据进行更多的限制,如:大于、小于、长度等。使用方法如下: ```python {7,10} from typing import Annotated from pydantic import Field from nonebot.params import Depends from nonebot.adapters import Event def get_user_id(event: Event) -> str: return event.get_user_id() async def _(user_id: Annotated[int, Depends(get_user_id, validate=Field(gt=100))]): print(user_id) ``` ```python {5,8} from pydantic import Field from nonebot.params import Depends from nonebot.adapters import Event def get_user_id(event: Event) -> str: return event.get_user_id() async def _(user_id: int = Depends(get_user_id, validate=Field(gt=100))): print(user_id) ``` ### 类作为依赖 在前面的事例中,我们使用了函数作为子依赖。实际上,我们还可以使用类作为依赖。当我们在实例化一个类的时候,其实我们就在调用它,类本身也是一个可调用对象。例如: ```python {16} from typing import Annotated from dataclasses import dataclass from nonebot.params import Depends from nonebot.adapters import Event from nonebot.typing import T_State def get_context(state: T_State) -> dict: return state.setdefault("context", {}) @dataclass class ClassDependency: event: Event context: dict = Depends(get_context) async def _(data: Annotated[ClassDependency, Depends(ClassDependency)]): print(data.event, data.context) ``` ```python {15} from dataclasses import dataclass from nonebot.params import Depends from nonebot.adapters import Event from nonebot.typing import T_State def get_context(state: T_State) -> dict: return state.setdefault("context", {}) @dataclass class ClassDependency: event: Event context: dict = Depends(get_context) async def _(data: ClassDependency = Depends(ClassDependency)): print(data.event, data.context) ``` 可以看到,我们使用 `dataclass` 定义了一个类。由于这个类的 `__init__` 方法可以被依赖注入系统解析,因此,我们可以将其作为子依赖进行声明。特别地,对于类依赖,`Depends` 的参数可以为空,NoneBot 将会使用参数的类型注解进行解析与推断: ```python from typing import Annotated async def _(data: Annotated[ClassDependency, Depends()]): print(data.event, data.context) ``` ```python async def _(data: ClassDependency = Depends()): print(data.event, data.context) ``` ### 生成器作为依赖 NoneBot 的依赖注入支持依赖项在事件处理流程结束后进行一些额外的工作,比如数据库 session 或者网络 IO 的关闭,互斥锁的解锁等等。同时,由于[依赖缓存](#依赖缓存)的存在,我们可以通过这种方式来实现共享一个 session 等功能。 要实现上述功能,我们可以用生成器函数作为依赖项,我们用 `yield` 关键字取代 `return` 关键字,并在 `yield` 之后进行额外的工作。 我们可以看下述代码段, 使用 `httpx.AsyncClient` 异步网络 IO,并在事件处理流程中共用一个 client: ```python {15} from typing import Annotated from collections.abc import AsyncGenerator import httpx from nonebot.params import Depends async def get_client() -> AsyncGenerator[httpx.AsyncClient, None]: try: async with httpx.AsyncClient() as client: yield client finally: # 在这里进行额外的工作 @test.handle() async def _(x: Annotated[httpx.AsyncClient, Depends(get_client)]): resp = await x.get("https://nonebot.dev") ``` ```python {15} from collections.abc import AsyncGenerator import httpx from nonebot.params import Depends async def get_client() -> AsyncGenerator[httpx.AsyncClient, None]: try: async with httpx.AsyncClient() as client: yield client finally: # 在这里进行额外的工作 @test.handle() async def _(x: httpx.AsyncClient = Depends(get_client)): resp = await x.get("https://nonebot.dev") ``` :::caution 注意 生成器作为依赖时,其中只能进行一次 `yield`,否则将会触发异常。如果对此有疑问并想探究原因,可以参考 [contextmanager](https://docs.python.org/zh-cn/3/library/contextlib.html#contextlib.contextmanager) 和 [asynccontextmanager](https://docs.python.org/zh-cn/3/library/contextlib.html#contextlib.asynccontextmanager) 文档。事实上,NoneBot 内部就使用了这两个装饰器。 ::: ### 可调用对象作为依赖 在 Python 里,为类定义 `__call__` 方法就可以使得这个类的实例成为一个可调用对象。因此,我们也可以将定义了 `__call__` 方法的类的实例作为依赖。事实上,NoneBot 的[内置响应规则](./matcher.md#内置响应规则)就广泛使用了这种方式,以 `is_type` 规则为例: ```python from nonebot.adapters import Event class IsTypeRule: def __init__(self, *types: type[Event]): self.types = types async def __call__(self, event: Event) -> bool: return isinstance(event, self.types) ``` 我们在使用 `is_type` 时,即实例化了 `IsTypeRule` 类,然后将实例作为响应规则依赖项传入。 ## 其他依赖注入 这一类的依赖注入通常基于子依赖编写,为我们开发者提供更方便的途径获取上下文信息。 ### EventType 获取当前事件的类型。 ```python {4} from typing import Annotated from nonebot.params import EventType async def _(foo: Annotated[str, EventType()]): ... ``` ```python {3} from nonebot.params import EventType async def _(foo: str = EventType()): ... ``` ### EventMessage 获取当前事件的消息。 ```python {5} from typing import Annotated from nonebot.adapters import Message from nonebot.params import EventMessage async def _(foo: Annotated[Message, EventMessage()]): ... ``` ```python {4} from nonebot.adapters import Message from nonebot.params import EventMessage async def _(foo: Message = EventMessage()): ... ``` ### EventPlainText 获取当前事件的消息纯文本部分。 ```python {4} from typing import Annotated from nonebot.params import EventPlainText async def _(foo: Annotated[str, EventPlainText()]): ... ``` ```python {3} from nonebot.params import EventPlainText async def _(foo: str = EventPlainText()): ... ``` ### EventToMe 获取当前事件是否与机器人相关。 ```python {4} from typing import Annotated from nonebot.params import EventToMe async def _(foo: Annotated[bool, EventToMe()]): ... ``` ```python {3} from nonebot.params import EventToMe async def _(foo: bool = EventToMe()): ... ``` ### Command 获取当前命令型消息的元组形式命令名。 ```python {4} from typing import Annotated from nonebot.params import Command async def _(foo: Annotated[tuple[str, ...], Command()]): ... ``` ```python {4} from nonebot.params import Command async def _(foo: tuple[str, ...] = Command()): ... ``` :::tip 提示 命令详情只能在**触发命令型事件响应器时**获取。如果在事件处理后续流程中获取,则会获取到不同的值。 ::: ### RawCommand 获取当前命令型消息的文本形式命令名。 ```python {4} from typing import Annotated from nonebot.params import RawCommand async def _(foo: Annotated[str, RawCommand()]): ... ``` ```python {3} from nonebot.params import RawCommand async def _(foo: str = RawCommand()): ... ``` :::tip 提示 命令详情只能在**触发命令型事件响应器时**获取。如果在事件处理后续流程中获取,则会获取到不同的值。 ::: ### CommandArg 获取命令型消息命令后跟随的参数。 ```python {5} from typing import Annotated from nonebot.adapters import Message from nonebot.params import CommandArg async def _(foo: Annotated[Message, CommandArg()]): ... ``` ```python {4} from nonebot.adapters import Message from nonebot.params import CommandArg async def _(foo: Message = CommandArg()): ... ``` :::tip 提示 命令详情只能在**触发命令型事件响应器时**获取。如果在事件处理后续流程中获取,则会获取到不同的值。 ::: ### CommandStart 获取命令型消息命令前缀。 ```python {4} from typing import Annotated from nonebot.params import CommandStart async def _(foo: Annotated[str, CommandStart()]): ... ``` ```python {3} from nonebot.params import CommandStart async def _(foo: str = CommandStart()): ... ``` :::tip 提示 命令详情只能在**触发命令型事件响应器时**获取。如果在事件处理后续流程中获取,则会获取到不同的值。 ::: ### CommandWhitespace 获取命令型消息命令与参数间空白符。 ```python {4} from typing import Annotated from nonebot.params import CommandWhitespace async def _(foo: Annotated[str, CommandWhitespace()]): ... ``` ```python {3} from nonebot.params import CommandWhitespace async def _(foo: str = CommandWhitespace()): ... ``` :::tip 提示 命令详情只能在**触发命令型事件响应器时**获取。如果在事件处理后续流程中获取,则会获取到不同的值。 ::: ### ShellCommandArgv 获取 shell 命令解析前的参数列表,列表中可能包含文本字符串和富文本消息段(如:图片)。当词法解析出错的时候,返回值将为 `None`。通过重载机制即可处理两种不同的情况。 ```python {4} from typing import Annotated from nonebot import on_shell_command from nonebot.params import ShellCommandArgv matcher = on_shell_command("cmd") # 解析失败 @matcher.handle() async def _(foo: Annotated[None, ShellCommandArgv()]): ... # 解析成功 @matcher.handle() async def _(foo: Annotated[list[str | MessageSegment], ShellCommandArgv()]): ... ``` ```python {4} from nonebot import on_shell_command from nonebot.params import ShellCommandArgv matcher = on_shell_command("cmd") # 解析失败 @matcher.handle() async def _(foo: None = ShellCommandArgv()): ... # 解析成功 @matcher.handle() async def _(foo: list[str | MessageSegment] = ShellCommandArgv()): ... ``` ```python {4} from typing import Union, Annotated from nonebot import on_shell_command from nonebot.params import ShellCommandArgv matcher = on_shell_command("cmd") # 解析失败 @matcher.handle() async def _(foo: Annotated[None, ShellCommandArgv()]): ... # 解析成功 @matcher.handle() async def _(foo: Annotated[list[Union[str, MessageSegment]], ShellCommandArgv()]): ... ``` ```python {4} from typing import Union from nonebot import on_shell_command from nonebot.params import ShellCommandArgv matcher = on_shell_command("cmd") # 解析失败 @matcher.handle() async def _(foo: None = ShellCommandArgv()): ... # 解析成功 @matcher.handle() async def _(foo: list[Union[str, MessageSegment]] = ShellCommandArgv()): ... ``` ### ShellCommandArgs 获取 shell 命令解析后的参数 Namespace,支持 MessageSegment 富文本(如:图片)。 :::tip 提示 如果参数解析成功,则为 parser 返回的 Namespace;如果参数解析失败,则为 [`ParserExit`](../api/exception.md#ParserExit) 异常,并携带错误码与错误信息。在前置词法解析失败时,返回值也为 [`ParserExit`](../api/exception.md#ParserExit) 异常。通过重载机制即可处理两种不同的情况。 由于 `ArgumentParser` 在解析到 `--help` 参数时也会抛出异常,这种情况下错误码为 `0` 且错误信息即为帮助信息。 ::: ```python {14,22} from typing import Annotated from nonebot import on_shell_command from nonebot.exception import ParserExit from nonebot.params import ShellCommandArgs from nonebot.rule import Namespace, ArgumentParser parser = ArgumentParser("demo") # parser.add_argument ... matcher = on_shell_command("cmd", parser=parser) # 解析失败 @matcher.handle() async def _(foo: Annotated[ParserExit, ShellCommandArgs()]): if foo.status == 0: foo.message # help message else: foo.message # error message # 解析成功 @matcher.handle() async def _(foo: Annotated[Namespace, ShellCommandArgs()]): arg_dict = vars(foo) ``` ```python {12,20} from nonebot import on_shell_command from nonebot.exception import ParserExit from nonebot.params import ShellCommandArgs from nonebot.rule import Namespace, ArgumentParser parser = ArgumentParser("demo") # parser.add_argument ... matcher = on_shell_command("cmd", parser=parser) # 解析失败 @matcher.handle() async def _(foo: ParserExit = ShellCommandArgs()): if foo.status == 0: foo.message # help message else: foo.message # error message # 解析成功 @matcher.handle() async def _(foo: Namespace = ShellCommandArgs()): arg_dict = vars(foo) ``` ### RegexMatched 获取正则匹配结果的对象。 ```python {5} from re import Match from typing import Annotated from nonebot.params import RegexMatched async def _(foo: Annotated[Match[str], RegexMatched()]): ... ``` ```python {4} from re import Match from nonebot.params import RegexMatched async def _(foo: Match[str] = RegexMatched()): ... ``` ### RegexStr 获取正则匹配结果的文本。 ```python {4} from typing import Annotated from nonebot.params import RegexStr async def _(foo: Annotated[str, RegexStr()]): ... ``` ```python {3} from nonebot.params import RegexStr async def _(foo: str = RegexStr()): ... ``` ### RegexGroup 获取正则匹配结果的 group 元组。 ```python {4} from typing import Any, Annotated from nonebot.params import RegexGroup async def _(foo: Annotated[tuple[Any, ...], RegexGroup()]): ... ``` ```python {4} from typing import Any from nonebot.params import RegexGroup async def _(foo: tuple[Any, ...] = RegexGroup()): ... ``` ### RegexDict 获取正则匹配结果的 group 字典。 ```python {4} from typing import Any, Annotated from nonebot.params import RegexDict async def _(foo: Annotated[dict[str, Any], RegexDict()]): ... ``` ```python {4} from typing import Any from nonebot.params import RegexDict async def _(foo: dict[str, Any] = RegexDict()): ... ``` ### Startswith 获取触发响应器的消息前缀字符串。 ```python {4} from typing import Annotated from nonebot.params import Startswith async def _(foo: Annotated[str, Startswith()]): ... ``` ```python {3} from nonebot.params import Startswith async def _(foo: str = Startswith()): ... ``` ### Endswith 获取触发响应器的消息后缀字符串。 ```python {4} from typing import Annotated from nonebot.params import Endswith async def _(foo: Annotated[str, Endswith()]): ... ``` ```python {3} from nonebot.params import Endswith async def _(foo: str = Endswith()): ... ``` ### Fullmatch 获取触发响应器的消息字符串。 ```python {4} from typing import Annotated from nonebot.params import Fullmatch async def _(foo: Annotated[str, Fullmatch()]): ... ``` ```python {3} from nonebot.params import Fullmatch async def _(foo: str = Fullmatch()): ... ``` ### Keyword 获取触发响应器的关键字字符串。 ```python {4} from typing import Annotated from nonebot.params import Keyword async def _(foo: Annotated[str, Keyword()]): ... ``` ```python {3} from nonebot.params import Keyword async def _(foo: str = Keyword()): ... ``` ### Received 获取某次 `receive` 接收的事件。 ```python {7} from typing import Annotated from nonebot.adapters import Event from nonebot.params import Received @matcher.receive("id") async def _(foo: Annotated[Event, Received("id")]): ... ``` ```python {5} from nonebot.adapters import Event from nonebot.params import Received @matcher.receive("id") async def _(foo: Event = Received("id")): ... ``` ### LastReceived 获取最近一次 `receive` 接收的事件。 ```python {7} from typing import Annotated from nonebot.adapters import Event from nonebot.params import LastReceived @matcher.receive("any") async def _(foo: Annotated[Event, LastReceived()]): ... ``` ```python {5} from nonebot.adapters import Event from nonebot.params import LastReceived @matcher.receive("any") async def _(foo: Event = LastReceived()): ... ``` ### ReceivePromptResult 获取某次 `receive` 发送提示消息的结果。 ```python {6} from typing import Any, Annotated from nonebot.params import ReceivePromptResult @matcher.receive("id", prompt="prompt") async def _(result: Annotated[Any, ReceivePromptResult("id")]): ... ``` ```python {6} from typing import Any from nonebot.params import ReceivePromptResult @matcher.receive("id", prompt="prompt") async def _(result: Any = ReceivePromptResult("id")): ... ``` ### Arg 获取某次 `got` 接收的参数。如果 `Arg` 参数留空,则使用函数的参数名作为要获取的参数。 ```python {7,8} from typing import Annotated from nonebot.params import Arg from nonebot.adapters import Message @matcher.got("key") async def _(key: Annotated[Message, Arg()]): ... async def _(foo: Annotated[Message, Arg("key")]): ... ``` ```python {5,6} from nonebot.params import Arg from nonebot.adapters import Message @matcher.got("key") async def _(key: Message = Arg()): ... async def _(foo: Message = Arg("key")): ... ``` ### ArgStr 获取某次 `got` 接收的参数,并转换为字符串。如果 `Arg` 参数留空,则使用函数的参数名作为要获取的参数。 ```python {6,7} from typing import Annotated from nonebot.params import ArgStr @matcher.got("key") async def _(key: Annotated[str, ArgStr()]): ... async def _(foo: Annotated[str, ArgStr("key")]): ... ``` ```python {4,5} from nonebot.params import ArgStr @matcher.got("key") async def _(key: str = ArgStr()): ... async def _(foo: str = ArgStr("key")): ... ``` ### ArgPlainText 获取某次 `got` 接收的参数的纯文本部分。如果 `Arg` 参数留空,则使用函数的参数名作为要获取的参数。 ```python {6,7} from typing import Annotated from nonebot.params import ArgPlainText @matcher.got("key") async def _(key: Annotated[str, ArgPlainText()]): ... async def _(foo: Annotated[str, ArgPlainText("key")]): ... ``` ```python {4,5} from nonebot.params import ArgPlainText @matcher.got("key") async def _(key: str = ArgPlainText()): ... async def _(foo: str = ArgPlainText("key")): ... ``` ### ArgPromptResult 获取某次 `got` 发送提示消息的结果。如果 `Arg` 参数留空,则使用函数的参数名作为要获取的参数。 ```python {6,7} from typing import Any, Annotated from nonebot.params import ArgPromptResult @matcher.got("key", prompt="prompt") async def _(result: Annotated[Any, ArgPromptResult()]): ... async def _(result: Annotated[Any, ArgPromptResult("key")]): ... ``` ```python {6,7} from typing import Any from nonebot.params import ArgPromptResult @matcher.got("key", prompt="prompt") async def _(result: Any = ArgPromptResult()): ... async def _(result: Any = ArgPromptResult("key")): ... ``` ### PausePromptResult 获取最近一次 `pause` 发送提示消息的结果。 ```python {6} from typing import Any, Annotated from nonebot.params import PausePromptResult @matcher.handle() async def _(): await matcher.pause(prompt="prompt") @matcher.handle() async def _(result: Annotated[Any, PausePromptResult()]): ... ``` ```python {6} from typing import Any from nonebot.params import PausePromptResult @matcher.handle() async def _(): await matcher.pause(prompt="prompt") @matcher.handle() async def _(result: Any = PausePromptResult()): ... ``` ================================================ FILE: website/versioned_docs/version-2.4.4/advanced/driver.md ================================================ --- sidebar_position: 0 description: 选择合适的驱动器运行机器人 options: menu: - category: advanced weight: 10 --- # 选择驱动器 驱动器 (Driver) 是机器人运行的基石,它是机器人初始化的第一步,主要负责数据收发。 :::important 提示 驱动器的选择通常与机器人所使用的协议适配器相关,如果不知道该选择哪个驱动器,可以先阅读相关协议适配器文档说明。 ::: :::tip 提示 如何**安装**驱动器请参考[安装驱动器](../tutorial/store.mdx#安装驱动器)。 ::: ## 驱动器类型 驱动器类型大体上可以分为两种: - `Forward`:即客户端型驱动器,多用于使用 HTTP 轮询,连接 WebSocket 服务器等情形。 - `Reverse`:即服务端型驱动器,多用于使用 WebHook,接收 WebSocket 客户端连接等情形。 客户端型驱动器可以分为以下两种: 1. 异步发送 HTTP 请求,自定义 `HTTP Method`、`URL`、`Header`、`Body`、`Cookie`、`Proxy`、`Timeout` 等。 2. 异步建立 WebSocket 连接上下文,自定义 `WebSocket URL`、`Header`、`Cookie`、`Proxy`、`Timeout` 等。 服务端型驱动器目前有: 1. ASGI 应用框架,具有以下功能: - 协议适配器自定义 HTTP 上报地址以及对上报数据处理的回调函数。 - 协议适配器自定义 WebSocket 连接请求地址以及对 WebSocket 请求处理的回调函数。 - 用户可以向 ASGI 应用添加任何服务端相关功能,如:[添加自定义路由](./routing.md)。 ## 配置驱动器 驱动器的配置方法已经在[配置](../appendices/config.mdx)章节中简单进行了介绍,这里将详细介绍驱动器配置的格式。 NoneBot 中的客户端和服务端型驱动器可以相互配合使用,但服务端型驱动器**仅能选择一个**。所有驱动器模块都会包含一个 `Driver` 子类,即驱动器类,他可以作为驱动器单独运行。同时,客户端驱动器模块中还会提供一个 `Mixin` 子类,用于在与其他驱动器配合使用时加载。因此,驱动器配置格式采用特殊语法:`[:][+[:]]*`。 其中,`` 代表**驱动器模块路径**;`` 代表**驱动器类名**,默认为 `Driver`;`` 代表**驱动器混入类名**,默认为 `Mixin`。即,我们需要选择一个主要驱动器,然后在其基础上配合使用其他驱动器的功能。主要驱动器可以为客户端或服务端类型,但混入类驱动器只能为客户端类型。 特别的,为了简化内置驱动器模块路径,我们可以使用 `~` 符号作为内置驱动器模块路径的前缀,如 `~fastapi` 代表使用内置驱动器 `fastapi`。NoneBot 内置了多个驱动器适配,但需要安装额外依赖才能使用,具体请参考[安装驱动器](../tutorial/store.mdx#安装驱动器)。常见的驱动器配置如下: ```dotenv DRIVER=~fastapi DRIVER=~aiohttp DRIVER=~httpx+~websockets DRIVER=~fastapi+~httpx+~websockets ``` ## 获取驱动器 在 NoneBot 框架初始化完成后,我们就可以通过 `get_driver()` 方法获取全局驱动器实例: ```python from nonebot import get_driver driver = get_driver() ``` ## 内置驱动器 ### None **类型:**服务端驱动器 NoneBot 内置的空驱动器,不提供任何收发数据功能,可以在不需要外部网络连接时使用。 ```env DRIVER=~none ``` ### FastAPI(默认) **类型:**ASGI 服务端驱动器 > FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints. [FastAPI](https://fastapi.tiangolo.com/) 是一个易上手、高性能的异步 Web 框架,具有极佳的编写体验。 FastAPI 可以通过类型注解、依赖注入等方式实现输入参数校验、自动生成 API 文档等功能,也可以挂载其他 ASGI、WSGI 应用。 ```env DRIVER=~fastapi ``` #### FastAPI 配置项 ##### `fastapi_openapi_url` 类型:`str | None` 默认值:`None` 说明:`FastAPI` 提供的 `OpenAPI` JSON 定义地址,如果为 `None`,则不提供 `OpenAPI` JSON 定义。 ##### `fastapi_docs_url` 类型:`str | None` 默认值:`None` 说明:`FastAPI` 提供的 `Swagger` 文档地址,如果为 `None`,则不提供 `Swagger` 文档。 ##### `fastapi_redoc_url` 类型:`str | None` 默认值:`None` 说明:`FastAPI` 提供的 `ReDoc` 文档地址,如果为 `None`,则不提供 `ReDoc` 文档。 ##### `fastapi_include_adapter_schema` 类型:`bool` 默认值:`True` 说明:`FastAPI` 提供的 `OpenAPI` JSON 定义中是否包含适配器路由的 `Schema`。 ##### `fastapi_reload` :::caution 警告 不推荐开启该配置项,在 Windows 平台上开启该功能有可能会造成预料之外的影响!替代方案:使用 `nb-cli` 命令行工具以及参数 `--reload` 启动 NoneBot。 ```bash nb run --reload ``` 开启该功能后,在 uvicorn 运行时(FastAPI 提供的 ASGI 底层,即 reload 功能的实际来源),asyncio 使用的事件循环会被 uvicorn 从默认的 `ProactorEventLoop` 强制切换到 `SelectorEventLoop`。 > 相关信息参考 [uvicorn#529](https://github.com/encode/uvicorn/issues/529),[uvicorn#1070](https://github.com/encode/uvicorn/pull/1070),[uvicorn#1257](https://github.com/encode/uvicorn/pull/1257) 后者(`SelectorEventLoop`)在 Windows 平台的可使用性不如前者(`ProactorEventLoop`),包括但不限于 1. 不支持创建子进程 2. 最多只支持 512 个套接字 3. ... > 具体信息参考 [Python 文档](https://docs.python.org/zh-cn/3/library/asyncio-platforms.html#windows) 所以,一些使用了 asyncio 的库因此可能无法正常工作,如: 1. [playwright](https://playwright.dev/python/docs/library#incompatible-with-selectoreventloop-of-asyncio-on-windows) 如果在开启该功能后,原本**正常运行**的代码报错,且打印的异常堆栈信息和 asyncio 有关(异常一般为 `NotImplementedError`), 你可能就需要考虑相关库对事件循环的支持,以及是否启用该功能。 ::: 类型:`bool` 默认值:`False` 说明:是否开启 `uvicorn` 的 `reload` 功能,需要在机器人入口文件提供 ASGI 应用路径。 ```python title=bot.py app = nonebot.get_asgi() nonebot.run(app="bot:app") ``` ##### `fastapi_reload_dirs` 类型:`List[str] | None` 默认值:`None` 说明:重载监控文件夹列表,默认为 uvicorn 默认值 ##### `fastapi_reload_delay` 类型:`float | None` 默认值:`None` 说明:重载延迟,默认为 uvicorn 默认值 ##### `fastapi_reload_includes` 类型:`List[str] | None` 默认值:`None` 说明:要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值 ##### `fastapi_reload_excludes` 类型:`List[str] | None` 默认值:`None` 说明:不要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值 ##### `fastapi_extra` 类型:`Dist[str, Any]` 默认值:`{}` 说明:传递给 `FastAPI` 的其他参数 ### Quart **类型:**ASGI 服务端驱动器 > Quart is an asyncio reimplementation of the popular Flask microframework API. [Quart](https://quart.palletsprojects.com/) 是一个类 Flask 的异步版本,拥有与 Flask 非常相似的接口和使用方法。 ```env DRIVER=~quart ``` #### Quart 配置项 ##### `quart_reload` :::caution 警告 不推荐开启该配置项,在 Windows 平台上开启该功能有可能会造成预料之外的影响!替代方案:使用 `nb-cli` 命令行工具以及参数 `--reload` 启动 NoneBot。 ```bash nb run --reload ``` ::: 类型:`bool` 默认值:`False` 说明:是否开启 `uvicorn` 的 `reload` 功能,需要在机器人入口文件提供 ASGI 应用路径。 ```python title=bot.py app = nonebot.get_asgi() nonebot.run(app="bot:app") ``` ##### `quart_reload_dirs` 类型:`List[str] | None` 默认值:`None` 说明:重载监控文件夹列表,默认为 uvicorn 默认值 ##### `quart_reload_delay` 类型:`float | None` 默认值:`None` 说明:重载延迟,默认为 uvicorn 默认值 ##### `quart_reload_includes` 类型:`List[str] | None` 默认值:`None` 说明:要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值 ##### `quart_reload_excludes` 类型:`List[str] | None` 默认值:`None` 说明:不要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值 ##### `quart_extra` 类型:`Dist[str, Any]` 默认值:`{}` 说明:传递给 `Quart` 的其他参数 ### HTTPX **类型:**HTTP 客户端驱动器 :::caution 注意 本驱动器仅支持 HTTP 请求,不支持 WebSocket 连接请求。 ::: > [HTTPX](https://www.python-httpx.org/) is a fully featured HTTP client for Python 3, which provides sync and async APIs, and support for both HTTP/1.1 and HTTP/2. ```env DRIVER=~httpx ``` ### websockets **类型:**WebSocket 客户端驱动器 :::caution 注意 本驱动器仅支持 WebSocket 连接请求,不支持 HTTP 请求。 ::: > [websockets](https://websockets.readthedocs.io/) is a library for building WebSocket servers and clients in Python with a focus on correctness, simplicity, robustness, and performance. ```env DRIVER=~websockets ``` ### AIOHTTP **类型:**HTTP/WebSocket 客户端驱动器 > [AIOHTTP](https://docs.aiohttp.org/): Asynchronous HTTP Client/Server for asyncio and Python. ```env DRIVER=~aiohttp ``` ================================================ FILE: website/versioned_docs/version-2.4.4/advanced/matcher-provider.md ================================================ --- sidebar_position: 10 description: 自定义事件响应器存储 options: menu: - category: advanced weight: 110 --- # 事件响应器存储 事件响应器是 NoneBot 处理事件的核心,它们默认存储在一个字典中。在进入会话状态后,事件响应器将会转为临时响应器,作为最高优先级同样存储于该字典中。因此,事件响应器的存储类似于会话存储,它决定了整个 NoneBot 对事件的处理行为。 NoneBot 默认使用 Python 的字典将事件响应器存储于内存中,但是我们也可以自定义事件响应器存储,将事件响应器存储于其他地方,例如 Redis 等。这样我们就可以实现持久化、在多实例间共享会话状态等功能。 ## 编写存储提供者 事件响应器的存储提供者 `MatcherProvider` 抽象类继承自 `MutableMapping[int, list[type[Matcher]]]`,即以优先级为键,以事件响应器列表为值的映射。我们可以方便地进行逐优先级事件传播。 编写一个自定义的存储提供者,只需要继承并实现 `MatcherProvider` 抽象类: ```python from nonebot.matcher import MatcherProvider class CustomProvider(MatcherProvider): ... ``` ## 设置存储提供者 我们可以通过 `matchers.set_provider` 方法设置存储提供者: ```python {3} from nonebot.matcher import matchers matchers.set_provider(CustomProvider) assert isinstance(matchers.provider, CustomProvider) ``` ================================================ FILE: website/versioned_docs/version-2.4.4/advanced/matcher.md ================================================ --- sidebar_position: 5 description: 事件响应器组成与内置响应规则 options: menu: - category: advanced weight: 60 --- # 事件响应器进阶 在[指南](../tutorial/matcher.md)与[深入](../appendices/rule.md)中,我们已经介绍了事件响应器的基本用法以及响应规则、权限控制等功能。在这一节中,我们将介绍事件响应器的组成,内置的响应规则,与第三方响应规则拓展。 :::tip 提示 事件响应器允许继承,你可以通过直接继承 `Matcher` 类来创建一个新的事件响应器。 ::: ## 事件响应器组成 ### 事件响应器类型 事件响应器类型 `type` 即是该响应器所要响应的事件类型,只有在接收到的事件类型与该响应器的类型相同时,才会触发该响应器。如果类型为空字符串 `""`,则响应器将会响应所有类型的事件。事件响应器类型的检查在所有其他检查(权限控制、响应规则)之前进行。 NoneBot 内置了四种常用事件类型:`meta_event`、`message`、`notice`、`request`,分别对应元事件、消息、通知、请求。通常情况下,协议适配器会将事件合理地分类至这四种类型中。如果有其他类型的事件需要响应,可以自行定义新的类型。 ### 事件触发权限 事件触发权限 `permission` 是一个 `Permission` 对象,这在[权限控制](../appendices/permission.mdx)一节中已经介绍过。事件触发权限会在事件响应器的类型检查通过后进行检查,如果权限检查通过,则执行响应规则检查。 ### 事件响应规则 事件响应规则 `rule` 是一个 `Rule` 对象,这在[响应规则](../appendices/rule.md)一节中已经介绍过。事件响应器的响应规则会在事件响应器的权限检查通过后进行匹配,如果响应规则检查通过,则触发该响应器。 ### 响应优先级 响应优先级 `priority` 是一个正整数,用于指定响应器的优先级。响应器的优先级越小,越先被触发。如果响应器的优先级相同,则按照响应器的注册顺序进行触发。 ### 阻断 阻断 `block` 是一个布尔值,用于指定响应器是否阻断事件的传播。如果阻断为 `True`,则在该响应器被触发后,事件将不会再传播给其他下一优先级的响应器。 NoneBot 内置的事件响应器中,所有非 `command` 规则的 `message` 类型的事件响应器都会阻断事件传递,其他则不会。 在部分情况中,可以使用 [`stop_propagation`](../appendices/session-control.mdx#stop_propagation) 方法动态阻止事件传播,该方法需要 handler 在参数中获取 matcher 实例后调用方法。 ### 有效期 事件响应器的有效期分为 `temp` 和 `expire_time` 。`temp` 是一个布尔值,用于指定响应器是否为临时响应器。如果为 `True`,则该响应器在被触发后会被自动销毁。`expire_time` 是一个 `datetime` 对象,用于指定响应器的过期时间。如果 `expire_time` 不为 `None`,则在该时间点后,该响应器会被自动销毁。 ### 默认状态 事件响应器的默认状态 `default_state` 是一个 `dict` 对象,用于指定响应器的默认状态。在响应器被触发时,响应器将会初始化默认状态然后开始执行事件处理流程。 ## 基本辅助函数 NoneBot 为四种类型的事件响应器提供了五个基本的辅助函数: - `on`:创建任何类型的事件响应器。 - `on_metaevent`:创建元事件响应器。 - `on_message`:创建消息事件响应器。 - `on_request`:创建请求事件响应器。 - `on_notice`:创建通知事件响应器。 除了 `on` 函数具有一个 `type` 参数外,其余参数均相同: - `rule`:响应规则,可以是 `Rule` 对象或者 `RuleChecker` 函数。 - `permission`:事件触发权限,可以是 `Permission` 对象或者 `PermissionChecker` 函数。 - `handlers`:事件处理函数列表。 - `temp`:是否为临时响应器。 - `expire_time`:响应器的过期时间。 - `priority`:响应器的优先级。 - `block`:是否阻断事件传播。 - `state`:响应器的默认状态。 在消息类型的事件响应器的基础上,NoneBot 还内置了一些常用的响应规则,并结合为辅助函数来方便我们快速创建指定功能的响应器。下面我们逐个介绍。 ## 内置响应规则 :::tip 响应规则的使用方法可以参考 [深入 - 响应规则](../appendices/rule.md)。 ::: ### `startswith` `startswith` 响应规则用于匹配消息纯文本部分的开头是否与指定字符串(或一系列字符串)相同。可选参数 `ignorecase` 用于指定是否忽略大小写,默认为 `False`。 例如,我们可以创建一个匹配消息开头为 `!` 或者 `/` 的规则: ```python from nonebot.rule import startswith rule = startswith(("!", "/"), ignorecase=False) ``` 也可以直接使用辅助函数新建一个响应器: ```python from nonebot import on_startswith matcher = on_startswith(("!", "/"), ignorecase=False) ``` ### `endswith` `endswith` 响应规则用于匹配消息纯文本部分的结尾是否与指定字符串(或一系列字符串)相同。可选参数 `ignorecase` 用于指定是否忽略大小写,默认为 `False`。 例如,我们可以创建一个匹配消息结尾为 `.` 或者 `。` 的规则: ```python from nonebot.rule import endswith rule = endswith((".", "。"), ignorecase=False) ``` 也可以直接使用辅助函数新建一个响应器: ```python from nonebot import on_endswith matcher = on_endswith((".", "。"), ignorecase=False) ``` ### `fullmatch` `fullmatch` 响应规则用于匹配消息纯文本部分是否与指定字符串(或一系列字符串)完全相同。可选参数 `ignorecase` 用于指定是否忽略大小写,默认为 `False`。 例如,我们可以创建一个匹配消息为 `ping` 或者 `pong` 的规则: ```python from nonebot.rule import fullmatch rule = fullmatch(("ping", "pong"), ignorecase=False) ``` 也可以直接使用辅助函数新建一个响应器: ```python from nonebot import on_fullmatch matcher = on_fullmatch(("ping", "pong"), ignorecase=False) ``` ### `keyword` `keyword` 响应规则用于匹配消息纯文本部分是否包含指定字符串(或一系列字符串)。 例如,我们可以创建一个匹配消息中包含 `hello` 或者 `hi` 的规则: ```python from nonebot.rule import keyword rule = keyword("hello", "hi") ``` 也可以直接使用辅助函数新建一个响应器: ```python from nonebot import on_keyword matcher = on_keyword({"hello", "hi"}) ``` ### `command` `command` 是最常用的响应规则,它用于匹配消息是否为命令。它会根据配置中的 [Command Start 和 Command Separator](../appendices/config.mdx#command-start-和-command-separator) 来判断消息是否为命令。 例如,当我们配置了 `Command Start` 为 `/`,`Command Separator` 为 `.` 时: ```python from nonebot.rule import command # 匹配 "/help" 或者 "/帮助" 开头的消息 rule = command("help", "帮助") # 匹配 "/help.cmd" 开头的消息 rule = command(("help", "cmd")) ``` 也可以直接使用辅助函数新建一个响应器: ```python from nonebot import on_command matcher = on_command("help", aliases={"帮助"}) ``` 此外,`command` 响应规则默认允许消息命令与参数间不加空格,如果需要严格匹配命令与参数间的空白符,可以使用 `command` 函数的 `force_whitespace` 参数。`force_whitespace` 参数可以是 bool 类型或者具体的字符串,默认为 `False`。如果为 `True`,则命令与参数间必须有任意个数的空白符;如果为字符串,则命令与参数间必须有且与给定字符串一致的空白符。 ```python rule = command("help", force_whitespace=True) rule = command("help", force_whitespace=" ") ``` 命令解析后的结果可以通过 [`Command`](./dependency.mdx#command)、[`RawCommand`](./dependency.mdx#rawcommand)、[`CommandArg`](./dependency.mdx#commandarg)、[`CommandStart`](./dependency.mdx#commandstart)、[`CommandWhitespace`](./dependency.mdx#commandwhitespace) 依赖注入获取。 ### `shell_command` `shell_command` 响应规则用于匹配类 shell 命令形式的消息。它首先与 [`command`](#command) 响应规则一样进行命令匹配,如果匹配成功,则会进行进一步的参数解析。参数解析采用 `argparse` 标准库进行,在此基础上添加了消息序列 `Message` 支持。 例如,我们可以创建一个匹配 `/cmd` 命令并且带有 `-v` 选项与默认 `-h` 帮助选项的规则: ```python from nonebot.rule import shell_command, ArgumentParser parser = ArgumentParser() parser.add_argument("-v", "--verbose", action="store_true") rule = shell_command("cmd", parser=parser) ``` 更多关于 `argparse` 的使用方法请参考 [argparse 文档](https://docs.python.org/zh-cn/3/library/argparse.html)。我们也可以选择不提供 `parser` 参数,这样 `shell_command` 将不会解析参数,但会提供参数列表 `argv`。 直接使用辅助函数新建一个响应器: ```python from nonebot import on_shell_command from nonebot.rule import ArgumentParser parser = ArgumentParser() parser.add_argument("-v", "--verbose", action="store_true") matcher = on_shell_command("cmd", parser=parser) ``` 参数解析后的结果可以通过 [`ShellCommandArgv`](./dependency.mdx#shellcommandargv)、[`ShellCommandArgs`](./dependency.mdx#shellcommandargs) 依赖注入获取。 ### `regex` `regex` 响应规则用于匹配消息是否与指定正则表达式匹配。 :::tip 提示 正则表达式匹配使用 search 而非 match,如需从头匹配请使用 `r"^xxx"` 模式来确保匹配开头。 ::: 例如,我们可以创建一个匹配消息中包含字母并且忽略大小写的规则: ```python from nonebot.rule import regex rule = regex(r"[a-z]+", flags=re.IGNORECASE) ``` 也可以直接使用辅助函数新建一个响应器: ```python from nonebot import on_regex matcher = on_regex(r"[a-z]+", flags=re.IGNORECASE) ``` 正则匹配后的结果可以通过 [`RegexStr`](./dependency.mdx#regexstr)、[`RegexGroup`](./dependency.mdx#regexgroup)、[`RegexDict`](./dependency.mdx#regexdict) 依赖注入获取。 ### `to_me` `to_me` 响应规则用于匹配事件是否与机器人相关。 例如: ```python from nonebot.rule import to_me rule = to_me() ``` ### `is_type` `is_type` 响应规则用于匹配事件类型是否为指定类型(或者一系列类型)。 例如,我们可以创建一个匹配 OneBot v11 私聊和群聊消息事件的规则: ```python from nonebot.rule import is_type from nonebot.adapters.onebot.v11 import PrivateMessageEvent, GroupMessageEvent rule = is_type(PrivateMessageEvent, GroupMessageEvent) ``` ## 响应器组 为了更方便的管理一系列功能相近的响应器,NoneBot 提供了两种响应器组,它们可以帮助我们进行响应器的统一管理。 ### `CommandGroup` `CommandGroup` 可以用于管理一系列具有相同前置命令的子命令响应器。 例如,我们创建 `/cmd`、`/cmd.sub`、`/cmd.help` 三个命令,他们具有相同的优先级: ```python from nonebot import CommandGroup group = CommandGroup("cmd", priority=10) cmd = group.command(tuple()) sub_cmd = group.command("sub") help_cmd = group.command("help") ``` 命令别名 aliases 默认不会添加 `CommandGroup` 设定的前缀,如果需要为 aliases 添加前缀,可以添加 `prefix_aliases=True` 参数: ```python from nonebot import CommandGroup group = CommandGroup("cmd", prefix_aliases=True) cmd = group.command(tuple()) help_cmd = group.command("help", aliases={"帮助"}) ``` 这样就能成功匹配 `/cmd`、`/cmd.help`、`/cmd.帮助` 命令。如果未设置,将默认匹配 `/cmd`、`/cmd.help`、`/帮助` 命令。 ### `MatcherGroup` `MatcherGroup` 可以用于管理一系列具有相同属性的响应器。 例如,我们创建一个具有相同响应规则的响应器组: ```python from nonebot.rule import to_me from nonebot import MatcherGroup group = MatcherGroup(rule=to_me()) matcher1 = group.on_message() matcher2 = group.on_message() ``` ## 第三方响应规则 ### Alconna [`nonebot-plugin-alconna`](https://github.com/nonebot/plugin-alconna) 是一类提供了拓展响应规则的插件。 该插件使用 [Alconna](https://github.com/ArcletProject/Alconna) 作为命令解析器, 是一个简单、灵活、高效的命令参数解析器, 并且不局限于解析命令式字符串。 该插件提供了一类新的事件响应器辅助函数 `on_alconna`,以及 `AlconnaResult` 等依赖注入函数。 基于 `Alconna` 的特性,该插件同时提供了一系列便捷的消息段标注。 标注可用于在 `Alconna` 中匹配消息中除 text 外的其他消息段,也可用于快速创建各适配器下的消息段。所有标注位于 `nonebot_plugin_alconna.adapters` 中。 该插件同时通过提供 `UniMessage` (通用消息模型) 实现了**跨平台接收和发送消息**的功能。 详情请阅读最佳实践中的 [命令解析拓展](../best-practice/alconna/README.mdx) 章节。 ================================================ FILE: website/versioned_docs/version-2.4.4/advanced/plugin-info.md ================================================ --- sidebar_position: 2 description: 填写与获取插件相关的信息 options: menu: - category: advanced weight: 30 --- # 插件信息 NoneBot 是一个插件化的框架,可以通过加载插件来扩展功能。同时,我们也可以通过 NoneBot 的插件系统来获取相关信息,例如插件的名称、使用方法,用于收集帮助信息等。下面我们将介绍如何为插件添加元数据,以及如何获取插件信息。 ## 插件元数据 在 NoneBot 中,插件 [`Plugin`](../api/plugin/model.md#Plugin) 对象中存储了插件系统所需要的一系列信息。包括插件的索引名称、插件模块、插件中的事件响应器、插件父子关系等。通常,只有插件开发者才需要关心这些信息,而插件使用者或者机器人用户想要看到的是插件使用方法等帮助信息。因此,我们可以为插件添加插件元数据 `PluginMetadata`,它允许插件开发者为插件添加一些额外的信息。这些信息编写于插件模块的顶层,可以直接通过源码查看,或者通过 NoneBot 插件系统获取收集到的信息,通过其他方式发送给机器人用户等。 现在,假设我们有一个插件 `example`, 它的模块结构如下: ```tree {4-6} title=Project 📦 awesome-bot ├── 📂 awesome_bot │ └── 📂 plugins | └── 📂 example | ├── 📜 __init__.py | └── 📜 config.py ├── 📜 pyproject.toml └── 📜 README.md ``` 我们需要在插件顶层模块 `example/__init__.py` 中添加插件元数据,如下所示: ```python {1,5-12} title=example/__init__.py from nonebot.plugin import PluginMetadata from .config import Config __plugin_meta__ = PluginMetadata( name="示例插件", description="这是一个示例插件", usage="没什么用", type="application", config=Config, extra={}, ) ``` 我们可以看到,插件元数据 `PluginMetadata` 有三个基本属性:插件名称、插件描述、插件使用方法。除此之外,还有几个可选的属性(具体填写见[发布插件](../developer/plugin-publishing.mdx#插件元数据)章节): - `type`:插件类别,发布插件必填。当前有效类别有:`library`(为其他插件编写提供功能),`application`(向机器人用户提供功能); - `homepage`:插件项目主页,发布插件必填; - `config`:插件的[配置类](../appendices/config.mdx#插件配置),发布插件时如有配置类则必须填写; - `supported_adapters`:支持的适配器模块名集合,若插件只使用了 NoneBot 基本抽象,应显式填写 `None`; - `extra`:一个字典,可以用于存储任意信息。其他插件可以通过约定 `extra` 字典的键名来达成收集某些特殊信息的目的。 请注意,这里的**插件名称**是供使用者或机器人用户查看的人类可读名称,与插件索引名称无关。**插件索引名称(插件模块名称)**仅用于 NoneBot 插件系统**内部索引**。 ## 获取插件信息 NoneBot 提供了多种获取插件对象的方法,例如获取当前所有已导入的插件: ```python import nonebot plugins: set[Plugin] = nonebot.get_loaded_plugins() ``` 也可以通过插件索引名称获取插件对象: ```python import nonebot plugin: Plugin | None = nonebot.get_plugin("example") ``` 或者通过模块路径获取插件对象: ```python import nonebot plugin: Plugin | None = nonebot.get_plugin_by_module_name("awesome_bot.plugins.example") ``` 如果需要获取所有当前声明的插件名称(可能还未加载),可以使用 `get_available_plugin_names` 函数: ```python import nonebot plugin_names: set[str] = nonebot.get_available_plugin_names() ``` 插件对象 `Plugin` 中包含了多个属性: - `name`:插件索引名称 - `module`:插件模块 - `module_name`:插件模块路径 - `manager`:插件管理器 - `matcher`:插件中定义的事件响应器 - `parent_plugin`:插件的父插件 - `sub_plugins`:插件的子插件集合 - `metadata`:插件元数据 通过这些属性以及插件元数据,我们就可以收集所需要的插件信息了。 ================================================ FILE: website/versioned_docs/version-2.4.4/advanced/plugin-nesting.md ================================================ --- sidebar_position: 3 description: 编写与加载嵌套插件 options: menu: - category: advanced weight: 40 --- # 嵌套插件 NoneBot 支持嵌套插件,即一个插件可以包含其他插件。通过这种方式,我们可以将一个大型插件拆分成多个功能子插件,使得插件更加清晰、易于维护。我们可以直接在插件中使用 NoneBot 加载插件的方法来加载子插件。 ## 创建嵌套插件 我们可以在使用 `nb-cli` 命令[创建插件](../tutorial/create-plugin.md#创建插件)时,选择直接通过模板创建一个嵌套插件: ```bash $ nb plugin create [?] 插件名称: parent [?] 使用嵌套插件? (y/N) Y [?] 输出目录: awesome_bot/plugins ``` 或者使用 `nb plugin create --sub-plugin` 选项直接创建一个嵌套插件。 ## 已有插件 如果你已经有一个插件,想要在其中嵌套加载子插件,可以在插件的 `__init__.py` 中添加如下代码: ```python title=parent/__init__.py import nonebot from pathlib import Path sub_plugins = nonebot.load_plugins( str(Path(__file__).parent.joinpath("plugins").resolve()) ) ``` 这样,`parent` 插件就会加载 `parent/plugins` 目录下的所有插件。NoneBot 会正确识别这些插件的父子关系,你可以在 `parent` 的插件信息中看到这些子插件的信息,也可以在子插件信息中看到它们的父插件信息。 ================================================ FILE: website/versioned_docs/version-2.4.4/advanced/requiring.md ================================================ --- sidebar_position: 4 description: 使用其他插件提供的功能 options: menu: - category: advanced weight: 50 --- # 跨插件访问 NoneBot 插件化系统的设计使得插件之间可以功能独立、各司其职,我们可以更好地维护和扩展插件。但是,有时候我们可能需要在不同插件之间调用功能。NoneBot 生态中就有一类插件,它们专为其他插件提供功能支持,如:[定时任务插件](../best-practice/scheduler.md)、[数据存储插件](../best-practice/data-storing.md)等。这时候我们就需要在插件之间进行跨插件访问。 ## 插件跟踪 由于 NoneBot 插件系统通过 [Import Hooks](https://docs.python.org/3/reference/import.html#import-hooks) 的方式实现插件加载与跟踪管理,因此我们**不能**在 NoneBot 跟踪插件前进行模块 import,这会导致插件加载失败。即,我们不能在使用 NoneBot 提供的加载插件方法前,直接使用 `import` 语句导入插件。 对于在项目目录下的插件,我们通常直接使用 `load_from_toml` 等方法一次性加载所有插件。由于这些插件已经被声明,即便插件导入顺序不同,NoneBot 也能正确跟踪插件。此时,我们不需要对跨插件访问进行特殊处理。但当我们使用了外部插件,如果没有事先声明或加载插件,NoneBot 并不会将其当作插件进行跟踪,可能会出现意料之外的错误出现。 简单来说,我们必须在 `import` 外部插件之前,确保依赖的外部插件已经被声明或加载。 ## 插件依赖声明 NoneBot 提供了一种方法来确保我们依赖的插件已经被正确加载,即使用 `require` 函数。通过 `require` 函数,我们可以在当前插件中声明依赖的插件,NoneBot 会在加载当前插件时,检查依赖的插件是否已经被加载,如果没有,会尝试优先加载依赖的插件。 假设我们有一个插件 `a` 依赖于插件 `b`,我们可以在插件 `a` 中使用 `require` 函数声明其依赖于插件 `b`: ```python {3} title=a/__init__.py from nonebot import require require("b") from b import some_function ``` 其中,`require` 函数的参数为插件索引名称或者外部插件的模块名称。在完成依赖声明后,我们可以在插件 `a` 中直接导入插件 `b` 所提供的功能。 ================================================ FILE: website/versioned_docs/version-2.4.4/advanced/routing.md ================================================ --- sidebar_position: 9 description: 添加服务端路由规则 options: menu: - category: advanced weight: 100 --- # 添加路由 在[驱动器](./driver.md)一节中,我们了解了驱动器的两种类型。既然驱动器可以作为服务端运行,那么我们就可以向驱动器添加路由规则,从而实现自定义的 API 接口等功能。在添加路由规则时,我们需要注意驱动器的类型,详情可以参考[选择驱动器](./driver.md#配置驱动器)。 NoneBot 中,我们可以通过两种途径向 ASGI 驱动器添加路由规则: 1. 通过 NoneBot 的兼容层建立路由规则。 2. 直接向 ASGI 应用添加路由规则。 这两种途径各有优劣,前者可以在各种服务端型驱动器下运行,但并不能直接使用 ASGI 应用框架提供的特性与功能;后者直接使用 ASGI 应用,更自由、功能完整,但只能在特定类型驱动器下运行。 在向驱动器添加路由规则时,我们需要注意驱动器是否为服务端类型,我们可以通过以下方式判断: ```python from nonebot import get_driver from nonebot.drivers import ASGIMixin # highlight-next-line can_use = isinstance(get_driver(), ASGIMixin) ``` ## 通过兼容层添加路由 NoneBot 兼容层定义了两个数据类 `HTTPServerSetup` 和 `WebSocketServerSetup`,分别用于定义 HTTP 服务端和 WebSocket 服务端的路由规则。 ### HTTP 路由 `HTTPServerSetup` 具有四个属性: - `path`:路由路径,不支持特殊占位表达式。类型为 `URL`。 - `method`:请求方法。类型为 `str`。 - `name`:路由名称,不可重复。类型为 `str`。 - `handle_func`:路由处理函数。类型为 `Callable[[Request], Awaitable[Response]]`。 例如,我们添加一个 `/hello` 的路由,当请求方法为 `GET` 时,返回 `200 OK` 以及返回体信息: ```python from nonebot import get_driver from nonebot.drivers import URL, Request, Response, ASGIMixin, HTTPServerSetup async def hello(request: Request) -> Response: return Response(200, content="Hello, world!") if isinstance((driver := get_driver()), ASGIMixin): driver.setup_http_server( HTTPServerSetup( path=URL("/hello"), method="GET", name="hello", handle_func=hello, ) ) ``` 对于 `Request` 和 `Response` 的详细信息,可以参考 [API 文档](../api/drivers/index.md)。 ### WebSocket 路由 `WebSocketServerSetup` 具有三个属性: - `path`:路由路径,不支持特殊占位表达式。类型为 `URL`。 - `name`:路由名称,不可重复。类型为 `str`。 - `handle_func`:路由处理函数。类型为 `Callable[[WebSocket], Awaitable[Any]]`。 例如,我们添加一个 `/ws` 的路由,发送所有接收到的数据: ```python from nonebot import get_driver from nonebot.drivers import URL, ASGIMixin, WebSocket, WebSocketServerSetup async def ws_handler(ws: WebSocket): await ws.accept() try: while True: data = await ws.receive() await ws.send(data) except WebSocketClosed as e: # handle closed ... finally: with contextlib.suppress(Exception): await websocket.close() # do some cleanup if isinstance((driver := get_driver()), ASGIMixin): driver.setup_websocket_server( WebSocketServerSetup( path=URL("/ws"), name="ws", handle_func=ws_handler, ) ) ``` 对于 `WebSocket` 的详细信息,可以参考 [API 文档](../api/drivers/index.md)。 ## 使用 ASGI 应用添加路由 ### 获取 ASGI 应用 NoneBot 服务端类型的驱动器具有两个属性 `server_app` 和 `asgi`,分别对应驱动框架应用和 ASGI 应用。通常情况下,这两个应用是同一个对象。我们可以通过 `get_app()` 方法快速获取: ```python import nonebot app = nonebot.get_app() asgi = nonebot.get_asgi() ``` ### 添加路由规则 在获取到了 ASGI 应用后,我们就可以直接使用 ASGI 应用框架提供的功能来添加路由规则了。这里我们以 [FastAPI](./driver.md#fastapi默认) 为例,演示如何添加路由规则。 在下面的代码中,我们添加了一个 `GET` 类型的 `/api` 路由,具体方法参考 [FastAPI 文档](https://fastapi.tiangolo.com/)。 ```python import nonebot from fastapi import FastAPI app: FastAPI = nonebot.get_app() @app.get("/api") async def custom_api(): return {"message": "Hello, world!"} ``` ================================================ FILE: website/versioned_docs/version-2.4.4/advanced/runtime-hook.md ================================================ --- sidebar_position: 8 description: 在特定的生命周期中执行代码 options: menu: - category: advanced weight: 90 --- # 钩子函数 > [钩子编程](https://zh.wikipedia.org/wiki/%E9%92%A9%E5%AD%90%E7%BC%96%E7%A8%8B)(hooking),也称作“挂钩”,是计算机程序设计术语,指通过拦截软件模块间的函数调用、消息传递、事件传递来修改或扩展操作系统、应用程序或其他软件组件的行为的各种技术。处理被拦截的函数调用、事件、消息的代码,被称为钩子(hook)。 在 NoneBot 中有一系列预定义的钩子函数,可以分为两类:**全局钩子函数**和**事件处理钩子函数**,这些钩子函数可以用装饰器的形式来使用。 ## 全局钩子函数 全局钩子函数是指 NoneBot 针对其本身运行过程的钩子函数。 这些钩子函数是由驱动器来运行的,故需要先[获得全局驱动器](./driver.md#获取驱动器)。 ### 启动准备 这个钩子函数会在 NoneBot 启动时运行。很多时候,我们并不希望在模块被导入时就执行一些耗时操作,如:连接数据库,这时候我们可以在这个钩子函数中进行这些操作。 ```python from nonebot import get_driver driver = get_driver() @driver.on_startup async def do_something(): pass ``` ### 终止处理 这个钩子函数会在 NoneBot 终止时运行。我们可以在这个钩子函数中进行一些清理工作,如:关闭数据库连接。 ```python from nonebot import get_driver driver = get_driver() @driver.on_shutdown async def do_something(): pass ``` ### Bot 连接处理 这个钩子函数会在任何协议适配器连接 `Bot` 对象至 NoneBot 时运行。支持依赖注入,可以直接注入 `Bot` 对象。 ```python from nonebot import get_driver driver = get_driver() @driver.on_bot_connect async def do_something(bot: Bot): pass ``` ### Bot 断开处理 这个钩子函数会在 `Bot` 断开与 NoneBot 的连接时运行。支持依赖注入,可以直接注入 `Bot` 对象。 ```python from nonebot import get_driver driver = get_driver() @driver.on_bot_disconnect async def do_something(bot: Bot): pass ``` ## 事件处理钩子函数 这些钩子函数指的是影响 NoneBot 进行**事件处理**的函数, 这些函数可以跟普通的事件处理函数一样接受相应的参数。 ### 事件预处理 这个钩子函数会在 NoneBot 接收到新的事件时运行。支持依赖注入,可以注入 `Bot` 对象、事件、会话状态。在这个钩子函数内抛出 `nonebot.exception.IgnoredException` 会使 NoneBot 忽略该事件。 ```python from nonebot.exception import IgnoredException from nonebot.message import event_preprocessor @event_preprocessor async def do_something(event: Event): if not event.is_tome(): raise IgnoredException("some reason") ``` ### 事件后处理 这个钩子函数会在 NoneBot 处理事件完成后运行。支持依赖注入,可以注入 `Bot` 对象、事件、会话状态。 ```python from nonebot.message import event_postprocessor @event_postprocessor async def do_something(event: Event): pass ``` ### 运行预处理 这个钩子函数会在 NoneBot 运行事件响应器前运行。支持依赖注入,可以注入 `Bot` 对象、事件、事件响应器、会话状态。在这个钩子函数内抛出 `nonebot.exception.IgnoredException` 也会使 NoneBot 忽略本次运行。 ```python from nonebot.message import run_preprocessor from nonebot.exception import IgnoredException @run_preprocessor async def do_something(event: Event, matcher: Matcher): if not event.is_tome(): raise IgnoredException("some reason") ``` ### 运行后处理 这个钩子函数会在 NoneBot 运行事件响应器后运行。支持依赖注入,可以注入 `Bot` 对象、事件、事件响应器、会话状态、运行中产生的异常。 ```python from nonebot.message import run_postprocessor @run_postprocessor async def do_something(event: Event, matcher: Matcher, exception: Optional[Exception]): pass ``` ### 平台接口调用钩子 这个钩子函数会在 `Bot` 对象调用平台接口时运行。在这个钩子函数中,我们可以通过引起 `MockApiException` 异常来阻止 `Bot` 对象调用平台接口并返回指定的结果。 ```python from nonebot.adapters import Bot from nonebot.exception import MockApiException @Bot.on_calling_api async def handle_api_call(bot: Bot, api: str, data: Dict[str, Any]): if api == "send_msg": raise MockApiException(result={"message_id": 123}) ``` ### 平台接口调用后钩子 这个钩子函数会在 `Bot` 对象调用平台接口后运行。在这个钩子函数中,我们可以通过引起 `MockApiException` 异常来忽略平台接口返回的结果并返回指定的结果。 ```python from nonebot.adapters import Bot from nonebot.exception import MockApiException @Bot.on_called_api async def handle_api_result( bot: Bot, exception: Optional[Exception], api: str, data: Dict[str, Any], result: Any ): if not exception and api == "send_msg": raise MockApiException(result={**result, "message_id": 123}) ``` ================================================ FILE: website/versioned_docs/version-2.4.4/advanced/session-updating.md ================================================ --- sidebar_position: 7 description: 控制会话响应对象 options: menu: - category: advanced weight: 80 --- # 会话更新 在 NoneBot 中,在某个事件响应器对事件响应后,即是进入了会话状态,会话状态会持续到整个事件响应流程结束。会话过程中,机器人可以与用户进行多次交互。每次需要等待用户事件时,NoneBot 将会复制一个新的临时事件响应器,并更新该事件响应器使其响应当前会话主体的消息,这个过程称为会话更新。 会话更新分为两部分:**更新[事件响应器类型](./matcher.md#事件响应器类型)**和**更新[事件触发权限](./matcher.md#事件触发权限)**。 ## 更新事件响应器类型 通常情况下,与机器人用户进行的会话都是通过消息事件进行的,因此会话更新后的默认响应事件类型为 `message`。如果希望接收一个特定类型的消息,比如 `notice` 等,我们需要自定义响应事件类型更新函数。响应事件类型更新函数是一个 `Dependent`,可以使用依赖注入。 ```python {3-5} foo = on_message() @foo.type_updater async def _() -> str: return "notice" ``` 在注册了上述响应事件类型更新函数后,当我们需要等待用户事件时,将只会响应 `notice` 类型的事件。如果希望在会话过程中的不同阶段响应不同类型的事件,我们就需要使用更复杂的逻辑来更新响应事件类型(如:根据会话状态),这里将不再展示。 ## 更新事件触发权限 会话通常是由机器人与用户进行的一对一交互,因此会话更新后的默认触发权限为当前事件的会话 ID。这个会话 ID 由协议适配器生成,通常由用户 ID 和群 ID 等组成。如果希望实现更复杂的会话功能(如:多用户同时参与的会话),我们需要自定义触发权限更新函数。触发权限更新函数是一个 `Dependent`,可以使用依赖注入。 ```python {5-7} from nonebot.permission import User foo = on_message() @foo.permission_updater async def _(event: Event, matcher: Matcher) -> Permission: return Permission(User.from_event(event, perm=matcher.permission)) ``` 上述权限更新函数是默认的权限更新函数,它将会话的触发权限更新为当前事件的会话 ID。如果我们希望响应多个用户的消息,我们可以如下修改: ```python {5-7} from nonebot.permission import USER foo = on_message() @foo.permission_updater async def _(matcher: Matcher) -> Permission: return USER("session1", "session2", perm=matcher.permission) ``` 请注意,此处为全大写字母的 `USER` 权限,它可以匹配多个会话 ID。通过这种方式,我们可以实现多用户同时参与的会话。 我们已经了解了如何控制会话的更新,相信你已经能够实现更复杂的会话功能了,例如多人小游戏等等。欢迎将你的作品分享到[插件商店](/store/plugins)。 ================================================ FILE: website/versioned_docs/version-2.4.4/api/.gitkeep ================================================ ================================================ FILE: website/versioned_docs/version-2.4.4/api/adapters/_category_.json ================================================ { "position": 15 } ================================================ FILE: website/versioned_docs/version-2.4.4/api/adapters/index.md ================================================ --- mdx: format: md sidebar_position: 0 description: nonebot.adapters 模块 --- # nonebot.adapters 本模块定义了协议适配基类,各协议请继承以下基类。 使用 [Driver.register_adapter](../drivers/index.md#Driver-register-adapter) 注册适配器。 ## _abstract class_ `Adapter(driver, **kwargs)` {#Adapter} - **说明** 协议适配器基类。 通常,在 Adapter 中编写协议通信相关代码,如: 建立通信连接、处理接收与发送 data 等。 - **参数** - `driver` ([Driver](../drivers/index.md#Driver)): [Driver](../drivers/index.md#Driver) 实例 - `**kwargs` (Any): 其他由 [Driver.register_adapter](../drivers/index.md#Driver-register-adapter) 传入的额外参数 ### _instance-var_ `driver` {#Adapter-driver} - **类型:** [Driver](../drivers/index.md#Driver) - **说明:** 实例 ### _instance-var_ `bots` {#Adapter-bots} - **类型:** dict[str, [Bot](#Bot)] - **说明:** 本协议适配器已建立连接的 [Bot](#Bot) 实例 ### _abstract classmethod_ `get_name()` {#Adapter-get-name} - **说明:** 当前协议适配器的名称 - **参数** empty - **返回** - str ### _property_ `config` {#Adapter-config} - **类型:** [Config](../config.md#Config) - **说明:** 全局 NoneBot 配置 ### _method_ `bot_connect(bot)` {#Adapter-bot-connect} - **说明** 告知 NoneBot 建立了一个新的 [Bot](#Bot) 连接。 当有新的 [Bot](#Bot) 实例连接建立成功时调用。 - **参数** - `bot` ([Bot](#Bot)): [Bot](#Bot) 实例 - **返回** - None ### _method_ `bot_disconnect(bot)` {#Adapter-bot-disconnect} - **说明** 告知 NoneBot [Bot](#Bot) 连接已断开。 当有 [Bot](#Bot) 实例连接断开时调用。 - **参数** - `bot` ([Bot](#Bot)): [Bot](#Bot) 实例 - **返回** - None ### _method_ `setup_http_server(setup)` {#Adapter-setup-http-server} - **说明:** 设置一个 HTTP 服务器路由配置 - **参数** - `setup` ([HTTPServerSetup](../drivers/index.md#HTTPServerSetup)) - **返回** - untyped ### _method_ `setup_websocket_server(setup)` {#Adapter-setup-websocket-server} - **说明:** 设置一个 WebSocket 服务器路由配置 - **参数** - `setup` ([WebSocketServerSetup](../drivers/index.md#WebSocketServerSetup)) - **返回** - untyped ### _async method_ `request(setup)` {#Adapter-request} - **说明:** 进行一个 HTTP 客户端请求 - **参数** - `setup` ([Request](../drivers/index.md#Request)) - **返回** - [Response](../drivers/index.md#Response) ### _method_ `websocket(setup)` {#Adapter-websocket} - **说明:** 建立一个 WebSocket 客户端连接请求 - **参数** - `setup` ([Request](../drivers/index.md#Request)) - **返回** - AsyncGenerator[[WebSocket](../drivers/index.md#WebSocket), None] ### _method_ `on_ready(func)` {#Adapter-on-ready} - **参数** - `func` (LIFESPAN_FUNC) - **返回** - LIFESPAN_FUNC ## _abstract class_ `Bot(adapter, self_id)` {#Bot} - **说明** Bot 基类。 用于处理上报消息,并提供 API 调用接口。 - **参数** - `adapter` ([Adapter](#Adapter)): 协议适配器实例 - `self_id` (str): 机器人 ID ### _instance-var_ `adapter` {#Bot-adapter} - **类型:** [Adapter](#Adapter) - **说明:** 协议适配器实例 ### _instance-var_ `self_id` {#Bot-self-id} - **类型:** str - **说明:** 机器人 ID ### _property_ `type` {#Bot-type} - **类型:** str - **说明:** 协议适配器名称 ### _property_ `config` {#Bot-config} - **类型:** [Config](../config.md#Config) - **说明:** 全局 NoneBot 配置 ### _async method_ `call_api(api, **data)` {#Bot-call-api} - **说明:** 调用机器人 API 接口,可以通过该函数或直接通过 bot 属性进行调用 - **参数** - `api` (str): API 名称 - `**data` (Any): API 数据 - **返回** - Any - **用法** ```python await bot.call_api("send_msg", message="hello world") await bot.send_msg(message="hello world") ``` ### _abstract async method_ `send(event, message, **kwargs)` {#Bot-send} - **说明:** 调用机器人基础发送消息接口 - **参数** - `event` ([Event](#Event)): 上报事件 - `message` (str | [Message](#Message) | [MessageSegment](#MessageSegment)): 要发送的消息 - `**kwargs` (Any): 任意额外参数 - **返回** - Any ### _classmethod_ `on_calling_api(func)` {#Bot-on-calling-api} - **说明** 调用 api 预处理。 钩子函数参数: - bot: 当前 bot 对象 - api: 调用的 api 名称 - data: api 调用的参数字典 - **参数** - `func` ([T_CallingAPIHook](../typing.md#T-CallingAPIHook)) - **返回** - [T_CallingAPIHook](../typing.md#T-CallingAPIHook) ### _classmethod_ `on_called_api(func)` {#Bot-on-called-api} - **说明** 调用 api 后处理。 钩子函数参数: - bot: 当前 bot 对象 - exception: 调用 api 时发生的错误 - api: 调用的 api 名称 - data: api 调用的参数字典 - result: api 调用的返回 - **参数** - `func` ([T_CalledAPIHook](../typing.md#T-CalledAPIHook)) - **返回** - [T_CalledAPIHook](../typing.md#T-CalledAPIHook) ## _abstract class_ `Event()` {#Event} - **说明:** Event 基类。提供获取关键信息的方法,其余信息可直接获取。 - **参数** auto ### _abstract method_ `get_type()` {#Event-get-type} - **说明:** 获取事件类型的方法,类型通常为 NoneBot 内置的四种类型。 - **参数** empty - **返回** - str ### _abstract method_ `get_event_name()` {#Event-get-event-name} - **说明:** 获取事件名称的方法。 - **参数** empty - **返回** - str ### _abstract method_ `get_event_description()` {#Event-get-event-description} - **说明:** 获取事件描述的方法,通常为事件具体内容。 - **参数** empty - **返回** - str ### _method_ `get_log_string()` {#Event-get-log-string} - **说明** 获取事件日志信息的方法。 通常你不需要修改这个方法,只有当希望 NoneBot 隐藏该事件日志时, 可以抛出 `NoLogException` 异常。 - **参数** empty - **返回** - str - **异常** - NoLogException: 希望 NoneBot 隐藏该事件日志 ### _abstract method_ `get_user_id()` {#Event-get-user-id} - **说明:** 获取事件主体 id 的方法,通常是用户 id 。 - **参数** empty - **返回** - str ### _abstract method_ `get_session_id()` {#Event-get-session-id} - **说明:** 获取会话 id 的方法,用于判断当前事件属于哪一个会话, 通常是用户 id、群组 id 组合。 - **参数** empty - **返回** - str ### _abstract method_ `get_message()` {#Event-get-message} - **说明:** 获取事件消息内容的方法。 - **参数** empty - **返回** - [Message](#Message) ### _method_ `get_plaintext()` {#Event-get-plaintext} - **说明** 获取消息纯文本的方法。 通常不需要修改,默认通过 `get_message().extract_plain_text` 获取。 - **参数** empty - **返回** - str ### _abstract method_ `is_tome()` {#Event-is-tome} - **说明:** 获取事件是否与机器人有关的方法。 - **参数** empty - **返回** - bool ## _abstract class_ `Message()` {#Message} - **说明:** 消息序列 - **参数** - `message`: 消息内容 ### _classmethod_ `template(format_string)` {#Message-template} - **说明** 创建消息模板。 用法和 `str.format` 大致相同,支持以 `Message` 对象作为消息模板并输出消息对象。 并且提供了拓展的格式化控制符, 可以通过该消息类型的 `MessageSegment` 工厂方法创建消息。 - **参数** - `format_string` (str | TM): 格式化模板 - **返回** - [MessageTemplate](#MessageTemplate)[Self]: 消息格式化器 ### _abstract classmethod_ `get_segment_class()` {#Message-get-segment-class} - **说明:** 获取消息段类型 - **参数** empty - **返回** - type[TMS] ### _abstract staticmethod_ `_construct(msg)` {#Message--construct} - **说明:** 构造消息数组 - **参数** - `msg` (str) - **返回** - Iterable[TMS] ### _method_ `__getitem__(args)` {#Message---getitem--} - **重载** **1.** `(args) -> Self` - **参数** - `args` (str): 消息段类型 - **返回** - Self: 所有类型为 `args` 的消息段 **2.** `(args) -> TMS` - **参数** - `args` (tuple[str, int]): 消息段类型和索引 - **返回** - TMS: 类型为 `args[0]` 的消息段第 `args[1]` 个 **3.** `(args) -> Self` - **参数** - `args` (tuple[str, slice]): 消息段类型和切片 - **返回** - Self: 类型为 `args[0]` 的消息段切片 `args[1]` **4.** `(args) -> TMS` - **参数** - `args` (int): 索引 - **返回** - TMS: 第 `args` 个消息段 **5.** `(args) -> Self` - **参数** - `args` (slice): 切片 - **返回** - Self: 消息切片 `args` ### _method_ `__contains__(value)` {#Message---contains--} - **说明:** 检查消息段是否存在 - **参数** - `value` (TMS | str): 消息段或消息段类型 - **返回** - bool: 消息内是否存在给定消息段或给定类型的消息段 ### _method_ `has(value)` {#Message-has} - **说明:** 与 [`__contains__`](#Message---contains--) 相同 - **参数** - `value` (TMS | str) - **返回** - bool ### _method_ `index(value, *args)` {#Message-index} - **说明:** 索引消息段 - **参数** - `value` (TMS | str): 消息段或者消息段类型 - `*args` (SupportsIndex) - `arg`: start 与 end - **返回** - int: 索引 index - **异常** - ValueError: 消息段不存在 ### _method_ `get(type_, count=None)` {#Message-get} - **说明:** 获取指定类型的消息段 - **参数** - `type_` (str): 消息段类型 - `count` (int | None): 获取个数 - **返回** - Self: 构建的新消息 ### _method_ `count(value)` {#Message-count} - **说明:** 计算指定消息段的个数 - **参数** - `value` (TMS | str): 消息段或消息段类型 - **返回** - int: 个数 ### _method_ `only(value)` {#Message-only} - **说明:** 检查消息中是否仅包含指定消息段 - **参数** - `value` (TMS | str): 指定消息段或消息段类型 - **返回** - bool: 是否仅包含指定消息段 ### _method_ `append(obj)` {#Message-append} - **说明:** 添加一个消息段到消息数组末尾。 - **参数** - `obj` (str | TMS): 要添加的消息段 - **返回** - Self ### _method_ `extend(obj)` {#Message-extend} - **说明:** 拼接一个消息数组或多个消息段到消息数组末尾。 - **参数** - `obj` (Self | Iterable[TMS]): 要添加的消息数组 - **返回** - Self ### _method_ `join(iterable)` {#Message-join} - **说明:** 将多个消息连接并将自身作为分割 - **参数** - `iterable` (Iterable[TMS | Self]): 要连接的消息 - **返回** - Self: 连接后的消息 ### _method_ `copy()` {#Message-copy} - **说明:** 深拷贝消息 - **参数** empty - **返回** - Self ### _method_ `include(*types)` {#Message-include} - **说明:** 过滤消息 - **参数** - `*types` (str): 包含的消息段类型 - **返回** - Self: 新构造的消息 ### _method_ `exclude(*types)` {#Message-exclude} - **说明:** 过滤消息 - **参数** - `*types` (str): 不包含的消息段类型 - **返回** - Self: 新构造的消息 ### _method_ `extract_plain_text()` {#Message-extract-plain-text} - **说明:** 提取消息内纯文本消息 - **参数** empty - **返回** - str ## _abstract class_ `MessageSegment()` {#MessageSegment} - **说明:** 消息段基类 - **参数** auto ### _instance-var_ `type` {#MessageSegment-type} - **类型:** str - **说明:** 消息段类型 ### _class-var_ `data` {#MessageSegment-data} - **类型:** dict[str, Any] - **说明:** 消息段数据 ### _abstract classmethod_ `get_message_class()` {#MessageSegment-get-message-class} - **说明:** 获取消息数组类型 - **参数** empty - **返回** - type[TM] ### _abstract method_ `__str__()` {#MessageSegment---str--} - **说明:** 该消息段所代表的 str,在命令匹配部分使用 - **参数** empty - **返回** - str ### _method_ `__add__(other)` {#MessageSegment---add--} - **参数** - `other` (str | Self | Iterable[Self]) - **返回** - TM ### _method_ `get(key, default=None)` {#MessageSegment-get} - **参数** - `key` (str) - `default` (Any) - **返回** - untyped ### _method_ `keys()` {#MessageSegment-keys} - **参数** empty - **返回** - untyped ### _method_ `values()` {#MessageSegment-values} - **参数** empty - **返回** - untyped ### _method_ `items()` {#MessageSegment-items} - **参数** empty - **返回** - untyped ### _method_ `join(iterable)` {#MessageSegment-join} - **参数** - `iterable` (Iterable[Self | TM]) - **返回** - TM ### _method_ `copy()` {#MessageSegment-copy} - **参数** empty - **返回** - Self ### _abstract method_ `is_text()` {#MessageSegment-is-text} - **说明:** 当前消息段是否为纯文本 - **参数** empty - **返回** - bool ## _class_ `MessageTemplate(template, factory=str, private_getattr=False)` {#MessageTemplate} - **说明:** 消息模板格式化实现类。 - **参数** - `template` (str | TM): 模板 - `factory` (type[str] | type[TM]): 消息类型工厂,默认为 `str` - `private_getattr` (bool): 是否允许在模板中访问私有属性,默认为 `False` ### _method_ `add_format_spec(spec, name=None)` {#MessageTemplate-add-format-spec} - **参数** - `spec` (FormatSpecFunc_T) - `name` (str | None) - **返回** - FormatSpecFunc_T ### _method_ `format(*args, **kwargs)` {#MessageTemplate-format} - **说明:** 根据传入参数和模板生成消息对象 - **参数** - `*args` - `**kwargs` - **返回** - TF ### _method_ `format_map(mapping)` {#MessageTemplate-format-map} - **说明:** 根据传入字典和模板生成消息对象, 在传入字段名不是有效标识符时有用 - **参数** - `mapping` (Mapping[str, Any]) - **返回** - TF ### _method_ `vformat(format_string, args, kwargs)` {#MessageTemplate-vformat} - **参数** - `format_string` (str) - `args` (Sequence[Any]) - `kwargs` (Mapping[str, Any]) - **返回** - TF ### _method_ `get_field(field_name, args, kwargs)` {#MessageTemplate-get-field} - **参数** - `field_name` (str) - `args` (Sequence[Any]) - `kwargs` (Mapping[str, Any]) - **返回** - tuple[Any, int | str] ### _method_ `format_field(value, format_spec)` {#MessageTemplate-format-field} - **参数** - `value` (Any) - `format_spec` (str) - **返回** - Any ================================================ FILE: website/versioned_docs/version-2.4.4/api/compat.md ================================================ --- mdx: format: md sidebar_position: 16 description: nonebot.compat 模块 --- # nonebot.compat 本模块为 Pydantic 版本兼容层模块 为兼容 Pydantic V1 与 V2 版本,定义了一系列兼容函数与类供使用。 ## _var_ `Required` {#Required} - **类型:** untyped - **说明:** Alias of Ellipsis for compatibility with pydantic v1 ## _library-attr_ `PydanticUndefined` {#PydanticUndefined} - **说明:** Pydantic Undefined object ## _library-attr_ `PydanticUndefinedType` {#PydanticUndefinedType} - **说明:** Pydantic Undefined type ## _var_ `DEFAULT_CONFIG` {#DEFAULT-CONFIG} - **类型:** untyped - **说明:** Default config for validations ## _def_ `LegacyUnionField()` {#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 - **参数** - `default` (Any) - `**kwargs` (Any) ### _property_ `extra` {#FieldInfo-extra} - **类型:** dict[str, Any] - **说明** Extra data that is not part of the standard pydantic fields. For compatibility with pydantic v1. ## _class_ `ModelField()` {#ModelField} - **说明:** ModelField class for compatibility with pydantic v1 - **参数** auto ### _instance-var_ `name` {#ModelField-name} - **类型:** str - **说明:** The name of the field. ### _instance-var_ `annotation` {#ModelField-annotation} - **类型:** Any - **说明:** The annotation of the field. ### _instance-var_ `field_info` {#ModelField-field-info} - **类型:** FieldInfo - **说明:** The FieldInfo of the field. ### _classmethod_ `construct(name, annotation, field_info=None)` {#ModelField-construct} - **说明:** Construct a ModelField from given infos. - **参数** - `name` (str) - `annotation` (Any) - `field_info` (FieldInfo | None) - **返回** - Self ### _method_ `get_default()` {#ModelField-get-default} - **说明:** Get the default value of the field. - **参数** empty - **返回** - Any ### _method_ `validate_value(value)` {#ModelField-validate-value} - **说明:** Validate the value pass to the field. - **参数** - `value` (Any) - **返回** - Any ## _def_ `model_fields(model)` {#model-fields} - **说明:** Get field list of a model. - **参数** - `model` (type[BaseModel]) - **返回** - list[ModelField] ## _def_ `model_config(model)` {#model-config} - **说明:** Get config of a model. - **参数** - `model` (type[BaseModel]) - **返回** - Any ## _def_ `model_dump(model, include=None, exclude=None, by_alias=False, exclude_unset=False, exclude_defaults=False, exclude_none=False)` {#model-dump} - **参数** - `model` (BaseModel) - `include` (set[str] | None) - `exclude` (set[str] | None) - `by_alias` (bool) - `exclude_unset` (bool) - `exclude_defaults` (bool) - `exclude_none` (bool) - **返回** - dict[str, Any] ## _def_ `type_validate_python(type_, data)` {#type-validate-python} - **说明:** Validate data with given type. - **参数** - `type_` (type[T]) - `data` (Any) - **返回** - T ## _def_ `type_validate_json(type_, data)` {#type-validate-json} - **说明:** Validate JSON with given type. - **参数** - `type_` (type[T]) - `data` (str | bytes) - **返回** - T ## _def_ `custom_validation(class_)` {#custom-validation} - **说明:** Use pydantic v1 like validator generator in pydantic v2 - **参数** - `class_` (type[CVC]) - **返回** - type[CVC] ================================================ FILE: website/versioned_docs/version-2.4.4/api/config.md ================================================ --- mdx: format: md sidebar_position: 1 description: nonebot.config 模块 --- # nonebot.config 本模块定义了 NoneBot 本身运行所需的配置项。 NoneBot 使用 [`pydantic`](https://pydantic-docs.helpmanual.io/) 以及 [`python-dotenv`](https://saurabh-kumar.com/python-dotenv/) 来读取配置。 配置项需符合特殊格式或 json 序列化格式 详情见 [`pydantic Field Type`](https://pydantic-docs.helpmanual.io/usage/types/) 文档。 ## _class_ `Env(_env_file=ENV_FILE_SENTINEL, _env_file_encoding=None, _env_nested_delimiter=None, **values)` {#Env} - **说明** 运行环境配置。大小写不敏感。 将会从 **环境变量** > **dotenv 配置文件** 的优先级读取环境信息。 - **参数** - `_env_file` (DOTENV_TYPE | None) - `_env_file_encoding` (str | None) - `_env_nested_delimiter` (str | None) - `**values` (Any) ### _class-var_ `environment` {#Env-environment} - **类型:** str - **说明** 当前环境名。 NoneBot 将从 `.env.{environment}` 文件中加载配置。 ## _class_ `Config(_env_file=ENV_FILE_SENTINEL, _env_file_encoding=None, _env_nested_delimiter=None, **values)` {#Config} - **说明** NoneBot 主要配置。大小写不敏感。 除了 NoneBot 的配置项外,还可以自行添加配置项到 `.env.{environment}` 文件中。 这些配置将会在 json 反序列化后一起带入 `Config` 类中。 配置方法参考: [配置](https://nonebot.dev/docs/appendices/config) - **参数** - `_env_file` (DOTENV_TYPE | None) - `_env_file_encoding` (str | None) - `_env_nested_delimiter` (str | None) - `**values` (Any) ### _class-var_ `driver` {#Config-driver} - **类型:** str - **说明** NoneBot 运行所使用的 `Driver` 。继承自 [Driver](drivers/index.md#Driver) 。 配置格式为 `[:][+[:]]*`。 `~` 为 `nonebot.drivers.` 的缩写。 配置方法参考: [配置驱动器](https://nonebot.dev/docs/advanced/driver#%E9%85%8D%E7%BD%AE%E9%A9%B1%E5%8A%A8%E5%99%A8) ### _class-var_ `host` {#Config-host} - **类型:** IPvAnyAddress - **说明:** NoneBot [ReverseDriver](drivers/index.md#ReverseDriver) 服务端监听的 IP/主机名。 ### _class-var_ `port` {#Config-port} - **类型:** int - **说明:** NoneBot [ReverseDriver](drivers/index.md#ReverseDriver) 服务端监听的端口。 ### _class-var_ `log_level` {#Config-log-level} - **类型:** int | str - **说明** NoneBot 日志输出等级,可以为 `int` 类型等级或等级名称。 参考 [记录日志](https://nonebot.dev/docs/appendices/log),[loguru 日志等级](https://loguru.readthedocs.io/en/stable/api/logger.html#levels)。 :::tip 提示 日志等级名称应为大写,如 `INFO`。 ::: - **用法** ```conf LOG_LEVEL=25 LOG_LEVEL=INFO ``` ### _class-var_ `api_timeout` {#Config-api-timeout} - **类型:** float | None - **说明:** API 请求超时时间,单位: 秒。 ### _class-var_ `superusers` {#Config-superusers} - **类型:** set[str] - **说明:** 机器人超级用户。 - **用法** ```conf SUPERUSERS=["12345789"] ``` ### _class-var_ `nickname` {#Config-nickname} - **类型:** set[str] - **说明:** 机器人昵称。 ### _class-var_ `command_start` {#Config-command-start} - **类型:** set[str] - **说明** 命令的起始标记,用于判断一条消息是不是命令。 参考[命令响应规则](https://nonebot.dev/docs/advanced/matcher#command)。 - **用法** ```conf COMMAND_START=["/", ""] ``` ### _class-var_ `command_sep` {#Config-command-sep} - **类型:** set[str] - **说明** 命令的分隔标记,用于将文本形式的命令切分为元组(实际的命令名)。 参考[命令响应规则](https://nonebot.dev/docs/advanced/matcher#command)。 - **用法** ```conf COMMAND_SEP=["."] ``` ### _class-var_ `session_expire_timeout` {#Config-session-expire-timeout} - **类型:** timedelta - **说明:** 等待用户回复的超时时间。 - **用法** ```conf SESSION_EXPIRE_TIMEOUT=[-][DD]D[,][HH:MM:]SS[.ffffff] SESSION_EXPIRE_TIMEOUT=[±]P[DD]DT[HH]H[MM]M[SS]S # ISO 8601 ``` ================================================ FILE: website/versioned_docs/version-2.4.4/api/consts.md ================================================ --- mdx: format: md sidebar_position: 9 description: nonebot.consts 模块 --- # nonebot.consts 本模块包含了 NoneBot 事件处理过程中使用到的常量。 ## _var_ `RECEIVE_KEY` {#RECEIVE-KEY} - **类型:** Literal['\_receive\_{id}'] - **说明:** `receive` 存储 key ## _var_ `LAST_RECEIVE_KEY` {#LAST-RECEIVE-KEY} - **类型:** Literal['\_last\_receive'] - **说明:** `last_receive` 存储 key ## _var_ `ARG_KEY` {#ARG-KEY} - **类型:** Literal['{key}'] - **说明:** `arg` 存储 key ## _var_ `REJECT_TARGET` {#REJECT-TARGET} - **类型:** Literal['\_current\_target'] - **说明:** 当前 `reject` 目标存储 key ## _var_ `REJECT_CACHE_TARGET` {#REJECT-CACHE-TARGET} - **类型:** Literal['\_next\_target'] - **说明:** 下一个 `reject` 目标存储 key ## _var_ `PAUSE_PROMPT_RESULT_KEY` {#PAUSE-PROMPT-RESULT-KEY} - **类型:** Literal['\_pause\_result'] - **说明:** `pause` prompt 发送结果存储 key ## _var_ `REJECT_PROMPT_RESULT_KEY` {#REJECT-PROMPT-RESULT-KEY} - **类型:** Literal['\_reject\_{key}\_result'] - **说明:** `reject` prompt 发送结果存储 key ## _var_ `PREFIX_KEY` {#PREFIX-KEY} - **类型:** Literal['\_prefix'] - **说明:** 命令前缀存储 key ## _var_ `CMD_KEY` {#CMD-KEY} - **类型:** Literal['command'] - **说明:** 命令元组存储 key ## _var_ `RAW_CMD_KEY` {#RAW-CMD-KEY} - **类型:** Literal['raw\_command'] - **说明:** 命令文本存储 key ## _var_ `CMD_ARG_KEY` {#CMD-ARG-KEY} - **类型:** Literal['command\_arg'] - **说明:** 命令参数存储 key ## _var_ `CMD_START_KEY` {#CMD-START-KEY} - **类型:** Literal['command\_start'] - **说明:** 命令开头存储 key ## _var_ `CMD_WHITESPACE_KEY` {#CMD-WHITESPACE-KEY} - **类型:** Literal['command\_whitespace'] - **说明:** 命令与参数间空白符存储 key ## _var_ `SHELL_ARGS` {#SHELL-ARGS} - **类型:** Literal['\_args'] - **说明:** shell 命令 parse 后参数字典存储 key ## _var_ `SHELL_ARGV` {#SHELL-ARGV} - **类型:** Literal['\_argv'] - **说明:** shell 命令原始参数列表存储 key ## _var_ `REGEX_MATCHED` {#REGEX-MATCHED} - **类型:** Literal['\_matched'] - **说明:** 正则匹配结果存储 key ## _var_ `STARTSWITH_KEY` {#STARTSWITH-KEY} - **类型:** Literal['\_startswith'] - **说明:** 响应触发前缀 key ## _var_ `ENDSWITH_KEY` {#ENDSWITH-KEY} - **类型:** Literal['\_endswith'] - **说明:** 响应触发后缀 key ## _var_ `FULLMATCH_KEY` {#FULLMATCH-KEY} - **类型:** Literal['\_fullmatch'] - **说明:** 响应触发完整消息 key ## _var_ `KEYWORD_KEY` {#KEYWORD-KEY} - **类型:** Literal['\_keyword'] - **说明:** 响应触发关键字 key ================================================ FILE: website/versioned_docs/version-2.4.4/api/dependencies/_category_.json ================================================ { "position": 13 } ================================================ FILE: website/versioned_docs/version-2.4.4/api/dependencies/index.md ================================================ --- mdx: format: md sidebar_position: 0 description: nonebot.dependencies 模块 --- # nonebot.dependencies 本模块模块实现了依赖注入的定义与处理。 ## _abstract class_ `Param(*args, validate=False, **kwargs)` {#Param} - **说明** 依赖注入的基本单元 —— 参数。 继承自 `pydantic.fields.FieldInfo`,用于描述参数信息(不包括参数名)。 - **参数** - `*args` - `validate` (bool) - `**kwargs` (Any) ## _class_ `Dependent()` {#Dependent} - **说明:** 依赖注入容器 - **参数** - `call`: 依赖注入的可调用对象,可以是任何 Callable 对象 - `pre_checkers`: 依赖注入解析前的参数检查 - `params`: 具名参数列表 - `parameterless`: 匿名参数列表 - `allow_types`: 允许的参数类型 ### _staticmethod_ `parse_params(call, allow_types)` {#Dependent-parse-params} - **参数** - `call` (\_DependentCallable[R]) - `allow_types` (tuple[type[Param], ...]) - **返回** - tuple[[ModelField](../compat.md#ModelField), ...] ### _staticmethod_ `parse_parameterless(parameterless, allow_types)` {#Dependent-parse-parameterless} - **参数** - `parameterless` (tuple[Any, ...]) - `allow_types` (tuple[type[Param], ...]) - **返回** - tuple[Param, ...] ### _classmethod_ `parse(*, call, parameterless=None, allow_types)` {#Dependent-parse} - **参数** - `call` (\_DependentCallable[R]) - `parameterless` (Iterable[Any] | None) - `allow_types` (Iterable[type[Param]]) - **返回** - Dependent[R] ### _async method_ `check(**params)` {#Dependent-check} - **参数** - `**params` (Any) - **返回** - None ### _async method_ `solve(**params)` {#Dependent-solve} - **参数** - `**params` (Any) - **返回** - dict[str, Any] ================================================ FILE: website/versioned_docs/version-2.4.4/api/dependencies/utils.md ================================================ --- mdx: format: md sidebar_position: 1 description: nonebot.dependencies.utils 模块 --- # nonebot.dependencies.utils ## _def_ `get_typed_signature(call)` {#get-typed-signature} - **说明:** 获取可调用对象签名 - **参数** - `call` ((...) -> Any) - **返回** - inspect.Signature ## _def_ `get_typed_annotation(param, globalns)` {#get-typed-annotation} - **说明:** 获取参数的类型注解 - **参数** - `param` (inspect.Parameter) - `globalns` (dict[str, Any]) - **返回** - Any ## _def_ `check_field_type(field, value)` {#check-field-type} - **说明:** 检查字段类型是否匹配 - **参数** - `field` ([ModelField](../compat.md#ModelField)) - `value` (Any) - **返回** - Any ================================================ FILE: website/versioned_docs/version-2.4.4/api/drivers/_category_.json ================================================ { "position": 14 } ================================================ FILE: website/versioned_docs/version-2.4.4/api/drivers/aiohttp.md ================================================ --- mdx: format: md sidebar_position: 2 description: nonebot.drivers.aiohttp 模块 --- # nonebot.drivers.aiohttp [AIOHTTP](https://aiohttp.readthedocs.io/en/stable/) 驱动适配器。 ```bash nb driver install aiohttp # 或者 pip install nonebot2[aiohttp] ``` :::tip 提示 本驱动仅支持客户端连接 ::: ## _class_ `Session(params=None, headers=None, cookies=None, version=HTTPVersion.H11, timeout=None, proxy=None)` {#Session} - **参数** - `params` (QueryTypes) - `headers` (HeaderTypes) - `cookies` (CookieTypes) - `version` (str | [HTTPVersion](index.md#HTTPVersion)) - `timeout` (TimeoutTypes) - `proxy` (str | None) ### _async method_ `request(setup)` {#Session-request} - **参数** - `setup` ([Request](index.md#Request)) - **返回** - [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} - **参数** empty - **返回** - None ### _async method_ `close()` {#Session-close} - **参数** empty - **返回** - None ## _class_ `Mixin()` {#Mixin} - **说明:** AIOHTTP Mixin - **参数** auto ### _async method_ `request(setup)` {#Mixin-request} - **参数** - `setup` ([Request](index.md#Request)) - **返回** - [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} - **参数** - `setup` ([Request](index.md#Request)) - **返回** - 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} - **参数** - `params` (QueryTypes) - `headers` (HeaderTypes) - `cookies` (CookieTypes) - `version` (str | [HTTPVersion](index.md#HTTPVersion)) - `timeout` (TimeoutTypes) - `proxy` (str | None) - **返回** - Session ## _class_ `WebSocket(*, request, session, websocket)` {#WebSocket} - **说明:** AIOHTTP Websocket Wrapper - **参数** - `request` ([Request](index.md#Request)) - `session` (aiohttp.ClientSession) - `websocket` (aiohttp.ClientWebSocketResponse) ### _async method_ `accept()` {#WebSocket-accept} - **参数** empty - **返回** - untyped ### _async method_ `close(code=1000, reason="")` {#WebSocket-close} - **参数** - `code` (int) - `reason` (str) - **返回** - untyped ### _async method_ `receive()` {#WebSocket-receive} - **参数** empty - **返回** - str ### _async method_ `receive_text()` {#WebSocket-receive-text} - **参数** empty - **返回** - str ### _async method_ `receive_bytes()` {#WebSocket-receive-bytes} - **参数** empty - **返回** - bytes ### _async method_ `send_text(data)` {#WebSocket-send-text} - **参数** - `data` (str) - **返回** - None ### _async method_ `send_bytes(data)` {#WebSocket-send-bytes} - **参数** - `data` (bytes) - **返回** - None ## _class_ `Driver(env, config)` {#Driver} - **参数** - `env` ([Env](../config.md#Env)) - `config` ([Config](../config.md#Config)) ================================================ FILE: website/versioned_docs/version-2.4.4/api/drivers/fastapi.md ================================================ --- mdx: format: md sidebar_position: 1 description: nonebot.drivers.fastapi 模块 --- # nonebot.drivers.fastapi [FastAPI](https://fastapi.tiangolo.com/) 驱动适配 ```bash nb driver install fastapi # 或者 pip install nonebot2[fastapi] ``` :::tip 提示 本驱动仅支持服务端连接 ::: ## _class_ `Config()` {#Config} - **说明:** FastAPI 驱动框架设置,详情参考 FastAPI 文档 - **参数** auto ### _class-var_ `fastapi_openapi_url` {#Config-fastapi-openapi-url} - **类型:** str | None - **说明:** `openapi.json` 地址,默认为 `None` 即关闭 ### _class-var_ `fastapi_docs_url` {#Config-fastapi-docs-url} - **类型:** str | None - **说明:** `swagger` 地址,默认为 `None` 即关闭 ### _class-var_ `fastapi_redoc_url` {#Config-fastapi-redoc-url} - **类型:** str | None - **说明:** `redoc` 地址,默认为 `None` 即关闭 ### _class-var_ `fastapi_include_adapter_schema` {#Config-fastapi-include-adapter-schema} - **类型:** bool - **说明:** 是否包含适配器路由的 schema,默认为 `True` ### _class-var_ `fastapi_reload` {#Config-fastapi-reload} - **类型:** bool - **说明:** 开启/关闭冷重载 ### _class-var_ `fastapi_reload_dirs` {#Config-fastapi-reload-dirs} - **类型:** list[str] | None - **说明:** 重载监控文件夹列表,默认为 uvicorn 默认值 ### _class-var_ `fastapi_reload_delay` {#Config-fastapi-reload-delay} - **类型:** float - **说明:** 重载延迟,默认为 uvicorn 默认值 ### _class-var_ `fastapi_reload_includes` {#Config-fastapi-reload-includes} - **类型:** list[str] | None - **说明:** 要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值 ### _class-var_ `fastapi_reload_excludes` {#Config-fastapi-reload-excludes} - **类型:** list[str] | None - **说明:** 不要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值 ### _class-var_ `fastapi_extra` {#Config-fastapi-extra} - **类型:** dict[str, Any] - **说明:** 传递给 `FastAPI` 的其他参数。 ## _class_ `Driver(env, config)` {#Driver} - **说明:** FastAPI 驱动框架。 - **参数** - `env` ([Env](../config.md#Env)) - `config` (NoneBotConfig) ### _property_ `type` {#Driver-type} - **类型:** str - **说明:** 驱动名称: `fastapi` ### _property_ `server_app` {#Driver-server-app} - **类型:** FastAPI - **说明:** `FastAPI APP` 对象 ### _property_ `asgi` {#Driver-asgi} - **类型:** FastAPI - **说明:** `FastAPI APP` 对象 ### _property_ `logger` {#Driver-logger} - **类型:** logging.Logger - **说明:** fastapi 使用的 logger ### _method_ `setup_http_server(setup)` {#Driver-setup-http-server} - **参数** - `setup` ([HTTPServerSetup](index.md#HTTPServerSetup)) - **返回** - untyped ### _method_ `setup_websocket_server(setup)` {#Driver-setup-websocket-server} - **参数** - `setup` ([WebSocketServerSetup](index.md#WebSocketServerSetup)) - **返回** - None ### _method_ `run(host=None, port=None, *args, app=None, **kwargs)` {#Driver-run} - **说明:** 使用 `uvicorn` 启动 FastAPI - **参数** - `host` (str | None) - `port` (int | None) - `*args` - `app` (str | None) - `**kwargs` - **返回** - untyped ## _class_ `FastAPIWebSocket(*, request, websocket)` {#FastAPIWebSocket} - **说明:** FastAPI WebSocket Wrapper - **参数** - `request` (BaseRequest) - `websocket` ([WebSocket](index.md#WebSocket)) ### _async method_ `accept()` {#FastAPIWebSocket-accept} - **参数** empty - **返回** - None ### _async method_ `close(code=status.WS_1000_NORMAL_CLOSURE, reason="")` {#FastAPIWebSocket-close} - **参数** - `code` (int) - `reason` (str) - **返回** - None ### _async method_ `receive()` {#FastAPIWebSocket-receive} - **参数** empty - **返回** - str | bytes ### _async method_ `receive_text()` {#FastAPIWebSocket-receive-text} - **参数** empty - **返回** - str ### _async method_ `receive_bytes()` {#FastAPIWebSocket-receive-bytes} - **参数** empty - **返回** - bytes ### _async method_ `send_text(data)` {#FastAPIWebSocket-send-text} - **参数** - `data` (str) - **返回** - None ### _async method_ `send_bytes(data)` {#FastAPIWebSocket-send-bytes} - **参数** - `data` (bytes) - **返回** - None ================================================ FILE: website/versioned_docs/version-2.4.4/api/drivers/httpx.md ================================================ --- mdx: format: md sidebar_position: 3 description: nonebot.drivers.httpx 模块 --- # nonebot.drivers.httpx [HTTPX](https://www.python-httpx.org/) 驱动适配 ```bash nb driver install httpx # 或者 pip install nonebot2[httpx] ``` :::tip 提示 本驱动仅支持客户端 HTTP 连接 ::: ## _class_ `Session(params=None, headers=None, cookies=None, version=HTTPVersion.H11, timeout=None, proxy=None)` {#Session} - **参数** - `params` (QueryTypes) - `headers` (HeaderTypes) - `cookies` (CookieTypes) - `version` (str | [HTTPVersion](index.md#HTTPVersion)) - `timeout` (TimeoutTypes) - `proxy` (str | None) ### _async method_ `request(setup)` {#Session-request} - **参数** - `setup` ([Request](index.md#Request)) - **返回** - [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} - **参数** empty - **返回** - None ### _async method_ `close()` {#Session-close} - **参数** empty - **返回** - None ## _class_ `Mixin()` {#Mixin} - **说明:** HTTPX Mixin - **参数** auto ### _async method_ `request(setup)` {#Mixin-request} - **参数** - `setup` ([Request](index.md#Request)) - **返回** - [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} - **参数** - `params` (QueryTypes) - `headers` (HeaderTypes) - `cookies` (CookieTypes) - `version` (str | [HTTPVersion](index.md#HTTPVersion)) - `timeout` (TimeoutTypes) - `proxy` (str | None) - **返回** - Session ## _class_ `Driver(env, config)` {#Driver} - **参数** - `env` ([Env](../config.md#Env)) - `config` ([Config](../config.md#Config)) ================================================ FILE: website/versioned_docs/version-2.4.4/api/drivers/index.md ================================================ --- mdx: format: md sidebar_position: 0 description: nonebot.drivers 模块 --- # nonebot.drivers 本模块定义了驱动适配器基类。 各驱动请继承以下基类。 ## _abstract class_ `ASGIMixin()` {#ASGIMixin} - **说明** ASGI 服务端基类。 将后端框架封装,以满足适配器使用。 - **参数** auto ### _abstract property_ `server_app` {#ASGIMixin-server-app} - **类型:** Any - **说明:** 驱动 APP 对象 ### _abstract property_ `asgi` {#ASGIMixin-asgi} - **类型:** Any - **说明:** 驱动 ASGI 对象 ### _abstract method_ `setup_http_server(setup)` {#ASGIMixin-setup-http-server} - **说明:** 设置一个 HTTP 服务器路由配置 - **参数** - `setup` ([HTTPServerSetup](#HTTPServerSetup)) - **返回** - None ### _abstract method_ `setup_websocket_server(setup)` {#ASGIMixin-setup-websocket-server} - **说明:** 设置一个 WebSocket 服务器路由配置 - **参数** - `setup` ([WebSocketServerSetup](#WebSocketServerSetup)) - **返回** - None ## _class_ `Cookies(cookies=None)` {#Cookies} - **参数** - `cookies` (CookieTypes) ### _method_ `set(name, value, domain="", path="/")` {#Cookies-set} - **参数** - `name` (str) - `value` (str) - `domain` (str) - `path` (str) - **返回** - None ### _method_ `get(name, default=None, domain=None, path=None)` {#Cookies-get} - **参数** - `name` (str) - `default` (str | None) - `domain` (str | None) - `path` (str | None) - **返回** - str | None ### _method_ `delete(name, domain=None, path=None)` {#Cookies-delete} - **参数** - `name` (str) - `domain` (str | None) - `path` (str | None) - **返回** - None ### _method_ `clear(domain=None, path=None)` {#Cookies-clear} - **参数** - `domain` (str | None) - `path` (str | None) - **返回** - None ### _method_ `update(cookies=None)` {#Cookies-update} - **参数** - `cookies` (CookieTypes) - **返回** - None ### _method_ `as_header(request)` {#Cookies-as-header} - **参数** - `request` (Request) - **返回** - dict[str, str] ## _abstract class_ `Driver(env, config)` {#Driver} - **说明** 驱动器基类。 驱动器控制框架的启动和停止,适配器的注册,以及机器人生命周期管理。 - **参数** - `env` ([Env](../config.md#Env)): 包含环境信息的 Env 对象 - `config` ([Config](../config.md#Config)): 包含配置信息的 Config 对象 ### _instance-var_ `env` {#Driver-env} - **类型:** str - **说明:** 环境名称 ### _instance-var_ `config` {#Driver-config} - **类型:** [Config](../config.md#Config) - **说明:** 全局配置对象 ### _property_ `bots` {#Driver-bots} - **类型:** dict[str, [Bot](../adapters/index.md#Bot)] - **说明:** 获取当前所有已连接的 Bot ### _method_ `register_adapter(adapter, **kwargs)` {#Driver-register-adapter} - **说明:** 注册一个协议适配器 - **参数** - `adapter` (type[[Adapter](../adapters/index.md#Adapter)]): 适配器类 - `**kwargs`: 其他传递给适配器的参数 - **返回** - None ### _abstract property_ `type` {#Driver-type} - **类型:** str - **说明:** 驱动类型名称 ### _abstract property_ `logger` {#Driver-logger} - **类型:** untyped - **说明:** 驱动专属 logger 日志记录器 ### _abstract method_ `run(*args, **kwargs)` {#Driver-run} - **说明:** 启动驱动框架 - **参数** - `*args` - `**kwargs` - **返回** - untyped ### _method_ `on_startup(func)` {#Driver-on-startup} - **说明:** 注册一个启动时执行的函数 - **参数** - `func` (LIFESPAN_FUNC) - **返回** - LIFESPAN_FUNC ### _method_ `on_shutdown(func)` {#Driver-on-shutdown} - **说明:** 注册一个停止时执行的函数 - **参数** - `func` (LIFESPAN_FUNC) - **返回** - LIFESPAN_FUNC ### _classmethod_ `on_bot_connect(func)` {#Driver-on-bot-connect} - **说明** 装饰一个函数使他在 bot 连接成功时执行。 钩子函数参数: - bot: 当前连接上的 Bot 对象 - **参数** - `func` ([T_BotConnectionHook](../typing.md#T-BotConnectionHook)) - **返回** - [T_BotConnectionHook](../typing.md#T-BotConnectionHook) ### _classmethod_ `on_bot_disconnect(func)` {#Driver-on-bot-disconnect} - **说明** 装饰一个函数使他在 bot 连接断开时执行。 钩子函数参数: - bot: 当前连接上的 Bot 对象 - **参数** - `func` ([T_BotDisconnectionHook](../typing.md#T-BotDisconnectionHook)) - **返回** - [T_BotDisconnectionHook](../typing.md#T-BotDisconnectionHook) ## _var_ `ForwardDriver` {#ForwardDriver} - **类型:** ForwardMixin - **说明** 支持客户端请求的驱动器。 **Deprecated**,请使用 [ForwardMixin](#ForwardMixin) 或其子类代替。 ## _abstract class_ `ForwardMixin()` {#ForwardMixin} - **说明:** 客户端混入基类。 - **参数** auto ## _abstract class_ `HTTPClientMixin()` {#HTTPClientMixin} - **说明:** HTTP 客户端混入基类。 - **参数** auto ### _abstract async method_ `request(setup)` {#HTTPClientMixin-request} - **说明:** 发送一个 HTTP 请求 - **参数** - `setup` ([Request](#Request)) - **返回** - [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 会话 - **参数** - `params` (QueryTypes) - `headers` (HeaderTypes) - `cookies` (CookieTypes) - `version` (str | [HTTPVersion](#HTTPVersion)) - `timeout` (TimeoutTypes) - `proxy` (str | None) - **返回** - HTTPClientSession ## _class_ `HTTPServerSetup()` {#HTTPServerSetup} - **说明:** HTTP 服务器路由配置。 - **参数** auto ## _enum_ `HTTPVersion` {#HTTPVersion} - **参数** auto - `H10: '1.0'` - `H11: '1.1'` - `H2: '2'` ## _abstract class_ `Mixin()` {#Mixin} - **说明:** 可与其他驱动器共用的混入基类。 - **参数** auto ### _abstract property_ `type` {#Mixin-type} - **类型:** str - **说明:** 混入驱动类型名称 ## _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} - **参数** - `method` (str | bytes) - `url` (URL | str | RawURL) - `params` (QueryTypes) - `headers` (HeaderTypes) - `cookies` (CookieTypes) - `content` (ContentTypes) - `data` (DataTypes) - `json` (Any) - `files` (FilesTypes) - `version` (str | HTTPVersion) - `timeout` (TimeoutTypes) - `proxy` (str | None) ## _class_ `Response(status_code, *, headers=None, content=None, request=None)` {#Response} - **参数** - `status_code` (int) - `headers` (HeaderTypes) - `content` (ContentTypes) - `request` (Request | None) ## _var_ `ReverseDriver` {#ReverseDriver} - **类型:** ReverseMixin - **说明** 支持服务端请求的驱动器。 **Deprecated**,请使用 [ReverseMixin](#ReverseMixin) 或其子类代替。 ## _abstract class_ `ReverseMixin()` {#ReverseMixin} - **说明:** 服务端混入基类。 - **参数** auto ## _class_ `Timeout()` {#Timeout} - **说明:** Request 超时配置。 - **参数** auto ## _abstract class_ `WebSocket(*, request)` {#WebSocket} - **参数** - `request` (Request) ### _abstract property_ `closed` {#WebSocket-closed} - **类型:** bool - **说明:** 连接是否已经关闭 ### _abstract async method_ `accept()` {#WebSocket-accept} - **说明:** 接受 WebSocket 连接请求 - **参数** empty - **返回** - None ### _abstract async method_ `close(code=1000, reason="")` {#WebSocket-close} - **说明:** 关闭 WebSocket 连接请求 - **参数** - `code` (int) - `reason` (str) - **返回** - None ### _abstract async method_ `receive()` {#WebSocket-receive} - **说明:** 接收一条 WebSocket text/bytes 信息 - **参数** empty - **返回** - str | bytes ### _abstract async method_ `receive_text()` {#WebSocket-receive-text} - **说明:** 接收一条 WebSocket text 信息 - **参数** empty - **返回** - str ### _abstract async method_ `receive_bytes()` {#WebSocket-receive-bytes} - **说明:** 接收一条 WebSocket binary 信息 - **参数** empty - **返回** - bytes ### _async method_ `send(data)` {#WebSocket-send} - **说明:** 发送一条 WebSocket text/bytes 信息 - **参数** - `data` (str | bytes) - **返回** - None ### _abstract async method_ `send_text(data)` {#WebSocket-send-text} - **说明:** 发送一条 WebSocket text 信息 - **参数** - `data` (str) - **返回** - None ### _abstract async method_ `send_bytes(data)` {#WebSocket-send-bytes} - **说明:** 发送一条 WebSocket binary 信息 - **参数** - `data` (bytes) - **返回** - None ## _abstract class_ `WebSocketClientMixin()` {#WebSocketClientMixin} - **说明:** WebSocket 客户端混入基类。 - **参数** auto ### _abstract method_ `websocket(setup)` {#WebSocketClientMixin-websocket} - **说明:** 发起一个 WebSocket 连接 - **参数** - `setup` ([Request](#Request)) - **返回** - AsyncGenerator[[WebSocket](#WebSocket), None] ## _class_ `WebSocketServerSetup()` {#WebSocketServerSetup} - **说明:** WebSocket 服务器路由配置。 - **参数** auto ## _def_ `combine_driver(driver, *mixins)` {#combine-driver} - **说明:** 将一个驱动器和多个混入类合并。 - **重载** **1.** `(driver) -> type[D]` - **参数** - `driver` (type[D]) - **返回** - type[D] **2.** `(driver, __m, /, *mixins) -> type[CombinedDriver]` - **参数** - `driver` (type[D]) - `__m` (type[[Mixin](#Mixin)]) - `*mixins` (type[[Mixin](#Mixin)]) - **返回** - type[CombinedDriver] ================================================ FILE: website/versioned_docs/version-2.4.4/api/drivers/none.md ================================================ --- mdx: format: md sidebar_position: 6 description: nonebot.drivers.none 模块 --- # nonebot.drivers.none None 驱动适配 :::tip 提示 本驱动不支持任何服务器或客户端连接 ::: ## _class_ `Driver(env, config)` {#Driver} - **说明:** None 驱动框架 - **参数** - `env` ([Env](../config.md#Env)) - `config` ([Config](../config.md#Config)) ### _property_ `type` {#Driver-type} - **类型:** str - **说明:** 驱动名称: `none` ### _property_ `logger` {#Driver-logger} - **类型:** untyped - **说明:** none driver 使用的 logger ### _method_ `run(*args, **kwargs)` {#Driver-run} - **说明:** 启动 none driver - **参数** - `*args` - `**kwargs` - **返回** - untyped ### _method_ `exit(force=False)` {#Driver-exit} - **说明:** 退出 none driver - **参数** - `force` (bool): 强制退出 - **返回** - untyped ================================================ FILE: website/versioned_docs/version-2.4.4/api/drivers/quart.md ================================================ --- mdx: format: md sidebar_position: 5 description: nonebot.drivers.quart 模块 --- # nonebot.drivers.quart [Quart](https://pgjones.gitlab.io/quart/index.html) 驱动适配 ```bash nb driver install quart # 或者 pip install nonebot2[quart] ``` :::tip 提示 本驱动仅支持服务端连接 ::: ## _class_ `Config()` {#Config} - **说明:** Quart 驱动框架设置 - **参数** auto ### _class-var_ `quart_reload` {#Config-quart-reload} - **类型:** bool - **说明:** 开启/关闭冷重载 ### _class-var_ `quart_reload_dirs` {#Config-quart-reload-dirs} - **类型:** list[str] | None - **说明:** 重载监控文件夹列表,默认为 uvicorn 默认值 ### _class-var_ `quart_reload_delay` {#Config-quart-reload-delay} - **类型:** float - **说明:** 重载延迟,默认为 uvicorn 默认值 ### _class-var_ `quart_reload_includes` {#Config-quart-reload-includes} - **类型:** list[str] | None - **说明:** 要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值 ### _class-var_ `quart_reload_excludes` {#Config-quart-reload-excludes} - **类型:** list[str] | None - **说明:** 不要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值 ### _class-var_ `quart_extra` {#Config-quart-extra} - **类型:** dict[str, Any] - **说明:** 传递给 `Quart` 的其他参数。 ## _class_ `Driver(env, config)` {#Driver} - **说明:** Quart 驱动框架 - **参数** - `env` ([Env](../config.md#Env)) - `config` (NoneBotConfig) ### _property_ `type` {#Driver-type} - **类型:** str - **说明:** 驱动名称: `quart` ### _property_ `server_app` {#Driver-server-app} - **类型:** Quart - **说明:** `Quart` 对象 ### _property_ `asgi` {#Driver-asgi} - **类型:** untyped - **说明:** `Quart` 对象 ### _property_ `logger` {#Driver-logger} - **类型:** untyped - **说明:** Quart 使用的 logger ### _method_ `setup_http_server(setup)` {#Driver-setup-http-server} - **参数** - `setup` ([HTTPServerSetup](index.md#HTTPServerSetup)) - **返回** - untyped ### _method_ `setup_websocket_server(setup)` {#Driver-setup-websocket-server} - **参数** - `setup` ([WebSocketServerSetup](index.md#WebSocketServerSetup)) - **返回** - None ### _method_ `run(host=None, port=None, *args, app=None, **kwargs)` {#Driver-run} - **说明:** 使用 `uvicorn` 启动 Quart - **参数** - `host` (str | None) - `port` (int | None) - `*args` - `app` (str | None) - `**kwargs` - **返回** - untyped ## _class_ `WebSocket(*, request, websocket_ctx)` {#WebSocket} - **说明:** Quart WebSocket Wrapper - **参数** - `request` (BaseRequest) - `websocket_ctx` (WebsocketContext) ### _async method_ `accept()` {#WebSocket-accept} - **参数** empty - **返回** - untyped ### _async method_ `close(code=1000, reason="")` {#WebSocket-close} - **参数** - `code` (int) - `reason` (str) - **返回** - untyped ### _async method_ `receive()` {#WebSocket-receive} - **参数** empty - **返回** - str | bytes ### _async method_ `receive_text()` {#WebSocket-receive-text} - **参数** empty - **返回** - str ### _async method_ `receive_bytes()` {#WebSocket-receive-bytes} - **参数** empty - **返回** - bytes ### _async method_ `send_text(data)` {#WebSocket-send-text} - **参数** - `data` (str) - **返回** - untyped ### _async method_ `send_bytes(data)` {#WebSocket-send-bytes} - **参数** - `data` (bytes) - **返回** - untyped ================================================ FILE: website/versioned_docs/version-2.4.4/api/drivers/websockets.md ================================================ --- mdx: format: md sidebar_position: 4 description: nonebot.drivers.websockets 模块 --- # nonebot.drivers.websockets [websockets](https://websockets.readthedocs.io/) 驱动适配 ```bash nb driver install websockets # 或者 pip install nonebot2[websockets] ``` :::tip 提示 本驱动仅支持客户端 WebSocket 连接 ::: ## _def_ `catch_closed(func)` {#catch-closed} - **参数** - `func` ((P) -> CoroutineType[Any, Any, T]) - **返回** - (P) -> CoroutineType[Any, Any, T] ## _class_ `Mixin()` {#Mixin} - **说明:** Websockets Mixin - **参数** auto ### _method_ `websocket(setup)` {#Mixin-websocket} - **参数** - `setup` ([Request](index.md#Request)) - **返回** - AsyncGenerator[[WebSocket](index.md#WebSocket), None] ## _class_ `WebSocket(*, request, websocket)` {#WebSocket} - **说明:** Websockets WebSocket Wrapper - **参数** - `request` ([Request](index.md#Request)) - `websocket` (ClientConnection) ### _async method_ `accept()` {#WebSocket-accept} - **参数** empty - **返回** - untyped ### _async method_ `close(code=1000, reason="")` {#WebSocket-close} - **参数** - `code` (int) - `reason` (str) - **返回** - untyped ### _async method_ `receive()` {#WebSocket-receive} - **参数** empty - **返回** - str | bytes ### _async method_ `receive_text()` {#WebSocket-receive-text} - **参数** empty - **返回** - str ### _async method_ `receive_bytes()` {#WebSocket-receive-bytes} - **参数** empty - **返回** - bytes ### _async method_ `send_text(data)` {#WebSocket-send-text} - **参数** - `data` (str) - **返回** - None ### _async method_ `send_bytes(data)` {#WebSocket-send-bytes} - **参数** - `data` (bytes) - **返回** - None ## _class_ `Driver(env, config)` {#Driver} - **参数** - `env` ([Env](../config.md#Env)) - `config` ([Config](../config.md#Config)) ================================================ FILE: website/versioned_docs/version-2.4.4/api/exception.md ================================================ --- mdx: format: md sidebar_position: 10 description: nonebot.exception 模块 --- # nonebot.exception 本模块包含了所有 NoneBot 运行时可能会抛出的异常。 这些异常并非所有需要用户处理,在 NoneBot 内部运行时被捕获,并进行对应操作。 ```bash NoneBotException ├── ParserExit ├── ProcessException | ├── IgnoredException | ├── SkippedException | | └── TypeMisMatch | ├── MockApiException | └── StopPropagation ├── MatcherException | ├── PausedException | ├── RejectedException | └── FinishedException ├── AdapterException | ├── NoLogException | ├── ApiNotAvailable | ├── NetworkError | └── ActionFailed └── DriverException └── WebSocketClosed ``` ## _class_ `NoneBotException()` {#NoneBotException} - **说明:** 所有 NoneBot 发生的异常基类。 - **参数** auto ## _class_ `ParserExit()` {#ParserExit} - **说明:** 处理消息失败时返回的异常。 - **参数** auto ## _class_ `ProcessException()` {#ProcessException} - **说明:** 事件处理过程中发生的异常基类。 - **参数** auto ## _class_ `IgnoredException()` {#IgnoredException} - **说明:** 指示 NoneBot 应该忽略该事件。可由 PreProcessor 抛出。 - **参数** - `reason`: 忽略事件的原因 ## _class_ `SkippedException()` {#SkippedException} - **说明** 指示 NoneBot 立即结束当前 `Dependent` 的运行。 例如,可以在 `Handler` 中通过 [Matcher.skip](matcher.md#Matcher-skip) 抛出。 - **参数** auto - **用法** ```python def always_skip(): Matcher.skip() @matcher.handle() async def handler(dependency = Depends(always_skip)): # never run ``` ## _class_ `TypeMisMatch()` {#TypeMisMatch} - **说明:** 当前 `Handler` 的参数类型不匹配。 - **参数** auto ## _class_ `MockApiException()` {#MockApiException} - **说明:** 指示 NoneBot 阻止本次 API 调用或修改本次调用返回值,并返回自定义内容。 可由 api hook 抛出。 - **参数** - `result`: 返回的内容 ## _class_ `StopPropagation()` {#StopPropagation} - **说明** 指示 NoneBot 终止事件向下层传播。 在 [Matcher.block](matcher.md#Matcher-block) 为 `True` 或使用 [Matcher.stop_propagation](matcher.md#Matcher-stop-propagation) 方法时抛出。 - **参数** auto - **用法** ```python matcher = on_notice(block=True) # 或者 @matcher.handle() async def handler(matcher: Matcher): matcher.stop_propagation() ``` ## _class_ `MatcherException()` {#MatcherException} - **说明:** 所有 Matcher 发生的异常基类。 - **参数** auto ## _class_ `PausedException()` {#PausedException} - **说明** 指示 NoneBot 结束当前 `Handler` 并等待下一条消息后继续下一个 `Handler`。 可用于用户输入新信息。 可以在 `Handler` 中通过 [Matcher.pause](matcher.md#Matcher-pause) 抛出。 - **参数** auto - **用法** ```python @matcher.handle() async def handler(): await matcher.pause("some message") ``` ## _class_ `RejectedException()` {#RejectedException} - **说明** 指示 NoneBot 结束当前 `Handler` 并等待下一条消息后重新运行当前 `Handler`。 可用于用户重新输入。 可以在 `Handler` 中通过 [Matcher.reject](matcher.md#Matcher-reject) 抛出。 - **参数** auto - **用法** ```python @matcher.handle() async def handler(): await matcher.reject("some message") ``` ## _class_ `FinishedException()` {#FinishedException} - **说明** 指示 NoneBot 结束当前 `Handler` 且后续 `Handler` 不再被运行。可用于结束用户会话。 可以在 `Handler` 中通过 [Matcher.finish](matcher.md#Matcher-finish) 抛出。 - **参数** auto - **用法** ```python @matcher.handle() async def handler(): await matcher.finish("some message") ``` ## _class_ `AdapterException()` {#AdapterException} - **说明:** 代表 `Adapter` 抛出的异常,所有的 `Adapter` 都要在内部继承自这个 `Exception`。 - **参数** - `adapter_name`: 标识 adapter ## _class_ `NoLogException()` {#NoLogException} - **说明** 指示 NoneBot 对当前 `Event` 进行处理但不显示 Log 信息。 可在 [Event.get_log_string](adapters/index.md#Event-get-log-string) 时抛出 - **参数** auto ## _class_ `ApiNotAvailable()` {#ApiNotAvailable} - **说明:** 在 API 连接不可用时抛出。 - **参数** auto ## _class_ `NetworkError()` {#NetworkError} - **说明:** 在网络出现问题时抛出, 如: API 请求地址不正确, API 请求无返回或返回状态非正常等。 - **参数** auto ## _class_ `ActionFailed()` {#ActionFailed} - **说明:** API 请求成功返回数据,但 API 操作失败。 - **参数** auto ## _class_ `DriverException()` {#DriverException} - **说明:** `Driver` 抛出的异常基类。 - **参数** auto ## _class_ `WebSocketClosed()` {#WebSocketClosed} - **说明:** WebSocket 连接已关闭。 - **参数** auto ================================================ FILE: website/versioned_docs/version-2.4.4/api/index.md ================================================ --- mdx: format: md sidebar_position: 0 description: nonebot 模块 --- # nonebot 本模块主要定义了 NoneBot 启动所需函数,供 bot 入口文件调用。 ## 快捷导入 为方便使用,本模块从子模块导入了部分内容,以下内容可以直接通过本模块导入: - `on` => [`on`](plugin/on.md#on) - `on_metaevent` => [`on_metaevent`](plugin/on.md#on-metaevent) - `on_message` => [`on_message`](plugin/on.md#on-message) - `on_notice` => [`on_notice`](plugin/on.md#on-notice) - `on_request` => [`on_request`](plugin/on.md#on-request) - `on_startswith` => [`on_startswith`](plugin/on.md#on-startswith) - `on_endswith` => [`on_endswith`](plugin/on.md#on-endswith) - `on_fullmatch` => [`on_fullmatch`](plugin/on.md#on-fullmatch) - `on_keyword` => [`on_keyword`](plugin/on.md#on-keyword) - `on_command` => [`on_command`](plugin/on.md#on-command) - `on_shell_command` => [`on_shell_command`](plugin/on.md#on-shell-command) - `on_regex` => [`on_regex`](plugin/on.md#on-regex) - `on_type` => [`on_type`](plugin/on.md#on-type) - `CommandGroup` => [`CommandGroup`](plugin/on.md#CommandGroup) - `Matchergroup` => [`MatcherGroup`](plugin/on.md#MatcherGroup) - `load_plugin` => [`load_plugin`](plugin/load.md#load-plugin) - `load_plugins` => [`load_plugins`](plugin/load.md#load-plugins) - `load_all_plugins` => [`load_all_plugins`](plugin/load.md#load-all-plugins) - `load_from_json` => [`load_from_json`](plugin/load.md#load-from-json) - `load_from_toml` => [`load_from_toml`](plugin/load.md#load-from-toml) - `load_builtin_plugin` => [`load_builtin_plugin`](plugin/load.md#load-builtin-plugin) - `load_builtin_plugins` => [`load_builtin_plugins`](plugin/load.md#load-builtin-plugins) - `get_plugin` => [`get_plugin`](plugin/index.md#get-plugin) - `get_plugin_by_module_name` => [`get_plugin_by_module_name`](plugin/index.md#get-plugin-by-module-name) - `get_loaded_plugins` => [`get_loaded_plugins`](plugin/index.md#get-loaded-plugins) - `get_available_plugin_names` => [`get_available_plugin_names`](plugin/index.md#get-available-plugin-names) - `get_plugin_config` => [`get_plugin_config`](plugin/index.md#get-plugin-config) - `require` => [`require`](plugin/load.md#require) ## _def_ `get_driver()` {#get-driver} - **说明** 获取全局 [Driver](drivers/index.md#Driver) 实例。 可用于在计划任务的回调等情形中获取当前 [Driver](drivers/index.md#Driver) 实例。 - **参数** empty - **返回** - [Driver](drivers/index.md#Driver): 全局 [Driver](drivers/index.md#Driver) 对象 - **异常** - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用) - **用法** ```python driver = nonebot.get_driver() ``` ## _def_ `get_adapter(name)` {#get-adapter} - **说明:** 获取已注册的 [Adapter](adapters/index.md#Adapter) 实例。 - **重载** **1.** `(name) -> Adapter` - **参数** - `name` (str): 适配器名称 - **返回** - [Adapter](adapters/index.md#Adapter): 指定名称的 [Adapter](adapters/index.md#Adapter) 对象 **2.** `(name) -> A` - **参数** - `name` (type[A]): 适配器类型 - **返回** - A: 指定类型的 [Adapter](adapters/index.md#Adapter) 对象 - **异常** - ValueError: 指定的 [Adapter](adapters/index.md#Adapter) 未注册 - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用) - **用法** ```python from nonebot.adapters.console import Adapter adapter = nonebot.get_adapter(Adapter) ``` ## _def_ `get_adapters()` {#get-adapters} - **说明:** 获取所有已注册的 [Adapter](adapters/index.md#Adapter) 实例。 - **参数** empty - **返回** - dict[str, [Adapter](adapters/index.md#Adapter)]: 所有 [Adapter](adapters/index.md#Adapter) 实例字典 - **异常** - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用) - **用法** ```python adapters = nonebot.get_adapters() ``` ## _def_ `get_app()` {#get-app} - **说明:** 获取全局 [ASGIMixin](drivers/index.md#ASGIMixin) 对应的 Server App 对象。 - **参数** empty - **返回** - Any: Server App 对象 - **异常** - AssertionError: 全局 Driver 对象不是 [ASGIMixin](drivers/index.md#ASGIMixin) 类型 - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用) - **用法** ```python app = nonebot.get_app() ``` ## _def_ `get_asgi()` {#get-asgi} - **说明:** 获取全局 [ASGIMixin](drivers/index.md#ASGIMixin) 对应的 [ASGI](https://asgi.readthedocs.io/) 对象。 - **参数** empty - **返回** - Any: ASGI 对象 - **异常** - AssertionError: 全局 Driver 对象不是 [ASGIMixin](drivers/index.md#ASGIMixin) 类型 - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用) - **用法** ```python asgi = nonebot.get_asgi() ``` ## _def_ `get_bot(self_id=None)` {#get-bot} - **说明** 获取一个连接到 NoneBot 的 [Bot](adapters/index.md#Bot) 对象。 当提供 `self_id` 时,此函数是 `get_bots()[self_id]` 的简写; 当不提供时,返回一个 [Bot](adapters/index.md#Bot)。 - **参数** - `self_id` (str | None): 用来识别 [Bot](adapters/index.md#Bot) 的 [Bot.self_id](adapters/index.md#Bot-self-id) 属性 - **返回** - [Bot](adapters/index.md#Bot): [Bot](adapters/index.md#Bot) 对象 - **异常** - KeyError: 对应 self_id 的 Bot 不存在 - ValueError: 没有传入 self_id 且没有 Bot 可用 - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用) - **用法** ```python assert nonebot.get_bot("12345") == nonebot.get_bots()["12345"] another_unspecified_bot = nonebot.get_bot() ``` ## _def_ `get_bots()` {#get-bots} - **说明:** 获取所有连接到 NoneBot 的 [Bot](adapters/index.md#Bot) 对象。 - **参数** empty - **返回** - dict[str, [Bot](adapters/index.md#Bot)]: 一个以 [Bot.self_id](adapters/index.md#Bot-self-id) 为键 [Bot](adapters/index.md#Bot) 对象为值的字典 - **异常** - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用) - **用法** ```python bots = nonebot.get_bots() ``` ## _def_ `init(*, _env_file=None, **kwargs)` {#init} - **说明** 初始化 NoneBot 以及 全局 [Driver](drivers/index.md#Driver) 对象。 NoneBot 将会从 .env 文件中读取环境信息,并使用相应的 env 文件配置。 也可以传入自定义的 `_env_file` 来指定 NoneBot 从该文件读取配置。 - **参数** - `_env_file` (DOTENV_TYPE | None): 配置文件名,默认从 `.env.{env_name}` 中读取配置 - `**kwargs` (Any): 任意变量,将会存储到 [Driver.config](drivers/index.md#Driver-config) 对象里 - **返回** - None - **用法** ```python nonebot.init(database=Database(...)) ``` ## _def_ `run(*args, **kwargs)` {#run} - **说明:** 启动 NoneBot,即运行全局 [Driver](drivers/index.md#Driver) 对象。 - **参数** - `*args` (Any): 传入 [Driver.run](drivers/index.md#Driver-run) 的位置参数 - `**kwargs` (Any): 传入 [Driver.run](drivers/index.md#Driver-run) 的命名参数 - **返回** - None - **用法** ```python nonebot.run(host="127.0.0.1", port=8080) ``` ================================================ FILE: website/versioned_docs/version-2.4.4/api/log.md ================================================ --- mdx: format: md sidebar_position: 7 description: nonebot.log 模块 --- # nonebot.log 本模块定义了 NoneBot 的日志记录 Logger。 NoneBot 使用 [`loguru`][loguru] 来记录日志信息。 自定义 logger 请参考 [自定义日志](https://nonebot.dev/docs/appendices/log) 以及 [`loguru`][loguru] 文档。 [loguru]: https://github.com/Delgan/loguru ## _var_ `logger` {#logger} - **类型:** Logger - **说明** NoneBot 日志记录器对象。 默认信息: - 格式: `[%(asctime)s %(name)s] %(levelname)s: %(message)s` - 等级: `INFO` ,根据 `config.log_level` 配置改变 - 输出: 输出至 stdout - **用法** ```python from nonebot.log import logger ``` ## _class_ `LoguruHandler()` {#LoguruHandler} - **说明:** logging 与 loguru 之间的桥梁,将 logging 的日志转发到 loguru。 - **参数** auto ### _method_ `emit(record)` {#LoguruHandler-emit} - **参数** - `record` (logging.LogRecord) - **返回** - untyped ## _def_ `default_filter(record)` {#default-filter} - **说明:** 默认的日志过滤器,根据 `config.log_level` 配置改变日志等级。 - **参数** - `record` (Record) - **返回** - untyped ## _var_ `default_format` {#default-format} - **类型:** str - **说明:** 默认日志格式 ================================================ FILE: website/versioned_docs/version-2.4.4/api/matcher.md ================================================ --- mdx: format: md sidebar_position: 3 description: nonebot.matcher 模块 --- # nonebot.matcher 本模块实现事件响应器的创建与运行,并提供一些快捷方法来帮助用户更好的与机器人进行对话。 ## _var_ `DEFAULT_PROVIDER_CLASS` {#DEFAULT-PROVIDER-CLASS} - **类型:** untyped - **说明:** 默认存储器类型 ## _class_ `Matcher()` {#Matcher} - **说明:** 事件响应器类 - **参数** empty ### _class-var_ `type` {#Matcher-type} - **类型:** ClassVar[str] - **说明:** 事件响应器类型 ### _class-var_ `rule` {#Matcher-rule} - **类型:** ClassVar[[Rule](rule.md#Rule)] - **说明:** 事件响应器匹配规则 ### _class-var_ `permission` {#Matcher-permission} - **类型:** ClassVar[[Permission](permission.md#Permission)] - **说明:** 事件响应器触发权限 ### _class-var_ `handlers` {#Matcher-handlers} - **类型:** ClassVar[list[[Dependent](dependencies/index.md#Dependent)[Any]]] - **说明:** 事件响应器拥有的事件处理函数列表 ### _class-var_ `priority` {#Matcher-priority} - **类型:** ClassVar[int] - **说明:** 事件响应器优先级 ### _class-var_ `block` {#Matcher-block} - **类型:** bool - **说明:** 事件响应器是否阻止事件传播 ### _class-var_ `temp` {#Matcher-temp} - **类型:** ClassVar[bool] - **说明:** 事件响应器是否为临时 ### _class-var_ `expire_time` {#Matcher-expire-time} - **类型:** ClassVar[datetime | None] - **说明:** 事件响应器过期时间点 ### _classmethod_ `new(type_="", rule=None, permission=None, handlers=None, temp=False, priority=1, block=False, *, plugin=None, module=None, source=None, expire_time=None, default_state=None, default_type_updater=None, default_permission_updater=None)` {#Matcher-new} - **说明:** 创建一个新的事件响应器,并存储至 `matchers <#matchers>`\_ - **参数** - `type_` (str): 事件响应器类型,与 `event.get_type()` 一致时触发,空字符串表示任意 - `rule` ([Rule](rule.md#Rule) | None): 匹配规则 - `permission` ([Permission](permission.md#Permission) | None): 权限 - `handlers` (list[[T\_Handler](typing.md#T-Handler) | [Dependent](dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器,即触发一次后删除 - `priority` (int): 响应优先级 - `block` (bool): 是否阻止事件向更低优先级的响应器传播 - `plugin` ([Plugin](plugin/model.md#Plugin) | None): **Deprecated.** 事件响应器所在插件 - `module` (ModuleType | None): **Deprecated.** 事件响应器所在模块 - `source` (MatcherSource | None): 事件响应器源代码上下文信息 - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `default_state` ([T_State](typing.md#T-State) | None): 默认状态 `state` - `default_type_updater` ([T_TypeUpdater](typing.md#T-TypeUpdater) | [Dependent](dependencies/index.md#Dependent)[str] | None): 默认事件类型更新函数 - `default_permission_updater` ([T_PermissionUpdater](typing.md#T-PermissionUpdater) | [Dependent](dependencies/index.md#Dependent)[[Permission](permission.md#Permission)] | None): 默认会话权限更新函数 - **返回** - type[Matcher]: 新的事件响应器类 ### _classmethod_ `destroy()` {#Matcher-destroy} - **说明:** 销毁当前的事件响应器 - **参数** empty - **返回** - None ### _classmethod_ `check_perm(bot, event, stack=None, dependency_cache=None)` {#Matcher-check-perm} - **说明:** 检查是否满足触发权限 - **参数** - `bot` ([Bot](adapters/index.md#Bot)): Bot 对象 - `event` ([Event](adapters/index.md#Event)): 上报事件 - `stack` (AsyncExitStack | None): 异步上下文栈 - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None): 依赖缓存 - **返回** - bool: 是否满足权限 ### _classmethod_ `check_rule(bot, event, state, stack=None, dependency_cache=None)` {#Matcher-check-rule} - **说明:** 检查是否满足匹配规则 - **参数** - `bot` ([Bot](adapters/index.md#Bot)): Bot 对象 - `event` ([Event](adapters/index.md#Event)): 上报事件 - `state` ([T_State](typing.md#T-State)): 当前状态 - `stack` (AsyncExitStack | None): 异步上下文栈 - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None): 依赖缓存 - **返回** - bool: 是否满足匹配规则 ### _classmethod_ `type_updater(func)` {#Matcher-type-updater} - **说明:** 装饰一个函数来更改当前事件响应器的默认响应事件类型更新函数 - **参数** - `func` ([T_TypeUpdater](typing.md#T-TypeUpdater)): 响应事件类型更新函数 - **返回** - [T_TypeUpdater](typing.md#T-TypeUpdater) ### _classmethod_ `permission_updater(func)` {#Matcher-permission-updater} - **说明:** 装饰一个函数来更改当前事件响应器的默认会话权限更新函数 - **参数** - `func` ([T_PermissionUpdater](typing.md#T-PermissionUpdater)): 会话权限更新函数 - **返回** - [T_PermissionUpdater](typing.md#T-PermissionUpdater) ### _classmethod_ `append_handler(handler, parameterless=None)` {#Matcher-append-handler} - **参数** - `handler` ([T_Handler](typing.md#T-Handler)) - `parameterless` (Iterable[Any] | None) - **返回** - [Dependent](dependencies/index.md#Dependent)[Any] ### _classmethod_ `handle(parameterless=None)` {#Matcher-handle} - **说明:** 装饰一个函数来向事件响应器直接添加一个处理函数 - **参数** - `parameterless` (Iterable[Any] | None): 非参数类型依赖列表 - **返回** - ([T_Handler](typing.md#T-Handler)) -> [T_Handler](typing.md#T-Handler) ### _classmethod_ `receive(id="", parameterless=None)` {#Matcher-receive} - **说明:** 装饰一个函数来指示 NoneBot 在接收用户新的一条消息后继续运行该函数 - **参数** - `id` (str): 消息 ID - `parameterless` (Iterable[Any] | None): 非参数类型依赖列表 - **返回** - ([T_Handler](typing.md#T-Handler)) -> [T_Handler](typing.md#T-Handler) ### _classmethod_ `got(key, prompt=None, parameterless=None)` {#Matcher-got} - **说明** 装饰一个函数来指示 NoneBot 获取一个参数 `key` 当要获取的 `key` 不存在时接收用户新的一条消息再运行该函数, 如果 `key` 已存在则直接继续运行 - **参数** - `key` (str): 参数名 - `prompt` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate) | None): 在参数不存在时向用户发送的消息 - `parameterless` (Iterable[Any] | None): 非参数类型依赖列表 - **返回** - ([T_Handler](typing.md#T-Handler)) -> [T_Handler](typing.md#T-Handler) ### _classmethod_ `send(message, **kwargs)` {#Matcher-send} - **说明:** 发送一条消息给当前交互用户 - **参数** - `message` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate)): 消息内容 - `**kwargs` (Any): [Bot.send](adapters/index.md#Bot-send) 的参数, 请参考对应 adapter 的 bot 对象 api - **返回** - Any ### _classmethod_ `finish(message=None, **kwargs)` {#Matcher-finish} - **说明:** 发送一条消息给当前交互用户并结束当前事件响应器 - **参数** - `message` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate) | None): 消息内容 - `**kwargs`: [Bot.send](adapters/index.md#Bot-send) 的参数, 请参考对应 adapter 的 bot 对象 api - **返回** - NoReturn ### _classmethod_ `pause(prompt=None, **kwargs)` {#Matcher-pause} - **说明:** 发送一条消息给当前交互用户并暂停事件响应器,在接收用户新的一条消息后继续下一个处理函数 - **参数** - `prompt` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate) | None): 消息内容 - `**kwargs`: [Bot.send](adapters/index.md#Bot-send) 的参数, 请参考对应 adapter 的 bot 对象 api - **返回** - NoReturn ### _classmethod_ `reject(prompt=None, **kwargs)` {#Matcher-reject} - **说明:** 最近使用 `got` / `receive` 接收的消息不符合预期, 发送一条消息给当前交互用户并将当前事件处理流程中断在当前位置,在接收用户新的一个事件后从头开始执行当前处理函数 - **参数** - `prompt` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate) | None): 消息内容 - `**kwargs`: [Bot.send](adapters/index.md#Bot-send) 的参数, 请参考对应 adapter 的 bot 对象 api - **返回** - NoReturn ### _classmethod_ `reject_arg(key, prompt=None, **kwargs)` {#Matcher-reject-arg} - **说明:** 最近使用 `got` 接收的消息不符合预期, 发送一条消息给当前交互用户并将当前事件处理流程中断在当前位置,在接收用户新的一条消息后从头开始执行当前处理函数 - **参数** - `key` (str): 参数名 - `prompt` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate) | None): 消息内容 - `**kwargs`: [Bot.send](adapters/index.md#Bot-send) 的参数, 请参考对应 adapter 的 bot 对象 api - **返回** - NoReturn ### _classmethod_ `reject_receive(id="", prompt=None, **kwargs)` {#Matcher-reject-receive} - **说明:** 最近使用 `receive` 接收的消息不符合预期, 发送一条消息给当前交互用户并将当前事件处理流程中断在当前位置,在接收用户新的一个事件后从头开始执行当前处理函数 - **参数** - `id` (str): 消息 id - `prompt` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate) | None): 消息内容 - `**kwargs`: [Bot.send](adapters/index.md#Bot-send) 的参数, 请参考对应 adapter 的 bot 对象 api - **返回** - NoReturn ### _classmethod_ `skip()` {#Matcher-skip} - **说明** 跳过当前事件处理函数,继续下一个处理函数 通常在事件处理函数的依赖中使用。 - **参数** empty - **返回** - NoReturn ### _method_ `get_receive(id, default=None)` {#Matcher-get-receive} - **说明** 获取一个 `receive` 事件 如果没有找到对应的事件,返回 `default` 值 - **重载** **1.** `(id) -> Event | None` - **参数** - `id` (str) - **返回** - [Event](adapters/index.md#Event) | None **2.** `(id, default) -> Event | T` - **参数** - `id` (str) - `default` (T) - **返回** - [Event](adapters/index.md#Event) | T ### _method_ `set_receive(id, event)` {#Matcher-set-receive} - **说明:** 设置一个 `receive` 事件 - **参数** - `id` (str) - `event` ([Event](adapters/index.md#Event)) - **返回** - None ### _method_ `get_last_receive(default=None)` {#Matcher-get-last-receive} - **说明** 获取最近一次 `receive` 事件 如果没有事件,返回 `default` 值 - **重载** **1.** `() -> Event | None` - **参数** empty - **返回** - [Event](adapters/index.md#Event) | None **2.** `(default) -> Event | T` - **参数** - `default` (T) - **返回** - [Event](adapters/index.md#Event) | T ### _method_ `get_arg(key, default=None)` {#Matcher-get-arg} - **说明** 获取一个 `got` 消息 如果没有找到对应的消息,返回 `default` 值 - **重载** **1.** `(key) -> Message | None` - **参数** - `key` (str) - **返回** - [Message](adapters/index.md#Message) | None **2.** `(key, default) -> Message | T` - **参数** - `key` (str) - `default` (T) - **返回** - [Message](adapters/index.md#Message) | T ### _method_ `set_arg(key, message)` {#Matcher-set-arg} - **说明:** 设置一个 `got` 消息 - **参数** - `key` (str) - `message` ([Message](adapters/index.md#Message)) - **返回** - None ### _method_ `set_target(target, cache=True)` {#Matcher-set-target} - **参数** - `target` (str) - `cache` (bool) - **返回** - None ### _method_ `get_target(default=None)` {#Matcher-get-target} - **重载** **1.** `() -> str | None` - **参数** empty - **返回** - str | None **2.** `(default) -> str | T` - **参数** - `default` (T) - **返回** - str | T ### _method_ `stop_propagation()` {#Matcher-stop-propagation} - **说明:** 阻止事件传播 - **参数** empty - **返回** - untyped ### _async method_ `update_type(bot, event, stack=None, dependency_cache=None)` {#Matcher-update-type} - **参数** - `bot` ([Bot](adapters/index.md#Bot)) - `event` ([Event](adapters/index.md#Event)) - `stack` (AsyncExitStack | None) - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None) - **返回** - str ### _async method_ `update_permission(bot, event, stack=None, dependency_cache=None)` {#Matcher-update-permission} - **参数** - `bot` ([Bot](adapters/index.md#Bot)) - `event` ([Event](adapters/index.md#Event)) - `stack` (AsyncExitStack | None) - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None) - **返回** - [Permission](permission.md#Permission) ### _async method_ `resolve_reject()` {#Matcher-resolve-reject} - **参数** empty - **返回** - untyped ### _method_ `ensure_context(bot, event)` {#Matcher-ensure-context} - **参数** - `bot` ([Bot](adapters/index.md#Bot)) - `event` ([Event](adapters/index.md#Event)) - **返回** - untyped ### _async method_ `simple_run(bot, event, state, stack=None, dependency_cache=None)` {#Matcher-simple-run} - **参数** - `bot` ([Bot](adapters/index.md#Bot)) - `event` ([Event](adapters/index.md#Event)) - `state` ([T_State](typing.md#T-State)) - `stack` (AsyncExitStack | None) - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None) - **返回** - untyped ### _async method_ `run(bot, event, state, stack=None, dependency_cache=None)` {#Matcher-run} - **参数** - `bot` ([Bot](adapters/index.md#Bot)) - `event` ([Event](adapters/index.md#Event)) - `state` ([T_State](typing.md#T-State)) - `stack` (AsyncExitStack | None) - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None) - **返回** - untyped ## _class_ `MatcherManager()` {#MatcherManager} - **说明** 事件响应器管理器 实现了常用字典操作,用于管理事件响应器。 - **参数** empty ### _method_ `keys()` {#MatcherManager-keys} - **参数** empty - **返回** - KeysView[int] ### _method_ `values()` {#MatcherManager-values} - **参数** empty - **返回** - ValuesView[list[type[[Matcher](#Matcher)]]] ### _method_ `items()` {#MatcherManager-items} - **参数** empty - **返回** - ItemsView[int, list[type[[Matcher](#Matcher)]]] ### _method_ `get(key, default=None)` {#MatcherManager-get} - **重载** **1.** `(key) -> list[type[Matcher]] | None` - **参数** - `key` (int) - **返回** - list[type[[Matcher](#Matcher)]] | None **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) - `default` (T) - **返回** - list[type[[Matcher](#Matcher)]] | T ### _method_ `pop(key)` {#MatcherManager-pop} - **参数** - `key` (int) - **返回** - list[type[[Matcher](#Matcher)]] ### _method_ `popitem()` {#MatcherManager-popitem} - **参数** empty - **返回** - tuple[int, list[type[[Matcher](#Matcher)]]] ### _method_ `clear()` {#MatcherManager-clear} - **参数** empty - **返回** - None ### _method_ `update(m, /)` {#MatcherManager-update} - **参数** - `m` (MutableMapping[int, list[type[[Matcher](#Matcher)]]]) - **返回** - None ### _method_ `setdefault(key, default)` {#MatcherManager-setdefault} - **参数** - `key` (int) - `default` (list[type[[Matcher](#Matcher)]]) - **返回** - list[type[[Matcher](#Matcher)]] ### _method_ `set_provider(provider_class)` {#MatcherManager-set-provider} - **说明:** 设置事件响应器存储器 - **参数** - `provider_class` (type[[MatcherProvider](#MatcherProvider)]): 事件响应器存储器类 - **返回** - None ## _abstract class_ `MatcherProvider(matchers)` {#MatcherProvider} - **说明:** 事件响应器存储器基类 - **参数** - `matchers` (Mapping[int, list[type[[Matcher](#Matcher)]]]): 当前存储器中已有的事件响应器 ## _var_ `matchers` {#matchers} - **类型:** untyped ================================================ FILE: website/versioned_docs/version-2.4.4/api/message.md ================================================ --- mdx: format: md sidebar_position: 2 description: nonebot.message 模块 --- # nonebot.message 本模块定义了事件处理主要流程。 NoneBot 内部处理并按优先级分发事件给所有事件响应器,提供了多个插槽以进行事件的预处理等。 ## _def_ `event_preprocessor(func)` {#event-preprocessor} - **说明** 事件预处理。 装饰一个函数,使它在每次接收到事件并分发给各响应器之前执行。 - **参数** - `func` ([T_EventPreProcessor](typing.md#T-EventPreProcessor)) - **返回** - [T_EventPreProcessor](typing.md#T-EventPreProcessor) ## _def_ `event_postprocessor(func)` {#event-postprocessor} - **说明** 事件后处理。 装饰一个函数,使它在每次接收到事件并分发给各响应器之后执行。 - **参数** - `func` ([T_EventPostProcessor](typing.md#T-EventPostProcessor)) - **返回** - [T_EventPostProcessor](typing.md#T-EventPostProcessor) ## _def_ `run_preprocessor(func)` {#run-preprocessor} - **说明** 运行预处理。 装饰一个函数,使它在每次事件响应器运行前执行。 - **参数** - `func` ([T_RunPreProcessor](typing.md#T-RunPreProcessor)) - **返回** - [T_RunPreProcessor](typing.md#T-RunPreProcessor) ## _def_ `run_postprocessor(func)` {#run-postprocessor} - **说明** 运行后处理。 装饰一个函数,使它在每次事件响应器运行后执行。 - **参数** - `func` ([T_RunPostProcessor](typing.md#T-RunPostProcessor)) - **返回** - [T_RunPostProcessor](typing.md#T-RunPostProcessor) ## _async def_ `check_and_run_matcher(Matcher, bot, event, state, stack=None, dependency_cache=None)` {#check-and-run-matcher} - **说明:** 检查并运行事件响应器。 - **参数** - `Matcher` (type[[Matcher](matcher.md#Matcher)]): 事件响应器 - `bot` ([Bot](adapters/index.md#Bot)): Bot 对象 - `event` ([Event](adapters/index.md#Event)): Event 对象 - `state` ([T_State](typing.md#T-State)): 会话状态 - `stack` (AsyncExitStack | None): 异步上下文栈 - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None): 依赖缓存 - **返回** - None ## _async def_ `handle_event(bot, event)` {#handle-event} - **说明:** 处理一个事件。调用该函数以实现分发事件。 - **参数** - `bot` ([Bot](adapters/index.md#Bot)): Bot 对象 - `event` ([Event](adapters/index.md#Event)): Event 对象 - **返回** - None - **用法** ```python driver.task_group.start_soon(handle_event, bot, event) ``` ================================================ FILE: website/versioned_docs/version-2.4.4/api/params.md ================================================ --- mdx: format: md sidebar_position: 4 description: nonebot.params 模块 --- # nonebot.params 本模块定义了依赖注入的各类参数。 ## _def_ `Arg(key=None)` {#Arg} - **说明:** Arg 参数消息 - **参数** - `key` (str | None) - **返回** - Any ## _class_ `ArgParam(*args, key, type, **kwargs)` {#ArgParam} - **说明** Arg 注入参数 本注入解析事件响应器操作 `got` 所获取的参数。 可以通过 `Arg`、`ArgStr`、`ArgPlainText` 等函数参数 `key` 指定获取的参数, 留空则会根据参数名称获取。 - **参数** - `*args` - `key` (str) - `type` (Literal['message', 'str', 'plaintext', 'prompt']) - `**kwargs` (Any) ## _def_ `ArgPlainText(key=None)` {#ArgPlainText} - **说明:** Arg 参数消息纯文本 - **参数** - `key` (str | None) - **返回** - str ## _def_ `ArgPromptResult(key=None)` {#ArgPromptResult} - **说明:** `arg` prompt 发送结果 - **参数** - `key` (str | None) - **返回** - Any ## _def_ `ArgStr(key=None)` {#ArgStr} - **说明:** Arg 参数消息文本 - **参数** - `key` (str | None) - **返回** - str ## _class_ `BotParam(*args, checker=None, **kwargs)` {#BotParam} - **说明** 注入参数。 本注入解析所有类型为且仅为 [Bot](adapters/index.md#Bot) 及其子类或 `None` 的参数。 为保证兼容性,本注入还会解析名为 `bot` 且没有类型注解的参数。 - **参数** - `*args` - `checker` ([ModelField](compat.md#ModelField) | None) - `**kwargs` (Any) ## _class_ `DefaultParam(*args, validate=False, **kwargs)` {#DefaultParam} - **说明** 默认值注入参数 本注入解析所有剩余未能解析且具有默认值的参数。 本注入参数应该具有最低优先级,因此应该在所有其他注入参数之后使用。 - **参数** - `*args` - `validate` (bool) - `**kwargs` (Any) ## _class_ `DependParam(*args, dependent, use_cache, **kwargs)` {#DependParam} - **说明** 子依赖注入参数。 本注入解析所有子依赖注入,然后将它们的返回值作为参数值传递给父依赖。 本注入应该具有最高优先级,因此应该在其他参数之前检查。 - **参数** - `*args` - `dependent` ([Dependent](dependencies/index.md#Dependent)[Any]) - `use_cache` (bool) - `**kwargs` (Any) ## _def_ `Depends(dependency=None, *, use_cache=True, validate=False)` {#Depends} - **说明:** 子依赖装饰器 - **参数** - `dependency` ([T_Handler](typing.md#T-Handler) | None): 依赖函数。默认为参数的类型注释。 - `use_cache` (bool): 是否使用缓存。默认为 `True`。 - `validate` (bool | PydanticFieldInfo): 是否使用 Pydantic 类型校验。默认为 `False`。 - **返回** - Any - **用法** ```python def depend_func() -> Any: return ... def depend_gen_func(): try: yield ... finally: ... async def handler( param_name: Any = Depends(depend_func), gen: Any = Depends(depend_gen_func), ): ... ``` ## _class_ `EventParam(*args, checker=None, **kwargs)` {#EventParam} - **说明** 注入参数 本注入解析所有类型为且仅为 [Event](adapters/index.md#Event) 及其子类或 `None` 的参数。 为保证兼容性,本注入还会解析名为 `event` 且没有类型注解的参数。 - **参数** - `*args` - `checker` ([ModelField](compat.md#ModelField) | None) - `**kwargs` (Any) ## _class_ `ExceptionParam(*args, validate=False, **kwargs)` {#ExceptionParam} - **说明** 的异常注入参数 本注入解析所有类型为 `Exception` 或 `None` 的参数。 为保证兼容性,本注入还会解析名为 `exception` 且没有类型注解的参数。 - **参数** - `*args` - `validate` (bool) - `**kwargs` (Any) ## _class_ `MatcherParam(*args, checker=None, **kwargs)` {#MatcherParam} - **说明** 事件响应器实例注入参数 本注入解析所有类型为且仅为 [Matcher](matcher.md#Matcher) 及其子类或 `None` 的参数。 为保证兼容性,本注入还会解析名为 `matcher` 且没有类型注解的参数。 - **参数** - `*args` - `checker` ([ModelField](compat.md#ModelField) | None) - `**kwargs` (Any) ## _class_ `StateParam(*args, validate=False, **kwargs)` {#StateParam} - **说明** 事件处理状态注入参数 本注入解析所有类型为 `T_State` 的参数。 为保证兼容性,本注入还会解析名为 `state` 且没有类型注解的参数。 - **参数** - `*args` - `validate` (bool) - `**kwargs` (Any) ## _def_ `EventType()` {#EventType} - **说明:** 类型参数 - **参数** empty - **返回** - str ## _def_ `EventMessage()` {#EventMessage} - **说明:** 消息参数 - **参数** empty - **返回** - Any ## _def_ `EventPlainText()` {#EventPlainText} - **说明:** 纯文本消息参数 - **参数** empty - **返回** - str ## _def_ `EventToMe()` {#EventToMe} - **说明:** `to_me` 参数 - **参数** empty - **返回** - bool ## _def_ `Command()` {#Command} - **说明:** 消息命令元组 - **参数** empty - **返回** - tuple[str, ...] ## _def_ `RawCommand()` {#RawCommand} - **说明:** 消息命令文本 - **参数** empty - **返回** - str ## _def_ `CommandArg()` {#CommandArg} - **说明:** 消息命令参数 - **参数** empty - **返回** - Any ## _def_ `CommandStart()` {#CommandStart} - **说明:** 消息命令开头 - **参数** empty - **返回** - str ## _def_ `CommandWhitespace()` {#CommandWhitespace} - **说明:** 消息命令与参数之间的空白 - **参数** empty - **返回** - str ## _def_ `ShellCommandArgs()` {#ShellCommandArgs} - **说明:** shell 命令解析后的参数字典 - **参数** empty - **返回** - Any ## _def_ `ShellCommandArgv()` {#ShellCommandArgv} - **说明:** shell 命令原始参数列表 - **参数** empty - **返回** - Any ## _def_ `RegexMatched()` {#RegexMatched} - **说明:** 正则匹配结果 - **参数** empty - **返回** - Match[str] ## _def_ `RegexStr(*groups)` {#RegexStr} - **说明:** 正则匹配结果文本 - **重载** **1.** `(group, /) -> str` - **参数** - `group` (Literal[0]) - **返回** - str **2.** `(group, /) -> str | Any` - **参数** - `group` (str | int) - **返回** - str | Any **3.** `(group1, group2, /, *groups) -> tuple[str | Any, ...]` - **参数** - `group1` (str | int) - `group2` (str | int) - `*groups` (str | int) - **返回** - tuple[str | Any, ...] ## _def_ `RegexGroup()` {#RegexGroup} - **说明:** 正则匹配结果 group 元组 - **参数** empty - **返回** - tuple[Any, ...] ## _def_ `RegexDict()` {#RegexDict} - **说明:** 正则匹配结果 group 字典 - **参数** empty - **返回** - dict[str, Any] ## _def_ `Startswith()` {#Startswith} - **说明:** 响应触发前缀 - **参数** empty - **返回** - str ## _def_ `Endswith()` {#Endswith} - **说明:** 响应触发后缀 - **参数** empty - **返回** - str ## _def_ `Fullmatch()` {#Fullmatch} - **说明:** 响应触发完整消息 - **参数** empty - **返回** - str ## _def_ `Keyword()` {#Keyword} - **说明:** 响应触发关键字 - **参数** empty - **返回** - str ## _def_ `Received(id=None, default=None)` {#Received} - **说明:** `receive` 事件参数 - **参数** - `id` (str | None) - `default` (Any) - **返回** - Any ## _def_ `LastReceived(default=None)` {#LastReceived} - **说明:** `last_receive` 事件参数 - **参数** - `default` (Any) - **返回** - Any ## _def_ `ReceivePromptResult(id=None)` {#ReceivePromptResult} - **说明:** `receive` prompt 发送结果 - **参数** - `id` (str | None) - **返回** - Any ## _def_ `PausePromptResult()` {#PausePromptResult} - **说明:** `pause` prompt 发送结果 - **参数** empty - **返回** - Any ================================================ FILE: website/versioned_docs/version-2.4.4/api/permission.md ================================================ --- mdx: format: md sidebar_position: 6 description: nonebot.permission 模块 --- # nonebot.permission 本模块是 [Matcher.permission](matcher.md#Matcher-permission) 的类型定义。 每个[事件响应器](matcher.md#Matcher) 拥有一个 [Permission](#Permission),其中是 `PermissionChecker` 的集合。 只要有一个 `PermissionChecker` 检查结果为 `True` 时就会继续运行。 ## _def_ `USER(*users, perm=None)` {#USER} - **说明** 匹配当前事件属于指定会话。 如果 `perm` 中仅有 `User` 类型的权限检查函数,则会去除原有检查函数的会话 ID 限制。 - **参数** - `*users` (str) - `perm` (Permission | None): 需要同时满足的权限 - `user`: 会话白名单 - **返回** - untyped ## _class_ `Permission(*checkers)` {#Permission} - **说明** 权限类。 当事件传递时,在 [Matcher](matcher.md#Matcher) 运行前进行检查。 - **参数** - `*checkers` ([T_PermissionChecker](typing.md#T-PermissionChecker) | [Dependent](dependencies/index.md#Dependent)[bool]): PermissionChecker - **用法** ```python Permission(async_function) | sync_function # 等价于 Permission(async_function, sync_function) ``` ### _instance-var_ `checkers` {#Permission-checkers} - **类型:** set[[Dependent](dependencies/index.md#Dependent)[bool]] - **说明:** 存储 `PermissionChecker` ### _async method_ `__call__(bot, event, stack=None, dependency_cache=None)` {#Permission---call--} - **说明:** 检查是否满足某个权限。 - **参数** - `bot` ([Bot](adapters/index.md#Bot)): Bot 对象 - `event` ([Event](adapters/index.md#Event)): Event 对象 - `stack` (AsyncExitStack | None): 异步上下文栈 - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None): 依赖缓存 - **返回** - bool ## _class_ `User(users, perm=None)` {#User} - **说明:** 检查当前事件是否属于指定会话。 - **参数** - `users` (tuple[str, ...]): 会话 ID 元组 - `perm` (Permission | None): 需同时满足的权限 ### _classmethod_ `from_event(event, perm=None)` {#User-from-event} - **说明** 从事件中获取会话 ID。 如果 `perm` 中仅有 `User` 类型的权限检查函数,则会去除原有的会话 ID 限制。 - **参数** - `event` ([Event](adapters/index.md#Event)): Event 对象 - `perm` (Permission | None): 需同时满足的权限 - **返回** - Self ### _classmethod_ `from_permission(*users, perm=None)` {#User-from-permission} - **说明** 指定会话与权限。 如果 `perm` 中仅有 `User` 类型的权限检查函数,则会去除原有的会话 ID 限制。 - **参数** - `*users` (str): 会话白名单 - `perm` (Permission | None): 需同时满足的权限 - **返回** - Self ## _class_ `Message()` {#Message} - **说明:** 检查是否为消息事件 - **参数** auto ## _class_ `Notice()` {#Notice} - **说明:** 检查是否为通知事件 - **参数** auto ## _class_ `Request()` {#Request} - **说明:** 检查是否为请求事件 - **参数** auto ## _class_ `MetaEvent()` {#MetaEvent} - **说明:** 检查是否为元事件 - **参数** auto ## _var_ `MESSAGE` {#MESSAGE} - **类型:** [Permission](#Permission) - **说明** 匹配任意 `message` 类型事件 仅在需要同时捕获不同类型事件时使用,优先使用 message type 的 Matcher。 ## _var_ `NOTICE` {#NOTICE} - **类型:** [Permission](#Permission) - **说明** 匹配任意 `notice` 类型事件 仅在需要同时捕获不同类型事件时使用,优先使用 notice type 的 Matcher。 ## _var_ `REQUEST` {#REQUEST} - **类型:** [Permission](#Permission) - **说明** 匹配任意 `request` 类型事件 仅在需要同时捕获不同类型事件时使用,优先使用 request type 的 Matcher。 ## _var_ `METAEVENT` {#METAEVENT} - **类型:** [Permission](#Permission) - **说明** 匹配任意 `meta_event` 类型事件 仅在需要同时捕获不同类型事件时使用,优先使用 meta_event type 的 Matcher。 ## _class_ `SuperUser()` {#SuperUser} - **说明:** 检查当前事件是否是消息事件且属于超级管理员 - **参数** auto ## _var_ `SUPERUSER` {#SUPERUSER} - **类型:** [Permission](#Permission) - **说明:** 匹配任意超级用户事件 ================================================ FILE: website/versioned_docs/version-2.4.4/api/plugin/_category_.json ================================================ { "position": 12 } ================================================ FILE: website/versioned_docs/version-2.4.4/api/plugin/index.md ================================================ --- mdx: format: md sidebar_position: 0 description: nonebot.plugin 模块 --- # nonebot.plugin 本模块为 NoneBot 插件开发提供便携的定义函数。 ## 快捷导入 为方便使用,本模块从子模块导入了部分内容,以下内容可以直接通过本模块导入: - `on` => [`on`](on.md#on) - `on_metaevent` => [`on_metaevent`](on.md#on-metaevent) - `on_message` => [`on_message`](on.md#on-message) - `on_notice` => [`on_notice`](on.md#on-notice) - `on_request` => [`on_request`](on.md#on-request) - `on_startswith` => [`on_startswith`](on.md#on-startswith) - `on_endswith` => [`on_endswith`](on.md#on-endswith) - `on_fullmatch` => [`on_fullmatch`](on.md#on-fullmatch) - `on_keyword` => [`on_keyword`](on.md#on-keyword) - `on_command` => [`on_command`](on.md#on-command) - `on_shell_command` => [`on_shell_command`](on.md#on-shell-command) - `on_regex` => [`on_regex`](on.md#on-regex) - `on_type` => [`on_type`](on.md#on-type) - `CommandGroup` => [`CommandGroup`](on.md#CommandGroup) - `Matchergroup` => [`MatcherGroup`](on.md#MatcherGroup) - `load_plugin` => [`load_plugin`](load.md#load-plugin) - `load_plugins` => [`load_plugins`](load.md#load-plugins) - `load_all_plugins` => [`load_all_plugins`](load.md#load-all-plugins) - `load_from_json` => [`load_from_json`](load.md#load-from-json) - `load_from_toml` => [`load_from_toml`](load.md#load-from-toml) - `load_builtin_plugin` => [`load_builtin_plugin`](load.md#load-builtin-plugin) - `load_builtin_plugins` => [`load_builtin_plugins`](load.md#load-builtin-plugins) - `require` => [`require`](load.md#require) - `PluginMetadata` => [`PluginMetadata`](model.md#PluginMetadata) ## _def_ `get_plugin(plugin_id)` {#get-plugin} - **说明** 获取已经导入的某个插件。 如果为 `load_plugins` 文件夹导入的插件,则为文件(夹)名。 如果为嵌套的子插件,标识符为 `父插件标识符:子插件文件(夹)名`。 - **参数** - `plugin_id` (str): 插件标识符,即 [Plugin.id\_](model.md#Plugin-id-)。 - **返回** - [Plugin](model.md#Plugin) | None ## _def_ `get_plugin_by_module_name(module_name)` {#get-plugin-by-module-name} - **说明** 通过模块名获取已经导入的某个插件。 如果提供的模块名为某个插件的子模块,同样会返回该插件。 - **参数** - `module_name` (str): 模块名,即 [Plugin.module_name](model.md#Plugin-module-name)。 - **返回** - [Plugin](model.md#Plugin) | None ## _def_ `get_loaded_plugins()` {#get-loaded-plugins} - **说明:** 获取当前已导入的所有插件。 - **参数** empty - **返回** - set[[Plugin](model.md#Plugin)] ## _def_ `get_available_plugin_names()` {#get-available-plugin-names} - **说明:** 获取当前所有可用的插件标识符(包含尚未加载的插件)。 - **参数** empty - **返回** - set[str] ## _def_ `get_plugin_config(config)` {#get-plugin-config} - **说明:** 从全局配置获取当前插件需要的配置项。 - **参数** - `config` (type[C]) - **返回** - C ================================================ FILE: website/versioned_docs/version-2.4.4/api/plugin/load.md ================================================ --- mdx: format: md sidebar_position: 1 description: nonebot.plugin.load 模块 --- # nonebot.plugin.load 本模块定义插件加载接口。 ## _def_ `load_plugin(module_path)` {#load-plugin} - **说明:** 加载单个插件,可以是本地插件或是通过 `pip` 安装的插件。 - **参数** - `module_path` (str | Path): 插件名称 `path.to.your.plugin` 或插件路径 `pathlib.Path(path/to/your/plugin)` - **返回** - [Plugin](model.md#Plugin) | None ## _def_ `load_plugins(*plugin_dir)` {#load-plugins} - **说明:** 导入文件夹下多个插件,以 `_` 开头的插件不会被导入! - **参数** - `*plugin_dir` (str): 文件夹路径 - **返回** - set[[Plugin](model.md#Plugin)] ## _def_ `load_all_plugins(module_path, plugin_dir)` {#load-all-plugins} - **说明:** 导入指定列表中的插件以及指定目录下多个插件,以 `_` 开头的插件不会被导入! - **参数** - `module_path` (Iterable[str]): 指定插件集合 - `plugin_dir` (Iterable[str]): 指定文件夹路径集合 - **返回** - set[[Plugin](model.md#Plugin)] ## _def_ `load_from_json(file_path, encoding="utf-8")` {#load-from-json} - **说明:** 导入指定 json 文件中的 `plugins` 以及 `plugin_dirs` 下多个插件。 以 `_` 开头的插件不会被导入! - **参数** - `file_path` (str): 指定 json 文件路径 - `encoding` (str): 指定 json 文件编码 - **返回** - set[[Plugin](model.md#Plugin)] - **用法** ```json title=plugins.json { "plugins": ["some_plugin"], "plugin_dirs": ["some_dir"] } ``` ```python nonebot.load_from_json("plugins.json") ``` ## _def_ `load_from_toml(file_path, encoding="utf-8")` {#load-from-toml} - **说明:** 导入指定 toml 文件 `[tool.nonebot]` 中的 `plugins` 以及 `plugin_dirs` 下多个插件。 以 `_` 开头的插件不会被导入! - **参数** - `file_path` (str): 指定 toml 文件路径 - `encoding` (str): 指定 toml 文件编码 - **返回** - set[[Plugin](model.md#Plugin)] - **用法** 新格式: ```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"] plugin_dirs = ["some_dir"] ``` ```python nonebot.load_from_toml("pyproject.toml") ``` ## _def_ `load_builtin_plugin(name)` {#load-builtin-plugin} - **说明:** 导入 NoneBot 内置插件。 - **参数** - `name` (str): 插件名称 - **返回** - [Plugin](model.md#Plugin) | None ## _def_ `load_builtin_plugins(*plugins)` {#load-builtin-plugins} - **说明:** 导入多个 NoneBot 内置插件。 - **参数** - `*plugins` (str): 插件名称列表 - **返回** - set[[Plugin](model.md#Plugin)] ## _def_ `require(name)` {#require} - **说明:** 声明依赖插件。 - **参数** - `name` (str): 插件模块名或插件标识符,仅在已声明插件的情况下可使用标识符。 - **返回** - ModuleType - **异常** - RuntimeError: 插件无法加载 ## _def_ `inherit_supported_adapters(*names)` {#inherit-supported-adapters} - **说明** 获取已加载插件的适配器支持状态集合。 如果传入了多个插件名称,返回值会自动取交集。 - **参数** - `*names` (str): 插件名称列表。 - **返回** - set[str] | None - **异常** - RuntimeError: 插件未加载 - ValueError: 插件缺少元数据 ================================================ FILE: website/versioned_docs/version-2.4.4/api/plugin/manager.md ================================================ --- mdx: format: md sidebar_position: 5 description: nonebot.plugin.manager 模块 --- # nonebot.plugin.manager 本模块实现插件加载流程。 参考: [import hooks](https://docs.python.org/3/reference/import.html#import-hooks), [PEP302](https://www.python.org/dev/peps/pep-0302/) ## _class_ `PluginManager(plugins=None, search_path=None)` {#PluginManager} - **说明:** 插件管理器。 - **参数** - `plugins` (Iterable[str] | None): 独立插件模块名集合。 - `search_path` (Iterable[str] | None): 插件搜索路径(文件夹),相对于当前工作目录。 ### _property_ `third_party_plugins` {#PluginManager-third-party-plugins} - **类型:** set[str] - **说明:** 返回所有独立插件标识符。 ### _property_ `searched_plugins` {#PluginManager-searched-plugins} - **类型:** set[str] - **说明:** 返回已搜索到的插件标识符。 ### _property_ `available_plugins` {#PluginManager-available-plugins} - **类型:** set[str] - **说明:** 返回当前插件管理器中可用的插件标识符。 ### _property_ `controlled_modules` {#PluginManager-controlled-modules} - **类型:** dict[str, str] - **说明:** 返回当前插件管理器中控制的插件标识符与模块路径映射字典。 ### _method_ `load_plugin(name)` {#PluginManager-load-plugin} - **说明** 加载指定插件。 可以使用完整插件模块名或者插件标识符加载。 - **参数** - `name` (str): 插件名称或插件标识符。 - **返回** - [Plugin](model.md#Plugin) | None ### _method_ `load_all_plugins()` {#PluginManager-load-all-plugins} - **说明:** 加载所有可用插件。 - **参数** empty - **返回** - set[[Plugin](model.md#Plugin)] ## _class_ `PluginFinder()` {#PluginFinder} - **参数** auto ### _method_ `find_spec(fullname, path, target=None)` {#PluginFinder-find-spec} - **参数** - `fullname` (str) - `path` (Sequence[str] | None) - `target` (ModuleType | None) - **返回** - untyped ## _class_ `PluginLoader(manager, fullname, path)` {#PluginLoader} - **参数** - `manager` (PluginManager) - `fullname` (str) - `path` (str) ### _method_ `create_module(spec)` {#PluginLoader-create-module} - **参数** - `spec` - **返回** - ModuleType | None ### _method_ `exec_module(module)` {#PluginLoader-exec-module} - **参数** - `module` (ModuleType) - **返回** - None ================================================ FILE: website/versioned_docs/version-2.4.4/api/plugin/model.md ================================================ --- mdx: format: md sidebar_position: 3 description: nonebot.plugin.model 模块 --- # nonebot.plugin.model 本模块定义插件相关信息。 ## _class_ `PluginMetadata()` {#PluginMetadata} - **说明:** 插件元信息,由插件编写者提供 - **参数** auto ### _instance-var_ `name` {#PluginMetadata-name} - **类型:** str - **说明:** 插件名称 ### _instance-var_ `description` {#PluginMetadata-description} - **类型:** str - **说明:** 插件功能介绍 ### _instance-var_ `usage` {#PluginMetadata-usage} - **类型:** str - **说明:** 插件使用方法 ### _class-var_ `type` {#PluginMetadata-type} - **类型:** str | None - **说明:** 插件类型,用于商店分类 ### _class-var_ `homepage` {#PluginMetadata-homepage} - **类型:** str | None - **说明:** 插件主页 ### _class-var_ `config` {#PluginMetadata-config} - **类型:** type[BaseModel] | None - **说明:** 插件配置项 ### _class-var_ `supported_adapters` {#PluginMetadata-supported-adapters} - **类型:** set[str] | None - **说明** 插件支持的适配器模块路径 格式为 `[:]`,`~` 为 `nonebot.adapters.` 的缩写。 `None` 表示支持**所有适配器**。 ### _class-var_ `extra` {#PluginMetadata-extra} - **类型:** dict[Any, Any] - **说明:** 插件额外信息,可由插件编写者自由扩展定义 ### _method_ `get_supported_adapters()` {#PluginMetadata-get-supported-adapters} - **说明:** 获取当前已安装的插件支持适配器类列表 - **参数** empty - **返回** - set[type[[Adapter](../adapters/index.md#Adapter)]] | None ## _class_ `Plugin()` {#Plugin} - **说明:** 存储插件信息 - **参数** auto ### _instance-var_ `name` {#Plugin-name} - **类型:** str - **说明:** 插件名称,NoneBot 使用 文件/文件夹 名称作为插件名称 ### _instance-var_ `module` {#Plugin-module} - **类型:** ModuleType - **说明:** 插件模块对象 ### _instance-var_ `module_name` {#Plugin-module-name} - **类型:** str - **说明:** 点分割模块路径 ### _instance-var_ `manager` {#Plugin-manager} - **类型:** [PluginManager](manager.md#PluginManager) - **说明:** 导入该插件的插件管理器 ### _class-var_ `matcher` {#Plugin-matcher} - **类型:** set[type[[Matcher](../matcher.md#Matcher)]] - **说明:** 插件加载时定义的 `Matcher` ### _class-var_ `parent_plugin` {#Plugin-parent-plugin} - **类型:** Plugin | None - **说明:** 父插件 ### _class-var_ `sub_plugins` {#Plugin-sub-plugins} - **类型:** set[Plugin] - **说明:** 子插件集合 ### _class-var_ `metadata` {#Plugin-metadata} - **类型:** PluginMetadata | None - **说明:** 插件元信息 ### _property_ `id_` {#Plugin-id-} - **类型:** str - **说明:** 插件索引标识 ================================================ FILE: website/versioned_docs/version-2.4.4/api/plugin/on.md ================================================ --- mdx: format: md sidebar_position: 2 description: nonebot.plugin.on 模块 --- # nonebot.plugin.on 本模块定义事件响应器便携定义函数。 ## _def_ `store_matcher(matcher)` {#store-matcher} - **说明:** 存储一个事件响应器到插件。 - **参数** - `matcher` (type[[Matcher](../matcher.md#Matcher)]): 事件响应器 - **返回** - None ## _def_ `get_matcher_plugin(depth=...)` {#get-matcher-plugin} - **说明** 获取事件响应器定义所在插件。 **Deprecated**, 请使用 [get_matcher_source](#get-matcher-source) 获取信息。 - **参数** - `depth` (int): 调用栈深度 - **返回** - [Plugin](model.md#Plugin) | None ## _def_ `get_matcher_module(depth=...)` {#get-matcher-module} - **说明** 获取事件响应器定义所在模块。 **Deprecated**, 请使用 [get_matcher_source](#get-matcher-source) 获取信息。 - **参数** - `depth` (int): 调用栈深度 - **返回** - ModuleType | None ## _def_ `get_matcher_source(depth=...)` {#get-matcher-source} - **说明:** 获取事件响应器定义所在源码信息。 - **参数** - `depth` (int): 调用栈深度 - **返回** - MatcherSource | None ## _def_ `on(type="", rule=..., permission=..., *, handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on} - **说明:** 注册一个基础事件响应器,可自定义类型。 - **参数** - `type` (str): 事件响应器类型 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ## _def_ `on_metaevent(rule=..., permission=..., *, handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-metaevent} - **说明:** 注册一个元事件响应器。 - **参数** - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ## _def_ `on_message(rule=..., permission=..., *, handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-message} - **说明:** 注册一个消息事件响应器。 - **参数** - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ## _def_ `on_notice(rule=..., permission=..., *, handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-notice} - **说明:** 注册一个通知事件响应器。 - **参数** - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ## _def_ `on_request(rule=..., permission=..., *, handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-request} - **说明:** 注册一个请求事件响应器。 - **参数** - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ## _def_ `on_startswith(msg, rule=..., ignorecase=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-startswith} - **说明:** 注册一个消息事件响应器,并且当消息的**文本部分**以指定内容开头时响应。 - **参数** - `msg` (str | tuple[str, ...]): 指定消息开头内容 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `ignorecase` (bool): 是否忽略大小写 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ## _def_ `on_endswith(msg, rule=..., ignorecase=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-endswith} - **说明:** 注册一个消息事件响应器,并且当消息的**文本部分**以指定内容结尾时响应。 - **参数** - `msg` (str | tuple[str, ...]): 指定消息结尾内容 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `ignorecase` (bool): 是否忽略大小写 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ## _def_ `on_fullmatch(msg, rule=..., ignorecase=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-fullmatch} - **说明:** 注册一个消息事件响应器,并且当消息的**文本部分**与指定内容完全一致时响应。 - **参数** - `msg` (str | tuple[str, ...]): 指定消息全匹配内容 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `ignorecase` (bool): 是否忽略大小写 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ## _def_ `on_keyword(keywords, rule=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-keyword} - **说明:** 注册一个消息事件响应器,并且当消息纯文本部分包含关键词时响应。 - **参数** - `keywords` (set[str]): 关键词列表 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ## _def_ `on_command(cmd, rule=..., aliases=..., force_whitespace=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-command} - **说明** 注册一个消息事件响应器,并且当消息以指定命令开头时响应。 命令匹配规则参考: `命令形式匹配 `\_ - **参数** - `cmd` (str | tuple[str, ...]): 指定命令内容 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `aliases` (set[str | tuple[str, ...]] | None): 命令别名 - `force_whitespace` (str | bool | None): 是否强制命令后必须有指定空白符 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ## _def_ `on_shell_command(cmd, rule=..., aliases=..., parser=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-shell-command} - **说明** 注册一个支持 `shell_like` 解析参数的命令消息事件响应器。 与普通的 `on_command` 不同的是,在添加 `parser` 参数时, 响应器会自动处理消息。 可以通过 [ShellCommandArgv](../params.md#ShellCommandArgv) 获取原始参数列表, 通过 [ShellCommandArgs](../params.md#ShellCommandArgs) 获取解析后的参数字典。 - **参数** - `cmd` (str | tuple[str, ...]): 指定命令内容 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `aliases` (set[str | tuple[str, ...]] | None): 命令别名 - `parser` ([ArgumentParser](../rule.md#ArgumentParser) | None): `nonebot.rule.ArgumentParser` 对象 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ## _def_ `on_regex(pattern, flags=..., rule=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-regex} - **说明** 注册一个消息事件响应器,并且当消息匹配正则表达式时响应。 命令匹配规则参考: `正则匹配 `\_ - **参数** - `pattern` (str): 正则表达式 - `flags` (int | re.RegexFlag): 正则匹配标志 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ## _def_ `on_type(types, rule=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-type} - **说明:** 注册一个事件响应器,并且当事件为指定类型时响应。 - **参数** - `types` (type[[Event](../adapters/index.md#Event)] | tuple[type[[Event](../adapters/index.md#Event)], ...]): 事件类型 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ## _class_ `CommandGroup(cmd, prefix_aliases=..., *, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#CommandGroup} - **参数** - `cmd` (str | tuple[str, ...]) - `prefix_aliases` (bool) - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None) - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None) - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None) - `temp` (bool) - `expire_time` (datetime | timedelta | None) - `priority` (int) - `block` (bool) - `state` ([T_State](../typing.md#T-State) | None) ### _method_ `command(cmd, *, rule=..., aliases=..., force_whitespace=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#CommandGroup-command} - **说明:** 注册一个新的命令。新参数将会覆盖命令组默认值 - **参数** - `cmd` (str | tuple[str, ...]): 指定命令内容 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `aliases` (set[str | tuple[str, ...]] | None): 命令别名 - `force_whitespace` (str | bool | None): 是否强制命令后必须有指定空白符 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ### _method_ `shell_command(cmd, *, rule=..., aliases=..., parser=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#CommandGroup-shell-command} - **说明:** 注册一个新的 `shell_like` 命令。新参数将会覆盖命令组默认值 - **参数** - `cmd` (str | tuple[str, ...]): 指定命令内容 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `aliases` (set[str | tuple[str, ...]] | None): 命令别名 - `parser` ([ArgumentParser](../rule.md#ArgumentParser) | None): `nonebot.rule.ArgumentParser` 对象 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ## _class_ `MatcherGroup(*, type=..., rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup} - **参数** - `type` (str) - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None) - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None) - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None) - `temp` (bool) - `expire_time` (datetime | timedelta | None) - `priority` (int) - `block` (bool) - `state` ([T_State](../typing.md#T-State) | None) ### _method_ `on(*, type=..., rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on} - **说明:** 注册一个基础事件响应器,可自定义类型。 - **参数** - `type` (str): 事件响应器类型 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ### _method_ `on_metaevent(*, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-metaevent} - **说明:** 注册一个元事件响应器。 - **参数** - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ### _method_ `on_message(*, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-message} - **说明:** 注册一个消息事件响应器。 - **参数** - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ### _method_ `on_notice(*, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-notice} - **说明:** 注册一个通知事件响应器。 - **参数** - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ### _method_ `on_request(*, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-request} - **说明:** 注册一个请求事件响应器。 - **参数** - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ### _method_ `on_startswith(msg, *, ignorecase=..., rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-startswith} - **说明:** 注册一个消息事件响应器,并且当消息的**文本部分**以指定内容开头时响应。 - **参数** - `msg` (str | tuple[str, ...]): 指定消息开头内容 - `ignorecase` (bool): 是否忽略大小写 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ### _method_ `on_endswith(msg, *, ignorecase=..., rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-endswith} - **说明:** 注册一个消息事件响应器,并且当消息的**文本部分**以指定内容结尾时响应。 - **参数** - `msg` (str | tuple[str, ...]): 指定消息结尾内容 - `ignorecase` (bool): 是否忽略大小写 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ### _method_ `on_fullmatch(msg, *, ignorecase=..., rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-fullmatch} - **说明:** 注册一个消息事件响应器,并且当消息的**文本部分**与指定内容完全一致时响应。 - **参数** - `msg` (str | tuple[str, ...]): 指定消息全匹配内容 - `ignorecase` (bool): 是否忽略大小写 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ### _method_ `on_keyword(keywords, *, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-keyword} - **说明:** 注册一个消息事件响应器,并且当消息纯文本部分包含关键词时响应。 - **参数** - `keywords` (set[str]): 关键词列表 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ### _method_ `on_command(cmd, aliases=..., force_whitespace=..., *, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-command} - **说明** 注册一个消息事件响应器,并且当消息以指定命令开头时响应。 命令匹配规则参考: `命令形式匹配 `\_ - **参数** - `cmd` (str | tuple[str, ...]): 指定命令内容 - `aliases` (set[str | tuple[str, ...]] | None): 命令别名 - `force_whitespace` (str | bool | None): 是否强制命令后必须有指定空白符 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ### _method_ `on_shell_command(cmd, aliases=..., parser=..., *, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-shell-command} - **说明** 注册一个支持 `shell_like` 解析参数的命令消息事件响应器。 与普通的 `on_command` 不同的是,在添加 `parser` 参数时, 响应器会自动处理消息。 可以通过 [ShellCommandArgv](../params.md#ShellCommandArgv) 获取原始参数列表, 通过 [ShellCommandArgs](../params.md#ShellCommandArgs) 获取解析后的参数字典。 - **参数** - `cmd` (str | tuple[str, ...]): 指定命令内容 - `aliases` (set[str | tuple[str, ...]] | None): 命令别名 - `parser` ([ArgumentParser](../rule.md#ArgumentParser) | None): `nonebot.rule.ArgumentParser` 对象 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ### _method_ `on_regex(pattern, flags=..., *, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-regex} - **说明** 注册一个消息事件响应器,并且当消息匹配正则表达式时响应。 命令匹配规则参考: `正则匹配 `\_ - **参数** - `pattern` (str): 正则表达式 - `flags` (int | re.RegexFlag): 正则匹配标志 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ### _method_ `on_type(types, *, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-type} - **说明:** 注册一个事件响应器,并且当事件为指定类型时响应。 - **参数** - `types` (type[[Event](../adapters/index.md#Event)] | tuple[type[[Event](../adapters/index.md#Event)]]): 事件类型 - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 - `temp` (bool): 是否为临时事件响应器(仅执行一次) - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 - `priority` (int): 事件响应器优先级 - `block` (bool): 是否阻止事件向更低优先级传递 - `state` ([T_State](../typing.md#T-State) | None): 默认 state - **返回** - type[[Matcher](../matcher.md#Matcher)] ================================================ FILE: website/versioned_docs/version-2.4.4/api/rule.md ================================================ --- mdx: format: md sidebar_position: 5 description: nonebot.rule 模块 --- # nonebot.rule 本模块是 [Matcher.rule](matcher.md#Matcher-rule) 的类型定义。 每个[事件响应器](matcher.md#Matcher)拥有一个 [Rule](#Rule),其中是 `RuleChecker` 的集合。 只有当所有 `RuleChecker` 检查结果为 `True` 时继续运行。 ## _class_ `Rule(*checkers)` {#Rule} - **说明** 规则类。 当事件传递时,在 [Matcher](matcher.md#Matcher) 运行前进行检查。 - **参数** - `*checkers` ([T_RuleChecker](typing.md#T-RuleChecker) | [Dependent](dependencies/index.md#Dependent)[bool]): RuleChecker - **用法** ```python Rule(async_function) & sync_function # 等价于 Rule(async_function, sync_function) ``` ### _instance-var_ `checkers` {#Rule-checkers} - **类型:** set[[Dependent](dependencies/index.md#Dependent)[bool]] - **说明:** 存储 `RuleChecker` ### _async method_ `__call__(bot, event, state, stack=None, dependency_cache=None)` {#Rule---call--} - **说明:** 检查是否符合所有规则 - **参数** - `bot` ([Bot](adapters/index.md#Bot)): Bot 对象 - `event` ([Event](adapters/index.md#Event)): Event 对象 - `state` ([T_State](typing.md#T-State)): 当前 State - `stack` (AsyncExitStack | None): 异步上下文栈 - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None): 依赖缓存 - **返回** - bool ## _class_ `CMD_RESULT()` {#CMD-RESULT} - **参数** auto ## _class_ `TRIE_VALUE()` {#TRIE-VALUE} - **说明:** TRIE_VALUE(command_start, command) - **参数** auto ## _class_ `StartswithRule(msg, ignorecase=False)` {#StartswithRule} - **说明:** 检查消息纯文本是否以指定字符串开头。 - **参数** - `msg` (tuple[str, ...]): 指定消息开头字符串元组 - `ignorecase` (bool): 是否忽略大小写 ## _def_ `startswith(msg, ignorecase=False)` {#startswith} - **说明:** 匹配消息纯文本开头。 - **参数** - `msg` (str | tuple[str, ...]): 指定消息开头字符串元组 - `ignorecase` (bool): 是否忽略大小写 - **返回** - [Rule](#Rule) ## _class_ `EndswithRule(msg, ignorecase=False)` {#EndswithRule} - **说明:** 检查消息纯文本是否以指定字符串结尾。 - **参数** - `msg` (tuple[str, ...]): 指定消息结尾字符串元组 - `ignorecase` (bool): 是否忽略大小写 ## _def_ `endswith(msg, ignorecase=False)` {#endswith} - **说明:** 匹配消息纯文本结尾。 - **参数** - `msg` (str | tuple[str, ...]): 指定消息开头字符串元组 - `ignorecase` (bool): 是否忽略大小写 - **返回** - [Rule](#Rule) ## _class_ `FullmatchRule(msg, ignorecase=False)` {#FullmatchRule} - **说明:** 检查消息纯文本是否与指定字符串全匹配。 - **参数** - `msg` (tuple[str, ...]): 指定消息全匹配字符串元组 - `ignorecase` (bool): 是否忽略大小写 ## _def_ `fullmatch(msg, ignorecase=False)` {#fullmatch} - **说明:** 完全匹配消息。 - **参数** - `msg` (str | tuple[str, ...]): 指定消息全匹配字符串元组 - `ignorecase` (bool): 是否忽略大小写 - **返回** - [Rule](#Rule) ## _class_ `KeywordsRule(*keywords)` {#KeywordsRule} - **说明:** 检查消息纯文本是否包含指定关键字。 - **参数** - `*keywords` (str): 指定关键字元组 ## _def_ `keyword(*keywords)` {#keyword} - **说明:** 匹配消息纯文本关键词。 - **参数** - `*keywords` (str): 指定关键字元组 - **返回** - [Rule](#Rule) ## _class_ `CommandRule(cmds, force_whitespace=None)` {#CommandRule} - **说明:** 检查消息是否为指定命令。 - **参数** - `cmds` (list[tuple[str, ...]]): 指定命令元组列表 - `force_whitespace` (str | bool | None): 是否强制命令后必须有指定空白符 ## _def_ `command(*cmds, force_whitespace=None)` {#command} - **说明** 匹配消息命令。 根据配置里提供的 [`command_start`](config.md#Config-command-start), [`command_sep`](config.md#Config-command-sep) 判断消息是否为命令。 可以通过 [Command](params.md#Command) 获取匹配成功的命令(例: `("test",)`), 通过 [RawCommand](params.md#RawCommand) 获取匹配成功的原始命令文本(例: `"/test"`), 通过 [CommandArg](params.md#CommandArg) 获取匹配成功的命令参数。 - **参数** - `*cmds` (str | tuple[str, ...]): 命令文本或命令元组 - `force_whitespace` (str | bool | None): 是否强制命令后必须有指定空白符 - **返回** - [Rule](#Rule) - **用法** 使用默认 `command_start`, `command_sep` 配置情况下: 命令 `("test",)` 可以匹配: `/test` 开头的消息 命令 `("test", "sub")` 可以匹配: `/test.sub` 开头的消息 :::tip 提示 命令内容与后续消息间无需空格! ::: ## _class_ `ArgumentParser()` {#ArgumentParser} - **说明** `shell_like` 命令参数解析器,解析出错时不会退出程序。 支持 [Message](adapters/index.md#Message) 富文本解析。 - **参数** auto - **用法** 用法与 `argparse.ArgumentParser` 相同, 参考文档: [argparse](https://docs.python.org/3/library/argparse.html) ### _method_ `parse_known_args(args=None, namespace=None)` {#ArgumentParser-parse-known-args} - **重载** **1.** `(args=None, namespace=None) -> tuple[Namespace, list[str | MessageSegment]]` - **参数** - `args` (Sequence[str | [MessageSegment](adapters/index.md#MessageSegment)] | None) - `namespace` (None) - **返回** - tuple[Namespace, list[str | [MessageSegment](adapters/index.md#MessageSegment)]] **2.** `(args, namespace) -> tuple[T, list[str | MessageSegment]]` - **参数** - `args` (Sequence[str | [MessageSegment](adapters/index.md#MessageSegment)] | None) - `namespace` (T) - **返回** - tuple[T, list[str | [MessageSegment](adapters/index.md#MessageSegment)]] **3.** `(*, namespace) -> tuple[T, list[str | MessageSegment]]` - **参数** - `namespace` (T) - **返回** - tuple[T, list[str | [MessageSegment](adapters/index.md#MessageSegment)]] ## _class_ `ShellCommandRule(cmds, parser)` {#ShellCommandRule} - **说明:** 检查消息是否为指定 shell 命令。 - **参数** - `cmds` (list[tuple[str, ...]]): 指定命令元组列表 - `parser` (ArgumentParser | None): 可选参数解析器 ## _def_ `shell_command(*cmds, parser=None)` {#shell-command} - **说明** 匹配 `shell_like` 形式的消息命令。 根据配置里提供的 [`command_start`](config.md#Config-command-start), [`command_sep`](config.md#Config-command-sep) 判断消息是否为命令。 可以通过 [Command](params.md#Command) 获取匹配成功的命令 (例: `("test",)`), 通过 [RawCommand](params.md#RawCommand) 获取匹配成功的原始命令文本 (例: `"/test"`), 通过 [ShellCommandArgv](params.md#ShellCommandArgv) 获取解析前的参数列表 (例: `["arg", "-h"]`), 通过 [ShellCommandArgs](params.md#ShellCommandArgs) 获取解析后的参数字典 (例: `{"arg": "arg", "h": True}`)。 :::caution 警告 如果参数解析失败,则通过 [ShellCommandArgs](params.md#ShellCommandArgs) 获取的将是 [ParserExit](exception.md#ParserExit) 异常。 ::: - **参数** - `*cmds` (str | tuple[str, ...]): 命令文本或命令元组 - `parser` (ArgumentParser | None): [ArgumentParser](#ArgumentParser) 对象 - **返回** - [Rule](#Rule) - **用法** 使用默认 `command_start`, `command_sep` 配置,更多示例参考 [argparse](https://docs.python.org/3/library/argparse.html) 标准库文档。 ```python from nonebot.rule import ArgumentParser parser = ArgumentParser() parser.add_argument("-a", action="store_true") rule = shell_command("ls", parser=parser) ``` :::tip 提示 命令内容与后续消息间无需空格! ::: ## _class_ `RegexRule(regex, flags=0)` {#RegexRule} - **说明:** 检查消息字符串是否符合指定正则表达式。 - **参数** - `regex` (str): 正则表达式 - `flags` (int): 正则表达式标记 ## _def_ `regex(regex, flags=0)` {#regex} - **说明** 匹配符合正则表达式的消息字符串。 可以通过 [RegexStr](params.md#RegexStr) 获取匹配成功的字符串, 通过 [RegexGroup](params.md#RegexGroup) 获取匹配成功的 group 元组, 通过 [RegexDict](params.md#RegexDict) 获取匹配成功的 group 字典。 - **参数** - `regex` (str): 正则表达式 - `flags` (int | re.RegexFlag): 正则表达式标记 - **返回** - [Rule](#Rule) :::tip 提示 正则表达式匹配使用 search 而非 match,如需从头匹配请使用 `r"^xxx"` 来确保匹配开头 ::: :::tip 提示 正则表达式匹配使用 `EventMessage` 的 `str` 字符串, 而非 `EventMessage` 的 `PlainText` 纯文本字符串 ::: ## _class_ `ToMeRule()` {#ToMeRule} - **说明:** 检查事件是否与机器人有关。 - **参数** auto ## _def_ `to_me()` {#to-me} - **说明:** 匹配与机器人有关的事件。 - **参数** empty - **返回** - [Rule](#Rule) ## _class_ `IsTypeRule(*types)` {#IsTypeRule} - **说明:** 检查事件类型是否为指定类型。 - **参数** - `*types` (type[[Event](adapters/index.md#Event)]) ## _def_ `is_type(*types)` {#is-type} - **说明:** 匹配事件类型。 - **参数** - `*types` (type[[Event](adapters/index.md#Event)]): 事件类型 - **返回** - [Rule](#Rule) ================================================ FILE: website/versioned_docs/version-2.4.4/api/typing.md ================================================ --- mdx: format: md sidebar_position: 11 description: nonebot.typing 模块 --- # nonebot.typing 本模块定义了 NoneBot 模块中共享的一些类型。 使用 Python 的 Type Hint 语法, 参考 [`PEP 484`](https://www.python.org/dev/peps/pep-0484/), [`PEP 526`](https://www.python.org/dev/peps/pep-0526/) 和 [`typing`](https://docs.python.org/3/library/typing.html)。 ## _def_ `overrides(InterfaceClass)` {#overrides} - **说明:** 标记一个方法为父类 interface 的 implement - **参数** - `InterfaceClass` (object) - **返回** - untyped ## _def_ `type_has_args(type_)` {#type-has-args} - **参数** - `type_` (type[Any]) - **返回** - bool ## _def_ `origin_is_union(origin)` {#origin-is-union} - **参数** - `origin` (type[Any] | None) - **返回** - bool ## _def_ `origin_is_literal(origin)` {#origin-is-literal} - **说明:** 判断是否是 Literal 类型 - **参数** - `origin` (type[Any] | None) - **返回** - bool ## _def_ `all_literal_values(type_)` {#all-literal-values} - **说明:** 获取 Literal 类型包含的所有值 - **参数** - `type_` (type[Any]) - **返回** - list[Any] ## _def_ `origin_is_annotated(origin)` {#origin-is-annotated} - **说明:** 判断是否是 Annotated 类型 - **参数** - `origin` (type[Any] | None) - **返回** - bool ## _def_ `is_none_type(type_)` {#is-none-type} - **说明:** 判断是否是 None 类型 - **参数** - `type_` (type[Any]) - **返回** - bool ## _def_ `is_type_alias_type(type_)` {#is-type-alias-type} - **参数** - `type_` (type[Any]) - **返回** - bool ## _def_ `evaluate_forwardref(ref, globalns, localns)` {#evaluate-forwardref} - **参数** - `ref` (ForwardRef) - `globalns` (dict[str, Any]) - `localns` (dict[str, Any]) - **返回** - Any ## _class_ `StateFlag()` {#StateFlag} - **参数** auto ## _var_ `T_State` {#T-State} - **类型:** dict[Any, Any] - **说明:** 事件处理状态 State 类型 ## _var_ `T_BotConnectionHook` {#T-BotConnectionHook} - **类型:** \_DependentCallable[Any] - **说明** Bot 连接建立时钩子函数 依赖参数: - DependParam: 子依赖参数 - BotParam: Bot 对象 - DefaultParam: 带有默认值的参数 ## _var_ `T_BotDisconnectionHook` {#T-BotDisconnectionHook} - **类型:** \_DependentCallable[Any] - **说明** Bot 连接断开时钩子函数 依赖参数: - DependParam: 子依赖参数 - BotParam: Bot 对象 - DefaultParam: 带有默认值的参数 ## _var_ `T_CallingAPIHook` {#T-CallingAPIHook} - **类型:** ([Bot](adapters/index.md#Bot), str, dict[str, Any]) -> Awaitable[Any] - **说明:** `bot.call_api` 钩子函数 ## _var_ `T_CalledAPIHook` {#T-CalledAPIHook} - **类型:** ([Bot](adapters/index.md#Bot), Exception | None, str, dict[str, Any], Any) -> Awaitable[Any] - **说明:** `bot.call_api` 后执行的函数,参数分别为 bot, exception, api, data, result ## _var_ `T_EventPreProcessor` {#T-EventPreProcessor} - **类型:** \_DependentCallable[Any] - **说明** 事件预处理函数 EventPreProcessor 类型 依赖参数: - DependParam: 子依赖参数 - BotParam: Bot 对象 - EventParam: Event 对象 - StateParam: State 对象 - DefaultParam: 带有默认值的参数 ## _var_ `T_EventPostProcessor` {#T-EventPostProcessor} - **类型:** \_DependentCallable[Any] - **说明** 事件后处理函数 EventPostProcessor 类型 依赖参数: - DependParam: 子依赖参数 - BotParam: Bot 对象 - EventParam: Event 对象 - StateParam: State 对象 - DefaultParam: 带有默认值的参数 ## _var_ `T_RunPreProcessor` {#T-RunPreProcessor} - **类型:** \_DependentCallable[Any] - **说明** 事件响应器运行前预处理函数 RunPreProcessor 类型 依赖参数: - DependParam: 子依赖参数 - BotParam: Bot 对象 - EventParam: Event 对象 - StateParam: State 对象 - MatcherParam: Matcher 对象 - DefaultParam: 带有默认值的参数 ## _var_ `T_RunPostProcessor` {#T-RunPostProcessor} - **类型:** \_DependentCallable[Any] - **说明** 事件响应器运行后后处理函数 RunPostProcessor 类型 依赖参数: - DependParam: 子依赖参数 - BotParam: Bot 对象 - EventParam: Event 对象 - StateParam: State 对象 - MatcherParam: Matcher 对象 - ExceptionParam: 异常对象(可能为 None) - DefaultParam: 带有默认值的参数 ## _var_ `T_RuleChecker` {#T-RuleChecker} - **类型:** \_DependentCallable[bool] - **说明** RuleChecker 即判断是否响应事件的处理函数。 依赖参数: - DependParam: 子依赖参数 - BotParam: Bot 对象 - EventParam: Event 对象 - StateParam: State 对象 - DefaultParam: 带有默认值的参数 ## _var_ `T_PermissionChecker` {#T-PermissionChecker} - **类型:** \_DependentCallable[bool] - **说明** PermissionChecker 即判断事件是否满足权限的处理函数。 依赖参数: - DependParam: 子依赖参数 - BotParam: Bot 对象 - EventParam: Event 对象 - DefaultParam: 带有默认值的参数 ## _var_ `T_Handler` {#T-Handler} - **类型:** \_DependentCallable[Any] - **说明:** Handler 处理函数。 ## _var_ `T_TypeUpdater` {#T-TypeUpdater} - **类型:** \_DependentCallable[str] - **说明** TypeUpdater 在 Matcher.pause, Matcher.reject 时被运行,用于更新响应的事件类型。 默认会更新为 `message`。 依赖参数: - DependParam: 子依赖参数 - BotParam: Bot 对象 - EventParam: Event 对象 - StateParam: State 对象 - MatcherParam: Matcher 对象 - DefaultParam: 带有默认值的参数 ## _var_ `T_PermissionUpdater` {#T-PermissionUpdater} - **类型:** \_DependentCallable[[Permission](permission.md#Permission)] - **说明** PermissionUpdater 在 Matcher.pause, Matcher.reject 时被运行,用于更新会话对象权限。 默认会更新为当前事件的触发对象。 依赖参数: - DependParam: 子依赖参数 - BotParam: Bot 对象 - EventParam: Event 对象 - StateParam: State 对象 - MatcherParam: Matcher 对象 - DefaultParam: 带有默认值的参数 ## _var_ `T_DependencyCache` {#T-DependencyCache} - **类型:** dict[\_DependentCallable[Any], DependencyCache] - **说明:** 依赖缓存, 用于存储依赖函数的返回值 ================================================ FILE: website/versioned_docs/version-2.4.4/api/utils.md ================================================ --- mdx: format: md sidebar_position: 8 description: nonebot.utils 模块 --- # nonebot.utils 本模块包含了 NoneBot 的一些工具函数 ## _def_ `escape_tag(s)` {#escape-tag} - **说明** 用于记录带颜色日志时转义 `` 类型特殊标签 参考: [loguru color 标签](https://loguru.readthedocs.io/en/stable/api/logger.html#color) - **参数** - `s` (str): 需要转义的字符串 - **返回** - str ## _def_ `deep_update(mapping, *updating_mappings)` {#deep-update} - **说明:** 深度更新合并字典 - **参数** - `mapping` (dict[K, Any]) - `*updating_mappings` (dict[K, Any]) - **返回** - dict[K, Any] ## _def_ `lenient_issubclass(cls, class_or_tuple)` {#lenient-issubclass} - **说明:** 检查 cls 是否是 class_or_tuple 中的一个类型子类并忽略类型错误。 - **参数** - `cls` (Any) - `class_or_tuple` (type[Any] | tuple[type[Any], ...]) - **返回** - bool ## _def_ `generic_check_issubclass(cls, class_or_tuple)` {#generic-check-issubclass} - **说明** 检查 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.List`、`typing.Dict` 等泛型类型, 则会检查其原始类型是否是 class_or_tuple 中一个类型的子类。 - **参数** - `cls` (Any) - `class_or_tuple` (type[Any] | tuple[type[Any], ...]) - **返回** - bool ## _def_ `type_is_complex(type_)` {#type-is-complex} - **说明:** 检查 type\_ 是否是复杂类型 - **参数** - `type_` (type[Any]) - **返回** - bool ## _def_ `is_coroutine_callable(call)` {#is-coroutine-callable} - **说明:** 检查 call 是否是一个 callable 协程函数 - **参数** - `call` ((...) -> Any) - **返回** - bool ## _def_ `is_gen_callable(call)` {#is-gen-callable} - **说明:** 检查 call 是否是一个生成器函数 - **参数** - `call` ((...) -> Any) - **返回** - bool ## _def_ `is_async_gen_callable(call)` {#is-async-gen-callable} - **说明:** 检查 call 是否是一个异步生成器函数 - **参数** - `call` ((...) -> Any) - **返回** - bool ## _def_ `run_sync(call)` {#run-sync} - **说明:** 一个用于包装 sync function 为 async function 的装饰器 - **参数** - `call` ((P) -> R): 被装饰的同步函数 - **返回** - (P) -> Coroutine[None, None, R] ## _def_ `run_sync_ctx_manager(cm)` {#run-sync-ctx-manager} - **说明:** 一个用于包装 sync context manager 为 async context manager 的执行函数 - **参数** - `cm` (AbstractContextManager[T]) - **返回** - AsyncGenerator[T, None] ## _async def_ `run_coro_with_catch(coro, exc, return_on_err=None)` {#run-coro-with-catch} - **说明:** 运行协程并当遇到指定异常时返回指定值。 - **重载** **1.** `(coro, exc, return_on_err=None) -> T | None` - **参数** - `coro` (Coroutine[Any, Any, T]) - `exc` (tuple[type[Exception], ...]) - `return_on_err` (None) - **返回** - T | None **2.** `(coro, exc, return_on_err) -> T | R` - **参数** - `coro` (Coroutine[Any, Any, T]) - `exc` (tuple[type[Exception], ...]) - `return_on_err` (R) - **返回** - T | R - **参数** - `coro`: 要运行的协程 - `exc`: 要捕获的异常 - `return_on_err`: 当发生异常时返回的值 - **返回** 协程的返回值或发生异常时的指定值 ## _async def_ `run_coro_with_shield(coro)` {#run-coro-with-shield} - **说明:** 运行协程并在取消时屏蔽取消异常。 - **参数** - `coro` (Coroutine[Any, Any, T]): 要运行的协程 - **返回** - T: 协程的返回值 ## _def_ `flatten_exception_group(exc_group)` {#flatten-exception-group} - **参数** - `exc_group` (BaseExceptionGroup[E]) - **返回** - Generator[E, None, None] ## _def_ `get_name(obj)` {#get-name} - **说明:** 获取对象的名称 - **参数** - `obj` (Any) - **返回** - str ## _def_ `path_to_module_name(path)` {#path-to-module-name} - **说明:** 转换路径为模块名 - **参数** - `path` (Path) - **返回** - str ## _def_ `resolve_dot_notation(obj_str, default_attr, default_prefix=None)` {#resolve-dot-notation} - **说明:** 解析并导入点分表示法的对象 - **参数** - `obj_str` (str) - `default_attr` (str) - `default_prefix` (str | None) - **返回** - Any ## _class_ `classproperty(func)` {#classproperty} - **说明:** 类属性装饰器 - **参数** - `func` ((Any) -> T) ## _class_ `DataclassEncoder()` {#DataclassEncoder} - **说明:** 可以序列化 [Message](adapters/index.md#Message)(List[Dataclass]) 的 `JSONEncoder` - **参数** auto ### _method_ `default(o)` {#DataclassEncoder-default} - **参数** - `o` - **返回** - untyped ## _def_ `logger_wrapper(logger_name)` {#logger-wrapper} - **说明:** 用于打印 adapter 的日志。 - **参数** - `logger_name` (str): adapter 的名称 - **返回** - untyped: 日志记录函数 日志记录函数的参数: - level: 日志等级 - message: 日志信息 - exception: 异常信息 ================================================ FILE: website/versioned_docs/version-2.4.4/appendices/api-calling.mdx ================================================ --- sidebar_position: 4 description: 使用平台接口,完成更多功能 options: menu: - category: appendices weight: 50 --- # 使用平台接口 import Messenger from "@/components/Messenger"; 在 NoneBot 中,除了使用事件响应器操作发送文本消息外,我们还可以直接通过使用协议适配器提供的方法来使用平台特定的接口,完成发送特殊消息、获取信息等其他平台提供的功能。同时,在部分无法使用事件响应器的情况中,例如[定时任务](../best-practice/scheduler.md),我们也可以使用平台接口来完成需要的功能。 ## 发送平台特殊消息 在之前的章节中,我们介绍了如何向用户发送文本消息以及[如何处理平台消息](../tutorial/message.md),现在我们来向用户发送平台特殊消息。 :::caution 注意 在以下的示例中,我们将使用 `Console` 协议适配器来演示如何发送平台消息。在实际使用中,你需要确保你使用的**消息序列类型**与你所要发送的**平台类型**一致。 ::: ```python {4,7-17} title=weather/__init__.py import inspect from nonebot.adapters.console import MessageSegment @weather.got("location", prompt=MessageSegment.emoji("question") + "请输入地名") async def got_location(location: str = ArgPlainText()): result = await weather.send( MessageSegment.markdown( inspect.cleandoc( f""" # {location} - 今天 ⛅ 多云 20℃~24℃ """ ) ) ) ``` 在上面的示例中,我们使用了 `Console` 协议适配器提供的 `MessageSegment` 类来发送平台特定的消息 `emoji` 和 `markdown`。这两种消息可以显示在终端中,但是无法在其他平台上使用。在事件响应器操作中,我们可以使用 `str`、消息序列、消息段、消息模板四种类型来发送消息,但其中只有 `str` 和[纯文本形式的消息模板类型](../tutorial/message.md#使用消息模板)消息可以在所有平台上使用。 `send` 事件响应器操作实际上是由协议适配器通过调用平台 API 来实现的,通常会将 API 调用的结果作为返回值返回。 ## 调用平台 API 在 NoneBot 中,我们可以通过 `Bot` 对象来调用协议适配器支持的平台 API,来完成更多的功能。 ### 获取 Bot 在调用平台 API 之前,我们首先要获得 Bot 对象。有两种方式可以获得 Bot 对象。 在事件处理流程的上下文中,我们可以直接使用依赖注入 Bot 来获取: ```python {1,4} title=weather/__init__.py from nonebot.adapters import Bot @weather.got("location", prompt="请输入地名") async def got_location(bot: Bot, location: str = ArgPlainText()): ... ``` 依赖注入会确保你获得的 Bot 对象与类型注解的 Bot 类型一致。也就是说,如果你使用的是 Bot 基类,将会允许任何平台的 Bot 对象;如果你使用的是平台特定的 Bot 类型,将会只允许该平台的 Bot 对象,其他类型的 Bot 将会跳过这个事件处理函数。更多详情请参考[事件处理重载](./overload.md)。 在其他情况下,我们可以通过 NoneBot 提供的方法来获取 Bot 对象,这些方法将会在[使用适配器](../advanced/adapter.md#获取-bot-对象)中详细介绍: ```python {4,6} from nonebot import get_bot # 获取当前所有 Bot 中的第一个 bot = get_bot() # 获取指定 ID 的 Bot bot = get_bot("bot_id") ``` ### 调用 API 在获得 Bot 对象后,我们可以通过 Bot 的实例方法来调用平台 API: ```python {2,5} # 通过 bot.api_name(**kwargs) 的方法调用 API result = await bot.get_user_info(user_id=12345678) # 通过 bot.call_api(api_name, **kwargs) 的方法调用 API result = await bot.call_api("get_user_info", user_id=12345678) ``` :::caution 注意 实际可以使用的 API 以及参数取决于平台提供的接口以及协议适配器的实现,请参考协议适配器以及平台文档。 ::: 在了解了如何调用 API 后,我们可以来改进 `weather` 插件,使得消息发送后,调用 `Console` 接口响铃提醒机器人用户: ```python {4,18} title=weather/__init__.py from nonebot.adapters.console import Bot, MessageSegment @weather.got("location", prompt=MessageSegment.emoji("question") + "请输入地名") async def got_location(bot: Bot, location: str = ArgPlainText()): await weather.send( MessageSegment.markdown( inspect.cleandoc( f""" # {location} - 今天 ⛅ 多云 20℃~24℃ """ ) ) ) await bot.bell() ``` ================================================ FILE: website/versioned_docs/version-2.4.4/appendices/config.mdx ================================================ --- sidebar_position: 0 description: 读取用户配置来控制插件行为 options: menu: - category: appendices weight: 10 --- # 配置 import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; 配置是项目中非常重要的一部分,为了方便我们控制机器人的行为,NoneBot 提供了一套配置系统。下面我们将会补充[指南](../quick-start.mdx)中的天气插件,使其能够读取用户配置。在这之前,我们需要先了解一下配置系统,如果你已经了解了 NoneBot 中的配置方法,可以跳转到[编写插件配置](#插件配置)。 NoneBot 使用 [`pydantic`](https://docs.pydantic.dev/) 以及 [`python-dotenv`](https://saurabh-kumar.com/python-dotenv/) 来读取 dotenv 配置文件以及环境变量,从而控制机器人行为。配置文件需要符合 dotenv 格式,复杂数据类型需使用 JSON 格式或 [pydantic 支持格式](https://docs.pydantic.dev/usage/types/)填写。 NoneBot 内置的配置项列表及含义可以在[内置配置项](#内置配置项)中查看。 :::caution 注意 NoneBot 自 2.2.0 起兼容了 Pydantic v1 与 v2 版本,以下文档中 Pydantic 相关示例均采用 v2 版本用法。 如果在使用商店或其他第三方插件的过程中遇到 Pydantic 相关警告或报错,例如: ```python pydantic_core._pydantic_core.ValidationError: 1 validation error for Config Input should be a valid dictionary or instance of Config [type=model_type, input_value=Config(...), input_type=Config] ``` 请考虑降级 Pydantic 至 v1 版本: ```bash pip install --force-reinstall 'pydantic~=1.10' ``` ::: ## 配置项的加载 在 NoneBot 中,我们可以把配置途径分为 **直接传入**、**系统环境变量**、**dotenv 配置文件** 三种,其加载优先级依次由高到低。 ### 直接传入 在 NoneBot 初始化的过程中,可以通过 `nonebot.init()` 传入任意合法的 Python 变量,也可以在初始化完成后直接赋值。 通常,在初始化前的传参会在机器人的入口文件(如 `bot.py`)中进行,而初始化后的赋值可以在任何地方进行。 ```python {4,8,9} title=bot.py import nonebot # 初始化时 nonebot.init(custom_config1="config on init") # 初始化后 config = nonebot.get_driver().config config.custom_config1 = "changed after init" config.custom_config2 = "new config after init" ``` ### 系统环境变量 在 dotenv 配置文件中定义的配置项,也会在环境变量中进行寻找。如果在环境变量中发现同名配置项(大小写不敏感),将会覆盖 dotenv 中所填值。 例如,在 dotenv 配置文件中存在配置项 `custom_config`: ```dotenv CUSTOM_CONFIG=config in dotenv ``` 同时,设置环境变量: ```bash # windows cmd set CUSTOM_CONFIG 'config in environment variables' # windows powershell $Env:CUSTOM_CONFIG='config in environment variables' # linux/macOS export CUSTOM_CONFIG='config in environment variables' ``` 那最终 NoneBot 所读取的内容为环境变量中的内容,即 `config in environment variables`。 :::caution 注意 如果一个环境变量既不是 NoneBot 的[**内置配置项**](#内置配置项),也不是任何插件所定义的[**插件配置**](#插件配置),那么 NoneBot 不会自发读取该环境变量,需要在 dotenv 配置文件中先行声明。 ::: ### dotenv 配置文件 dotenv 是一种便捷的跨平台配置通用模式,也是我们推荐的配置方式。 NoneBot 在启动时将会从系统环境变量或者 `.env` 文件中寻找配置项 `ENVIRONMENT` (大小写不敏感),默认值为 `prod`。这将决定 NoneBot 后续进一步加载环境配置的文件路径 `.env.{ENVIRONMENT}`。 #### 配置项解析 dotenv 文件中的配置值使用 JSON 进行解析。如果配置项值无法被解析,将作为**字符串**处理。例如: ```dotenv STRING_CONFIG=some string LIST_CONFIG=[1, 2, 3] DICT_CONFIG={"key": "value"} MULTILINE_CONFIG=' [ { "item_key": "item_value" } ] ' EMPTY_CONFIG= NULL_CONFIG ``` 将被解析为: ```python dotenv_config = { "string_config": "some string", "list_config": [1, 2, 3], "dict_config": {"key": "value"}, "multiline_config": [{"item_key": "item_value"}], "empty_config": "", "null_config": None } ``` 特别的,NoneBot 支持使用 `env_nested_delimiter` 配置嵌套字典,在层与层之间使用 `__` 分隔即可: ```dotenv DICT={"k1": "v1", "k2": null} DICT__K2=v2 DICT__K3=v3 DICT__INNER__K4=v4 ``` 将被解析为: ```python dotenv_config = { "dict": { "k1": "v1", "k2": "v2", "k3": "v3", "inner": { "k4": "v4" } } } ``` #### .env 文件 `.env` 文件是基础配置文件,该文件中的配置项在不同环境下都会被加载,但会被 `.env.{ENVIRONMENT}` 文件中的配置所**覆盖**。 我们可以在 `.env` 文件中写入当前的环境信息: ```dotenv ENVIRONMENT=dev COMMON_CONFIG=common config # 这个配置项在任何环境中都会被加载 ``` 这样,我们在启动 NoneBot 时就会从 `.env.dev` 文件中加载剩余配置项。 :::tip 提示 在生产环境中,可以通过设置环境变量 `ENVIRONMENT=prod` 来确保 NoneBot 读取正确的环境配置。 ::: #### .env.\{ENVIRONMENT\} 文件 `.env.{ENVIRONMENT}` 文件类似于预设,可以让我们在多套不同的配置方案中灵活切换,默认 NoneBot 会读取 `.env.prod` 配置。如果你使用了 `nb-cli` 创建 `simple` 项目,那么将含有两套预设配置:`.env.dev` 和 `.env.prod`。 在 NoneBot 初始化时,可以指定加载某个环境配置文件: ```python nonebot.init(_env_file=".env.dev") ``` 这将忽略在 `.env` 文件或环境变量中指定的 `ENVIRONMENT` 配置项。 ## 读取全局配置项 NoneBot 的全局配置对象可以通过 `driver` 获取,如: ```python import nonebot config = nonebot.get_driver().config ``` 如果我们需要获取某个配置项,可以直接通过 `config` 对象的属性访问: ```python superusers = config.superusers ``` 如果配置项不存在,将会抛出异常。 ## 插件配置 在一个涉及大量配置项的项目中,通过直接读取全局配置项的方式显然并不高效。同时,由于额外的全局配置项没有预先定义,开发时编辑器将无法提示字段与类型,并且运行时没有对配置项直接进行合法性检查。那么就需要一种方式来规范定义插件配置项。 在 NoneBot 中,我们使用强大高效的 `pydantic` 来定义配置模型,这个模型可以被用于配置的读取和类型检查等。例如在 `weather` 插件目录中新建 `config.py` 来定义一个模型: ```python title=weather/config.py from pydantic import BaseModel, field_validator class Config(BaseModel): weather_api_key: str weather_command_priority: int = 10 weather_plugin_enabled: bool = True @field_validator("weather_command_priority") @classmethod def check_priority(cls, v: int) -> int: if v >= 1: return v raise ValueError("weather command priority must greater than 1") ``` 在 `config.py` 中,我们定义了一个 `Config` 类,它继承自 `pydantic.BaseModel`,并定义了一些配置项。在 `Config` 类中,我们还定义了一个 `check_priority` 方法,它用于检查 `weather_command_priority` 配置项的合法性。更多关于 `pydantic` 的编写方式,可以参考 [pydantic 官方文档](https://docs.pydantic.dev/)。 在定义好配置模型后,我们可以在插件加载时通过配置模型获取插件配置: ```python {5,11} title=weather/__init__.py from nonebot import get_plugin_config from .config import Config plugin_config = get_plugin_config(Config) weather = on_command( "天气", rule=to_me(), aliases={"weather", "查天气"}, priority=plugin_config.weather_command_priority, block=True, ) ``` 然后,我们便可以从 `plugin_config` 中读取配置了,例如 `plugin_config.weather_api_key`。 这种方式可以简洁、高效地读取配置项,同时也可以设置默认值或者在运行时对配置项进行合法性检查,防止由于配置项导致的插件出错等情况出现。 :::tip 可配置的事件响应优先级 发布插件应该为自身的事件响应器提供可配置的优先级,以便插件使用者可以自定义多个插件间的响应顺序。 ::: :::tip 插件配置获取逻辑 无论是否在 dotenv 文件中声明了插件配置项,使用 `get_plugin_config` 获取插件配置模型中定义的配置项时都遵循[**配置项的加载**](#配置项的加载)一节中的优先级顺序进行读取。 ::: ### 避免插件配置名称冲突 由于插件配置项是从全局配置和环境变量中读取的,通常我们需要在配置项名称前面添加前缀名,以防止配置项冲突。例如在上方的示例中,我们就添加了配置项前缀 `weather_`。但是这样会导致使用配置项时变量名过长,此时我们可以使用 `pydantic` 的 `alias` 或者通过配置 scope 来简化配置项名称。这里我们以 scope 配置为例: ```python title=weather/config.py from pydantic import BaseModel class ScopedConfig(BaseModel): api_key: str command_priority: int = 10 plugin_enabled: bool = True class Config(BaseModel): weather: ScopedConfig ``` ```python title=weather/__init__.py from nonebot import get_plugin_config from .config import Config plugin_config = get_plugin_config(Config).weather ``` 这样我们就可以省略插件配置项名称中的前缀 `weather_` 了。但需要注意的是,如果我们使用了 scope 配置,那么在配置文件中也需要使用 [`env_nested_delimiter` 格式](#配置项解析),例如: ```dotenv WEATHER__API_KEY=123456 WEATHER__COMMAND_PRIORITY=10 ``` ## 内置配置项 配置项 API 文档可以前往 [Config 类](../api/config.md#Config)查看。 ### Driver - **类型**: `str` - **默认值**: `"~fastapi"` NoneBot 运行所使用的驱动器。具体配置方法可以参考[安装驱动器](../tutorial/store.mdx#安装驱动器)和[选择驱动器](../advanced/driver.md)。 ```dotenv DRIVER=~fastapi+~httpx+~websockets ``` ```bash # windows cmd set DRIVER '~fastapi+~httpx+~websockets' # windows powershell $Env:DRIVER='~fastapi+~httpx+~websockets' # linux/macOS export DRIVER='~fastapi+~httpx+~websockets' ``` ```python title=bot.py import nonebot nonebot.init(driver="~fastapi+~httpx+~websockets") ``` ### Host - **类型**: `IPvAnyAddress` - **默认值**: `127.0.0.1` 当 NoneBot 作为服务端时,监听的 IP / 主机名。 ```dotenv HOST=127.0.0.1 ``` ```bash # windows cmd set HOST '127.0.0.1' # windows powershell $Env:HOST='127.0.0.1' # linux/macOS export HOST='127.0.0.1' ``` ```python title=bot.py import nonebot nonebot.init(host="127.0.0.1") ``` ### Port - **类型**: `int` (1 ~ 65535) - **默认值**: `8080` 当 NoneBot 作为服务端时,监听的端口。 ```dotenv PORT=8080 ``` ```bash # windows cmd set PORT '8080' # windows powershell $Env:PORT='8080' # linux/macOS export PORT='8080' ``` ```python title=bot.py import nonebot nonebot.init(port=8080) ``` ### Log Level - **类型**: `int | str` - **默认值**: `INFO` NoneBot 日志输出等级,可以为 `int` 类型等级或等级名称。具体等级对照表参考 [loguru 日志等级](https://loguru.readthedocs.io/en/stable/api/logger.html#levels)。 :::tip 提示 日志等级名称应为大写,如 `INFO`。 ::: ```dotenv LOG_LEVEL=DEBUG ``` ```bash # windows cmd set LOG_LEVEL 'DEBUG' # windows powershell $Env:LOG_LEVEL='DEBUG' # linux/macOS export LOG_LEVEL='DEBUG' ``` ```python title=bot.py import nonebot nonebot.init(log_level="DEBUG") ``` ### API Timeout - **类型**: `float | None` - **默认值**: `30.0` 调用平台接口的超时时间,单位为秒。`None` 表示不设置超时时间。 ```dotenv API_TIMEOUT=10.0 ``` ```bash # windows cmd set API_TIMEOUT '10.0' # windows powershell $Env:API_TIMEOUT='10.0' # linux/macOS export API_TIMEOUT='10.0' ``` ```python title=bot.py import nonebot nonebot.init(api_timeout=10.0) ``` ### SuperUsers - **类型**: `set[str]` - **默认值**: `set()` 机器人超级用户,可以使用权限 [`SUPERUSER`](../api/permission.md#SUPERUSER)。 ```dotenv SUPERUSERS=["123123123"] ``` ```bash # windows cmd set SUPERUSERS '["123123123"]' # windows powershell $Env:SUPERUSERS='["123123123"]' # linux/macOS export SUPERUSERS='["123123123"]' ``` ```python title=bot.py import nonebot nonebot.init(superusers={"123123123"}) ``` ### Nickname - **类型**: `set[str]` - **默认值**: `set()` 机器人昵称,通常协议适配器会根据用户是否 @bot 或者是否以机器人昵称开头来判断是否是向机器人发送的消息。 ```dotenv NICKNAME=["bot"] ``` ```bash # windows cmd set NICKNAME '["bot"]' # windows powershell $Env:NICKNAME='["bot"]' # linux/macOS export NICKNAME='["bot"]' ``` ```python title=bot.py import nonebot nonebot.init(nickname={"bot"}) ``` ### Command Start 和 Command Separator - **类型**: `set[str]` - **默认值**: - Command Start: `{"/"}` - Command Separator: `{"."}` 命令消息的起始符和分隔符。用于 [`command`](../advanced/matcher.md#command) 规则。 ```dotenv COMMAND_START=["/", ""] COMMAND_SEP=[".", " "] ``` ```bash # windows cmd set COMMAND_START '["/", ""]' set COMMAND_SEP '[".", " "]' # windows powershell $Env:COMMAND_START='["/", ""]' $Env:COMMAND_SEP='[".", " "]' # linux/macOS export COMMAND_START='["/", ""]' export COMMAND_SEP='[".", " "]' ``` ```python title=bot.py import nonebot nonebot.init(command_start={"/", ""}, command_sep={".", " "}) ``` ### Session Expire Timeout - **类型**: `timedelta` - **默认值**: `timedelta(minutes=2)` 用户会话超时时间,配置格式参考 [Datetime Types](https://docs.pydantic.dev/latest/api/standard_library_types/#datetimetimedelta)。 ```dotenv SESSION_EXPIRE_TIMEOUT=00:02:00 ``` ```bash # windows cmd set SESSION_EXPIRE_TIMEOUT '00:02:00' # windows powershell $Env:SESSION_EXPIRE_TIMEOUT='00:02:00' # linux/macOS export SESSION_EXPIRE_TIMEOUT='00:02:00' ``` ```python title=bot.py import nonebot nonebot.init(session_expire_timeout=120) ``` ================================================ FILE: website/versioned_docs/version-2.4.4/appendices/log.md ================================================ --- sidebar_position: 6 description: 记录与控制日志 options: menu: - category: appendices weight: 70 --- # 日志 无论是在开发还是在生产环境中,日志都是一个重要的功能,可以帮助我们了解运行状况、排查问题等。虽然我们可以使用 `print` 来将需要的信息输出到控制台,但是这种方式难以控制,而且不利于日志的归档、分析等。NoneBot 使用优秀的 [Loguru](https://loguru.readthedocs.io/) 库来进行日志记录。 ## 记录日志 我们可以从 NoneBot 中导入 `logger` 对象,然后使用 `logger` 对象的方法来记录日志。 ```python from nonebot import logger logger.trace("This is a trace message") logger.debug("This is a debug message") logger.info("This is an info message") logger.success("This is a success message") logger.warning("This is a warning message") logger.error("This is an error message") logger.critical("This is a critical message") ``` 我们仅需一行代码即可记录对应级别的日志。日志可以通过配置 [`LOG_LEVEL` 配置项](./config.mdx#log-level)来过滤输出等级,控制台中仅会输出大于等于 `LOG_LEVEL` 的日志。默认的 `LOG_LEVEL` 为 `INFO`,即只会输出 `INFO`、`SUCCESS`、`WARNING`、`ERROR`、`CRITICAL` 级别的日志。 如果需要记录 `Exception traceback` 日志,可以向 `logger` 添加 `exception` 选项: ```python {4} try: 1 / 0 except ZeroDivisionError: logger.opt(exception=True).error("ZeroDivisionError") ``` 如果需要输出彩色日志,可以向 `logger` 添加 `colors` 选项: ```python logger.opt(colors=True).warning("We got a BIG problem") ``` 更多日志记录方法请参考 [Loguru 文档](https://loguru.readthedocs.io/)。 ## 自定义日志输出 NoneBot 在启动时会添加一个默认的日志处理器,该处理器会将日志输出到**stdout**,并且根据 `LOG_LEVEL` 配置项过滤日志等级。 默认的日志格式为: ```text {time:MM-DD HH:mm:ss} [{level}] {name} | {message} ``` 我们可以从 `nonebot.log` 模块导入以使用 NoneBot 的默认格式和过滤器: ```python from nonebot.log import default_format, default_filter ``` 如果需要自定义日志格式,我们需要移除 NoneBot 默认的日志处理器并添加新的日志处理器。例如,在机器人入口文件中 `nonebot.init` 之前添加以下内容: ```python title=bot.py from nonebot.log import logger_id # 移除 NoneBot 默认的日志处理器 logger.remove(logger_id) # 添加新的日志处理器 logger.add( sys.stdout, level=0, diagnose=True, format="{time:MM-DD HH:mm:ss} [{level}] {name} | {message}", filter=default_filter ) ``` 如果想要输出日志到文件,我们可以使用 `logger.add` 方法添加文件处理器: ```python title=bot.py logger.add("error.log", level="ERROR", format=default_format, rotation="1 week") ``` 更多日志处理器的使用方法请参考 [Loguru 文档](https://loguru.readthedocs.io/)。 ## 重定向 logging 日志 `logging` 是 Python 标准库中的日志模块,NoneBot 提供了一个 logging handler 用于将 `logging` 日志重定向到 `loguru` 处理。 ```python from nonebot.log import LoguruHandler # root logger 添加 LoguruHandler logging.basicConfig(handlers=[LoguruHandler()]) # 或者为其他 logging.Logger 添加 LoguruHandler logger.addHandler(LoguruHandler()) ``` ================================================ FILE: website/versioned_docs/version-2.4.4/appendices/overload.md ================================================ --- sidebar_position: 7 description: 根据事件类型进行不同的处理 options: menu: - category: appendices weight: 80 --- # 事件类型与重载 在之前的示例中,我们已经了解了如何[获取事件信息](../tutorial/event-data.mdx)以及[使用平台接口](./api-calling.mdx)。但是,事件信息通常不仅仅包含消息这一个内容,还有其他平台提供的信息,例如消息发送时间、消息发送者等等。同时,在使用平台接口时,我们需要确保使用的**平台接口**与所要发送的**平台类型**一致,对不同类型的事件需要做出不同的处理。在本章节中,我们将介绍如何获取事件更多的信息以及根据事件类型进行不同的处理。 ## 事件类型 在 NoneBot 中,事件均是 `nonebot.adapters.Event` 基类的子类型,基类对一些必要的属性进行了抽象,子类型则根据不同的平台进行了实现。在[自定义权限](./permission.mdx#自定义权限)一节中,我们就使用了 `Event` 的抽象方法 `get_user_id` 来获取事件发送者 ID,这个方法由协议适配器进行了实现,返回机器人用户对应的平台 ID。更多的基类抽象方法可以在[使用适配器](../advanced/adapter.md#获取事件通用信息)中查看。 既然事件是基类的子类型,我们实际可以获得的信息通常多于基类抽象方法所提供的。如果我们不满足于基类能获得的信息,我们可以小小的修改一下事件处理函数的事件参数类型注解,使其变为子类型,这样我们就可以通过协议适配器定义的子类型来获取更多的信息。我们以 `Console` 协议适配器为例: ```python {4} title=weather/__init__.py from nonebot.adapters.console import MessageEvent @weather.got("location", prompt="请输入地名") async def got_location(event: MessageEvent, location: str = ArgPlainText()): await weather.finish(f"{event.time.strftime('%Y-%m-%d')} {location} 的天气是...") ``` 在上面的代码中,我们获取了 `Console` 协议适配器的消息事件提供的发送时间 `time` 属性。 :::caution 注意 如果**基类**就能满足你的需求,那么就**不要修改**事件参数类型注解,这样可以使你的代码更加**通用**,可以在更多平台上运行。如何根据不同平台事件类型进行不同的处理,我们将在[重载](#重载)一节中介绍。 ::: ## 重载 我们在编写机器人时,常常会遇到这样一个问题:如何对私聊和群聊消息进行不同的处理?如何对不同平台的事件进行不同的处理?针对这些问题,NoneBot 提供了一个便捷而高效的解决方案 ── 重载。简单来说,依赖函数会根据其参数的类型注解来决定是否执行,忽略不符合其参数类型注解的情况。这样,我们就可以通过修改事件参数类型注解来实现对不同事件的处理,或者修改 `Bot` 参数类型注解来实现使用不同平台的接口。我们以 `OneBot` 协议适配器为例: ```python {4,8} from nonebot.adapters.onebot.v11 import PrivateMessageEvent, GroupMessageEvent @matcher.handle() async def handle_private(event: PrivateMessageEvent): await matcher.finish("私聊消息") @matcher.handle() async def handle_group(event: GroupMessageEvent): await matcher.finish("群聊消息") ``` 这样,机器人用户就会在私聊和群聊中分别收到不同的回复。同样的,我们也可以通过修改 `Bot` 参数类型注解来实现使用不同平台的接口: ```python from nonebot.adapters.console import Bot as ConsoleBot from nonebot.adapters.onebot.v11 import Bot as OneBot @matcher.handle() async def handle_console(bot: ConsoleBot): await bot.bell() @matcher.handle() async def handle_onebot(bot: OneBot): await bot.send_group_message(group_id=123123, message="OneBot") ``` :::caution 注意 重载机制对所有的参数类型注解都有效,因此,依赖注入也可以使用这个特性来对不同的返回值进行处理。 但 Bot、Event 和 Matcher 三者的参数类型注解具有最高检查优先级,如果三者任一类型注解不匹配,那么其他依赖注入将不会执行(如:`Depends`)。 ::: :::tip 提示 如何更好地编写一个跨平台的插件,我们将在[最佳实践](../best-practice/multi-adapter.mdx)中介绍。 ::: ================================================ FILE: website/versioned_docs/version-2.4.4/appendices/permission.mdx ================================================ --- sidebar_position: 5 description: 控制事件响应器的权限 options: menu: - category: appendices weight: 60 --- # 权限控制 import Messenger from "@site/src/components/Messenger"; **权限控制**是机器人在实际应用中需要解决的重点问题之一,NoneBot 提供了灵活的权限控制机制 —— `Permission`。 类似于响应规则 `Rule`,`Permission` 是由非负整数个 `PermissionChecker` 所共同组成的**用于筛选事件**的对象。但需要特别说明的是,权限和响应规则有如下区别: 1. 权限检查**先于**响应规则检查 2. `Permission` 只需**其中一个** `PermissionChecker` 返回 `True` 时就会检查通过 3. 权限检查进行时,上下文中并不存在会话状态 `state` 4. `Rule` 仅在**初次触发**事件响应器时进行检查,在余下的会话中并不会限制事件;而 `Permission` 会**持续生效**,在连续对话中一直对事件主体加以限制。 ## 基础使用 通常情况下,`Permission` 更侧重于对于**触发事件的机器人用户**的筛选,例如由 NoneBot 自身提供的 `SUPERUSER` 权限,便是筛选出会话发起者是否为超级用户。它可以对输入的用户进行鉴别,如果符合要求则会被认为通过并返回 `True`,反之则返回 `False`。 简单来说,`Permission` 是一个用于筛选出符合要求的用户的机制,可以通过 `Permission` 精确的控制响应对象的覆盖范围,从而拒绝掉我们所不希望的事件。 例如,我们可以在 `weather` 插件中添加一个超级用户可用的指令: ```python {3,9} title=weather/__init__.py from typing import Tuple from nonebot.params import Command from nonebot.permission import SUPERUSER manage = on_command( ("天气", "启用"), rule=to_me(), aliases={("天气", "禁用")}, permission=SUPERUSER, ) @manage.handle() async def control(cmd: Tuple[str, str] = Command()): _, action = cmd if action == "启用": plugin_config.weather_plugin_enabled = True elif action == "禁用": plugin_config.weather_plugin_enabled = False await manage.finish(f"天气插件已{action}") ``` 如上方示例所示,在注册事件响应器时,我们设置了 `permission` 参数,那么这个事件处理器在触发事件前的检查阶段会对用户身份进行验证,如果不符合我们设置的条件(此处即为**超级用户**)则不会响应。此时,我们向机器人发送 `/天气.禁用` 指令,机器人不会有任何响应,因为我们还不是机器人的超级管理员。我们在 dotenv 文件中设置了 `SUPERUSERS` 配置项之后,机器人就会响应我们的指令了。 ```dotenv title=.env SUPERUSERS=["console_user"] ``` ## 自定义权限 与事件响应规则类似,`PermissionChecker` 也是一个返回值为 `bool` 类型的依赖函数,即 `PermissionChecker` 支持依赖注入。例如,我们可以限制用户的指令调用次数: ```python title=weather/__init__.py from nonebot.adapters import Event fake_db: Dict[str, int] = {} async def limit_permission(event: Event): count = fake_db.setdefault(event.get_user_id(), 100) if count > 0: fake_db[event.get_user_id()] -= 1 return True return False weather = on_command("天气", permission=limit_permission) ``` ## 权限组合 权限之间可以通过 `|` 运算符进行组合,使得任意一个权限检查返回 `True` 时通过。例如: ```python {4-6} perm1 = Permission(foo_checker) perm2 = Permission(bar_checker) perm = perm1 | perm2 perm = perm1 | bar_checker perm = foo_checker | perm2 ``` 同样的,我们也无需担心组合了一个 `None` 值,`Permission` 会自动忽略 `None` 值。 ```python assert (perm | None) is perm ``` ## 主动使用权限 除了在事件响应器中使用权限外,我们也可以主动使用权限来判断事件是否符合条件。例如: ```python {3} perm = Permission(some_checker) result: bool = await perm(bot, event) ``` 我们只需要传入 `Bot` 实例、事件,`Permission` 会并发调用所有 `PermissionChecker` 进行检查,并返回结果。 ================================================ FILE: website/versioned_docs/version-2.4.4/appendices/rule.md ================================================ --- sidebar_position: 1 description: 自定义响应规则 options: menu: - category: appendices weight: 20 --- # 响应规则 机器人在实际应用中,往往会接收到多种多样的事件类型,NoneBot 通过响应规则来控制事件的处理。 在[指南](../tutorial/matcher.md#为事件响应器添加参数)中,我们为 `weather` 命令添加了一个 `rule=to_me()` 参数,这个参数就是一个响应规则,确保只有在私聊或者 `@bot` 时才会响应。 响应规则是一个 `Rule` 对象,它由一系列的 `RuleChecker` 函数组成,每个 `RuleChecker` 函数都会检查事件是否符合条件,如果所有的检查都通过,则事件会被处理。 ## RuleChecker `RuleChecker` 是一个返回值为 `bool` 类型的依赖函数,即 `RuleChecker` 支持依赖注入。我们可以根据上一节中添加的[配置项](./config.mdx#插件配置),在 `weather` 插件目录中编写一个响应规则: ```python {7,8} title=weather/__init__.py from nonebot import get_plugin_config from .config import Config plugin_config = get_plugin_config(Config) async def is_enable() -> bool: return plugin_config.weather_plugin_enabled weather = on_command("天气", rule=is_enable) ``` 在上面的代码中,我们定义了一个函数 `is_enable`,它会检查配置项 `weather_plugin_enabled` 是否为 `True`。这个函数 `is_enable` 即为一个 `RuleChecker`。 ## Rule `Rule` 是若干个 `RuleChecker` 的集合,它会并发调用每个 `RuleChecker`,只有当所有 `RuleChecker` 检查通过时匹配成功。例如:我们可以组合两个 `RuleChecker`,一个用于检查插件是否启用,一个用于检查用户是否在黑名单中: ```python {10} from nonebot.rule import Rule from nonebot.adapters import Event async def is_enable() -> bool: return plugin_config.weather_plugin_enabled async def is_blacklisted(event: Event) -> bool: return event.get_user_id() not in BLACKLIST rule = Rule(is_enable, is_blacklisted) weather = on_command("天气", rule=rule) ``` ## 合并响应规则 在定义响应规则时,我们可以将规则进行细分,来更好地复用规则。而在使用时,我们需要合并多个规则。除了使用 `Rule` 对象来组合多个 `RuleChecker` 外,我们还可以对 `Rule` 对象进行合并。在原 `weather` 插件中,我们可以将 `rule=to_me()` 与 `rule=is_enable` 使用 `&` 运算符合并: ```python {13} title=weather/__init__.py from nonebot.rule import to_me from nonebot import get_plugin_config from .config import Config plugin_config = get_plugin_config(Config) async def is_enable() -> bool: return plugin_config.weather_plugin_enabled weather = on_command( "天气", rule=to_me() & is_enable, aliases={"weather", "查天气"}, priority=plugin_config.weather_command_priority, block=True, ) ``` 这样,`weather` 命令就只会在插件启用且在私聊或者 `@bot` 时才会响应。 合并响应规则可以有多种形式,例如: ```python {4-6} rule1 = Rule(foo_checker) rule2 = Rule(bar_checker) rule = rule1 & rule2 rule = rule1 & bar_checker rule = foo_checker & rule2 ``` 同时,我们也无需担心合并了一个 `None` 值,`Rule` 会忽略 `None` 值。 ```python assert (rule & None) is rule ``` ## 主动使用响应规则 除了在事件响应器中使用响应规则外,我们也可以主动使用响应规则来判断事件是否符合条件。例如: ```python {3} rule = Rule(some_checker) result: bool = await rule(bot, event, state) ``` 我们只需要传入 `Bot` 对象、事件和会话状态,`Rule` 会并发调用所有 `RuleChecker` 进行检查,并返回结果。 ## 内置响应规则 NoneBot 内置了一些常用的响应规则,可以直接通过事件响应器辅助函数或者自行合并其他规则使用。内置响应规则列表可以参考[事件响应器进阶](../advanced/matcher.md) ================================================ FILE: website/versioned_docs/version-2.4.4/appendices/session-control.mdx ================================================ --- sidebar_position: 2 description: 更灵活的会话控制 options: menu: - category: appendices weight: 30 --- # 会话控制 import Messenger from "@site/src/components/Messenger"; 在[指南](../tutorial/event-data.mdx#使用依赖注入)的 `weather` 插件中,我们使用依赖注入获取了机器人用户发送的地名参数,并根据地名参数进行相应的回复。但是,一问一答的对话模式仅仅适用于简单的对话场景,如果我们想要实现更复杂的对话模式,就需要使用会话控制。 ## 询问并获取用户输入 在 `weather` 插件中,我们对于用户未输入地名参数的情况直接回复了 `请输入地名` 并结束了事件流程。但是,这样用户体验并不好,需要重新输入指令和地名参数才能获取天气回复。我们现在来实现询问并获取用户地名参数的功能。 ### 询问用户 我们可以使用事件响应器操作中的 `got` 装饰器来表示当前事件处理流程需要询问并获取用户输入的消息: ```python {6} title=weather/__init__.py @weather.handle() async def handle_function(args: Message = CommandArg()): if location := args.extract_plain_text(): await weather.finish(f"今天{location}的天气是...") @weather.got("location", prompt="请输入地名") async def got_location(): ... ``` 在上面的代码中,我们使用 `got` 事件响应器操作来向用户发送 `prompt` 消息,并等待用户的回复。用户的回复消息将会被作为 `location` 参数存储于事件响应器状态中。 :::tip 提示 事件处理函数根据定义的顺序依次执行。 ::: ### 获取用户输入 在询问以及用户回复之后,我们就可以获取到我们需要的 `location` 参数了。我们使用 `ArgPlainText` 依赖注入来获取参数纯文本信息: ```python {9} title=weather/__init__.py from nonebot.params import ArgPlainText @weather.handle() async def handle_function(args: Message = CommandArg()): if location := args.extract_plain_text(): await weather.finish(f"今天{location}的天气是...") @weather.got("location", prompt="请输入地名") async def got_location(location: str = ArgPlainText()): await weather.finish(f"今天{location}的天气是...") ``` 在上面的代码中,我们在 `got_location` 函数中定义了一个依赖注入参数 `location`,他的值将会是用户回复的消息纯文本信息。获取到用户输入的地名参数后,我们就可以进行天气查询并回复了。 :::tip 提示 如果想要获取用户回复的消息对象 `Message` ,可以使用 `Arg` 依赖注入。 ::: ### 跳过询问 在上面的代码中,如果用户在输入天气指令时,同时提供了地名参数,我们直接回复了天气信息,这部分的逻辑是和询问用户地名参数之后的逻辑一致的。如果在复杂的业务场景下,我们希望这部分代码应该复用以减少代码冗余。我们可以使用事件响应器操作中的 `set_arg` 来主动设置一个参数: ```python {4,6} title=weather/__init__.py from nonebot.matcher import Matcher @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()): await weather.finish(f"今天{location}的天气是...") ``` 请注意,设置参数需要使用依赖注入来获取 `Matcher` 实例以确保上下文正确,且参数值应为 `Message` 对象。 在 `location` 参数被设置之后,`got` 事件响应器操作将不再会询问并等待用户的回复,而是直接进入 `got_location` 函数。 ## 请求重新输入 在实际的业务场景中,用户的输入很有可能并非是我们所期望的,而结束事件处理流程让用户重新发送指令也不是一个好的体验。这时我们可以使用 `reject` 事件响应器操作来请求用户重新输入: ```python {8,9} title=weather/__init__.py @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}的天气是...") ``` 在上面的代码中,我们在 `got_location` 函数中判断用户输入的地名是否在支持的城市列表中,如果不在,则使用 `reject` 事件响应器操作。操作将会向用户发送 `reject` 参数中的消息,并等待用户回复后,重新执行 `got_location` 函数。通过 `got` 和 `reject` 事件响应器操作,我们实现了类似于**循环**的执行方式。 `reject` 事件响应器操作与 `finish` 类似,NoneBot 会在向机器人用户发送消息内容后抛出 `RejectedException` 异常来暂停事件响应流程以等待用户输入。也就是说,在 `reject` 被执行后,后续的程序同样是不会被执行的。 ## 更多事件响应器操作 在之前的章节中,我们已经大致了解了五个事件响应器操作:`handle`、`got`、`finish`、`send` 和 `reject`。现在我们来完整地介绍一下这些操作。 事件响应器操作可以分为两大类:**交互操作**和**流程控制操作**。我们可以通过交互操作来与用户进行交互,而流程控制操作则可以用来控制事件处理流程的执行。 :::tip 提示 事件处理流程按照事件处理函数添加顺序执行,已经结束的事件处理函数不可能被恢复执行。 ::: ### handle `handle` 事件响应器操作是一个装饰器,用于向事件处理流程添加一个事件处理函数。 ```python @matcher.handle() async def handle_func(): ... ``` `handle` 装饰器支持嵌套操作,即一个事件处理函数可以被添加多次: ```python @matcher.handle() @matcher.handle() async def handle_func(): # 这个函数会被执行两次 ... ``` ### got `got` 事件响应器操作也是一个装饰器,它会在当前装饰的事件处理函数运行之前,中断当前事件处理流程,等待接收一个新的事件。它可以通过 `prompt` 参数来向用户发送询问消息,然后等待用户的回复消息,贴近对话形式会话。 `got` 装饰器接受一个参数 `key` 和一个可选参数 `prompt`。当会话状态中不存在 `key` 对应的消息时,会向用户发送 `prompt` 参数的消息,并等待用户回复。`prompt` 参数的类型和 [`send`](#send) 事件响应器操作的参数类型一致。 在事件处理函数中,可以通过依赖注入的方式来获取接收到的消息,参考:[`Arg`](../advanced/dependency.mdx#arg)、[`ArgStr`](../advanced/dependency.mdx#argstr)、[`ArgPlainText`](../advanced/dependency.mdx#argplaintext)。 ```python @matcher.got("key", prompt="请输入...") async def got_func(key: Message = Arg()): ... ``` `got` 装饰器支持与 `got` 和 `receive` 装饰器嵌套操作,即一个事件处理函数可以在接收多个事件或消息后执行: ```python @matcher.got("key1", prompt="请输入key1...") @matcher.got("key2", prompt="请输入key2...") @matcher.receive("key3") async def got_func(key1: Message = Arg(), key2: Message = Arg(), key3: Event = Received("key3")): ... ``` ### receive `receive` 事件响应器操作也是一个装饰器,它会在当前装饰的事件处理函数运行之前,中断当前事件处理流程,等待接收一个新的事件。与 `got` 不同的是,`receive` 不会向用户发送询问消息,并且等待一个用户事件。可以接收的事件类型取决于[会话更新](../advanced/session-updating.md)。 `receive` 装饰器接受一个可选参数 id,用于标识当前需要接收的事件,如果不指定,则默认为空 `""`。 在事件处理函数中,可以通过依赖注入的方式来获取接收到的事件,参考:[`Received`](../advanced/dependency.mdx#received)、[`LastReceived`](../advanced/dependency.mdx#lastreceived)。 ```python @matcher.receive("id") async def receive_func(event: Event = Received("id")): ... ``` `receive` 装饰器支持与 `got` 和 `receive` 装饰器嵌套操作,即一个事件处理函数可以在接收多个事件或消息后执行: ```python @matcher.receive("key1") @matcher.got("key2", prompt="请输入key2...") @matcher.got("key3", prompt="请输入key3...") async def receive_func(key1: Event = Received("key1"), key2: Message = Arg(), key3: Message = Arg()): ... ``` ### send `send` 事件响应器操作用于向用户回复一条消息。协议适配器会根据当前 event 选择回复的途径。 `send` 操作接受一个参数 message 和其他任何协议适配器接受的参数。message 参数类型可以是字符串、消息序列、消息段或者消息模板。消息模板将会使用会话状态字典进行渲染后发送。 这个操作等同于使用 `bot.send(event, message, **kwargs)`,但不需要自行传入 `event`。 ```python @matcher.handle() async def _(): await matcher.send("Hello world!") ``` ### finish 向用户回复一条消息(可选),并立即结束**整个处理流程**。 参数与 [`send`](#send) 相同。 ```python @matcher.handle() async def _(): await matcher.finish("Hello world!") # 下面的代码不会被执行 ``` ### pause 向用户回复一条消息(可选),立即结束**当前**事件处理函数,等待接收一个新的事件后进入**下一个**事件处理函数。 参数与 [`send`](#send) 相同。 ```python @matcher.handle() async def _(): if need_confirm: await matcher.pause("请在两分钟内确认执行") @matcher.handle() async def _(): ... ``` ### reject 向用户回复一条消息(可选),立即结束**当前**事件处理函数,等待接收一个新的事件后再次执行**当前**事件处理函数。 `reject` 可以用于拒绝当前 `receive` 接收的事件或 `got` 接收的参数。通常在用户回复不符合格式或标准需要重新输入,或者用于循环进行用户交互。 参数与 [`send`](#send) 相同。 ```python @matcher.got("arg") async def _(arg: str = ArgPlainText()): if not is_valid(arg): await matcher.reject("Invalid arg!") ``` ### reject_arg 向用户回复一条消息(可选),立即结束**当前**事件处理函数,等待接收一个新的消息后再次执行**当前**事件处理函数。 `reject_arg` 用于拒绝指定 `got` 接收的参数,通常在嵌套装饰器时使用。 `reject_arg` 操作接受一个 key 参数以及可选的 prompt 参数。prompt 参数与 [`send`](#send) 相同。 ```python @matcher.got("a") @matcher.got("b") async def _(a: str = ArgPlainText(), b: str = ArgPlainText()): if a not in b: await matcher.reject_arg("a", "Invalid a!") ``` ### reject_receive 向用户回复一条消息(可选),立即结束**当前**事件处理函数,等待接收一个新的事件后再次执行**当前**事件处理函数。 `reject_receive` 用于拒绝指定 `receive` 接收的事件,通常在嵌套装饰器时使用。 `reject_receive` 操作接受一个可选的 id 参数以及可选的 prompt 参数。id 参数默认为空 `""`,prompt 参数与 [`send`](#send) 相同。 ```python @matcher.receive("a") @matcher.receive("b") async def _(a: Event = Received("a"), b: Event = Received("b")): if a.get_user_id() != b.get_user_id(): await matcher.reject_receive("a") ``` ### skip 立即结束当前事件处理函数,进入下一个事件处理函数。 通常在依赖注入中使用,用于跳过当前事件处理函数的执行。 ```python from nonebot.params import Depends async def dependency(): matcher.skip() @matcher.handle() async def _(check=Depends(dependency)): # 这个函数不会被执行 ``` ### stop_propagation 阻止事件向更低优先级的事件响应器传播。 ```python from nonebot.matcher import Matcher @foo.handle() async def _(matcher: Matcher): matcher.stop_propagation() ``` :::caution 注意 `stop_propagation` 操作是实例方法,需要先通过依赖注入获取事件响应器实例再进行调用。 ::: ### get_arg 获取一个 `got` 接收的参数。 `get_arg` 操作接受一个 key 参数和一个可选的 default 参数。当参数不存在时,将返回 default 或 `None`。 ```python from nonebot.matcher import Matcher @matcher.handle() async def _(matcher: Matcher): key = matcher.get_arg("key", default=None) ``` ### set_arg 设置 / 覆盖一个 `got` 接收的参数。 `set_arg` 操作接受一个 key 参数和一个 value 参数。请注意,value 参数必须是消息序列对象,如需存储其他数据请使用[会话状态](./session-state.md)。 ```python from nonebot.matcher import Matcher @matcher.handle() async def _(matcher: Matcher): matcher.set_arg("key", Message("value")) ``` ### get_receive 获取一个 `receive` 接收的事件。 `get_receive` 操作接受一个 id 参数和一个可选的 default 参数。当事件不存在时,将返回 default 或 `None`。 ```python from nonebot.matcher import Matcher @matcher.handle() async def _(matcher: Matcher): event = matcher.get_receive("id", default=None) ``` ### get_last_receive 获取最近的一个 `receive` 接收的事件。 `get_last_receive` 操作接受一个可选的 default 参数。当事件不存在时,将返回 default 或 `None`。 ```python from nonebot.matcher import Matcher @matcher.handle() async def _(matcher: Matcher): event = matcher.get_last_receive(default=None) ``` ### set_receive 设置 / 覆盖一个 `receive` 接收的事件。 `set_receive` 操作接受一个 id 参数和一个 event 参数。请注意,event 参数必须是事件对象,如需存储其他数据请使用[会话状态](./session-state.md)。 ```python from nonebot.matcher import Matcher @matcher.handle() async def _(matcher: Matcher): matcher.set_receive("key", Event()) ``` ================================================ FILE: website/versioned_docs/version-2.4.4/appendices/session-state.md ================================================ --- sidebar_position: 3 description: 会话状态信息 options: menu: - category: appendices weight: 40 --- # 会话状态 在事件处理流程中,和用户交互的过程即是会话。在会话中,我们可能需要记录一些信息,例如用户的重试次数等等,以便在会话中的不同阶段进行判断和处理。这些信息都可以存储于会话状态中。 NoneBot 中的会话状态是一个字典,可以通过类型 `T_State` 来获取。字典内可以存储任意类型的数据,但是要注意的是,NoneBot 本身会在会话状态中存储一些信息,因此不要使用 [NoneBot 使用的键名](../api/consts.md)。 ```python from nonebot.typing import T_State @matcher.got("key", prompt="请输入密码") async def _(state: T_State, key: str = ArgPlainText()): if key != "some password": try_count = state.get("try_count", 1) if try_count >= 3: await matcher.finish("密码错误次数过多") else: state["try_count"] = try_count + 1 await matcher.reject("密码错误,请重新输入") await matcher.finish("密码正确") ``` 会话状态的生命周期与事件处理流程相同,在期间的任何一个事件处理函数都可以进行读写。 ```python from nonebot.typing import T_State @matcher.handle() async def _(state: T_State): state["key"] = "value" @matcher.handle() async def _(state: T_State): await matcher.finish(state["key"]) ``` 会话状态还可以用于发送动态消息,消息模板在发送时会使用会话状态字典进行渲染。消息模板的使用方法已经在[消息处理](../tutorial/message.md#使用消息模板)中介绍过,这里不再赘述。 ```python from nonebot.typing import T_State from nonebot.adapters import MessageTemplate @matcher.handle() async def _(state: T_State): state["username"] = "user" @matcher.got("password", prompt=MessageTemplate("请输入 {username} 的密码")) async def _(): await matcher.finish(MessageTemplate("密码为 {password}")) ``` ================================================ FILE: website/versioned_docs/version-2.4.4/appendices/whats-next.md ================================================ --- sidebar_position: 99 description: 下一步──进阶! --- # 下一步 至此,我们已经了解了 NoneBot 的大多数功能用法,相信你已经可以独自写出一个插件了。现在你可以选择: - 即刻开始插件编写! - 更深入地了解 NoneBot 的[更多功能和原理](../advanced/plugin-info.md)! ================================================ FILE: website/versioned_docs/version-2.4.4/best-practice/alconna/README.mdx ================================================ 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#安装插件)来了解并选择安装插件的方式。如: 在**项目目录**下执行以下命令: ```shell nb plugin install nonebot-plugin-alconna ``` ```shell pip install nonebot-plugin-alconna ``` ```shell pdm add nonebot-plugin-alconna ``` ## 导入插件 由于 `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` 来改写这个插件。
插件示例 ```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}的天气是...") ```
```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) ================================================ FILE: website/versioned_docs/version-2.4.4/best-practice/alconna/_category_.json ================================================ { "label": "命令解析拓展", "position": 6 } ================================================ FILE: website/versioned_docs/version-2.4.4/best-practice/alconna/builtins.mdx ================================================ --- 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` 插件能将用户发送的消息原样返回。 #### help `help` 插件能列出所有 Alconna 指令。同时还能查询某个指令对应的插件信息。 help 插件的帮助信息如下: ``` /help ## 注释 query: 选择某条命令的id或者名称查看具体帮助 显示所有命令帮助 用法: 可以使用 --hide 参数来显示隐藏命令,使用 -P 参数来显示命令所属插件名称 可用的子命令有: * 是否列出命令所属命名空间 -N│--namespace│命名空间 [target: str] ## 注释 target: 指定的命名空间 该子命令内可用的选项有: * 列出所有命名空间 --list 可用的选项有: * 查看指定页数的命令帮助 --page * 查看命令所属插件的信息 -P│插件信息│--plugin-info * 是否列出隐藏命令 隐藏│-H│--hide ``` #### lang `lang` 插件能切换 i18n 的语言设置。 lang 插件的帮助信息如下: ``` /lang i18n配置相关功能 可用的选项有: * 查看支持的语言列表 list [name: str] * 切换语言 switch [locale: str] ``` 其中 `list` 选项可以查找某一插件下的语言支持情况 (例如 `/lang list nonebot_plugin_alconna`)。 #### switch `switch` 插件能用来启用/禁用某个命令,其使用方法与 `help` 类似。 #### with `with` 插件能在当前会话中设置一个局部命令前缀,以便于有多个子命令的指令使用。 with 插件的帮助信息如下: ``` .with [name: str] with 指令 用法: 设置局部命令前缀 可用的选项有: * 设置可能的生效时间 --expire│expire * 取消当前前缀 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 的音乐分享卡片 ================================================ FILE: website/versioned_docs/version-2.4.4/best-practice/alconna/command.md ================================================ --- sidebar_position: 2 description: Alconna 基本介绍 --- # Alconna 本体 [`Alconna`](https://github.com/ArcletProject/Alconna) 隶属于 `ArcletProject`,是一个简单、灵活、高效的命令参数解析器, 并且不局限于解析命令式字符串。 我们先通过一个例子来讲解 **Alconna** 的核心 —— `Args`, `Subcommand`, `Option`: ```python from arclet.alconna import Alconna, Args, Subcommand, Option alc = Alconna( "pip", Subcommand( "install", Args["package", str], Option("-r|--requirement", Args["file", str]), Option("-i|--index-url", Args["url", str]), ) ) res = alc.parse("pip install nonebot2 -i URL") print(res) # matched=True, header_match=(origin='pip' result='pip' matched=True groups={}), subcommands={'install': (value=Ellipsis args={'package': 'nonebot2'} options={'index-url': (value=None args={'url': 'URL'})} subcommands={})}, other_args={'package': 'nonebot2', 'url': 'URL'} print(res.all_matched_args) # {'package': 'nonebot2', 'url': 'URL'} ``` 这段代码通过`Alconna`创捷了一个接受主命令名为`pip`, 子命令为`install`且子命令接受一个 **Args** 参数`package`和二个 **Option** 参数`-r`和`-i`的命令参数解析器, 通过`parse`方法返回解析结果 **Arparma** 的实例。 ## 命令头 命令头是指命令的前缀 (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", "bar"] | "baz" | `"foobaz"` 或 `"barbaz"` | 纯文字头 | | [int, bool] | "foo" | `[123, "foo"]` 或 `[False, "foo"]` | 类型头 | | [123, 4567] | "foo" | `[123, "foo"]` 或 `[4567, "foo"]` | 元素头 | | [nepattern.NUMBER] | "bar" | `[123, "bar"]` 或 `[123.456, "bar"]` | 表达式头 | | [123, "foo"] | "bar" | `[123, "bar"]` 或 `"foobar"` 或 `["foo", "bar"]` | 混合头 | | [(int, "foo"), (456, "bar")] | "baz" | `[123, "foobaz"]` 或 `[456, "foobaz"]` 或 `[456, "barbaz"]` | 对头 | 对于无前缀的类型头,此时会将传入的值尝试转为 BasePattern,例如 `int` 会转为 `nepattern.INTEGER`。如此该命令头会匹配对应的类型, 例如 `int` 会匹配 `123` 或 `"456"`,但不会匹配 `"foo"`。解析后,Alconna 会将命令头匹配到的值转为对应的类型,例如 `int` 会将 `"123"` 转为 `123`。 :::tip **正则内容只在命令名上生效,前缀中的正则会被转义** ::: 除了通过传入 `re:xxx` 来使用正则表达式外,Alconna 还提供了一种更加简洁的方式来使用正则表达式,称为 Bracket Header: ```python alc = Alconna(".rd{roll:int}") assert alc.parse(".rd123").header["roll"] == 123 ``` Bracket Header 类似 python 里的 f-string 写法,通过 `"{}"` 声明匹配类型 `"{}"` 中的内容为 "name:type or pat": - `"{}"`, `"{:}"` ⇔ `"(.+)"`, 占位符 - `"{foo}"` ⇔ `"(?P<foo>.+)"` - `"{:\d+}"` ⇔ `"(\d+)"` - `"{foo:int}"` ⇔ `"(?P<foo>\d+)"`,其中 `"int"` 部分若能转为 `BasePattern` 则读取里面的表达式 ## 参数声明(Args) `Args` 是用于声明命令参数的组件, 可以通过以下几种方式构造 **Args** : - `Args[key, var, default][key1, var1, default1][...]` - `Args[(key, var, default)]` - `Args.key[var, default]` 其中,key **一定**是字符串,而 var 一般为参数的类型,default 为具体的值或者 **arclet.alconna.args.Field** 其与函数签名类似,但是允许含有默认值的参数在前;同时支持 keyword-only 参数不依照构造顺序传入 (但是仍需要在非 keyword-only 参数之后)。 ### key `key` 的作用是用以标记解析出来的参数并存放于 **Arparma** 中,以方便用户调用。 其有三种为 Args 注解的标识符: `?`、`/`、 `!`, 标识符与 key 之间建议以 `;` 分隔: - `!` 标识符表示该处传入的参数应**不是**规定的类型,或**不在**指定的值中。 - `?` 标识符表示该参数为**可选**参数,会在无参数匹配时跳过。 - `/` 标识符表示该参数的类型注解需要隐藏。 另外,对于参数的注释也可以标记在 `key` 中,其与 key 或者标识符 以 `#` 分割: `foo#这是注释;?` 或 `foo?#这是注释` :::tip `Args` 中的 `key` 在实际命令中并不需要传入(keyword 参数除外): ```python from arclet.alconna import Alconna, Args alc = Alconna("test", Args["foo", str]) alc.parse("test --foo abc") # 错误 alc.parse("test abc") # 正确 ``` 若需要 `test --foo abc`,你应该使用 `Option`: ```python from arclet.alconna import Alconna, Args, Option alc = Alconna("test", Option("--foo", Args["foo", str])) ``` ::: ### var var 负责命令参数的**类型检查**与**类型转化** `Args` 的`var`表面上看需要传入一个 `type`,但实际上它需要的是一个 `nepattern.BasePattern` 的实例: ```python from arclet.alconna import Args from nepattern import BasePattern # 表示 foo 参数需要匹配一个 @number 样式的字符串 args = Args["foo", BasePattern("@\d+")] ``` `pip` 示例中可以传入 `str` 是因为 `str` 已经注册在了 `nepattern.global_patterns` 中,因此会替换为 `nepattern.global_patterns[str]` `nepattern.global_patterns`默认支持的类型有: - `str`: 匹配任意字符串 - `int`: 匹配整数 - `float`: 匹配浮点数 - `bool`: 匹配 `True` 与 `False` 以及他们小写形式 - `hex`: 匹配 `0x` 开头的十六进制字符串 - `url`: 匹配网址 - `email`: 匹配 `xxxx@xxx` 的字符串 - `ipv4`: 匹配 `xxx.xxx.xxx.xxx` 的字符串 - `list`: 匹配类似 `["foo","bar","baz"]` 的字符串 - `dict`: 匹配类似 `{"foo":"bar","baz":"qux"}` 的字符串 - `datetime`: 传入一个 `datetime` 支持的格式字符串,或时间戳 - `Any`: 匹配任意类型 - `AnyString`: 匹配任意类型,转为 `str` - `Number`: 匹配 `int` 与 `float`,转为 `int` 同时可以使用 typing 中的类型: - `Literal[X]`: 匹配其中的任意一个值 - `Union[X, Y]`: 匹配其中的任意一个类型 - `Optional[xxx]`: 会自动将默认值设为 `None`,并在解析失败时使用默认值 - `List[X]`: 匹配一个列表,其中的元素为 `X` 类型 - `Dict[X, Y]`: 匹配一个字典,其中的 key 为 `X` 类型,value 为 `Y` 类型 - ... :::tip 几类特殊的传入标记: - `"foo"`: 匹配字符串 "foo" (若没有某个 `BasePattern` 与之关联) - `RawStr("foo")`: 匹配字符串 "foo" (即使有 `BasePattern` 与之关联也不会被替换) - `"foo|bar|baz"`: 匹配 "foo" 或 "bar" 或 "baz" - `[foo, bar, Baz, ...]`: 匹配其中的任意一个值或类型 - `Callable[[X], Y]`: 匹配一个参数为 `X` 类型的值,并返回通过该函数调用得到的 `Y` 类型的值 - `"re:xxx"`: 匹配一个正则表达式 `xxx`,会返回 Match[0] - `"rep:xxx"`: 匹配一个正则表达式 `xxx`,会返回 `re.Match` 对象 - `{foo: bar, baz: qux}`: 匹配字典中的任意一个键, 并返回对应的值 (特殊的键 ... 会匹配任意的值) - ... **特别的**,你可以不传入 `var`,此时会使用 `key` 作为 `var`, 匹配 `key` 字符串。 ::: #### MultiVar 与 KeyWordVar `MultiVar` 是一个特殊的标注,用于告知解析器该参数可以接受多个值,类似于函数中的 `*args`,其构造方法形如 `MultiVar(str)`。 同样的还有 `KeyWordVar`,类似于函数中的 `*, name: type`,其构造方法形如 `KeyWordVar(str)`,用于告知解析器该参数为一个 keyword-only 参数。 :::tip `MultiVar` 与 `KeyWordVar` 组合时,代表该参数为一个可接受多个 key-value 的参数,类似于函数中的 `**kwargs`,其构造方法形如 `MultiVar(KeyWordVar(str))` `MultiVar` 与 `KeyWordVar` 也可以传入 `default` 参数,用于指定默认值 `MultiVar` 不能在 `KeyWordVar` 之后传入 ::: #### AllParam `AllParam` 是一个特殊的标注,用于告知解析器该参数接收命令中在此位置之后的所有参数并**结束解析**,可以认为是**泛匹配参数**。 `AllParam` 可直接使用 (`Args["xxx", AllParam]`), 也可以传入指定的接收类型 (`Args["xxx", AllParam(str)]`)。 :::tip 在 `nonebot_plugin_alconna` 下,`AllParam` 的返回值为 [`UniMessage`](./uniseg/message.mdx) ::: ### default `default` 传入的是该参数的默认值或者 `Field`,以携带对于该参数的更多信息。 默认情况下 (即不声明) `default` 的值为特殊值 `Empty`。这也意味着你可以将默认值设置为 `None` 表示默认值为空值。 `Field` 构造需要的参数说明如下: - default: 参数单元的默认值 - alias: 参数单元默认值的别名 - completion: 参数单元的补全说明生成函数 - unmatch_tips: 参数单元的错误提示生成函数,其接收一个表示匹配失败的元素的参数 - missing_tips: 参数单元的缺失提示生成函数 ## 选项与子命令(Option & Subcommand) `Option` 和 `Subcommand` 可以传入一组 `alias`,如 `Option("--foo|-F|--FOO|-f")`,`Subcommand("foo", alias=["F"])` 传入别名后,选项与子命令会选择其中长度最长的作为其名称。若传入为 "--foo|-f",则命令名称为 "--foo" :::tip 特别提醒!!! Option 的名字或别名**没有要求**必须在前面写上 `-` Option 与 Subcommand 的唯一区别在于 Subcommand 可以传入自己的 **Option** 与 **Subcommand** ::: 他们拥有如下共同参数: - `help_text`: 传入该组件的帮助信息 - `dest`: 被指定为解析完成时标注匹配结果的标识符,不传入时默认为选项或子命令的名称 (name) - `requires`: 一段指定顺序的字符串列表,作为唯一的前置序列与命令嵌套替换 对于命令 `test foo bar baz qux ` 来讲,因为`foo bar baz` 仅需要判断是否相等, 所以可以这么编写: ```python Alconna("test", Option("qux", Args.a[int], requires=["foo", "bar", "baz"])) ``` - `default`: 默认值,在该组件未被解析时使用使用该值替换。 特别的,使用 `OptionResult` 或 `SubcomanndResult` 可以设置包括参数字典在内的默认值: ```python from arclet.alconna import Option, OptionResult opt1 = Option("--foo", default=False) opt2 = Option("--foo", default=OptionResult(value=False, args={"bar": 1})) ``` ### Action `Option` 可以特别设置传入一类 `Action`,作为解析操作 `Action` 分为三类: - `store`: 无 Args 时, 仅存储一个值, 默认为 Ellipsis; 有 Args 时, 后续的解析结果会覆盖之前的值 - `append`: 无 Args 时, 将多个值存为列表, 默认为 Ellipsis; 有 Args 时, 每个解析结果会追加到列表中, 当存在默认值并且不为列表时, 会自动将默认值变成列表, 以保证追加的正确性 - `count`: 无 Args 时, 计数器加一; 有 Args 时, 表现与 STORE 相同, 当存在默认值并且不为数字时, 会自动将默认值变成 1, 以保证计数器的正确性。 `Alconna` 提供了预制的几类 `Action`: - `store`(默认),`store_value`,`store_true`,`store_false` - `append`,`append_value` - `count` ## 解析结果 `Alconna.parse` 会返回由 **Arparma** 承载的解析结果 `Arparma` 有如下属性: - 调试类 - matched: 是否匹配成功 - error_data: 解析失败时剩余的数据 - error_info: 解析失败时的异常内容 - origin: 原始命令,可以类型标注 - 分析类 - header_match: 命令头部的解析结果,包括原始头部、解析后头部、解析结果与可能的正则匹配组 - main_args: 命令的主参数的解析结果 - options: 命令所有选项的解析结果 - subcommands: 命令所有子命令的解析结果 - other_args: 除主参数外的其他解析结果 - all_matched_args: 所有 Args 的解析结果 ### 路径查询 `Arparma` 同时提供了便捷的查询方法 `query[type]()`,会根据传入的 `path` 查找参数并返回 `path` 支持如下: - `main_args`, `options`, ...: 返回对应的属性 - `args`: 返回 all_matched_args - `args.`: 返回 all_matched_args 中 `key` 键对应的值 - `main_args.`: 返回主命令的解析参数字典中 `key` 键对应的值 - ``: 返回选项/子命令 `node` 的解析结果 (OptionResult | SubcommandResult) - `.value`: 返回选项/子命令 `node` 的解析值 - `.args`: 返回选项/子命令 `node` 的解析参数字典 - `.`, `.args.`: 返回选项/子命令 `node` 的参数字典中 `key` 键对应的值 以及: - `options.`: 返回选项 `opt` 的解析结果 (OptionResult) - `options..value`: 返回选项 `opt` 的解析值 - `options..args`: 返回选项 `opt` 的解析参数字典 - `options..`, `options..args.`: 返回选项 `opt` 的参数字典中 `key` 键对应的值 - `subcommands.`: 返回子命令 `subcmd` 的解析结果 (SubcommandResult) - `subcommands..value`: 返回子命令 `subcmd` 的解析值 - `subcommands..args`: 返回子命令 `subcmd` 的解析参数字典 - `subcommands..`, `subcommands..args.`: 返回子命令 `subcmd` 的参数字典中 `key` 键对应的值 ## 元数据(CommandMeta) `Alconna` 的元数据相当于其配置,拥有以下条目: - `description`: 命令的描述 - `usage`: 命令的用法 - `example`: 命令的使用样例 - `author`: 命令的作者 - `fuzzy_match`: 命令是否开启模糊匹配 - `fuzzy_threshold`: 模糊匹配阈值 - `raise_exception`: 命令是否抛出异常 - `hide`: 命令是否对 manager 隐藏 - `hide_shortcut`: 命令的快捷指令是否在 help 信息中隐藏 - `keep_crlf`: 命令解析时是否保留换行字符 - `compact`: 命令是否允许第一个参数紧随头部 - `strict`: 命令是否严格匹配,若为 False 则未知参数将作为名为 $extra 的参数 - `context_style`: 命令上下文插值的风格,None 为关闭,bracket 为 `{...}`,parentheses 为 `$(...)` - `extra`: 命令的自定义额外信息 元数据一定使用 `meta=...` 形式传入: ```python from arclet.alconna import Alconna, CommandMeta alc = Alconna(..., meta=CommandMeta("foo", example="bar")) ``` ## 命名空间配置 命名空间配置 (以下简称命名空间) 相当于 `Alconna` 的默认配置,其优先度低于 `CommandMeta`。 `Alconna` 默认使用 "Alconna" 命名空间。 命名空间有以下几个属性: - name: 命名空间名称 - prefixes: 默认前缀配置 - separators: 默认分隔符配置 - formatter_type: 默认格式化器类型 - fuzzy_match: 默认是否开启模糊匹配 - raise_exception: 默认是否抛出异常 - builtin_option_name: 默认的内置选项名称(--help, --shortcut, --comp) - disable_builtin_options: 默认禁用的内置选项(--help, --shortcut, --comp) - enable_message_cache: 默认是否启用消息缓存 - compact: 默认是否开启紧凑模式 - strict: 命令是否严格匹配 - context_style: 命令上下文插值的风格 - ... ### 新建命名空间并替换 ```python from arclet.alconna import Alconna, namespace, Namespace, Subcommand, Args, config ns = Namespace("foo", prefixes=["/"]) # 创建 "foo"命名空间配置, 它要求创建的Alconna的主命令前缀必须是/ alc = Alconna("pip", Subcommand("install", Args["package", str]), namespace=ns) # 在创建Alconna时候传入命名空间以替换默认命名空间 # 可以通过with方式创建命名空间 with namespace("bar") as np1: np1.prefixes = ["!"] # 以上下文管理器方式配置命名空间,此时配置会自动注入上下文内创建的命令 np1.formatter_type = ShellTextFormatter # 设置此命名空间下的命令的 formatter 默认为 ShellTextFormatter np1.builtin_option_name["help"] = {"帮助", "-h"} # 设置此命名空间下的命令的帮助选项名称 # 你还可以使用config来管理所有命名空间并切换至任意命名空间 config.namespaces["foo"] = ns # 将命名空间挂载到 config 上 alc = Alconna("pip", Subcommand("install", Args["package", str]), namespace=config.namespaces["foo"]) # 也是同样可以切换到"foo"命名空间 ``` ### 修改默认的命名空间 ```python from arclet.alconna import config, namespace, Namespace config.default_namespace.prefixes = [...] # 直接修改默认配置 np = Namespace("xxx", prefixes=[...]) config.default_namespace = np # 更换默认的命名空间 with namespace(config.default_namespace.name) as np: np.prefixes = [...] ``` ## 快捷指令 快捷命令可以做到标识一段命令, 并且传递参数给原命令 一般情况下你可以通过 `Alconna.shortcut` 进行快捷指令操作 (创建,删除) `shortcut` 的第一个参数为快捷指令名称,第二个参数为 `ShortcutArgs`,作为快捷指令的配置: ```python class ShortcutArgs(TypedDict): """快捷指令参数""" command: NotRequired[str] """快捷指令的命令""" args: NotRequired[list[Any]] """快捷指令的附带参数""" fuzzy: NotRequired[bool] """是否允许命令后随参数""" prefix: NotRequired[bool] """是否调用时保留指令前缀""" wrapper: NotRequired[ShortcutRegWrapper] """快捷指令的正则匹配结果的额外处理函数""" humanized: NotRequired[str] """快捷指令的人类可读描述""" ``` ### args的使用 ```python from arclet.alconna import Alconna, Args alc = Alconna("setu", Args["count", int]) alc.shortcut("涩图(\d+)张", {"args": ["{0}"]}) # 'Alconna::setu 的快捷指令: "涩图(\\d+)张" 添加成功' alc.parse("涩图3张").query("count") # 3 ``` ### command的使用 ```python from arclet.alconna import Alconna, Args alc = Alconna("eval", Args["content", str]) alc.shortcut("echo", {"command": "eval print(\\'{*}\\')"}) # 'Alconna::eval 的快捷指令: "echo" 添加成功' alc.shortcut("echo", delete=True) # 删除快捷指令 # 'Alconna::eval 的快捷指令: "echo" 删除成功' @alc.bind() # 绑定一个命令执行器, 若匹配成功则会传入参数, 自动执行命令执行器 def cb(content: str): eval(content, {}, {}) alc.parse('eval print(\\"hello world\\")') # hello world alc.parse("echo hello world!") # hello world! ``` 当 `fuzzy` 为 False 时,第一个例子中传入 `"涩图1张 abc"` 之类的快捷指令将视为解析失败 快捷指令允许三类特殊的 placeholder: - `{%X}`: 如 `setu {%0}`,表示此处填入快捷指令后随的第 X 个参数。 例如,若快捷指令为 `涩图`, 配置为 `{"command": "setu {%0}"}`, 则指令 `涩图 1` 相当于 `setu 1` - `{*}`: 表示此处填入所有后随参数,并且可以通过 `{*X}` 的方式指定组合参数之间的分隔符。 - `{X}`: 表示此处填入可能的正则匹配的组: - 若 `command` 中存在匹配组 `(xxx)`,则 `{X}` 表示第 X 个匹配组的内容 - 若 `command` 中存储匹配组 `(?P...)`, 则 `{X}` 表示 **名字** 为 X 的匹配结果 除此之外, 通过 **Alconna** 内置选项 `--shortcut` 可以动态操作快捷指令 例如: - `cmd --shortcut ` 来增加一个快捷指令 - `cmd --shortcut list` 来列出当前指令的所有快捷指令 - `cmd --shortcut delete key` 来删除一个快捷指令 ```python from arclet.alconna import Alconna, Args alc = Alconna("eval", Args["content", str]) alc.shortcut("echo", {"command": "eval print(\\'{*}\\')"}) alc.parse("eval --shortcut list") # 'echo' ``` ## 紧凑命令 `Alconna`, `Option` 与 `Subcommand` 可以设置 `compact=True` 使得解析命令时允许名称与后随参数之间没有分隔: ```python from arclet.alconna import Alconna, Option, CommandMeta, Args alc = Alconna("test", Args["foo", int], Option("BAR", Args["baz", str], compact=True), meta=CommandMeta(compact=True)) assert alc.parse("test123 BARabc").matched ``` 这使得我们可以实现如下命令: ```python from arclet.alconna import Alconna, Option, Args, append alc = Alconna("gcc", Option("--flag|-F", Args["content", str], action=append, compact=True)) print(alc.parse("gcc -Fabc -Fdef -Fxyz").query[list]("flag.content")) # ['abc', 'def', 'xyz'] ``` 当 `Option` 的 `action` 为 `count` 时,其自动支持 `compact` 特性: ```python from arclet.alconna import Alconna, Option, count alc = Alconna("pp", Option("--verbose|-v", action=count, default=0)) print(alc.parse("pp -vvv").query[int]("verbose.value")) # 3 ``` ## 模糊匹配 模糊匹配会应用在任意需要进行名称判断的地方,如 **命令名称**,**选项名称** 和 **参数名称** (如指定需要传入参数名称)。 ```python from arclet.alconna import Alconna, CommandMeta alc = Alconna("test_fuzzy", meta=CommandMeta(fuzzy_match=True)) alc.parse("test_fuzy") # test_fuzy is not matched. Do you mean "test_fuzzy"? ``` ## 半自动补全 半自动补全为用户提供了推荐后续输入的功能 补全默认通过 `--comp` 或 `-cp` 或 `?` 触发:(命名空间配置可修改名称) ```python from arclet.alconna import Alconna, Args, Option alc = Alconna("test", Args["abc", int]) + Option("foo") + Option("bar") alc.parse("test --comp") ''' output 以下是建议的输入: * * --help * -h * -sct * --shortcut * foo * bar ''' ``` ## Duplication **Duplication** 用来提供更好的自动补全,类似于 **ArgParse** 的 **Namespace** 普通情况下使用,需要利用到 **ArgsStub**、**OptionStub** 和 **SubcommandStub** 三个部分 以pip为例,其对应的 Duplication 应如下构造: ```python from arclet.alconna import Alconna, Args, Option, OptionResult, Duplication, SubcommandStub, Subcommand, count class MyDup(Duplication): verbose: OptionResult install: SubcommandStub alc = Alconna( "pip", Subcommand( "install", Args["package", str], Option("-r|--requirement", Args["file", str]), Option("-i|--index-url", Args["url", str]), ), Option("-v|--version"), Option("-v|--verbose", action=count), ) res = alc.parse("pip -v install ...") # 不使用duplication获得的提示较少 print(res.query("install")) # (value=Ellipsis args={'package': '...'} options={} subcommands={}) result = alc.parse("pip -v install ...", duplication=MyDup) print(result.install) # SubcommandStub(_origin=Subcommand('install', args=Args('package': str)), _value=Ellipsis, available=True, args=ArgsStub(_origin=Args('package': str), _value={'package': '...'}, available=True), dest='install', options=[OptionStub(_origin=Option('requirement', args=Args('file': str)), _value=None, available=False, args=ArgsStub(_origin=Args('file': str), _value={}, available=False), dest='requirement', aliases=['r', 'requirement'], name='requirement'), OptionStub(_origin=Option('index-url', args=Args('url': str)), _value=None, available=False, args=ArgsStub(_origin=Args('url': str), _value={}, available=False), dest='index-url', aliases=['index-url', 'i'], name='index-url')], subcommands=[], name='install') ``` **Duplication** 也可以如 **Namespace** 一样直接标明参数名称和类型: ```python from typing import Optional from arclet.alconna import Duplication class MyDup(Duplication): package: str file: Optional[str] = None url: Optional[str] = None ``` ## 上下文插值 当 `context_style` 条目被设置后,传入的命令中符合上下文插值的字段会被自动替换成当前上下文中的信息。 上下文可以在 `parse` 中传入: ```python from arclet.alconna import Alconna, Args, CommandMeta alc = Alconna("test", Args["foo", int], meta=CommandMeta(context_style="parentheses")) alc.parse("test $(bar)", {"bar": 123}) # {"foo": 123} ``` context_style 的值分两种: - `"bracket"`: 插值格式为 `{...}`,例如 `{foo}` - `"parentheses"`: 插值格式为 `$(...)`,例如 `$(bar)` ================================================ FILE: website/versioned_docs/version-2.4.4/best-practice/alconna/config.md ================================================ --- sidebar_position: 4 description: 配置项 --- # 配置项 ## alconna_auto_send_output - **类型**: `bool | None` - **默认值**: `None` 是否全局启用输出信息自动发送,不启用则会在触发特殊内置选项后仍然将解析结果传递至响应器。 ## alconna_use_command_start - **类型**: `bool` - **默认值**: `False` 是否读取 Nonebot 的配置项 `COMMAND_START` 来作为全局的 Alconna 命令前缀 ## alconna_global_completion - **类型**: [`CompConfig | None`](./matcher.mdx#补全会话) - **默认值**: `None` 全局的补全会话配置 (不代表全局启用补全会话)。 ## alconna_use_origin - **类型**: `bool` - **默认值**: `False` 是否全局使用原始消息 (即未经过 to_me 等处理的),该选项会影响到 Alconna 的匹配行为。 ## alconna_use_command_sep - **类型**: `bool` - **默认值**: `False` 是否读取 Nonebot 的配置项 `COMMAND_SEP` 来作为全局的 Alconna 命令分隔符。 ## alconna_global_extensions - **类型**: `list[str]` - **默认值**: `[]` 全局加载的扩展,其读取路径以 . 分隔,如 `foo.bar.baz:DemoExtension`。 对于内置扩展,路径为 `nonebot_plugin_alconna.builtins.extensions` 下的模块名,如 `ReplyMergeExtension`,可以使用 `@` 来缩写路径, 如 `@reply:ReplyMergeExtension`。 ## alconna_context_style - **类型**: `Optional[Literal["bracket", "parentheses"]]` - **默认值**: `None` 全局命令上下文插值的风格,None 为关闭,bracket 为 `{...}`,parentheses 为 `$(...)`。 ## alconna_enable_saa_patch - **类型**: `bool` - **默认值**: `False` 是否启用 SAA 补丁。 ## alconna_apply_filehost - **类型**: `bool` - **默认值**: `False` 是否启用文件托管。 ## alconna_apply_fetch_targets - **类型**: `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 自身发送的消息。 ================================================ FILE: website/versioned_docs/version-2.4.4/best-practice/alconna/matcher.mdx ================================================ --- 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("添加成功") ``` ## 声明 `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`:命令的源命令 - `: Match[]`:上述的匹配项,使用 `key` 作为查询路径 - `xxx: Query[] = Query(, default)`:上述的查询项,必需声明默认值以设置查询路径 `path` - 当用来查询选项/子命令是否存在时,可不写 `Query[]` - `xxx: Extension`:当前 `AlconnaMatcher` 使用的指定类型的匹配扩展 - `xxx: SelectedExtensions`:当前 `AlconnaMatcher` 使用的所有可用的匹配扩展 - `: `: 其他情况 - 当 `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. ```yaml title="zh-CN.yml" # 中文语言文件 demo: command: role-group: add: 添加 {name} 成功! ``` ```yaml title="en-US.yml" # 英文语言文件 demo: command: role-group: add: Add {name} successfully! ``` ```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("添加成功") ``` 此时,当用户输入 `添加教师` 时,会自动提示用户输入姓名,手机号和教师号,用户输入后会自动进入下一个提示: 补全会话配置如下: ```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] """进行补全会话时是否阻塞响应器""" ``` ================================================ FILE: website/versioned_docs/version-2.4.4/best-practice/alconna/shortcut.md ================================================ --- 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 ") .option("writer", "--anonymous", {"id": 0}) .usage("book [-w | --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 ") .option("writer", "--anonymous", {"id": 0}) .usage("book [-w | --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 " - name: writer opt: "--anonymous" default: id: 1 usage: book [-w | --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") ``` ================================================ FILE: website/versioned_docs/version-2.4.4/best-practice/alconna/uniseg/README.md ================================================ # 通用消息组件 `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) ``` ================================================ FILE: website/versioned_docs/version-2.4.4/best-practice/alconna/uniseg/_category_.json ================================================ { "label": "通用消息组件", "position": 5 } ================================================ FILE: website/versioned_docs/version-2.4.4/best-practice/alconna/uniseg/message.mdx ================================================ --- sidebar_position: 3 description: 消息序列 --- import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; # 通用消息序列 `uniseg` 提供了一个类似于 `Message` 的 `UniMessage` 类型,其元素为[通用消息段](./segment.md)。 你可以用如下方式获取 `UniMessage`: 通过提供的 `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) ... ``` 注意,`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) ``` ## 发送消息 你还可以通过 `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 = (attrs: dict, children: Segment[]) => T; type Transformer = boolean | Fragment | Render; ``` ### 字符串操作 类似于 `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 格式。 ================================================ FILE: website/versioned_docs/version-2.4.4/best-practice/alconna/uniseg/segment.md ================================================ --- 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 的处理方法。 ================================================ FILE: website/versioned_docs/version-2.4.4/best-practice/alconna/uniseg/utils.mdx ================================================ --- sidebar_position: 4 description: 辅助模型 --- import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; # 辅助功能 `uniseg` 模块同时提供了多种方法以通用消息操作。 :::note 这些方法中与 `event`, `bot` 相关的参数都会尝试从上下文中获取对象。 ::: ## 消息事件 ID 消息事件 ID 是用来标识当前消息事件的唯一 ID,通常用于回复/撤回/编辑/表态当前消息。 通过提供的 `MessageId` 或 `MsgId` 依赖注入器来获取消息事件 id。 ```python from nonebot_plugin_alconna.uniseg import MsgId matcher = on_xxx(...) @matcher.handle() asycn def _(msg_id: MsgId): ... ``` ```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) ``` :::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] """额外信息,用于适配器扩展""" ``` 通过提供的 `MessageTarget` 或 `MsgTarget` 依赖注入器来获取消息发送对象。 ```python from nonebot_plugin_alconna.uniseg import MsgTarget matcher = on_xxx(...) @matcher.handle() asycn def _(target: MsgTarget): ... ``` ```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) ``` 主动构造一个发送对象时,则需要如下参数: - `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()) ``` ================================================ FILE: website/versioned_docs/version-2.4.4/best-practice/data-storing.md ================================================ --- sidebar_position: 1 description: 存储数据文件到本地 --- # 数据存储 在使用插件的过程中,难免会需要存储一些持久化数据,例如用户的个人信息、群组的信息等。除了使用数据库等第三方存储之外,还可以使用本地文件来自行管理数据。NoneBot 提供了 `nonebot-plugin-localstore` 插件,可用于获取正确的数据存储路径并写入数据。 ## 安装插件 在使用前请先安装 `nonebot-plugin-localstore` 插件至项目环境中,可参考[获取商店插件](../tutorial/store.mdx#安装插件)来了解并选择安装插件的方式。如: 在**项目目录**下执行以下命令: ```bash nb plugin install nonebot-plugin-localstore ``` ## 使用插件 `nonebot-plugin-localstore` 插件兼容 Windows、Linux 和 macOS 等操作系统,使用时无需关心操作系统的差异。同时插件提供 `nb-cli` 脚本,可以使用 `nb localstore` 命令来检查数据存储路径。 在使用本插件前同样需要使用 `require` 方法进行**加载**并**导入**需要使用的方法,可参考 [跨插件访问](../advanced/requiring.md) 一节进行了解,如: ```python from nonebot import require require("nonebot_plugin_localstore") import nonebot_plugin_localstore as store # 获取插件缓存目录 cache_dir = store.get_plugin_cache_dir() # 获取插件缓存文件 cache_file = store.get_plugin_cache_file("file_name") # 获取插件数据目录 data_dir = store.get_plugin_data_dir() # 获取插件数据文件 data_file = store.get_plugin_data_file("file_name") # 获取插件配置目录 config_dir = store.get_plugin_config_dir() # 获取插件配置文件 config_file = store.get_plugin_config_file("file_name") ``` :::danger 警告 在 Windows 和 macOS 系统下,插件的数据目录和配置目录是同一个目录,因此在使用时需要注意避免文件名冲突。 ::: 插件提供的方法均返回一个 `pathlib.Path` 路径,可以参考 [pathlib 文档](https://docs.python.org/zh-cn/3/library/pathlib.html)来了解如何使用。常用的方法有: ```python from pathlib import Path data_file = store.get_plugin_data_file("file_name") # 写入文件内容 data_file.write_text("Hello World!") # 读取文件内容 data = data_file.read_text() ``` :::note 提示 对于嵌套插件,子插件的存储目录将位于父插件存储目录下。 ::: ## 配置项 ### localstore_use_cwd 使用当前工作目录作为数据存储目录,以下数据目录配置项默认值将会对应变更 默认值:`False` ```dotenv LOCALSTORE_USE_CWD=true ``` ### localstore_cache_dir 自定义缓存目录 默认值: 当 `localstore_use_cwd` 为 `True` 时,缓存目录为 `/cache`,否则: - macOS: `~/Library/Caches/nonebot2` - Unix: `~/.cache/nonebot2` (XDG default) - Windows: `C:\Users\\AppData\Local\nonebot2\Cache` ```dotenv LOCALSTORE_CACHE_DIR=/tmp/cache ``` ### localstore_data_dir 自定义数据目录 默认值: 当 `localstore_use_cwd` 为 `True` 时,数据目录为 `/data`,否则: - macOS: `~/Library/Application Support/nonebot2` - Unix: `~/.local/share/nonebot2` or in $XDG_DATA_HOME, if defined - Win XP (not roaming): `C:\Documents and Settings\\Application Data\nonebot2` - Win 7 (not roaming): `C:\Users\\AppData\Local\nonebot2` ```dotenv LOCALSTORE_DATA_DIR=/tmp/data ``` ### localstore_config_dir 自定义配置目录 默认值: 当 `localstore_use_cwd` 为 `True` 时,配置目录为 `/config`,否则: - macOS: same as user_data_dir - Unix: `~/.config/nonebot2` - Win XP (roaming): `C:\Documents and Settings\\Local Settings\Application Data\nonebot2` - Win 7 (roaming): `C:\Users\\AppData\Roaming\nonebot2` ```dotenv LOCALSTORE_CONFIG_DIR=/tmp/config ``` ### localstore_plugin_cache_dir 自定义插件缓存目录 默认值:`{}` ```dotenv LOCALSTORE_PLUGIN_CACHE_DIR=' { "plugin_id": "/tmp/plugin_cache" } ' ``` ### localstore_plugin_data_dir 自定义插件数据目录 默认值:`{}` ```dotenv LOCALSTORE_PLUGIN_DATA_DIR=' { "plugin_id": "/tmp/plugin_data" } ' ``` ### localstore_plugin_config_dir 自定义插件配置目录 默认值:`{}` ```dotenv LOCALSTORE_PLUGIN_CONFIG_DIR=' { "plugin_id": "/tmp/plugin_config" } ' ``` ================================================ FILE: website/versioned_docs/version-2.4.4/best-practice/database/README.mdx ================================================ import TabItem from "@theme/TabItem"; import Tabs from "@theme/Tabs"; # 数据库 [`nonebot-plugin-orm`](https://github.com/nonebot/plugin-orm) 是 NoneBot 的数据库支持插件。 本插件基于 [SQLAlchemy](https://www.sqlalchemy.org/) 和 [Alembic](https://alembic.sqlalchemy.org/),提供了许多与 NoneBot 紧密集成的功能: - 多 Engine / Connection 支持 - Session 管理 - 关系模型管理、依赖注入支持 - 数据库迁移 ## 安装 ```shell nb plugin install nonebot-plugin-orm ``` ```shell pip install nonebot-plugin-orm ``` ```shell pdm add nonebot-plugin-orm ``` ## 数据库驱动和后端 本插件只提供了 ORM 功能,没有数据库后端,也没有直接连接数据库后端的能力。 所以你需要另行安装数据库驱动和数据库后端,并且配置数据库连接信息。 ### SQLite [SQLite](https://www.sqlite.org/) 是一个轻量级的嵌入式数据库,它的数据以单文件的形式存储在本地,不需要单独的数据库后端。 SQLite 非常适合用于开发环境和小型应用,但是不适合用于大型应用的生产环境。 虽然不需要另行安装数据库后端,但你仍然需要安装数据库驱动: ```shell pip install "nonebot-plugin-orm[sqlite]" ``` ```shell pdm add "nonebot-plugin-orm[sqlite]" ``` 默认情况下,数据库文件为 `/nonebot-plugin-orm/db.sqlite3`(数据目录由 [nonebot-plugin-localstore](../data-storing) 提供)。 或者,你可以通过配置 `SQLALCHEMY_DATABASE_URL` 来指定数据库文件路径: ```shell SQLALCHEMY_DATABASE_URL=sqlite+aiosqlite:///file_path ``` ### PostgreSQL [PostgreSQL](https://www.postgresql.org/) 是世界上最先进的开源关系数据库之一,对各种高级且广泛应用的功能有最好的支持,是中小型应用的首选数据库。 ```shell pip install nonebot-plugin-orm[postgresql] ``` ```shell pdm add nonebot-plugin-orm[postgresql] ``` ```shell SQLALCHEMY_DATABASE_URL=postgresql+psycopg://user:password@host:port/dbname[?key=value&key=value...] ``` ### MySQL / MariaDB [MySQL](https://www.mysql.com/) 和 [MariaDB](https://mariadb.com/) 是经典的开源关系数据库,适合用于中小型应用。 ```shell pip install nonebot-plugin-orm[mysql] ``` ```shell pdm add nonebot-plugin-orm[mysql] ``` ```shell SQLALCHEMY_DATABASE_URL=mysql+aiomysql://user:password@host:port/dbname[?key=value&key=value...] ``` ## 使用 本插件提供了数据库迁移功能(此功能依赖于 [nb-cli 脚手架](../../quick-start#安装脚手架))。 在安装了新的插件或机器人之后,你需要执行一次数据库迁移操作,将数据库同步至与机器人一致的状态: ```shell nb orm upgrade ``` 运行完毕后,可以检查一下: ```shell nb orm check ``` 如果输出是 `没有检测到新的升级操作`,那么恭喜你,数据库已经迁移完成了,你可以启动机器人了。 ================================================ FILE: website/versioned_docs/version-2.4.4/best-practice/database/_category_.json ================================================ { "label": "数据库", "position": 7 } ================================================ FILE: website/versioned_docs/version-2.4.4/best-practice/database/developer/README.md ================================================ # 开发者指南 开发者指南内容较多,故分为了一个示例以及数个专题。 阅读(并且最好跟随实践)示例后,你将会对使用 `nonebot-plugin-orm` 开发插件有一个基本的认识。 如果想要更深入地学习关于 [SQLAlchemy](https://www.sqlalchemy.org/) 和 [Alembic](https://alembic.sqlalchemy.org/) 的知识,或者在使用过程中遇到了问题,可以查阅专题以及其官方文档。 ## 示例 ### 模型定义 首先,我们需要设计存储的数据的结构。 例如天气插件,需要存储**什么地方 (`location`)** 的**天气是什么 (`weather`)**。 其中,一个地方只会有一种天气,而不同地方可能有相同的天气。 所以,我们可以设计出如下的模型: ```python title=weather/__init__.py showLineNumbers from nonebot_plugin_orm import Model from sqlalchemy.orm import Mapped, mapped_column class Weather(Model): location: Mapped[str] = mapped_column(primary_key=True) weather: Mapped[str] ``` 其中,`primary_key=True` 意味着此列 (`location`) 是主键,即内容是唯一的且非空的。 每一个模型必须有至少一个主键。 我们可以用以下代码检查模型生成的数据库模式是否正确: ```python from sqlalchemy.schema import CreateTable print(CreateTable(Weather.__table__)) ``` ```sql CREATE TABLE weather_weather ( location VARCHAR NOT NULL, weather VARCHAR NOT NULL, CONSTRAINT pk_weather_weather PRIMARY KEY (location) ) ``` 可以注意到表名是 `weather_weather` 而不是 `Weather` 或者 `weather`。 这是因为 `nonebot-plugin-orm` 会自动为模型生成一个表名,规则是:`<插件模块名>_<类名小写>`。 你也可以通过指定 `__tablename__` 属性来自定义表名: ```python {2} class Weather(Model): __tablename__ = "weather" ... ``` ```sql {1} CREATE TABLE weather ( ... ) ``` 但是,并不推荐你这么做,因为这可能会导致不同插件间的表名重复,引发冲突。 特别是当你会发布插件时,你并不知道其他插件会不会使用相同的表名。 ### 首次迁移 我们成功定义了模型,现在启动机器人试试吧: ```shell $ nb run 01-02 15:04:05 [SUCCESS] nonebot | NoneBot is initializing... 01-02 15:04:05 [ERROR] nonebot_plugin_orm | 启动检查失败 01-02 15:04:05 [ERROR] nonebot | Application startup failed. Exiting. Traceback (most recent call last): ... click.exceptions.UsageError: 检测到新的升级操作: [('add_table', Table('weather', MetaData(), Column('location', String(), table=, primary_key=True, nullable=False), Column('weather', String(), table=, nullable=False), schema=None))] ``` 咦,发生了什么? `nonebot-plugin-orm` 试图阻止我们启动机器人。 原来是我们定义了模型,但是数据库中并没有对应的表,这会导致插件不能正常运行。 所以,我们需要迁移数据库。 首先,我们需要创建一个迁移脚本: ```shell nb orm revision -m "first revision" --branch-label weather ``` 其中,`-m` 参数是迁移脚本的描述,`--branch-label` 参数是迁移脚本的分支,一般为插件模块名。 执行命令过后,出现了一个 `weather/migrations` 目录,其中有一个 `xxxxxxxxxxxx_first_revision.py` 文件: ```shell {4,5} weather ├── __init__.py ├── config.py └── migrations └── xxxxxxxxxxxx_first_revision.py ``` 这就是我们创建的迁移脚本,它记录了数据库模式的变化。 我们可以查看一下它的内容: ```python title=weather/migrations/xxxxxxxxxxxx_first_revision.py {25-33,39-41} showLineNumbers """first revision 迁移 ID: xxxxxxxxxxxx 父迁移: 创建时间: 2006-01-02 15:04:05.999999 """ from __future__ import annotations from collections.abc import Sequence import sqlalchemy as sa from alembic import op revision: str = "xxxxxxxxxxxx" down_revision: str | Sequence[str] | None = None branch_labels: str | Sequence[str] | None = ("weather",) depends_on: str | Sequence[str] | None = None def upgrade(name: str = "") -> None: if name: return # ### commands auto generated by Alembic - please adjust! ### op.create_table( "weather_weather", sa.Column("location", sa.String(), nullable=False), sa.Column("weather", sa.String(), nullable=False), sa.PrimaryKeyConstraint("location", name=op.f("pk_weather_weather")), info={"bind_key": "weather"}, ) # ### end Alembic commands ### def downgrade(name: str = "") -> None: if name: return # ### commands auto generated by Alembic - please adjust! ### op.drop_table("weather_weather") # ### end Alembic commands ### ``` 可以注意到脚本的主体部分(其余是模版代码,请勿修改)是: ```python # ### commands auto generated by Alembic - please adjust! ### op.create_table( # CREATE TABLE "weather_weather", # weather_weather sa.Column("location", sa.String(), nullable=False), # location VARCHAR NOT NULL, sa.Column("weather", sa.String(), nullable=False), # weather VARCHAR NOT NULL, sa.PrimaryKeyConstraint("location", name=op.f("pk_weather_weather")), # CONSTRAINT pk_weather_weather PRIMARY KEY (location) info={"bind_key": "weather"}, ) # ### end Alembic commands ### ``` ```python # ### commands auto generated by Alembic - please adjust! ### op.drop_table("weather_weather") # DROP TABLE weather_weather; # ### end Alembic commands ### ``` 虽然我们不是很懂这些代码的意思,但是可以注意到它们几乎与 SQL 语句 (DDL) 一一对应。 显然,它们是用来创建和删除表的。 我们还可以注意到,`upgrade()` 和 `downgrade()` 函数中的代码是**互逆**的。 也就是说,执行一次 `upgrade()` 函数,再执行一次 `downgrade()` 函数后,数据库的模式就会回到原来的状态。 这就是迁移脚本的作用:记录数据库模式的变化,以便我们在不同的环境中(例如开发环境和生产环境)**可复现地**、**可逆地**同步数据库模式,正如 git 对我们的代码做的事情那样。 对了,不要忘记还有一段注释:`commands auto generated by Alembic - please adjust!`。 它在提醒我们,这些代码是由 Alembic 自动生成的,我们应该检查它们,并且根据需要进行调整。 :::caution 注意 迁移脚本冗长且繁琐,我们一般不会手写它们,而是由 Alembic 自动生成。 一般情况下,Alembic 足够智能,可以正确地生成迁移脚本。 但是,在复杂或有歧义的情况下,我们可能需要手动调整迁移脚本。 所以,**永远要检查迁移脚本,并且在开发环境中测试!** **迁移脚本中任何一处错误都足以使数据付之东流!** ::: 确定迁移脚本正确后,我们就可以执行迁移脚本,将数据库模式同步到数据库中: ```shell nb orm upgrade ``` 现在,我们可以正常启动机器人了。 开发过程中,我们可能会频繁地修改模型,这意味着我们需要频繁地创建并执行迁移脚本,非常繁琐。 实际上,此时我们不在乎数据安全,只需要数据库模式与模型定义一致即可。 所以,我们可以关闭 `nonebot-plugin-orm` 的启动检查: ```shell title=.env.dev ALEMBIC_STARTUP_CHECK=false ``` 现在,每次启动机器人时,数据库模式会自动与模型定义同步,无需手动迁移。 ### 会话管理 我们已经成功定义了模型,并且迁移了数据库,现在可以开始使用数据库了……吗? 并不能,因为模型只是数据结构的定义,并不能通过它操作数据(如果你曾经使用过 [Tortoise ORM](https://tortoise.github.io/),可能会知道 `await Weather.get(location="上海")` 这样的面向对象编程。 但是 SQLAlchemy 不同,选择了命令式编程)。 我们需要使用**会话**操作数据: ```python title=weather/__init__.py {10,13} showLineNumbers from nonebot import on_command from nonebot.adapters import Message from nonebot.params import CommandArg from nonebot_plugin_orm import async_scoped_session weather = on_command("天气") @weather.handle() async def _(session: async_scoped_session, args: Message = CommandArg()): location = args.extract_plain_text() if wea := await session.get(Weather, location): await weather.finish(f"今天{location}的天气是{wea.weather}") await weather.finish(f"未查询到{location}的天气") ``` 我们通过 `session: async_scoped_session` 依赖注入获得了一个会话,然后使用 `await session.get(Weather, location)` 查询数据库。 `async_scoped_session` 是一个有作用域限制的会话,作用域为当前事件、当前事件响应器。 会话产生的模型实例(例如此处的 `wea := await session.get(Weather, location)`)作用域与会话相同。 :::caution 注意 此处提到的“会话”指的是 ORM 会话,而非 [NoneBot 会话](../../../appendices/session-control),两者的生命周期也是不同的(NoneBot 会话的生命周期中可能包含多个事件,不同的事件也会有不同的事件响应器)。 具体而言,就是不要将 ORM 会话和模型实例存储在 NoneBot 会话状态中: ```python {12} from nonebot.params import ArgPlainText from nonebot.typing import T_State @weather.got("location", prompt="请输入地名") async def _(state: T_State, session: async_scoped_session, location: str = ArgPlainText()): wea = await session.get(Weather, location) if not wea: await weather.finish(f"未查询到{location}的天气") state["weather"] = wea # 不要这么做,除非你知道自己在做什么 ``` 当然非要这么做也不是不可以: ```python {6} @weather.handle() async def _(state: T_State, session: async_scoped_session): # 通过 await session.merge(state["weather"]) 获得了此 ORM 会话中的相应模型实例, # 而非直接使用会话状态中的模型实例, # 因为先前的 ORM 会话已经关闭了。 wea = await session.merge(state["weather"]) await weather.finish(f"今天{state['location']}的天气是{wea.weather}") ``` ::: 当有数据更改时,我们需要提交事务,也要注意会话作用域问题: ```python title=weather/__init__.py {12,20} showLineNumbers from nonebot.params import Depends async def get_weather( session: async_scoped_session, args: Message = CommandArg() ) -> Weather: location = args.extract_plain_text() if not (wea := await session.get(Weather, location)): wea = Weather(location=location, weather="未知") session.add(wea) # await session.commit() # 不应该在其他地方提交事务 return wea @weather.handle() async def _(session: async_scoped_session, wea: Weather = Depends(get_weather)): await weather.send(f"今天的天气是{wea.weather}") await session.commit() # 而应该在事件响应器结束前提交事务 ``` 当然我们也可以获得一个新的会话,不过此时就要手动管理会话了: ```python title=weather/__init__.py {5-6} showLineNumbers from nonebot_plugin_orm import get_session async def get_weather(location: str) -> str: session = get_session() async with session.begin(): wea = await session.get(Weather, location) if not wea: wea = Weather(location=location, weather="未知") session.add(wea) return wea.weather @weather.handle() async def _(args: Message = CommandArg()): wea = await get_weather(args.extract_plain_text()) await weather.send(f"今天的天气是{wea}") ``` ### 依赖注入 在上面的示例中,我们都是通过会话获得数据的。 不过,我们也可以通过依赖注入获得数据: ```python title=weather/__init__.py {12-14} showLineNumbers from sqlalchemy import select from nonebot.params import Depends from nonebot_plugin_orm import SQLDepends def extract_arg_plain_text(args: Message = CommandArg()) -> str: return args.extract_plain_text() @weather.handle() async def _( wea: Weather = SQLDepends( select(Weather).where(Weather.location == Depends(extract_arg_plain_text)) ), ): await weather.send(f"今天的天气是{wea.weather}") ``` 其中,`SQLDepends` 是一个特殊的依赖注入,它会根据类型标注和 SQL 语句提供数据,SQL 语句中也可以有子依赖。 不同的类型标注也会获得不同形式的数据: ```python title=weather/__init__.py {5} showLineNumbers from collections.abc import Sequence @weather.handle() async def _( weas: Sequence[Weather] = SQLDepends( select(Weather).where(Weather.weather == Depends(extract_arg_plain_text)) ), ): await weather.send(f"今天的天气是{weas[0].weather}的城市有{','.join(wea.location for wea in weas)}") ``` 支持的类型标注请参见 [依赖注入](dependency)。 我们也可以像 [类作为依赖](../../../advanced/dependency#类作为依赖) 那样,在类属性中声明子依赖: ```python title=weather/__init__.py {5-6,10} showLineNumbers from collections.abc import Sequence class Weather(Model): location: Mapped[str] = mapped_column(primary_key=True) weather: Mapped[str] = Depends(extract_arg_plain_text) # weather: Annotated[Mapped[str], Depends(extract_arg_plain_text)] # Annotated 支持 @weather.handle() async def _(weas: Sequence[Weather]): await weather.send( f"今天的天气是{weas[0].weather}的城市有{','.join(wea.location for wea in weas)}" ) ``` ================================================ FILE: website/versioned_docs/version-2.4.4/best-practice/database/developer/_category_.json ================================================ { "label": "开发者指南", "position": 3 } ================================================ FILE: website/versioned_docs/version-2.4.4/best-practice/database/developer/dependency.md ================================================ --- sidebar_position: 3 description: 依赖注入 --- # 依赖注入 `nonebot-plugin-orm` 提供了强大且灵活的依赖注入,可以方便地帮助你获取数据库会话和查询数据。 ## 数据库会话 ### AsyncSession 新数据库会话,常用于有独立的数据库操作逻辑的插件。 ```python {13,26} from nonebot import on_message from nonebot.params import Depends from nonebot_plugin_orm import AsyncSession, Model, async_scoped_session from sqlalchemy.orm import Mapped, mapped_column message = on_message() class Message(Model): id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) async def get_message(session: AsyncSession) -> Message: # 等价于 session = get_session() async with session: msg = Message() session.add(msg) await session.commit() await session.refresh(msg) return msg @message.handle() async def _(session: async_scoped_session, msg: Message = Depends(get_message)): await session.rollback() # 无法回退 get_message() 中的更改 await message.send(str(msg.id)) # msg 被存储,msg.id 递增 ``` ### async_scoped_session 数据库作用域会话,常用于事件响应器和有与响应逻辑相关的数据库操作逻辑的插件。 ```python {13,26} from nonebot import on_message from nonebot.params import Depends from nonebot_plugin_orm import Model, async_scoped_session from sqlalchemy.orm import Mapped, mapped_column message = on_message() class Message(Model): id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) async def get_message(session: async_scoped_session) -> Message: # 等价于 session = get_scoped_session() msg = Message() session.add(msg) await session.flush() await session.refresh(msg) return msg @message.handle() async def _(session: async_scoped_session, msg: Message = Depends(get_message)): await session.rollback() # 可以回退 get_message() 中的更改 await message.send(str(msg.id)) # msg 没有被存储,msg.id 不变 ``` ## 查询数据 ### Model 支持类作为依赖。 ```python from typing import Annotated from nonebot.params import Depends from nonebot_plugin_orm import Model from sqlalchemy.orm import Mapped, mapped_column def get_id() -> int: ... class Message(Model): id: Annotated[Mapped[int], Depends(get_id)] = mapped_column( primary_key=True, autoincrement=True ) async def _(msg: Message): # 等价于 msg = ( # await (await session.stream(select(Message).where(Message.id == get_id()))) # .scalars() # .one_or_none() # ) ... ``` ### SQLDepends 参数为一个 SQL 语句,决定依赖注入的内容,SQL 语句中可以使用子依赖。 ```python {11-13} from nonebot.params import Depends from nonebot_plugin_orm import Model, SQLDepends from sqlalchemy import select def get_id() -> int: ... async def _( model: Model = SQLDepends(select(Model).where(Model.id == Depends(get_id))), ): ... ``` 参数可以是任意 SQL 语句,但不建议使用 `select` 以外的语句,因为语句可能没有返回值(`returning` 除外),而且代码不清晰。 ### 类型标注 类型标注决定依赖注入的数据结构,主要影响以下几个层面: - 迭代器(`session.execute()`)或异步迭代器(`session.stream()`) - 标量(`session.execute().scalars()`)或元组(`session.execute()`) - 一个(`session.execute().one_or_none()`,注意 `None` 时可能触发 [重载](../../../appendices/overload#重载))或全部(`session.execute()` / `session.execute().all()`) - 连续(`session().execute()`)或分块(`session.execute().partitions()`) 具体如下(可以使用父类型作为类型标注): - ```python async def _(rows_partitions: AsyncIterator[Sequence[Tuple[Model, ...]]]): # 等价于 rows_partitions = await (await session.stream(sql).partitions()) async for partition in rows_partitions: for row in partition: print(row[0], row[1], ...) ``` - ```python async def _(model_partitions: AsyncIterator[Sequence[Model]]): # 等价于 model_partitions = await (await session.stream(sql).scalars().partitions()) async for partition in model_partitions: for model in partition: print(model) ``` - ```python async def _(row_partitions: Iterator[Sequence[Tuple[Model, ...]]]): # 等价于 row_partitions = await session.execute(sql).partitions() for partition in rows_partitions: for row in partition: print(row[0], row[1], ...) ``` - ```python async def _(model_partitions: Iterator[Sequence[Model]]): # 等价于 model_partitions = await (await session.execute(sql).scalars().partitions()) for partition in model_partitions: for model in partition: print(model) ``` - ```python async def _(rows: sa_async.AsyncResult[Tuple[Model, ...]]): # 等价于 rows = await session.stream(sql) async for row in rows: print(row[0], row[1], ...) ``` - ```python async def _(models: sa_async.AsyncScalarResult[Model]): # 等价于 models = await session.stream(sql).scalars() async for model in models: print(model) ``` - ```python async def _(rows: sa.Result[Tuple[Model, ...]]): # 等价于 rows = await session.execute(sql) for row in rows: print(row[0], row[1], ...) ``` - ```python async def _(models: sa.ScalarResult[Model]): # 等价于 models = await session.execute(sql).scalars() for model in models: print(model) ``` - ```python async def _(rows: Sequence[Tuple[Model, ...]]): # 等价于 rows = await (await session.stream(sql).all()) for row in rows: print(row[0], row[1], ...) ``` - ```python async def _(models: Sequence[Model]): # 等价于 models = await (await session.stream(sql).scalars().all()) for model in models: print(model) ``` - ```python async def _(row: Tuple[Model, ...]): # 等价于 row = await (await session.stream(sql).one_or_none()) print(row[0], row[1], ...) ``` - ```python async def _(model: Model): # 等价于 model = await (await session.stream(sql).scalars().one_or_none()) print(model) ``` ================================================ FILE: website/versioned_docs/version-2.4.4/best-practice/database/developer/test.md ================================================ --- sidebar_position: 2 description: 测试 --- # 测试 百思不如一试,测试是发现问题的最佳方式。 不同的用户会有不同的配置,为了提高项目的兼容性,我们需要在不同数据库后端上测试。 手动进行大量的、重复的测试不可靠,也不现实,因此我们推荐使用 [GitHub Actions](https://github.com/features/actions) 进行自动化测试: ```yaml title=.github/workflows/test.yml {12-42,52-53} showLineNumbers name: Test on: push: branches: - main jobs: test: runs-on: ubuntu-latest strategy: matrix: db: - sqlite+aiosqlite:///db.sqlite3 - postgresql+psycopg://postgres:postgres@localhost:5432/postgres - mysql+aiomysql://mysql:mysql@localhost:3306/mymysql fail-fast: false env: SQLALCHEMY_DATABASE_URL: ${{ matrix.db }} services: postgresql: image: ${{ startsWith(matrix.db, 'postgresql') && 'postgres' || '' }} env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: postgres ports: - 5432:5432 mysql: image: ${{ startsWith(matrix.db, 'mysql') && 'mysql' || '' }} env: MYSQL_ROOT_PASSWORD: mysql MYSQL_USER: mysql MYSQL_PASSWORD: mysql MYSQL_DATABASE: mymysql ports: - 3306:3306 steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 - name: Install dependencies run: pip install -r requirements.txt - name: Run migrations run: pipx run nb-cli orm upgrade - name: Run tests run: pytest ``` 如果项目还需要考虑跨平台和跨 Python 版本兼容,测试矩阵中还需要增加这两个维度。 但是,我们没必要在所有平台和 Python 版本上运行所有数据库的测试,因为很显然,PostgreSQL 和 MySQL 这类独立的数据库后端不会受平台和 Python 影响,而且 Github Actions 的非 Linux 平台不支持运行独立服务: | | Python 3.9 | Python 3.10 | Python 3.11 | Python 3.12 | | ----------- | ---------- | ----------- | ----------- | --------------------------- | | **Linux** | SQLite | SQLite | SQLite | SQLite / PostgreSQL / MySQL | | **Windows** | SQLite | SQLite | SQLite | SQLite | | **macOS** | SQLite | SQLite | SQLite | SQLite | ```yaml title=.github/workflows/test.yml {12-24} showLineNumbers name: Test on: push: branches: - main jobs: test: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] python-version: ["3.9", "3.10", "3.11", "3.12"] db: ["sqlite+aiosqlite:///db.sqlite3"] include: - os: ubuntu-latest python-version: "3.12" db: postgresql+psycopg://postgres:postgres@localhost:5432/postgres - os: ubuntu-latest python-version: "3.12" db: mysql+aiomysql://mysql:mysql@localhost:3306/mymysql fail-fast: false env: SQLALCHEMY_DATABASE_URL: ${{ matrix.db }} services: postgresql: image: ${{ startsWith(matrix.db, 'postgresql') && 'postgres' || '' }} env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: postgres ports: - 5432:5432 mysql: image: ${{ startsWith(matrix.db, 'mysql') && 'mysql' || '' }} env: MYSQL_ROOT_PASSWORD: mysql MYSQL_USER: mysql MYSQL_PASSWORD: mysql MYSQL_DATABASE: mymysql ports: - 3306:3306 steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: pip install -r requirements.txt - name: Run migrations run: pipx run nb-cli orm upgrade - name: Run tests run: pytest ``` ================================================ FILE: website/versioned_docs/version-2.4.4/best-practice/database/user.md ================================================ --- sidebar_position: 2 description: 用户指南 --- # 用户指南 `nonebot-plugin-orm` 功能强大且复杂,使用上有一定难度。 不过,对于用户而言,只需要掌握部分功能即可。 :::caution 注意 请注意区分插件的项目名(如:`nonebot-plugin-wordcloud`)和模块名(如:`nonebot_plugin_wordcloud`)。`nonebot-plugin-orm` 中统一使用插件模块名。参见 [插件命名规范](../../developer/plugin-publishing#插件命名规范)。 ::: ## 示例 ### 创建新机器人 我们想要创建一个机器人,并安装 `nonebot-plugin-wordcloud` 插件,只需要执行以下命令: ```shell nb init # 初始化项目文件夹 pip install nonebot-plugin-orm[sqlite] # 安装 nonebot-plugin-orm,并附带 SQLite 支持 nb plugin install nonebot-plugin-wordcloud # 安装插件 # nb orm heads # 查看有什么插件使用到了数据库(可选) nb orm upgrade # 升级数据库 # nb orm check # 检查一下数据库模式是否与模型定义一致(可选) nb run # 启动机器人 ``` ### 卸载插件 我们已经安装了 `nonebot-plugin-wordcloud` 插件,但是现在想要卸载它,并且**删除它的数据**,只需要执行以下命令: ```shell nb plugin uninstall nonebot-plugin-wordcloud # 卸载插件 # nb orm heads # 查看有什么插件使用到了数据库。(可选) nb orm downgrade nonebot_plugin_wordcloud@base # 降级数据库,删除数据 # nb orm check # 检查一下数据库模式是否与模型定义一致(可选) ``` ## CLI 接下来,让我们了解下示例中出现的 CLI 命令的含义: ### heads 显示所有的分支头。一般一个分支对应一个插件。 ```shell nb orm heads ``` 输出格式为 `<迁移 ID> (<插件模块名>) (<头部类型>)`: ``` 46327b837dd8 (nonebot_plugin_chatrecorder) (head) 9492159f98f7 (nonebot_plugin_user) (head) 71a72119935f (nonebot_plugin_session_orm) (effective head) ade8cdca5470 (nonebot_plugin_wordcloud) (head) ``` ### upgrade 升级数据库。每次安装新的插件或更新插件版本后,都需要执行此命令。 ```shell nb orm upgrade <插件模块名>@<迁移 ID> ``` 其中,`<插件模块名>@<迁移 ID>` 是可选参数。如果不指定,则会将所有分支升级到最新版本,这也是最常见的用法: ```shell nb orm upgrade ``` ### downgrade 降级数据库。当需要回滚插件版本或删除插件时,可以执行此命令。 ```shell nb orm downgrade <插件模块名>@<迁移 ID> ``` 其中,`<迁移 ID>` 也可以是 `base`,即回滚到初始状态。常用于卸载插件后删除其数据: ```shell nb orm downgrade <插件模块名>@base ``` ### check 检查数据库模式是否与模型定义一致。机器人启动前会自动运行此命令(`ALEMBIC_STARTUP_CHECK=true` 时),并在检查失败时阻止启动。 ```shell nb orm check ``` ## 配置 ### sqlalchemy_database_url 默认数据库连接 URL。参见 [数据库驱动和后端](.#数据库驱动和后端) 和 [引擎配置 — SQLAlchemy 2.0 文档](https://docs.sqlalchemy.org/en/20/core/engines.html#database-urls)。 ```shell SQLALCHEMY_DATABASE_URL=dialect+driver://username:password@host:port/database ``` ### sqlalchemy_bind bind keys(一般为插件模块名)到数据库连接 URL、[`create_async_engine()`](https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html#sqlalchemy.ext.asyncio.create_async_engine) 参数字典或 [`AsyncEngine`](https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html#sqlalchemy.ext.asyncio.AsyncEngine) 实例的字典。 例如,我们想要让 `nonebot-plugin-wordcloud` 插件使用一个 SQLite 数据库,并开启 [Echo 选项](https://docs.sqlalchemy.org/en/20/core/engines.html#sqlalchemy.create_engine.params.echo) 便于 debug,而其他插件使用默认的 PostgreSQL 数据库,可以这样配置: ```shell SQLALCHEMY_BINDS='{ "": "postgresql+psycopg://scott:tiger@localhost/mydatabase", "nonebot_plugin_wordcloud": { "url": "sqlite+aiosqlite://", "echo": true } }' ``` ### sqlalchemy_engine_options [`create_async_engine()`](https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html#sqlalchemy.ext.asyncio.create_async_engine) 默认参数字典。 ```shell SQLALCHEMY_ENGINE_OPTIONS='{ "pool_size": 5, "max_overflow": 10, "pool_timeout": 30, "pool_recycle": 3600, "echo": true }' ``` ### sqlalchemy_echo 开启 [Echo 选项](https://docs.sqlalchemy.org/en/20/core/engines.html#sqlalchemy.create_engine.params.echo) 和 [Echo Pool 选项](https://docs.sqlalchemy.org/en/20/core/engines.html#sqlalchemy.create_engine.params.echo_pool) 便于 debug。 ```shell SQLALCHEMY_ECHO=true ``` :::caution 注意 以上配置之间有覆盖关系,遵循特殊优先于一般的原则,具体为 [`sqlalchemy_database_url`](#sqlalchemy_database_url) > [`sqlalchemy_bind`](#sqlalchemy_bind) > [`sqlalchemy_echo`](#sqlalchemy_echo) > [`sqlalchemy_engine_options`](#sqlalchemy_engine_options)。 但覆盖顺序并非显而易见,出于清晰考虑,请只配置必要的选项。 ::: ================================================ FILE: website/versioned_docs/version-2.4.4/best-practice/deployment.mdx ================================================ --- sidebar_position: 3 description: 部署你的机器人 --- # 部署 import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; 在编写完成各类插件后,我们需要长期运行机器人来使得用户能够正常使用。通常,我们会使用云服务器来部署机器人。 我们在开发插件时,机器人运行的环境称为开发环境;而在部署后,机器人运行的环境称为生产环境。与开发环境不同的是,在生产环境中,开发者通常不能随意地修改/添加/删除代码,开启或停止服务。 ## 部署前准备 ### 项目依赖管理 由于部署后的机器人运行在生产环境中,因此,为确保机器人能够正常运行,我们需要保证机器人的运行环境与开发环境一致。我们可以通过以下几种方式来进行依赖管理: [Poetry](https://python-poetry.org/) 是一个 Python 项目的依赖管理工具。它可以通过声明项目所依赖的库,为你管理(安装/更新)它们。Poetry 提供了一个 `poetry.lock` 文件,以确保可重复安装,并可以构建用于分发的项目。 Poetry 会在安装依赖时自动生成 `poetry.lock` 文件,在**项目目录**下执行以下命令: ```bash # 初始化 poetry 配置 poetry init # 添加项目依赖,这里以 nonebot2[fastapi] 为例 poetry add nonebot2[fastapi] ``` [PDM](https://pdm.fming.dev/) 是一个现代 Python 项目的依赖管理工具。它采用 [PEP621](https://www.python.org/dev/peps/pep-0621/) 标准,依赖解析快速;同时支持 [PEP582](https://www.python.org/dev/peps/pep-0582/) 和 [virtualenv](https://virtualenv.pypa.io/)。PDM 提供了一个 `pdm.lock` 文件,以确保可重复安装,并可以构建用于分发的项目。 PDM 会在安装依赖时自动生成 `pdm.lock` 文件,在**项目目录**下执行以下命令: ```bash # 初始化 pdm 配置 pdm init # 添加项目依赖,这里以 nonebot2[fastapi] 为例 pdm add nonebot2[fastapi] ``` [pip](https://pip.pypa.io/) 是 Python 包管理工具。他并不是一个依赖管理工具,为了尽可能保证环境的一致性,我们可以使用 `requirements.txt` 文件来声明依赖。 ```bash pip freeze > requirements.txt ``` ### 安装 Docker [Docker](https://www.docker.com/) 是一个应用容器引擎,可以让开发者打包应用以及依赖包到一个可移植的镜像中,然后发布到服务器上。 我们可以参考 [Docker 官方文档](https://docs.docker.com/get-docker/) 来安装 Docker 。 在 Linux 上,我们可以使用以下一键脚本来安装 Docker 以及 Docker Compose Plugin: ```bash curl -fsSL https://get.docker.com | sh -s -- --mirror Aliyun ``` 在 Windows/macOS 上,我们可以使用 [Docker Desktop](https://docs.docker.com/desktop/) 来安装 Docker 以及 Docker Compose Plugin。 ### 安装脚手架 Docker 插件 我们可以使用 [nb-cli-plugin-docker](https://github.com/nonebot/cli-plugin-docker) 来快速部署机器人。 插件可以帮助我们生成配置文件并构建 Docker 镜像,以及启动/停止/重启机器人。使用以下命令安装脚手架 Docker 插件: ```bash nb self install nb-cli-plugin-docker ``` ## Docker 部署 ### 快速部署 使用脚手架命令即可一键生成配置并部署: ```bash nb docker up ``` 当看到 `Running` 字样时,说明机器人已经启动成功。我们可以通过以下命令来查看机器人的运行日志: ```bash nb docker logs ``` ```bash docker compose logs ``` 如果需要停止机器人,我们可以使用以下命令: ```bash nb docker down ``` ```bash docker compose down ``` ### 自定义部署 在部分情况下,我们需要事先生成 Docker 配置文件,再到生产环境进行部署;或者自动生成的配置文件并不能满足复杂场景,需要根据实际需求手动修改配置文件。我们可以使用以下命令来生成基础配置文件: ```bash nb docker generate ``` nb-cli 将会在项目目录下生成 `docker-compose.yml` 和 `Dockerfile` 等配置文件。在 nb-cli 完成配置文件的生成后,我们可以根据部署环境的实际情况使用 nb-cli 或者 Docker Compose 来启动机器人。 我们可以参考 [Dockerfile 文件规范](https://docs.docker.com/engine/reference/builder/)和 [Compose 文件规范](https://docs.docker.com/compose/compose-file/)修改这两个文件。 修改完成后我们可以直接启动或者手动构建镜像: ```bash # 启动机器人 nb docker up # 手动构建镜像 nb docker build ``` ```bash # 启动机器人 docker compose up -d # 手动构建镜像 docker compose build ``` ### 持续集成 我们可以使用 GitHub Actions 来实现持续集成(CI),我们只需要在 GitHub 上发布 Release 即可自动构建镜像并推送至镜像仓库。 首先,我们需要在 [Docker Hub](https://hub.docker.com/) (或者其他平台,如:[GitHub Packages](https://github.com/features/packages)、[阿里云容器镜像服务](https://www.alibabacloud.com/zh/product/container-registry)等)上创建镜像仓库,用于存放镜像。 前往项目仓库的 `Settings` > `Secrets` > `actions` 栏目 `New Repository Secret` 添加构建所需的密钥: - `DOCKERHUB_USERNAME`: 你的 Docker Hub 用户名 - `DOCKERHUB_TOKEN`: 你的 Docker Hub PAT([创建方法](https://docs.docker.com/docker-hub/access-tokens/)) 将以下文件添加至**项目目录**下的 `.github/workflows/` 目录下,并将文件中高亮行中的仓库名称替换为你的仓库名称: ```yaml title=.github/workflows/build.yml name: Docker Hub Release on: push: tags: - "v*" jobs: docker: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 - name: Set up QEMU uses: docker/setup-qemu-action@v2 - name: Setup Docker uses: docker/setup-buildx-action@v2 - name: Login to DockerHub uses: docker/login-action@v2 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Generate Tags uses: docker/metadata-action@v4 id: metadata with: images: | # highlight-next-line {organization}/{repository} tags: | type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=sha type=raw,value=latest - name: Build and Publish uses: docker/build-push-action@v4 with: context: . push: true tags: ${{ steps.metadata.outputs.tags }} labels: ${{ steps.metadata.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max ``` ### 持续部署 在完成发布并构建镜像后,我们可以自动将镜像部署到服务器上。 前往项目仓库的 `Settings` > `Secrets` > `actions` 栏目 `New Repository Secret` 添加部署所需的密钥: - `DEPLOY_HOST`: 部署服务器的 SSH 地址 - `DEPLOY_USER`: 部署服务器用户名 - `DEPLOY_KEY`: 部署服务器私钥([创建方法](https://github.com/appleboy/ssh-action#setting-up-a-ssh-key)) - `DEPLOY_PATH`: 部署服务器上的项目路径 将以下文件添加至**项目目录**下的 `.github/workflows/` 目录下,在构建成功后触发部署: ```yaml title=.github/workflows/deploy.yml name: Deploy on: workflow_run: workflows: - Docker Hub Release types: - completed jobs: deploy: runs-on: ubuntu-latest if: ${{ github.event.workflow_run.conclusion == 'success' }} steps: - name: Start Deployment uses: bobheadxi/deployments@v1 id: deployment with: step: start token: ${{ secrets.GITHUB_TOKEN }} env: bot - name: Run Remote SSH Command uses: appleboy/ssh-action@master env: DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }} with: host: ${{ secrets.DEPLOY_HOST }} username: ${{ secrets.DEPLOY_USER }} key: ${{ secrets.DEPLOY_KEY }} envs: DEPLOY_PATH script: | cd $DEPLOY_PATH docker compose up -d --pull always - name: update deployment status uses: bobheadxi/deployments@v0.6.2 if: always() with: step: finish token: ${{ secrets.GITHUB_TOKEN }} status: ${{ job.status }} env: ${{ steps.deployment.outputs.env }} deployment_id: ${{ steps.deployment.outputs.deployment_id }} ``` 将上一部分的 `docker-compose.yml` 文件以及 `.env.prod` 配置文件添加至 `DEPLOY_PATH` 目录下,并修改 `docker-compose.yml` 文件中的镜像配置,替换为 Docker Hub 的仓库名称: ```diff - build: . + image: {organization}/{repository}:latest ``` ================================================ FILE: website/versioned_docs/version-2.4.4/best-practice/error-tracking.md ================================================ --- sidebar_position: 2 description: 使用 sentry 进行错误跟踪 --- # 错误跟踪 在应用实际运行过程中,可能会出现各种各样的错误。可能是由于代码逻辑错误,也可能是由于用户输入错误,甚至是由于第三方服务的错误。这些错误都会导致应用的运行出现问题,这时候就需要对错误进行跟踪,以便及时发现问题并进行修复。NoneBot 提供了 `nonebot-plugin-sentry` 插件,支持 [sentry](https://sentry.io/) 平台,可以方便地进行错误跟踪。 ## 安装插件 在使用前请先安装 `nonebot-plugin-sentry` 插件至项目环境中,可参考[获取商店插件](../tutorial/store.mdx#安装插件)来了解并选择安装插件的方式。如: 在**项目目录**下执行以下命令: ```bash nb plugin install nonebot-plugin-sentry ``` ## 使用插件 在安装完成之后,仅需要对插件进行简单的配置即可使用。 ### 获取 sentry DSN 前往 [sentry](https://sentry.io/) 平台,注册并创建一个新的项目,然后在项目设置中找到 `Client Keys (DSN)`,复制其中的 `DSN` 值。 ### 配置插件 :::caution 注意 错误跟踪通常在生产环境中使用,因此开发环境中 `sentry_dsn` 留空即会停用插件。 ::: 在项目 dotenv 配置文件中添加以下配置即可使用: ```dotenv SENTRY_DSN= ``` ## 配置项 配置项具体含义参考 [Sentry Docs](https://docs.sentry.io/platforms/python/configuration/options/)。 - `sentry_dsn: str` - `sentry_debug: bool = False` - `sentry_release: str | None = None` - `sentry_release: str | None = None` - `sentry_environment: str | None = nonebot env` - `sentry_server_name: str | None = None` - `sentry_sample_rate: float = 1.` - `sentry_max_breadcrumbs: int = 100` - `sentry_attach_stacktrace: bool = False` - `sentry_send_default_pii: bool = False` - `sentry_in_app_include: List[str] = Field(default_factory=list)` - `sentry_in_app_exclude: List[str] = Field(default_factory=list)` - `sentry_request_bodies: str = "medium"` - `sentry_with_locals: bool = True` - `sentry_ca_certs: str | None = None` - `sentry_before_send: Callable[[Any, Any], Any | None] | None = None` - `sentry_before_breadcrumb: Callable[[Any, Any], Any | None] | None = None` - `sentry_transport: Any | None = None` - `sentry_http_proxy: str | None = None` - `sentry_https_proxy: str | None = None` - `sentry_shutdown_timeout: int = 2` ================================================ FILE: website/versioned_docs/version-2.4.4/best-practice/htmlkit-render.md ================================================ --- sidebar_position: 8 description: 轻量化 HTML 绘图 --- # 轻量化 HTML 绘图 图片是机器人交互中不可或缺的一部分,对于信息展示的直观性、美观性有很大的作用。 基于 PIL 直接绘制图片具有良好的性能和存储开销,但是难以调试、维护过程式的绘图代码。 使用浏览器渲染类插件可以方便地绘制网页,且能够直接通过 JS 对网页效果进行编程,但是它占用的存储和内存空间相对可观。 NoneBot 提供的 `nonebot-plugin-htmlkit` 提供了另一种基于 HTML 和 CSS 语法的轻量化绘图选择:它基于 `litehtml` 解析库,无须安装额外的依赖即可使用,没有进程间通信带来的额外开销,且在支持 `webp` `avif` 等丰富图片格式的前提下,安装用的 wheel 文件大小仅有约 10 MB。 作为粗略的性能参考,在一台 Ryzen 7 9700X 的 Windows 电脑上,渲染 [PEP 7](https://peps.python.org/pep-0007/) 的 HTML 页面(分辨率为 800x5788,大小约 1.4MB,从本地文件系统读取 CSS)大约需要 100ms,每个渲染任务内存最高占用约为 40MB. ## 安装插件 在使用前请先安装 `nonebot-plugin-htmlkit` 插件至项目环境中,可参考[获取商店插件](../tutorial/store.mdx#安装插件)来了解并选择安装插件的方式。如: 在**项目目录**下执行以下命令: ```bash nb plugin install nonebot-plugin-htmlkit ``` `nonebot-plugin-htmlkit` 插件目前兼容以下系统架构: - Windows x64 - macOS arm64(M-系列芯片) - Linux x64 (非 Alpine 等 musl 系发行版) - Linux arm64 (非 Alpine 等 musl 系发行版) :::caution 访问网络内容 如果需要访问网络资源(如 http(s) 网页内容),NoneBot 需要客户端型驱动器(Forward)。内置的驱动器有 `~httpx` 与 `~aiohttp`。 详见[选择驱动器](../advanced/driver.md)。 ::: ## 使用插件 ### 加载插件 在使用本插件前同样需要使用 `require` 方法进行**加载**并**导入**需要使用的方法,可参考 [跨插件访问](../advanced/requiring.md) 一节进行了解,如: ```python from nonebot import require require("nonebot_plugin_htmlkit") from nonebot_plugin_htmlkit import html_to_pic, md_to_pic, template_to_pic, text_to_pic ``` 插件会自动使用[配置中的参数](#配置-fontconfig)初始化 `fontconfig` 以提供字体查找功能。 ### 渲染 API `nonebot-plugin-htmlkit` 主要提供以下**异步**渲染函数: #### html_to_pic ```python async def html_to_pic( html: str, *, base_url: str = "", dpi: float = 144.0, max_width: float = 800.0, device_height: float = 600.0, default_font_size: float = 12.0, font_name: str = "sans-serif", allow_refit: bool = True, image_format: Literal["png", "jpeg"] = "png", jpeg_quality: int = 100, lang: str = "zh", culture: str = "CN", img_fetch_fn: ImgFetchFn = combined_img_fetcher, css_fetch_fn: CSSFetchFn = combined_css_fetcher, urljoin_fn: Callable[[str, str], str] = urllib3.parse.urljoin, ) -> bytes: ... ``` 最核心的渲染函数。 `base_url` 和 `urljoin_fn` 控制着传入 `image_fetch_fn` 和 `css_fetch_fn` 回调的 url 内容。 `allow_refit` 如果为真,渲染时会自动缩小产出图片的宽度到最适合的宽度,否则必定产出 `max_width` 宽度的图片。 `max_width` 与 `device_height` 会在 `@media` 判断中被使用。 `img_fetch_fn` 预期为一个异步可调用对象(函数),接收图片 url 并返回对应 url 的 jpeg 或 png 二进制数据(`bytes`),可在拒绝加载时返回 `None`. `css_fetch_fn` 预期为一个异步可调用对象(函数),接收目标 CSS url 并返回对应 url 的 CSS 文本(`str`),可在拒绝加载时返回 `None`. 以下为辅助的封装函数,关键字参数若未特殊说明均与 `html_to_pic` 含义相同。 #### text_to_pic ```python async def text_to_pic( text: str, css_path: str = "", *, max_width: int = 500, allow_refit: bool = True, image_format: Literal["png", "jpeg"] = "png", jpeg_quality: int = 100, ) -> bytes: ... ``` 可用于渲染多行文本。 `text` 会被放置于 `
` 中,可据此编写 CSS 来改变文本表现。 #### md_to_pic ```python async def md_to_pic( md: str = "", md_path: str = "", css_path: str = "", *, max_width: int = 500, img_fetch_fn: ImgFetchFn = combined_img_fetcher, allow_refit: bool = True, image_format: Literal["png", "jpeg"] = "png", jpeg_quality: int = 100, ) -> bytes: ... ``` 可用于渲染 Markdown 文本。默认为 GitHub Markdown Light 风格,支持基于 `pygments` 的代码高亮。 `md` 和 `md_path` 二选一,前者设置时应为 Markdown 的文本,后者设置时应为指向 Markdown 文本文件的路径。 #### template_to_pic ```python async def template_to_pic( template_path: str | PathLike[str] | Sequence[str | PathLike[str]], template_name: str, templates: Mapping[Any, Any], filters: None | Mapping[str, Any] = None, *, max_width: int = 500, device_height: int = 600, base_url: str | None = None, img_fetch_fn: ImgFetchFn = combined_img_fetcher, css_fetch_fn: CSSFetchFn = combined_css_fetcher, allow_refit: bool = True, image_format: Literal["png", "jpeg"] = "png", jpeg_quality: int = 100, ) -> bytes: ... ``` 渲染 jinja2 模板。 `template_path` 为 jinja2 环境的路径,`template_name` 是环境中要加载模板的名字,`templates` 为传入模板的参数,`filters` 为过滤器名 -> 自定义过滤器的映射。 ### 控制外部资源获取 通过传入 `img_fetch_fn` 与 `css_fetch_fn`,我们可以在实际访问资源前进行审查,修改资源的来源,或是对 IO 操作进行缓存。 `img_fetch_fn` 预期为一个异步可调用对象(函数),接收图片 url 并返回对应 url 的 jpeg 或 png 二进制数据(`bytes`),可在拒绝加载时返回 `None`. `css_fetch_fn` 预期为一个异步可调用对象(函数),接收目标 CSS url 并返回对应 url 的 CSS 文本(`str`),可在拒绝加载时返回 `None`. 如果你想要禁用外部资源加载/只从文件系统加载/只从网络加载,可以使用 `none_fetcher` `filesystem_***_fetcher` `network_***_fetcher`。 默认的 fetcher 行为(对于 `file://` 从文件系统加载,其余从网络加载)位于 `combined_***_fetcher`,可以通过对其封装实现缓存等操作。 ## 配置项 ### 配置 fontconfig `htmlkit` 使用 `fontconfig` 查找字体,请参阅 [`fontconfig 用户手册`](https://fontconfig.pages.freedesktop.org/fontconfig/fontconfig-user) 了解环境变量的具体含义、如何通过编写配置文件修改字体配置等。 #### fontconfig_file - **类型**: `str | None` - **默认值**: `None` 覆盖默认的配置文件路径。 #### fontconfig_path - **类型**: `str | None` - **默认值**: `None` 覆盖默认的配置目录。 #### fontconfig_sysroot - **类型**: `str | None` - **默认值**: `None` 覆盖默认的 sysroot。 #### fc_debug - **类型**: `str | None` - **默认值**: `None` 设置 Fontconfig 的 debug 级别。 #### fc_dbg_match_filter - **类型**: `str | None` - **默认值**: `None` 当 `FC_DEBUG` 设置为 `MATCH2` 时,过滤 debug 输出。 #### fc_lang - **类型**: `str | None` - **默认值**: `None` 设置默认语言,否则从 `LOCALE` 环境变量获取。 #### fontconfig_use_mmap - **类型**: `str | None` - **默认值**: `None` 是否使用 `mmap(2)` 读取字体缓存。 ================================================ FILE: website/versioned_docs/version-2.4.4/best-practice/multi-adapter.mdx ================================================ --- sidebar_position: 4 description: 插件跨平台支持 --- # 插件跨平台支持 import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; ## 使用 NoneBot 本身 由于不同平台的事件与接口之间,存在着极大的差异性,NoneBot 通过[重载](../appendices/overload.md)的方式,使得插件可以在不同平台上正确响应。但为了减少跨平台的兼容性问题,我们应该尽可能的使用基类方法实现原生跨平台,而不是使用特定平台的方法。当基类方法无法满足需求时,我们可以使用依赖注入的方式,将特定平台的事件或机器人注入到事件处理函数中,实现针对特定平台的处理。 :::tip 提示 如果需要在多平台上**使用**跨平台插件,首先应该根据[注册适配器](../advanced/adapter.md#注册适配器)一节,为机器人注册各平台对应的适配器。 ::: ### 基于基类的跨平台 在[事件通用信息](../advanced/adapter.md#获取事件通用信息)中,我们了解了事件基类能够提供的通用信息。同时,[事件响应器操作](../appendices/session-control.mdx#更多事件响应器操作)也为我们提供了基本的用户交互方式。使用这些方法,可以让我们的插件运行在任何平台上。例如,一个简单的命令处理插件: ```python {5,11} from nonebot import on_command from nonebot.adapters import Event async def is_blacklisted(event: Event) -> bool: return event.get_user_id() not in BLACKLIST weather = on_command("天气", rule=is_blacklisted, priority=10, block=True) @weather.handle() async def handle_function(): await weather.finish("今天的天气是...") ``` 由于此插件仅使用了事件通用信息和事件响应器操作的纯文本交互方式,这些方法不使用特定平台的信息或接口,因此是原生跨平台的,并不需要额外处理。但在一些较为复杂的需求下,例如发送图片消息时,并非所有平台都具有统一的接口,因此基类便无能为力,我们需要引入特定平台的适配器了。 ### 基于重载的跨平台 重载是 NoneBot 跨平台操作的核心,在[事件类型与重载](../appendices/overload.md#重载)一节中,我们初步了解了如何通过类型注解来实现针对不同平台事件的处理方式。在[依赖注入](../advanced/dependency.mdx)一节中,我们又对依赖注入的使用方法进行了详细的介绍。结合这两节内容,我们可以实现更复杂的跨平台操作。 #### 处理近似事件 对于一系列**差异不大**的事件,我们往往具有相同的处理逻辑。这时,我们不希望将相同的逻辑编写两遍,而应该复用代码,以实现在同一个事件处理函数中处理多个近似事件。我们可以使用[事件重载](../advanced/dependency.mdx#event)的特性来实现这一功能。例如: ```python from nonebot import on_command from nonebot.adapters import Message from nonebot.params import CommandArg from nonebot.adapters.onebot.v11 import MessageEvent as OnebotV11MessageEvent from nonebot.adapters.onebot.v12 import MessageEvent as OnebotV12MessageEvent echo = on_command("echo", priority=10, block=True) @echo.handle() async def handle_function(event: OnebotV11MessageEvent | OnebotV12MessageEvent, args: Message = CommandArg()): await echo.finish(args) ``` ```python from typing import Union from nonebot import on_command from nonebot.adapters import Message from nonebot.params import CommandArg from nonebot.adapters.onebot.v11 import MessageEvent as OnebotV11MessageEvent from nonebot.adapters.onebot.v12 import MessageEvent as OnebotV12MessageEvent echo = on_command("echo", priority=10, block=True) @echo.handle() async def handle_function(event: Union[OnebotV11MessageEvent, OnebotV12MessageEvent], args: Message = CommandArg()): await echo.finish(args) ``` #### 在依赖注入中使用重载 NoneBot 依赖注入系统提供了自定义子依赖的方法,子依赖的类型同样会影响到事件处理函数的重载行为。例如: ```python from datetime import datetime from nonebot import on_command from nonebot.adapters.console import MessageEvent echo = on_command("echo", priority=10, block=True) def get_event_time(event: MessageEvent): return event.time # 处理控制台消息事件 @echo.handle() async def handle_function(time: datetime = Depends(get_event_time)): await echo.finish(time.strftime("%Y-%m-%d %H:%M:%S")) ``` 示例中 ,我们为 `handle_function` 事件处理函数注入了自定义的 `get_event_time` 子依赖,而此子依赖注入参数为 Console 适配器的 `MessageEvent`。因此 `handle_function` 仅会响应 Console 适配器的 `MessageEvent` ,而不能响应其他事件。 #### 处理多平台事件 不同平台的事件之间,往往存在着极大的差异性。为了满足我们插件的跨平台运行,通常我们需要抽离业务逻辑,以保证代码的复用性。一个合理的做法是,在事件响应器的处理流程中,首先先针对不同平台的事件分别进行处理,提取出核心业务逻辑所需要的信息;然后再将这些信息传递给业务逻辑处理函数;最后将业务逻辑的输出以各平台合适的方式返回给用户。也就是说,与平台绑定的处理部分应该与平台无关部分尽量分离。例如: ```python import inspect from nonebot import on_command from nonebot.typing import T_State from nonebot.matcher import Matcher from nonebot.adapters import Message from nonebot.params import CommandArg, ArgPlainText from nonebot.adapters.console import Bot as ConsoleBot from nonebot.adapters.onebot.v11 import Bot as OnebotBot from nonebot.adapters.console import MessageSegment as ConsoleMessageSegment weather = on_command("天气", priority=10, block=True) @weather.handle() async def handle_function(matcher: Matcher, args: Message = CommandArg()): if args.extract_plain_text(): matcher.set_arg("location", args) async def get_weather(state: T_State, location: str = ArgPlainText()): if location not in ["北京", "上海", "广州", "深圳"]: await weather.reject(f"你想查询的城市 {location} 暂不支持,请重新输入!") state["weather"] = "⛅ 多云 20℃~24℃" # 处理控制台询问 @weather.got( "location", prompt=ConsoleMessageSegment.emoji("question") + "请输入地名", parameterless=[Depends(get_weather)], ) async def handle_console(bot: ConsoleBot): pass # 处理 OneBot 询问 @weather.got( "location", prompt="请输入地名", parameterless=[Depends(get_weather)], ) async def handle_onebot(bot: OnebotBot): pass # 通过依赖注入或事件处理函数来进行业务逻辑处理 # 处理控制台回复 @weather.handle() async def handle_console_reply(bot: ConsoleBot, state: T_State, location: str = ArgPlainText()): await weather.send( ConsoleMessageSegment.markdown( inspect.cleandoc( f""" # {location} - 今天 {state['weather']} """ ) ) ) # 处理 OneBot 回复 @weather.handle() async def handle_onebot_reply(bot: OnebotBot, state: T_State, location: str = ArgPlainText()): await weather.send(f"今天{location}的天气是{state['weather']}") ``` ## 使用插件 得益于众多开发者为 NoneBot 社区做出的贡献,我们可以通过一系列插件来完成跨平台插件的开发。 这些插件可以分为三类: ### 事件处理 - [all4one](https://github.com/nonepkg/nonebot-plugin-all4one): 将不同平台的事件转为符合 OneBot V12 协议的插件 - 支持的适配器: OneBot V11/V12, Discord, QQ, Telegram ### 消息处理 - [alconna](https://github.com/nonebot/plugin-alconna): 对几乎所有适配器中消息的收发、撤回、编辑、表态的统一插件 - 支持的适配器: OneBot V11/V12, Telegram, Feishu, Github, QQ, Ding, Console, Kaiheila, Mirai, NtChat, Minecraft, Discord, Satori, Red, Dodo, Kritor, Tailchat, Mail, WXMP, Heybox, Gewechat - [send-anything-anywhere](https://github.com/felinae98/nonebot-plugin-send-anything-anywhere): 帮助处理不同适配器消息的适配和发送的插件 - 支持的适配器: OneBot V11/V12, Kaiheila, Telegram, Feishu, Red, DoDo, Satori, QQ, Discord ### 会话信息提取 - [uninfo](https://github.com/RF-Tar-Railt/nonebot-plugin-uninfo): 多平台的会话信息(用户、群组、频道)获取插件 - 支持的适配器: OneBot V11/V12, Telegram, Feishu, QQ, Console, Kaiheila, Mirai, Minecraft, Discord, Satori, Dodo, Kritor, Mail, WXMP, Gewechat - [session](https://github.com/noneplugin/nonebot-plugin-session): 会话信息提取与会话 id 定义插件 - 支持的适配器: OneBot V11/V12, Console, Kaiheila, Telegram, Feishu, Red, DoDo, Satori, QQ, Discord - [userinfo](https://github.com/noneplugin/nonebot-plugin-userinfo: 用户信息获取插件 - 支持的适配器: OneBot V11/V12, Console, Kaiheila, Telegram, Feishu, Red, DoDo, Satori, QQ, Discord ================================================ FILE: website/versioned_docs/version-2.4.4/best-practice/scheduler.md ================================================ --- sidebar_position: 0 description: 定时执行任务 --- # 定时任务 [APScheduler](https://apscheduler.readthedocs.io/en/3.x/) (Advanced Python Scheduler) 是一个 Python 第三方库,其强大的定时任务功能被广泛应用于各个场景。在 NoneBot 中,定时任务作为一个额外功能,依赖于基于 APScheduler 开发的 [`nonebot-plugin-apscheduler`](https://github.com/nonebot/plugin-apscheduler) 插件进行支持。 ## 安装插件 在使用前请先安装 `nonebot-plugin-apscheduler` 插件至项目环境中,可参考[获取商店插件](../tutorial/store.mdx#安装插件)来了解并选择安装插件的方式。如: 在**项目目录**下执行以下命令: ```bash nb plugin install nonebot-plugin-apscheduler ``` ## 使用插件 `nonebot-plugin-apscheduler` 本质上是对 [APScheduler](https://apscheduler.readthedocs.io/en/3.x/) 进行了封装以适用于 NoneBot 开发,因此其使用方式与 APScheduler 本身并无显著区别。在此我们会简要介绍其调用方法,更多的使用方面的功能请参考[APScheduler 官方文档](https://apscheduler.readthedocs.io/en/3.x/userguide.html)。 ### 导入调度器 由于 `nonebot_plugin_apscheduler` 作为插件,因此需要在使用前对其进行**加载**并**导入**其中的 `scheduler` 调度器来创建定时任务。使用 `require` 方法可轻松完成这一过程,可参考 [跨插件访问](../advanced/requiring.md) 一节进行了解。 ```python from nonebot import require require("nonebot_plugin_apscheduler") from nonebot_plugin_apscheduler import scheduler ``` ### 添加定时任务 在 [APScheduler 官方文档](https://apscheduler.readthedocs.io/en/3.x/userguide.html#adding-jobs) 中提供了以下两种直接添加任务的方式: ```python from nonebot import require require("nonebot_plugin_apscheduler") from nonebot_plugin_apscheduler import scheduler # 基于装饰器的方式 @scheduler.scheduled_job("cron", hour="*/2", id="job_0", args=[1], kwargs={arg2: 2}) async def run_every_2_hour(arg1: int, arg2: int): pass # 基于 add_job 方法的方式 def run_every_day(arg1: int, arg2: int): pass scheduler.add_job( run_every_day, "interval", days=1, id="job_1", args=[1], kwargs={arg2: 2} ) ``` :::caution 注意 由于 APScheduler 的定时任务并不是**由事件响应器所触发的事件**,因此其任务函数无法同[事件处理函数](../tutorial/handler.mdx#事件处理函数)一样通过[依赖注入](../tutorial/event-data.mdx#认识依赖注入)获取上下文信息,也无法通过事件响应器对象的方法进行任何操作,因此我们需要使用[调用平台 API](../appendices/api-calling.mdx#调用平台-api)的方式来获取信息或收发消息。 相对于事件处理依赖而言,编写定时任务更像是编写普通的函数,需要我们自行获取信息以及发送信息,请**不要**将事件处理依赖的特殊语法用于定时任务! ::: 关于 APScheduler 的更多使用方法,可以参考 [APScheduler 官方文档](https://apscheduler.readthedocs.io/en/3.x/index.html) 进行了解。 ### 配置项 #### apscheduler_autostart - **类型**: `bool` - **默认值**: `True` 是否自动启动 `scheduler` ,若不启动需要自行调用 `scheduler.start()`。 #### apscheduler_log_level - **类型**: `int` - **默认值**: `30` apscheduler 输出的日志等级 - `WARNING` = `30` (默认) - `INFO` = `20` - `DEBUG` = `10` (只有在开启 nonebot 的 debug 模式才会显示 debug 日志) #### apscheduler_config - **类型**: `dict` - **默认值**: `{ "apscheduler.timezone": "Asia/Shanghai" }` `apscheduler` 的相关配置。参考[配置调度器](https://apscheduler.readthedocs.io/en/latest/userguide.html#scheduler-config), [配置参数](https://apscheduler.readthedocs.io/en/latest/modules/schedulers/base.html#apscheduler.schedulers.base.BaseScheduler) 配置需要包含 `apscheduler.` 作为前缀,例如 `apscheduler.timezone`。 ================================================ FILE: website/versioned_docs/version-2.4.4/best-practice/testing/README.mdx ================================================ --- sidebar_position: 1 description: 使用 NoneBug 进行单元测试 slug: /best-practice/testing/ --- # 配置与测试事件响应器 import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; > 在计算机编程中,单元测试(Unit Testing)又称为模块测试,是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。 为了保证代码的正确运行,我们不仅需要对错误进行跟踪,还需要对代码进行正确性检验,也就是测试。NoneBot 提供了一个测试工具——NoneBug,它是一个 [pytest](https://docs.pytest.org/en/stable/) 插件,可以帮助我们便捷地进行单元测试。 :::tip 提示 建议在阅读本文档前先阅读 [pytest 官方文档](https://docs.pytest.org/en/stable/)来了解 pytest 的相关术语和基本用法。 ::: ## 安装 NoneBug 在**项目目录**下激活虚拟环境后运行以下命令安装 NoneBug: ```bash poetry add nonebug -G test ``` ```bash pdm add nonebug -dG test ``` ```bash pip install nonebug ``` 要运行 NoneBug 测试,还需要额外安装 pytest 异步插件 `pytest-asyncio` 或 `anyio` 以支持异步测试。文档中,我们以 `pytest-asyncio` 为例: ```bash poetry add pytest-asyncio -G test ``` ```bash pdm add pytest-asyncio -dG test ``` ```bash pip install pytest-asyncio ``` ## 配置测试 在开始测试之前,我们需要对测试进行一些配置,以正确启动我们的机器人。 首先我们需要配置 pytest-asyncio,在 `pyproject.toml` 的 pytest 配置部分添加: ```toml [tool.pytest.ini_options] asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "session" ``` 然后,我们在 `tests` 目录下新建 `conftest.py` 文件,添加以下内容: ```python title=tests/conftest.py import pytest import nonebot from pytest_asyncio import is_async_test # 导入适配器 from nonebot.adapters.console import Adapter as ConsoleAdapter def pytest_collection_modifyitems(items: list[pytest.Item]): pytest_asyncio_tests = (item for item in items if is_async_test(item)) session_scope_marker = pytest.mark.asyncio(loop_scope="session") for async_test in pytest_asyncio_tests: async_test.add_marker(session_scope_marker, append=False) @pytest.fixture(scope="session", autouse=True) async def after_nonebot_init(after_nonebot_init: None): # 加载适配器 driver = nonebot.get_driver() driver.register_adapter(ConsoleAdapter) # 加载插件 nonebot.load_from_toml("pyproject.toml") ``` 这样,我们就可以在测试中使用机器人的插件了。通常,我们不需要自行初始化 NoneBot,NoneBug 已经为我们运行了 `nonebot.init()`。如果需要自定义 NoneBot 初始化的参数,我们可以在 `conftest.py` 中添加 `pytest_configure` 钩子函数。例如,我们可以修改 NoneBot 配置环境为 `test` 并从环境变量中输入配置: ```python {4,6,8-10} title=tests/conftest.py import os import pytest from nonebug import NONEBOT_INIT_KWARGS os.environ["ENVIRONMENT"] = "test" def pytest_configure(config: pytest.Config): config.stash[NONEBOT_INIT_KWARGS] = {"secret": os.getenv("INPUT_SECRET")} ``` NoneBug 默认也会为我们管理 lifespan 的 startup 与 shutdown。如果不希望 NoneBug 管理 lifespan,你可以在 `pytest_configure` 里添加以下配置: ```python import pytest from nonebug import NONEBOT_START_LIFESPAN def pytest_configure(config: pytest.Config): config.stash[NONEBOT_START_LIFESPAN] = False ``` ## 编写插件测试 在配置完成插件加载后,我们就可以在测试中使用插件了。NoneBug 通过 pytest fixture `app` 提供各种测试方法,我们可以在测试中使用它来测试插件。现在,我们创建一个测试脚本来测试[深入指南](../../appendices/session-control.mdx)中编写的天气插件。首先,我们先要导入我们需要的模块:
插件示例 ```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}的天气是...") ```
```python {4,5,9,11-16} title=tests/test_weather.py from datetime import datetime import pytest from nonebug import App from nonebot.adapters.console import User, Message, MessageEvent @pytest.mark.asyncio async def test_weather(app: App): from awesome_bot.plugins.weather import weather event = MessageEvent( time=datetime.now(), self_id="test", message=Message("/天气 北京"), user=User(id="user"), ) ``` 在上面的代码中,我们引入了 NoneBug 的测试 `App` 对象,以及必要的适配器消息与事件定义等。在测试函数 `test_weather` 中,我们导入了要进行测试的事件响应器 `weather`。请注意,由于需要等待 NoneBot 初始化并加载插件完毕,插件内容必须在**测试函数内部**进行导入。然后,我们创建了一个 `MessageEvent` 事件对象,它模拟了一个用户发送了 `/天气 北京` 的消息。接下来,我们使用 `app.test_matcher` 方法来测试 `weather` 事件响应器: ```python {11-15} title=tests/test_weather.py @pytest.mark.asyncio async def test_weather(app: App): from awesome_bot.plugins.weather import weather event = MessageEvent( time=datetime.now(), self_id="test", message=Message("/天气 北京"), user=User(id="user"), ) async with app.test_matcher(weather) as ctx: bot = ctx.create_bot() ctx.receive_event(bot, event) ctx.should_call_send(event, "今天北京的天气是...", result=None) ctx.should_finished(weather) ``` 这里我们使用 `async with` 语句并通过参数指定要测试的事件响应器 `weather` 来进入测试上下文。在测试上下文中,我们可以使用 `ctx.create_bot` 方法创建一个虚拟的机器人实例,并使用 `ctx.receive_event` 方法来模拟机器人接收到消息事件。然后,我们就可以定义预期行为来测试机器人是否正确运行。在上面的代码中,我们使用 `ctx.should_call_send` 方法来断言机器人应该发送 `今天北京的天气是...` 这条消息,并且将发送函数的调用结果作为第三个参数返回给事件处理函数。如果断言失败,测试将会不通过。我们也可以使用 `ctx.should_finished` 方法来断言机器人应该结束会话。 为了测试更复杂的情况,我们可以为添加更多的测试用例。例如,我们可以测试用户输入了一个不支持的地名时机器人的反应: ```python {17-21,23-26} title=tests/test_weather.py def make_event(message: str = "") -> MessageEvent: return MessageEvent( time=datetime.now(), self_id="test", message=Message(message), user=User(id="user"), ) @pytest.mark.asyncio async def test_weather(app: App): from awesome_bot.plugins.weather import weather async with app.test_matcher(weather) as ctx: ... # 省略前面的测试用例 async with app.test_matcher(weather) as ctx: bot = ctx.create_bot() event = make_event("/天气 南京") ctx.receive_event(bot, event) ctx.should_call_send(event, "你想查询的城市 南京 暂不支持,请重新输入!", result=None) ctx.should_rejected(weather) event = make_event("北京") ctx.receive_event(bot, event) ctx.should_call_send(event, "今天北京的天气是...", result=None) ctx.should_finished(weather) ``` 在上面的代码中,我们使用 `ctx.should_rejected` 来断言机器人应该请求用户重新输入。然后,我们再次使用 `ctx.receive_event` 方法来模拟用户回复了 `北京`,并使用 `ctx.should_finished` 来断言机器人应该结束会话。 更多的 NoneBug 用法将在后续章节中介绍。 ================================================ FILE: website/versioned_docs/version-2.4.4/best-practice/testing/_category_.json ================================================ { "label": "单元测试", "position": 5 } ================================================ FILE: website/versioned_docs/version-2.4.4/best-practice/testing/behavior.mdx ================================================ --- sidebar_position: 2 description: 测试事件响应、平台接口调用和会话控制 --- # 测试事件响应与会话操作 import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; 在 NoneBot 接收到事件时,事件响应器根据优先级依次通过权限、响应规则来判断当前事件是否应该触发。事件响应流程中,机器人可能会通过 `send` 发送消息或者调用平台接口来执行预期的操作。因此,我们需要对这两种操作进行单元测试。 在上一节中,我们对单个事件响应器进行了简单测试。但是在实际场景中,机器人可能定义了多个事件响应器,由于优先级和响应规则的存在,预期的事件响应器可能并不会被触发。NoneBug 支持同时测试多个事件响应器,以此来测试机器人的整体行为。 ## 测试事件响应 NoneBug 提供了六种定义 `Rule` 和 `Permission` 预期行为的方法: - `should_pass_rule` - `should_not_pass_rule` - `should_ignore_rule` - `should_pass_permission` - `should_not_pass_permission` - `should_ignore_permission` :::tip 提示 事件响应器类型的检查属于 `Permission` 的一部分,因此可以通过 `should_pass_permission` 和 `should_not_pass_permission` 方法来断言事件响应器类型的检查。 ::: 下面我们根据插件示例来测试事件响应行为,我们首先定义两个事件响应器作为测试的对象: ```python title=example.py from nonebot import on_command def never_pass(): return False foo = on_command("foo") bar = on_command("bar", permission=never_pass) ``` 在这两个事件响应器中,`foo` 当收到 `/foo` 消息时会执行,而 `bar` 则不会执行。我们使用 NoneBug 来测试它们: ```python {21,22,28,29} title=tests/test_example.py from datetime import datetime import pytest from nonebug import App from nonebot.adapters.console import User, Message, MessageEvent def make_event(message: str = "") -> MessageEvent: return MessageEvent( time=datetime.now(), self_id="test", message=Message(message), user=User(id="user"), ) @pytest.mark.asyncio async def test_example(app: App): from awesome_bot.plugins.example import foo, bar async with app.test_matcher(foo) as ctx: bot = ctx.create_bot() event = make_event("/foo") ctx.receive_event(bot, event) ctx.should_pass_rule() ctx.should_pass_permission() async with app.test_matcher(bar) as ctx: bot = ctx.create_bot() event = make_event("/foo") ctx.receive_event(bot, event) ctx.should_not_pass_rule() ctx.should_not_pass_permission() ``` 在上面的代码中,我们分别对 `foo` 和 `bar` 事件响应器进行响应测试。我们使用 `ctx.should_pass_rule` 和 `ctx.should_pass_permission` 断言 `foo` 事件响应器应该被触发,使用 `ctx.should_not_pass_rule` 和 `ctx.should_not_pass_permission` 断言 `bar` 事件响应器应该被忽略。 ```python title=tests/test_example.py from datetime import datetime import pytest from nonebug import App from nonebot.adapters.console import User, Message, MessageEvent def make_event(message: str = "") -> MessageEvent: return MessageEvent( time=datetime.now(), self_id="test", message=Message(message), user=User(id="user"), ) @pytest.mark.asyncio async def test_example(app: App): from awesome_bot.plugins.example import foo, bar async with app.test_matcher() as ctx: bot = ctx.create_bot() event = make_event("/foo") ctx.receive_event(bot, event) ctx.should_pass_rule(foo) ctx.should_pass_permission(foo) ctx.should_not_pass_rule(bar) ctx.should_not_pass_permission(bar) ``` 在上面的代码中,我们对 `foo` 和 `bar` 事件响应器一起进行响应测试。我们使用 `ctx.should_pass_rule` 和 `ctx.should_pass_permission` 断言 `foo` 事件响应器应该被触发,使用 `ctx.should_not_pass_rule` 和 `ctx.should_not_pass_permission` 断言 `bar` 事件响应器应该被忽略。通过参数,我们可以指定断言的事件响应器。 当然,如果需要忽略某个事件响应器的响应规则和权限检查,强行进入响应流程,我们可以使用 `should_ignore_rule` 和 `should_ignore_permission` 方法: ```python {21,22} title=tests/test_example.py from datetime import datetime import pytest from nonebug import App from nonebot.adapters.console import User, Message, MessageEvent def make_event(message: str = "") -> MessageEvent: return MessageEvent( time=datetime.now(), self_id="test", message=Message(message), user=User(id="user"), ) @pytest.mark.asyncio async def test_example(app: App): from awesome_bot.plugins.example import foo, bar async with app.test_matcher(bar) as ctx: bot = ctx.create_bot() event = make_event("/foo") ctx.receive_event(bot, event) ctx.should_ignore_rule(bar) ctx.should_ignore_permission(bar) ``` 在忽略了响应规则和权限检查之后,就会进入 `bar` 事件响应器的响应流程。 ## 测试平台接口使用 上一节的示例插件测试中,我们已经尝试了测试插件对事件的消息回复。通常情况下,事件处理流程中对平台接口的使用会通过事件响应器操作或者调用平台 API 两种途径进行。针对这两种途径,NoneBug 分别提供了 `ctx.should_call_send` 和 `ctx.should_call_api` 方法来测试平台接口的使用情况。 1. `should_call_send` 定义事件响应器预期发送的消息,即通过[事件响应器操作 send](../../appendices/session-control.mdx#send)进行的操作。`should_call_send` 有四个参数: - `event`:回复的目标事件。 - `message`:预期的消息对象,可以是 `str`、`Message` 或 `MessageSegment`。 - `result`:send 的返回值,将会返回给插件。 - `bot`(可选):发送消息的 bot 对象。 - `**kwargs`:send 方法的额外参数。 2. `should_call_api` 定义事件响应器预期调用的平台 API 接口,即通过[调用平台 API](../../appendices/api-calling.mdx#调用平台-api)进行的操作。`should_call_api` 有四个参数: - `api`:API 名称。 - `data`:预期的请求数据。 - `result`:call_api 的返回值,将会返回给插件。 - `adapter`(可选):调用 API 的平台适配器对象。 - `**kwargs`:call_api 方法的额外参数。 下面是一个使用 `should_call_send` 和 `should_call_api` 方法的示例: 我们先定义一个测试插件,在响应流程中向用户发送一条消息并调用 `Console` 适配器的 `bell` API。 ```python {8,9} title=example.py from nonebot import on_command from nonebot.adapters.console import Bot foo = on_command("foo") @foo.handle() async def _(bot: Bot): await foo.send("message") await bot.bell() ``` 然后我们对该插件进行测试: ```python title=tests/test_example.py from datetime import datetime import pytest import nonebot from nonebug import App from nonebot.adapters.console import Bot, User, Adapter, Message, MessageEvent def make_event(message: str = "") -> MessageEvent: return MessageEvent( time=datetime.now(), self_id="test", message=Message(message), user=User(id="user"), ) @pytest.mark.asyncio async def test_example(app: App): from awesome_bot.plugins.example import foo async with app.test_matcher(foo) as ctx: # highlight-start adapter = nonebot.get_adapter(Adapter) bot = ctx.create_bot(base=Bot, adapter=adapter) # highlight-end event = make_event("/foo") ctx.receive_event(bot, event) # highlight-start ctx.should_call_send(event, "message", result=None, bot=bot) ctx.should_call_api("bell", {}, result=None, adapter=adapter) # highlight-end ``` 请注意,对于在依赖注入中使用了非基类对象的情况,我们需要在 `create_bot` 方法中指定 `base` 和 `adapter` 参数,确保不会因为重载功能而出现非预期情况。 ## 测试会话控制 在[会话控制](../../appendices/session-control.mdx)一节中,我们介绍了如何使用事件响应器操作来实现对用户的交互式会话。在上一节的示例插件测试中,我们其实已经使用了 `ctx.should_finished` 来断言会话结束。NoneBug 针对各种流程控制操作分别提供了相应的方法来定义预期的会话处理行为。它们分别是: - `should_finished`:断言会话结束,对应 `matcher.finish` 操作。 - `should_rejected`:断言会话等待用户输入并重新执行当前事件处理函数,对应 `matcher.reject` 系列操作。 - `should_paused`: 断言会话等待用户输入并执行下一个事件处理函数,对应 `matcher.pause` 操作。 我们仅需在测试用例中的正确位置调用这些方法,就可以断言会话的预期行为。例如: ```python title=example.py from nonebot import on_command from nonebot.typing import T_State foo = on_command("foo") @foo.got("key", prompt="请输入密码") async def _(state: T_State, key: str = ArgPlainText()): if key != "some password": try_count = state.get("try_count", 1) if try_count >= 3: await foo.finish("密码错误次数过多") else: state["try_count"] = try_count + 1 await foo.reject("密码错误,请重新输入") await foo.finish("密码正确") ``` ```python title=tests/test_example.py from datetime import datetime import pytest from nonebug import App from nonebot.adapters.console import User, Message, MessageEvent def make_event(message: str = "") -> MessageEvent: return MessageEvent( time=datetime.now(), self_id="test", message=Message(message), user=User(id="user"), ) @pytest.mark.asyncio async def test_example(app: App): from awesome_bot.plugins.example import foo async with app.test_matcher(foo) as ctx: bot = ctx.create_bot() event = make_event("/foo") ctx.receive_event(bot, event) ctx.should_call_send(event, "请输入密码", result=None) ctx.should_rejected(foo) event = make_event("wrong password") ctx.receive_event(bot, event) ctx.should_call_send(event, "密码错误,请重新输入", result=None) ctx.should_rejected(foo) event = make_event("wrong password") ctx.receive_event(bot, event) ctx.should_call_send(event, "密码错误,请重新输入", result=None) ctx.should_rejected(foo) event = make_event("wrong password") ctx.receive_event(bot, event) ctx.should_call_send(event, "密码错误次数过多", result=None) ctx.should_finished(foo) ``` ================================================ FILE: website/versioned_docs/version-2.4.4/best-practice/testing/mock-network.md ================================================ --- sidebar_position: 3 description: 模拟网络通信以进行测试 --- # 模拟网络通信 NoneBot 驱动器提供了多种方法来帮助适配器进行网络通信,主要包括客户端和服务端两种类型。模拟网络通信可以帮助我们更加接近实际机器人应用场景,进行更加真实的集成测试。同时,通过这种途径,我们还可以完成对适配器的测试。 NoneBot 中的网络通信主要包括以下几种: - HTTP 服务端(WebHook) - WebSocket 服务端 - HTTP 客户端 - WebSocket 客户端 下面我们将分别介绍如何使用 NoneBug 来模拟这几种通信方式。 ## 测试 HTTP 服务端 当 NoneBot 作为 ASGI 服务端应用时,我们可以定义一系列的路由来处理 HTTP 请求,适配器同样也可以通过定义路由来响应机器人相关的网络通信。下面假设我们使用了一个适配器 `fake` ,它定义了一个路由 `/fake/http` ,用于接收平台 WebHook 并处理。实际应用测试时,应将该路由地址替换为**真实适配器注册的路由地址**。 我们首先需要获取测试用模拟客户端: ```python {5,6} title=tests/test_http_server.py from nonebug import App @pytest.mark.asyncio async def test_http_server(app: App): async with app.test_server() as ctx: client = ctx.get_client() ``` 默认情况下,`app.test_server()` 会通过 `nonebot.get_asgi` 获取测试对象,我们也可以通过参数指定 ASGI 应用: ```python async with app.test_server(asgi=asgi_app) as ctx: ... ``` 获取到模拟客户端后,即可像 `requests`、`httpx` 等库类似的方法进行使用: ```python {3,11-14,16} title=tests/test_http_server.py import nonebot from nonebug import App from nonebot.adapters.fake import Adapter @pytest.mark.asyncio async def test_http_server(app: App): adapter = nonebot.get_adapter(Adapter) async with app.test_server() as ctx: client = ctx.get_client() response = await client.post("/fake/http", json={"bot_id": "fake"}) assert response.status_code == 200 assert response.json() == {"status": "success"} assert "fake" in nonebot.get_bots() adapter.bot_disconnect(nonebot.get_bot("fake")) ``` 在上面的测试中,我们向 `/fake/http` 发送了一个模拟 POST 请求,适配器将会对该请求进行处理,我们可以通过检查请求返回是否正确、Bot 对象是否创建等途径来验证机器人是否正确运行。在完成测试后,我们通常需要对 Bot 对象进行清理,以避免对其他测试产生影响。 ## 测试 WebSocket 服务端 当 NoneBot 作为 ASGI 服务端应用时,我们还可以定义一系列的路由来处理 WebSocket 通信。下面假设我们使用了一个适配器 `fake` ,它定义了一个路由 `/fake/ws` ,用于处理平台 WebSocket 连接信息。实际应用测试时,应将该路由地址替换为**真实适配器注册的路由地址**。 我们同样需要通过 `app.test_server()` 获取测试用模拟客户端,这里就不再赘述。在获取到模拟客户端后,我们可以通过 `client.websocket_connect` 方法来模拟 WebSocket 连接: ```python {3,11-15} title=tests/test_ws_server.py import nonebot from nonebug import App from nonebot.adapters.fake import Adapter @pytest.mark.asyncio async def test_ws_server(app: App): adapter = nonebot.get_adapter(Adapter) async with app.test_server() as ctx: client = ctx.get_client() async with client.websocket_connect("/fake/ws") as ws: await ws.send_json({"bot_id": "fake"}) response = await ws.receive_json() assert response == {"status": "success"} assert "fake" in nonebot.get_bots() ``` 在上面的测试中,我们向 `/fake/ws` 进行了 WebSocket 模拟通信,通过发送消息与机器人进行交互,然后检查机器人发送的信息是否正确。 ## 测试 HTTP 客户端 ~~暂不支持~~ ## 测试 WebSocket 客户端 ~~暂不支持~~ ================================================ FILE: website/versioned_docs/version-2.4.4/community/contact.md ================================================ --- sidebar-position: 0 description: 遇到问题如何获取帮助 --- # 参与讨论 如果在安装或者开发 NoneBot 过程中遇到了任何问题,或者有新奇的点子,欢迎参与我们的社区讨论: 1. 点击下方链接前往 GitHub,前往 Issues 页面,在 `New Issue` Template 中选择 `Question` NoneBot:[![NoneBot project link](https://img.shields.io/github/stars/nonebot/nonebot2?style=social)](https://github.com/nonebot/nonebot2) 2. 通过 QQ 群(点击下方链接直达) [![QQ Chat Group](https://img.shields.io/badge/QQ%E7%BE%A4-768887710-orange?style=social)](https://jq.qq.com/?_wv=1027&k=5OFifDh) 3. 通过 QQ 频道 [![QQ Channel](https://img.shields.io/badge/QQ%E9%A2%91%E9%81%93-NoneBot-orange?style=social)](https://qun.qq.com/qqweb/qunpro/share?_wv=3&_wwv=128&appChannel=share&inviteCode=7b4a3&appChannel=share&businessType=9&from=246610&biz=ka) 4. 通过 Discord 服务器(点击下方链接直达) [![Discord Server](https://discordapp.com/api/guilds/847819937858584596/widget.png?style=shield)](https://discord.gg/VKtE6Gdc4h) ================================================ FILE: website/versioned_docs/version-2.4.4/community/contributing.md ================================================ --- sidebar-position: 1 description: 如何为 NoneBot 贡献代码 --- # 贡献指南 ## Code of Conduct 请参阅 [Code of Conduct](https://github.com/nonebot/nonebot2/blob/master/CODE_OF_CONDUCT.md)。 ## 参与开发 请参阅 [Contributing](https://github.com/nonebot/nonebot2/blob/master/CONTRIBUTING.md)。 ## 鸣谢 感谢以下开发者对 NoneBot2 作出的贡献: ================================================ FILE: website/versioned_docs/version-2.4.4/developer/adapter-writing.md ================================================ --- sidebar_position: 1 description: 编写适配器对接新的平台 --- # 编写适配器 在编写适配器之前,我们需要先了解[适配器的功能与组成](../advanced/adapter#适配器功能与组成),适配器通常由 `Adapter`、`Bot`、`Event` 和 `Message` 四个部分组成,在编写适配器时,我们需要继承 NoneBot 中的基类,并根据实际平台来编写每个部分功能。 ## 组织结构 NoneBot 适配器项目通常以 `nonebot-adapter-{adapter-name}` 作为项目名,并以**命名空间包**的形式编写,即在 `nonebot/adapters/{adapter-name}` 目录中编写实际代码,例如: ```tree 📦 nonebot-adapter-{adapter-name} ├── 📂 nonebot │ ├── 📂 adapters │ │ ├── 📂 {adapter-name} │ │ │ ├── 📜 __init__.py │ │ │ ├── 📜 adapter.py │ │ │ ├── 📜 bot.py │ │ │ ├── 📜 config.py │ │ │ ├── 📜 event.py │ │ │ └── 📜 message.py ├── 📜 pyproject.toml └── 📜 README.md ``` :::tip 提示 上述的项目结构仅作推荐,不做强制要求,保证实际可用性即可。 ::: ### 使用 NB-CLI 创建项目 我们可以使用脚手架快速创建项目: ```shell nb adapter create ``` 按照指引,输入适配器名称以及存储位置,即可创建一个带有基本结构的适配器项目。 ## 组成部分 :::tip 提示 本章节的代码中提到的 `Adapter`、`Bot`、`Event` 和 `Message` 等,均为下文中适配器所编写的类,而非 NoneBot 中的基类。 ::: ### Log 适配器在处理时通常需要打印日志,但直接使用 NoneBot 的默认 `logger` 不方便区分适配器输出和其它日志。因此我们可以使用 NoneBot 提供的 `logger_wrapper` 方法,自定义一个 `log` 函数用于快捷打印适配器日志: ```python {3} title=log.py from nonebot.utils import logger_wrapper log = logger_wrapper("your_adapter_name") ``` 这个 `log` 函数会在默认 `logger` 中添加适配器名称前缀,它接收三个参数:日志等级、日志内容以及可选的异常,具体用法如下: ```python from .log import log log("DEBUG", "A DEBUG log.") log("INFO", "A INFO log.") try: ... except Exception as e: log("ERROR", "something error.", e) ``` ### Config 通常适配器需要一些配置项,例如平台连接密钥等。适配器的配置方法与[插件配置](../appendices/config#%E6%8F%92%E4%BB%B6%E9%85%8D%E7%BD%AE)类似,例如: ```python title=config.py from pydantic import BaseModel class Config(BaseModel): xxx_id: str xxx_token: str ``` 配置项的读取将在下方 [Adapter](#adapter) 中介绍。 ### Adapter Adapter 负责转换事件、调用接口,以及正确创建 Bot 对象并注册到 NoneBot 中。在编写平台相关内容之前,我们需要继承基类,并实现适配器的基本信息: ```python {9,11,14,18} title=adapter.py from typing import Any from typing_extensions import override from nonebot.drivers import Driver from nonebot import get_plugin_config from nonebot.adapters import Adapter as BaseAdapter from .config import Config class Adapter(BaseAdapter): @override def __init__(self, driver: Driver, **kwargs: Any): super().__init__(driver, **kwargs) # 读取适配器所需的配置项 self.adapter_config: Config = get_plugin_config(Config) @classmethod @override def get_name(cls) -> str: """适配器名称""" return "your_adapter_name" ``` #### 与平台交互 NoneBot 提供了多种 [Driver](../advanced/driver) 来帮助适配器进行网络通信,主要分为客户端和服务端两种类型。我们需要**根据平台文档和特性**选择合适的通信方式,并编写相关方法用于初始化适配器,与平台建立连接和进行交互: ##### 客户端通信方式 ```python {12,23,24} title=adapter.py import asyncio from typing_extensions import override from nonebot import get_plugin_config from nonebot.exception import WebSocketClosed from nonebot.drivers import Request, WebSocketClientMixin class Adapter(BaseAdapter): @override def __init__(self, driver: Driver, **kwargs: Any): super().__init__(driver, **kwargs) self.adapter_config: Config = get_plugin_config(Config) self.task: Optional[asyncio.Task] = None # 存储 ws 任务 self.setup() def setup(self) -> None: if not isinstance(self.driver, WebSocketClientMixin): # 判断用户配置的Driver类型是否符合适配器要求,不符合时应抛出异常 raise RuntimeError( f"Current driver {self.config.driver} doesn't support websocket client connections!" f"{self.get_name()} Adapter need a WebSocket Client Driver to work." ) # 在 NoneBot 启动和关闭时进行相关操作 self.driver.on_startup(self.startup) self.driver.on_shutdown(self.shutdown) async def startup(self) -> None: """定义启动时的操作,例如和平台建立连接""" self.task = asyncio.create_task(self._forward_ws()) # 建立 ws 连接 async def _forward_ws(self): request = Request( method="GET", url="your_platform_websocket_url", headers={"token": "..."}, # 鉴权请求头 ) while True: try: async with self.websocket(request) as ws: try: # 处理 websocket ... except WebSocketClosed as e: log( "ERROR", "WebSocket Closed", e, ) except Exception as e: log( "ERROR", "Error while process data from " "websocket platform_websocket_url. " "Trying to reconnect...", e, ) finally: # 这里要断开 Bot 连接 except Exception as e: # 尝试重连 log( "ERROR", "Error while setup websocket to " "platform_websocket_url. Trying to reconnect...", e, ) await asyncio.sleep(3) # 重连间隔 async def shutdown(self) -> None: """定义关闭时的操作,例如停止任务、断开连接""" # 断开 ws 连接 if self.task is not None and not self.task.done(): self.task.cancel() ``` ##### 服务端通信方式 ```python {30,38} title=adapter.py from nonebot import get_plugin_config from nonebot.drivers import ( Request, ASGIMixin, WebSocket, HTTPServerSetup, WebSocketServerSetup ) class Adapter(BaseAdapter): @override def __init__(self, driver: Driver, **kwargs: Any): super().__init__(driver, **kwargs) self.adapter_config: Config = get_plugin_config(Config) self.setup() def setup(self) -> None: if not isinstance(self.driver, ASGIMixin): raise RuntimeError( f"Current driver {self.config.driver} doesn't support asgi server!" f"{self.get_name()} Adapter need a asgi server driver to work." ) # 建立服务端路由 # HTTP Webhook 路由 http_setup = HTTPServerSetup( URL("your_webhook_url"), # 路由地址 "POST", # 接收的方法 "WEBHOOK name", # 路由名称 self._handle_http, # 处理函数 ) self.setup_http_server(http_setup) # 反向 Websocket 路由 ws_setup = WebSocketServerSetup( URL("your_websocket_url"), # 路由地址 "WebSocket name", # 路由名称 self._handle_ws, # 处理函数 ) self.setup_websocket_server(ws_setup) async def _handle_http(self, request: Request) -> Response: """HTTP 路由处理函数,只有一个类型为 Request 的参数,且返回值类型为 Response""" ... return Response( status_code=200, # 状态码 headers={"something": "something"}, # 响应头 content="xxx", # 响应内容 ) async def _handle_ws(self, websocket: WebSocket) -> Any: """WebSocket 路由处理函数,只有一个类型为 WebSocket 的参数""" ... ``` 更多通信交互方式可以参考以下适配器: - [OneBot](https://github.com/nonebot/adapter-onebot/blob/master/nonebot/adapters/onebot/v11/adapter.py) - `WebSocket 客户端`、`WebSocket 服务端`、`HTTP WEBHOOK`、`HTTP POST` - [QQ](https://github.com/nonebot/adapter-qq/blob/master/nonebot/adapters/qq/adapter.py) - `WebSocket 服务端`、`HTTP WEBHOOK` - [Telegram](https://github.com/nonebot/adapter-telegram/blob/beta/nonebot/adapters/telegram/adapter.py) - `HTTP WEBHOOK` #### 建立 Bot 连接 在与平台建立连接后,我们需要将 [Bot](#bot) 实例化,并调用适配器提供的的 `bot_connect` 方法告知 NoneBot 建立了 Bot 连接。在与平台断开连接或出现某些异常进行重连时,我们需要调用 `bot_disconnect` 方法告知 NoneBot 断开了 Bot 连接。 ```python {7,8,11} title=adapter.py from .bot import Bot class Adapter(BaseAdapter): def _handle_connect(self): bot_id = ... # 通过配置或者平台 API 等方式,获取到 Bot 的 ID bot = Bot(self, self_id=bot_id) # 实例化 Bot self.bot_connect(bot) # 建立 Bot 连接 def _handle_disconnect(self): self.bot_disconnect(bot) # 断开 Bot 连接 ``` #### 转换 Event 事件 在接收到来自平台的事件数据后,我们需要将其转为适配器的 [Event](#event),并调用 Bot 的 `handle_event` 方法来让 Bot 对事件进行处理: ```python title=adapter.py import asyncio from typing import Any, Dict from nonebot.compat import type_validate_python from .bot import Bot from .event import Event from .log import log class Adapter(BaseAdapter): @classmethod def payload_to_event(cls, payload: Dict[str, Any]) -> Event: """根据平台事件的特性,转换平台 payload 为具体 Event Event 模型继承自 pydantic.BaseModel,具体请参考 pydantic 文档 """ # 做一层异常处理,以应对平台事件数据的变更 try: return type_validate_python(your_event_class, payload) except Exception as e: # 无法正常解析为具体 Event 时,给出日志提示 log( "WARNING", f"Parse event error: {str(payload)}", ) # 也可以尝试转为基础 Event 进行处理 return type_validate_python(Event, payload) async def _forward(self, bot: Bot): payload: Dict[str, Any] # 接收到的事件数据 event = self.payload_to_event(payload) # 让 bot 对事件进行处理 asyncio.create_task(bot.handle_event(event)) ``` #### 调用平台 API 我们需要实现 `Adapter` 的 `_call_api` 方法,使开发者能够调用平台提供的 API。如果通过 WebSocket 通信可以通过 `send` 方法来发送数据,如果采用 HTTP 请求,则需要通过 NoneBot 提供的 `Request` 对象,调用 `driver` 的 `request` 方法来发送请求。 ```python {11} title=adapter.py from typing import Any from typing_extensions import override from nonebot.drivers import Request, WebSocket from .bot import Bot class Adapter(BaseAdapter): @override async def _call_api(self, bot: Bot, api: str, **data: Any) -> Any: log("DEBUG", f"Calling API {api}") # 给予日志提示 platform_data = your_handle_data_method(data) # 自行将数据转为平台所需要的格式 # 采用 HTTP 请求的方式,需要构造一个 Request 对象 request = Request( method="GET", # 请求方法 url=api, # 接口地址 headers=..., # 请求头,通常需要包含鉴权信息 params=platform_data, # 自行处理数据的传输形式 # json=platform_data, # data=platform_data, ) # 发送请求,返回结果 return await self.driver.request(request) # 采用 WebSocket 通信的方式,可以直接调用 send 方法发送数据 # 通过某种方式获取到 bot 对应的 websocket 对象 ws: WebSocket = your_get_websocket_method(bot.self_id) await ws.send_text(platform_data) # 发送 str 类型的数据 await ws.send_bytes(platform_data) # 发送 bytes 类型的数据 await ws.send(platform_data) # 是以上两种方式的合体 # 接收并返回结果,同样的,也有 str 和 bytes 的区别 return await ws.receive_text() return await ws.receive_bytes() return await ws.receive() ``` `调用平台 API` 实现方式具体可以参考以下适配器: Websocket: - [OneBot V11](https://github.com/nonebot/adapter-onebot/blob/54270edbbdb2a71332d744f90b1a3d7f4bf6463a/nonebot/adapters/onebot/v11/adapter.py#L167-L177) - [OneBot V12](https://github.com/nonebot/adapter-onebot/blob/54270edbbdb2a71332d744f90b1a3d7f4bf6463a/nonebot/adapters/onebot/v12/adapter.py#L204-L218) HTTP: - [OneBot V11](https://github.com/nonebot/adapter-onebot/blob/54270edbbdb2a71332d744f90b1a3d7f4bf6463a/nonebot/adapters/onebot/v11/adapter.py#L179-L215) - [OneBot V12](https://github.com/nonebot/adapter-onebot/blob/54270edbbdb2a71332d744f90b1a3d7f4bf6463a/nonebot/adapters/onebot/v12/adapter.py#L220-L266) - [QQ](https://github.com/nonebot/adapter-qq/blob/dc5d437e101f0e3db542de3300758a035ed7036e/nonebot/adapters/qq/adapter.py#L599-L605) - [Telegram](https://github.com/nonebot/adapter-telegram/blob/4a8633627e619245516767f5503dec2f58fe2193/nonebot/adapters/telegram/adapter.py#L148-L253) - [飞书](https://github.com/nonebot/adapter-feishu/blob/f8ab05e6d57a5e9013b944b0d019ca777725dfb0/nonebot/adapters/feishu/adapter.py#L201-L218) ### Bot Bot 是机器人开发者能够直接获取并使用的核心对象,负责存储平台机器人相关信息,并提供回复事件、调用 API 的上层方法。我们需要继承基类 `Bot`,并实现相关方法: ```python {20,25,34} title=bot.py from typing import TYPE_CHECKING, Any, Union from typing_extensions import override from nonebot.message import handle_event from nonebot.adapters import Bot as BaseBot from .event import Event from .message import Message, MessageSegment if TYPE_CHECKING: from .adapter import Adapter class Bot(BaseBot): """ your_adapter_name 协议 Bot 适配。 """ @override def __init__(self, adapter: Adapter, self_id: str, **kwargs: Any): super().__init__(adapter, self_id) self.adapter: Adapter = adapter # 一些有关 Bot 的信息也可以在此定义和存储 async def handle_event(self, event: Event): # 根据需要,对事件进行某些预处理,例如: # 检查事件是否和机器人有关操作,去除事件消息首尾的 @bot # 检查事件是否有回复消息,调用平台 API 获取原始消息的消息内容 ... # 调用 handle_event 让 NoneBot 对事件进行处理 await handle_event(self, event) @override async def send( self, event: Event, message: Union[str, Message, MessageSegment], **kwargs: Any, ) -> Any: # 根据平台实现 Bot 回复事件的方法 # 将消息处理为平台所需的格式后,调用发送消息接口进行发送,例如: data = message_to_platform_data(message) await self.send_message( data=data, ... ) ``` ### Event Event 是 NoneBot 中的事件主体对象,所有平台消息在进入处理流程前需要转换为 NoneBot 事件。我们需要继承基类 `Event`,并实现相关方法: ```python {5,8,13,18,23,28,33} title=event.py from typing_extensions import override from nonebot.compat import model_dump from nonebot.adapters import Event as BaseEvent class Event(BaseEvent): @override def get_event_name(self) -> str: # 返回事件的名称,用于日志打印 return "event name" @override def get_event_description(self) -> str: # 返回事件的描述,用于日志打印,请注意转义 loguru tag return escape_tag(repr(model_dump(self))) @override def get_message(self): # 获取事件消息的方法,根据事件具体实现,如果事件非消息类型事件,则抛出异常 raise ValueError("Event has no message!") @override def get_user_id(self) -> str: # 获取用户 ID 的方法,根据事件具体实现,如果事件没有用户 ID,则抛出异常 raise ValueError("Event has no context!") @override def get_session_id(self) -> str: # 获取事件会话 ID 的方法,根据事件具体实现,如果事件没有相关 ID,则抛出异常 raise ValueError("Event has no context!") @override def is_tome(self) -> bool: # 判断事件是否和机器人有关 return False ``` 然后根据平台消息的类型,编写各种不同的事件,并且注意要根据事件类型实现 `get_type` 方法,具体请参考[事件类型](../advanced/adapter#事件类型)。消息类型事件还应重写 `get_message` 和 `get_user_id` 等方法,例如: ```python {7,16,20,25,34,42} title=event.py from .message import Message class HeartbeatEvent(Event): """心跳时间,通常为元事件""" @override def get_type(self) -> str: return "meta_event" class MessageEvent(Event): """消息事件""" message_id: str user_id: str @override def get_type(self) -> str: return "message" @override def get_message(self) -> Message: # 返回事件消息对应的 NoneBot Message 对象 return self.message @override def get_user_id(self) -> str: return self.user_id class JoinRoomEvent(Event): """加入房间事件,通常为通知事件""" user_id: str room_id: str @override def get_type(self) -> str: return "notice" class ApplyAddFriendEvent(Event): """申请添加好友事件,通常为请求事件""" user_id: str @override def get_type(self) -> str: return "request" ``` ### Message Message 负责正确序列化消息,以便机器人插件处理。我们需要继承 `MessageSegment` 和 `Message` 两个类,并实现相关方法: ```python {9,12,17,22,27,30,36} title=message.py from typing import Type, Iterable from typing_extensions import override from nonebot.utils import escape_tag from nonebot.adapters import Message as BaseMessage from nonebot.adapters import MessageSegment as BaseMessageSegment class MessageSegment(BaseMessageSegment["Message"]): @classmethod @override def get_message_class(cls) -> Type["Message"]: # 返回适配器的 Message 类型本身 return Message @override def __str__(self) -> str: # 返回该消息段的纯文本表现形式,通常在日志中展示 return "text of MessageSegment" @override def is_text(self) -> bool: # 判断该消息段是否为纯文本 return self.type == "text" class Message(BaseMessage[MessageSegment]): @classmethod @override def get_segment_class(cls) -> Type[MessageSegment]: # 返回适配器的 MessageSegment 类型本身 return MessageSegment @staticmethod @override def _construct(msg: str) -> Iterable[MessageSegment]: # 实现从字符串中构造消息数组,如无字符串嵌入格式可直接返回文本类型 MessageSegment ... ``` 然后根据平台具体的消息类型,来实现各种 `MessageSegment` 消息段,具体可以参考以下适配器: - [OneBot V11](https://github.com/nonebot/adapter-onebot/blob/54270edbbdb2a71332d744f90b1a3d7f4bf6463a/nonebot/adapters/onebot/v11/message.py#L25-L259) - [QQ](https://github.com/nonebot/adapter-qq/blob/dc5d437e101f0e3db542de3300758a035ed7036e/nonebot/adapters/qq/message.py#L30-L520) - [Telegram](https://github.com/nonebot/adapter-telegram/blob/4a8633627e619245516767f5503dec2f58fe2193/nonebot/adapters/telegram/message.py#L13-L414) ## 适配器测试 关于适配器测试相关内容在这里不再展开,开发者可以根据需要进行合适的测试。这里为开发者提供几个常见问题的解决方法: 1. 在测试中无法导入 editable 模式安装的适配器代码。在 pytest 的 `conftest.py` 内添加如下代码: ```python title=tests/conftest.py from pathlib import Path import nonebot.adapters nonebot.adapters.__path__.append( # type: ignore str((Path(__file__).parent.parent / "nonebot" / "adapters").resolve()) ) ``` 2. 需要计算适配器测试覆盖率,请在 `pyproject.toml` 中添加 pytest 配置: ```toml title=pyproject.toml [tool.pytest.ini_options] addopts = "--cov nonebot/adapters/{adapter-name} --cov-report term-missing" ``` ## 后续工作 在完成适配器代码的编写后,如果想要将适配器发布到 NoneBot 商店,我们需要将适配器发布到 PyPI 中,然后前往[商店](/store/adapters)页面,切换到适配器页签,点击**发布适配器**按钮,填写适配器相关信息并提交。 另外建议编写适配器文档或者一些插件开发示例,以便其他开发者使用我们的适配器。 ================================================ FILE: website/versioned_docs/version-2.4.4/developer/plugin-publishing.mdx ================================================ --- sidebar_position: 0 description: 在商店发布自己的插件 --- # 发布插件 import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; NoneBot 为开发者提供了分享插件的官方商店。本指南囊括**从创建项目到发布到 PyPI,最终提交商店审核**的全过程。 :::warning 警告 如果你的插件只是满足自用需求,则完全可以选择**不发布插件**。发布插件**不是**使用插件的必要条件。 NoneBot 社区对于插件有一定质量要求,对于不符合要求的插件,社区成员将会要求修改,直至符合要求后才能通过审核;如果长期未更新修改,社区将会关闭当前请求,之后如需发布请重新提交发布插件请求。相应的要求会在本章节以下部分介绍。 ::: :::tip 提示 本章节仅包含插件发布流程指导,插件开发请查阅前述章节。 ::: ## 准备工作 ### 插件命名规范 NoneBot 插件使用下述命名规范: - 对于**项目名**,建议统一以 `nonebot-plugin-` 开头,之后为拟定的插件名字,词间用横杠 `-` 分隔; - **项目名**用于代码仓库名称、PyPI 包的发布名称等; - 本文使用 `nonebot-plugin-{your-plugin-name}` 为例。 - 对于**模块名**,建议与**项目名**一致,但词间用下划线 `_` 分隔,即统一以 `nonebot_plugin_` 开头,之后为拟定的插件名字; - **模块名**用于程序导入使用,应为插件文件(夹)的名称; - 本文使用 `nonebot_plugin_{your_plugin_name}` 为例。 ### 项目结构 :::tip 提示 本段所述的项目结构仅作推荐,不做强制要求。 ::: 插件程序本身结构可参考[插件结构](../tutorial/create-plugin.md#插件结构)一节,唯一区别在于,插件包可以直接处于项目顶层。 插件项目的一种组织结构如下: ```tree 📦 nonebot-plugin-{your-plugin-name} ├── 📂 nonebot_plugin_{your_plugin_name} │ ├── 📜 __init__.py │ └── 📜 config.py ├── 📜 pyproject.toml └── 📜 README.md ``` 功能开发可以在 `__init__.py` 中进行或在包内部创建其他模块并在 `__init__.py` 中导入。 ### 从项目模板开始 为降低新手门槛,我们提供三条清晰、完整、可复制的发布路径。 :::tip 提示 你只需选择一条与你习惯一致的路径,**完整跟随即可成功发布**。无需在不同工具间切换或猜测配置。 ::: NoneBot 生态目前有如下插件项目模板: - [RF-Tar-Railt/nonebot-plugin-template](https://github.com/RF-Tar-Railt/nonebot-plugin-template) 此路径使用 **PDM** 项目管理器,符合 PEP 621 标准,自动化程度高。 - [fllesser/nonebot-plugin-template](https://github.com/fllesser/nonebot-plugin-template) 此路径使用 **uv** 项目管理器和 **PoeThePoet** 任务运行器,构建速度快,适合追求效率的开发者。 - [A-kirami/nonebot-plugin-template](https://github.com/A-kirami/nonebot-plugin-template) 此路径使用 **Poetry** 项目管理器,适合熟悉传统 Python 生态的开发者。 #### 1. 创建项目 1. 访问上述三个模板之一。 2. 点击 **“Use this template”** → **“Create a new repository”**。 3. 仓库名称填写:`nonebot-plugin-{your-plugin-name}`(此部分以 `nonebot-plugin-weather` 为例)。 4. 点击 **“Create repository from template”**。 #### 2. 配置发布权限 1. 进入新仓库 → **Settings** → **Actions** → **General**。 2. 在 **Workflow permissions** 下,勾选 **“Read and write permissions”** → 点击 **Save**。 #### 3. 全局替换项目信息 在仓库中点击 **“Add file”** → **“Create new file”**,创建一个空文件 `LICENSE`,选择开源协议并提交(此操作会触发工作流)。 然后在本地克隆仓库,使用编辑器对以下内容进行**全局替换**: :::tip 提示 此部分以“天气插件”为例,实际的替换内容应该根据你所创建的插件名称等相应调整。 ::: | 原内容 | 替换为 | | ------------------------------ | ---------------------------------- | | `nonebot-plugin-template` | `nonebot-plugin-weather` | | `nonebot_plugin_template` | `nonebot_plugin_weather` | | `` | `天气查询` | | `` | `查询指定城市的实时天气与未来预报` | | `` | `你的GitHub用户名` | | `` | `你的邮箱` | #### 4. 安装依赖与开发 ```bash # 安装 PDM(若未安装) curl -sSL https://pdm-project.org/install-pdm.py | python3 - # 安装项目依赖(自动创建虚拟环境) pdm sync # 添加新依赖(如 httpx) pdm add httpx ``` ```bash # 安装 uv(Windows) powershell -c "irm https://astral.sh/uv/install.ps1 | iex" # 安装 uv(macOS/Linux) curl -LsSf https://astral.sh/uv/install.sh | sh # 安装所有依赖(含 dev) uv sync --all-groups -p 3.12 # 添加新依赖 uv add httpx ``` ```bash # 安装 Poetry(推荐方式) curl -sSL https://install.python-poetry.org | python3 - # 安装项目依赖 poetry install # 添加新依赖 poetry add httpx ``` #### 5. 更新版本并发布 [bump-my-version](https://github.com/callowayproject/bump-my-version) 是一个功能强大、可配置的 Python 项目版本更新工具,支持自动提交到 Git 等 VCS。 ```bash # 安装 bump-my-version pdm add --dev bump-my-version # 更新 patch 版本 pdm run bump patch # 推送 tag 触发发布 git push origin --tags ``` ```bash # 更新 patch 版本 uv run poe bump patch # 推送 tag 触发发布 git push origin --tags ``` ```bash # 安装 bump-my-version poetry add --dev bump-my-version # 更新 patch 版本 poetry run bump patch # 推送 tag 触发发布 git push origin --tags ``` 需要安装 PDM 插件 [pdm-bump](https://github.com/carstencodes/pdm-bump)。 ```bash # 安装 pdm-bump pdm self add pdm-bump # 更新 patch 版本 pdm bump patch # 推送 tag 触发发布 git push origin --tags ``` ```bash # 更新 patch 版本 uv version --bump patch # 创建相应提交与标签 git add pyproject.toml git commit -m "chore: release v0.1.1" # 替换为实际的版本号 git tag v0.1.1 # 替换为实际的版本号 # 推送 tag 触发发布 git push origin --tags ``` ```bash # 更新版本(自动提交并打标签) poetry version patch # 推送 tag 触发发布 git push origin --tags ``` 手动更新 `pyproject.toml` 中的 `version` 字段,然后推送 tag 触发发布工作流 ```bash git add pyproject.toml git commit -m "chore: release v0.1.1" # 替换为实际的版本号 git tag v0.1.1 # 替换为实际的版本号 git push origin --tags ``` 推送 `v*` 标签后,模板提供的 GitHub Actions 工作流将自动构建并发布到 PyPI。 #### 6. 发布到 [PyPI](https://pypi.org) 不同模板使用的发布方式可能不同,具体配置流程参考对应模板的详细使用指南。 根据选用的构建系统,在项目的 `pyproject.toml` 中填入必要信息后进行构建与发布。 :::tip 提示 不同构建工具的使用可能存在差别。本文仅以 [`pdm`](https://pdm-project.org/zh/latest/), [`poetry`](https://python-poetry.org/docs/), [`setuptools`](https://setuptools.pypa.io/en/latest/) 构建系统**本地构建与发布**为示例讲解,其余构建/管理工具等和自动化构建的用法请读者自行探索。 ::: ```bash poetry publish --build # 构建并发布 # 等效于以下两个命令 poetry build # 只构建 poetry publish # 只发布先前的构建 ``` ```bash pdm publish # 构建并发布 # 等效于以下两个命令 pdm build # 只构建 pdm publish --no-build # 只发布先前的构建 ``` ```bash pip install build twine # 安装通用构建与发布工具 python -m build --sdist --wheel . # 只构建 twine upload dist/* # 只发布先前的构建 ``` :::tip 提示 发布前建议自行测试构建包是否可用,避免遗漏代码文件或资源文件等问题。 ::: ## 基本要求 无论你选择哪条路径,以下内容**必须**完成,否则无法通过商店自动检查: ### 能够正确加载 插件包必须能够被 NoneBot 正确加载,在商店审核中会通过 **NoneFlow** 自动化加载测试进行。 #### 依赖其他插件 如果插件依赖其他插件提供的功能,则**必须**在代码中使用 `require()` 来引入该插件,然后才能 `import` 该插件提供的功能。具体细节参阅[跨插件访问](../advanced/requiring.md)。 使用示例如下: ```python title=nonebot_plugin_weather/__init__.py from nonebot import require require("nonebot_plugin_apscheduler") from nonebot_plugin_apscheduler import scheduler ``` #### 不能零配置加载的插件 如果插件需要必要配置项才能正常导入,则**必须**在商店提交表单中填写必要的配置项内容。 但一种更好的做法是,**将插件设计为零配置即可加载**(允许缺少必要配置项时插件仍能正常导入,但不执行需要相应配置项的功能),尤其是对于一些必要配置含有敏感信息(如密钥、Token、API Key 等)的插件。这样可以避免在商店提交表单时填写敏感信息的风险。 ### 插件元数据 插件包**必须**填写元数据才能通过 **NoneFlow** 自动化检查。 下面是一个示例: ```python title=nonebot_plugin_weather/__init__.py from nonebot.plugin import PluginMetadata from .config import Config __plugin_meta__ = PluginMetadata( # 基本信息(必填) name="天气查询", # 插件名称 description="查询指定城市的实时天气与未来预报", # 插件介绍 usage="发送【天气 城市名】获取天气信息", # 插件用法 # 发布额外信息 type="application", # 插件分类 # 发布必填,当前有效类型有:`library`(为其他插件编写提供功能),`application`(向机器人用户提供功能)。 homepage="https://github.com/你的用户名/nonebot-plugin-weather", # 发布必填。 config=Config, # 插件配置项类,如果有配置类则必须填写。 supported_adapters={"~onebot.v11"}, # 支持的适配器集合,其中 `~` 在此处代表前缀 `nonebot.adapters.`,其余适配器亦按此格式填写。 # 若插件只使用了 NoneBot 基本抽象,应显式填写 None,否则应该列出插件支持的适配器。 ) ``` :::caution 注意 `__plugin_meta__` 变量**必须**处于插件最外层(如 `__init__.py` 中),否则无法正常识别。 一般做法是在 `__init__.py` 中定义 `__plugin_meta__`。 ::: #### 继承其他插件支持的适配器 如果你的插件依赖于其他插件提供的支持功能,而其他插件可能支持更少的适配器,这时就应该使用 [inherit_supported_adapters()](../api/plugin/load#inherit-supported-adapters) 函数来继承其他插件支持的适配器。 示例用法如下: ```python title=nonebot_plugin_weather/__init__.py from nonebot import require from nonebot.plugin import PluginMetadata, inherit_supported_adapters from .config import Config require("nonebot_plugin_alconna") # 必须先 require 才能被 inherit_supported_adapters 处理 __plugin_meta__ = PluginMetadata( name="天气查询", description="查询指定城市的实时天气与未来预报", usage="发送【天气 城市名】获取天气信息", type="application", homepage="https://github.com/你的用户名/nonebot-plugin-weather", config=Config, supported_adapters=inherit_supported_adapters("nonebot_plugin_alconna"), # 继承 nonebot_plugin_alconna 插件的适配器支持列表 ) ``` ### 准备项目主页 通常可以使用 GitHub 项目页面作为项目主页,在 `README.md` 文件中编写插件介绍等内容。 内容大致包括: - 插件功能介绍; - 安装方法 - **必须**有 NB-CLI 方式安装 - 可选依赖可以给出其他安装方式 - **不得**使用旧式的 `bot.py` 配置 - 插件配置项(如 `Config` 类字段,若无可跳过) - 插件设置的触发规则(若无可跳过) - 插件的其它用法(按需编写) - 效果图、权限说明(按需编写) ## 质量要求 以下内容**强烈建议**完成,否则社区成员将会要求修改: ### 依赖管理原则 - **必须**包含 `nonebot2`。 - **必须**将插件直接使用的适配器加入依赖列表,如:使用 OneBot 适配器的插件应添加 `nonebot-adapter-onebot` 依赖; - **禁止**使用 `==` 锁定单一版本,使用 `>=` 或 `~=`。 - **禁止**添加 `nonebot`(V1)作为依赖。 - 所有在代码中 `import` 的第三方库,必须在 `pyproject.toml` 的 `dependencies` 中列出。 ### 避免误用同步操作 NoneBot 是一个异步框架,插件中**禁止**使用任何可能阻塞事件循环的同步操作,例如: - 同步 HTTP 请求(如 `requests` 库); **推荐**操作(以 `httpx` 为例): ```python import httpx async with httpx.AsyncClient() as client: response = await client.get("https://api.example.com/data") # 异步操作,不阻塞机器人 ``` **禁止**操作: ```python import requests requests.get("https://api.example.com/data") # 同步操作,会阻塞机器人 ``` - 其他可能长时间运行阻塞事件循环的操作。 ### 本地文件存储 如果插件需要在本地存储数据、配置或缓存文件,**必须**使用 [`nonebot-plugin-localstore`](https://github.com/nonebot/plugin-localstore) 管理,具体细节参阅[本地存储](../best-practice/data-storing.md)章节。 参考示例: ```python title=nonebot_plugin_weather/__init__.py from pathlib import Path from nonebot import require require("nonebot_plugin_localstore") import nonebot_plugin_localstore as store # 获取插件缓存文件(夹)路径 weather_cache_dir: Path = store.get_plugin_cache_dir() weather_cache_file: Path = store.get_plugin_cache_file("cache.json") # 获取插件配置文件(夹)路径 weather_config_dir: Path = store.get_plugin_config_dir() weather_config_file: Path = store.get_plugin_config_file("config.toml") # 获取插件数据文件(夹)路径 weather_data_dir: Path = store.get_plugin_data_dir() weather_data_file: Path = store.get_plugin_data_file("resource-index.json") ``` ## 商店审核 ### 提交申请 完成在 PyPI 的插件发布流程后,前往[商店](/store/plugins)页面,切换到插件页签,点击 **发布插件** 按钮。 在弹出的插件信息提交表单内,填入您所要发布的相应插件信息。请注意,如果插件需要必要配置项才能正常导入,请在“插件配置项”中填写必要的内容(请勿填写密钥等敏感信息)。 完成填写后,点击 **发布** 按钮,这将自动跳转 NoneBot 仓库内的“发布插件”页面。确认信息无误后点击页面下方的 `Submit new issue` 按钮进行最终提交即可。 ### 等待插件审核 插件发布 Issue 创建后,将会经过 **NoneFlow Bot** 的自动前置检查,以确保插件信息正确无误、插件能被正确加载。 :::tip 提示 若插件检查未通过或信息有误,**不必**关闭当前 Issue。只需更新插件并上传到 PyPI/修改信息后勾选插件测试勾选框即可重新触发插件检查。 ::: 之后,NoneBot 的维护者和一些志愿者会初步检查插件代码,帮助减少该插件的问题。 完成这些步骤后,您的插件将会被自动合并到[商店](/store/plugins),而您也将成为 [**NoneBot 贡献者**](https://github.com/nonebot/nonebot2/graphs/contributors)的一员。 ================================================ FILE: website/versioned_docs/version-2.4.4/editor-support.md ================================================ --- sidebar_position: 2 description: 配置编辑器以获得最佳体验 --- # 编辑器支持 框架基于 [PEP 484](https://www.python.org/dev/peps/pep-0484/)、[PEP 561](https://www.python.org/dev/peps/pep-0561/)、[PEP 8](https://www.python.org/dev/peps/pep-0008/) 等规范进行开发并且**拥有完整类型注解**。框架使用 Pyright(Pylance)工具进行类型检查,确保代码可以被编辑器正确解析。 ## CLI 脚手架提供的编辑器工具支持 在使用 NB-CLI [创建项目](./quick-start.mdx#创建项目)时,如果选择了用于插件开发的 `simple` 模板,其会根据选择的开发工具,**自动配置项目根目录下的 `.vscode/extensions.json` 文件**,以推荐最匹配的 VS Code 插件,同时自动将相应的预设配置项写入 `pyproject.toml` 作为“开箱即用”配置,从而提升开发体验。 ```bash [?] 选择一个要使用的模板: simple (插件开发者) ... [?] 要使用哪些开发工具? ``` ### 支持的开发工具 1. Pyright (Pylance) [VS Code 插件](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance) | [项目](https://github.com/microsoft/pyright) | [文档](https://microsoft.github.io/pyright/) 由微软开发的 Python 静态类型检查器和语言服务器,提供智能感知、跳转定义、查找引用、实时错误检查等强大功能。 作为 VS Code 官方推荐的 Python 语言服务器,与 Pylance 扩展配合使用,能提供最流畅、最准确的代码补全和类型推断体验,是绝大多数开发者的首选。 2. Ruff [VS Code 插件](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff) | [项目](https://github.com/astral-sh/ruff) | [文档](https://docs.astral.sh/ruff/) 一个用 Rust 编写的超快 Python 代码格式化和 lint 工具,完全兼容 `black`、`isort`、`flake8` 等主流工具的规则。 速度极快(比 `black` 和 `flake8` 快 100 倍以上),配置简单,能自动格式化代码并检测潜在错误、代码风格问题(尤其是误用同步网络请求库),是提升代码质量和开发效率的必备利器。 3. MyPy [VS Code 插件](https://marketplace.visualstudio.com/items?itemName=matangover.mypy) | [项目](https://github.com/python/mypy) | [文档](https://mypy.readthedocs.io/en/stable/index.html) 一个官方实现的 Python 静态类型检查器,通过分析代码中的类型注解来发现类型错误。 4. BasedPyright [VS Code 插件](https://marketplace.visualstudio.com/items?itemName=detachhead.basedpyright) | [项目](https://github.com/DetachHead/basedpyright) | [文档](https://docs.basedpyright.com/) 一个基于 Pyright 的、由社区维护的替代性 Python 语言服务器,旨在提供更优的类型检查支持与接近 Pylance 的更好的使用体验。 相较于 Pylance,BasedPyright 允许配合 VS Code 之外的其他编辑器使用,同时也复刻了部分 Pylance 限定的功能。 如果您是高级用户,希望尝试 Pylance 的替代方案,或遇到 Pylance 在特定环境下的兼容性问题,可以考虑使用 BasedPyright。 :::caution 提示 为避免 `Pylance` 和 `BasedPyright` 相互冲突导致配置混乱甚至异常,脚手架默认不允许在创建项目时同时配置这两者。 如果确实需要同时使用,请在创建项目时选择 Pylance/Pyright 并根据[相关文档](https://docs.basedpyright.com/latest/installation/ides/#vscode-vscodium)进行手动配置。 ::: ### 配置效果 选择上述工具后,NB-CLI 会在您的项目根目录下生成一个 `.vscode/extensions.json` 文件并在 `pyproject.toml` 文件中写入相应的配置项。当您在 VS Code 中打开此项目时,IDE 会自动弹出提示,建议您安装这些推荐的扩展,一键即可完成开发环境的初始化,让您可以立即开始编写代码,无需手动搜索和安装插件。 ## 编辑器推荐配置 ### 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` 前往编辑。 ================================================ FILE: website/versioned_docs/version-2.4.4/ospp/2021.md ================================================ --- sidebar_position: 0 description: 开源软件供应链点亮计划 - 暑期 2021 mdx: format: md --- # 暑期 2021 **开源软件供应链点亮计划 - 暑期 2021** 是**中国科学院软件研究所**与 **openEuler 社区**共同举办的一项面向高校学生的暑期活动,旨在鼓励在校学生积极参与开源软件的开发维护,促进优秀开源软件社区的蓬勃发展。关于具体的活动规划、报名方式,请查看该活动的 [官网](https://summer.iscas.ac.cn/) 和 [帮助文档](https://summer.iscas.ac.cn/help/)。 NoneBot 社区有幸作为开源社区参与了本次活动,下面列出了目前我们已经发布的项目,欢迎感兴趣的同学在上面给出的活动官网报名,或通过 [contact@nonebot.dev](mailto:contact@nonebot.dev) 联系我们。 ## NoneBot v1 ### 更新 NoneBot v1 文档中的“指南”部分 由于 NoneBot v1 和 aiocqhttp 最初基于的 QQ 机器人平台不再提供服务,CQHTTP 接口也转型且改名为 OneBot 标准,目前 NoneBot v1 文档的“指南”部分和 aiocqhttp 文档有部分过时内容需要更新。我们希望将其中与旧的机器人平台相关的内容改为基于 go-cqhttp 或通用的 OneBot 表述,同时对 NoneBot v1 的 awesome-bot 示例做一次全面检查,修改其中可能已经不可用的部分。 **难度**:低 **导师**:[@cleoold](https://github.com/cleoold) **产出要求** - 修改“指南”文档和 aiocqhttp 文档中与旧的 QQ 机器人平台相关的部分 - 检查 awesome-bot 示例是否有已经过时/不可用的地方,并更新/修复 - 修改“图灵机器人”案例,使用其它 AI 聊天 API 提供商(需先做简单调研) **技术要求** - 熟悉 Python 编程语言及 asyncio 机制 - 了解 Git 基本用法 - 了解聊天机器人基本开发过程 - 了解 VuePress 更佳 ### NoneBot v1 API 文档自动生成 目前 NoneBot v1 的文档中“API”部分是手动编写的,在更新代码接口的同时需要手动更新文档,可能造成文档与代码不匹配,形成额外的维护成本。我们希望将 API 文档改为直接编写在 Python docstring 中,通过工具自动生成 API 文档。 **难度**:中 **导师**:[@cleoold](https://github.com/cleoold) **产出要求** - 调研市面上常见的 Python API 文档生成工具 - 在代码中补充 API 文档 - 编写或应用开源工具自动生成 API 文档 - 配置 GitHub Actions 或其它 CI 自动化构建和部署 API 文档 **技术要求** - 熟悉 Python 编程语言及 asyncio 和 Type Hints - 了解 Git 基本用法 - 了解 Sphinx 等文档生成工具更佳 - 了解 GitHub Actions 等 CI 工具更佳 ## NoneBot v2 ### NoneBot v2 自动化测试框架“NoneBug” 在聊天机器人的开发过程中,一套自动化的测试机制是非常重要的,特别是对于 NoneBot 2 这类为大型机器人开发而设计的项目来说,需要手动测试每一个边际条件是非常痛苦的。我们希望能够开发一款基于 NoneBot 2 插件机制的自动化测试框架,为 NoneBot 2 用户提供一套易用便捷、高度灵活的自动化测试框架。 **难度**:高 **导师**:[@yanyongyu](https://github.com/yanyongyu) **产出要求** - 调研现有的 Python 和其它语言集成测试框架 - 设计 NoneBug 的用户 API 和实现方式 - 实现 NoneBug 自动化测试框架 - 编写详细的使用文档 **技术要求** - 熟悉 Python 编程语言及 asyncio 和 Type Hints - 了解 Git 基本用法 - 了解 NoneBot v2 的基本原理和使用方式 - 了解主流的 Python 自动化测试框架 ### NoneBot v2 Telegram 适配器 目前 NoneBot v2 已支持 OneBot、Mirai HTTP API、钉钉协议,社区反馈有更多的平台需求,希望能在 NoneBot v2 获得更多的跨平台支持,提高机器人的便携性。同时,我们也希望随着新平台加入,提升现有 NoneBot v2 核心代码的平台通用性。Telegram 是一款较为广泛使用的安全即时聊天软件,同时其官方提供了丰富的聊天机器人 API,因此我们希望为 NoneBot v2 编写一个 Telegram 适配器来支持 Telegram 机器人的开发。 **难度**:中 **导师**:[@yanyongyu](https://github.com/yanyongyu) **产出要求** - 调研 Telegram Bot API 以及 WebHook 等官方接口 - 编写 Telegram 适配器并能够使用 - 代码遵守项目 Contributing 规范 **技术要求** - 熟悉 Python 编程语言及 asyncio 和 Type Hints - 了解 Git 基本用法 - 了解 Web 开发相关知识 - 了解 Sphinx 等文档生成工具更佳 ### NoneBot v2 飞书适配器 目前 NoneBot v2 已支持 OneBot、Mirai HTTP API、钉钉协议,社区反馈有更多的平台需求,希望能在 NoneBot v2 获得更多的跨平台支持,提高机器人的便携性。同时,我们也希望随着新平台加入,提升现有 NoneBot v2 核心代码的平台通用性。飞书是目前企业用户广泛使用的即时聊天和协作软件,其官方提供了丰富的聊天机器人 API,因此我们希望为 NoneBot v2 编写一个飞书适配器来支持飞书机器人的开发。 **难度**:中 **导师**:[@yanyongyu](https://github.com/yanyongyu) **产出要求** - 调研飞书机器人 API 以及 WebHook 等官方接口 - 编写飞书适配器并能够使用 - 代码遵守项目 Contributing 规范 **技术要求** - 熟悉 Python 编程语言及 asyncio 和 Type Hints - 了解 Git 基本用法 - 了解 Web 开发相关知识 - 了解 Sphinx 等文档生成工具更佳 ## OneBot ### 设计 OneBot v12 接口标准 目前的 OneBot 标准的 v11 版本仍然与 QQ 平台有较多耦合,我们希望在 v12 去掉与 QQ 耦合的历史包袱,形成一个通用的、可扩展的、易于使用的同时易于实现的聊天机器人接口标准。 **难度**:中 **导师**:[@richardchien](https://github.com/richardchien) **产出要求** - 调研各聊天机器人平台的官方/非官方接口特点 - 通用化 OneBot 核心 API,分离 QQ 特定的 API,去掉无用 API - 优化现有的通信、消息表示机制 - 补充 QQ 特定的缺失 API - 文档需符合风格指南 **技术要求** - 熟悉至少两个聊天平台的聊天机器人开发 - 了解 Git 基本用法 - 了解使用不同语言编写聊天机器人时的常用实践 - 对文档的优雅性与美观性有追求更佳 ### 实现 Rust 版 libonebot 目前最常用的 OneBot 实现包括 go-cqhttp、onebot-kotlin、node-onebot 等,这些实现都各自重复实现了 Web 通信、消息解析、配置读写等功能,当社区中的开发者想针对一个新的聊天平台实现 OneBot 时,他们往往同样需要再次实现类似逻辑。我们希望使用 Rust 编写一个 libonebot 模块,该模块实现所有 OneBot 实现所共享的功能,从而方便其他开发者们使用 Rust 快速编写具体的 OneBot 实现。同时,我们希望借此项目在聊天机器人社区中推广 Rust 编程语言。 > 注:这里的逻辑是 libonebot + 针对某聊天平台的对接代码 = 某聊天平台的 OneBot 实现,libonebot 要做的是让 OneBot 实现的开发者只需编写针对特定聊天平台的对接代码,而无需关心 OneBot 标准定义的通信方式、消息格式等。 **难度**:高 **导师**:[@richardchien](https://github.com/richardchien) **产出要求** - 实现所有 OneBot 实现所共享的功能,包括 Web 通信、消息解析、配置读写等 - 充分考虑同时兼容 OneBot v11 和 v12 接口 - 能够根据用户(OneBot 实现的开发者)所实现的接口自动实现类似 get_available_apis 等接口 - 编写详细的使用文档 - 如果可能,与 v12 设计项目联动,实现第一手 v12 支持 **技术要求** - 熟悉聊天机器人开发 - 熟悉 Rust Web 开发 ### 实现自选语言版 libonebot 目前最常用的 OneBot 实现包括 go-cqhttp、onebot-kotlin、node-onebot 等,这些实现都各自重复实现了 Web 通信、消息解析、配置读写等功能,当社区中的开发者想针对一个新的聊天平台实现 OneBot 时,他们往往同样需要再次实现类似逻辑。我们希望使用 Python、Go、Kotlin、Node、PHP、C#.NET 等主流语言(任选一个)编写 libonebot 模块,该模块实现所有 OneBot 实现所共享的功能,从而方便其他开发者们使用对应语言快速编写具体的 OneBot 实现。 > 注:这里的逻辑是 libonebot + 针对某聊天平台的对接代码 = 某聊天平台的 OneBot 实现,libonebot 要做的是让 OneBot 实现的开发者只需编写针对特定聊天平台的对接代码,而无需关心 OneBot 标准定义的通信方式、消息格式等。 **难度**:中 **导师**:[@richardchien](https://github.com/richardchien) **产出要求** - 实现所有 OneBot 实现所共享的功能,包括 Web 通信、消息解析、配置读写等 - 充分考虑同时兼容 OneBot v11 和 v12 接口 - 编写详细的使用文档 - 如果可能,实现更多附加特性,如根据用户(OneBot 实现的开发者)所实现的接口自动实现类似 get_available_apis 等接口、实现第一手 v12 支持等 **技术要求** - 熟悉聊天机器人开发 - 熟悉所选语言的 Web 开发 ================================================ FILE: website/versioned_docs/version-2.4.4/ospp/2022.md ================================================ --- sidebar_position: 1 description: 开源之夏 - 暑期 2022 mdx: format: md --- # 暑期 2022 **开源之夏 - 暑期 2022** 是由**开源软件供应链点亮计划**发起、由**中国科学院软件研究所**与 **openEuler 社区**主办的一项面向高校学生的暑期活动,类似 Google Summer of Code(GSoC),旨在鼓励在校学生积极参与开源软件的开发维护,促进优秀开源软件社区的蓬勃发展。关于具体的活动规划、报名方式,请查看该活动的 [官网](https://summer-ospp.ac.cn/) 和 [帮助文档](https://summer-ospp.ac.cn/help/)。 NoneBot 社区有幸作为开源社区 [参与](https://summer-ospp.ac.cn/#/org/orgdetail/e1fb5b8d-125a-4138-b756-25bd32c0a31a/) 了本次活动,下面列出了目前我们已经发布的项目,欢迎感兴趣的同学加入 QQ 群 [737131827](https://jq.qq.com/?_wv=1027&k=PEgyGeEu) 或通过 [contact@nonebot.dev](mailto:contact@nonebot.dev) 联系我们。 ## NoneBot2 命令行 CLI 交互体验升级 NoneBot2 为用户提供了命令行脚手架 ──`nb-cli`,辅助用户更好地上手项目以及进行开发。nb-cli 主要包括:创建项目、运行项目、安装与卸载插件、部署项目等功能。随着 NoneBot2 Beta 版本的发布,脚手架功能存在一定的定位不明确、功能体验不佳。本项目旨在重新设计 nb-cli 功能框架,完善功能,优化用户体验。 **难度**:进阶 **导师**:[@yanyongyu](https://github.com/yanyongyu) **产出要求** - 设计 nb-cli 功能框架 - 明确各功能模块 - 设计用户交互模式 - 完成 nb-cli 主要功能代码 - 项目管理 - 插件管理 - 其它 - 同步更新使用文档 **技术要求** - 熟悉 Python 命令行交互代码编写 - 熟悉 NoneBot2 框架功能 - 熟悉 NoneBot2 项目组织方式 **成果仓库** - - ## NoneBot2 命令行即时交互通信设计与实现 NoneBot2 在早期提供了基于网页的 nonebot-plugin-test 插件,无需平台适配接入即可对机器人进行测试,方便了开发者直观的感受机器人文本交互功能。我们希望提供一款基于命令行的适配器/驱动器,用于无平台适配接入、可以运行机器人的场景进行功能体验或测试。 **难度**:进阶 **导师**:[@mnixry](https://github.com/mnixry) **产出要求** - 设计命令行与 NoneBot2 通信模式 - 直接调用/HTTP/WebSocket - 设计命令行交互界面 - 实现相应适配器/驱动器 - 同步更新使用说明文档 **技术要求** - 熟悉 Python 命令行交互代码编写 - 熟悉 NoneBot2 框架功能 - 熟悉 NoneBot2 项目组织方式 **成果仓库** - ## NoneBot2 用户上手与深入教程设计 NoneBot2 为用户提供了详细的文档介绍,辅助用户更好的上手项目以及进行开发。文档分为基础与进阶两个部分。基础部分帮助新用户快速上手开发,主要包括:安装 NoneBot2、使用脚手架、创建配置项目、使用适配器、加载插件、定义消息事件、处理消息事件、调用平台 API 等。进阶部分向已经熟悉开发流程的用户介绍更多高级技巧,主要包括:NoneBot2 工作原理、定时任务、权限控制、钩子函数、跨插件访问、单元测试、发布插件等。目前文档对于用户而言过于费解,导致用户难以理解 NoneBot2 开发。本项目旨在优化文档内容,使其更加通俗易懂,不让文档成为用户上手的阻碍,同时完善进阶内容,让有更复杂需求的用户,同样能从文档中受益。 相关 issue: - - **难度**:进阶 **导师**:[@SK-415](https://github.com/SK-415) **产出要求** - 文档通俗易懂 - 附有适当的图片指引(如 asciinema) - 内容完整,由浅入深 - 适当的界面美化,合理分配布局 **技术要求** - 熟悉文档结构组织与语言表达 - 熟悉 NoneBot2 框架功能 - 熟悉 NoneBot2 项目组织方式 **成果仓库** - ================================================ FILE: website/versioned_docs/version-2.4.4/ospp/2023.md ================================================ --- sidebar_position: 2 description: 开源之夏 - 暑期 2023 mdx: format: md --- # 暑期 2023 **开源之夏 - 暑期 2023** 是由**开源软件供应链点亮计划**发起、由**中国科学院软件研究所**与 **openEuler 社区**主办的一项面向高校学生的暑期活动,类似 Google Summer of Code(GSoC),旨在鼓励在校学生积极参与开源软件的开发维护,促进优秀开源软件社区的蓬勃发展。关于具体的活动规划、报名方式,请查看该活动的 [官网](https://summer-ospp.ac.cn/) 和 [帮助文档](https://summer-ospp.ac.cn/help/)。 NoneBot 社区有幸作为开源社区 [参与](https://summer-ospp.ac.cn/org/orgdetail/e1fb5b8d-125a-4138-b756-25bd32c0a31a?lang=zh) 了本次活动,下面列出了目前我们已经发布的项目,欢迎感兴趣的同学通过 [contact@nonebot.dev](mailto:contact@nonebot.dev) 联系我们。 ## NoneBot 项目管理图形化面板 NoneBot 目前提供了开箱即用的命令行脚手架来帮助初次使用的用户更快的上手编写应用。但是,对于未有一定开发经验的用户,命令行的使用仍具有一定的困难。此外,其他项目如 koishi、vue 等,均可通过图形化界面的形式为用户提供更便捷的项目开发。因此,我们希望借助现有命令行脚手架的可扩展特性,提供一个项目管理面板服务,以网页的形式帮助用户开发 NoneBot 应用。 **难度**:进阶 **导师**:[@mnixry](https://github.com/mnixry) **产出要求** - 设计并实现项目管理面板相关功能 - 创建与管理项目 - 配置与运行项目 - NoneBot 插件管理 - 实现相应 nb-cli 插件提供面板服务 - 代码符合 NoneBot Contributing 规范 **技术要求** - 熟悉 nb-cli 相关功能 - 熟悉 NoneBot 框架功能 - 熟悉前后端相关实现方式 **成果仓库** - ## NoneBot Discord 适配器 NoneBot 作为一个跨平台聊天机器人框架,目前已有 OneBot、飞书、Telegram、QQ 频道等诸多平台的适配支持。作为众多用户期待的平台适配之一,我们希望借此机会接入 Discord 聊天机器人。 **难度**:进阶 **导师**:[@iyume](https://github.com/iyume) **产出要求** - 调研 Discord Bot 相关功能与接口 - 设计与编写 NoneBot Discord 适配器 - 代码符合 NoneBot Contributing 规范 **技术要求** - 熟悉 NoneBot 框架功能 - 熟悉 NoneBot 各模块职责与适配器编写 **成果仓库** - ## NoneBot 数据库支持插件 NoneBot 的插件系统为用户实现应用提供了极高的便捷性,但因此也增加了插件统一管理的难度。目前,我们发现许多用户发布的插件中存在文件存储结构化数据、数据存放散乱等现象,同时插件间也可能产生冲突。因此,我们希望提供一个统一的数据存储与管理方式,便于用户读写应用数据。 **难度**:进阶 **导师**:[@yanyongyu](https://github.com/yanyongyu) **产出要求** - 设计并实现 ORM 插件 - 提供关系模型定义功能 - 提供模型迁移与管理功能 - 能较好的支持 Python 类型检查与推导 - 编写相应的用户使用文档 - 代码符合 NoneBot Contributing 规范 **技术要求** - 熟悉 NoneBot 框架功能与插件编写 - 熟悉 SQLAlchemy 等 ORM 框架 - 熟悉 SQLAlchemy ORM - 熟悉 alembic 等迁移工具 - 熟悉 nb-cli 插件编写 **成果仓库** - ================================================ FILE: website/versioned_docs/version-2.4.4/ospp/2024.md ================================================ --- sidebar_position: 3 description: 开源之夏 - 暑期 2024 mdx: format: md --- # 暑期 2024 **开源之夏 - 暑期 2024** 是**中国科学院软件研究所**发起的**开源软件供应链点亮计划**系列暑期活动,旨在鼓励高校学生积极参与开源软件的开发维护,促进优秀开源软件社区的蓬勃发展。活动联合各大开源社区,针对重要开源软件的开发与维护提供项目开发任务,并向全球高校学生开放报名。关于具体的活动规划、报名方式,请查看该活动的 [官网](https://summer-ospp.ac.cn/) 和 [帮助文档](https://summer-ospp.ac.cn/help/)。 NoneBot 社区有幸作为开源社区 [参与](https://summer-ospp.ac.cn/org/orgdetail/e1fb5b8d-125a-4138-b756-25bd32c0a31a?lang=zh) 了本次活动,下面列出了目前我们已经发布的项目,欢迎感兴趣的同学通过 [contact@nonebot.dev](mailto:contact@nonebot.dev) 联系我们。 ## NonePress 官网组件库更新与优化 NoneBot 官网目前采用基于 TailwindCSS 自研的 NonePress 组件库及 Docusaurus 框架进行构建。由于相关依赖版本迭代迅速,目前官网组件库已产生了较大的版本落后。本项目希望在跟进框架新版本的基础上,对文档整体视觉体验进行重新设计,提升页面的无障碍访问性,基于 React Hydrate 特性实现完整的静态网站生成(SSG)以提升搜索引擎优化(SEO)水平。在解决以上问题的基础上,可对网页的开发以及生产构建性能做相应的优化提升,例如在生产构建使用自有的 webpack loader、替换现有的热重载逻辑以减少开发环境启动耗时等。 **难度**:进阶 **导师**:[@yanyongyu](https://github.com/yanyongyu) **产出要求** - 基于 Docusaurus v3 重构 NonePress 组件库及相关插件 - 升级相关依赖并重新打造 Docusaurus theme(布局与组件) - 根据需求实现/修改 Docusaurus 插件使得官网内容构建正常 - 能够提升页面渲染性能与 MDX 相关能力 - 升级官网采用新版组件库 - Algolia 索引与 SEO 正常 - 桌面端与移动端显示正常 - 优化官网开发与生产构建体验 - (可选)优化官网部分页面 - 优化官网过长的 changelog - 优化官网插件商店的展示细节 **技术要求** - 熟练掌握 TS、PostCSS、TSX、MDX等相关技术 - 掌握 React、Docusaurus、tailwind css 等框架 - 熟悉静态网站生成 SSG、SEO 优化与 Algolia 索引原理等 **成果仓库** - ## NoneFlow 社区自动化工作流管理优化 NoneFlow 在 NoneBot 社区中承担着重要的角色,它由 NoneBot 框架基于 GitHub APP 编写而成,能够自动化的完成许多复杂流程的处理,如:用户请求提交插件到商店时进行自动化检测,并在人工审核通过后自动存储至 registry;定时自动更新 registry 内插件信息,跟进插件新版本情况等。但是,在长期的使用中发现了一些问题和不足的地方,例如:项目本身结构复杂耦合,添加新自动化流程与维护现有流程困难;目前采用了 GitHub 用户名作为插件作者名,但已有不少插件作者改名;插件存储至 registry 并定时更新,缺少统计相关信息以帮助商店更好的展示当前插件状态;插件作者想要修改插件信息时无法便捷的找到操作方式等。本项目希望针对以上问题与不足的地方进行修复与优化,提升用户体验。 **难度**:进阶 **导师**:[@uy/sun](https://github.com/he0119) **产出要求** - 重构现有工作流处理结构 - 整合现有 Issue、Pull Request、Git 相关操作 - 提供用户修改信息的处理方式 - 正确处理 PR 的 Open、Close、Draft 状态 - 修复流程中存在的问题 - 插件作者名正确展示 - registry 定时更新中需要插件测试环境隔离 - 在 registry 定时更新的同时提供统计数据 **技术要求** - 掌握 GitHub APP 开发 - 熟悉 GitHub REST API、GraphQL 等 - 熟悉 GitHub APP 权限限制 - 熟悉 NoneBot 框架与 Python 相关技术 - 熟悉 Git、GitHub Action、GitHub 工作流 **成果仓库** - ## NoneBlockly 低代码框架开发 经过深入分析社区反馈,我们发现部分新手因不熟悉编程概念或框架本身而遇到问题。为了解决初学者在使用面向开发者的聊天机器人框架 NoneBot 时遇到的挑战,我们计划引入 Blockly 提供低代码编程支持。通过减少常见的编码错误和降低入门门槛,使框架对初学者更加友好,从而提升用户体验并有助于 NoneBot 生态的成长。本项目将基于 Blockly 实现 NoneBot 插件的低代码编写,使得用户能够快速搭建聊天机器人。 **难度**:进阶 **导师**:[@mnixry](https://github.com/mnixry) **产出要求** - 实现 NoneBlockly 低代码开发框架 - 能够基于 Alconna 编写跨平台插件 - 确保插件对 Python 和 NoneBot 版本的兼容性 - 支持对多种类型 NoneBot 事件的响应 - 支持对 NoneBot 消息对象的便捷操作 - 集成 localstore 文件存储、apscheduler 定时任务、网络请求等常用功能 - 对接 NB-CLI 脚手架,通过脚手架扩展使用低代码框架 **技术要求** - 掌握 Python 与 NoneBot 框架的使用 - 熟悉 NoneBot 插件的开发,包括事件响应与消息处理等 - 熟悉 NoneBot 生态组件(Alconna、localstore、apscheduler等)的使用 - 了解 NB-CLI 脚手架的扩展开发 - 熟悉 Blockly 低代码框架的使用和开发 **成果仓库** - ================================================ FILE: website/versioned_docs/version-2.4.4/ospp/2025.md ================================================ --- sidebar_position: 4 description: 开源之夏 - 暑期 2025 mdx: format: md --- # 暑期 2025 **开源之夏 - 暑期 2025** 是**中国科学院软件研究所**发起的**开源软件供应链点亮计划**系列暑期活动,旨在鼓励高校学生积极参与开源软件的开发维护,培养和发掘更多优秀的开发者,促进优秀开源软件社区的蓬勃发展,助力开源软件供应链建设。活动联合各大开源社区,针对重要开源软件的开发与维护提供项目开发任务,并向全球高校学生开放报名。关于具体的活动规划、报名方式,请查看该活动的 [官网](https://summer-ospp.ac.cn/) 和 [帮助文档](https://summer-ospp.ac.cn/help/)。 NoneBot 社区有幸作为开源社区 [参与](https://summer-ospp.ac.cn/org/orgdetail/e1fb5b8d-125a-4138-b756-25bd32c0a31a?lang=zh) 了本次活动,下面列出了目前我们已经发布的项目,欢迎感兴趣的同学通过 [contact@nonebot.dev](mailto:contact@nonebot.dev) 联系我们。 ## NoneBot HTML 图片渲染插件 文字与图片一直是聊天机器人的两大主流交互方式,而图片的渲染一直是用户开发应用的一大痛点。常见的方式包括 PIL 图片编辑、浏览器渲染 HTML 截图等。PIL 图片编辑依赖人工构建图片布局,容易出现自适应问题,且提升图片特效、美观程度需要极大的开发成本。浏览器渲染方案通过 HTML 与 CSS 能够轻松完成美观自适应能力强的布局,但其部署门槛较高,难以支撑较大规模调用量。而其他轻量化渲染引擎通常不具有完整 HTML/CSS 现代化标准实现,且未提供 Python Binding 直接使用。 本项目希望调研并实现一种高效、便捷的图片渲染方案。该方案需要在保障跨平台一致性、最大程度保证 HTML 与 CSS 现代化标准的前提下,低成本(资源消耗与吞吐量)将 HTML 渲染为对应图片。 **难度**:进阶 **导师**:[@MelodyKnit](https://github.com/MelodyKnit) **产出要求** - 调研 HTML/CSS 渲染引擎 - 调研 litehtml 等渲染引擎 标准支持能力与兼容性 - 基于渲染引擎实现 HTML 图片渲染插件 - 将渲染引擎通过 binding 等方式集成为 Python 模块 - 基于集成模块实现 HTML 图片渲染能力 - 编写插件使用文档 **技术要求** - 掌握 Python 及其异步编程 - 熟悉 NoneBot 框架及其插件编写 - 了解浏览器与 HTML 渲染原理 **成果仓库** - ## NB-CLI 命令行工具交互优化 NB-CLI 作为 NoneBot 生态的核心入门与管理工具,主要负责新手引导项目创建、项目运行以及插件管理几大功能。目前该脚手架工具仍存在几点缺陷: - 作为插件管理工具,由于存储数据的局限性,无法很好地展示用户项目当前安装插件状态,并进行卸载等操作; - 当前插件管理高度依赖云端 registry 提供插件信息,在离线情况下完全无法使用; - 由于插件信息繁多,工具未能向用户展示充分的信息,交互复杂 体验较差。 以上问题对用户使用 NB-CLI 管理项目插件造成了极大的阻碍。 本项目希望重点针对插件管理部分,重构工具插件管理模块,完善框架缺陷,并通过缓存等方式确保可用性。其次,调研同类工具方案与 TUI 等相关技术,优化信息展示能力、用户交互方式,提升工具整体交互体验。 **相关链接** - https://github.com/nonebot/nb-cli/issues/138 - https://github.com/nonebot/nb-cli/issues/140 **难度**:基础 **导师**:[@yanyongyu](https://github.com/yanyongyu) **产出要求** - 重构 NB-CLI 插件管理模块 - 优化项目插件信息存储方式,支持列出、卸载插件等操作 - 通过缓存 registry 数据等方式确保离线场景的可用性 - 提升 NB-CLI 交互体验 - 调研同类工具方案与 TUI 等相关技术 - 优化 registry 多字段信息展示能力 - 基于 TUI 等技术优化用户交互方式,提升整体交互体验 **技术要求** - 熟练掌握 Python 及其异步编程 - 熟悉 NoneBot 框架与 NB-CLI 使用方法 - 了解 TUI 等终端交互技术 **成果仓库** - ================================================ FILE: website/versioned_docs/version-2.4.4/quick-start.mdx ================================================ --- sidebar_position: 1 description: 尝试使用 NoneBot options: menu: - category: tutorial weight: 10 --- import Asciinema from "@site/src/components/Asciinema"; import Messenger from "@site/src/components/Messenger"; # 快速上手 :::caution 前提条件 - 请确保你的 Python 版本 >= 3.9 - **我们强烈建议使用虚拟环境进行开发**,如果没有使用虚拟环境,请确保已经卸载可能存在的 NoneBot v1!!! ```bash pip uninstall nonebot ``` ::: 在本章节中,我们将介绍如何使用脚手架来创建一个 NoneBot 简易项目。项目将基于 nb-cli 脚手架运行,并允许我们从商店安装插件。 ## 安装脚手架 确保你已经安装了 Python 3.9 及以上版本,然后在命令行中执行以下命令: 1. 安装 [pipx](https://pypa.github.io/pipx/) ```bash python -m pip install --user pipx python -m pipx ensurepath ``` 如果在此步骤的输出中出现了“open a new terminal”或者“re-login”字样,那么请关闭当前终端并重新打开一个新的终端。 2. 安装脚手架 ```bash pipx install nb-cli ``` 安装完成后,你可以在命令行使用 `nb` 命令来使用脚手架。如果出现无法找到命令的情况(例如出现“Command not found”字样),请参考 [pipx 文档](https://pypa.github.io/pipx/) 检查你的环境变量。 ## 创建项目 使用脚手架来创建一个项目: ```bash nb create ``` 这一指令将会执行创建项目的流程,你将会看到一些询问: 1. 项目模板 ```bash [?] 选择一个要使用的模板: bootstrap (初学者或用户) ``` 这里我们选择 `bootstrap` 模板,它是一个简单的项目模板,能够安装商店插件。如果你需要**自行编写插件**,这里请选择 `simple` 模板。 2. 项目名称 ```bash [?] 项目名称: awesome-bot ``` 这里我们以 `awesome-bot` 为例,作为项目名称。你可以根据自己的需要来命名。 3. 其他选项 请注意,多选项使用**空格**选中或取消,**回车**确认。 ```bash [?] 要使用哪些适配器? Console (基于终端的交互式适配器) [?] 要使用哪些驱动器? FastAPI (FastAPI 驱动器) [?] 要使用什么本地存储策略? 用户全局 (默认,适用于单用户下单实例) [?] 立即安装依赖? (Y/n) Yes [?] 创建虚拟环境? (Y/n) Yes ``` 这里我们选择了创建虚拟环境,nb-cli 在之后的操作中将会自动使用这个虚拟环境。如果你不需要自动创建虚拟环境或者已经创建了其他虚拟环境,nb-cli 将会安装依赖至当前激活的 Python 虚拟环境。 4. 选择内置插件 ```bash [?] 要使用哪些内置插件? echo ``` 这里我们选择 `echo` 插件作为示例。这是一个简单的复读回显插件,可以用于测试你的机器人是否正常运行。 ## 运行项目 在项目创建完成后,你可以在**项目目录**中使用以下命令来运行项目: ```bash nb run ``` 你现在应该已经运行起来了你的第一个 NoneBot 项目了!请注意,生成的项目中使用了 `FastAPI` 驱动器和 `Console` 适配器,你之后可以自行修改配置或安装其他适配器。 ## 尝试使用 在项目运行起来后,`Console` 适配器会在你的终端启动交互模式,你可以直接在输入框中输入 `/echo hello world` 来测试你的机器人是否正常运行。 ================================================ FILE: website/versioned_docs/version-2.4.4/tutorial/application.mdx ================================================ --- sidebar_position: 0 description: 创建一个 NoneBot 项目 options: menu: - category: tutorial weight: 20 --- # 手动创建项目 import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; 在[快速上手](../quick-start.mdx)中,我们已经介绍了如何安装和使用 `nb-cli` 创建一个项目。在本章节中,我们将简要介绍如何在不使用 `nb-cli` 的方式创建一个机器人项目的**最小实例**并启动。如果你想要了解 NoneBot 的启动流程,也可以阅读本章节。 :::caution 警告 我们十分不推荐直接创建机器人项目,请优先考虑使用 nb-cli 进行项目创建。 ::: 一个机器人项目的**最小实例**中**至少**需要包含以下内容: - 入口文件:初始化并运行机器人的 Python 文件 - 配置文件:存储机器人启动所需的配置 - 插件:为机器人提供具体的功能 下面我们创建一个项目文件夹,来存放项目所需文件,以下步骤均在该文件夹中进行。 ## 安装依赖 在创建项目前,我们首先需要将项目所需依赖安装至环境中。 1. (可选)创建虚拟环境,以 venv 为例 ```bash # 创建虚拟环境 python -m venv .venv --prompt nonebot2 # 激活虚拟环境 .venv\Scripts\activate ``` ```bash # 创建虚拟环境 python -m venv .venv --prompt nonebot2 # 激活虚拟环境 source .venv/bin/activate ``` 2. 安装 nonebot2 以及驱动器,以 Fastapi 驱动器为例 ```bash pip install "nonebot2[fastapi]" ``` ```bash pip install "nonebot2[fastapi]" ``` 驱动器包名可以在 [驱动器商店](/store/drivers) 中找到,请替换上文方括号中的内容。 3. 安装适配器,以 Console 适配器为例 ```bash pip install nonebot-adapter-console ``` 适配器包名可以在 [适配器商店](/store/adapters) 中找到。 ## 创建配置文件 配置文件用于存放 NoneBot 运行所需要的配置项,使用 [`pydantic`](https://docs.pydantic.dev/) 以及 [`python-dotenv`](https://saurabh-kumar.com/python-dotenv/) 来读取配置。配置项需符合 dotenv 格式,复杂类型数据需使用 JSON 格式填写。具体可选配置方式以及配置项详情参考[配置](../appendices/config.mdx)。 在**项目文件夹**中创建一个名为 `.env` 的文件,并写入以下内容: ```bash title=.env HOST=0.0.0.0 # 配置 NoneBot 监听的 IP / 主机名 PORT=8080 # 配置 NoneBot 监听的端口 COMMAND_START=["/"] # 配置命令起始字符 COMMAND_SEP=["."] # 配置命令分割字符 ``` ## 创建入口文件 入口文件( Entrypoint )顾名思义,是用来初始化并运行机器人的 Python 文件。入口文件需要完成框架的初始化、注册适配器、加载插件等工作。 :::tip 提示 如果你使用 `nb-cli` 创建项目,入口文件不会被创建,该文件功能会被 `nb run` 命令代替。 ::: 在**项目文件夹**中创建一个 `bot.py` 文件,并写入以下内容: ```python title=bot.py import nonebot from nonebot.adapters.console import Adapter as ConsoleAdapter # 避免重复命名 # 初始化 NoneBot nonebot.init() # 注册适配器 driver = nonebot.get_driver() driver.register_adapter(ConsoleAdapter) # 在这里加载插件 nonebot.load_builtin_plugins("echo") # 内置插件 # nonebot.load_plugin("thirdparty_plugin") # 第三方插件 # nonebot.load_plugins("awesome_bot/plugins") # 本地插件 if __name__ == "__main__": nonebot.run() ``` 我们暂时不需要了解其中内容的含义,这些将会在稍后的章节中逐一介绍。在创建完成以上文件并确认已安装所需适配器和插件后,即可运行机器人。 ## 运行机器人 在**项目文件夹**中,使用配置好环境的 Python 解释器运行入口文件: ```bash # 激活虚拟环境(未使用虚拟环境时跳过此行) .venv\Scripts\activate # 运行机器人 python bot.py ``` ```bash # 激活虚拟环境(未使用虚拟环境时跳过此行) source .venv/bin/activate # 运行机器人 python bot.py ``` 如果你后续使用了 `nb-cli` ,你仍可以使用 `nb run` 命令来运行机器人,`nb-cli` 会自动检测入口文件 `bot.py` 是否存在并运行。同时,你也可以使用 `nb run --reload` 来自动检测代码的更改并自动重新运行入口文件。 ================================================ FILE: website/versioned_docs/version-2.4.4/tutorial/create-plugin.md ================================================ --- sidebar_position: 3 description: 创建并加载自定义插件 options: menu: - category: tutorial weight: 50 --- # 插件编写准备 在正式编写插件之前,我们需要先了解一下插件的概念。 ## 插件结构 在 NoneBot 中,插件即是 Python 的一个[模块(module)](https://docs.python.org/zh-cn/3/glossary.html#term-module)。NoneBot 会在导入时对这些模块做一些特殊的处理使得他们成为一个插件。插件间应尽量减少耦合,可以进行有限制的相互调用,NoneBot 能够正确解析插件间的依赖关系。 ### 单文件插件 一个普通的 `.py` 文件即可以作为一个插件,例如创建一个 `foo.py` 文件: ```tree title=Project 📂 plugins └── 📜 foo.py ``` 这个时候模块 `foo` 已经可以被称为一个插件了,尽管它还什么都没做。 ### 包插件 一个包含 `__init__.py` 的文件夹即是一个常规 Python [包 `package`](https://docs.python.org/zh-cn/3/glossary.html#term-regular-package),例如创建一个 `foo` 文件夹: ```tree title=Project 📂 plugins └── 📂 foo └── 📜 __init__.py ``` 这个时候包 `foo` 同样是一个合法的插件,插件内容可以在 `__init__.py` 文件中编写。 ## 创建插件 :::caution 注意 如果在之前的[快速上手](../quick-start.mdx)章节中已经使用 `bootstrap` 模板创建了项目,那么你需要做出如下修改: 1. 在项目目录中创建一个两层文件夹 `awesome_bot/plugins` ```tree title=Project 📦 awesome-bot ├── 📂 .venv ├── 📂 awesome_bot │ └── 📂 plugins ├── 📜 .env.prod ├── 📜 pyproject.toml └── 📜 README.md ``` 2. 修改 `pyproject.toml` 文件中的 `nonebot` 配置项,在 `plugin_dirs` 中添加 `awesome_bot/plugins` ```toml title=pyproject.toml [tool.nonebot] plugin_dirs = ["awesome_bot/plugins"] ``` ::: :::caution 注意 如果在之前的[创建项目](./application.mdx)章节中手动创建了相关文件,那么你需要做出如下修改: 1. 在项目目录中创建一个两层文件夹 `awesome_bot/plugins` ```tree title=Project 📦 awesome-bot ├── 📂 awesome_bot │ └── 📂 plugins └── 📜 bot.py ``` 2. 修改 `bot.py` 文件中的加载插件部分,取消注释或者添加如下代码 ```python title=bot.py # 在这里加载插件 nonebot.load_builtin_plugins("echo") # 内置插件 nonebot.load_plugins("awesome_bot/plugins") # 本地插件 ``` ::: 创建插件可以通过 `nb-cli` 命令从完整模板创建,也可以手动新建空白文件。通过以下命令创建一个名为 `weather` 的插件: ```bash $ nb plugin create [?] 插件名称: weather [?] 使用嵌套插件? (y/N) N [?] 请输入插件存储位置: awesome_bot/plugins ``` `nb-cli` 会在 `awesome_bot/plugins` 目录下创建一个名为 `weather` 的文件夹,其中包含的文件将在稍后章节中用到。 ```tree title=Project 📦 awesome-bot ├── 📂 .venv ├── 📂 awesome_bot │ └── 📂 plugins | └── 📂 weather | ├── 📜 __init__.py | └── 📜 config.py ├── 📜 .env.prod ├── 📜 pyproject.toml └── 📜 README.md ``` ## 加载插件 :::danger 警告 请勿在插件被加载前 `import` 插件模块,这会导致 NoneBot 无法将其转换为插件而出现意料之外的情况。 ::: 加载插件是在机器人入口文件中完成的,需要在框架初始化之后,运行之前进行。 请注意,加载的插件模块名称(插件文件名或文件夹名)**不能相同**,且每一个插件**只能被加载一次**,重复加载将会导致异常。 如果你使用 `nb-cli` 管理插件,那么你可以跳过这一节,`nb-cli` 将会自动处理加载。 如果你**使用自定义的入口文件** `bot.py`,那么你需要在 `bot.py` 中加载插件。 ```python {5} title=bot.py import nonebot nonebot.init() # 加载插件 nonebot.run() ``` 加载插件的方式有多种,但在底层的加载逻辑是一致的。以下是为加载插件提供的几种方式: ### `load_plugin` 通过点分割模块名称或使用 [`pathlib`](https://docs.python.org/zh-cn/3/library/pathlib.html) 的 `Path` 对象来加载插件。通常用于加载第三方插件或者项目插件。例如: ```python from pathlib import Path nonebot.load_plugin("path.to.your.plugin") # 加载第三方插件 nonebot.load_plugin(Path("./path/to/your/plugin.py")) # 加载项目插件 ``` :::caution 注意 请注意,本地插件的路径应该为相对机器人 **入口文件(通常为 bot.py)** 可导入的,例如在项目 `plugins` 目录下。 ::: ### `load_plugins` 加载传入插件目录中的所有插件,通常用于加载一系列本地编写的项目插件。例如: ```python nonebot.load_plugins("src/plugins", "path/to/your/plugins") ``` :::caution 注意 请注意,插件目录应该为相对机器人 **入口文件(通常为 bot.py)** 可导入的,例如在项目 `plugins` 目录下。 ::: ### `load_all_plugins` 这种加载方式是以上两种方式的混合,加载所有传入的插件模块名称,以及所有给定目录下的插件。例如: ```python nonebot.load_all_plugins(["path.to.your.plugin"], ["path/to/your/plugins"]) ``` ### `load_from_json` 通过 JSON 文件加载插件,是 [`load_all_plugins`](#load_all_plugins) 的 JSON 变种。通过读取 JSON 文件中的 `plugins` 字段和 `plugin_dirs` 字段进行加载。例如: ```json title=plugin_config.json { "plugins": ["path.to.your.plugin"], "plugin_dirs": ["path/to/your/plugins"] } ``` ```python nonebot.load_from_json("plugin_config.json", encoding="utf-8") ``` :::tip 提示 如果 JSON 配置文件中的字段无法满足你的需求,可以使用 [`load_all_plugins`](#load_all_plugins) 方法自行读取配置来加载插件。 ::: ### `load_from_toml` 通过 TOML 文件加载插件,是 [`load_all_plugins`](#load_all_plugins) 的 TOML 变种。通过读取 TOML 文件中的 `[tool.nonebot]` Table 中的 `plugin_dirs` Array 与 `[tool.nonebot.plugins]` Table 中的多个 Array 进行加载。例如: ```toml title=plugin_config.toml [tool.nonebot] plugin_dirs = ["path/to/your/plugins"] [tool.nonebot.plugins] "@local" = ["path.to.your.plugin"] # 本地插件等非插件商店来源的插件 "nonebot-plugin-someplugin" = ["nonebot_plugin_someplugin"] # 插件商店来源的插件 ``` ```python nonebot.load_from_toml("plugin_config.toml", encoding="utf-8") ``` :::tip 提示 如果 TOML 配置文件中的字段无法满足你的需求,可以使用 [`load_all_plugins`](#load_all_plugins) 方法自行读取配置来加载插件。 ::: ### `load_builtin_plugin` 加载一个内置插件,传入的插件名必须为 NoneBot 内置插件。该方法是 [`load_plugin`](#load_plugin) 的封装。例如: ```python nonebot.load_builtin_plugin("echo") ``` ### `load_builtin_plugins` 加载传入插件列表中的所有内置插件。例如: ```python nonebot.load_builtin_plugins("echo", "single_session") ``` ### 其他加载方式 有关其他插件加载的方式,可参考[跨插件访问](../advanced/requiring.md)和[嵌套插件](../advanced/plugin-nesting.md)。 ================================================ FILE: website/versioned_docs/version-2.4.4/tutorial/event-data.mdx ================================================ --- sidebar_position: 6 description: 通过依赖注入获取所需事件信息 options: menu: - category: tutorial weight: 80 --- # 获取事件信息 import Messenger from "@site/src/components/Messenger"; 在 NoneBot 事件处理流程中,获取事件信息并做出对应的操作是非常常见的场景。本章节中我们将介绍如何通过**依赖注入**获取事件信息。 ## 认识依赖注入 在事件处理流程中,事件响应器具有自己独立的上下文,例如:当前响应的事件、收到事件的机器人或者其他处理流程中新增的信息等。这些数据可以根据我们的需求,通过依赖注入的方式,在执行事件处理流程中注入到事件处理函数中。 相对于传统的信息获取方法,通过依赖注入获取信息的最大特色在于**按需获取**。如果该事件处理函数不需要任何额外信息即可运行,那么可以不进行依赖注入。如果事件处理函数需要额外的数据,可以通过依赖注入的方式灵活的标注出需要的依赖,在函数运行时便会被按需注入。 ## 使用依赖注入 使用依赖注入获取上下文信息的方法十分简单,我们仅需要在函数的参数中声明所需的依赖,并正确的将函数添加为事件处理依赖即可。在 NoneBot 中,我们可以直接使用 `nonebot.params` 模块中定义的参数类型来声明依赖。 例如,我们可以继续改进上一章节中的 `weather` 插件,使其可以获取到 `天气` 命令的地名参数,并根据地名返回天气信息。 ```python {9,11} title=weather/__init__.py from nonebot import on_command from nonebot.rule import to_me from nonebot.adapters import Message from nonebot.params import CommandArg weather = on_command("天气", rule=to_me(), aliases={"weather", "查天气"}, priority=10, block=True) @weather.handle() async def handle_function(args: Message = CommandArg()): # 提取参数纯文本作为地名,并判断是否有效 if location := args.extract_plain_text(): await weather.finish(f"今天{location}的天气是...") else: await weather.finish("请输入地名") ``` 如上方示例所示,我们使用了 `args` 作为注入参数名,注入的内容为 `CommandArg()`,也就是**消息命令后跟随的内容**。在这个示例中,我们获得的参数会被检查是否有效,对无效参数则会结束事件。 :::tip 提示 命令与参数之间可以不需要空格,`CommandArg()` 获取的信息为命令后跟随的内容并去除了头部空白符。例如:`/天气 上海` 消息的参数为 `上海`。 ::: :::tip 提示 `:=` 是 Python 3.8 引入的新语法 [Assignment Expressions](https://docs.python.org/zh-cn/3/reference/expressions.html#assignment-expressions),也称为海象表达式,可以在表达式中直接赋值。 ::: NoneBot 提供了多种依赖注入类型,可以获取不同的信息,具体内容可参考[依赖注入](../advanced/dependency.mdx)。 ================================================ FILE: website/versioned_docs/version-2.4.4/tutorial/fundamentals.md ================================================ --- sidebar_position: 1 description: NoneBot 机器人构成及基本使用 options: menu: - category: tutorial weight: 30 --- # 机器人的构成 了解机器人的基本构成有助于你更好地使用 NoneBot,本章节将介绍 NoneBot 中的基本组成部分,稍后的文档中将会使用到这些概念。 使用 NoneBot 框架搭建的机器人具有以下几个基本组成部分: 1. NoneBot 机器人框架主体:负责连接各个组成部分,提供基本的机器人功能 2. 驱动器 `Driver`:客户端/服务端的功能实现,负责接收和发送消息(通常为 HTTP 通信) 3. 适配器 `Adapter`:驱动器的上层,负责将**平台消息**与 NoneBot 事件/操作系统的消息格式相互转换 4. 插件 `Plugin`:机器人的功能实现,通常为负责处理事件并进行一系列的操作 除 NoneBot 机器人框架主体外,其他部分均可按需选择、互相搭配,但由于平台的兼容性问题,部分插件可能仅在某些特定平台上可用(这由插件编写者决定)。 在接下来的章节中,我们将重点介绍机器人功能实现,即插件 `Plugin` 部分。 ================================================ FILE: website/versioned_docs/version-2.4.4/tutorial/handler.mdx ================================================ --- sidebar_position: 5 description: 处理接收到的特定事件 options: menu: - category: tutorial weight: 70 --- # 事件处理 import Messenger from "@site/src/components/Messenger"; 在我们收到事件,并被某个事件响应器正确响应后,便正式开启了对于这个事件的**处理流程**。 ## 认识事件处理流程 就像我们在解决问题时需要遵循流程一样,处理一个事件也需要一套流程。在事件响应器对一个事件进行响应之后,会依次执行一系列的**事件处理依赖**(通常是函数)。简单来说,事件处理流程并不是一个函数、一个对象或一个方法,而是一整套由开发者设计的流程。 在这个流程中,我们**目前**只需要了解两个概念:函数形式的“事件处理依赖”(下称“事件处理函数”)和“事件响应器操作”。 ## 事件处理函数 在事件响应器中,事件处理流程可以由一个或多个“事件处理函数”组成,这些事件处理函数将会按照顺序依次对事件进行处理,直到全部执行完成或被中断。我们可以采用事件响应器的“事件处理函数装饰器”来添加这些“事件处理函数”。 顾名思义,“事件处理函数装饰器”是一个[装饰器(decorator)](https://docs.python.org/zh-cn/3/glossary.html#term-decorator),那么它的使用方法也同[函数定义](https://docs.python.org/zh-cn/3/reference/compound_stmts.html#function-definitions)中所展示的包装用法相同。例如: ```python {6-8} title=weather/__init__.py from nonebot import on_command from nonebot.rule import to_me weather = on_command("天气", rule=to_me(), aliases={"weather", "查天气"}, priority=10, block=True) @weather.handle() async def handle_function(): pass # do something here ``` 如上方示例所示,我们使用 `weather` 响应器的 `handle` 装饰器装饰了一个函数 `handle_function`。`handle_function` 函数会被添加到 `weather` 的事件处理流程中。在 `weather` 响应器被触发之后,将会依次调用 `weather` 响应器的事件处理函数,即 `handle_function` 来对事件进行处理。 ## 事件响应器操作 在事件处理流程中,我们可以使用事件响应器操作来进行一些交互或改变事件处理流程,例如向机器人用户发送消息或提前结束事件处理流程等。 事件响应器操作与事件处理函数装饰器类似,通常作为事件响应器 `Matcher` 的[类方法](https://docs.python.org/zh-cn/3/library/functions.html#classmethod)存在,因此事件响应器操作的调用方法也是 `Matcher.func()` 的形式。不过不同的是,事件响应器操作并不是装饰器,因此并不需要@进行标注。 ```python {8,9} title=weather/__init__.py from nonebot import on_command from nonebot.rule import to_me weather = on_command("天气", rule=to_me(), aliases={"weather", "查天气"}, priority=10, block=True) @weather.handle() async def handle_function(): # await weather.send("天气是...") await weather.finish("天气是...") ``` 如上方示例所示,我们使用 `weather` 响应器的 `finish` 操作方法向机器人用户回复了 `天气是...` 并结束了事件处理流程。效果如下: 值得注意的是,在执行 `finish` 方法时,NoneBot 会在向机器人用户发送消息内容后抛出 `FinishedException` 异常来结束事件响应流程。也就是说,在 `finish` 被执行后,后续的程序是不会被执行的。如果你需要回复机器人用户消息但不想事件处理流程结束,可以使用注释的部分中展示的 `send` 方法。 :::danger 警告 由于 `finish` 是通过抛出 `FinishedException` 异常来结束事件的,因此异常可能会被未加限制的 `try-except` 捕获,影响事件处理流程正确处理,导致无法正常结束此事件。请务必在异常捕获中指定错误类型或排除所有 [MatcherException](../api/exception.md#MatcherException) 类型的异常(如下所示),或将 `finish` 移出捕获范围进行使用。 ```python from nonebot.exception import MatcherException try: await weather.finish("天气是...") except MatcherException: raise except Exception as e: pass # do something here ``` ::: 目前 NoneBot 提供了多种事件响应器操作,其中包括用于机器人用户交互与流程控制两大类,进阶使用方法可以查看[会话控制](../appendices/session-control.mdx)。 ================================================ FILE: website/versioned_docs/version-2.4.4/tutorial/matcher.md ================================================ --- sidebar_position: 4 description: 响应接收到的特定事件 options: menu: - category: tutorial weight: 60 --- # 事件响应器 事件响应器(Matcher)是对接收到的事件进行响应的基本单元,所有的事件响应器都继承自 `Matcher` 基类。 在 NoneBot 中,事件响应器可以通过一系列特定的规则**筛选**出**具有某种特征的事件**,并按照**特定的流程**交由**预定义的事件处理依赖**进行处理。例如,在[快速上手](../quick-start.mdx)中,我们使用了内置插件 `echo` ,它定义的事件响应器能响应机器人用户发送的“/echo hello world”消息,提取“hello world”信息并作为回复消息发送。 ## 事件响应器辅助函数 NoneBot 中所有事件响应器均继承自 `Matcher` 基类,但直接使用 `Matcher.new()` 方法创建事件响应器过于繁琐且不能记录插件信息。因此,NoneBot 中提供了一系列“事件响应器辅助函数”(下称“辅助函数”)来辅助我们用**最简的方式**创建**带有不同规则预设**的事件响应器,提高代码可读性和书写效率。通常情况下,我们只需要使用辅助函数即可完成事件响应器的创建。 在 NoneBot 中,辅助函数以 `on()` 或 `on_()` 形式出现(例如 `on_command()`),调用后根据不同的参数返回一个 `Type[Matcher]` 类型的新事件响应器。 目前 NoneBot 提供了多种功能各异的辅助函数、具有共同命令名称前缀的命令组以及具有共同参数的响应器组,均可以从 `nonebot` 模块直接导入使用,具体内容参考[事件响应器进阶](../advanced/matcher.md)。 ## 创建事件响应器 在上一节[创建插件](./create-plugin.md#创建插件)中,我们创建了一个 `weather` 插件,现在我们来实现他的功能。 我们直接使用 `on_command()` 辅助函数来创建一个事件响应器: ```python {3} title=weather/__init__.py from nonebot import on_command weather = on_command("天气") ``` 这样,我们就获得一个名为 `weather` 的事件响应器了,这个事件响应器会对 `/天气` 开头的消息进行响应。 :::tip 提示 如果一条消息中包含“@机器人”或以“机器人的昵称”开始,例如 `@bot /天气` 时,协议适配器会将 `event.is_tome()` 判断为 `True` ,同时也会自动去除 `@bot`,即事件响应器收到的信息内容为 `/天气`,方便进行命令匹配。 ::: ### 为事件响应器添加参数 在辅助函数中,我们可以添加一些参数来对事件响应器进行更加精细的调整,例如事件响应器的优先级、匹配规则等。例如: ```python {4} title=weather/__init__.py from nonebot import on_command from nonebot.rule import to_me weather = on_command("天气", rule=to_me(), aliases={"weather", "查天气"}, priority=10, block=True) ``` 这样,我们就获得了一个可以响应 `天气`、`weather`、`查天气` 三个命令的响应规则,需要私聊或 `@bot` 时才会响应,优先级为 10(越小越优先),阻断事件向后续优先级传播的事件响应器了。这些内容的意义和使用方法将会在后续的章节中一一介绍。 :::tip 提示 需要注意的是,不同的辅助函数有不同的可选参数,在使用之前可以参考[事件响应器进阶 - 基本辅助函数](../advanced/matcher.md#基本辅助函数)或 [API 文档](../api/plugin/on.md#on)。 ::: ================================================ FILE: website/versioned_docs/version-2.4.4/tutorial/message.md ================================================ --- sidebar_position: 7 description: 处理消息序列与消息段 options: menu: - category: tutorial weight: 90 --- # 处理消息 在不同平台中,一条消息可能会有承载有各种不同的表现形式,它可能是一段纯文本、一张图片、一段语音、一篇富文本文章,也有可能是多种类型的组合等等。 在 NoneBot 中,为确保消息的正常处理与跨平台兼容性,采用了扁平化的消息序列形式,即 `Message` 对象。消息序列是 NoneBot 中的消息载体,无论是接收还是发送的消息,都采用消息序列的形式进行处理。 ## 认识消息类型 ### 消息序列 `Message` 在 NoneBot 中,消息序列 `Message` 的主要作用是用于表达“一串消息”。由于消息序列继承自 `List[MessageSegment]`,所以 `Message` 的本质是由若干消息段所组成的序列。因此,消息序列的使用方法与 `List` 有很多相似之处,例如切片、索引、拼接等。 在上一节的[使用依赖注入](./event-data.mdx#使用依赖注入)中,我们已经通过依赖注入 `CommandArg()` 获取了命令的参数,它的类型即是消息序列。我们使用了消息序列的 `extract_plain_text()` 方法来获取消息序列中的纯文本内容。 ### 消息段 `MessageSegment` 顾名思义,消息段 `MessageSegment` 是一段消息。由于消息序列的本质是由若干消息段所组成的序列,消息段可以被认为是构成消息序列的最小单位。简单来说,消息序列类似于一个自然段,而消息段则是组成自然段的一句话。同时,作为特殊消息载体的存在,绝大多数的平台都有着**独特的消息类型**,这些独特的内容均需要由对应的**协议适配器**所提供,以适应不同平台中的消息模式。**这也意味着,你需要导入对应的协议适配器中的消息序列和消息段后才能使用其特殊的工厂方法。** :::caution 注意 消息段的类型是由协议适配器提供的,因此你需要参考协议适配器的文档并导入对应的消息段后才能使用其特殊的消息类型。 在上一节的[使用依赖注入](./event-data.mdx#使用依赖注入)中,我们导入的为 `nonebot.adapters.Message` 抽象基类,因此我们无法使用平台特有的消息类型。仅能使用 `str` 作为纯文本消息回复。 ::: ## 使用消息序列 :::caution 注意 在以下的示例中,为了更好的理解多种类型的消息组成方式,我们将使用 `Console` 协议适配器来演示消息序列的使用方法。在实际使用中,你需要确保你使用的**消息序列类型**与你所要发送的**平台类型**一致。 ::: 通常情况下,适配器在接收到消息时,会将消息转换为消息序列,可以通过依赖注入 [`EventMessage`](../advanced/dependency.mdx#eventmessage),或者使用 `event.get_message()` 获取。 由于消息序列是 `List[MessageSegment]` 的子类,所以你总是可以用和操作 `List` 类似的方式来处理消息序列。例如: ```python >>> from nonebot.adapters.console import Message, MessageSegment >>> message = Message([ MessageSegment(type="text", data={"text":"hello"}), MessageSegment(type="markdown", data={"markup":"**world**"}), ]) >>> for segment in message: ... print(segment.type, segment.data) ... text {'text': 'hello'} markdown {'markup': '**world**'} >>> len(message) 2 ``` ### 构造消息序列 在使用事件响应器操作发送消息时,既可以使用 `str` 作为消息,也可以使用 `Message`、`MessageSegment` 或者 `MessageTemplate`。那么,我们就需要先构造一个消息序列。消息序列可以通过多种方式构造: #### 直接构造 `Message` 类可以直接实例化,支持 `str`、`MessageSegment`、`Iterable[MessageSegment]` 或适配器自定义类型的参数。 ```python from nonebot.adapters.console import Message, MessageSegment # str Message("Hello, world!") # MessageSegment Message(MessageSegment.text("Hello, world!")) # List[MessageSegment] Message([MessageSegment.text("Hello, world!")]) ``` #### 运算构造 `Message` 对象可以通过 `str`、`MessageSegment` 相加构造,详情请参考[拼接消息](#拼接消息)。 #### 从字典数组构造 `Message` 对象支持 Pydantic 自定义类型构造,可以使用 Pydantic 的 `TypeAdapter` 方法进行构造。 ```python from pydantic import TypeAdapter from nonebot.adapters.console import Message, MessageSegment # 由字典构造消息段 TypeAdapter(MessageSegment).validate_python( {"type": "text", "data": {"text": "text"}} ) == MessageSegment.text("text") # 由字典数组构造消息序列 TypeAdapter(Message).validate_python( [MessageSegment.text("text"), {"type": "text", "data": {"text": "text"}}], ) == Message([MessageSegment.text("text"), MessageSegment.text("text")]) ``` ### 获取消息纯文本 由于消息中存在各种类型的消息段,因此 `str(message)` 通常**不能得到消息的纯文本**,而是一个消息序列的字符串表示。 NoneBot 为消息段定义了一个方法 `is_text()` ,可以用于判断消息段是否为纯文本;也可以使用 `message.extract_plain_text()` 方法获取消息纯文本。 ```python from nonebot.adapters.console import Message, MessageSegment # 判断消息段是否为纯文本 MessageSegment.text("text").is_text() == True # 提取消息纯文本字符串 Message( [MessageSegment.text("text"), MessageSegment.markdown("**markup**")] ).extract_plain_text() == "text" ``` ### 遍历 消息序列继承自 `List[MessageSegment]` ,因此可以使用 `for` 循环遍历消息段。 ```python for segment in message: ... ``` ### 比较 消息和消息段都可以使用 `==` 或 `!=` 运算符比较是否相同。 ```python MessageSegment.text("text") != MessageSegment.text("foo") some_message == Message([MessageSegment.text("text")]) ``` ### 检查消息段 我们可以通过 `in` 运算符或消息序列的 `has` 方法来: ```python # 是否存在消息段 MessageSegment.text("text") in message # 是否存在指定类型的消息段 "text" in message ``` 我们还可以使用消息序列的 `only` 方法来检查消息中是否仅包含指定的消息段。 ```python # 是否都为指定消息段 message.only(MessageSegment.text("test")) # 是否仅包含指定类型的消息段 message.only("text") ``` ### 过滤、索引与切片 消息序列对列表的索引与切片进行了增强,在原有列表 `int` 索引与 `slice` 切片的基础上,支持 `type` 过滤索引与切片。 ```python from nonebot.adapters.console import Message, MessageSegment message = Message( [ MessageSegment.text("test"), MessageSegment.markdown("test2"), MessageSegment.markdown("test3"), MessageSegment.text("test4"), ] ) # 索引 message[0] == MessageSegment.text("test") # 切片 message[0:2] == Message( [MessageSegment.text("test"), MessageSegment.markdown("test2")] ) # 类型过滤 message["markdown"] == Message( [MessageSegment.markdown("test2"), MessageSegment.markdown("test3")] ) # 类型索引 message["markdown", 0] == MessageSegment.markdown("test2") # 类型切片 message["markdown", 0:2] == Message( [MessageSegment.markdown("test2"), MessageSegment.markdown("test3")] ) ``` 我们也可以通过消息序列的 `include`、`exclude` 方法进行类型过滤。 ```python message.include("text", "markdown") message.exclude("text") ``` 同样的,消息序列对列表的 `index`、`count` 方法也进行了增强,可以用于索引指定类型的消息段。 ```python # 指定类型首个消息段索引 message.index("markdown") == 1 # 指定类型消息段数量 message.count("markdown") == 2 ``` 此外,消息序列添加了一个 `get` 方法,可以用于获取指定类型指定个数的消息段。 ```python # 获取指定类型指定个数的消息段 message.get("markdown", 1) == Message([MessageSegment.markdown("test2")]) ``` ### 拼接消息 `str`、`Message`、`MessageSegment` 对象之间可以直接相加,相加均会返回一个新的 `Message` 对象。 ```python # 消息序列与消息段相加 Message([MessageSegment.text("text")]) + MessageSegment.text("text") # 消息序列与字符串相加 Message([MessageSegment.text("text")]) + "text" # 消息序列与消息序列相加 Message([MessageSegment.text("text")]) + Message([MessageSegment.text("text")]) # 字符串与消息序列相加 "text" + Message([MessageSegment.text("text")]) # 消息段与消息段相加 MessageSegment.text("text") + MessageSegment.text("text") # 消息段与字符串相加 MessageSegment.text("text") + "text" # 消息段与消息序列相加 MessageSegment.text("text") + Message([MessageSegment.text("text")]) # 字符串与消息段相加 "text" + MessageSegment.text("text") ``` 如果需要在当前消息序列后直接拼接新的消息段,可以使用 `Message.append`、`Message.extend` 方法,或者使用自加。 ```python msg = Message([MessageSegment.text("text")]) # 自加 msg += "text" msg += MessageSegment.text("text") msg += Message([MessageSegment.text("text")]) # 附加 msg.append("text") msg.append(MessageSegment.text("text")) # 扩展 msg.extend([MessageSegment.text("text")]) ``` 我们也可以通过消息段或消息序列的 `join` 方法来拼接一串消息: ```python seg = MessageSegment.text("text") msg = seg.join( [ MessageSegment.text("first"), Message( [ MessageSegment.text("second"), MessageSegment.text("third"), ] ) ] ) msg == Message( [ MessageSegment.text("first"), MessageSegment.text("text"), MessageSegment.text("second"), MessageSegment.text("third"), ] ) ``` ### 使用消息模板 为了提供安全可靠的跨平台模板字符,我们提供了一个消息模板功能来构建消息序列 它在以下常见场景中尤其有用: - 多行富文本编排(包含图片,文字以及表情等) - 客制化(由 Bot 最终用户提供消息模板时) 在事实上,它的用法和 `str.format` 极为相近,所以你在使用的时候,总是可以参考[Python 文档](https://docs.python.org/zh-cn/3/library/stdtypes.html#str.format)来达到你想要的效果,这里给出几个简单的例子。 默认情况下,消息模板采用 `str` 纯文本形式的格式化: ```python title=基础格式化用法 >>> from nonebot.adapters import MessageTemplate >>> MessageTemplate("{} {}").format("hello", "world") 'hello world' ``` 如果 `Message.template` 构建消息模板,那么消息模板将采用消息序列形式的格式化,此时的消息将会是平台特定的: :::caution 注意 使用 `Message.template` 构建消息模板时,应注意消息序列为平台适配器提供的类型,不能使用 `nonebot.adapters.Message` 基类作为模板构建。使用基类构建模板与使用 `str` 构建模板的效果是一样的,因此请使用上述的 `MessageTemplate` 类直接构建模板。: ::: ```python title=平台格式化用法 >>> from nonebot.adapters.console import Message, MessageSegment >>> Message.template("{} {}").format("hello", "world") Message( MessageSegment.text("hello"), MessageSegment.text(" "), MessageSegment.text("world") ) ``` 消息模板支持使用消息段进行格式化: ```python title=对消息段进行安全的拼接 >>> from nonebot.adapters.console import Message, MessageSegment >>> Message.template("{}{}").format(MessageSegment.markdown("**markup**"), "world") Message( MessageSegment(type='markdown', data={'markup': '**markup**'}), MessageSegment(type='text', data={'text': 'world'}) ) ``` 消息模板同样支持使用消息序列作为模板: ```python title=以消息对象作为模板 >>> from nonebot.adapters.console import Message, MessageSegment >>> Message.template( ... MessageSegment.text("{user_id}") + MessageSegment.emoji("tada") + ... MessageSegment.text("{message}") ... ).format_map({"user_id": 123456, "message": "hello world"}) Message( MessageSegment(type='text', data={'text': '123456'}), MessageSegment(type='emoji', data={'emoji': 'tada'}), MessageSegment(type='text', data={'text': 'hello world'}) ) ``` :::caution 注意 只有消息序列中的文本类型消息段才能被格式化,其他类型的消息段将会原样添加。 ::: 消息模板支持使用拓展控制符来控制消息段类型: ```python title=使用消息段的拓展控制符 >>> from nonebot.adapters.console import Message, MessageSegment >>> Message.template("{name:emoji}").format(name='tada') Message(MessageSegment(type='emoji', data={'name': 'tada'})) ``` ================================================ FILE: website/versioned_docs/version-2.4.4/tutorial/store.mdx ================================================ --- sidebar_position: 2 description: 从商店安装适配器和插件 options: menu: - category: tutorial weight: 40 --- # 获取商店内容 import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; import Asciinema from "@site/src/components/Asciinema"; :::tip 提示 如果你暂时没有获取商店内容的需求,可以跳过本章节。 ::: NoneBot 提供了一个[商店](/store/plugins),商店内容均由社区开发者贡献。你可以在商店中查找你需要的适配器和插件等,进行安装或者参考其文档等。 商店中每个内容的卡片都包含了其名称和简介等信息,点击**卡片右上角**链接图标即可跳转到其主页。 与此同时,NB-CLI 也提供了一个 TUI 版本的商店界面,可通过 `nb adapter store`、`nb plugin store`、`nb driver store` 命令或 CLI 交互式界面进入。其提供了接近网页商店的体验,同时允许快捷安装到当前项目。 ## 安装插件 在商店插件页面中,点击你需要安装的插件下方的 `点击复制安装命令` 按钮,即可复制 `nb-cli` 命令。 请在你的**项目目录**下执行该命令。`nb-cli` 会自动安装插件并将其添加到加载列表中。 ```bash nb plugin install <插件名称> ``` ```bash $ nb plugin install [?] 想要安装的插件名称: <插件名称> ``` ```bash pip install <插件包名> ``` 插件包名可以在商店插件卡片中找到,或者使用 `nb-cli` 搜索插件显示的详情中找到。安装完成后,需要参考[加载插件章节](./create-plugin.md#加载插件)自行加载。 如果想要查看插件列表,可以使用以下命令 ```bash # 列出商店所有插件 nb plugin list # 搜索商店插件 nb plugin search [可选关键词] ``` 升级和卸载插件可以使用以下命令 ```bash nb plugin update <插件名称> nb plugin uninstall <插件名称> ``` ```bash $ nb plugin update [?] 想要安装的插件名称: <插件名称> $ nb plugin uninstall [?] 想要卸载的插件名称: <插件名称> ``` ```bash pip install --upgrade <插件包名> pip uninstall <插件包名> ``` 插件包名可以在商店插件卡片中找到,或者使用 `nb-cli` 搜索插件显示的详情中找到。卸载完成后,需要自行移除插件加载。 ## 安装适配器 安装适配器与安装插件类似,只是将命令换为 `nb adapter`,这里就不再赘述。 请在你的**项目目录**下执行该命令。`nb-cli` 会自动安装适配器并将其添加到注册列表中。 ```bash nb adapter install <适配器名称> ``` ```bash $ nb adapter install [?] 想要安装的适配器名称: <适配器名称> ``` ```bash pip install <适配器包名> ``` 适配器包名可以在商店适配器卡片中找到,或者使用 `nb-cli` 搜索适配器显示的详情中找到。安装完成后,需要参考[注册适配器章节](../advanced/adapter.md#注册适配器)自行注册。 如果想要查看适配器列表,可以使用以下命令 ```bash # 列出商店所有适配器 nb adapter list # 搜索商店适配器 nb adapter search [可选关键词] ``` 升级和卸载适配器可以使用以下命令 ```bash nb adapter update <适配器名称> nb adapter uninstall <适配器名称> ``` ```bash $ nb adapter update [?] 想要安装的适配器名称: <适配器名称> $ nb adapter uninstall [?] 想要卸载的适配器名称: <适配器名称> ``` ```bash pip install --upgrade <适配器包名> pip uninstall <适配器包名> ``` 适配器包名可以在商店适配器卡片中找到,或者使用 `nb-cli` 搜索适配器显示的详情中找到。卸载完成后,需要自行移除适配器加载。 ## 安装驱动器 安装驱动器与安装插件同样类似,只是将命令换为 `nb driver`,这里就不再赘述。 如果你使用了虚拟环境,请在你的**项目目录**下执行该命令,`nb-cli` 会自动安装驱动器到虚拟环境中。 请注意 `nb-cli` 并不会在安装驱动器后修改项目所使用的驱动器,请自行参考[配置方法](../appendices/config.mdx)章节以及 [`DRIVER` 配置项](../appendices/config.mdx#driver)修改驱动器。 ```bash nb driver install <驱动器名称> ``` ```bash $ nb driver install [?] 想要安装的驱动器名称: <驱动器名称> ``` ```bash pip install <驱动器包名> ``` 驱动器包名可以在商店驱动器卡片中找到,或者使用 `nb-cli` 搜索驱动器显示的详情中找到。 如果想要查看驱动器列表,可以使用以下命令 ```bash # 列出商店所有驱动器 nb driver list # 搜索商店驱动器 nb driver search [可选关键词] ``` 升级和卸载驱动器可以使用以下命令 ```bash nb driver update <驱动器名称> nb driver uninstall <驱动器名称> ``` ```bash $ nb driver update [?] 想要安装的驱动器名称: <驱动器名称> $ nb driver uninstall [?] 想要卸载的驱动器名称: <驱动器名称> ``` ```bash pip install --upgrade <驱动器包名> pip uninstall <驱动器包名> ``` 驱动器包名可以在商店驱动器卡片中找到,或者使用 `nb-cli` 搜索驱动器显示的详情中找到。卸载完成后,需要自行移除适配器加载。 ================================================ FILE: website/versioned_sidebars/version-2.4.2-sidebars.json ================================================ { "tutorial": [ { "type": "category", "label": "开始", "collapsible": false, "items": ["index", "quick-start", "editor-support"] }, { "type": "category", "label": "指南", "items": [ { "type": "autogenerated", "dirName": "tutorial" } ] }, { "type": "category", "label": "深入", "items": [ { "type": "autogenerated", "dirName": "appendices" } ] }, { "type": "category", "label": "进阶", "items": [ { "type": "autogenerated", "dirName": "advanced" } ] }, { "type": "category", "label": "最佳实践", "items": [ { "type": "autogenerated", "dirName": "best-practice" } ] }, { "type": "category", "label": "开发者", "items": [ { "type": "autogenerated", "dirName": "developer" } ] } ], "api": [ { "type": "autogenerated", "dirName": "api" } ], "ecosystem": [ { "type": "category", "label": "关于我们", "collapsible": false, "items": [ { "type": "autogenerated", "dirName": "community" } ] }, { "type": "category", "label": "开源之夏", "collapsible": true, "items": [ { "type": "autogenerated", "dirName": "ospp" } ] }, { "type": "category", "label": "社区资源", "collapsible": false, "items": [ { "type": "link", "label": "插件商店", "href": "/store/plugins" }, { "type": "link", "label": "适配器商店", "href": "/store/adapters" }, { "type": "link", "label": "驱动器商店", "href": "/store/drivers" }, { "type": "link", "label": "机器人商店", "href": "/store/bots" }, { "type": "link", "label": "Awesome NoneBot", "href": "https://awesome.nonebot.dev" }, { "type": "link", "label": "论坛", "href": "https://discussions.nonebot.dev" } ] } ], "changelog": [ { "type": "category", "label": "更新日志", "collapsible": false, "items": [ { "type": "link", "label": "v2.4.2", "href": "/changelog/" }, { "type": "link", "label": "v2.1.2", "href": "/changelog/1" }, { "type": "link", "label": "v2.0.0-beta.4", "href": "/changelog/2" }, { "type": "link", "label": "v2.0.0a9", "href": "/changelog/3" } ] } ] } ================================================ FILE: website/versioned_sidebars/version-2.4.3-sidebars.json ================================================ { "tutorial": [ { "type": "category", "label": "开始", "collapsible": false, "items": ["index", "quick-start", "editor-support"] }, { "type": "category", "label": "指南", "items": [ { "type": "autogenerated", "dirName": "tutorial" } ] }, { "type": "category", "label": "深入", "items": [ { "type": "autogenerated", "dirName": "appendices" } ] }, { "type": "category", "label": "进阶", "items": [ { "type": "autogenerated", "dirName": "advanced" } ] }, { "type": "category", "label": "最佳实践", "items": [ { "type": "autogenerated", "dirName": "best-practice" } ] }, { "type": "category", "label": "开发者", "items": [ { "type": "autogenerated", "dirName": "developer" } ] } ], "api": [ { "type": "autogenerated", "dirName": "api" } ], "ecosystem": [ { "type": "category", "label": "关于我们", "collapsible": false, "items": [ { "type": "autogenerated", "dirName": "community" } ] }, { "type": "category", "label": "开源之夏", "collapsible": true, "items": [ { "type": "autogenerated", "dirName": "ospp" } ] }, { "type": "category", "label": "社区资源", "collapsible": false, "items": [ { "type": "link", "label": "插件商店", "href": "/store/plugins" }, { "type": "link", "label": "适配器商店", "href": "/store/adapters" }, { "type": "link", "label": "驱动器商店", "href": "/store/drivers" }, { "type": "link", "label": "机器人商店", "href": "/store/bots" }, { "type": "link", "label": "Awesome NoneBot", "href": "https://awesome.nonebot.dev" }, { "type": "link", "label": "论坛", "href": "https://discussions.nonebot.dev" } ] } ], "changelog": [ { "type": "category", "label": "更新日志", "collapsible": false, "items": [ { "type": "link", "label": "v2.4.3", "href": "/changelog/" }, { "type": "link", "label": "v2.1.3", "href": "/changelog/1" }, { "type": "link", "label": "v2.0.0-beta.5", "href": "/changelog/2" }, { "type": "link", "label": "v2.0.0a10", "href": "/changelog/3" } ] } ] } ================================================ FILE: website/versioned_sidebars/version-2.4.4-sidebars.json ================================================ { "tutorial": [ { "type": "category", "label": "开始", "collapsible": false, "items": ["index", "quick-start", "editor-support"] }, { "type": "category", "label": "指南", "items": [ { "type": "autogenerated", "dirName": "tutorial" } ] }, { "type": "category", "label": "深入", "items": [ { "type": "autogenerated", "dirName": "appendices" } ] }, { "type": "category", "label": "进阶", "items": [ { "type": "autogenerated", "dirName": "advanced" } ] }, { "type": "category", "label": "最佳实践", "items": [ { "type": "autogenerated", "dirName": "best-practice" } ] }, { "type": "category", "label": "开发者", "items": [ { "type": "autogenerated", "dirName": "developer" } ] } ], "api": [ { "type": "autogenerated", "dirName": "api" } ], "ecosystem": [ { "type": "category", "label": "关于我们", "collapsible": false, "items": [ { "type": "autogenerated", "dirName": "community" } ] }, { "type": "category", "label": "开源之夏", "collapsible": true, "items": [ { "type": "autogenerated", "dirName": "ospp" } ] }, { "type": "category", "label": "社区资源", "collapsible": false, "items": [ { "type": "link", "label": "插件商店", "href": "/store/plugins" }, { "type": "link", "label": "适配器商店", "href": "/store/adapters" }, { "type": "link", "label": "驱动器商店", "href": "/store/drivers" }, { "type": "link", "label": "机器人商店", "href": "/store/bots" }, { "type": "link", "label": "Awesome NoneBot", "href": "https://awesome.nonebot.dev" }, { "type": "link", "label": "论坛", "href": "https://discussions.nonebot.dev" } ] } ], "changelog": [ { "type": "category", "label": "更新日志", "collapsible": false, "items": [ { "type": "link", "label": "v2.4.4", "href": "/changelog/" }, { "type": "link", "label": "v2.2.0", "href": "/changelog/1" }, { "type": "link", "label": "v2.0.0-rc.1", "href": "/changelog/2" }, { "type": "link", "label": "v2.0.0a11", "href": "/changelog/3" } ] } ] } ================================================ FILE: website/versions.json ================================================ ["2.4.4", "2.4.3", "2.4.2"]