├── .coveragerc ├── .editorconfig ├── .env.prod ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.yml │ ├── config.yml │ └── feature_request.yml ├── dependabot.yml ├── release-drafter.yml └── workflows │ ├── codecov.yml │ ├── release-draft.yml │ └── release.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .python-version ├── .vscode └── launch.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── nonebot_plugin_treehelp ├── __init__.py ├── config.py └── data_source.py ├── pyproject.toml ├── requirements-dev.lock ├── requirements.lock └── tests ├── __init__.py ├── conftest.py ├── plugins ├── adapters │ ├── __init__.py │ └── plugins │ │ ├── sub1.py │ │ ├── sub2.py │ │ ├── sub3.py │ │ ├── sub4.py │ │ └── sub5.py ├── nested │ ├── __init__.py │ └── plugins │ │ ├── sub1.py │ │ └── sub2.py └── tree │ ├── __init__.py │ └── plugins │ ├── alc.py │ ├── simple.py │ └── sub │ ├── __init__.py │ └── subsub │ └── sub2.py ├── test_adapters.py ├── test_help.py ├── test_ignored.py ├── test_nested.py ├── test_tree.py └── utils.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | exclude_lines = 3 | pragma: no cover 4 | if TYPE_CHECKING: 5 | @(abc\.)?abstractmethod 6 | raise NotImplementedError 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_style = space 8 | indent_size = 2 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.py] 13 | indent_size = 4 14 | 15 | [*.md] 16 | max_line_length = off 17 | insert_final_newline = false 18 | trim_trailing_whitespace = false 19 | -------------------------------------------------------------------------------- /.env.prod: -------------------------------------------------------------------------------- 1 | driver=~none 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: Bug 反馈 2 | title: "Bug: 出现异常" 3 | description: 提交 Bug 反馈以帮助我们改进代码 4 | labels: ["bug"] 5 | body: 6 | - type: textarea 7 | id: describe 8 | attributes: 9 | label: 描述问题 10 | description: 清晰简洁地说明问题是什么 11 | validations: 12 | required: true 13 | 14 | - type: textarea 15 | id: reproduction 16 | attributes: 17 | label: 复现步骤 18 | description: 提供能复现此问题的详细操作步骤 19 | placeholder: | 20 | 1. 首先…… 21 | 2. 然后…… 22 | 3. 发生…… 23 | validations: 24 | required: true 25 | 26 | - type: textarea 27 | id: expected 28 | attributes: 29 | label: 期望的结果 30 | description: 清晰简洁地描述你期望发生的事情 31 | 32 | - type: textarea 33 | id: logs 34 | attributes: 35 | label: 截图或日志 36 | description: 提供有助于诊断问题的任何日志和截图 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 讨论区 4 | url: https://github.com/he0119/nonebot-plugin-treehelp/discussions 5 | about: 使用中若遇到问题,请先在这里求助。 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: 功能建议 2 | title: "Feature: 功能描述" 3 | description: 提出关于项目新功能的想法 4 | labels: ["enhancement"] 5 | body: 6 | - type: textarea 7 | id: problem 8 | attributes: 9 | label: 希望能解决的问题 10 | description: 在使用中遇到什么问题而需要新的功能? 11 | validations: 12 | required: true 13 | 14 | - type: textarea 15 | id: feature 16 | attributes: 17 | label: 描述所需要的功能 18 | description: 请说明需要的功能或解决方法 19 | validations: 20 | required: true 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pip" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | groups: 8 | production-dependencies: 9 | dependency-type: "production" 10 | update-types: 11 | - "patch" 12 | - "minor" 13 | development-dependencies: 14 | dependency-type: "development" 15 | update-types: 16 | - "patch" 17 | - "minor" 18 | - "major" 19 | - package-ecosystem: "github-actions" 20 | directory: "/" 21 | schedule: 22 | interval: "daily" 23 | groups: 24 | actions: 25 | patterns: 26 | - "*" 27 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | template: $CHANGES 2 | name-template: "v$RESOLVED_VERSION" 3 | tag-template: "v$RESOLVED_VERSION" 4 | exclude-labels: 5 | - "dependencies" 6 | - "skip-changelog" 7 | autolabeler: 8 | - label: "bug" 9 | branch: 10 | - '/fix\/.+/' 11 | - label: "change" 12 | branch: 13 | - '/change\/.+/' 14 | - label: "enhancement" 15 | branch: 16 | - '/feature\/.+/' 17 | - '/feat\/.+/' 18 | - '/improve\/.+/' 19 | - label: "ci" 20 | files: 21 | - .github/**/* 22 | - label: "breaking-change" 23 | title: 24 | - "/.+!:.+/" 25 | categories: 26 | - title: 💥 破坏性变更 27 | labels: 28 | - breaking-change 29 | - title: 🚀 新功能 30 | labels: 31 | - enhancement 32 | - title: 🐛 Bug 修复 33 | labels: 34 | - bug 35 | - title: 💫 杂项 36 | change-template: "- $TITLE @$AUTHOR (#$NUMBER)" 37 | change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. 38 | version-resolver: 39 | major: 40 | labels: 41 | - "major" 42 | minor: 43 | labels: 44 | - "minor" 45 | patch: 46 | labels: 47 | - "patch" 48 | default: patch 49 | -------------------------------------------------------------------------------- /.github/workflows/codecov.yml: -------------------------------------------------------------------------------- 1 | name: Code Coverage 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | pull_request: 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | test: 15 | name: Test 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | matrix: 19 | python-version: ["3.9", "3.10", "3.11", "3.12"] 20 | os: [ubuntu-latest, windows-latest, macos-latest] 21 | fail-fast: false 22 | env: 23 | OS: ${{ matrix.os }} 24 | PYTHON_VERSION: ${{ matrix.python-version }} 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | - name: Setup rye 29 | uses: eifinger/setup-rye@d4c3ac7b15d8bf2e0b45e2d257c6b5cdbebc3643 30 | with: 31 | enable-cache: true 32 | cache-prefix: ${{ env.PYTHON_VERSION }} 33 | - name: Install prerequisites 34 | run: | 35 | rye pin ${{ env.PYTHON_VERSION }} 36 | rye sync 37 | - name: Run tests 38 | run: rye run test 39 | - name: Upload coverage to Codecov 40 | uses: codecov/codecov-action@v4 41 | with: 42 | token: ${{ secrets.CODECOV_TOKEN }} 43 | env_vars: OS,PYTHON_VERSION 44 | 45 | check: 46 | if: always() 47 | needs: test 48 | runs-on: ubuntu-latest 49 | steps: 50 | - name: Decide whether the needed jobs succeeded or failed 51 | uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe 52 | with: 53 | jobs: ${{ toJSON(needs) }} 54 | -------------------------------------------------------------------------------- /.github/workflows/release-draft.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | types: [opened, reopened, synchronize] 9 | 10 | jobs: 11 | update_release_draft: 12 | name: Update Release Draft 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: release-drafter/release-drafter@v6 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | runs-on: ubuntu-latest 12 | environment: release 13 | permissions: 14 | id-token: write 15 | contents: write 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | 20 | - name: Setup rye 21 | uses: eifinger/setup-rye@d4c3ac7b15d8bf2e0b45e2d257c6b5cdbebc3643 22 | with: 23 | enable-cache: true 24 | 25 | - name: Install prerequisites 26 | run: rye sync 27 | 28 | - name: Get Version 29 | id: version 30 | run: | 31 | echo "VERSION=$(rye version)" >> $GITHUB_OUTPUT 32 | echo "TAG_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT 33 | echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT 34 | 35 | - name: Check Version 36 | if: steps.version.outputs.VERSION != steps.version.outputs.TAG_VERSION 37 | run: exit 1 38 | 39 | - name: Build 40 | run: rye build 41 | 42 | - name: Publish a Python distribution to PyPI 43 | uses: pypa/gh-action-pypi-publish@release/v1 44 | 45 | - name: Upload Release Asset 46 | run: gh release upload --clobber ${{ steps.version.outputs.TAG_NAME }} dist/*.tar.gz dist/*.whl 47 | env: 48 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/python 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=python 3 | 4 | ### Python ### 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | pip-wheel-metadata/ 28 | share/python-wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | *.py,cover 55 | .hypothesis/ 56 | .pytest_cache/ 57 | pytestdebug.log 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | 63 | # Django stuff: 64 | *.log 65 | local_settings.py 66 | db.sqlite3 67 | db.sqlite3-journal 68 | 69 | # Flask stuff: 70 | instance/ 71 | .webassets-cache 72 | 73 | # Scrapy stuff: 74 | .scrapy 75 | 76 | # Sphinx documentation 77 | docs/_build/ 78 | doc/_build/ 79 | 80 | # PyBuilder 81 | target/ 82 | 83 | # Jupyter Notebook 84 | .ipynb_checkpoints 85 | 86 | # IPython 87 | profile_default/ 88 | ipython_config.py 89 | 90 | # pyenv 91 | .python-version 92 | 93 | # pipenv 94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 97 | # install all needed dependencies. 98 | #Pipfile.lock 99 | 100 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 101 | __pypackages__/ 102 | 103 | # Celery stuff 104 | celerybeat-schedule 105 | celerybeat.pid 106 | 107 | # SageMath parsed files 108 | *.sage.py 109 | 110 | # Environments 111 | .env 112 | .venv 113 | env/ 114 | venv/ 115 | ENV/ 116 | env.bak/ 117 | venv.bak/ 118 | 119 | # Spyder project settings 120 | .spyderproject 121 | .spyproject 122 | 123 | # Rope project settings 124 | .ropeproject 125 | 126 | # mkdocs documentation 127 | /site 128 | 129 | # mypy 130 | .mypy_cache/ 131 | .dmypy.json 132 | dmypy.json 133 | 134 | # Pyre type checker 135 | .pyre/ 136 | 137 | # pytype static type analyzer 138 | .pytype/ 139 | 140 | # End of https://www.toptal.com/developers/gitignore/api/python 141 | 142 | # VSCode 143 | .vscode/* 144 | !.vscode/launch.json 145 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | ci: 2 | autofix_commit_msg: "style: auto fix by pre-commit hooks" 3 | autofix_prs: true 4 | autoupdate_branch: main 5 | autoupdate_schedule: quarterly 6 | autoupdate_commit_msg: "chore: auto update by pre-commit hooks" 7 | repos: 8 | - repo: https://github.com/astral-sh/ruff-pre-commit 9 | rev: v0.5.0 10 | hooks: 11 | - id: ruff 12 | args: [--fix, --exit-non-zero-on-fix] 13 | stages: [commit] 14 | - id: ruff-format 15 | 16 | - repo: https://github.com/pre-commit/mirrors-prettier 17 | rev: v4.0.0-alpha.8 18 | hooks: 19 | - id: prettier 20 | types_or: [javascript, jsx, ts, tsx, markdown, yaml, json] 21 | stages: [commit] 22 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.12.3 2 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Run", 9 | "type": "python", 10 | "request": "launch", 11 | "module": "nb_cli", 12 | "args": ["run", "--reload"], 13 | "justMyCode": false 14 | }, 15 | { 16 | "name": "Pytest", 17 | "type": "python", 18 | "request": "launch", 19 | "module": "pytest" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/lang/zh-CN/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.5.0] - 2024-07-13 11 | 12 | ### Fixed 13 | 14 | - 适配最新版的 Alconna 15 | 16 | ### Removed 17 | 18 | - 移除 Python 3.8 支持 19 | 20 | ## [0.4.1] - 2024-03-01 21 | 22 | ### Fixed 23 | 24 | - 修复命令没有 shortcut 时的报错 25 | 26 | ## [0.4.0] - 2024-02-29 27 | 28 | ### Added 29 | 30 | - 支持 Alconna 31 | - 适配 Pydantic V2 32 | 33 | ## [0.3.0] - 2023-06-17 34 | 35 | ### Added 36 | 37 | - 通过插件名称忽略指定插件 38 | 39 | ## [0.2.0] - 2023-06-06 40 | 41 | ### Added 42 | 43 | - 适配 NoneBot 2.0 正式版的插件元数据 44 | 45 | ## [0.1.0] - 2023-04-16 46 | 47 | ### Changed 48 | 49 | - 发送 /help 默认显示 /help -l 的内容 50 | 51 | ## [0.0.5] - 2023-03-06 52 | 53 | ### Fixed 54 | 55 | - 修复设置多个分隔符时无法匹配的问题 56 | 57 | ## [0.0.4] - 2023-03-06 58 | 59 | ### Added 60 | 61 | - 支持通过插件内命令来获取插件帮助 62 | 63 | ## [0.0.3] - 2023-02-18 64 | 65 | ### Changed 66 | 67 | - 美化插件树的显示 68 | - 使用 shell_command 处理命令 69 | 70 | ## [0.0.2] - 2023-02-06 71 | 72 | ### Added 73 | 74 | - 支持输出插件树 75 | - 支持通过设置 adapters 属性来指定支持的适配器 76 | 77 | ### Fixed 78 | 79 | - 修复子插件导入其他子插件时的错误判断 80 | 81 | ## [0.0.1] - 2022-07-02 82 | 83 | ### Added 84 | 85 | - 可以使用的版本。 86 | 87 | [unreleased]: https://github.com/he0119/nonebot-plugin-treehelp/compare/v0.5.0...HEAD 88 | [0.5.0]: https://github.com/he0119/nonebot-plugin-treehelp/compare/v0.4.1...v0.5.0 89 | [0.4.1]: https://github.com/he0119/nonebot-plugin-treehelp/compare/v0.4.0...v0.4.1 90 | [0.4.0]: https://github.com/he0119/nonebot-plugin-treehelp/compare/v0.3.0...v0.4.0 91 | [0.3.0]: https://github.com/he0119/nonebot-plugin-treehelp/compare/v0.2.0...v0.3.0 92 | [0.2.0]: https://github.com/he0119/nonebot-plugin-treehelp/compare/v0.1.0...v0.2.0 93 | [0.1.0]: https://github.com/he0119/nonebot-plugin-treehelp/compare/v0.0.5...v0.1.0 94 | [0.0.5]: https://github.com/he0119/nonebot-plugin-treehelp/compare/v0.0.4...v0.0.5 95 | [0.0.4]: https://github.com/he0119/nonebot-plugin-treehelp/compare/v0.0.3...v0.0.4 96 | [0.0.3]: https://github.com/he0119/nonebot-plugin-treehelp/compare/v0.0.2...v0.0.3 97 | [0.0.2]: https://github.com/he0119/nonebot-plugin-treehelp/compare/v0.0.1...v0.0.2 98 | [0.0.1]: https://github.com/he0119/nonebot-plugin-treehelp/releases/tag/v0.0.1 99 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 hemengyang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | nonebot 5 |

6 | 7 |
8 | 9 | # NoneBot Plugin TreeHelp 10 | 11 | _✨ NoneBot 树形帮助插件 ✨_ 12 | 13 |
14 | 15 |

16 | 17 | license 18 | 19 | 20 | pypi 21 | 22 | python 23 | 24 | codecov 25 | 26 | 27 | QQ Chat Group 28 | 29 |

30 | 31 | ## 简介 32 | 33 | 使用插件元数据获取插件信息,并通过插件与子插件的组织形式,来区分插件的多种功能。 34 | 35 | 树形帮助插件,最重要的功能当然是显示插件树! 36 | 37 | 发送 `/help --tree`,你将获得如下帮助: 38 | 39 | ```text 40 | 插件: 41 | 帮助 # 获取插件帮助信息 42 | 测试 # 一个测试插件 43 | ├── 复杂功能 # 测试插件复杂子插件 44 | │ └── 二级功能 # 测试插件二级插件 45 | └── 简单功能 # 测试插件简单子插件 46 | ``` 47 | 48 | ## 使用方式 49 | 50 | 加载插件后发送 `/help help` 或 `/help --help` 获取具体用法。 51 | 52 | ## 插件适配 53 | 54 | 插件与子插件写法可参考 [示例插件](./tests/plugins/tree/)。 55 | 56 | ### 声明适配器 57 | 58 | 通过设置 adapters 属性来指定支持的适配器。如果不设置或留空则默认支持全部适配器。如果插件不支持该适配器,则不会在帮助列表上显示。 59 | 60 | ```python 61 | __plugin_meta__ = PluginMetadata( 62 | name="OneBot", 63 | description="测试 OneBot 适配器", 64 | usage="/onebot", 65 | type="application", 66 | supported_adapters={"~onebot.v11", "~onebot.v12"}, 67 | ) 68 | ``` 69 | 70 | ## 配置项 71 | 72 | 配置方式:直接在 `NoneBot` 全局配置文件中添加以下配置项即可。 73 | 74 | ### treehelp_ignored_plugins 75 | 76 | - 类型: `list[str]` 77 | - 默认: `[]` 78 | - 说明: 需要忽略的插件名称列表 79 | 80 | ## 计划 81 | 82 | - [ ] 支持输出插件版本 83 | - [x] 支持输出插件树 84 | - [ ] 支持输出插件内的命令名称 85 | -------------------------------------------------------------------------------- /nonebot_plugin_treehelp/__init__.py: -------------------------------------------------------------------------------- 1 | """帮助 2 | 3 | 通过读取插件元信息生成帮助信息 4 | """ 5 | 6 | from nonebot import on_shell_command 7 | from nonebot.adapters import Bot 8 | from nonebot.exception import ParserExit 9 | from nonebot.params import ShellCommandArgs 10 | from nonebot.plugin import PluginMetadata 11 | from nonebot.rule import ArgumentParser, Namespace 12 | 13 | from .config import Config 14 | from .data_source import get_plugin_help, get_plugin_list 15 | 16 | __plugin_meta__ = PluginMetadata( 17 | name="帮助", 18 | description="获取插件帮助信息", 19 | usage="""获取插件列表 20 | /help 21 | 获取插件树 22 | /help -t 23 | /help --tree 24 | 获取某个插件的帮助 25 | /help 插件名 26 | 获取某个插件的树 27 | /help --tree 插件名 28 | """, 29 | type="application", 30 | homepage="https://github.com/he0119/nonebot-plugin-treehelp", 31 | config=Config, 32 | ) 33 | 34 | parser = ArgumentParser("帮助", description="获取插件帮助信息") 35 | parser.add_argument("plugin_name", nargs="?", type=str, help="插件名", metavar="插件名") 36 | parser.add_argument("-t", "--tree", action="store_true", help="获取插件树") 37 | help_cmd = on_shell_command("help", aliases={"帮助"}, parser=parser) 38 | 39 | 40 | @help_cmd.handle() 41 | async def help_handle(bot: Bot, args: Namespace = ShellCommandArgs()): 42 | plugin_name = args.plugin_name 43 | 44 | if plugin_name is None: 45 | await help_cmd.finish(get_plugin_list(bot, args.tree)) 46 | 47 | plugin_help = get_plugin_help(bot, plugin_name, args.tree) 48 | if plugin_help: 49 | await help_cmd.finish(plugin_help) 50 | else: 51 | await help_cmd.finish(f"未找到插件 {plugin_name}") 52 | 53 | 54 | @help_cmd.handle() 55 | async def _(foo: ParserExit = ShellCommandArgs()): 56 | await help_cmd.finish(foo.message) 57 | -------------------------------------------------------------------------------- /nonebot_plugin_treehelp/config.py: -------------------------------------------------------------------------------- 1 | """配置""" 2 | 3 | from nonebot import get_plugin_config 4 | from pydantic import BaseModel 5 | 6 | 7 | class Config(BaseModel): 8 | treehelp_ignored_plugins: list[str] = [] 9 | """需要忽略的插件""" 10 | 11 | 12 | plugin_config = get_plugin_config(Config) 13 | -------------------------------------------------------------------------------- /nonebot_plugin_treehelp/data_source.py: -------------------------------------------------------------------------------- 1 | """帮助数据 2 | 3 | 获取插件的帮助信息,并通过子插件的形式获取次级菜单 4 | """ 5 | 6 | from typing import TYPE_CHECKING, Optional, Union, cast 7 | 8 | from nonebot import get_driver, get_loaded_plugins, require 9 | from nonebot.rule import CommandRule, ShellCommandRule 10 | 11 | try: 12 | require("nonebot_plugin_alconna") 13 | from nonebot_plugin_alconna import command_manager 14 | from nonebot_plugin_alconna.rule import AlconnaRule 15 | 16 | COMMAND_RULES = (CommandRule, ShellCommandRule, AlconnaRule) 17 | except (ImportError, RuntimeError): # pragma: no cover 18 | command_manager = None 19 | AlconnaRule = None 20 | COMMAND_RULES = (CommandRule, ShellCommandRule) 21 | 22 | from .config import plugin_config 23 | 24 | if TYPE_CHECKING: 25 | from nonebot.adapters import Bot 26 | from nonebot.plugin import Plugin, PluginMetadata 27 | 28 | global_config = get_driver().config 29 | 30 | _plugins: Optional[dict[str, "Plugin"]] = None 31 | _commands: dict[tuple[str, ...], "Plugin"] = {} 32 | 33 | 34 | def map_command_to_plguin(plugin: "Plugin"): 35 | """建立命令与插件的映射""" 36 | matchers = plugin.matcher 37 | for matcher in matchers: 38 | checkers = matcher.rule.checkers 39 | command_handler = next( 40 | filter(lambda x: isinstance(x.call, COMMAND_RULES), checkers), 41 | None, 42 | ) 43 | if not command_handler: 44 | continue 45 | 46 | if ( 47 | AlconnaRule 48 | and isinstance(command_handler.call, AlconnaRule) 49 | and command_manager 50 | ): 51 | command = command_handler.call.command() 52 | if not command: # pragma: no cover 53 | continue 54 | 55 | cmds = [(str(command.command),)] 56 | shortcuts = command_manager.get_shortcut(command) 57 | cmds.extend([(shortcut,) for shortcut in shortcuts]) 58 | 59 | else: 60 | command = cast(Union[CommandRule, ShellCommandRule], command_handler.call) 61 | cmds = command.cmds 62 | 63 | for cmd in cmds: 64 | _commands[cmd] = plugin 65 | 66 | 67 | def format_description(plugins: list["Plugin"]) -> str: 68 | """格式化描述""" 69 | return "\n".join( 70 | sorted(f"{x.metadata.name} # {x.metadata.description}" for x in plugins) # type: ignore 71 | ) 72 | 73 | 74 | def is_supported_adapter(bot: "Bot", metadata: "PluginMetadata") -> bool: 75 | """是否是支持的适配器""" 76 | if metadata.supported_adapters is None: 77 | return True 78 | 79 | supported_adapters = metadata.get_supported_adapters() 80 | if not supported_adapters: 81 | return False 82 | 83 | for adapter in supported_adapters: 84 | if isinstance(bot.adapter, adapter): 85 | return True 86 | 87 | return False 88 | 89 | 90 | def is_supported_type(metadata: "PluginMetadata") -> bool: 91 | """是否是支持的插件类型 92 | 93 | 当前有 library 和 application 两种类型 94 | 仅支持 application 类型的插件 95 | """ 96 | type_ = metadata.type 97 | # 如果没有指定类型,则默认支持 98 | if type_ is None: 99 | return True 100 | # 当前仅支持 application 类型 101 | if type_ == "application": 102 | return True 103 | return False 104 | 105 | 106 | def is_supported(bot: "Bot", plugin: "Plugin") -> bool: 107 | """是否是支持的插件""" 108 | if plugin.metadata is None: 109 | return False 110 | 111 | if plugin.metadata.name in plugin_config.treehelp_ignored_plugins: 112 | return False 113 | 114 | if not is_supported_type(plugin.metadata): 115 | return False 116 | 117 | if not is_supported_adapter(bot, plugin.metadata): 118 | return False 119 | 120 | return True 121 | 122 | 123 | def get_plugins() -> dict[str, "Plugin"]: 124 | """获取适配了元信息的插件""" 125 | global _plugins 126 | 127 | if _plugins is None: 128 | plugins = filter(lambda x: x.metadata is not None, get_loaded_plugins()) 129 | _plugins = {x.metadata.name: x for x in plugins} # type: ignore 130 | for plugin in _plugins.values(): 131 | map_command_to_plguin(plugin) 132 | 133 | return _plugins 134 | 135 | 136 | def get_plugin_list(bot: "Bot", tree: bool = False) -> str: 137 | """获取插件列表""" 138 | # 仅保留根插件 139 | plugins = [ 140 | plugin 141 | for plugin in get_plugins().values() 142 | if plugin.parent_plugin is None and is_supported(bot, plugin) 143 | ] 144 | sorted_plugins = sorted(plugins, key=lambda x: x.metadata.name) # type: ignore 145 | 146 | docs = ["插件:"] 147 | if tree: 148 | for plugin in sorted_plugins: 149 | docs.append(f"{plugin.metadata.name} # {plugin.metadata.description}") # type: ignore 150 | get_tree_string(bot, docs, plugin.sub_plugins, "") 151 | else: 152 | docs.append(format_description(sorted_plugins)) 153 | return "\n".join(docs) 154 | 155 | 156 | def get_plugin_help(bot: "Bot", name: str, tree: bool = False) -> Optional[str]: 157 | """通过插件获取命令的帮助""" 158 | plugins = get_plugins() 159 | 160 | plugin = plugins.get(name) 161 | if not plugin: 162 | # str.split 只支持单个分隔符 163 | # 用 re 又太麻烦了,直接遍历所有分隔符,找到就停止 164 | for sep in global_config.command_sep: 165 | plugin = _commands.get(tuple(name.split(sep))) 166 | if plugin: 167 | break 168 | if not plugin: 169 | return 170 | 171 | # 排除不支持的插件 172 | if not is_supported(bot, plugin): 173 | return 174 | 175 | metadata = cast("PluginMetadata", plugin.metadata) 176 | 177 | if tree: 178 | docs = [f"{metadata.name} # {metadata.description}"] 179 | get_tree_string(bot, docs, plugin.sub_plugins, "") 180 | return "\n".join(docs) 181 | 182 | sub_plugins = [plugin for plugin in plugin.sub_plugins if is_supported(bot, plugin)] 183 | sub_plugins_desc = format_description(sub_plugins) 184 | return "\n\n".join( 185 | [x for x in [metadata.name, metadata.usage, sub_plugins_desc] if x] 186 | ) 187 | 188 | 189 | def get_tree_string( 190 | bot: "Bot", 191 | docs: list[str], 192 | plugins: set["Plugin"], 193 | previous_tree_bar: str, 194 | ) -> None: 195 | """通过递归获取树形结构的字符串""" 196 | previous_tree_bar = previous_tree_bar.replace("├", "│") 197 | 198 | filtered_plugins = filter(lambda x: is_supported(bot, x), plugins) 199 | sorted_plugins = sorted(filtered_plugins, key=lambda x: x.metadata.name) # type: ignore 200 | 201 | tree_bar = previous_tree_bar + "├" 202 | total = len(sorted_plugins) 203 | for i, plugin in enumerate(sorted_plugins, 1): 204 | if i == total: 205 | tree_bar = previous_tree_bar + "└" 206 | docs.append( 207 | f"{tree_bar}── {plugin.metadata.name} # {plugin.metadata.description}" # type: ignore 208 | ) 209 | tree_bar = tree_bar.replace("└", " ") 210 | get_tree_string(bot, docs, plugin.sub_plugins, tree_bar + " ") 211 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "nonebot-plugin-treehelp" 3 | version = "0.5.0" 4 | description = "适用于 Nonebot2 的树形帮助插件" 5 | authors = [{ name = "uy_sun", email = "hmy0119@gmail.com" }] 6 | dependencies = ["nonebot2>=2.2.0"] 7 | readme = "README.md" 8 | license = { file = "LICENSE" } 9 | requires-python = ">= 3.9" 10 | 11 | [project.urls] 12 | Homepage = "https://github.com/he0119/nonebot-plugin-treehelp" 13 | Repository = "https://github.com/he0119/nonebot-plugin-treehelp.git" 14 | Issues = "https://github.com/he0119/nonebot-plugin-treehelp/issues" 15 | Changelog = "https://github.com/he0119/nonebot-plugin-treehelp/blob/main/CHANGELOG.md" 16 | 17 | [project.optional-dependencies] 18 | alconna = ["nonebot-plugin-alconna>=0.49.0"] 19 | 20 | [build-system] 21 | requires = ["hatchling"] 22 | build-backend = "hatchling.build" 23 | 24 | [tool.hatch.metadata] 25 | allow-direct-references = true 26 | 27 | [tool.hatch.build.targets.wheel] 28 | packages = ["nonebot_plugin_treehelp"] 29 | 30 | [tool.hatch.build.targets.sdist] 31 | only-include = ["nonebot_plugin_treehelp"] 32 | 33 | [tool.rye] 34 | managed = true 35 | universal = true 36 | dev-dependencies = [ 37 | "nonebug>=0.3.7", 38 | "pytest-cov>=5.0.0", 39 | "pytest-xdist>=3.6.1", 40 | "pytest-mock>=3.14.0", 41 | "pytest-asyncio>=0.23.7", 42 | "nonebot2[fastapi]>=2.3.2", 43 | "nonebot-adapter-onebot>=2.4.4", 44 | "nonebot-adapter-console>=0.5.0", 45 | "nonebot-plugin-alconna>=0.49.0", 46 | ] 47 | 48 | [tool.rye.scripts] 49 | test = "pytest --cov=nonebot_plugin_treehelp --cov-report xml -n auto" 50 | 51 | [tool.pyright] 52 | pythonVersion = "3.9" 53 | pythonPlatform = "All" 54 | typeCheckingMode = "standard" 55 | 56 | [tool.ruff] 57 | line-length = 88 58 | target-version = "py39" 59 | 60 | [tool.ruff.lint] 61 | select = [ 62 | "W", # pycodestyle warnings 63 | "E", # pycodestyle errors 64 | "F", # pyflakes 65 | "UP", # pyupgrade 66 | "C4", # flake8-comprehensions 67 | "T10", # flake8-debugger 68 | "T20", # flake8-print 69 | "PYI", # flake8-pyi 70 | "PT", # flake8-pytest-style 71 | "Q", # flake8-quotes 72 | ] 73 | ignore = [ 74 | "E402", # module-import-not-at-top-of-file 75 | ] 76 | 77 | [tool.nonebot] 78 | adapters = [ 79 | { name = "OneBot V11", module_name = "nonebot.adapters.onebot.v11" }, 80 | ] 81 | plugins = ["nonebot_plugin_treehelp", "tests.plugins.tree"] 82 | 83 | [tool.coverage.report] 84 | exclude_lines = [ 85 | "pragma: no cover", 86 | "raise NotImplementedError", 87 | "if __name__ == .__main__.:", 88 | "if TYPE_CHECKING:", 89 | "@overload", 90 | "except ImportError:", 91 | ] 92 | 93 | [tool.pytest.ini_options] 94 | addopts = ["--import-mode=importlib"] 95 | asyncio_mode = "auto" 96 | -------------------------------------------------------------------------------- /requirements-dev.lock: -------------------------------------------------------------------------------- 1 | # generated by rye 2 | # use `rye lock` or `rye sync` to update this lockfile 3 | # 4 | # last locked with the following flags: 5 | # pre: false 6 | # features: [] 7 | # all-features: false 8 | # with-sources: false 9 | # generate-hashes: false 10 | # universal: true 11 | 12 | -e file:. 13 | annotated-types==0.7.0 14 | # via pydantic 15 | anyio==4.4.0 16 | # via httpx 17 | # via starlette 18 | # via watchfiles 19 | arclet-alconna==1.8.18 20 | # via arclet-alconna-tools 21 | # via nonebot-plugin-alconna 22 | arclet-alconna-tools==0.7.6 23 | # via nonebot-plugin-alconna 24 | asgiref==3.8.1 25 | # via nonebug 26 | async-asgi-testclient==1.4.11 27 | # via nonebug 28 | certifi==2024.7.4 29 | # via httpcore 30 | # via httpx 31 | # via requests 32 | charset-normalizer==3.3.2 33 | # via requests 34 | click==8.1.7 35 | # via typer 36 | # via uvicorn 37 | colorama==0.4.6 ; platform_system == 'Windows' or sys_platform == 'win32' 38 | # via click 39 | # via loguru 40 | # via pytest 41 | # via uvicorn 42 | coverage==7.6.0 43 | # via pytest-cov 44 | dnspython==2.6.1 45 | # via email-validator 46 | email-validator==2.2.0 47 | # via fastapi 48 | execnet==2.1.1 49 | # via pytest-xdist 50 | fastapi==0.111.0 51 | # via nonebot2 52 | fastapi-cli==0.0.4 53 | # via fastapi 54 | h11==0.14.0 55 | # via httpcore 56 | # via uvicorn 57 | httpcore==1.0.5 58 | # via httpx 59 | httptools==0.6.1 60 | # via uvicorn 61 | httpx==0.27.0 62 | # via fastapi 63 | idna==3.7 64 | # via anyio 65 | # via email-validator 66 | # via httpx 67 | # via requests 68 | # via yarl 69 | importlib-metadata==8.0.0 70 | # via nonebot-plugin-alconna 71 | # via textual 72 | iniconfig==2.0.0 73 | # via pytest 74 | jinja2==3.1.4 75 | # via fastapi 76 | linkify-it-py==2.0.3 77 | # via markdown-it-py 78 | loguru==0.7.2 79 | # via nonebot2 80 | markdown-it-py==3.0.0 81 | # via mdit-py-plugins 82 | # via rich 83 | # via textual 84 | markupsafe==2.1.5 85 | # via jinja2 86 | mdit-py-plugins==0.4.1 87 | # via markdown-it-py 88 | mdurl==0.1.2 89 | # via markdown-it-py 90 | msgpack==1.0.8 91 | # via nonebot-adapter-onebot 92 | multidict==6.0.5 93 | # via async-asgi-testclient 94 | # via yarl 95 | nepattern==0.7.4 96 | # via arclet-alconna 97 | # via arclet-alconna-tools 98 | # via nonebot-plugin-alconna 99 | nonebot-adapter-console==0.6.0 100 | nonebot-adapter-onebot==2.4.4 101 | nonebot-plugin-alconna==0.49.0 102 | nonebot-plugin-waiter==0.6.2 103 | # via nonebot-plugin-alconna 104 | nonebot2==2.3.2 105 | # via nonebot-adapter-console 106 | # via nonebot-adapter-onebot 107 | # via nonebot-plugin-alconna 108 | # via nonebot-plugin-treehelp 109 | # via nonebot-plugin-waiter 110 | # via nonebug 111 | nonebug==0.3.7 112 | nonechat==0.2.1 113 | # via nonebot-adapter-console 114 | orjson==3.10.6 115 | # via fastapi 116 | packaging==24.1 117 | # via pytest 118 | pluggy==1.5.0 119 | # via pytest 120 | pydantic==2.8.2 121 | # via fastapi 122 | # via nonebot-adapter-console 123 | # via nonebot-adapter-onebot 124 | # via nonebot2 125 | pydantic-core==2.20.1 126 | # via pydantic 127 | pygments==2.18.0 128 | # via rich 129 | pygtrie==2.5.0 130 | # via nonebot2 131 | pytest==8.2.2 132 | # via nonebug 133 | # via pytest-asyncio 134 | # via pytest-cov 135 | # via pytest-mock 136 | # via pytest-xdist 137 | pytest-asyncio==0.23.7 138 | pytest-cov==5.0.0 139 | pytest-mock==3.14.0 140 | pytest-xdist==3.6.1 141 | python-dotenv==1.0.1 142 | # via nonebot2 143 | # via uvicorn 144 | python-multipart==0.0.9 145 | # via fastapi 146 | pyyaml==6.0.1 147 | # via uvicorn 148 | requests==2.32.3 149 | # via async-asgi-testclient 150 | rich==13.7.1 151 | # via textual 152 | # via typer 153 | shellingham==1.5.4 154 | # via typer 155 | sniffio==1.3.1 156 | # via anyio 157 | # via httpx 158 | starlette==0.37.2 159 | # via fastapi 160 | tarina==0.5.4 161 | # via arclet-alconna 162 | # via nepattern 163 | # via nonebot-plugin-alconna 164 | textual==0.29.0 165 | # via nonechat 166 | typer==0.12.3 167 | # via fastapi-cli 168 | typing-extensions==4.12.2 169 | # via arclet-alconna 170 | # via fastapi 171 | # via nepattern 172 | # via nonebot-adapter-console 173 | # via nonebot-adapter-onebot 174 | # via nonebot2 175 | # via nonebug 176 | # via pydantic 177 | # via pydantic-core 178 | # via tarina 179 | # via textual 180 | # via typer 181 | uc-micro-py==1.0.3 182 | # via linkify-it-py 183 | ujson==5.10.0 184 | # via fastapi 185 | urllib3==2.2.2 186 | # via requests 187 | uvicorn==0.30.1 188 | # via fastapi 189 | # via nonebot2 190 | uvloop==0.19.0 ; sys_platform != 'win32' and (platform_python_implementation != 'PyPy' and sys_platform != 'cygwin') 191 | # via uvicorn 192 | watchfiles==0.22.0 193 | # via uvicorn 194 | websockets==12.0 195 | # via uvicorn 196 | win32-setctime==1.1.0 ; sys_platform == 'win32' 197 | # via loguru 198 | yarl==1.9.4 199 | # via nonebot2 200 | zipp==3.19.2 201 | # via importlib-metadata 202 | -------------------------------------------------------------------------------- /requirements.lock: -------------------------------------------------------------------------------- 1 | # generated by rye 2 | # use `rye lock` or `rye sync` to update this lockfile 3 | # 4 | # last locked with the following flags: 5 | # pre: false 6 | # features: [] 7 | # all-features: false 8 | # with-sources: false 9 | # generate-hashes: false 10 | # universal: true 11 | 12 | -e file:. 13 | annotated-types==0.7.0 14 | # via pydantic 15 | colorama==0.4.6 ; sys_platform == 'win32' 16 | # via loguru 17 | idna==3.7 18 | # via yarl 19 | loguru==0.7.2 20 | # via nonebot2 21 | multidict==6.0.5 22 | # via yarl 23 | nonebot2==2.3.2 24 | # via nonebot-plugin-treehelp 25 | pydantic==2.8.2 26 | # via nonebot2 27 | pydantic-core==2.20.1 28 | # via pydantic 29 | pygtrie==2.5.0 30 | # via nonebot2 31 | python-dotenv==1.0.1 32 | # via nonebot2 33 | typing-extensions==4.12.2 34 | # via nonebot2 35 | # via pydantic 36 | # via pydantic-core 37 | win32-setctime==1.1.0 ; sys_platform == 'win32' 38 | # via loguru 39 | yarl==1.9.4 40 | # via nonebot2 41 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/he0119/nonebot-plugin-treehelp/0307a850d008269c12b3afeb720a86f7fee36380/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import nonebot 2 | import pytest 3 | from nonebug import NONEBOT_INIT_KWARGS, App 4 | 5 | from .utils import clear_plugins 6 | 7 | 8 | def pytest_configure(config: pytest.Config) -> None: 9 | config.stash[NONEBOT_INIT_KWARGS] = {"command_sep": [".", "。"]} 10 | 11 | 12 | @pytest.fixture(autouse=True) 13 | def _register_adapters(nonebug_init: None): 14 | from nonebot import get_driver 15 | from nonebot.adapters.console import Adapter as ConsoleAdapter 16 | from nonebot.adapters.onebot.v11 import Adapter as OnebotV11Adapter 17 | from nonebot.adapters.onebot.v12 import Adapter as OnebotV12Adapter 18 | 19 | driver = get_driver() 20 | driver.register_adapter(ConsoleAdapter) 21 | driver.register_adapter(OnebotV11Adapter) 22 | driver.register_adapter(OnebotV12Adapter) 23 | 24 | 25 | @pytest.fixture() 26 | def app(nonebug_init: None): 27 | clear_plugins() 28 | # 加载插件 29 | nonebot.load_plugin("nonebot_plugin_treehelp") 30 | return App() 31 | -------------------------------------------------------------------------------- /tests/plugins/adapters/__init__.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import nonebot 4 | from nonebot.plugin import PluginMetadata 5 | 6 | __plugin_meta__ = PluginMetadata( 7 | name="适配器", 8 | description="测试不同适配器", 9 | usage="", 10 | type="application", 11 | ) 12 | 13 | _sub_plugins = set() 14 | _sub_plugins |= nonebot.load_plugins(str((Path(__file__).parent / "plugins").resolve())) 15 | -------------------------------------------------------------------------------- /tests/plugins/adapters/plugins/sub1.py: -------------------------------------------------------------------------------- 1 | from nonebot.plugin import PluginMetadata 2 | 3 | __plugin_meta__ = PluginMetadata( 4 | name="OneBot", 5 | description="测试 OneBot 适配器", 6 | usage="/onebot", 7 | supported_adapters={"~onebot.v11", "~onebot.v12"}, 8 | ) 9 | -------------------------------------------------------------------------------- /tests/plugins/adapters/plugins/sub2.py: -------------------------------------------------------------------------------- 1 | from nonebot.plugin import PluginMetadata 2 | 3 | __plugin_meta__ = PluginMetadata( 4 | name="Console", 5 | description="测试 Console 适配器", 6 | usage="/console", 7 | supported_adapters={"~console"}, 8 | ) 9 | -------------------------------------------------------------------------------- /tests/plugins/adapters/plugins/sub3.py: -------------------------------------------------------------------------------- 1 | from nonebot.plugin import PluginMetadata 2 | 3 | __plugin_meta__ = PluginMetadata( 4 | name="library", 5 | description="测试库插件", 6 | usage="/library", 7 | supported_adapters={"~console"}, 8 | type="library", 9 | ) 10 | -------------------------------------------------------------------------------- /tests/plugins/adapters/plugins/sub4.py: -------------------------------------------------------------------------------- 1 | """插件支持的适配器不存在""" 2 | 3 | from nonebot.plugin import PluginMetadata 4 | 5 | __plugin_meta__ = PluginMetadata( 6 | name="Console", 7 | description="测试 Console 适配器", 8 | usage="/console", 9 | supported_adapters={"~fake"}, 10 | ) 11 | -------------------------------------------------------------------------------- /tests/plugins/adapters/plugins/sub5.py: -------------------------------------------------------------------------------- 1 | """没有插件元数据""" 2 | -------------------------------------------------------------------------------- /tests/plugins/nested/__init__.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import nonebot 4 | from nonebot.plugin import PluginMetadata 5 | 6 | __plugin_meta__ = PluginMetadata( 7 | name="测试", 8 | description="一个测试插件", 9 | usage="", 10 | ) 11 | 12 | _sub_plugins = set() 13 | _sub_plugins |= nonebot.load_plugins(str((Path(__file__).parent / "plugins").resolve())) 14 | -------------------------------------------------------------------------------- /tests/plugins/nested/plugins/sub1.py: -------------------------------------------------------------------------------- 1 | from nonebot.plugin import PluginMetadata 2 | 3 | plugin_id = 1 4 | 5 | __plugin_meta__ = PluginMetadata( 6 | name="功能一", 7 | description="测试插件子插件一", 8 | usage="/功能一", 9 | ) 10 | -------------------------------------------------------------------------------- /tests/plugins/nested/plugins/sub2.py: -------------------------------------------------------------------------------- 1 | from nonebot.plugin import PluginMetadata 2 | 3 | from .sub1 import plugin_id # noqa: F401 4 | 5 | __plugin_meta__ = PluginMetadata( 6 | name="功能二", 7 | description="测试插件子插件二", 8 | usage="/功能二", 9 | ) 10 | -------------------------------------------------------------------------------- /tests/plugins/tree/__init__.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import nonebot 4 | from nonebot.plugin import PluginMetadata 5 | 6 | __plugin_meta__ = PluginMetadata( 7 | name="测试", 8 | description="一个测试插件", 9 | usage="", 10 | ) 11 | 12 | _sub_plugins = set() 13 | _sub_plugins |= nonebot.load_plugins(str((Path(__file__).parent / "plugins").resolve())) 14 | -------------------------------------------------------------------------------- /tests/plugins/tree/plugins/alc.py: -------------------------------------------------------------------------------- 1 | from nonebot import require 2 | from nonebot.plugin import PluginMetadata 3 | 4 | require("nonebot_plugin_alconna") 5 | from nonebot_plugin_alconna import Alconna, on_alconna 6 | 7 | __plugin_meta__ = PluginMetadata( 8 | name="Alconna", 9 | description="测试插件 alconna 子插件", 10 | usage="/alconna", 11 | ) 12 | 13 | alconna = on_alconna(Alconna("alconna")) 14 | alconna.shortcut( 15 | "alc", 16 | { 17 | "prefix": True, 18 | "command": "alconna", 19 | }, 20 | ) 21 | 22 | no_shortcut = on_alconna(Alconna("no_shortcut")) 23 | -------------------------------------------------------------------------------- /tests/plugins/tree/plugins/simple.py: -------------------------------------------------------------------------------- 1 | from nonebot import on_command, on_message 2 | from nonebot.plugin import PluginMetadata 3 | 4 | __plugin_meta__ = PluginMetadata( 5 | name="简单功能", 6 | description="测试插件简单子插件", 7 | usage="/简单功能", 8 | ) 9 | 10 | simple = on_command("simple", aliases={("simple", "alias")}, priority=1, block=True) 11 | 12 | simple_message = on_message() 13 | -------------------------------------------------------------------------------- /tests/plugins/tree/plugins/sub/__init__.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import nonebot 4 | from nonebot import on_shell_command 5 | from nonebot.plugin import PluginMetadata 6 | 7 | __plugin_meta__ = PluginMetadata( 8 | name="复杂功能", 9 | description="测试插件复杂子插件", 10 | usage="/复杂功能", 11 | ) 12 | 13 | _sub_plugins = set() 14 | _sub_plugins |= nonebot.load_plugins(str((Path(__file__).parent / "subsub").resolve())) 15 | 16 | sub = on_shell_command("sub", aliases={("sub", "alias")}) 17 | -------------------------------------------------------------------------------- /tests/plugins/tree/plugins/sub/subsub/sub2.py: -------------------------------------------------------------------------------- 1 | from nonebot.plugin import PluginMetadata 2 | 3 | __plugin_meta__ = PluginMetadata( 4 | name="二级功能", 5 | description="测试插件二级插件", 6 | usage="/二级功能", 7 | ) 8 | -------------------------------------------------------------------------------- /tests/test_adapters.py: -------------------------------------------------------------------------------- 1 | from nonebug import App 2 | 3 | from .utils import make_fake_event, make_fake_message 4 | 5 | 6 | async def test_adapters(app: App): 7 | """当没有指定 adapters 时,默认显示""" 8 | from nonebot import require 9 | 10 | require("tests.plugins.adapters") 11 | from nonebot_plugin_treehelp import help_cmd 12 | 13 | async with app.test_matcher(help_cmd) as ctx: 14 | bot = ctx.create_bot() 15 | message = message = make_fake_message()("/help") 16 | event = make_fake_event(_message=message)() 17 | 18 | ctx.receive_event(bot, event) 19 | ctx.should_call_send( 20 | event, "插件:\n帮助 # 获取插件帮助信息\n适配器 # 测试不同适配器", True 21 | ) 22 | ctx.should_finished() 23 | 24 | 25 | async def test_adapters_plugin(app: App): 26 | """仅支持 Console 适配器的插件""" 27 | from nonebot import get_adapter, require 28 | from nonebot.adapters.console import Adapter 29 | 30 | adapter = get_adapter(Adapter) 31 | 32 | require("tests.plugins.adapters") 33 | from nonebot_plugin_treehelp import help_cmd 34 | 35 | async with app.test_matcher(help_cmd) as ctx: 36 | bot = ctx.create_bot(adapter=adapter) 37 | message = message = make_fake_message()("/help 适配器") 38 | event = make_fake_event(_message=message)() 39 | 40 | ctx.receive_event(bot, event) 41 | ctx.should_call_send(event, "适配器\n\nConsole # 测试 Console 适配器", True) 42 | ctx.should_finished() 43 | 44 | 45 | async def test_adapters_supported_plugin(app: App): 46 | """支持的插件""" 47 | from nonebot import get_adapter, require 48 | from nonebot.adapters.console import Adapter 49 | 50 | adapter = get_adapter(Adapter) 51 | 52 | require("tests.plugins.adapters") 53 | from nonebot_plugin_treehelp import help_cmd 54 | 55 | async with app.test_matcher(help_cmd) as ctx: 56 | bot = ctx.create_bot(adapter=adapter) 57 | message = message = make_fake_message()("/help 适配器") 58 | event = make_fake_event(_message=message)() 59 | 60 | ctx.receive_event(bot, event) 61 | ctx.should_call_send(event, "适配器\n\nConsole # 测试 Console 适配器", True) 62 | ctx.should_finished() 63 | 64 | 65 | async def test_adapters_unsupported_plugin(app: App): 66 | """不支持的插件""" 67 | from nonebot import require 68 | 69 | require("tests.plugins.adapters") 70 | from nonebot_plugin_treehelp import help_cmd 71 | 72 | async with app.test_matcher(help_cmd) as ctx: 73 | bot = ctx.create_bot() 74 | message = message = make_fake_message()("/help OneBot") 75 | event = make_fake_event(_message=message)() 76 | 77 | ctx.receive_event(bot, event) 78 | ctx.should_call_send(event, "未找到插件 OneBot", True) 79 | ctx.should_finished() 80 | -------------------------------------------------------------------------------- /tests/test_help.py: -------------------------------------------------------------------------------- 1 | from nonebug import App 2 | 3 | from .utils import make_fake_event, make_fake_message 4 | 5 | 6 | async def test_help(app: App): 7 | """测试帮助""" 8 | from nonebot_plugin_treehelp import help_cmd 9 | 10 | async with app.test_matcher(help_cmd) as ctx: 11 | bot = ctx.create_bot() 12 | message = message = make_fake_message()("/help") 13 | event = make_fake_event(_message=message)() 14 | 15 | ctx.receive_event(bot, event) 16 | ctx.should_call_send(event, "插件:\n帮助 # 获取插件帮助信息", True) 17 | ctx.should_finished() 18 | 19 | 20 | async def test_help_help(app: App): 21 | """测试获取帮助插件帮助""" 22 | from nonebot_plugin_treehelp import __plugin_meta__, help_cmd 23 | 24 | async with app.test_matcher(help_cmd) as ctx: 25 | bot = ctx.create_bot() 26 | message = message = make_fake_message()("/help 帮助") 27 | event = make_fake_event(_message=message)() 28 | 29 | ctx.receive_event(bot, event) 30 | ctx.should_call_send( 31 | event, 32 | f"帮助\n\n{__plugin_meta__.usage}", 33 | True, 34 | ) 35 | ctx.should_finished() 36 | 37 | 38 | async def test_help_not_found(app: App): 39 | """测试插件不存在""" 40 | from nonebot_plugin_treehelp import help_cmd 41 | 42 | async with app.test_matcher(help_cmd) as ctx: 43 | bot = ctx.create_bot() 44 | message = message = make_fake_message()("/help test") 45 | event = make_fake_event(_message=message)() 46 | 47 | ctx.receive_event(bot, event) 48 | ctx.should_call_send(event, "未找到插件 test", True) 49 | ctx.should_finished() 50 | 51 | 52 | async def test_help_command_error(app: App): 53 | """测试命令错误""" 54 | from nonebot_plugin_treehelp import help_cmd 55 | 56 | async with app.test_matcher(help_cmd) as ctx: 57 | bot = ctx.create_bot() 58 | message = message = make_fake_message()("/help --test") 59 | event = make_fake_event(_message=message)() 60 | 61 | ctx.receive_event(bot, event) 62 | ctx.should_call_send( 63 | event, 64 | "usage: 帮助 [-h] [-t] [插件名]\n" 65 | "帮助: error: unrecognized arguments: --test\n", 66 | True, 67 | ) 68 | ctx.should_finished() 69 | 70 | 71 | async def test_help_by_command(app: App): 72 | """测试通过命令获取帮助信息""" 73 | from nonebot import require 74 | 75 | require("tests.plugins.tree") 76 | from nonebot_plugin_treehelp import help_cmd 77 | 78 | async with app.test_matcher(help_cmd) as ctx: 79 | bot = ctx.create_bot() 80 | message = message = make_fake_message()("/help simple") 81 | event = make_fake_event(_message=message)() 82 | 83 | ctx.receive_event(bot, event) 84 | ctx.should_call_send(event, "简单功能\n\n/简单功能", True) 85 | ctx.should_finished() 86 | 87 | async with app.test_matcher(help_cmd) as ctx: 88 | bot = ctx.create_bot() 89 | message = message = make_fake_message()("/help simple.alias") 90 | event = make_fake_event(_message=message)() 91 | 92 | ctx.receive_event(bot, event) 93 | ctx.should_call_send(event, "简单功能\n\n/简单功能", True) 94 | ctx.should_finished() 95 | 96 | async with app.test_matcher(help_cmd) as ctx: 97 | bot = ctx.create_bot() 98 | message = message = make_fake_message()("/help sub") 99 | event = make_fake_event(_message=message)() 100 | 101 | ctx.receive_event(bot, event) 102 | ctx.should_call_send( 103 | event, "复杂功能\n\n/复杂功能\n\n二级功能 # 测试插件二级插件", True 104 | ) 105 | ctx.should_finished() 106 | 107 | async with app.test_matcher(help_cmd) as ctx: 108 | bot = ctx.create_bot() 109 | message = message = make_fake_message()("/help sub.alias") 110 | event = make_fake_event(_message=message)() 111 | 112 | ctx.receive_event(bot, event) 113 | ctx.should_call_send( 114 | event, "复杂功能\n\n/复杂功能\n\n二级功能 # 测试插件二级插件", True 115 | ) 116 | ctx.should_finished() 117 | 118 | async with app.test_matcher(help_cmd) as ctx: 119 | bot = ctx.create_bot() 120 | message = message = make_fake_message()("/help sub。alias") 121 | event = make_fake_event(_message=message)() 122 | 123 | ctx.receive_event(bot, event) 124 | ctx.should_call_send( 125 | event, "复杂功能\n\n/复杂功能\n\n二级功能 # 测试插件二级插件", True 126 | ) 127 | ctx.should_finished() 128 | 129 | async with app.test_matcher(help_cmd) as ctx: 130 | bot = ctx.create_bot() 131 | message = message = make_fake_message()("/help alconna") 132 | event = make_fake_event(_message=message)() 133 | 134 | ctx.receive_event(bot, event) 135 | ctx.should_call_send(event, "Alconna\n\n/alconna", True) 136 | ctx.should_finished(help_cmd) 137 | 138 | async with app.test_matcher(help_cmd) as ctx: 139 | bot = ctx.create_bot() 140 | message = message = make_fake_message()("/help alc") 141 | event = make_fake_event(_message=message)() 142 | 143 | ctx.receive_event(bot, event) 144 | ctx.should_call_send(event, "Alconna\n\n/alconna", True) 145 | ctx.should_finished(help_cmd) 146 | 147 | async with app.test_matcher(help_cmd) as ctx: 148 | bot = ctx.create_bot() 149 | message = message = make_fake_message()("/help no_shortcut") 150 | event = make_fake_event(_message=message)() 151 | 152 | ctx.receive_event(bot, event) 153 | ctx.should_call_send(event, "Alconna\n\n/alconna", True) 154 | ctx.should_finished(help_cmd) 155 | -------------------------------------------------------------------------------- /tests/test_ignored.py: -------------------------------------------------------------------------------- 1 | from nonebug import App 2 | from pytest_mock import MockerFixture 3 | 4 | from .utils import make_fake_event, make_fake_message 5 | 6 | 7 | async def test_ignored(app: App, mocker: MockerFixture): 8 | """通过插件名称忽略指定插件""" 9 | from nonebot import require 10 | 11 | from nonebot_plugin_treehelp.config import plugin_config 12 | 13 | mocker.patch.object(plugin_config, "treehelp_ignored_plugins", ["帮助"]) 14 | 15 | require("tests.plugins.adapters") 16 | from nonebot_plugin_treehelp import help_cmd 17 | 18 | async with app.test_matcher(help_cmd) as ctx: 19 | bot = ctx.create_bot() 20 | message = message = make_fake_message()("/help") 21 | event = make_fake_event(_message=message)() 22 | 23 | ctx.receive_event(bot, event) 24 | ctx.should_call_send(event, "插件:\n适配器 # 测试不同适配器", True) 25 | ctx.should_finished() 26 | -------------------------------------------------------------------------------- /tests/test_nested.py: -------------------------------------------------------------------------------- 1 | from nonebug import App 2 | 3 | from .utils import make_fake_event, make_fake_message 4 | 5 | 6 | async def test_parent(app: App): 7 | """测试父插件""" 8 | from nonebot import require 9 | 10 | require("tests.plugins.nested") 11 | from nonebot_plugin_treehelp import help_cmd 12 | 13 | async with app.test_matcher(help_cmd) as ctx: 14 | bot = ctx.create_bot() 15 | message = message = make_fake_message()("/help") 16 | event = make_fake_event(_message=message)() 17 | 18 | ctx.receive_event(bot, event) 19 | ctx.should_call_send( 20 | event, "插件:\n帮助 # 获取插件帮助信息\n测试 # 一个测试插件", True 21 | ) 22 | ctx.should_finished() 23 | 24 | 25 | async def test_sub(app: App): 26 | """测试子插件""" 27 | from nonebot import require 28 | 29 | require("tests.plugins.nested") 30 | from nonebot_plugin_treehelp import help_cmd 31 | 32 | async with app.test_matcher(help_cmd) as ctx: 33 | bot = ctx.create_bot() 34 | message = message = make_fake_message()("/help 测试") 35 | event = make_fake_event(_message=message)() 36 | 37 | ctx.receive_event(bot, event) 38 | ctx.should_call_send( 39 | event, "测试\n\n功能一 # 测试插件子插件一\n功能二 # 测试插件子插件二", True 40 | ) 41 | ctx.should_finished() 42 | -------------------------------------------------------------------------------- /tests/test_tree.py: -------------------------------------------------------------------------------- 1 | from nonebug import App 2 | 3 | from .utils import make_fake_event, make_fake_message 4 | 5 | 6 | async def test_root(app: App): 7 | """测试帮助""" 8 | from nonebot import require 9 | 10 | require("tests.plugins.tree") 11 | from nonebot_plugin_treehelp import help_cmd 12 | 13 | async with app.test_matcher(help_cmd) as ctx: 14 | bot = ctx.create_bot() 15 | message = message = make_fake_message()("/help") 16 | event = make_fake_event(_message=message)() 17 | 18 | ctx.receive_event(bot, event) 19 | ctx.should_call_send( 20 | event, "插件:\n帮助 # 获取插件帮助信息\n测试 # 一个测试插件", True 21 | ) 22 | ctx.should_finished() 23 | 24 | 25 | async def test_sub_plugins(app: App): 26 | """测试帮助""" 27 | from nonebot import require 28 | 29 | require("tests.plugins.tree") 30 | from nonebot_plugin_treehelp import help_cmd 31 | 32 | async with app.test_matcher(help_cmd) as ctx: 33 | bot = ctx.create_bot() 34 | message = message = make_fake_message()("/help 测试") 35 | event = make_fake_event(_message=message)() 36 | 37 | ctx.receive_event(bot, event) 38 | ctx.should_call_send( 39 | event, 40 | "测试\n\n" 41 | "Alconna # 测试插件 alconna 子插件\n" 42 | "复杂功能 # 测试插件复杂子插件\n" 43 | "简单功能 # 测试插件简单子插件", 44 | True, 45 | ) 46 | ctx.should_finished() 47 | 48 | 49 | async def test_tree_view(app: App): 50 | """测试树形结构""" 51 | from nonebot import require 52 | 53 | require("tests.plugins.tree") 54 | from nonebot_plugin_treehelp import help_cmd 55 | 56 | async with app.test_matcher(help_cmd) as ctx: 57 | bot = ctx.create_bot() 58 | message = message = make_fake_message()("/help --tree") 59 | event = make_fake_event(_message=message)() 60 | 61 | ctx.receive_event(bot, event) 62 | ctx.should_call_send( 63 | event, 64 | "插件:\n帮助 # 获取插件帮助信息\n测试 # 一个测试插件\n├── Alconna # 测试插件 alconna 子插件\n├── 复杂功能 # 测试插件复杂子插件\n│ └── 二级功能 # 测试插件二级插件\n└── 简单功能 # 测试插件简单子插件", # noqa: E501 65 | True, 66 | ) 67 | ctx.should_finished() 68 | 69 | async with app.test_matcher(help_cmd) as ctx: 70 | bot = ctx.create_bot() 71 | message = message = make_fake_message()("/help --tree 测试") 72 | event = make_fake_event(_message=message)() 73 | 74 | ctx.receive_event(bot, event) 75 | ctx.should_call_send( 76 | event, 77 | "测试 # 一个测试插件\n├── Alconna # 测试插件 alconna 子插件\n├── 复杂功能 # 测试插件复杂子插件\n│ └── 二级功能 # 测试插件二级插件\n└── 简单功能 # 测试插件简单子插件", # noqa: E501 78 | True, 79 | ) 80 | ctx.should_finished() 81 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from typing import TYPE_CHECKING, Optional 3 | 4 | from pydantic import create_model 5 | 6 | if TYPE_CHECKING: 7 | from nonebot.adapters import Event, Message 8 | 9 | 10 | def make_fake_message() -> type["Message"]: 11 | from nonebot.adapters import Message, MessageSegment 12 | 13 | class FakeMessageSegment(MessageSegment): 14 | @classmethod 15 | def get_message_class(cls): 16 | return FakeMessage 17 | 18 | def __str__(self) -> str: 19 | return self.data["text"] if self.type == "text" else f"[fake:{self.type}]" 20 | 21 | @classmethod 22 | def text(cls, text: str): 23 | return cls("text", {"text": text}) 24 | 25 | @classmethod 26 | def image(cls, url: str): 27 | return cls("image", {"url": url}) 28 | 29 | def is_text(self) -> bool: 30 | return self.type == "text" 31 | 32 | class FakeMessage(Message): 33 | @classmethod 34 | def get_segment_class(cls): 35 | return FakeMessageSegment 36 | 37 | @staticmethod 38 | def _construct(msg: str): 39 | yield FakeMessageSegment.text(msg) 40 | 41 | return FakeMessage 42 | 43 | 44 | def make_fake_event( 45 | _type: str = "message", 46 | _name: str = "test", 47 | _description: str = "test", 48 | _user_id: str = "test", 49 | _session_id: str = "test", 50 | _message: Optional["Message"] = None, 51 | _to_me: bool = True, 52 | **fields, 53 | ) -> type["Event"]: 54 | from nonebot.adapters import Event 55 | 56 | _Fake = create_model("_Fake", __base__=Event, **fields) 57 | 58 | class FakeEvent(_Fake): 59 | def get_type(self) -> str: 60 | return _type 61 | 62 | def get_event_name(self) -> str: 63 | return _name 64 | 65 | def get_event_description(self) -> str: 66 | return _description 67 | 68 | def get_user_id(self) -> str: 69 | return _user_id 70 | 71 | def get_session_id(self) -> str: 72 | return _session_id 73 | 74 | def get_message(self) -> "Message": 75 | if _message is not None: 76 | return _message 77 | raise NotImplementedError 78 | 79 | def is_tome(self) -> bool: 80 | return _to_me 81 | 82 | class Config: 83 | extra = "forbid" 84 | 85 | return FakeEvent 86 | 87 | 88 | def clear_plugins() -> None: 89 | from nonebot.plugin import _managers, _plugins 90 | 91 | for plugin in _plugins.values(): 92 | keys = [key for key in sys.modules if key.startswith(plugin.module_name)] 93 | for key in keys: 94 | del sys.modules[key] 95 | _plugins.clear() 96 | _managers.clear() 97 | --------------------------------------------------------------------------------