[
  {
    "path": ".devcontainer/devcontainer.json",
    "content": "{\n  \"name\": \"Ubuntu\",\n  \"image\": \"mcr.microsoft.com/devcontainers/base:ubuntu\",\n  \"features\": {\n    \"ghcr.io/jsburckhardt/devcontainer-features/uv:1\": {},\n    \"ghcr.io/devcontainers/features/node:1\": {},\n    \"ghcr.io/meaningful-ooo/devcontainer-features/fish:2\": {}\n  },\n  \"postCreateCommand\": \"corepack enable && COREPACK_ENABLE_DOWNLOAD_PROMPT=0 ./scripts/setup-envs.sh\",\n  \"customizations\": {\n    \"vscode\": {\n      \"settings\": {\n        \"python.analysis.diagnosticMode\": \"workspace\",\n        \"[python]\": {\n          \"editor.defaultFormatter\": \"charliermarsh.ruff\",\n          \"editor.codeActionsOnSave\": {\n            \"source.fixAll.ruff\": \"explicit\",\n            \"source.organizeImports\": \"explicit\"\n          }\n        },\n        \"[javascript]\": {\n          \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n        },\n        \"[html]\": {\n          \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n        },\n        \"[typescript]\": {\n          \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n        },\n        \"[javascriptreact]\": {\n          \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n        },\n        \"[typescriptreact]\": {\n          \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n        },\n        \"files.exclude\": {\n          \"**/__pycache__\": true\n        },\n        \"files.watcherExclude\": {\n          \"**/target/**\": true,\n          \"**/__pycache__\": true\n        }\n      },\n      \"extensions\": [\n        \"ms-python.python\",\n        \"ms-python.vscode-pylance\",\n        \"charliermarsh.ruff\",\n        \"EditorConfig.EditorConfig\",\n        \"esbenp.prettier-vscode\",\n        \"bradlc.vscode-tailwindcss\"\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": ".editorconfig",
    "content": "# http://editorconfig.org\nroot = true\n\n[*]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n\n# The JSON files contain newlines inconsistently\n[*.json]\ninsert_final_newline = ignore\n\n# Minified JavaScript files shouldn't be changed\n[**.min.js]\nindent_style = ignore\ninsert_final_newline = ignore\n\n# Makefiles always use tabs for indentation\n[Makefile]\nindent_style = tab\n\n# Batch files use tabs for indentation\n[*.bat]\nindent_style = tab\n\n[*.md]\ntrim_trailing_whitespace = false\n\n# Matches the exact files either package.json or .travis.yml\n[{package.json,.travis.yml}]\nindent_size = 2\n\n[{*.py,*.pyi}]\nindent_size = 4\n"
  },
  {
    "path": ".eslintignore",
    "content": "dist\nnode_modules\n.yarn\n.history\nbuild\nlib\n"
  },
  {
    "path": ".eslintrc.js",
    "content": "const OFF = 0;\nconst WARNING = 1;\nconst ERROR = 2;\n\n// Prevent importing lodash, usually for browser bundle size reasons\nconst LodashImportPatterns = [\"lodash\", \"lodash.**\", \"lodash/**\"];\n\nmodule.exports = {\n  root: true,\n  env: {\n    browser: true,\n    commonjs: true,\n    node: true,\n  },\n  parser: \"@typescript-eslint/parser\",\n  parserOptions: {},\n  globals: {\n    JSX: true,\n  },\n  extends: [\n    \"eslint:recommended\",\n    \"plugin:react-hooks/recommended\",\n    \"airbnb\",\n    \"plugin:@typescript-eslint/recommended\",\n    // 'plugin:@typescript-eslint/recommended-requiring-type-checking',\n    // 'plugin:@typescript-eslint/strict',\n    \"plugin:regexp/recommended\",\n    \"prettier\",\n    \"plugin:@docusaurus/all\",\n  ],\n  settings: {\n    \"import/resolver\": {\n      node: {\n        extensions: [\".js\", \".jsx\", \".ts\", \".tsx\"],\n      },\n    },\n  },\n  reportUnusedDisableDirectives: true,\n  plugins: [\"react-hooks\", \"@typescript-eslint\", \"regexp\", \"@docusaurus\"],\n  rules: {\n    \"react/jsx-uses-react\": OFF, // JSX runtime: automatic\n    \"react/react-in-jsx-scope\": OFF, // JSX runtime: automatic\n    \"array-callback-return\": WARNING,\n    camelcase: WARNING,\n    \"class-methods-use-this\": OFF, // It's a way of allowing private variables.\n    curly: [WARNING, \"all\"],\n    \"global-require\": WARNING,\n    \"lines-between-class-members\": OFF,\n    \"max-classes-per-file\": OFF,\n    \"max-len\": [\n      WARNING,\n      {\n        code: Infinity, // Code width is already enforced by Prettier\n        tabWidth: 2,\n        comments: 80,\n        ignoreUrls: true,\n        ignorePattern: \"(eslint-disable|@)\",\n      },\n    ],\n    \"arrow-body-style\": OFF,\n    \"no-await-in-loop\": OFF,\n    \"no-case-declarations\": WARNING,\n    \"no-console\": OFF,\n    \"no-constant-binary-expression\": ERROR,\n    \"no-continue\": OFF,\n    \"no-control-regex\": WARNING,\n    \"no-else-return\": OFF,\n    \"no-empty\": [WARNING, { allowEmptyCatch: true }],\n    \"no-lonely-if\": WARNING,\n    \"no-nested-ternary\": WARNING,\n    \"no-param-reassign\": [WARNING, { props: false }],\n    \"no-prototype-builtins\": WARNING,\n    \"no-restricted-exports\": OFF,\n    \"no-restricted-properties\": [\n      ERROR,\n      .../** @type {[string, string][]} */ ([\n        // TODO: TS doesn't make Boolean a narrowing function yet,\n        // so filter(Boolean) is problematic type-wise\n        // ['compact', 'Array#filter(Boolean)'],\n        [\"concat\", \"Array#concat\"],\n        [\"drop\", \"Array#slice(n)\"],\n        [\"dropRight\", \"Array#slice(0, -n)\"],\n        [\"fill\", \"Array#fill\"],\n        [\"filter\", \"Array#filter\"],\n        [\"find\", \"Array#find\"],\n        [\"findIndex\", \"Array#findIndex\"],\n        [\"first\", \"foo[0]\"],\n        [\"flatten\", \"Array#flat\"],\n        [\"flattenDeep\", \"Array#flat(Infinity)\"],\n        [\"flatMap\", \"Array#flatMap\"],\n        [\"fromPairs\", \"Object.fromEntries\"],\n        [\"head\", \"foo[0]\"],\n        [\"indexOf\", \"Array#indexOf\"],\n        [\"initial\", \"Array#slice(0, -1)\"],\n        [\"join\", \"Array#join\"],\n        // Unfortunately there's no great alternative to _.last yet\n        // Candidates: foo.slice(-1)[0]; foo[foo.length - 1]\n        // Array#at is ES2022; could replace _.nth as well\n        // ['last'],\n        [\"map\", \"Array#map\"],\n        [\"reduce\", \"Array#reduce\"],\n        [\"reverse\", \"Array#reverse\"],\n        [\"slice\", \"Array#slice\"],\n        [\"take\", \"Array#slice(0, n)\"],\n        [\"takeRight\", \"Array#slice(-n)\"],\n        [\"tail\", \"Array#slice(1)\"],\n      ]).map(([property, alternative]) => ({\n        object: \"_\",\n        property,\n        message: `Use ${alternative} instead.`,\n      })),\n      ...[\n        \"readdirSync\",\n        \"readFileSync\",\n        \"statSync\",\n        \"lstatSync\",\n        \"existsSync\",\n        \"pathExistsSync\",\n        \"realpathSync\",\n        \"mkdirSync\",\n        \"mkdirpSync\",\n        \"mkdirsSync\",\n        \"writeFileSync\",\n        \"writeJsonSync\",\n        \"outputFileSync\",\n        \"outputJsonSync\",\n        \"moveSync\",\n        \"copySync\",\n        \"copyFileSync\",\n        \"ensureFileSync\",\n        \"ensureDirSync\",\n        \"ensureLinkSync\",\n        \"ensureSymlinkSync\",\n        \"unlinkSync\",\n        \"removeSync\",\n        \"emptyDirSync\",\n      ].map((property) => ({\n        object: \"fs\",\n        property,\n        message: \"Do not use sync fs methods.\",\n      })),\n    ],\n    \"no-restricted-syntax\": [\n      WARNING,\n      // Copied from airbnb, removed for...of statement, added export all\n      {\n        selector: \"ForInStatement\",\n        message:\n          \"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.\",\n      },\n      {\n        selector: \"LabeledStatement\",\n        message:\n          \"Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand.\",\n      },\n      {\n        selector: \"WithStatement\",\n        message:\n          \"`with` is disallowed in strict mode because it makes code impossible to predict and optimize.\",\n      },\n      {\n        selector: \"ExportAllDeclaration\",\n        message:\n          \"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.\",\n      },\n      // TODO make an internal plugin to ensure this\n      // {\n      //   selector:\n      // @   'ExportDefaultDeclaration > Identifier, ExportNamedDeclaration[source=null] > ExportSpecifier',\n      //   message: 'Export in one statement'\n      // },\n      ...[\"path\", \"fs-extra\", \"webpack\", \"lodash\"].map((m) => ({\n        selector: `ImportDeclaration[importKind=value]:has(Literal[value=${m}]) > ImportSpecifier[importKind=value]`,\n        message:\n          \"Default-import this, both for readability and interoperability with ESM\",\n      })),\n    ],\n    \"no-template-curly-in-string\": WARNING,\n    \"no-unused-expressions\": [\n      WARNING,\n      { allowTaggedTemplates: true, allowShortCircuit: true },\n    ],\n    \"no-useless-escape\": WARNING,\n    \"no-void\": [ERROR, { allowAsStatement: true }],\n    \"prefer-destructuring\": WARNING,\n    \"prefer-named-capture-group\": WARNING,\n    \"prefer-template\": WARNING,\n    yoda: WARNING,\n\n    \"import/extensions\": OFF,\n    // This rule doesn't yet support resolving .js imports when the actual file\n    // is .ts. Plus it's not all that useful when our code is fully TS-covered.\n    \"import/no-unresolved\": [\n      OFF,\n      {\n        // Ignore certain webpack aliases because they can't be resolved\n        ignore: [\n          \"^@theme\",\n          \"^@docusaurus\",\n          \"^@generated\",\n          \"^@site\",\n          \"^@testing-utils\",\n        ],\n      },\n    ],\n    \"import/order\": [\n      WARNING,\n      {\n        groups: [\n          \"builtin\",\n          \"external\",\n          \"internal\",\n          [\"parent\", \"sibling\", \"index\"],\n          \"type\",\n        ],\n        \"newlines-between\": \"always\",\n        pathGroups: [\n          // always put css import to the last, ref:\n          // https://github.com/import-js/eslint-plugin-import/issues/1239\n          {\n            pattern: \"*.+(css|sass|less|scss|pcss|styl)\",\n            group: \"unknown\",\n            patternOptions: { matchBase: true },\n            position: \"after\",\n          },\n          { pattern: \"react\", group: \"builtin\", position: \"before\" },\n          { pattern: \"react-dom\", group: \"builtin\", position: \"before\" },\n          { pattern: \"react-dom/**\", group: \"builtin\", position: \"before\" },\n          { pattern: \"stream\", group: \"builtin\", position: \"before\" },\n          { pattern: \"fs-extra\", group: \"builtin\" },\n          { pattern: \"lodash\", group: \"external\", position: \"before\" },\n          { pattern: \"clsx\", group: \"external\", position: \"before\" },\n          // 'Bit weird to not use the `import/internal-regex` option, but this\n          // way, we can make `import type { Props } from \"@theme/*\"` appear\n          // before `import styles from \"styles.module.css\"`, which is what we\n          // always did. This should be removable once we stop using ambient\n          // module declarations for theme aliases.\n          { pattern: \"@theme/**\", group: \"internal\" },\n          { pattern: \"@site/**\", group: \"internal\" },\n          { pattern: \"@theme-init/**\", group: \"internal\" },\n          { pattern: \"@theme-original/**\", group: \"internal\" },\n          { pattern: \"@/components/**\", group: \"internal\" },\n          { pattern: \"@/libs/**\", group: \"internal\" },\n          { pattern: \"@/types/**\", group: \"type\" },\n        ],\n        pathGroupsExcludedImportTypes: [],\n        // example: let `import './nprogress.css';` after importing others\n        // in `packages/docusaurus-theme-classic/src/nprogress.ts`\n        // see more: https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/order.md#warnonunassignedimports-truefalse\n        warnOnUnassignedImports: true,\n      },\n    ],\n    \"import/prefer-default-export\": OFF,\n\n    \"jsx-a11y/click-events-have-key-events\": WARNING,\n    \"jsx-a11y/no-noninteractive-element-interactions\": WARNING,\n    \"jsx-a11y/html-has-lang\": OFF,\n\n    \"react-hooks/rules-of-hooks\": ERROR,\n    \"react-hooks/exhaustive-deps\": ERROR,\n\n    // Sometimes we do need the props as a whole, e.g. when spreading\n    \"react/destructuring-assignment\": OFF,\n    \"react/function-component-definition\": [\n      WARNING,\n      {\n        namedComponents: \"function-declaration\",\n        unnamedComponents: \"arrow-function\",\n      },\n    ],\n    \"react/jsx-filename-extension\": OFF,\n    \"react/jsx-key\": [ERROR, { checkFragmentShorthand: true }],\n    \"react/jsx-no-useless-fragment\": [ERROR, { allowExpressions: true }],\n    \"react/jsx-props-no-spreading\": OFF,\n    \"react/no-array-index-key\": OFF, // We build a static site, and nearly all components don't change.\n    \"react/no-unstable-nested-components\": [WARNING, { allowAsProps: true }],\n    \"react/prefer-stateless-function\": WARNING,\n    \"react/prop-types\": OFF,\n    \"react/require-default-props\": [\n      ERROR,\n      { ignoreFunctionalComponents: true },\n    ],\n\n    \"@typescript-eslint/consistent-type-definitions\": OFF,\n    \"@typescript-eslint/require-await\": OFF,\n\n    \"@typescript-eslint/ban-ts-comment\": [\n      ERROR,\n      { \"ts-expect-error\": \"allow-with-description\" },\n    ],\n    \"@typescript-eslint/consistent-indexed-object-style\": OFF,\n    \"@typescript-eslint/consistent-type-imports\": [\n      WARNING,\n      { disallowTypeAnnotations: false },\n    ],\n    \"@typescript-eslint/explicit-module-boundary-types\": WARNING,\n    \"@typescript-eslint/method-signature-style\": ERROR,\n    \"@typescript-eslint/no-empty-function\": OFF,\n    \"@typescript-eslint/no-empty-interface\": [\n      ERROR,\n      {\n        allowSingleExtends: true,\n      },\n    ],\n    \"@typescript-eslint/no-inferrable-types\": OFF,\n    \"@typescript-eslint/no-namespace\": [WARNING, { allowDeclarations: true }],\n    \"no-use-before-define\": OFF,\n    \"@typescript-eslint/no-use-before-define\": [\n      ERROR,\n      { functions: false, classes: false, variables: true },\n    ],\n    \"@typescript-eslint/no-non-null-assertion\": OFF,\n    \"no-redeclare\": OFF,\n    \"@typescript-eslint/no-redeclare\": ERROR,\n    \"no-shadow\": OFF,\n    \"@typescript-eslint/no-shadow\": ERROR,\n    \"no-unused-vars\": OFF,\n    // We don't provide any escape hatches for this rule. Rest siblings and\n    // function placeholder params are always ignored, and any other unused\n    // locals must be justified with a disable comment.\n    \"@typescript-eslint/no-unused-vars\": [ERROR, { ignoreRestSiblings: true }],\n    \"@typescript-eslint/prefer-optional-chain\": ERROR,\n    \"@docusaurus/no-html-links\": ERROR,\n    \"@docusaurus/prefer-docusaurus-heading\": ERROR,\n    \"@docusaurus/no-untranslated-text\": [\n      WARNING,\n      {\n        ignoredStrings: [\n          \"·\",\n          \"-\",\n          \"—\",\n          \"×\",\n          \"​\", // zwj: &#8203;\n          \"@\",\n          \"WebContainers\",\n          \"Twitter\",\n          \"GitHub\",\n          \"Dev.to\",\n          \"1.x\",\n        ],\n      },\n    ],\n  },\n  overrides: [\n    {\n      files: [\"packages/*/src/theme/**/*.{js,ts,tsx}\"],\n      excludedFiles: \"*.test.{js,ts,tsx}\",\n      rules: {\n        \"no-restricted-imports\": [\n          \"error\",\n          {\n            patterns: LodashImportPatterns.concat(\n              // Prevents relative imports between React theme components\n              [\n                \"../**\",\n                \"./**\",\n                // Allows relative styles module import with consistent filename\n                \"!./styles.module.css\",\n                // Allows relative tailwind css import with consistent filename\n                \"!./styles.css\",\n              ]\n            ),\n          },\n        ],\n      },\n    },\n    {\n      files: [\"packages/*/src/theme/**/*.{js,ts,tsx}\"],\n      rules: {\n        \"import/no-named-export\": ERROR,\n      },\n    },\n    {\n      files: [\"*.d.ts\"],\n      rules: {\n        \"import/no-duplicates\": OFF,\n      },\n    },\n    {\n      files: [\"*.{ts,tsx}\"],\n      rules: {\n        \"no-undef\": OFF,\n        \"import/no-import-module-exports\": OFF,\n      },\n    },\n    {\n      files: [\"*.{js,mjs,cjs}\"],\n      rules: {\n        // Make JS code directly runnable in Node.\n        \"@typescript-eslint/no-var-requires\": OFF,\n        \"@typescript-eslint/explicit-module-boundary-types\": OFF,\n      },\n    },\n    {\n      // Internal files where extraneous deps don't matter much at long as\n      // they run\n      files: [\"website/**\"],\n      rules: {\n        \"import/no-extraneous-dependencies\": OFF,\n      },\n    },\n  ],\n};\n"
  },
  {
    "path": ".gitattributes",
    "content": "website/versioned_*/** linguist-documentation\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "open_collective: nonebot\ncustom: [\"https://afdian.com/@nonebot\"]\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/adapter_publish.yml",
    "content": "name: 发布适配器\ntitle: \"Adapter: {name}\"\ndescription: 发布适配器到 NoneBot 官方商店\nlabels: [\"Adapter\", \"Publish\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        # 发布须知\n\n        非特殊情况下，请通过 [NoneBot 适配器商店](https://nonebot.dev/store/adapters) 的发布表单进行插件发布信息填写。\n\n  - type: input\n    id: name\n    attributes:\n      label: 适配器名称\n      description: 适配器名称\n    validations:\n      required: true\n\n  - type: input\n    id: description\n    attributes:\n      label: 适配器描述\n      description: 适配器描述\n    validations:\n      required: true\n\n  - type: input\n    id: pypi\n    attributes:\n      label: PyPI 项目名\n      description: PyPI 项目名\n      placeholder: e.g. nonebot-adapter-xxx\n    validations:\n      required: true\n\n  - type: input\n    id: module\n    attributes:\n      label: 适配器 import 包名\n      description: 适配器 import 包名\n      placeholder: e.g. nonebot_adapter_xxx\n    validations:\n      required: true\n\n  - type: input\n    id: homepage\n    attributes:\n      label: 适配器项目仓库/主页链接\n      description: 适配器项目仓库/主页链接\n      placeholder: e.g. https://github.com/xxx/xxx\n    validations:\n      required: true\n\n  - type: input\n    id: tags\n    attributes:\n      label: 标签\n      description: 标签\n      placeholder: 'e.g. [{\"label\": \"标签名\", \"color\": \"#ea5252\"}]'\n      value: \"[]\"\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bot_publish.yml",
    "content": "name: 发布机器人\ntitle: \"Bot: {name}\"\ndescription: 发布机器人到 NoneBot 官方商店\nlabels: [\"Bot\", \"Publish\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        # 发布须知\n\n        非特殊情况下，请通过 [NoneBot 机器人商店](https://nonebot.dev/store/bots) 的发布表单进行插件发布信息填写。\n\n  - type: input\n    id: name\n    attributes:\n      label: 机器人名称\n      description: 机器人名称\n    validations:\n      required: true\n\n  - type: input\n    id: description\n    attributes:\n      label: 机器人描述\n      description: 机器人描述\n    validations:\n      required: true\n\n  - type: input\n    id: homepage\n    attributes:\n      label: 机器人项目仓库/主页链接\n      description: 机器人项目仓库/主页链接\n      placeholder: e.g. https://github.com/xxx/xxx\n\n  - type: input\n    id: tags\n    attributes:\n      label: 标签\n      description: 标签\n      placeholder: 'e.g. [{\"label\": \"标签名\", \"color\": \"#ea5252\"}]'\n      value: \"[]\"\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: Bug 反馈\ntitle: \"Bug: 出现异常\"\ndescription: 提交 Bug 反馈以帮助我们改进代码\nlabels: [\"bug\"]\nbody:\n  - type: dropdown\n    id: env-os\n    attributes:\n      label: 操作系统\n      description: 选择运行 NoneBot 的系统\n      options:\n        - Windows\n        - MacOS\n        - Linux\n        - Other\n    validations:\n      required: true\n\n  - type: input\n    id: env-python-ver\n    attributes:\n      label: Python 版本\n      description: 填写运行 NoneBot 的 Python 版本\n      placeholder: e.g. 3.11.0\n    validations:\n      required: true\n\n  - type: input\n    id: env-nb-ver\n    attributes:\n      label: NoneBot 版本\n      description: 填写 NoneBot 版本\n      placeholder: e.g. 2.0.0\n    validations:\n      required: true\n\n  - type: input\n    id: env-adapter\n    attributes:\n      label: 适配器\n      description: 填写使用的适配器以及版本\n      placeholder: e.g. OneBot v11 2.2.2\n    validations:\n      required: true\n\n  - type: input\n    id: env-protocol\n    attributes:\n      label: 协议端\n      description: 填写连接 NoneBot 的协议端及版本\n      placeholder: e.g. go-cqhttp 1.0.0\n    validations:\n      required: true\n\n  - type: textarea\n    id: describe\n    attributes:\n      label: 描述问题\n      description: 清晰简洁地说明问题是什么\n    validations:\n      required: true\n\n  - type: textarea\n    id: reproduction\n    attributes:\n      label: 复现步骤\n      description: 提供能复现此问题的详细操作步骤\n      placeholder: |\n        1. 首先……\n        2. 然后……\n        3. 发生……\n    validations:\n      required: true\n\n  - type: textarea\n    id: expected\n    attributes:\n      label: 期望的结果\n      description: 清晰简洁地描述你期望发生的事情\n\n  - type: textarea\n    id: logs\n    attributes:\n      label: 截图或日志\n      description: 提供有助于诊断问题的任何日志和截图\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: NoneBot 论坛\n    url: https://discussions.nonebot.dev/\n    about: 前往 NoneBot 论坛提问\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/document.yml",
    "content": "name: 文档改进\ntitle: \"Docs: 描述\"\ndescription: 文档错误及改进意见反馈\nlabels: [\"documentation\"]\nbody:\n  - type: textarea\n    id: problem\n    attributes:\n      label: 描述问题或主题\n    validations:\n      required: true\n\n  - type: textarea\n    id: improve\n    attributes:\n      label: 需做出的修改\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "content": "name: 功能建议\ntitle: \"Feature: 功能描述\"\ndescription: 提出关于项目新功能的想法\nlabels: [\"enhancement\"]\nbody:\n  - type: textarea\n    id: problem\n    attributes:\n      label: 希望能解决的问题\n      description: 在使用中遇到什么问题而需要新的功能？\n    validations:\n      required: true\n\n  - type: textarea\n    id: feature\n    attributes:\n      label: 描述所需要的功能\n      description: 请说明需要的功能或解决方法\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/plugin_publish.yml",
    "content": "name: 发布插件\ntitle: \"Plugin: {name}\"\ndescription: 发布插件到 NoneBot 官方商店\nlabels: [\"Plugin\", \"Publish\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        # 发布须知\n\n        非特殊情况下，请通过 [NoneBot 插件商店](https://nonebot.dev/store/plugins) 的发布表单进行插件发布信息填写。\n        在发布前请阅读 [NoneBot 插件发布流程指导](https://nonebot.dev/docs/developer/plugin-publishing) 并确保满足其中所述条件。\n\n  - type: input\n    id: pypi\n    attributes:\n      label: PyPI 项目名\n      description: PyPI 项目名\n      placeholder: e.g. nonebot-plugin-xxx\n    validations:\n      required: true\n\n  - type: input\n    id: module\n    attributes:\n      label: 插件模块名\n      description: 加载插件时所使用的模块名称\n      placeholder: e.g. nonebot_plugin_apscheduler\n    validations:\n      required: true\n\n  - type: input\n    id: tags\n    attributes:\n      label: 标签\n      description: 标签\n      placeholder: 'e.g. [{\"label\": \"标签名\", \"color\": \"#ea5252\"}]'\n      value: \"[]\"\n    validations:\n      required: true\n\n  - type: textarea\n    id: config\n    attributes:\n      label: 插件配置项\n      description: 插件配置项\n      render: dotenv\n      placeholder: |\n        # e.g.\n        # KEY=VALUE\n        # KEY2=VALUE2\n"
  },
  {
    "path": ".github/actions/build-api-doc/action.yml",
    "content": "name: Build API Doc\ndescription: Build API Doc\n\nruns:\n  using: \"composite\"\n  steps:\n    - run: |\n        uv run --no-sync bash ./scripts/build-api-docs.sh\n      shell: bash\n"
  },
  {
    "path": ".github/actions/setup-node/action.yml",
    "content": "name: Setup Node\ndescription: Setup Node\n\nruns:\n  using: \"composite\"\n  steps:\n    - uses: actions/setup-node@v6\n      with:\n        node-version: lts/*\n        cache: yarn\n\n    - run: yarn install --frozen-lockfile\n      shell: bash\n"
  },
  {
    "path": ".github/actions/setup-python/action.yml",
    "content": "name: Setup Python\ndescription: Setup Python\n\ninputs:\n  python-version:\n    description: Python version\n    required: false\n    default: \"3.12\"\n  env-group:\n    description: Environment group\n    required: false\n    default: \"pydantic-v2\"\n\nruns:\n  using: \"composite\"\n  steps:\n    - uses: astral-sh/setup-uv@v7\n      with:\n        python-version: ${{ inputs.python-version }}\n        cache-suffix: ${{ inputs.env-group }}\n\n    - run: |\n        uv sync --all-extras --locked --group ${{ inputs.env-group }}\n      shell: bash\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: github-actions\n    directory: \"/\"\n    schedule:\n      interval: daily\n    groups:\n      actions:\n        patterns:\n          - \"*\"\n\n  - package-ecosystem: github-actions\n    directory: \"/.github/actions/build-api-doc\"\n    schedule:\n      interval: daily\n    groups:\n      actions:\n        patterns:\n          - \"*\"\n\n  - package-ecosystem: github-actions\n    directory: \"/.github/actions/setup-node\"\n    schedule:\n      interval: daily\n    groups:\n      actions:\n        patterns:\n          - \"*\"\n\n  - package-ecosystem: github-actions\n    directory: \"/.github/actions/setup-python\"\n    schedule:\n      interval: daily\n    groups:\n      actions:\n        patterns:\n          - \"*\"\n\n  - package-ecosystem: devcontainers\n    directory: \"/\"\n    schedule:\n      interval: daily\n    groups:\n      devcontainers:\n        patterns:\n          - \"*\"\n"
  },
  {
    "path": ".github/release-drafter.yml",
    "content": "template: $CHANGES\ncategory-template: \"### $TITLE\"\nname-template: \"Release v$RESOLVED_VERSION 🌈\"\ntag-template: \"v$RESOLVED_VERSION\"\nchange-template: \"- $TITLE [@$AUTHOR](https://github.com/$AUTHOR) ([#$NUMBER]($URL))\"\nchange-title-escapes: '\\<&'\nexclude-labels:\n  - \"dependencies\"\n  - \"skip-changelog\"\ncategories:\n  - title: \"💥 破坏性变更\"\n    labels:\n      - \"Breaking\"\n  - title: \"🚀 新功能\"\n    labels:\n      - \"feature\"\n      - \"enhancement\"\n  - title: \"🐛 Bug 修复\"\n    labels:\n      - \"fix\"\n      - \"bugfix\"\n      - \"bug\"\n  - title: \"📝 文档\"\n    labels:\n      - \"documentation\"\n  - title: \"💫 杂项\"\n  - title: \"🍻 插件发布\"\n    label: \"Plugin\"\n  - title: \"🍻 机器人发布\"\n    label: \"Bot\"\n  - title: \"🍻 适配器发布\"\n    label: \"Adapter\"\nversion-resolver:\n  major:\n    labels:\n      - \"major\"\n  minor:\n    labels:\n      - \"minor\"\n  patch:\n    labels:\n      - \"patch\"\n  default: patch\n"
  },
  {
    "path": ".github/workflows/codecov.yml",
    "content": "name: Code Coverage\n\non:\n  push:\n    branches:\n      - master\n  pull_request:\n    paths:\n      - \"envs/**\"\n      - \"nonebot/**\"\n      - \"packages/**\"\n      - \"tests/**\"\n      - \".github/actions/setup-python/**\"\n      - \".github/workflows/codecov.yml\"\n      - \"pyproject.toml\"\n      - \"uv.lock\"\n\njobs:\n  test:\n    name: Test Coverage\n    runs-on: ${{ matrix.os }}\n    concurrency:\n      group: test-coverage-${{ github.ref }}-${{ matrix.os }}-${{ matrix.python-version }}-${{ matrix.env }}\n      cancel-in-progress: true\n    strategy:\n      fail-fast: false\n      matrix:\n        python-version: [\"3.10\", \"3.11\", \"3.12\", \"3.13\"]\n        os: [ubuntu-latest, windows-latest, macos-latest]\n        env: [pydantic-v1, pydantic-v2]\n    env:\n      OS: ${{ matrix.os }}\n      PYTHON_VERSION: ${{ matrix.python-version }}\n      PYDANTIC_VERSION: ${{ matrix.env }}\n\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Setup Python environment\n        uses: ./.github/actions/setup-python\n        with:\n          python-version: ${{ matrix.python-version }}\n          env-group: ${{ matrix.env }}\n\n      - name: Run Pytest\n        run: |\n          uv run --no-sync bash ./scripts/run-tests.sh\n\n      - name: Upload test results\n        uses: codecov/test-results-action@v1\n        with:\n          env_vars: OS,PYTHON_VERSION,PYDANTIC_VERSION\n          files: ./tests/junit.xml\n          flags: unittests\n        env:\n          CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}\n\n      - name: Upload coverage report\n        uses: codecov/codecov-action@v5\n        with:\n          env_vars: OS,PYTHON_VERSION,PYDANTIC_VERSION\n          files: ./tests/coverage.xml\n          flags: unittests\n          fail_ci_if_error: true\n        env:\n          CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/noneflow.yml",
    "content": "name: NoneFlow\n\non:\n  issues:\n    types: [opened, reopened, edited]\n  issue_comment:\n    types: [created]\n  pull_request_target:\n    types: [closed]\n  pull_request_review:\n    types: [submitted]\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.event.issue.number && format('publish/issue{0}', github.event.issue.number) || github.head_ref || github.run_id }}\n  cancel-in-progress: ${{ startsWith(github.head_ref, 'publish/issue')}}\n\njobs:\n  noneflow:\n    runs-on: ubuntu-latest\n    name: noneflow\n    # do not run on forked PRs, do not run on not related issues, do not run on pr comments\n    if: |\n      !(\n        (\n          github.event.pull_request &&\n          (\n            github.event.pull_request.head.repo.fork ||\n            !(\n              contains(github.event.pull_request.labels.*.name, 'Plugin') ||\n              contains(github.event.pull_request.labels.*.name, 'Adapter') ||\n              contains(github.event.pull_request.labels.*.name, 'Bot')\n            )\n          )\n        ) ||\n        (\n          github.event_name == 'issue_comment' && github.event.issue.pull_request\n        )\n      )\n    steps:\n      - name: Generate token\n        id: generate-token\n        uses: tibdex/github-app-token@v2\n        with:\n          app_id: ${{ secrets.APP_ID }}\n          private_key: ${{ secrets.APP_KEY }}\n\n      - name: Checkout Code\n        uses: actions/checkout@v6\n        with:\n          token: ${{ steps.generate-token.outputs.token }}\n\n      - name: NoneFlow\n        uses: docker://ghcr.io/nonebot/noneflow:latest\n        with:\n          config: >\n            {\n              \"base\": \"master\",\n              \"plugin_path\": \"assets/plugins.json5\",\n              \"bot_path\": \"assets/bots.json5\",\n              \"adapter_path\": \"assets/adapters.json5\",\n              \"registry_repository\": \"nonebot/registry\",\n              \"artifact_path\": \"artifact\"\n            }\n        env:\n          APP_ID: ${{ secrets.APP_ID }}\n          PRIVATE_KEY: ${{ secrets.APP_KEY }}\n\n      - name: Upload Artifact\n        uses: actions/upload-artifact@v7\n        with:\n          name: noneflow\n          path: artifact/*\n          if-no-files-found: ignore\n"
  },
  {
    "path": ".github/workflows/pyright.yml",
    "content": "name: Pyright Lint\n\non:\n  push:\n    branches:\n      - master\n  pull_request:\n    paths:\n      - \"envs/**\"\n      - \"nonebot/**\"\n      - \"packages/**\"\n      - \"tests/**\"\n      - \".github/actions/setup-python/**\"\n      - \".github/workflows/pyright.yml\"\n      - \"pyproject.toml\"\n      - \"uv.lock\"\n\njobs:\n  pyright:\n    name: Pyright Lint\n    runs-on: ubuntu-latest\n    concurrency:\n      group: pyright-${{ github.ref }}-${{ matrix.env }}\n      cancel-in-progress: true\n    strategy:\n      matrix:\n        env: [pydantic-v1, pydantic-v2]\n      fail-fast: false\n\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Setup Python environment\n        uses: ./.github/actions/setup-python\n        with:\n          env-group: ${{ matrix.env }}\n\n      - run: |\n          echo \"$(dirname $(uv python find))\" >> $GITHUB_PATH\n          if [ \"${{ matrix.env }}\" = \"pydantic-v1\" ]; then\n            sed -i 's/PYDANTIC_V2 = true/PYDANTIC_V2 = false/g' ./pyproject.toml\n          fi\n        shell: bash\n\n      - name: Run Pyright\n        uses: jakebailey/pyright-action@v3\n        with:\n          pylance-version: latest-release\n"
  },
  {
    "path": ".github/workflows/release-drafter.yml",
    "content": "name: Release Drafter\n\non:\n  push:\n    tags:\n      - v*\n  pull_request_target:\n    branches:\n      - master\n    types:\n      - closed\n\njobs:\n  update-release-draft:\n    if: github.event_name == 'pull_request_target'\n    runs-on: ubuntu-latest\n    concurrency:\n      group: pull-request-changelog\n      cancel-in-progress: true\n    steps:\n      - name: Generate token\n        id: generate-token\n        uses: tibdex/github-app-token@v2\n        with:\n          app_id: ${{ secrets.APP_ID }}\n          private_key: ${{ secrets.APP_KEY }}\n\n      - uses: actions/checkout@v6\n        with:\n          token: ${{ steps.generate-token.outputs.token }}\n\n      - name: Setup Node Environment\n        uses: ./.github/actions/setup-node\n\n      - uses: release-drafter/release-drafter@v6.0.0\n        id: release-drafter\n        env:\n          GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}\n\n      - name: Update Changelog\n        uses: docker://ghcr.io/nonebot/auto-changelog:master\n        with:\n          changelog_file: website/src/changelog/changelog.md\n          latest_changes_position: '# 更新日志\\n\\n'\n          latest_changes_title: \"## 最近更新\"\n          replace_regex: '(?<=## 最近更新\\n)[\\s\\S]*?(?=\\n## )'\n          changelog_body: ${{ steps.release-drafter.outputs.body }}\n          commit_and_push: false\n\n      - name: Commit and Push\n        run: |\n          yarn prettier\n          git config user.name noneflow[bot]\n          git config user.email 129742071+noneflow[bot]@users.noreply.github.com\n          git add .\n          git diff-index --quiet HEAD || git commit -m \":memo: Update changelog\"\n          git push\n\n  release:\n    if: startsWith(github.ref, 'refs/tags/')\n    runs-on: ubuntu-latest\n    permissions:\n      id-token: write\n      contents: write\n    steps:\n      - name: Generate token\n        id: generate-token\n        uses: tibdex/github-app-token@v2\n        with:\n          app_id: ${{ secrets.APP_ID }}\n          private_key: ${{ secrets.APP_KEY }}\n\n      - uses: actions/checkout@v6\n\n      - name: Setup Python Environment\n        uses: ./.github/actions/setup-python\n\n      - name: Setup Node Environment\n        uses: ./.github/actions/setup-node\n\n      - name: Build API Doc\n        uses: ./.github/actions/build-api-doc\n\n      - name: Get Version\n        id: version\n        run: |\n          echo \"VERSION=$(uv version --short)\" >> $GITHUB_OUTPUT\n          echo \"TAG_VERSION=${GITHUB_REF#refs/tags/v}\" >> $GITHUB_OUTPUT\n          echo \"TAG_NAME=${GITHUB_REF#refs/tags/}\" >> $GITHUB_OUTPUT\n\n      - name: Check Version\n        if: steps.version.outputs.VERSION != steps.version.outputs.TAG_VERSION\n        run: exit 1\n\n      - uses: release-drafter/release-drafter@v6.0.0\n        with:\n          name: Release ${{ steps.version.outputs.TAG_NAME }} 🌈\n          tag: ${{ steps.version.outputs.TAG_NAME }}\n          publish: true\n        env:\n          GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}\n\n      - name: Build Package\n        run: |\n          uv build\n          uv publish\n\n      - name: Publish package to GitHub\n        run: |\n          gh release upload --clobber ${{ steps.version.outputs.TAG_NAME }} dist/*.tar.gz dist/*.whl\n        env:\n          GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}\n\n      - name: Build and Publish Doc Package\n        run: |\n          yarn build:plugin --out-dir ../packages/nonebot-plugin-docs/nonebot_plugin_docs/dist\n          cd packages/nonebot-plugin-docs/\n          uv version ${{ steps.version.outputs.VERSION }}\n          uv build\n          uv publish\n\n      - name: Publish Doc Package to GitHub\n        run: |\n          cd packages/nonebot-plugin-docs/\n          gh release upload --clobber ${{ steps.version.outputs.TAG_NAME }} dist/*.tar.gz dist/*.whl\n        env:\n          GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\n\non:\n  workflow_dispatch:\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Generate token\n        id: generate-token\n        uses: tibdex/github-app-token@v2\n        with:\n          app_id: ${{ secrets.APP_ID }}\n          private_key: ${{ secrets.APP_KEY }}\n\n      - uses: actions/checkout@v6\n        with:\n          token: ${{ steps.generate-token.outputs.token }}\n\n      - name: Setup Python Environment\n        uses: ./.github/actions/setup-python\n\n      - name: Setup Node Environment\n        uses: ./.github/actions/setup-node\n\n      - name: Build API Doc\n        uses: ./.github/actions/build-api-doc\n\n      - run: echo \"TAG_NAME=v$(uv version --short)\" >> $GITHUB_ENV\n\n      - name: Archive Changelog\n        uses: docker://ghcr.io/nonebot/auto-changelog:master\n        with:\n          changelog_file: website/src/changelog/changelog.md\n          archive_regex: '(?<=## )最近更新(?=\\n)'\n          archive_title: ${{ env.TAG_NAME }}\n          commit_and_push: false\n\n      - name: Archive Files\n        run: |\n          yarn archive $(uv version --short)\n          yarn prettier\n\n      - name: Push Tag\n        run: |\n          git config user.name noneflow[bot]\n          git config user.email 129742071+noneflow[bot]@users.noreply.github.com\n          git add .\n          git commit -m \":bookmark: Release $(uv version --short)\"\n          git tag ${{ env.TAG_NAME }}\n          git push && git push --tags\n"
  },
  {
    "path": ".github/workflows/ruff.yml",
    "content": "name: Ruff Lint\n\non:\n  push:\n    branches:\n      - master\n  pull_request:\n    paths:\n      - \"envs/**\"\n      - \"nonebot/**\"\n      - \"packages/**\"\n      - \"tests/**\"\n      - \".github/actions/setup-python/**\"\n      - \".github/workflows/ruff.yml\"\n      - \"pyproject.toml\"\n      - \"uv.lock\"\n\njobs:\n  ruff:\n    name: Ruff Lint\n    runs-on: ubuntu-latest\n    concurrency:\n      group: ruff-${{ github.ref }}\n      cancel-in-progress: true\n\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Run Ruff Lint\n        uses: astral-sh/ruff-action@v3\n"
  },
  {
    "path": ".github/workflows/website-deploy.yml",
    "content": "name: Site Deploy\n\non:\n  push:\n    branches:\n      - master\n\njobs:\n  publish:\n    runs-on: ubuntu-latest\n    concurrency:\n      group: website-deploy-${{ github.ref }}\n      cancel-in-progress: true\n\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      - name: Setup Python Environment\n        uses: ./.github/actions/setup-python\n\n      - name: Setup Node Environment\n        uses: ./.github/actions/setup-node\n\n      - name: Build API Doc\n        uses: ./.github/actions/build-api-doc\n\n      - name: Build Doc\n        run: yarn build\n\n      - name: Get Branch Name\n        run: echo \"BRANCH_NAME=$(echo ${GITHUB_REF#refs/heads/})\" >> $GITHUB_ENV\n\n      - name: Deploy to Netlify\n        uses: nwtgck/actions-netlify@v3\n        with:\n          publish-dir: \"./website/build\"\n          production-deploy: true\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n          deploy-message: \"Deploy ${{ env.BRANCH_NAME }}@${{ github.sha }}\"\n          enable-commit-comment: false\n          alias: ${{ env.BRANCH_NAME }}\n        env:\n          NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}\n          NETLIFY_SITE_ID: ${{ secrets.SITE_ID }}\n"
  },
  {
    "path": ".github/workflows/website-preview-cd.yml",
    "content": "name: Site Deploy (Preview CD)\n\non:\n  workflow_run:\n    workflows: [\"Site Deploy (Preview CI)\"]\n    types:\n      - completed\n\njobs:\n  preview-cd:\n    runs-on: ubuntu-latest\n    concurrency:\n      group: pull-request-preview-${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }}\n      cancel-in-progress: true\n\n    if: ${{ github.event.workflow_run.conclusion == 'success' }}\n\n    environment: pull request\n\n    permissions:\n      actions: read\n      statuses: write\n      pull-requests: write\n\n    steps:\n      - name: Set Commit Status\n        uses: actions/github-script@v8\n        with:\n          script: |\n            github.rest.repos.createCommitStatus({\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              sha: context.payload.workflow_run.head_sha,\n              context: 'Website Preview',\n              description: 'Deploying...',\n              state: 'pending',\n            })\n\n      - name: Download Artifact\n        uses: actions/download-artifact@v8\n        with:\n          name: website-preview\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n          run-id: ${{ github.event.workflow_run.id }}\n\n      - name: Restore Context\n        run: |\n          PR_NUMBER=$(cat ./pr-number)\n          if ! [[ \"${PR_NUMBER}\" =~ ^[0-9]+$ ]]; then\n            echo \"Invalid PR number: ${PR_NUMBER}\"\n            exit 1\n          fi\n          echo \"PR_NUMBER=${PR_NUMBER}\" >> \"${GITHUB_ENV}\"\n\n      - name: Set Deploy Name\n        run: |\n          echo \"DEPLOY_NAME=deploy-preview-${PR_NUMBER}\" >> \"${GITHUB_ENV}\"\n\n      - name: Deploy to Netlify\n        id: deploy\n        uses: nwtgck/actions-netlify@v3\n        with:\n          publish-dir: ./website/build\n          production-deploy: false\n          deploy-message: \"Deploy ${{ env.DEPLOY_NAME }}@${{ github.event.workflow_run.head_sha }}\"\n          alias: ${{ env.DEPLOY_NAME }}\n        env:\n          NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}\n          NETLIFY_SITE_ID: ${{ secrets.SITE_ID }}\n\n      # action netlify has no pull request context, so we need to comment by ourselves\n      - name: Comment on Pull Request\n        uses: marocchino/sticky-pull-request-comment@v3\n        with:\n          header: website\n          number: ${{ env.PR_NUMBER }}\n          message: |\n            :rocket: Deployed to ${{ steps.deploy.outputs.deploy-url }}\n\n      - name: Set Commit Status\n        uses: actions/github-script@v8\n        if: always()\n        with:\n          script: |\n            if (`${{ job.status }}` === 'success') {\n              github.rest.repos.createCommitStatus({\n                owner: context.repo.owner,\n                repo: context.repo.repo,\n                sha: context.payload.workflow_run.head_sha,\n                context: 'Website Preview',\n                description: `Deployed to ${{ steps.deploy.outputs.deploy-url }}`,\n                state: 'success',\n                target_url: `${{ steps.deploy.outputs.deploy-url }}`,\n              })\n            } else {\n              github.rest.repos.createCommitStatus({\n                owner: context.repo.owner,\n                repo: context.repo.repo,\n                sha: context.payload.workflow_run.head_sha,\n                context: 'Website Preview',\n                description: `Deploy ${{ job.status }}`,\n                state: 'failure',\n              })\n            }\n"
  },
  {
    "path": ".github/workflows/website-preview-ci.yml",
    "content": "name: Site Deploy (Preview CI)\n\non:\n  pull_request:\n\njobs:\n  preview-ci:\n    runs-on: ubuntu-latest\n    concurrency:\n      group: pull-request-preview-${{ github.event.number }}\n      cancel-in-progress: true\n\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          ref: ${{ github.event.pull_request.head.sha }}\n          fetch-depth: 0\n\n      - name: Setup Python Environment\n        uses: ./.github/actions/setup-python\n\n      - name: Setup Node Environment\n        uses: ./.github/actions/setup-node\n\n      - name: Build API Doc\n        uses: ./.github/actions/build-api-doc\n\n      - name: Build Doc\n        run: yarn build\n\n      - name: Export Context\n        run: |\n          echo \"${{ github.event.pull_request.number }}\" > ./pr-number\n\n      - name: Upload Artifact\n        uses: actions/upload-artifact@v7\n        with:\n          name: website-preview\n          path: |\n            ./website/build\n            ./pr-number\n          retention-days: 1\n"
  },
  {
    "path": ".gitignore",
    "content": "# ----- Project -----\n\n.idea\n.vscode\ndev\ndocs_build/_build\n!tests/.env\n.docusaurus\nwebsite/docs/api/**/*.md\nwebsite/src/pages/changelog/**/*\n\n# Created by https://www.toptal.com/developers/gitignore/api/python,node,visualstudiocode,jetbrains,macos,windows,linux\n# Edit at https://www.toptal.com/developers/gitignore?templates=python,node,visualstudiocode,jetbrains,macos,windows,linux\n\n### JetBrains ###\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictionaries\n.idea/**/shelf\n\n# AWS User-specific\n.idea/**/aws.xml\n\n# Generated files\n.idea/**/contentModel.xml\n\n# Sensitive or high-churn files\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n.idea/**/dbnavigator.xml\n\n# Gradle\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn.  Uncomment if using\n# auto-import.\n# .idea/artifacts\n# .idea/compiler.xml\n# .idea/jarRepositories.xml\n# .idea/modules.xml\n# .idea/*.iml\n# .idea/modules\n# *.iml\n# *.ipr\n\n# CMake\ncmake-build-*/\n\n# Mongo Explorer plugin\n.idea/**/mongoSettings.xml\n\n# File-based project format\n*.iws\n\n# IntelliJ\nout/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# Editor-based Rest Client\n.idea/httpRequests\n\n# Android studio 3.1+ serialized cache file\n.idea/caches/build_file_checksums.ser\n\n### JetBrains Patch ###\n# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721\n\n# *.iml\n# modules.xml\n# .idea/misc.xml\n# *.ipr\n\n# Sonarlint plugin\n# https://plugins.jetbrains.com/plugin/7973-sonarlint\n.idea/**/sonarlint/\n\n# SonarQube Plugin\n# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin\n.idea/**/sonarIssues.xml\n\n# Markdown Navigator plugin\n# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced\n.idea/**/markdown-navigator.xml\n.idea/**/markdown-navigator-enh.xml\n.idea/**/markdown-navigator/\n\n# Cache file creation bug\n# See https://youtrack.jetbrains.com/issue/JBR-2257\n.idea/$CACHE_FILE$\n\n# CodeStream plugin\n# https://plugins.jetbrains.com/plugin/12206-codestream\n.idea/codestream.xml\n\n### Linux ###\n*~\n\n# temporary files which can be created if a process still has a handle open of a deleted file\n.fuse_hidden*\n\n# KDE directory preferences\n.directory\n\n# Linux trash folder which might appear on any partition or disk\n.Trash-*\n\n# .nfs files are created when an open file is removed but is still being accessed\n.nfs*\n\n### macOS ###\n# General\n.DS_Store\n.AppleDouble\n.LSOverride\n\n# Icon must end with two \\r\n# Icon\n\n# Thumbnails\n._*\n\n# Files that might appear in the root of a volume\n.DocumentRevisions-V100\n.fseventsd\n.Spotlight-V100\n.TemporaryItems\n.Trashes\n.VolumeIcon.icns\n.com.apple.timemachine.donotpresent\n\n# Directories potentially created on remote AFP share\n.AppleDB\n.AppleDesktop\nNetwork Trash Folder\nTemporary Items\n.apdisk\n\n### Node ###\n# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n.pnpm-debug.log*\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\nreport.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n*.lcov\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# Snowpack dependency directory (https://snowpack.dev/)\nweb_modules/\n\n# TypeScript cache\n*.tsbuildinfo\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Microbundle cache\n.rpt2_cache/\n.rts2_cache_cjs/\n.rts2_cache_es/\n.rts2_cache_umd/\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variables file\n.env\n.env.test\n.env.production\n\n# parcel-bundler cache (https://parceljs.org/)\n.cache\n.parcel-cache\n\n# Next.js build output\n.next\nout\n\n# Nuxt.js build / generate output\n.nuxt\ndist\n\n# Gatsby files\n.cache/\n# Comment in the public line in if your project uses Gatsby and not Next.js\n# https://nextjs.org/blog/next-9-1#public-directory-support\n# public\n\n# vuepress build output\n.vuepress/dist\n\n# Serverless directories\n.serverless/\n\n# FuseBox cache\n.fusebox/\n\n# DynamoDB Local files\n.dynamodb/\n\n# TernJS port file\n.tern-port\n\n# Stores VSCode versions used for testing VSCode extensions\n.vscode-test\n\n# yarn v2\n.yarn/cache\n.yarn/unplugged\n.yarn/build-state.yml\n.yarn/install-state.gz\n.pnp.*\n\n### Node Patch ###\n# Serverless Webpack directories\n.webpack/\n\n# Optional stylelint cache\n.stylelintcache\n\n# SvelteKit build / generate output\n.svelte-kit\n\n### Python ###\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\ncover/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\n.pybuilder/\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n#   For a library or package, you might want to ignore these files since the code is\n#   intended to run in multiple environments; otherwise, check them in:\n# .python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n#Pipfile.lock\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n\n# pytype static type analyzer\n.pytype/\n\n# Cython debug symbols\ncython_debug/\n\n### VisualStudioCode ###\n.vscode/*\n!.vscode/settings.json\n!.vscode/tasks.json\n!.vscode/launch.json\n!.vscode/extensions.json\n*.code-workspace\n\n# Local History for Visual Studio Code\n.history/\n\n### VisualStudioCode Patch ###\n# Ignore all local history of files\n.history\n.ionide\n\n# Support for Project snippet scope\n!.vscode/*.code-snippets\n\n### Windows ###\n# Windows thumbnail cache files\nThumbs.db\nThumbs.db:encryptable\nehthumbs.db\nehthumbs_vista.db\n\n# Dump file\n*.stackdump\n\n# Folder config file\n[Dd]esktop.ini\n\n# Recycle Bin used on file shares\n$RECYCLE.BIN/\n\n# Windows Installer files\n*.cab\n*.msi\n*.msix\n*.msm\n*.msp\n\n# Windows shortcuts\n*.lnk\n\n# End of https://www.toptal.com/developers/gitignore/api/python,node,visualstudiocode,jetbrains,macos,windows,linux\n"
  },
  {
    "path": ".markdownlint.yaml",
    "content": "MD013: false\nMD024: # 重复标题\n  siblings_only: true\nMD033: false # 允许 html\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "default_install_hook_types: [pre-commit, prepare-commit-msg]\nci:\n  autofix_commit_msg: \":rotating_light: auto fix by pre-commit hooks\"\n  autofix_prs: true\n  autoupdate_branch: master\n  autoupdate_schedule: monthly\n  autoupdate_commit_msg: \":arrow_up: auto update by pre-commit hooks\"\nrepos:\n  - repo: https://github.com/astral-sh/ruff-pre-commit\n    rev: v0.15.4\n    hooks:\n      - id: ruff-check\n        args: [--fix]\n        stages: [pre-commit]\n      - id: ruff-format\n        stages: [pre-commit]\n\n  - repo: https://github.com/nonebot/nonemoji\n    rev: v0.1.4\n    hooks:\n      - id: nonemoji\n        stages: [prepare-commit-msg]\n"
  },
  {
    "path": ".prettierignore",
    "content": ".github/**/*.md\nwebsite/docs/tutorial/application.mdx\nwebsite/versioned_docs/*/tutorial/application.mdx\n"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"tabWidth\": 2,\n  \"useTabs\": false,\n  \"endOfLine\": \"lf\",\n  \"arrowParens\": \"always\",\n  \"singleQuote\": false,\n  \"trailingComma\": \"es5\",\n  \"semi\": true,\n  \"overrides\": [\n    {\n      \"files\": [\n        \"**/devcontainer.json\",\n        \"**/tsconfig.json\",\n        \"**/tsconfig.*.json\"\n      ],\n      \"options\": {\n        \"parser\": \"json\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": ".stylelintrc.js",
    "content": "module.exports = {\n  extends: [\"stylelint-config-standard\", \"stylelint-prettier/recommended\"],\n  overrides: [\n    {\n      files: [\"*.css\"],\n      rules: {\n        \"function-no-unknown\": [true, { ignoreFunctions: [\"theme\"] }],\n        \"selector-class-pattern\": [\n          \"^([a-z][a-z0-9]*)(-[a-z0-9]+)*$\",\n          {\n            resolveNestedSelectors: true,\n            message: (selector) =>\n              `Expected class selector \"${selector}\" to be kebab-case`,\n          },\n        ],\n      },\n    },\n    {\n      files: [\"*.module.css\"],\n      rules: {\n        \"selector-class-pattern\": [\n          \"^[a-z][a-zA-Z0-9]+$\",\n          {\n            message: (selector) =>\n              `Expected class selector \"${selector}\" to be lowerCamelCase`,\n          },\n        ],\n      },\n    },\n  ],\n};\n"
  },
  {
    "path": ".yarnrc",
    "content": "registry \"https://registry.npmjs.org/\"\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\nSee [changelog.md](./website/src/changelog/changelog.md) or <https://nonebot.dev/changelog>\n"
  },
  {
    "path": "CITATION.cff",
    "content": "# This CITATION.cff file was generated with cffinit.\n# Visit https://bit.ly/cffinit to generate yours today!\n\ncff-version: 1.2.0\ntitle: NoneBot\nmessage: >-\n  If you use this software, please cite it using the\n  metadata from this file.\ntype: software\nauthors:\n  - given-names: Yongyu\n    family-names: Yan\n    email: yyy@nonebot.dev\n  - name: NoneBot Team\n    email: contact@nonebot.dev\n    website: 'https://github.com/nonebot'\nrepository-code: 'https://github.com/nonebot/nonebot2'\nurl: 'https://nonebot.dev/'\nabstract: >-\n  NoneBot, an asynchronous multi-platform chatbot framework\n  written in Python\nkeywords:\n  - nonebot\n  - chatbot\n  - pydantic\nlicense: MIT\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# NoneBot2 贡献者公约\n\n## 我们的承诺\n\n身为项目成员、贡献者、负责人，我们保证参与此社区的每个人都不受骚扰，不论其年龄、体型、身体条件、民族、性征、性别认同与表现、经验水平、教育程度、社会地位、国籍、相貌、种族、宗教信仰及性取向如何。\n\n我们承诺致力于建设开放、友善、多元、包容、健康的社区环境。\n\n## 我们的准则\n\n有助于促进本社区积极环境的行为包括但不限于：\n\n- 与人为善、推己及人\n- 尊重不同的主张、观点和经历\n- 积极提出、耐心接受有益批评\n- 面对过失，承担责任、认真道歉、从中学习\n- 关注社区共同诉求，而非一己私利\n\n不当行为包括但不限于：\n\n- 发布与性有关的言论或图像，以及任何形式的献殷勤或勾引\n- 挑衅行为、侮辱或贬损的言论、人身及政治攻击\n- 公开或私下骚扰\n- 未获明确授权擅自发布他人的资料，如地址、电子邮箱等\n- 其他有理由认定为违反职业操守的不当行为\n\n## 落实之义务\n\n社区负责人有责任诠释什么是“妥当行为”，并据此准则，妥善公正地认定与处置不当、威胁、冒犯及有害的行为。\n\n社区负责人有权利和义务删除、编辑、拒绝违背本公约的评论（comment）、提交（commit）、代码、维基（wiki）编辑、问题（issue）等贡献。如有必要，需告知采取措施的理由。\n\n## 适用范围\n\n此行为标准适用于本社区全部场合，以及在其他场合代表本社区的个人。\n\n代表本社区的情形包括但不限于：使用官方电子邮件与社交平台、作为指定代表参与在线或线下活动。\n\n## 贯彻落实\n\n如遇滥用、骚扰等不当行为，请通过 contact@nonebot.dev 向我们举报。我们将迅速审议并调查全部投诉。\n\n社区全体负责人有义务保密举报者信息。\n\n## 指导方针\n\n社区负责人将依据下列方案判断并处置违纪行为：\n\n### 一、督促\n\n**社区影响**：用语不当、举止不符合道德或不受社区欢迎。\n\n**处理意见**：由社区负责人予以非公开的书面警告，阐明违纪事由、解释举止如何不妥。或要求公开道歉。\n\n### 二、警告\n\n**社区影响**：一起或多起事件中的违纪行为。\n\n**处理意见**：警告继续违纪的后果、违纪者在特定时间内禁止与当事人往来、不得擅自与社区执法者往来，禁令涵盖社区内外、社交网络在内的一切联络。如有违反，可致封禁乃至开除。\n\n### 三、封禁\n\n**社区影响**：严重违纪行为，包括屡教不改。\n\n**处理意见**：违纪者在特定时间内禁止与社区的任何往来或公开联络，禁止任何与当事人公开或私下往来，不得擅自与社区管理者往来。如有违反，可导致开除。\n\n### 四、开除\n\n**社区影响**：典型违纪行为，例如屡教不改、骚扰某个人、敌对或贬低某个群体。\n\n**处理意见**：无限期禁止违纪者与项目社区的一切公开往来。\n\n## 来源\n\n本行为标准改编自[参与者公约][homepage]2.0 版，可在此查阅：[https://www.contributor-covenant.org/zh-cn/version/2/0/code_of_conduct.html][v2.0]\n\n指导方针借鉴自[Mozilla 纪检分级][mozilla coc]。\n\n此行为标准常见问题请洽：[https://www.contributor-covenant.org/faq][faq]。\n另有诸译本：[https://www.contributor-covenant.org/translations][translations]。\n\n[homepage]: https://www.contributor-covenant.org\n[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html\n[mozilla coc]: https://github.com/mozilla/diversity\n[faq]: https://www.contributor-covenant.org/faq\n[translations]: https://www.contributor-covenant.org/translations\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# NoneBot2 贡献指南\n\n首先，感谢你愿意为 NoneBot2 贡献自己的一份力量！\n\n本指南旨在引导你更规范地向 NoneBot2 提交贡献，请务必认真阅读。\n\n## 提交 Issue\n\n在提交 Issue 前，我们建议你先查看 [FAQ](https://github.com/nonebot/discussions/discussions/13) 与 [已有的 Issues](https://github.com/nonebot/nonebot2/issues)，以防重复提交。\n\n### 报告问题、故障与漏洞\n\n如果你在使用过程中发现问题并确信是由 NoneBot2 引起的，欢迎提交 Issue。\n\n### 建议功能\n\n为了让开发者更好地理解你的意图，请认真描述你所需要的特性，可能的话可以提出你认为可行的解决方案。\n\n## Pull Request\n\nNoneBot 使用 [uv](https://docs.astral.sh/uv/) 管理项目依赖，由于 pre-commit 也经其管理，所以在此一并说明。\n\n下面的命令能在已安装 uv 和 yarn 的情况下帮你快速配置开发环境。\n\n```bash\n# 安装 python 依赖\nuv sync --all-extras\n# 安装 pre-commit git hook\nuv run pre-commit install\n```\n\n### 使用 GitHub Codespaces（Dev Container）\n\n[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new?hide_repo_select=true&ref=master&repo=289605524)\n\n### Commit 规范\n\n请确保你的每一个 commit 都能清晰地描述其意图，一个 commit 尽量只有一个意图。\n\nNoneBot 的 commit message 格式遵循 [gitmoji](https://gitmoji.dev/) 规范，在创建 commit 时请牢记这一点。\n\n或者使用 [nonemoji](https://github.com/nonebot/nonemoji) 代替 git 进行 commit，nonemoji 已默认作为项目开发依赖安装。\n\n```bash\nnonemoji commit [-e EMOJI] [-m MESSAGE] [-- ...]\n```\n\n### 工作流概述\n\n`master` 分支为 NoneBot 的开发分支，在任何情况下都请不要直接修改 `master` 分支，而是创建一个目标分支为 `nonebot:master` 的 Pull Request 来提交修改。Pull Request 标题请尽量更改成中文，以便自动生成更新日志。\n\n如果你不是 NoneBot 团队的成员，可在 fork 本仓库后，向本仓库的 `master` 分支发起 Pull Request，注意遵循先前提到的 commit message 规范创建 commit。我们将在 code review 通过后通过 squash merge 方式将您的贡献合并到主分支。\n\n### 撰写文档\n\nNoneBot2 的文档使用 [docusaurus](https://docusaurus.io/)，它有一些 [Markdown 特性](https://docusaurus.io/zh-CN/docs/markdown-features) 可能会帮助到你。\n\n如果你需要在本地预览修改后的文档，可以使用 yarn 安装文档依赖后启动 dev server，如下所示：\n\n```bash\nyarn install\nyarn start\n```\n\nNoneBot2 文档并没有具体的行文风格规范，但我们建议你尽量写得简单易懂。\n\n以下是比较重要的编写与排版规范。目前 NoneBot2 文档中仍有部分文档不完全遵守此规范，如果在阅读时发现欢迎提交 PR。\n\n1. 中文与英文、数字、半角符号之间需要有空格。例：`NoneBot2 是一个可扩展的 Python 异步机器人框架。`\n2. 若非英文整句，使用全角标点符号。例：`现在你可以看到机器人回复你：“Hello, World !”。`\n3. 直引号`「」`和弯引号`“”`都可接受，但同一份文件里应使用同种引号。\n4. **不要使用斜体**，你不需要一种与粗体不同的强调。除此之外，你也可以考虑使用 docusaurus 提供的[告示](https://docusaurus.io/zh-CN/docs/markdown-features/admonitions)功能。\n5. 文档中应以“我们”指代机器人开发者，以“机器人用户”指代机器人的使用者。\n\n以上由[社区创始人 richardchien 的中文排版规范](https://stdrc.cc/style-guides/chinese)补充修改得到。\n\n如果你需要编辑器检查 Markdown 规范，可以在 VSCode 中安装 markdownlint 扩展。\n\n### 参与开发\n\nNoneBot2 的代码风格遵循 [PEP 8](https://www.python.org/dev/peps/pep-0008/) 与 [PEP 484](https://www.python.org/dev/peps/pep-0484/) 规范，请确保你的代码风格和项目已有的代码保持一致，变量命名清晰，有适当的注释与测试代码。\n\n## 为社区做贡献\n\n你可以在 NoneBot 商店上架自己的适配器、插件、机器人，具体步骤可参考 [发布插件](https://nonebot.dev/docs/developer/plugin-publishing) 一节。\n\n我们仅对插件的兼容性进行简单测试，并会在下一个版本发布前对与该版本不兼容的插件作出处理。\n\n虽然对插件的内容没有严格限制，但我们还是建议在上架插件之前先查看商店有无功能一致的插件。如果你想要上架商店的插件功能与现有插件不完全重合，请在插件说明中补充其与现有插件的区别。\n\n同时，如果你参考或基于他人发行的代码进行开发，请注意遵守各代码所使用的开源许可协议。\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\nCopyright (c) 2020 NoneBot Team\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<!-- markdownlint-disable MD033 MD041 -->\n<p align=\"center\">\n  <a href=\"https://nonebot.dev/\"><img src=\"https://nonebot.dev/logo.png\" width=\"200\" height=\"200\" alt=\"nonebot\"></a>\n</p>\n\n<div align=\"center\">\n\n# NoneBot\n\n<!-- prettier-ignore-start -->\n<!-- markdownlint-disable-next-line MD036 -->\n_✨ 跨平台 Python 异步机器人框架 ✨_\n<!-- prettier-ignore-end -->\n\n</div>\n\n<p align=\"center\">\n  <a href=\"https://raw.githubusercontent.com/nonebot/nonebot2/master/LICENSE\">\n    <img src=\"https://img.shields.io/github/license/nonebot/nonebot2\" alt=\"license\">\n  </a>\n  <a href=\"https://pypi.python.org/pypi/nonebot2\">\n    <img src=\"https://img.shields.io/pypi/v/nonebot2?logo=python&logoColor=edb641\" alt=\"pypi\">\n  </a>\n  <img src=\"https://img.shields.io/badge/python-3.9+-blue?logo=python&logoColor=edb641\" alt=\"python\">\n  <a href=\"https://github.com/psf/black\">\n    <img src=\"https://img.shields.io/badge/code%20style-black-000000.svg?logo=python&logoColor=edb641\" alt=\"black\">\n  </a>\n  <a href=\"https://github.com/Microsoft/pyright\">\n    <img src=\"https://img.shields.io/badge/types-pyright-797952.svg?logo=python&logoColor=edb641\" alt=\"pyright\">\n  </a>\n  <a href=\"https://github.com/astral-sh/ruff\">\n    <img src=\"https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json\" alt=\"ruff\">\n  </a>\n  <br />\n  <a href=\"https://codecov.io/gh/nonebot/nonebot2\">\n    <img src=\"https://codecov.io/gh/nonebot/nonebot2/branch/master/graph/badge.svg?token=2P0G0VS7N4\" alt=\"codecov\"/>\n  </a>\n  <a href=\"https://github.com/nonebot/nonebot2/actions/workflows/website-deploy.yml\">\n    <img src=\"https://github.com/nonebot/nonebot2/actions/workflows/website-deploy.yml/badge.svg?branch=master&event=push\" alt=\"site\"/>\n  </a>\n  <a href=\"https://results.pre-commit.ci/latest/github/nonebot/nonebot2/master\">\n    <img src=\"https://results.pre-commit.ci/badge/github/nonebot/nonebot2/master.svg\" alt=\"pre-commit\" />\n  </a>\n  <a href=\"https://github.com/nonebot/nonebot2/actions/workflows/pyright.yml\">\n    <img src=\"https://github.com/nonebot/nonebot2/actions/workflows/pyright.yml/badge.svg?branch=master&event=push\" alt=\"pyright\">\n  </a>\n  <a href=\"https://github.com/nonebot/nonebot2/actions/workflows/ruff.yml\">\n    <img src=\"https://github.com/nonebot/nonebot2/actions/workflows/ruff.yml/badge.svg?branch=master&event=push\" alt=\"ruff\">\n  </a>\n  <br />\n  <a href=\"https://onebot.dev/\">\n    <img src=\"https://img.shields.io/badge/OneBot-v11-black?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABABAMAAABYR2ztAAAAIVBMVEUAAAAAAAADAwMHBwceHh4UFBQNDQ0ZGRkoKCgvLy8iIiLWSdWYAAAAAXRSTlMAQObYZgAAAQVJREFUSMftlM0RgjAQhV+0ATYK6i1Xb+iMd0qgBEqgBEuwBOxU2QDKsjvojQPvkJ/ZL5sXkgWrFirK4MibYUdE3OR2nEpuKz1/q8CdNxNQgthZCXYVLjyoDQftaKuniHHWRnPh2GCUetR2/9HsMAXyUT4/3UHwtQT2AggSCGKeSAsFnxBIOuAggdh3AKTL7pDuCyABcMb0aQP7aM4AnAbc/wHwA5D2wDHTTe56gIIOUA/4YYV2e1sg713PXdZJAuncdZMAGkAukU9OAn40O849+0ornPwT93rphWF0mgAbauUrEOthlX8Zu7P5A6kZyKCJy75hhw1Mgr9RAUvX7A3csGqZegEdniCx30c3agAAAABJRU5ErkJggg==\" alt=\"onebot\">\n  </a>\n  <a href=\"https://onebot.dev/\">\n    <img src=\"https://img.shields.io/badge/OneBot-v12-black?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABABAMAAABYR2ztAAAAIVBMVEUAAAAAAAADAwMHBwceHh4UFBQNDQ0ZGRkoKCgvLy8iIiLWSdWYAAAAAXRSTlMAQObYZgAAAQVJREFUSMftlM0RgjAQhV+0ATYK6i1Xb+iMd0qgBEqgBEuwBOxU2QDKsjvojQPvkJ/ZL5sXkgWrFirK4MibYUdE3OR2nEpuKz1/q8CdNxNQgthZCXYVLjyoDQftaKuniHHWRnPh2GCUetR2/9HsMAXyUT4/3UHwtQT2AggSCGKeSAsFnxBIOuAggdh3AKTL7pDuCyABcMb0aQP7aM4AnAbc/wHwA5D2wDHTTe56gIIOUA/4YYV2e1sg713PXdZJAuncdZMAGkAukU9OAn40O849+0ornPwT93rphWF0mgAbauUrEOthlX8Zu7P5A6kZyKCJy75hhw1Mgr9RAUvX7A3csGqZegEdniCx30c3agAAAABJRU5ErkJggg==\" alt=\"onebot\">\n  </a>\n  <a href=\"https://bot.q.qq.com/wiki/\">\n    <img src=\"https://img.shields.io/badge/QQ-Bot-lightgrey?style=social&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMTIuODIgMTMwLjg5Ij48ZyBkYXRhLW5hbWU9IuWbvuWxgiAyIj48ZyBkYXRhLW5hbWU9IuWbvuWxgiAxIj48cGF0aCBkPSJNNTUuNjMgMTMwLjhjLTcgMC0xMy45LjA4LTIwLjg2IDAtMTkuMTUtLjI1LTMxLjcxLTExLjQtMzQuMjItMzAuMy00LjA3LTMwLjY2IDE0LjkzLTU5LjIgNDQuODMtNjYuNjQgMi0uNTEgNS4yMS0uMzEgNS4yMS0xLjYzIDAtMi4xMy4xNC0yLjEzLjE0LTUuNTcgMC0uODktMS4zLTEuNDYtMi4yMi0yLjMxLTYuNzMtNi4yMy03LjY3LTEzLjQxLTEtMjAuMTggNS40LTUuNTIgMTEuODctNS40IDE3LjgtLjU5IDYuNDkgNS4yNiA2LjMxIDEzLjA4LS44NiAyMS0uNjguNzQtMS43OCAxLjYtMS43OCAyLjY3djQuMjFjMCAxLjM1IDIuMiAxLjYyIDQuNzkgMi4zNSAzMS4wOSA4LjY1IDQ4LjE3IDM0LjEzIDQ1IDY2LjM3LTEuNzYgMTguMTUtMTQuNTYgMzAuMjMtMzIuNyAzMC42My04LjAyLjE5LTE2LjA3LS4wMS0yNC4xMy0uMDF6IiBmaWxsPSIjMDI5OWZlIi8+PHBhdGggZD0iTTMxLjQ2IDExOC4zOGMtMTAuNS0uNjktMTYuOC02Ljg2LTE4LjM4LTE3LjI3LTMtMTkuNDIgMi43OC0zNS44NiAxOC40Ni00Ny44MyAxNC4xNi0xMC44IDI5Ljg3LTEyIDQ1LjM4LTMuMTkgMTcuMjUgOS44NCAyNC41OSAyNS44MSAyNCA0NS4yOS0uNDkgMTUuOS04LjQyIDIzLjE0LTI0LjM4IDIzLjUtNi41OS4xNC0xMy4xOSAwLTE5Ljc5IDAiIGZpbGw9IiNmZWZlZmUiLz48cGF0aCBkPSJNNDYuMDUgNzkuNThjLjA5IDUgLjIzIDkuODItNyA5Ljc3LTcuODItLjA2LTYuMS01LjY5LTYuMjQtMTAuMTktLjE1LTQuODItLjczLTEwIDYuNzMtOS44NHM2LjM3IDUuNTUgNi41MSAxMC4yNnoiIGZpbGw9IiMxMDlmZmUiLz48cGF0aCBkPSJNODAuMjcgNzkuMjdjLS41MyAzLjkxIDEuNzUgOS42NC01Ljg4IDEwLTcuNDcuMzctNi44MS00LjgyLTYuNjEtOS41LjItNC4zMi0xLjgzLTEwIDUuNzgtMTAuNDJzNi41OSA0Ljg5IDYuNzEgOS45MnoiIGZpbGw9IiMwODljZmUiLz48L2c+PC9nPjwvc3ZnPg==\" alt=\"QQ\">\n  </a>\n  <a href=\"https://core.telegram.org/bots/api\">\n    <img src=\"https://img.shields.io/badge/telegram-Bot-lightgrey?style=social&logo=telegram\" alt=\"telegram\">\n  </a>\n  <a href=\"https://open.feishu.cn/document/home/index\">\n    <img src=\"https://img.shields.io/badge/%E9%A3%9E%E4%B9%A6-Bot-lightgrey?style=social&logo=data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDQ4IDQ4IiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Ik0xNyAyOUMyMSAyOSAyNSAyNi45MzM5IDI4IDIzLjQwNjVDMzYgMTQgNDEuNDI0MiAxNi44MTY2IDQ0IDE3Ljk5OThDMzguNSAyMC45OTk4IDQwLjUgMjkuNjIzMyAzMyAzNS45OTk4QzI4LjM4MiAzOS45MjU5IDIzLjQ5NDUgNDEuMDE0IDE5IDQxQzEyLjUyMzEgNDAuOTc5OSA2Ljg2MjI2IDM3Ljc2MzcgNCAzNS40MDYzVjE2Ljk5OTgiIHN0cm9rZT0iIzMzMyIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48cGF0aCBkPSJNNS42NDgwOCAxNS44NjY5QzUuMDIyMzEgMTQuOTU2NyAzLjc3NzE1IDE0LjcyNjEgMi44NjY5NCAxNS4zNTE5QzEuOTU2NzMgMTUuOTc3NyAxLjcyNjE1IDE3LjIyMjggMi4zNTE5MiAxOC4xMzMxTDUuNjQ4MDggMTUuODY2OVpNMzYuMDAyMSAzNS43MzA5QzM2Ljk1OCAzNS4xNzc0IDM3LjI4NDMgMzMuOTUzOSAzNi43MzA5IDMyLjk5NzlDMzYuMTc3NCAzMi4wNDIgMzQuOTUzOSAzMS43MTU3IDMzLjk5NzkgMzIuMjY5MUwzNi4wMDIxIDM1LjczMDlaTTIuMzUxOTIgMTguMTMzMUM1LjI0MzUgMjIuMzM5IDEwLjc5OTIgMjguMTQ0IDE2Ljg4NjUgMzIuMjIzOUMxOS45MzQ1IDM0LjI2NjcgMjMuMjE3IDM1Ljk0NiAyNi40NDkgMzYuNzMyNEMyOS42OTQ2IDM3LjUyMiAzMy4wNDUxIDM3LjQ0MjggMzYuMDAyMSAzNS43MzA5TDMzLjk5NzkgMzIuMjY5MUMzMi4yMDQ5IDMzLjMwNzIgMjkuOTkyOSAzMy40NzggMjcuMzk0NyAzMi44NDU4QzI0Ljc4MyAzMi4yMTAzIDIxLjk0MDUgMzAuNzk1OCAxOS4xMTM1IDI4LjkwMTFDMTMuNDUwOCAyNS4xMDYgOC4yNTY1IDE5LjY2MSA1LjY0ODA4IDE1Ljg2NjlMMi4zNTE5MiAxOC4xMzMxWiIgZmlsbD0iIzMzMyIvPjxwYXRoIGQ9Ik0zMy41OTQ1IDE3QzMyLjgzOTggMTQuNzAyNyAzMC44NTQ5IDkuOTQwNTQgMjcuNTk0NSA3SDExLjU5NDVDMTUuMjE3MSAxMC42NzU3IDIzIDE2IDI3IDI0IiBzdHJva2U9IiMzMzMiIHN0cm9rZS13aWR0aD0iNCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PC9zdmc+\" alt=\"feishu\">\n  </a>\n  <a href=\"https://docs.github.com/en/developers/apps\">\n    <img src=\"https://img.shields.io/badge/GitHub-Bot-181717?style=social&logo=github\" alt=\"github\"/>\n  </a>\n  <!-- <a href=\"https://ding-doc.dingtalk.com/document#/org-dev-guide/elzz1p\">\n    <img src=\"https://img.shields.io/badge/%E9%92%89%E9%92%89-Bot-lightgrey?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAnFBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4jUzeAAAAM3RSTlMAQKSRaA+/f0YyFevh29R3cyklIfrlyrGsn41tVUs48c/HqJm9uZdhX1otGwkF9IN8V1CX0Q+IAAABY0lEQVRYw+3V2W7CMBAF0JuNQAhhX9OEfYdu9///rUVWpagE27Ef2gfO+0zGozsKnv6bMGzAhkNytIe5gDdzrwtTCwrbI8x4/NF668NAxgI3Q3UtFi3TyPwNQtPLUUmDd8YfqGLNe4v22XwEYb5zoOuF5baHq2UHtsKe5ivWfGAwrWu2mC34QM0PoCAuqZdOmiwV+5BLyMRtZ7dTSEcs48rzWfzwptMLyzpApka1SJ5FtR4kfCqNIBPEVDmqoqgwUYY5plQOlf6UEjNoOPnuKB6wzDyCrks///TDza8+PnR109WQdxLo8RKWq0PPnuXG0OXKQ6wWLFnCg75uYYbhmMIVVdQ709q33aHbGIj6Duz+2k1HQFX9VwqmY8xYsEJll2ahvhWgsjYLHFRXvIi2Qb0jzMQCzC3FAoydxCma88UCzE3JCWwkjCNYyMUCzHX4DiuTMawEwwhW6hnshPhjZzzJfAH0YacpbmRd7QAAAABJRU5ErkJggg==\" alt=\"dingtalk\"> -->\n  </a>\n  <br />\n  <a href=\"https://jq.qq.com/?_wv=1027&k=5OFifDh\">\n    <img src=\"https://img.shields.io/badge/QQ%E7%BE%A4-768887710-orange?style=flat-square\" alt=\"QQ Chat Group\">\n  </a>\n  <a href=\"https://qun.qq.com/qqweb/qunpro/share?_wv=3&_wwv=128&appChannel=share&inviteCode=7b4a3&appChannel=share&businessType=9&from=246610&biz=ka\">\n    <img src=\"https://img.shields.io/badge/QQ%E9%A2%91%E9%81%93-NoneBot-5492ff?style=flat-square\" alt=\"QQ Channel\">\n  </a>\n  <a href=\"https://t.me/botuniverse\">\n    <img src=\"https://img.shields.io/badge/telegram-botuniverse-blue?style=flat-square\" alt=\"Telegram Channel\">\n  </a>\n  <a href=\"https://discord.gg/VKtE6Gdc4h\">\n    <img src=\"https://discordapp.com/api/guilds/847819937858584596/widget.png?style=shield\" alt=\"Discord Server\">\n  </a>\n</p>\n\n<p align=\"center\">\n  <a href=\"https://nonebot.dev/\">文档</a>\n  ·\n  <a href=\"https://nonebot.dev/docs/quick-start\">快速上手</a>\n  ·\n  <a href=\"#插件\">文档打不开？</a>\n</p>\n\n<p align=\"center\">\n  <a href=\"https://asciinema.org/a/569440\">\n    <img src=\"https://nonebot.dev/img/setup.svg\" alt=\"setup\" >\n  </a>\n</p>\n\n## 简介\n\nNoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架，它基于 Python 的类型注解和异步特性，能够为你的需求实现提供便捷灵活的支持。\n\n## 特色\n\n- 异步优先：基于 Python 的异步特性，即使是~~非常~~大量的消息，也能吞吐自如\n- 易于开发：配合 NB-CLI 脚手架，代码编写上手简单，没有过多的冗余代码，可以让开发者专注于业务逻辑\n- 生而可靠：100% 类型注解覆盖，配合编辑器的类型推导功能，能将绝大多数的 Bug 杜绝在编辑器中 ([编辑器支持](https://nonebot.dev/docs/editor-support))\n- 社区丰富：社区用户众多，直接和间接用户超过十万人，每天都有大量的活跃用户 ([社区资源](#社区资源))\n- 海纳百川：一个框架，支持多个聊天软件平台，可自定义通信协议\n\n  |                                                       协议名称                                                        | 状态 |                                   注释                                    |\n  | :-------------------------------------------------------------------------------------------------------------------: | :--: | :-----------------------------------------------------------------------: |\n  |               OneBot（[仓库](https://github.com/nonebot/adapter-onebot)，[协议](https://onebot.dev/)）                |  ✅  | 支持 QQ、TG、微信公众号、KOOK 等[平台](https://onebot.dev/ecosystem.html) |\n  |      Telegram（[仓库](https://github.com/nonebot/adapter-telegram)，[协议](https://core.telegram.org/bots/api)）      |  ✅  |                                                                           |\n  |     飞书（[仓库](https://github.com/nonebot/adapter-feishu)，[协议](https://open.feishu.cn/document/home/index)）     |  ✅  |                                                                           |\n  |         GitHub（[仓库](https://github.com/nonebot/adapter-github)，[协议](https://docs.github.com/en/apps)）          |  ✅  |                          GitHub APP & OAuth APP                           |\n  |                QQ（[仓库](https://github.com/nonebot/adapter-qq)，[协议](https://bot.q.qq.com/wiki/)）                |  ✅  |                            QQ 官方接口调整较多                            |\n  |                             Console（[仓库](https://github.com/nonebot/adapter-console)）                             |  ✅  |                                控制台交互                                 |\n  |     Red（[仓库](https://github.com/nonebot/adapter-red)，[协议](https://chrononeko.github.io/QQNTRedProtocol/)）      |  ✅  |                                 QQNT 协议                                  |\n  |           Satori（[仓库](https://github.com/nonebot/adapter-satori)，[协议](https://satori.js.org/zh-CN)）            |  ✅  |               支持 Onebot、TG、飞书、微信公众号、Koishi 等                |\n  |   Discord（[仓库](https://github.com/nonebot/adapter-discord)，[协议](https://discord.com/developers/docs/intro)）    |  ✅  |                             Discord Bot 协议                              |\n  |               DoDo（[仓库](https://github.com/nonebot/adapter-dodo)，[协议](https://open.imdodo.com/)）               |  ✅  |                               DoDo Bot 协议                               |\n  |        Kritor（[仓库](https://github.com/nonebot/adapter-kritor)，[协议](https://github.com/KarinJS/kritor)）         |  ✅  |                Kritor (OnebotX) 协议，QQNT 机器人接口标准                  |\n  |    Mirai（[仓库](https://github.com/nonebot/adapter-mirai)，[协议](https://docs.mirai.mamoe.net/mirai-api-http/)）    |  ✅  |                                  QQ 协议                                  |\n  |    Milky（[仓库](https://github.com/nonebot/adapter-milky)，[协议](https://milky.ntqqrev.org/)）                      |  ✅  |                           QQNT 机器人应用接口标准                          |\n  |         钉钉（[仓库](https://github.com/nonebot/adapter-ding)，[协议](https://open.dingtalk.com/document/)）          |  🤗  |                        寻找 Maintainer（暂不可用）                        |\n  |     开黑啦（[仓库](https://github.com/Tian-que/nonebot-adapter-kaiheila)，[协议](https://developer.kookapp.cn/)）     |  ↗️  |                                由社区贡献                                 |\n  |                          Ntchat（[仓库](https://github.com/JustUndertaker/adapter-ntchat)）                           |  ↗️  |                           微信协议，由社区贡献                            |\n  |                      MineCraft（[仓库](https://github.com/17TheWord/nonebot-adapter-minecraft)）                      |  ↗️  |                                由社区贡献                                 |\n  |                       Walle-Q（[仓库](https://github.com/onebot-walle/nonebot_adapter_walleq)）                       |  ↗️  |                            QQ 协议，由社区贡献                            |\n  |                       Villa（[仓库](https://github.com/CMHopeSunshine/nonebot-adapter-villa)）                        |  ❌  |                     米游社大别野 Bot 协议，官方已下线                     |\n  | Rocket.Chat（[仓库](https://github.com/IUnlimit/nonebot-adapter-rocketchat)，[协议](https://developer.rocket.chat/)） |  ↗️  |                     Rocket.Chat Bot 协议，由社区贡献                      |\n  |     Tailchat（[仓库](https://github.com/eya46/nonebot-adapter-tailchat)，[协议](https://tailchat.msgbyte.com/)）      |  ↗️  |                  Tailchat 开放平台 Bot 协议，由社区贡献                   |\n  |                             Mail（[仓库](https://github.com/mobyw/nonebot-adapter-mail)）                             |  ↗️  |                         邮件收发协议，由社区贡献                          |\n  |     黑盒语音（[仓库](https://github.com/lclbm/adapter-heybox)，[协议](https://github.com/QingFengOpen/HeychatDoc)）     |  ↗️  |                       黑盒语音机器人协议，由社区贡献                             |\n  | 微信公众平台（[仓库](https://github.com/YangRucheng/nonebot-adapter-wxmp)，[协议](https://developers.weixin.qq.com/doc/)）|  ↗️  |                       微信公众平台协议，由社区贡献                             |\n  | Gewechat（[仓库](https://github.com/Shine-Light/nonebot-adapter-gewechat)，[协议](https://github.com/Devo919/Gewechat)）|  ❌  |                      Gewechat 微信协议，Gewechat不再维护及可用                            |\n  |  EFChat（[仓库](https://github.com/molanp/nonebot_adapter_efchat)，[协议](https://irinu-live.melon.fish/efc-help/)）   |  ↗️  |                            恒五聊平台协议，由社区贡献                          |\n  |  VoceChat （[仓库](https://github.com/5656565566/nonebot-adapter-vocechat)，[协议](https://doc.voce.chat/zh-cn/bot/bot-and-webhook)）   |  ↗️  |                            VoceChat 平台协议，由社区贡献                          |\n  |  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/开放平台）协议，由社区贡献                          |\n\n- 坚实后盾：支持多种 web 框架，可自定义替换、组合\n\n  |                              驱动框架                               |  类型  |\n  | :-----------------------------------------------------------------: | :----: |\n  |              [FastAPI](https://fastapi.tiangolo.com/)               | 服务端 |\n  | [Quart](https://quart.palletsprojects.com/en/latest/)（异步 Flask） | 服务端 |\n  |           [aiohttp](https://docs.aiohttp.org/en/stable/)            | 客户端 |\n  |               [httpx](https://www.python-httpx.org/)                | 客户端 |\n  |     [websockets](https://websockets.readthedocs.io/en/stable/)      | 客户端 |\n\n更多：[概览](https://nonebot.dev/docs/)\n\n## 什么不是 NoneBot2\n\nNoneBot2 不是某个平台或者协议的具体实现，它只负责和已有协议适配器通信，并处理接收到的事件。所以，“NoneBot 有 blabla 平台的 blabla 功能吗？”这种问题是与 NoneBot2 无关的。请在相应平台的功能文档中确认，或与相应平台的协议适配开发者联系。\n\nNoneBot2 不是 NoneBot1 的替代品。事实上，它们都在被积极的维护着。但是，如果你想尝试一些新功能，或者想要支持更多的平台，可以考虑使用 NoneBot2。\n\n> ~~NoneBot2 和 NoneBot1 的区别，就像是 VisualStudio Code 和 VisualStudio 一样~~\n\n## 即刻开始\n\n~~完整~~文档可以在 [这里](https://nonebot.dev/) 查看。\n\n懒得看文档？下面是快速安装指南：\n\n1. 安装 [pipx](https://pypa.github.io/pipx/)\n\n   ```bash\n   python -m pip install --user pipx\n   python -m pipx ensurepath\n   ```\n\n2. 安装脚手架\n\n   ```bash\n   pipx install nb-cli\n   ```\n\n3. 使用脚手架创建项目\n\n   ```bash\n   nb create\n   ```\n\n4. 运行项目\n\n   ```bash\n   nb run\n   ```\n\n## 社区资源\n\n### 常见问题\n\n- [常见问题解答(FAQ)](https://faq.nonebot.dev/)\n- [论坛(Discussion)](https://discussions.nonebot.dev/)\n\n### 教程/实际项目/经验分享\n\n- [awesome-nonebot](https://github.com/nonebot/awesome-nonebot)\n\n### 插件\n\n此外，NoneBot2 还有丰富的官方以及第三方现成的插件供大家使用：\n\n- [NoneBot-Plugin-Docs](https://github.com/nonebot/nonebot2/tree/master/packages/nonebot-plugin-docs)：离线文档至本地项目使用 (别再说文档打不开了！)\n\n  在项目目录下执行：\n\n  ```bash\n  nb plugin install nonebot_plugin_docs\n  ```\n\n  或者尝试以下镜像：\n\n  - [文档镜像(中国境内)](https://nb2.baka.icu)\n\n- 其他插件请查看 [商店](https://nonebot.dev/store/plugins)\n\n## 许可证\n\n`NoneBot` 采用 `MIT` 许可证进行开源\n\n```text\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n```\n\n## 贡献\n\n请参考 [贡献指南](./CONTRIBUTING.md)\n\n## 鸣谢\n\n### 赞助者\n\n感谢以下产品对 NoneBot 项目提供的赞助：\n\n<p align=\"center\">\n  <a href=\"https://github.com/\">\n    <picture>\n      <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://assets.nonebot.dev/github-dark.png\">\n      <img src=\"https://assets.nonebot.dev/github-light.png\" height=\"50\" alt=\"GitHub\">\n    </picture>\n  </a>&nbsp;&nbsp;&nbsp;&nbsp;\n  <a href=\"https://www.netlify.com/\">\n    <picture>\n      <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://assets.nonebot.dev/netlify-dark.svg\">\n      <img src=\"https://assets.nonebot.dev/netlify-light.svg\" height=\"50\" alt=\"netlify\">\n    </picture>\n  </a>&nbsp;&nbsp;&nbsp;&nbsp;\n  <a href=\"https://sentry.io/\">\n    <picture>\n      <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://assets.nonebot.dev/sentry-dark.svg\">\n      <img src=\"https://assets.nonebot.dev/sentry-light.svg\" height=\"50\" alt=\"sentry\">\n    </picture>\n  </a>\n</p>\n<p align=\"center\">\n  <a href=\"https://www.docker.com/\">\n    <picture>\n      <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://assets.nonebot.dev/docker-dark.svg\">\n      <img src=\"https://assets.nonebot.dev/docker-light.svg\" height=\"50\" alt=\"docker\">\n    </picture>\n  </a>&nbsp;&nbsp;&nbsp;&nbsp;\n  <a href=\"https://www.algolia.com/\">\n    <picture>\n      <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://assets.nonebot.dev/algolia-dark.svg\">\n      <img src=\"https://assets.nonebot.dev/algolia-light.svg\" height=\"50\" alt=\"algolia\">\n    </picture>\n  </a>\n</p>\n<p align=\"center\">\n  <a href=\"https://www.jetbrains.com/\">\n    <img src=\"https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.svg\" height=\"80\" alt=\"JetBrains\" >\n  </a>\n</p>\n\n感谢以下赞助者对 NoneBot 项目提供的资金支持：\n\n<a href=\"https://assets.nonebot.dev/sponsors.svg\">\n  <img src=\"https://assets.nonebot.dev/sponsors.svg\" alt=\"sponsors\" />\n</a>\n\n### 开发者\n\n感谢以下开发者对 NoneBot2 作出的贡献：\n\n<a href=\"https://github.com/nonebot/nonebot2/graphs/contributors\">\n  <img src=\"https://contrib.rocks/image?repo=nonebot/nonebot2&max=1000\" alt=\"contributors\" />\n</a>\n"
  },
  {
    "path": "assets/adapters.json5",
    "content": "[\n  {\n    \"module_name\": \"nonebot.adapters.onebot.v11\",\n    \"project_link\": \"nonebot-adapter-onebot\",\n    \"name\": \"OneBot V11\",\n    \"desc\": \"OneBot V11 协议\",\n    \"author_id\": 42488585,\n    \"homepage\": \"https://onebot.adapters.nonebot.dev/\",\n    \"tags\": [],\n    \"is_official\": true\n  },\n  {\n    \"module_name\": \"nonebot.adapters.ding\",\n    \"project_link\": \"nonebot-adapter-ding\",\n    \"name\": \"钉钉\",\n    \"desc\": \"钉钉协议\",\n    \"author_id\": 1184028,\n    \"homepage\": \"https://github.com/nonebot/adapter-ding\",\n    \"tags\": [],\n    \"is_official\": true\n  },\n  {\n    \"module_name\": \"nonebot.adapters.feishu\",\n    \"project_link\": \"nonebot-adapter-feishu\",\n    \"name\": \"飞书\",\n    \"desc\": \"飞书协议\",\n    \"author_id\": 14922941,\n    \"homepage\": \"https://github.com/nonebot/adapter-feishu\",\n    \"tags\": [],\n    \"is_official\": true\n  },\n  {\n    \"module_name\": \"nonebot.adapters.telegram\",\n    \"project_link\": \"nonebot-adapter-telegram\",\n    \"name\": \"Telegram\",\n    \"desc\": \"Telegram 协议\",\n    \"author_id\": 50312681,\n    \"homepage\": \"https://github.com/nonebot/adapter-telegram\",\n    \"tags\": [],\n    \"is_official\": true\n  },\n  {\n    \"module_name\": \"nonebot.adapters.qq\",\n    \"project_link\": \"nonebot-adapter-qq\",\n    \"name\": \"QQ\",\n    \"desc\": \"QQ 官方机器人\",\n    \"author_id\": 42488585,\n    \"homepage\": \"https://github.com/nonebot/adapter-qq\",\n    \"tags\": [],\n    \"is_official\": true\n  },\n  {\n    \"module_name\": \"nonebot.adapters.kaiheila\",\n    \"project_link\": \"nonebot-adapter-kaiheila\",\n    \"name\": \"开黑啦\",\n    \"desc\": \"开黑啦协议适配\",\n    \"author_id\": 37477320,\n    \"homepage\": \"https://github.com/Tian-que/nonebot-adapter-kaiheila\",\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot.adapters.mirai\",\n    \"project_link\": \"nonebot-adapter-mirai\",\n    \"name\": \"Mirai\",\n    \"desc\": \"mirai-api-http v2 协议适配\",\n    \"author_id\": 42648639,\n    \"homepage\": \"https://github.com/nonebot/adapter-mirai\",\n    \"tags\": [],\n    \"is_official\": true\n  },\n  {\n    \"module_name\": \"nonebot.adapters.onebot.v12\",\n    \"project_link\": \"nonebot-adapter-onebot\",\n    \"name\": \"OneBot V12\",\n    \"desc\": \"OneBot V12 协议\",\n    \"author_id\": 42488585,\n    \"homepage\": \"https://onebot.adapters.nonebot.dev/\",\n    \"tags\": [],\n    \"is_official\": true\n  },\n  {\n    \"module_name\": \"nonebot.adapters.console\",\n    \"project_link\": \"nonebot-adapter-console\",\n    \"name\": \"Console\",\n    \"desc\": \"基于终端的交互式适配器\",\n    \"author_id\": 50488999,\n    \"homepage\": \"https://github.com/nonebot/adapter-console\",\n    \"tags\": [],\n    \"is_official\": true\n  },\n  {\n    \"module_name\": \"nonebot.adapters.github\",\n    \"project_link\": \"nonebot-adapter-github\",\n    \"name\": \"GitHub\",\n    \"desc\": \"GitHub APP & OAuth APP integration\",\n    \"author_id\": 42488585,\n    \"homepage\": \"https://github.com/nonebot/adapter-github\",\n    \"tags\": [],\n    \"is_official\": true\n  },\n  {\n    \"module_name\": \"nonebot.adapters.ntchat\",\n    \"project_link\": \"nonebot-adapter-ntchat\",\n    \"name\": \"Ntchat\",\n    \"desc\": \"pc hook的微信客户端适配\",\n    \"author_id\": 37363867,\n    \"homepage\": \"https://github.com/JustUndertaker/adapter-ntchat\",\n    \"tags\": [\n      {\n        \"label\": \"微信\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot.adapters.minecraft\",\n    \"project_link\": \"nonebot-adapter-minecraft\",\n    \"name\": \"Minecraft\",\n    \"desc\": \"MineCraft通信适配，支持Rcon\",\n    \"author_id\": 54731914,\n    \"homepage\": \"https://github.com/17TheWord/nonebot-adapter-minecraft\",\n    \"tags\": [\n      {\n        \"label\": \"Minecraft\",\n        \"color\": \"#4ef0ea\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot.adapters.bilibili\",\n    \"project_link\": \"nonebot-adapter-bilibili\",\n    \"name\": \"BilibiliLive\",\n    \"desc\": \"b站直播间ws协议\",\n    \"author_id\": 39620657,\n    \"homepage\": \"https://github.com/wwweww/adapter-bilibili\",\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_adapter_walleq\",\n    \"project_link\": \"nonebot-adapter-walleq\",\n    \"name\": \"Walle-Q\",\n    \"desc\": \"内置 QQ 协议实现\",\n    \"author_id\": 18395948,\n    \"homepage\": \"https://github.com/onebot-walle/nonebot_adapter_walleq\",\n    \"tags\": [\n      {\n        \"label\": \"QQ\",\n        \"color\": \"#34a9cc\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot.adapters.villa\",\n    \"project_link\": \"nonebot-adapter-villa\",\n    \"name\": \"大别野\",\n    \"desc\": \"米游社大别野官方Bot适配\",\n    \"author_id\": 63870437,\n    \"homepage\": \"https://github.com/CMHopeSunshine/nonebot-adapter-villa\",\n    \"tags\": [\n      {\n        \"label\": \"米哈游\",\n        \"color\": \"#e10909\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot.adapters.red\",\n    \"project_link\": \"nonebot-adapter-red\",\n    \"name\": \"RedProtocol\",\n    \"desc\": \"QQNT RedProtocol 适配\",\n    \"author_id\": 55650833,\n    \"homepage\": \"https://github.com/nonebot/adapter-red\",\n    \"tags\": [],\n    \"is_official\": true\n  },\n  {\n    \"module_name\": \"nonebot.adapters.discord\",\n    \"project_link\": \"nonebot-adapter-discord\",\n    \"name\": \"Discord\",\n    \"desc\": \"Discord 官方 Bot 协议适配\",\n    \"author_id\": 63870437,\n    \"homepage\": \"https://github.com/nonebot/adapter-discord\",\n    \"tags\": [],\n    \"is_official\": true\n  },\n  {\n    \"module_name\": \"nonebot.adapters.satori\",\n    \"project_link\": \"nonebot-adapter-satori\",\n    \"name\": \"Satori\",\n    \"desc\": \"Satori 协议适配器\",\n    \"author_id\": 42648639,\n    \"homepage\": \"https://github.com/nonebot/adapter-satori\",\n    \"tags\": [\n      {\n        \"label\": \"跨平台\",\n        \"color\": \"#bf40bf\"\n      }\n    ],\n    \"is_official\": true\n  },\n  {\n    \"module_name\": \"nonebot.adapters.dodo\",\n    \"project_link\": \"nonebot-adapter-dodo\",\n    \"name\": \"DoDo\",\n    \"desc\": \"DoDo Bot 协议适配器\",\n    \"author_id\": 63870437,\n    \"homepage\": \"https://github.com/nonebot/adapter-dodo\",\n    \"tags\": [],\n    \"is_official\": true\n  },\n  {\n    \"module_name\": \"nonebot.adapters.rocketchat\",\n    \"project_link\": \"nonebot-adapter-rocketchat\",\n    \"name\": \"RocketChat\",\n    \"desc\": \"RocketChat adapter for nonebot2\",\n    \"author_id\": 78360471,\n    \"homepage\": \"https://github.com/IUnlimit/nonebot-adapter-rocketchat\",\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot.adapters.kritor\",\n    \"project_link\": \"nonebot-adapter-kritor\",\n    \"name\": \"Kritor\",\n    \"desc\": \"Kritor 协议适配\",\n    \"author_id\": 42648639,\n    \"homepage\": \"https://github.com/nonebot/adapter-kritor\",\n    \"tags\": [\n      {\n        \"label\": \"QQNT\",\n        \"color\": \"#35a7c9\"\n      }\n    ],\n    \"is_official\": true\n  },\n  {\n    \"module_name\": \"nonebot_adapter_tailchat\",\n    \"project_link\": \"nonebot-adapter-tailchat\",\n    \"name\": \"Tailchat\",\n    \"desc\": \"Tailchat 适配器\",\n    \"author_id\": 61458340,\n    \"homepage\": \"https://github.com/eya46/nonebot-adapter-tailchat\",\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot.adapters.mail\",\n    \"project_link\": \"nonebot-adapter-mail\",\n    \"name\": \"Mail\",\n    \"desc\": \"邮件收发协议\",\n    \"author_id\": 44370805,\n    \"homepage\": \"https://github.com/mobyw/nonebot-adapter-mail\",\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot.adapters.heybox\",\n    \"project_link\": \"nonebot-adapter-heybox\",\n    \"name\": \"黑盒语音\",\n    \"desc\": \"黑盒语音机器人适配\",\n    \"author_id\": 54730982,\n    \"homepage\": \"https://github.com/lclbm/adapter-heybox\",\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot.adapters.wxmp\",\n    \"project_link\": \"nonebot-adapter-wxmp\",\n    \"name\": \"WXMP\",\n    \"desc\": \"微信公众平台 客服适配器\",\n    \"author_id\": 60175467,\n    \"homepage\": \"https://github.com/YangRucheng/nonebot-adapter-wxmp\",\n    \"tags\": [\n      {\n        \"label\": \"微信公众平台\",\n        \"color\": \"#843dbc\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot.adapters.milky\",\n    \"project_link\": \"nonebot-adapter-milky\",\n    \"name\": \"nonebot-adapter-milky\",\n    \"desc\": \"Milky 协议适配器\",\n    \"author_id\": 42648639,\n    \"homepage\": \"https://github.com/nonebot/adapter-milky\",\n    \"tags\": [],\n    \"is_official\": true\n  },\n  {\n    \"module_name\": \"nonebot.adapters.efchat\",\n    \"project_link\": \"nonebot-adapter-efchat\",\n    \"name\": \"nonebot-adapter-efchat\",\n    \"desc\": \"适用于EFChat(恒五聊)聊天室的nonebot适配器\",\n    \"author_id\": 104612722,\n    \"homepage\": \"https://github.com/molanp/nonebot_adapter_efchat\",\n    \"tags\": [\n      {\n        \"label\": \"EFChat\",\n        \"color\": \"#ac6161\"\n      },\n      {\n        \"label\": \"恒五聊\",\n        \"color\": \"#cc99ff\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot.adapters.vocechat\",\n    \"project_link\": \"nonebot-adapter-vocechat\",\n    \"name\": \"nonebot-adapter-vocechat\",\n    \"desc\": \"Vocechat 协议适配器\",\n    \"author_id\": 56059687,\n    \"homepage\": \"https://github.com/5656565566/nonebot-adapter-vocechat\",\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot.adapters.bilibili_live\",\n    \"project_link\": \"nonebot-adapter-bilibili-live\",\n    \"name\": \"B站直播间\",\n    \"desc\": \"B 站直播间协议（Web API/开放平台）支持\",\n    \"author_id\": 68982190,\n    \"homepage\": \"https://github.com/MingxuanGame/nonebot-adapter-bilibili-live\",\n    \"tags\": [\n      {\n        \"label\": \"bilibili\",\n        \"color\": \"#ff6699\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"yunhu\",\n    \"project_link\": \"nonebot-adapter-yunhu\",\n    \"name\": \"云湖适配器\",\n    \"desc\": \"云湖的NoneBot适配器\",\n    \"author_id\": 104612722,\n    \"homepage\": \"https://github.com/molanp/nonebot-adapter-yunhu\",\n    \"tags\": [\n      {\n        \"label\": \"云湖\",\n        \"color\": \"#8a74eb\"\n      }\n    ],\n    \"is_official\": false\n  },\n]\n"
  },
  {
    "path": "assets/bots.json5",
    "content": "[\n  {\n    \"name\": \"HarukaBot\",\n    \"desc\": \"将B站UP主的动态和直播信息推送至QQ\",\n    \"author_id\": 36433929,\n    \"homepage\": \"https://github.com/SK-415/HarukaBot\",\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"Omega Miya\",\n    \"desc\": \"B站推送Pixiv搜图识番求签抽卡表情包还有其他杂七杂八的功能\",\n    \"author_id\": 41713304,\n    \"homepage\": \"https://github.com/Ailitonia/omega-miya\",\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"Github Bot\",\n    \"desc\": \"在QQ获取/处理Github repo/pr/issue\",\n    \"author_id\": 42488585,\n    \"homepage\": \"https://github.com/cscs181/QQ-GitHub-Bot\",\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"YanXiBot\",\n    \"desc\": \"动漫资源查找与娱乐机器人\",\n    \"author_id\": 50488999,\n    \"homepage\": \"https://github.com/Melodyknit/YanXiBot\",\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"绪山真寻bot\",\n    \"desc\": \"含有不少的娱乐功能同时稍稍有一些实用的功能 :P\",\n    \"author_id\": 45528451,\n    \"homepage\": \"https://github.com/HibiKier/zhenxun_bot\",\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"ATRI\",\n    \"desc\": \"高性能文爱萝卜子，糅杂了各类有趣小功能\",\n    \"author_id\": 37587870,\n    \"homepage\": \"https://github.com/Kyomotoi/ATRI\",\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"dumbot傻瓜机器人\",\n    \"desc\": \"猜一猜游戏、新闻一览、英文每日一词一短语等等，含一键启动及docker容器部署就绪\",\n    \"author_id\": 52522252,\n    \"homepage\": \"https://github.com/ffreemt/koyeb-nb2\",\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"DicePP\",\n    \"desc\": \"TRPG骰娘, 带先攻, 查询等功能, 主要面向DND5E. 面对骰主推出的船新版本, 内置Windows/Linux详细部署指南以及方便的自定义骰娘方法, 从回复文本到查询资料库都可轻松配置~\",\n    \"author_id\": 88259371,\n    \"homepage\": \"https://github.com/pear-studio/nonebot-dicepp\",\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"SetuBot\",\n    \"desc\": \"每个群配置文件独立,可以控制频率,socks http代理,R18开关,支持多tag,自建API lolicon Pixiv热度榜\",\n    \"author_id\": 39484884,\n    \"homepage\": \"https://github.com/yuban10703/setu-nonebot2\",\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"剑网三bot\",\n    \"desc\": \"网络游戏《剑侠情缘三》的群聊机器人，数据使用：www.jx3api.com\",\n    \"author_id\": 37363867,\n    \"homepage\": \"https://github.com/JustUndertaker/mini_jx3_bot\",\n    \"tags\": [\n      {\n        \"label\": \"剑网三\",\n        \"color\": \"#5393ec\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"PixivBot\",\n    \"desc\": \"顾名思义是Pixiv的bot（随机推荐插画、随机指定关键词插画、随机书签、查看排行榜、查看指定id插画）\",\n    \"author_id\": 17331698,\n    \"homepage\": \"https://github.com/ssttkkl/PixivBot\",\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"SeaBot_QQ\",\n    \"desc\": \"一个能够获取新闻资讯并推送至QQ的群聊机器人。\",\n    \"author_id\": 31682561,\n    \"homepage\": \"https://github.com/B1ue1nWh1te/SeaBot_QQ\",\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"琪露诺Bot\",\n    \"desc\": \"用QQ机器人控制Minecraft服务器！服务器状态查询/服务器白名单/插件列表/玩家查询/转发服务器消息/执行指令...   其他实用娱乐功能，三步即可成功部署的QQ bot\",\n    \"author_id\": 56951617,\n    \"homepage\": \"https://github.com/summerkirakira/CirnoBot\",\n    \"tags\": [\n      {\n        \"label\": \"Minecraft\",\n        \"color\": \"#5393ec\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"Inkar Suki\",\n    \"desc\": \"一个十分方便的Bot，支持包括Webhook、群管、剑网3等一系列功能，持续更新中……\",\n    \"author_id\": 68726147,\n    \"homepage\": \"https://github.com/HornCopper/Inkar-Suki\",\n    \"tags\": [\n      {\n        \"label\": \"Minecraft\",\n        \"color\": \"#d03790\"\n      },\n      {\n        \"label\": \"GitHub\",\n        \"color\": \"#374fd0\"\n      },\n      {\n        \"label\": \"剑网3\",\n        \"color\": \"#ff0033\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"屑岛风Bot\",\n    \"desc\": \"自家用屑Bot\",\n    \"author_id\": 71873002,\n    \"homepage\": \"https://github.com/kexue-z/Dao-bot\",\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"LiteyukiBot-轻雪机器人\",\n    \"desc\": \"一个有各种琐事功能的bot，有AI接口，能陪聊\",\n    \"author_id\": 79104275,\n    \"homepage\": \"https://github.com/snowyfirefly/Liteyuki\",\n    \"tags\": [\n      {\n        \"label\": \"可爱\",\n        \"color\": \"#ffc0cb\"\n      },\n      {\n        \"label\": \"AI\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"nya_bot\",\n    \"desc\": \"喵服——战魂铭人联机服务器兼机器人\",\n    \"author_id\": 31379266,\n    \"homepage\": \"https://github.com/nikissXI/nya_bot\",\n    \"tags\": [\n      {\n        \"label\": \"战魂铭人\",\n        \"color\": \"#25aaf4\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"真宵Bot\",\n    \"desc\": \"专注群聊的QQ机器人\",\n    \"author_id\": 71173418,\n    \"homepage\": \"https://github.com/Shine-Light/Nonebot_Bot_MayaFey\",\n    \"tags\": [\n      {\n        \"label\": \"QQ\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"娱乐\",\n        \"color\": \"#a46e49\"\n      },\n      {\n        \"label\": \"群管\",\n        \"color\": \"#41aecb\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"SkadiBot\",\n    \"desc\": \"明日方舟主题机器人—斯卡蒂\",\n    \"author_id\": 101615359,\n    \"homepage\": \"https://github.com/yuyuziYYZ/skadi_bot\",\n    \"tags\": [\n      {\n        \"label\": \"明日方舟\",\n        \"color\": \"#a48888\"\n      },\n      {\n        \"label\": \"斯卡蒂\",\n        \"color\": \"#a48888\"\n      },\n      {\n        \"label\": \"arknights\",\n        \"color\": \"#a48888\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"小白机器人\",\n    \"desc\": \"一个高度依赖数据库的群管理机器人\",\n    \"author_id\": 69745333,\n    \"homepage\": \"https://github.com/SDIJF1521/qqai\",\n    \"tags\": [\n      {\n        \"label\": \"群管理\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"新人作品\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"LittlePaimon\",\n    \"desc\": \"小派蒙，多功能原神机器人。\",\n    \"author_id\": 63870437,\n    \"homepage\": \"https://github.com/CMHopeSunshine/LittlePaimon\",\n    \"tags\": [\n      {\n        \"label\": \"原神\",\n        \"color\": \"#7a52ea\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"IdhagnBot\",\n    \"desc\": \"🐱🤖 一个以娱乐功能为主的缝合怪（划掉）QQ机器人，包含一定Furry要素但是不会卖萌（就是逊啦！）\",\n    \"author_id\": 17371317,\n    \"homepage\": \"https://github.com/su226/IdhagnBot\",\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"hsbot\",\n    \"desc\": \"服务于《炉石传说》玩家的机器人，上线至今已有加入十余个个炉石相关群聊，上千名用户使用，响应请求数万次。 数据使用：HSreplay, Fbigame, Hearthstone API\",\n    \"author_id\": 67055520,\n    \"homepage\": \"https://github.com/gzy02/hsbot\",\n    \"tags\": [\n      {\n        \"label\": \"炉石传说\",\n        \"color\": \"#526fea\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"Bread Dog Bot\",\n    \"desc\": \"Terraria TShock QQ 机器人\",\n    \"author_id\": 160252668,\n    \"homepage\": \"https://github.com/Qianyiovo/bread_dog_bot\",\n    \"tags\": [\n      {\n        \"label\": \"TShock\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"泰拉瑞亚\",\n        \"color\": \"#5dea52\"\n      },\n      {\n        \"label\": \"Terraria\",\n        \"color\": \"#5dea52\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"RanBot\",\n    \"desc\": \"不@会很安静的Bot\",\n    \"author_id\": 88923783,\n    \"homepage\": \"https://github.com/Hecatia-Hell-Workshop/RanBot\",\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"辞辞(cici)Bot\",\n    \"desc\": \"一个集成娱乐和群管为一体的机器人\",\n    \"author_id\": 90902259,\n    \"homepage\": \"https://github.com/mengxinyuan638/cici-bot\",\n    \"tags\": [\n      {\n        \"label\": \"辞辞Bot\",\n        \"color\": \"#04de4d\"\n      },\n      {\n        \"label\": \"萌新源\",\n        \"color\": \"#fd1c06\"\n      },\n      {\n        \"label\": \"群管\",\n        \"color\": \"#06b8fd\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"SuzunoBot\",\n    \"desc\": \"多功能音游bot，主要服务maimaiDX、Arcaea\",\n    \"author_id\": 29980586,\n    \"homepage\": \"https://github.com/Rinfair-CSP-A016/SuzunoBot-AGLAS\",\n    \"tags\": [\n      {\n        \"label\": \"maimaiDX\",\n        \"color\": \"#189ede\"\n      },\n      {\n        \"label\": \"Arcaea\",\n        \"color\": \"#d551ef\"\n      },\n      {\n        \"label\": \"coc\",\n        \"color\": \"#7fe4d0\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"青岚\",\n    \"desc\": \"基于NoneBot的与Minecraft Server互通消息的机器人\",\n    \"author_id\": 54731914,\n    \"homepage\": \"https://github.com/17TheWord/qinglan_bot\",\n    \"tags\": [\n      {\n        \"label\": \"MineCraft\",\n        \"color\": \"#4ef0ea\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"ChensQBOTv2\",\n    \"desc\": \"多功能QQ群机器人，权限管理/联ban/社工等等等等，以及拥有一个强大的开发者\",\n    \"author_id\": 116929900,\n    \"homepage\": \"https://github.com/cnchens/ChensQBOTv2\",\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"koishi\",\n    \"desc\": \"支持爬取 codeforces, atcoder, 牛客上程序设计赛事的 bot。\",\n    \"author_id\": 71639222,\n    \"homepage\": \"https://github.com/CupidsBow/koishi\",\n    \"tags\": [\n      {\n        \"label\": \"acm\",\n        \"color\": \"#f71d1d\"\n      },\n      {\n        \"label\": \"codeforces\",\n        \"color\": \"#1df721\"\n      },\n      {\n        \"label\": \"atcoder\",\n        \"color\": \"#aa1df7\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"脑积水\",\n    \"desc\": \"一个超级缝合怪...\",\n    \"author_id\": 66541860,\n    \"homepage\": \"https://github.com/zhulinyv/NJS\",\n    \"tags\": [\n      {\n        \"label\": \"脑积水\",\n        \"color\": \"#ff00ac\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"LOVE酱\",\n    \"desc\": \"为铁锈战争游戏群服务的虚拟少女,内置了爬取铁锈房间列表功能,以及游戏内单位查询功能,并制作了教学系统以及铁锈相关游戏群的收集功能。\",\n    \"author_id\": 106828088,\n    \"homepage\": \"https://github.com/allureluoli/LOVE-\",\n    \"tags\": [\n      {\n        \"label\": \"铁锈战争\",\n        \"color\": \"#19e229\"\n      },\n      {\n        \"label\": \"RW\",\n        \"color\": \"#19e229\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"fubot\",\n    \"desc\": \"基于nonebot与go-cqhttp的QQ娱乐bot，提供群日常娱乐功能与舞萌DX游戏相关的信息查询功能。\",\n    \"author_id\": 54059896,\n    \"homepage\": \"https://github.com/HCskia/fu-Bot\",\n    \"tags\": [\n      {\n        \"label\": \"maimai\",\n        \"color\": \"#52eaa5\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"桃桃酱\",\n    \"desc\": \"一个会拆家的高性能缝合萝卜子\",\n    \"author_id\": 107618388,\n    \"homepage\": \"https://github.com/tkgs0/Momoko\",\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"CoolQBot\",\n    \"desc\": \"基于 NoneBot2 的聊天机器人\",\n    \"author_id\": 5219550,\n    \"homepage\": \"https://github.com/he0119/CoolQBot\",\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"XDbot2\",\n    \"desc\": \"简单的QQ功能型机器人\",\n    \"author_id\": 104149371,\n    \"homepage\": \"https://github.com/ITCraftDevelopmentTeam/XDbot2\",\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"March7th\",\n    \"desc\": \"三月七 - 崩坏：星穹铁道机器人\",\n    \"author_id\": 44370805,\n    \"homepage\": \"https://github.com/Mar-7th/March7th\",\n    \"tags\": [\n      {\n        \"label\": \"StarRail\",\n        \"color\": \"#5a8ccc\"\n      },\n      {\n        \"label\": \"星穹铁道\",\n        \"color\": \"#6faec6\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"ay机器人\",\n    \"desc\": \"codeforces和洛谷卷王监视、股票监控、ai聊天\",\n    \"author_id\": 77315378,\n    \"homepage\": \"https://github.com/863109569/qqbot\",\n    \"tags\": [\n      {\n        \"label\": \"acm\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"洛谷\",\n        \"color\": \"#81ea52\"\n      },\n      {\n        \"label\": \"codeforces\",\n        \"color\": \"#5261ea\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"狐尾\",\n    \"desc\": \"一个整合了兽云祭api的机器人，支持账号令牌操作，以及上传兽图\",\n    \"author_id\": 99388013,\n    \"homepage\": \"https://github.com/bingqiu456/shouyun\",\n    \"tags\": [\n      {\n        \"label\": \"shouyun\",\n        \"color\": \"#52ea7a\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"ReimeiBot-黎明机器人\",\n    \"desc\": \"流星飞逝，黎明终将到来。\",\n    \"author_id\": 65395090,\n    \"homepage\": \"https://github.com/3rdBit/ReimeiBot\",\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"web_bot\",\n    \"desc\": \"把机器人搬到网络上\",\n    \"author_id\": 63489103,\n    \"homepage\": \"https://github.com/wsdtl/web_bot\",\n    \"tags\": [\n      {\n        \"label\": \"xiaonan\",\n        \"color\": \"#775151\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"林汐\",\n    \"desc\": \"多平台功能型Bot\",\n    \"author_id\": 110453675,\n    \"homepage\": \"https://github.com/netsora/SoraBot\",\n    \"tags\": [\n      {\n        \"label\": \"QQ频道\",\n        \"color\": \"#f47070\"\n      },\n      {\n        \"label\": \"OneBot v11\",\n        \"color\": \"#212121\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"米缸\",\n    \"desc\": \"基于nonebot2的米缸Bot\",\n    \"author_id\": 13503375,\n    \"homepage\": \"https://github.com/LambdaYH/MigangBot\",\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"不正经的妹妹\",\n    \"desc\": \"一款功能丰富、简单易用、自定义性强、扩展性强的可爱的QQ娱乐机器人\",\n    \"author_id\": 104713034,\n    \"homepage\": \"https://github.com/itsevin/sister_bot\",\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"星见Kirami\",\n    \"desc\": \"🌟 读作 Kirami，写作星见，简明轻快的聊天机器人应用。\",\n    \"author_id\": 66513481,\n    \"homepage\": \"https://kiramibot.dev/\",\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"OCNbot\",\n    \"desc\": \"OI Contest Notifier bot，一个可以推送洛谷、cf、atcoder、牛客比赛通知的bot\",\n    \"author_id\": 91535478,\n    \"homepage\": \"https://github.com/ACnoway/OCNbot\",\n    \"tags\": [\n      {\n        \"label\": \"OI\",\n        \"color\": \"#2fccff\"\n      },\n      {\n        \"label\": \"ACM\",\n        \"color\": \"#ff0004\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"妃爱\",\n    \"desc\": \"超可爱的妃爱QQ群聊机器人\",\n    \"author_id\": 52267304,\n    \"homepage\": \"https://github.com/jiangyuxiaoxiao/Hiyori\",\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"芙芙\",\n    \"desc\": \"供 Mooncell Wiki 协作使用的跨平台机器人\",\n    \"author_id\": 14922941,\n    \"homepage\": \"https://github.com/MooncellWiki/BotFooChan\",\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"Sakiko\",\n    \"desc\": \"基于 LiteLoaderBDS 的 Minecraft 基岩版 Bot\",\n    \"author_id\": 55650833,\n    \"homepage\": \"https://github.com/zhaomaoniu/Sakiko\",\n    \"tags\": [\n      {\n        \"label\": \"Minecraft\",\n        \"color\": \"#6cc349\"\n      },\n      {\n        \"label\": \"BanGDream\",\n        \"color\": \"#e70050\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"Minecraft_QQBot\",\n    \"desc\": \"基于 NoneBot2 的 Minecraft 群服互联 QQ 机器人，支持多服务器多种方式连接。\",\n    \"author_id\": 90964775,\n    \"homepage\": \"https://github.com/Minecraft-QQBot/BotServer\",\n    \"tags\": [\n      {\n        \"label\": \"Minecraft\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"娱乐\",\n        \"color\": \"#37a7e7\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"小安提Bot\",\n    \"desc\": \"服务于音游 舞萌DX 的多功能Bot\",\n    \"author_id\": 186144551,\n    \"homepage\": \"https://github.com/Ant1816/Ant1Bot\",\n    \"tags\": [\n      {\n        \"label\": \"maimaiDX\",\n        \"color\": \"#52ea9a\"\n      },\n      {\n        \"label\": \"音游\",\n        \"color\": \"#f74b18\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"CanrotBot\",\n    \"desc\": \"有很多实用功能的bot，也有很多没什么用的娱乐功能；接入了大模型，并且有一部分功能可以被大模型调用。主打一个全都有（\",\n    \"author_id\": 18070676,\n    \"homepage\": \"https://github.com/wangyw15/CanrotBot\",\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"Mio澪\",\n    \"desc\": \"超可爱多功能Qbot\",\n    \"author_id\": 50508678,\n    \"homepage\": \"https://github.com/EienSakura/mio\",\n    \"tags\": [\n      {\n        \"label\": \"娱乐\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"AntiFraudBot\",\n    \"desc\": \"反诈机器人\",\n    \"author_id\": 104713034,\n    \"homepage\": \"https://github.com/itsevin/AntiFraudBot\",\n    \"tags\": [\n      {\n        \"label\": \"反诈\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"Nekro Agent Bot\",\n    \"desc\": \"基于生成式人工智能与沙盒技术的 Nekro Agent 代理执行 AI 机器人，支持聊天、识图、通用文件处理等扩展能力，提供了 WebUI 维护界面、一键部署脚本\",\n    \"author_id\": 57167362,\n    \"homepage\": \"https://github.com/KroMiose/nekro-agent\",\n    \"tags\": [\n      {\n        \"label\": \"聊天\",\n        \"color\": \"#c65856\"\n      },\n      {\n        \"label\": \"大模型\",\n        \"color\": \"#46a34c\"\n      },\n      {\n        \"label\": \"WebUI\",\n        \"color\": \"#4a96c6\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"PickStarsBot\",\n    \"desc\": \"欢迎使用PickStarsBot！这是一款基于NoneBot2构建的智能QQ机器人，提供丰富的功能，包括一言、历史上的今天、60秒早报等，快来试试吧！\",\n    \"author_id\": 183461085,\n    \"homepage\": \"https://github.com/PickStars308/PickStarsBot\",\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"LiteBot\",\n    \"desc\": \"Web功能/MC/数据功能Bot\",\n    \"author_id\": 67693593,\n    \"homepage\": \"https://github.com/LiteSuggarDEV/LiteBot-NEO/\",\n    \"tags\": [\n      {\n        \"label\": \"Minecraft\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"Web\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"Muicebot\",\n    \"desc\": \"Muice-Chatbot 的 Nonebot2 实现，支持调用主流大模型，支持 Function Call 和内置 MCP Host 实现\",\n    \"author_id\": 72406624,\n    \"homepage\": \"https://github.com/Moemu/MuiceBot\",\n    \"tags\": [\n      {\n        \"label\": \"LLM\",\n        \"color\": \"#3e97ff\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"nsybot\",\n    \"desc\": \"定时获取推特/bilibili等平台用户文章并推送到QQ群\",\n    \"author_id\": 148176849,\n    \"homepage\": \"https://github.com/AhsokaTano26/nsybot\",\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"Amrita\",\n    \"desc\": \"LLM聊天机器人框架\",\n    \"author_id\": 67693593,\n    \"homepage\": \"https://github.com/LiteSuggarDEV/Amrita\",\n    \"tags\": [\n      {\n        \"label\": \"聊天\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"LLM\",\n        \"color\": \"#5c86db\"\n      },\n      {\n        \"label\": \"快捷部署\",\n        \"color\": \"#eebe0b\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"name\": \"Rosmontis.io\",\n    \"desc\": \"简单的机器人\",\n    \"author_id\": 225668725,\n    \"homepage\": \"https://github.com/com-wuqi/Rosmontis.io\",\n    \"tags\": [\n      {\n        \"label\": \"可爱\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n]\n"
  },
  {
    "path": "assets/drivers.json5",
    "content": "[\n  {\n    \"module_name\": \"~none\",\n    \"project_link\": \"\",\n    \"name\": \"None\",\n    \"desc\": \"None 驱动器\",\n    \"author_id\": 42488585,\n    \"homepage\": \"/docs/advanced/driver\",\n    \"tags\": [],\n    \"is_official\": true\n  },\n  {\n    \"module_name\": \"~fastapi\",\n    \"project_link\": \"nonebot2[fastapi]\",\n    \"name\": \"FastAPI\",\n    \"desc\": \"FastAPI 驱动器\",\n    \"author_id\": 42488585,\n    \"homepage\": \"/docs/advanced/driver\",\n    \"tags\": [],\n    \"is_official\": true\n  },\n  {\n    \"module_name\": \"~quart\",\n    \"project_link\": \"nonebot2[quart]\",\n    \"name\": \"Quart\",\n    \"desc\": \"Quart 驱动器\",\n    \"author_id\": 42488585,\n    \"homepage\": \"/docs/advanced/driver\",\n    \"tags\": [],\n    \"is_official\": true\n  },\n  {\n    \"module_name\": \"~httpx\",\n    \"project_link\": \"nonebot2[httpx]\",\n    \"name\": \"HTTPX\",\n    \"desc\": \"HTTPX 驱动器\",\n    \"author_id\": 42488585,\n    \"homepage\": \"/docs/advanced/driver\",\n    \"tags\": [],\n    \"is_official\": true\n  },\n  {\n    \"module_name\": \"~websockets\",\n    \"project_link\": \"nonebot2[websockets]\",\n    \"name\": \"websockets\",\n    \"desc\": \"websockets 驱动器\",\n    \"author_id\": 42488585,\n    \"homepage\": \"/docs/advanced/driver\",\n    \"tags\": [],\n    \"is_official\": true\n  },\n  {\n    \"module_name\": \"~aiohttp\",\n    \"project_link\": \"nonebot2[aiohttp]\",\n    \"name\": \"AIOHTTP\",\n    \"desc\": \"AIOHTTP 驱动器\",\n    \"author_id\": 42488585,\n    \"homepage\": \"/docs/advanced/driver\",\n    \"tags\": [],\n    \"is_official\": true\n  },\n]\n"
  },
  {
    "path": "assets/plugins.json5",
    "content": "[\n  {\n    \"module_name\": \"nonebot_plugin_status\",\n    \"project_link\": \"nonebot-plugin-status\",\n    \"author_id\": 42488585,\n    \"tags\": [\n      {\n        \"label\": \"server\",\n        \"color\": \"#aeeaa8\"\n      }\n    ],\n    \"is_official\": true\n  },\n  {\n    \"module_name\": \"haruka_bot\",\n    \"project_link\": \"haruka-bot\",\n    \"author_id\": 36433929,\n    \"tags\": [\n      {\n        \"label\": \"bilibili\",\n        \"color\": \"#e55d80\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_rauthman\",\n    \"project_link\": \"nonebot-plugin-rauthman\",\n    \"author_id\": 59906398,\n    \"tags\": [\n      {\n        \"label\": \"rule\",\n        \"color\": \"#4ec9b0\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_docs\",\n    \"project_link\": \"nonebot-plugin-docs\",\n    \"author_id\": 63496654,\n    \"tags\": [],\n    \"is_official\": true\n  },\n  {\n    \"module_name\": \"nonebot_plugin_sentry\",\n    \"project_link\": \"nonebot-plugin-sentry\",\n    \"author_id\": 42488585,\n    \"tags\": [\n      {\n        \"label\": \"log\",\n        \"color\": \"#6be3ea\"\n      }\n    ],\n    \"is_official\": true\n  },\n  {\n    \"module_name\": \"nonebot_plugin_apscheduler\",\n    \"project_link\": \"nonebot-plugin-apscheduler\",\n    \"author_id\": 42488585,\n    \"tags\": [],\n    \"is_official\": true\n  },\n  {\n    \"module_name\": \"nonebot_plugin_picsearcher\",\n    \"project_link\": \"nonebot-plugin-picsearcher\",\n    \"author_id\": 50922489,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_navicat\",\n    \"project_link\": \"nonebot-plugin-navicat\",\n    \"author_id\": 50922489,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_translator\",\n    \"project_link\": \"nonebot-plugin-translator\",\n    \"author_id\": 59906398,\n    \"tags\": [\n      {\n        \"label\": \"func\",\n        \"color\": \"#dcdcaa\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_mqtt\",\n    \"project_link\": \"nonebot-plugin-mqtt\",\n    \"author_id\": 50922489,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_songpicker2\",\n    \"project_link\": \"nonebot-plugin-songpicker2\",\n    \"author_id\": 20412597,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_strman\",\n    \"project_link\": \"nonebot-plugin-strman\",\n    \"author_id\": 95678113,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_bison\",\n    \"project_link\": \"nonebot-bison\",\n    \"author_id\": 23295345,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot-plugin-ncm\",\n    \"project_link\": \"nonebot-plugin-ncm\",\n    \"author_id\": 68675068,\n    \"tags\": [\n      {\n        \"label\": \"Netease\",\n        \"color\": \"#ec4141\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_cocdicer\",\n    \"project_link\": \"nonebot-plugin-cocdicer\",\n    \"author_id\": 18395948,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_guess\",\n    \"project_link\": \"nonebot-plugin-guess\",\n    \"author_id\": 52522252,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_abbrreply\",\n    \"project_link\": \"nonebot-plugin-abbrreply\",\n    \"author_id\": 49887895,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_biliav\",\n    \"project_link\": \"nonebot_plugin_biliav\",\n    \"author_id\": 9247530,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_analysis_bilibili\",\n    \"project_link\": \"nonebot-plugin-analysis-bilibili\",\n    \"author_id\": 36481080,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_localstore\",\n    \"project_link\": \"nonebot-plugin-localstore\",\n    \"author_id\": 42488585,\n    \"tags\": [],\n    \"is_official\": true\n  },\n  {\n    \"module_name\": \"nonebot_plugin_alconna\",\n    \"project_link\": \"nonebot-plugin-alconna\",\n    \"author_id\": 42648639,\n    \"tags\": [\n      {\n        \"label\": \"多适配器\",\n        \"color\": \"#5280ea\"\n      },\n      {\n        \"label\": \"消息匹配\",\n        \"color\": \"#ea6f52\"\n      },\n      {\n        \"label\": \"跨平台\",\n        \"color\": \"#5452ea\"\n      }\n    ],\n    \"is_official\": true\n  },\n  {\n    \"module_name\": \"nonebot_plugin_mcstatus\",\n    \"project_link\": \"nonebot-plugin-mcstatus\",\n    \"author_id\": 50312681,\n    \"tags\": [\n      {\n        \"label\": \"Minecraft\",\n        \"color\": \"#80070B\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_help\",\n    \"project_link\": \"nonebot-plugin-help\",\n    \"author_id\": 41534161,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_alias\",\n    \"project_link\": \"nonebot_plugin_alias\",\n    \"author_id\": 33149974,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_withdraw\",\n    \"project_link\": \"nonebot_plugin_withdraw\",\n    \"author_id\": 33149974,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_pixivrank_search\",\n    \"project_link\": \"nonebot-plugin-pixivrank-search\",\n    \"author_id\": 45528451,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_russian\",\n    \"project_link\": \"nonebot-plugin-russian\",\n    \"author_id\": 45528451,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_setu\",\n    \"project_link\": \"nonebot-plugin-setu\",\n    \"author_id\": 63199041,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_heweather\",\n    \"project_link\": \"nonebot-plugin-heweather\",\n    \"author_id\": 71873002,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_autohelp\",\n    \"project_link\": \"nonebot-plugin-autohelp\",\n    \"author_id\": 52522252,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_flexperm\",\n    \"project_link\": \"nonebot-plugin-flexperm\",\n    \"author_id\": 13314764,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_epicfree\",\n    \"project_link\": \"nonebot-plugin-epicfree\",\n    \"author_id\": 22407052,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"ELF_RSS2\",\n    \"project_link\": \"ELF-RSS\",\n    \"author_id\": 32663291,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nb2chan\",\n    \"project_link\": \"nb2chan\",\n    \"author_id\": 16970614,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_setu_now\",\n    \"project_link\": \"nonebot-plugin-setu-now\",\n    \"author_id\": 71873002,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"leetcode\",\n    \"project_link\": \"nonebot-plugin-leetcode\",\n    \"author_id\": 32358438,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_youthstudy\",\n    \"project_link\": \"nonebot-plugin-youthstudy\",\n    \"author_id\": 63199041,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_shindan\",\n    \"project_link\": \"nonebot_plugin_shindan\",\n    \"author_id\": 33149974,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_code\",\n    \"project_link\": \"nonebot-plugin-code\",\n    \"author_id\": 51691024,\n    \"tags\": [\n      {\n        \"label\": \"func\",\n        \"color\": \"#dcdcaa\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_picsbank\",\n    \"project_link\": \"nonebot-plugin-picsbank\",\n    \"author_id\": 35657483,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_tvseries\",\n    \"project_link\": \"nonebot-plugin-tvseries\",\n    \"author_id\": 71873002,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_lolmatch\",\n    \"project_link\": \"nonebot_plugin_lolmatch\",\n    \"author_id\": 35657483,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"OlivOS.nonebot\",\n    \"project_link\": \"OlivOS.nb2\",\n    \"author_id\": 50312681,\n    \"tags\": [\n      {\n        \"label\": \"OlivOS\",\n        \"color\": \"#00a0ea\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_htmlrender\",\n    \"project_link\": \"nonebot-plugin-htmlrender\",\n    \"author_id\": 71873002,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_admin\",\n    \"project_link\": \"nonebot-plugin-admin\",\n    \"author_id\": 51691024,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_memes\",\n    \"project_link\": \"nonebot_plugin_memes\",\n    \"author_id\": 33149974,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_repeater\",\n    \"project_link\": \"nonebot-plugin-repeater\",\n    \"author_id\": 29861280,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_directlinker\",\n    \"project_link\": \"nonebot-plugin-directlinker\",\n    \"author_id\": 29861280,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_forwarder\",\n    \"project_link\": \"nonebot-plugin-forwarder\",\n    \"author_id\": 29861280,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_roll\",\n    \"project_link\": \"nonebot_plugin_roll\",\n    \"author_id\": 69038090,\n    \"tags\": [\n      {\n        \"label\": \"roll\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_crazy_thursday\",\n    \"project_link\": \"nonebot_plugin_crazy_thursday\",\n    \"author_id\": 69038090,\n    \"tags\": [\n      {\n        \"label\": \"Thursday\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_covid19_news\",\n    \"project_link\": \"nonebot-plugin-covid19-news\",\n    \"author_id\": 57753690,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_remake\",\n    \"project_link\": \"nonebot_plugin_remake\",\n    \"author_id\": 33149974,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_weather_lite\",\n    \"project_link\": \"nonebot-plugin-weather-lite\",\n    \"author_id\": 57033359,\n    \"tags\": [\n      {\n        \"label\": \"天气\",\n        \"color\": \"#6ec3d9\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_fortune\",\n    \"project_link\": \"nonebot-plugin-fortune\",\n    \"author_id\": 69038090,\n    \"tags\": [\n      {\n        \"label\": \"fortune\",\n        \"color\": \"#ea6f52\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_tarot\",\n    \"project_link\": \"nonebot_plugin_tarot\",\n    \"author_id\": 69038090,\n    \"tags\": [\n      {\n        \"label\": \"tarot\",\n        \"color\": \"#461264\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_emojimix\",\n    \"project_link\": \"nonebot_plugin_emojimix\",\n    \"author_id\": 33149974,\n    \"tags\": [\n      {\n        \"label\": \"emoji\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_what2eat\",\n    \"project_link\": \"nonebot-plugin-what2eat\",\n    \"author_id\": 69038090,\n    \"tags\": [\n      {\n        \"label\": \"what2eat\",\n        \"color\": \"#f09526\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_datastore\",\n    \"project_link\": \"nonebot-plugin-datastore\",\n    \"author_id\": 5219550,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_boardgame\",\n    \"project_link\": \"nonebot_plugin_boardgame\",\n    \"author_id\": 33149974,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_wordcloud\",\n    \"project_link\": \"nonebot-plugin-wordcloud\",\n    \"author_id\": 5219550,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_chatrecorder\",\n    \"project_link\": \"nonebot_plugin_chatrecorder\",\n    \"author_id\": 33149974,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_antiflash\",\n    \"project_link\": \"nonebot-plugin-antiflash\",\n    \"author_id\": 69038090,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_word_bank2\",\n    \"project_link\": \"nonebot-plugin-word-bank2\",\n    \"author_id\": 71873002,\n    \"tags\": [\n      {\n        \"label\": \"wordbank\",\n        \"color\": \"#0b00ff\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_txt2img\",\n    \"project_link\": \"nonebot-plugin-txt2img\",\n    \"author_id\": 44370805,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_morning\",\n    \"project_link\": \"nonebot-plugin-morning\",\n    \"author_id\": 69038090,\n    \"tags\": [\n      {\n        \"label\": \"morning\",\n        \"color\": \"#ebc025\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_pixiv\",\n    \"project_link\": \"nonebot-plugin-pixiv\",\n    \"author_id\": 49887895,\n    \"tags\": [\n      {\n        \"label\": \" pixiv\",\n        \"color\": \"#0096fa\"\n      },\n      {\n        \"label\": \"R18\",\n        \"color\": \"#ffff00\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"YetAnotherPicSearch\",\n    \"project_link\": \"YetAnotherPicSearch\",\n    \"author_id\": 23137034,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_gachalogs\",\n    \"project_link\": \"nonebot-plugin-gachalogs\",\n    \"author_id\": 22407052,\n    \"tags\": [\n      {\n        \"label\": \"Genshin\",\n        \"color\": \"#ffd49f\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_everyday_en\",\n    \"project_link\": \"nonebot-plugin-everyday-en\",\n    \"author_id\": 81250368,\n    \"tags\": [\n      {\n        \"label\": \"EveryDay\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_fire\",\n    \"project_link\": \"nonebot-plugin-fire\",\n    \"author_id\": 45707511,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_qrcode\",\n    \"project_link\": \"nonebot-plugin-qrcode\",\n    \"author_id\": 71873002,\n    \"tags\": [\n      {\n        \"label\": \"QRcode\",\n        \"color\": \"#0020ff\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_ygo\",\n    \"project_link\": \"nonebot-plugin-ygo\",\n    \"author_id\": 49887895,\n    \"tags\": [\n      {\n        \"label\": \"游戏王\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"口胡王\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"ygo\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_bilibilibot\",\n    \"project_link\": \"nonebot-plugin-bilibilibot\",\n    \"author_id\": 54183084,\n    \"tags\": [\n      {\n        \"label\": \"bilibili\",\n        \"color\": \"#f605dd\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_color\",\n    \"project_link\": \"nonebot-plugin-color\",\n    \"author_id\": 22407052,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_blackjack\",\n    \"project_link\": \"nonebot-plugin-blackjack\",\n    \"author_id\": 30517062,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_arcaeabot\",\n    \"project_link\": \"nonebot-plugin-arcaeabot\",\n    \"author_id\": 9484642,\n    \"tags\": [\n      {\n        \"label\": \"Arcaea\",\n        \"color\": \"#db52ea\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_ddcheck\",\n    \"project_link\": \"nonebot_plugin_ddcheck\",\n    \"author_id\": 33149974,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_leetcode2\",\n    \"project_link\": \"nonebot-plugin-leetcode2\",\n    \"author_id\": 30568146,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_mediawiki\",\n    \"project_link\": \"nonebot-plugin-mediawiki\",\n    \"author_id\": 68314080,\n    \"tags\": [\n      {\n        \"label\": \"wiki\",\n        \"color\": \"#679ff9\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_wordle\",\n    \"project_link\": \"nonebot_plugin_wordle\",\n    \"author_id\": 33149974,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_giyf\",\n    \"project_link\": \"nonebot-plugin-giyf\",\n    \"author_id\": 68314080,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_abstract\",\n    \"project_link\": \"nonebot-plugin-abstract\",\n    \"author_id\": 98074861,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_params\",\n    \"project_link\": \"nonebot-plugin-params\",\n    \"author_id\": 48091591,\n    \"tags\": [\n      {\n        \"label\": \"helper\",\n        \"color\": \"#ffe873\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_handle\",\n    \"project_link\": \"nonebot_plugin_handle\",\n    \"author_id\": 33149974,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_minesweeper\",\n    \"project_link\": \"nonebot_plugin_minesweeper\",\n    \"author_id\": 33149974,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_draw\",\n    \"project_link\": \"nonebot-plugin-draw\",\n    \"author_id\": 98812723,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_randomtkk\",\n    \"project_link\": \"nonebot-plugin-randomtkk\",\n    \"author_id\": 69038090,\n    \"tags\": [\n      {\n        \"label\": \"Tan Kuku\",\n        \"color\": \"#fdaf75\"\n      },\n      {\n        \"label\": \"Liyuu\",\n        \"color\": \"#465dfd\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_dida\",\n    \"project_link\": \"nonebot-plugin-dida\",\n    \"author_id\": 54183084,\n    \"tags\": [\n      {\n        \"label\": \"滴答清单\",\n        \"color\": \"#007ffd\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_alipayvoice\",\n    \"project_link\": \"nonebot-plugin-alipayvoice\",\n    \"author_id\": 66513481,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_answersbook\",\n    \"project_link\": \"nonebot-plugin-answersbook\",\n    \"author_id\": 66513481,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_hitokoto\",\n    \"project_link\": \"nonebot-plugin-hitokoto\",\n    \"author_id\": 66513481,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_bilicover\",\n    \"project_link\": \"nonebot-plugin-bilicover\",\n    \"author_id\": 66513481,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_cchess\",\n    \"project_link\": \"nonebot_plugin_cchess\",\n    \"author_id\": 33149974,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_chess\",\n    \"project_link\": \"nonebot_plugin_chess\",\n    \"author_id\": 33149974,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_charpic\",\n    \"project_link\": \"nonebot-plugin-charpic\",\n    \"author_id\": 66518048,\n    \"tags\": [\n      {\n        \"label\": \"字符画\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"多平台适配\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_miragetank\",\n    \"project_link\": \"nonebot-plugin-miragetank\",\n    \"author_id\": 66518048,\n    \"tags\": [\n      {\n        \"label\": \"幻影坦克\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"多平台适配\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_yulu\",\n    \"project_link\": \"nonebot-plugin-yulu\",\n    \"author_id\": 99388013,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_maze\",\n    \"project_link\": \"nonebot-plugin-maze\",\n    \"author_id\": 100039483,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_moyu\",\n    \"project_link\": \"nonebot-plugin-moyu\",\n    \"author_id\": 66513481,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_mockingbird\",\n    \"project_link\": \"nonebot-plugin-mockingbird\",\n    \"author_id\": 55268546,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_baidutranslate\",\n    \"project_link\": \"nonebot-plugin-baidutranslate\",\n    \"author_id\": 52584526,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_tortoise_orm\",\n    \"project_link\": \"nonebot-plugin-tortoise-orm\",\n    \"author_id\": 71873002,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_dailysign\",\n    \"project_link\": \"nonebot-plugin-dailysign\",\n    \"author_id\": 71873002,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_tetris_stats\",\n    \"project_link\": \"nonebot-plugin-tetris-stats\",\n    \"author_id\": 51957264,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_bilibili_viode\",\n    \"project_link\": \"nonebot-plugin-bilibili-viode\",\n    \"author_id\": 61133548,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_imagetools\",\n    \"project_link\": \"nonebot_plugin_imagetools\",\n    \"author_id\": 33149974,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_warframe_clock\",\n    \"project_link\": \"nonebot-plugin-warframe-clock\",\n    \"author_id\": 124094085,\n    \"tags\": [\n      {\n        \"label\": \"Warframe\",\n        \"color\": \"#149090\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"hikari_bot\",\n    \"project_link\": \"hikari-bot\",\n    \"author_id\": 48101337,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_who_at_me\",\n    \"project_link\": \"nonebot-plugin-who-at-me\",\n    \"author_id\": 9484642,\n    \"tags\": [\n      {\n        \"label\": \"群聊\",\n        \"color\": \"#52afea\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_covid_19_by\",\n    \"project_link\": \"nonebot-plugin-covid-19-by\",\n    \"author_id\": 99388013,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_reboot\",\n    \"project_link\": \"nonebot-plugin-reboot\",\n    \"author_id\": 22175295,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_setu4\",\n    \"project_link\": \"nonebot-plugin-setu4\",\n    \"author_id\": 87489040,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_smart_reply\",\n    \"project_link\": \"nonebot-plugin-smart-reply\",\n    \"author_id\": 87489040,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_today_in_history\",\n    \"project_link\": \"nonebot-plugin-today-in-history\",\n    \"author_id\": 64363680,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_BitTorrent\",\n    \"project_link\": \"nonebot-plugin-BitTorrent\",\n    \"author_id\": 87489040,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_namelist\",\n    \"project_link\": \"nonebot-plugin-namelist\",\n    \"author_id\": 66513481,\n    \"tags\": [\n      {\n        \"label\": \"黑名单\",\n        \"color\": \"#323232\"\n      },\n      {\n        \"label\": \"白名单\",\n        \"color\": \"#fafafa\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_bread_shop\",\n    \"project_link\": \"nonebot-plugin-bread-shop\",\n    \"author_id\": 62082723,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_PicMenu\",\n    \"project_link\": \"nonebot-plugin-PicMenu\",\n    \"author_id\": 61297321,\n    \"tags\": [\n      {\n        \"label\": \"menu\",\n        \"color\": \"#753dc6\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_horserace\",\n    \"project_link\": \"nonebot-plugin-horserace\",\n    \"author_id\": 105840558,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_firexN\",\n    \"project_link\": \"nonebot-plugin-firexN\",\n    \"author_id\": 94956933,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_bfinfo\",\n    \"project_link\": \"nonebot-plugin-bfinfo\",\n    \"author_id\": 94956933,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_osubot\",\n    \"project_link\": \"nonebot-plugin-osubot\",\n    \"author_id\": 30517062,\n    \"tags\": [\n      {\n        \"label\": \"OSU\",\n        \"color\": \"#eb5d9b\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_acc_calculate\",\n    \"project_link\": \"nonebot-plugin-acc-calculate\",\n    \"author_id\": 117957183,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_kawaii_robot\",\n    \"project_link\": \"nonebot-plugin-kawaii-robot\",\n    \"author_id\": 51886078,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_addFriend\",\n    \"project_link\": \"nonebot-plugin-addfriend\",\n    \"author_id\": 77319678,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_date_name\",\n    \"project_link\": \"nonebot-plugin-date-name\",\n    \"author_id\": 99388013,\n    \"tags\": [\n      {\n        \"label\": \"qun_card\",\n        \"color\": \"#e552ea\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_easyCommand\",\n    \"project_link\": \"nonebot-plugin-easycommand\",\n    \"author_id\": 77319678,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_report\",\n    \"project_link\": \"nonebot-plugin-report\",\n    \"author_id\": 61999173,\n    \"tags\": [\n      {\n        \"label\": \"webhook\",\n        \"color\": \"#51b3a8\"\n      },\n      {\n        \"label\": \"notify\",\n        \"color\": \"#3985f7\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_hammer_nbnhhsh\",\n    \"project_link\": \"nonebot-plugin-hammer-nbnhhsh\",\n    \"author_id\": 15799382,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_mcqq\",\n    \"project_link\": \"nonebot-plugin-mcqq\",\n    \"author_id\": 54731914,\n    \"tags\": [\n      {\n        \"label\": \"Minecraft\",\n        \"color\": \"#52ea6f\"\n      },\n      {\n        \"label\": \"消息互通\",\n        \"color\": \"#52eadf\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_covid_19_by_guild\",\n    \"project_link\": \"nonebot-plugin-covid-19-by-guild\",\n    \"author_id\": 99388013,\n    \"tags\": [\n      {\n        \"label\": \"疫情小助手\",\n        \"color\": \"#526fea\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_wiki\",\n    \"project_link\": \"nonebot-plugin-wiki\",\n    \"author_id\": 60338092,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_groupmanager\",\n    \"project_link\": \"nonebot-plugin-groupmanager\",\n    \"author_id\": 76118866,\n    \"tags\": [\n      {\n        \"label\": \"简易群管\",\n        \"color\": \"#53e950\"\n      },\n      {\n        \"label\": \"插件改良\",\n        \"color\": \"#2b7be2\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_game_collection\",\n    \"project_link\": \"nonebot-plugin-game-collection\",\n    \"author_id\": 51886078,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_drawer\",\n    \"project_link\": \"nonebot-plugin-drawer\",\n    \"author_id\": 35400185,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_jrrp-n\",\n    \"project_link\": \"nonebot-plugin-jrrp-n\",\n    \"author_id\": 82658163,\n    \"tags\": [\n      {\n        \"label\": \"每日人品\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"jrrp\",\n        \"color\": \"#529fea\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_moegoe\",\n    \"project_link\": \"nonebot-plugin-moegoe\",\n    \"author_id\": 10485632,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_pixivbot\",\n    \"project_link\": \"nonebot-plugin-pixivbot\",\n    \"author_id\": 17331698,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_workscore\",\n    \"project_link\": \"nonebot-plugin-workscore\",\n    \"author_id\": 51691024,\n    \"tags\": [\n      {\n        \"label\": \"工作性价比计算器\",\n        \"color\": \"#3898fc\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_treehelp\",\n    \"project_link\": \"nonebot-plugin-treehelp\",\n    \"author_id\": 5219550,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"cqsat\",\n    \"project_link\": \"nonebot-plugin-cqsat\",\n    \"author_id\": 51691024,\n    \"tags\": [\n      {\n        \"label\": \"业余无线电\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"HAM\",\n        \"color\": \"#3898fc\"\n      },\n      {\n        \"label\": \"卫星追踪\",\n        \"color\": \"#fca638\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_course\",\n    \"project_link\": \"nonebot-plugin-course\",\n    \"author_id\": 101713235,\n    \"tags\": [\n      {\n        \"label\": \"课表\",\n        \"color\": \"#6e9af2\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_dialectlist\",\n    \"project_link\": \"nonebot-plugin-dialectlist\",\n    \"author_id\": 91937041,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_offline_mahjong_helper\",\n    \"project_link\": \"nonebot-plugin-offline-mahjong-helper\",\n    \"author_id\": 30568146,\n    \"tags\": [\n      {\n        \"label\": \"Mahjong\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"雀魂\",\n        \"color\": \"#eaa452\"\n      },\n      {\n        \"label\": \"线下约桌\",\n        \"color\": \"#52a6ea\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_send\",\n    \"project_link\": \"nonebot-plugin-send\",\n    \"author_id\": 34237511,\n    \"tags\": [\n      {\n        \"label\": \"send\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"notice\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"公告\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_todo_nlp\",\n    \"project_link\": \"nonebot-plugin-todo-nlp\",\n    \"author_id\": 52662784,\n    \"tags\": [\n      {\n        \"label\": \"todo\",\n        \"color\": \"#499bdd\"\n      },\n      {\n        \"label\": \"nlp\",\n        \"color\": \"#83b279\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_wordsnorote\",\n    \"project_link\": \"nonebot-plugin-wordsnorote\",\n    \"author_id\": 94956933,\n    \"tags\": [\n      {\n        \"label\": \"四六级\",\n        \"color\": \"#24a0d8\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_CyberSensoji\",\n    \"project_link\": \"nonebot-plugin-CyberSensoji\",\n    \"author_id\": 80341233,\n    \"tags\": [\n      {\n        \"label\": \"抽签\",\n        \"color\": \"#52eadf\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_gspanel\",\n    \"project_link\": \"nonebot-plugin-gspanel\",\n    \"author_id\": 22407052,\n    \"tags\": [\n      {\n        \"label\": \"Genshin\",\n        \"color\": \"#ffd49f\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_gsmaterial\",\n    \"project_link\": \"nonebot-plugin-gsmaterial\",\n    \"author_id\": 22407052,\n    \"tags\": [\n      {\n        \"label\": \"Genshin\",\n        \"color\": \"#ffd49f\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_mystool\",\n    \"project_link\": \"nonebot-plugin-mystool\",\n    \"author_id\": 63289359,\n    \"tags\": [\n      {\n        \"label\": \"米游社\",\n        \"color\": \"#66e0ff\"\n      },\n      {\n        \"label\": \"原神\",\n        \"color\": \"#faf3c4\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_warframe\",\n    \"project_link\": \"nonebot-plugin-warframe\",\n    \"author_id\": 54731914,\n    \"tags\": [\n      {\n        \"label\": \"星际战甲\",\n        \"color\": \"#ed3f3f\"\n      },\n      {\n        \"label\": \"WarFrame\",\n        \"color\": \"#edea3f\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_mcqq_server\",\n    \"project_link\": \"nonebot-plugin-mcqq-server\",\n    \"author_id\": 51886078,\n    \"tags\": [\n      {\n        \"label\": \"Minecraft\",\n        \"color\": \"#52ea64\"\n      },\n      {\n        \"label\": \"消息互通\",\n        \"color\": \"#52e5ea\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_RealESRGAN\",\n    \"project_link\": \"nonebot-plugin-RealESRGAN\",\n    \"author_id\": 78833215,\n    \"tags\": [\n      {\n        \"label\": \"图像超分辨率重建\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot-plugin-wolf-kill\",\n    \"project_link\": \"nonebot-plugin-wolf-kill\",\n    \"author_id\": 30973981,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"iot\",\n    \"project_link\": \"nonebot-plugin-iot\",\n    \"author_id\": 70781619,\n    \"tags\": [\n      {\n        \"label\": \"物联网\",\n        \"color\": \"#4b86d7\"\n      },\n      {\n        \"label\": \"天猫精灵\",\n        \"color\": \"#4b86d7\"\n      },\n      {\n        \"label\": \"IOT\",\n        \"color\": \"#4b86d7\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_bwiki_navigator\",\n    \"project_link\": \"nonebot-plugin-bwiki-navigator\",\n    \"author_id\": 41534161,\n    \"tags\": [\n      {\n        \"label\": \"wiki\",\n        \"color\": \"#29a5e3\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_bottle\",\n    \"project_link\": \"nonebot_plugin_bottle\",\n    \"author_id\": 97968466,\n    \"tags\": [\n      {\n        \"label\": \"漂流瓶\",\n        \"color\": \"#0893f2\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_tts_gal\",\n    \"project_link\": \"nonebot-plugin-tts-gal\",\n    \"author_id\": 89716406,\n    \"tags\": [\n      {\n        \"label\": \"VITS\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_alicdk_get\",\n    \"project_link\": \"nonebot-plugin-alicdk-get\",\n    \"author_id\": 53631287,\n    \"tags\": [\n      {\n        \"label\": \"兑换码\",\n        \"color\": \"#595fd6\"\n      },\n      {\n        \"label\": \"auto\",\n        \"color\": \"#595fd6\"\n      },\n      {\n        \"label\": \"阿里云盘\",\n        \"color\": \"#595fd6\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_picstatus\",\n    \"project_link\": \"nonebot-plugin-picstatus\",\n    \"author_id\": 59048777,\n    \"tags\": [\n      {\n        \"label\": \"server\",\n        \"color\": \"#8bff00\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_tuling\",\n    \"project_link\": \"nonebot-plugin-tuling\",\n    \"author_id\": 45281765,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_makemidi\",\n    \"project_link\": \"nonebot-plugin-makemidi\",\n    \"author_id\": 79776324,\n    \"tags\": [\n      {\n        \"label\": \"midi\",\n        \"color\": \"#6515a8\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_ocr\",\n    \"project_link\": \"nonebot-plugin-ocr\",\n    \"author_id\": 111744697,\n    \"tags\": [\n      {\n        \"label\": \"ocr \",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_listener\",\n    \"project_link\": \"nonebot-plugin-listener\",\n    \"author_id\": 30973981,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_BiliRequestAll\",\n    \"project_link\": \"nonebot-plugin-BiliRequestAll\",\n    \"author_id\": 112923496,\n    \"tags\": [\n      {\n        \"label\": \"bilibili\",\n        \"color\": \"#ea52e9\"\n      },\n      {\n        \"label\": \"request\",\n        \"color\": \"#5eea52\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_russian_ban\",\n    \"project_link\": \"nonebot-plugin-russian-ban\",\n    \"author_id\": 51886078,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_ygo_trade\",\n    \"project_link\": \"nonebot-plugin-ygo-trade\",\n    \"author_id\": 41512913,\n    \"tags\": [\n      {\n        \"label\": \"游戏王\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"YGO\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"集换社\",\n        \"color\": \"#eada52\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_novelai\",\n    \"project_link\": \"nonebot-plugin-novelai\",\n    \"author_id\": 34237511,\n    \"tags\": [\n      {\n        \"label\": \"aidraw\",\n        \"color\": \"#ffc646\"\n      },\n      {\n        \"label\": \"naifu\",\n        \"color\": \"#ffc646\"\n      },\n      {\n        \"label\": \"webui\",\n        \"color\": \"#ffc646\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"ayaka_games\",\n    \"project_link\": \"ayaka-games\",\n    \"author_id\": 47290820,\n    \"tags\": [\n      {\n        \"label\": \"小游戏\",\n        \"color\": \"#e36306\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"ayaka_timezone\",\n    \"project_link\": \"nonebot-plugin-ayaka-timezone\",\n    \"author_id\": 47290820,\n    \"tags\": [\n      {\n        \"label\": \"timezone\",\n        \"color\": \"#e36306\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"ayaka_prevent_bad_words\",\n    \"project_link\": \"nonebot-plugin-ayaka-prevent-bad-words\",\n    \"author_id\": 47290820,\n    \"tags\": [\n      {\n        \"label\": \"撤回\",\n        \"color\": \"#e36306\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_savor\",\n    \"project_link\": \"nonebot-plugin-savor\",\n    \"author_id\": 66513481,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_kfcrazy\",\n    \"project_link\": \"nonebot-plugin-kfcrazy\",\n    \"author_id\": 53631287,\n    \"tags\": [\n      {\n        \"label\": \"肯德基\",\n        \"color\": \"#d93b3b\"\n      },\n      {\n        \"label\": \"疯狂星期四\",\n        \"color\": \"#e52124\"\n      },\n      {\n        \"label\": \"KFC\",\n        \"color\": \"#cb5c5e\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot-plugin-random\",\n    \"project_link\": \"nonebot-plugin-random\",\n    \"author_id\": 52129454,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_blacklist\",\n    \"project_link\": \"nonebot-plugin-blacklist\",\n    \"author_id\": 107618388,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_antiinsult\",\n    \"project_link\": \"nonebot-plugin-antiinsult\",\n    \"author_id\": 107618388,\n    \"tags\": [\n      {\n        \"label\": \"被动\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_oddtext\",\n    \"project_link\": \"nonebot-plugin-oddtext\",\n    \"author_id\": 33149974,\n    \"tags\": [\n      {\n        \"label\": \"RCNB!\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_mahjong_scoreboard\",\n    \"project_link\": \"nonebot-plugin-mahjong-scoreboard\",\n    \"author_id\": 17331698,\n    \"tags\": [\n      {\n        \"label\": \"日麻\",\n        \"color\": \"#4684d3\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_cartoon\",\n    \"project_link\": \"nonebot-plugin-cartoon\",\n    \"author_id\": 66513481,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_mahjong_utils\",\n    \"project_link\": \"nonebot-plugin-mahjong-utils\",\n    \"author_id\": 17331698,\n    \"tags\": [\n      {\n        \"label\": \"日麻\",\n        \"color\": \"#edad34\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_animeres\",\n    \"project_link\": \"nonebot-plugin-animeres\",\n    \"author_id\": 50488999,\n    \"tags\": [\n      {\n        \"label\": \"anime\",\n        \"color\": \"#ec5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot-plugin-person\",\n    \"project_link\": \"nonebot-plugin-person\",\n    \"author_id\": 52129454,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_apex_api_query\",\n    \"project_link\": \"nonebot-plugin-apex-api-query\",\n    \"author_id\": 40495719,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_reborn\",\n    \"project_link\": \"nonebot-plugin-reborn\",\n    \"author_id\": 113450723,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_searchBiliInfo\",\n    \"project_link\": \"nonebot-plugin-searchbiliinfo\",\n    \"author_id\": 40910637,\n    \"tags\": [\n      {\n        \"label\": \"bilibili\",\n        \"color\": \"#e55d80\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_colab_novelai\",\n    \"project_link\": \"nonebot-plugin-colab-novelai\",\n    \"author_id\": 100039483,\n    \"tags\": [\n      {\n        \"label\": \"NovelAI\",\n        \"color\": \"#eacd52\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_sky\",\n    \"project_link\": \"nonebot-plugin-sky\",\n    \"author_id\": 53631287,\n    \"tags\": [\n      {\n        \"label\": \"光遇\",\n        \"color\": \"#7ebdf0\"\n      },\n      {\n        \"label\": \"攻略\",\n        \"color\": \"#2079c1\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_zyk_novelai\",\n    \"project_link\": \"nonebot-plugin-zyk-novelai\",\n    \"author_id\": 110616928,\n    \"tags\": [\n      {\n        \"label\": \"Free\",\n        \"color\": \"#42e22f\"\n      },\n      {\n        \"label\": \"Simple\",\n        \"color\": \"#e2d92f\"\n      },\n      {\n        \"label\": \"Novelai\",\n        \"color\": \"#3e10e9\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_repeep\",\n    \"project_link\": \"nonebot-plugin-repeep\",\n    \"author_id\": 51946313,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_gscode\",\n    \"project_link\": \"nonebot-plugin-gscode\",\n    \"author_id\": 22407052,\n    \"tags\": [\n      {\n        \"label\": \"Genshin\",\n        \"color\": \"#ffd49f\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_note\",\n    \"project_link\": \"nonebot-plugin-note\",\n    \"author_id\": 111600679,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot-plugin-bilibili-image\",\n    \"project_link\": \"nonebot-plugin-bilibili-image\",\n    \"author_id\": 52129454,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_trace_moe\",\n    \"project_link\": \"nonebot-plugin-trace-moe\",\n    \"author_id\": 40910637,\n    \"tags\": [\n      {\n        \"label\": \"trace\",\n        \"color\": \"#191919\"\n      },\n      {\n        \"label\": \"image\",\n        \"color\": \"#00a0ff\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_zyk_music\",\n    \"project_link\": \"nonebot-plugin-zyk-music\",\n    \"author_id\": 110616928,\n    \"tags\": [\n      {\n        \"label\": \"Free\",\n        \"color\": \"#26d019\"\n      },\n      {\n        \"label\": \"Simple\",\n        \"color\": \"#b8c10d\"\n      },\n      {\n        \"label\": \"Music\",\n        \"color\": \"#0d92c1\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_chatgpt\",\n    \"project_link\": \"nonebot-plugin-chatgpt\",\n    \"author_id\": 66513481,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_majsoul\",\n    \"project_link\": \"nonebot-plugin-majsoul\",\n    \"author_id\": 17331698,\n    \"tags\": [\n      {\n        \"label\": \"majsoul\",\n        \"color\": \"#e54141\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_remove_bg\",\n    \"project_link\": \"nonebot-plugin-remove-bg\",\n    \"author_id\": 40910637,\n    \"tags\": [\n      {\n        \"label\": \"img\",\n        \"color\": \"#111111\"\n      },\n      {\n        \"label\": \"removeBG\",\n        \"color\": \"#7a7a7a\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_broadcast\",\n    \"project_link\": \"nonebot-plugin-broadcast\",\n    \"author_id\": 66513481,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_exchangerate\",\n    \"project_link\": \"nonebot-plugin-exchangerate\",\n    \"author_id\": 66513481,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_access_control\",\n    \"project_link\": \"nonebot-plugin-access-control\",\n    \"author_id\": 17331698,\n    \"tags\": [\n      {\n        \"label\": \"权限控制\",\n        \"color\": \"#0e9763\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_colormind\",\n    \"project_link\": \"nonebot-plugin-colormind\",\n    \"author_id\": 40910637,\n    \"tags\": [\n      {\n        \"label\": \"color\",\n        \"color\": \"#ffffff\"\n      },\n      {\n        \"label\": \"配色\",\n        \"color\": \"#fbff03\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_abstain_diary\",\n    \"project_link\": \"nonebot-plugin-abstain-diary\",\n    \"author_id\": 40910637,\n    \"tags\": [\n      {\n        \"label\": \"戒\",\n        \"color\": \"#ffffff\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_backup\",\n    \"project_link\": \"nonebot-plugin-backup\",\n    \"author_id\": 46314093,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_kuma_san\",\n    \"project_link\": \"nonebot-plugin-kuma-san\",\n    \"author_id\": 30224828,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_gpt3\",\n    \"project_link\": \"nonebot-plugin-gpt3\",\n    \"author_id\": 63803385,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_ikun_evolution\",\n    \"project_link\": \"nonebot-plugin-ikun-evolution\",\n    \"author_id\": 11630758,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_random_ban\",\n    \"project_link\": \"nonebot-plugin-random-ban\",\n    \"author_id\": 40910637,\n    \"tags\": [\n      {\n        \"label\": \"禁言\",\n        \"color\": \"#020202\"\n      },\n      {\n        \"label\": \"ban\",\n        \"color\": \"#ffffff\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_antirecall\",\n    \"project_link\": \"nonebot-plugin-antirecall\",\n    \"author_id\": 105533056,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_mc_server_status\",\n    \"project_link\": \"nonebot_plugin_mc_server_status\",\n    \"author_id\": 31379266,\n    \"tags\": [\n      {\n        \"label\": \"Minecraft\",\n        \"color\": \"#a438cd\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_no_repeat\",\n    \"project_link\": \"nonebot-plugin-no-repeat\",\n    \"author_id\": 47290820,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_bfchat\",\n    \"project_link\": \"nonebot-plugin-bfchat\",\n    \"author_id\": 30611816,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_summon\",\n    \"project_link\": \"nonebot-plugin-summon\",\n    \"author_id\": 66541860,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_ping\",\n    \"project_link\": \"nonebot-plugin-ping\",\n    \"author_id\": 66541860,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_face2cartoonpic\",\n    \"project_link\": \"nonebot-plugin-face2cartoonpic\",\n    \"author_id\": 96008766,\n    \"tags\": [\n      {\n        \"label\": \"以图绘图\",\n        \"color\": \"#72f15e\"\n      },\n      {\n        \"label\": \"腾讯云\",\n        \"color\": \"#3785f1\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_servicestate\",\n    \"project_link\": \"nonebot-plugin-servicestate\",\n    \"author_id\": 44545625,\n    \"tags\": [\n      {\n        \"label\": \"api\",\n        \"color\": \"#52ea7f\"\n      },\n      {\n        \"label\": \"state\",\n        \"color\": \"#52cfea\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_animalVoice\",\n    \"project_link\": \"nonebot-plugin-animalvoice\",\n    \"author_id\": 96008766,\n    \"tags\": [\n      {\n        \"label\": \"切噜语~\",\n        \"color\": \"#e75f9d\"\n      },\n      {\n        \"label\": \"兽语\",\n        \"color\": \"#5fe5e7\"\n      },\n      {\n        \"label\": \"加密语言\",\n        \"color\": \"#79e556\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_ayaka_scan_cmd\",\n    \"project_link\": \"nonebot-plugin-ayaka-scan-cmd\",\n    \"author_id\": 47290820,\n    \"tags\": [\n      {\n        \"label\": \"命令探查\",\n        \"color\": \"#e36306\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_HttpCat\",\n    \"project_link\": \"nonebot-plugin-HttpCat\",\n    \"author_id\": 96008766,\n    \"tags\": [\n      {\n        \"label\": \"HttpCat\",\n        \"color\": \"#1f4ddc\"\n      },\n      {\n        \"label\": \"http状态码\",\n        \"color\": \"#dc1f1f\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_revoke\",\n    \"project_link\": \"nonebot-plugin-revoke\",\n    \"author_id\": 17331698,\n    \"tags\": [\n      {\n        \"label\": \"gocqhttp\",\n        \"color\": \"#52ea95\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_setu_customization\",\n    \"project_link\": \"nonebot_plugin_setu_customization\",\n    \"author_id\": 31379266,\n    \"tags\": [\n      {\n        \"label\": \"色图\",\n        \"color\": \"#e9ea52\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_l4d2_server\",\n    \"project_link\": \"nonebot-plugin-l4d2-server\",\n    \"author_id\": 70925546,\n    \"tags\": [\n      {\n        \"label\": \"l4d2\",\n        \"color\": \"#05ff00\"\n      },\n      {\n        \"label\": \"Alconna\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_learning_chat\",\n    \"project_link\": \"nonebot-plugin-learning-chat\",\n    \"author_id\": 63870437,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_couplets\",\n    \"project_link\": \"nonebot-plugin-couplets\",\n    \"author_id\": 63870437,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_Imagelabels\",\n    \"project_link\": \"nonebot-plugin-Imagelabels\",\n    \"author_id\": 110215026,\n    \"tags\": [\n      {\n        \"label\": \"Yolov5\",\n        \"color\": \"#9a2828\"\n      },\n      {\n        \"label\": \"图像标注\",\n        \"color\": \"#e981dc\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_cloudsignx\",\n    \"project_link\": \"nonebot-plugin-cloudsignx\",\n    \"author_id\": 42509185,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_imgexploration\",\n    \"project_link\": \"nonebot-plugin-imgexploration\",\n    \"author_id\": 46257373,\n    \"tags\": [\n      {\n        \"label\": \"搜图\",\n        \"color\": \"#453df1\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_hypixel\",\n    \"project_link\": \"nonebot-plugin-hypixel\",\n    \"author_id\": 82658163,\n    \"tags\": [\n      {\n        \"label\": \"MC\",\n        \"color\": \"#6fea52\"\n      },\n      {\n        \"label\": \"Hypixel\",\n        \"color\": \"#d5ea52\"\n      },\n      {\n        \"label\": \"Hyp\",\n        \"color\": \"#d5ea52\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_nowtime\",\n    \"project_link\": \"nonebot_plugin_nowtime\",\n    \"author_id\": 106718176,\n    \"tags\": [\n      {\n        \"label\": \"整点报时\",\n        \"color\": \"#5eea52\"\n      },\n      {\n        \"label\": \"语音\",\n        \"color\": \"#c84fdb\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_cave\",\n    \"project_link\": \"nonebot-plugin-cave\",\n    \"author_id\": 85006030,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_xingzuo\",\n    \"project_link\": \"nonebot-plugin-xingzuo\",\n    \"author_id\": 90902259,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_BingImage\",\n    \"project_link\": \"nonebot-plugin-BingImage\",\n    \"author_id\": 71204348,\n    \"tags\": [\n      {\n        \"label\": \"风景图\",\n        \"color\": \"#0ce354\"\n      },\n      {\n        \"label\": \"Bing\",\n        \"color\": \"#0c43e3\"\n      },\n      {\n        \"label\": \"必应\",\n        \"color\": \"#eddf13\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_soup\",\n    \"project_link\": \"nonebot-plugin-soup\",\n    \"author_id\": 42509185,\n    \"tags\": [\n      {\n        \"label\": \"心灵鸡汤\",\n        \"color\": \"#52eaea\"\n      },\n      {\n        \"label\": \"鸡汤\",\n        \"color\": \"#ea529a\"\n      },\n      {\n        \"label\": \"毒鸡汤\",\n        \"color\": \"#604a55\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_yuanshen_notice\",\n    \"project_link\": \"nonebot-plugin-yuanshen-notice\",\n    \"author_id\": 90902259,\n    \"tags\": [\n      {\n        \"label\": \"原神\",\n        \"color\": \"#ef3700\"\n      },\n      {\n        \"label\": \"公告\",\n        \"color\": \"#00ef04\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_bilibili_yuan\",\n    \"project_link\": \"nonebot-plugin-bilibili-yuan\",\n    \"author_id\": 90902259,\n    \"tags\": [\n      {\n        \"label\": \"bilibili\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_easy_translate\",\n    \"project_link\": \"nonebot_plugin_easy_translate\",\n    \"author_id\": 31379266,\n    \"tags\": [\n      {\n        \"label\": \"翻译\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_orangedice\",\n    \"project_link\": \"nonebot-plugin-orangedice\",\n    \"author_id\": 63897047,\n    \"tags\": [\n      {\n        \"label\": \"dice\",\n        \"color\": \"#08c0bb\"\n      },\n      {\n        \"label\": \"COC\",\n        \"color\": \"#a2bc0c\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_record\",\n    \"project_link\": \"nonebot-plugin-record\",\n    \"author_id\": 104713034,\n    \"tags\": [\n      {\n        \"label\": \"语音\",\n        \"color\": \"#fff35d\"\n      },\n      {\n        \"label\": \"语音识别\",\n        \"color\": \"#37c0f6\"\n      },\n      {\n        \"label\": \"语音事件响应器\",\n        \"color\": \"#18e13c\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_nya_cook_menu\",\n    \"project_link\": \"nonebot_plugin_nya_cook_menu\",\n    \"author_id\": 31379266,\n    \"tags\": [\n      {\n        \"label\": \"菜谱\",\n        \"color\": \"#e65de5\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"criminal_dance\",\n    \"project_link\": \"criminal-dance\",\n    \"author_id\": 47290820,\n    \"tags\": [\n      {\n        \"label\": \"文字版桌游\",\n        \"color\": \"#e36306\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_picmcstat\",\n    \"project_link\": \"nonebot-plugin-picmcstat\",\n    \"author_id\": 59048777,\n    \"tags\": [\n      {\n        \"label\": \"Minecraft\",\n        \"color\": \"#7fbf55\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_wantwords\",\n    \"project_link\": \"nonebot-plugin-wantwords\",\n    \"author_id\": 81900789,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_pvz\",\n    \"project_link\": \"nonebot-plugin-pvz\",\n    \"author_id\": 75836227,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_report_manager\",\n    \"project_link\": \"nonebot-plugin-report-manager\",\n    \"author_id\": 75826243,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"qinglan_bot\",\n    \"project_link\": \"qinglan-bot\",\n    \"author_id\": 54731914,\n    \"tags\": [\n      {\n        \"label\": \"MineCraft\",\n        \"color\": \"#4ef0ea\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_easy_group_manager\",\n    \"project_link\": \"nonebot-plugin-easy-group-manager\",\n    \"author_id\": 66541860,\n    \"tags\": [\n      {\n        \"label\": \"群管\",\n        \"color\": \"#1eb262\"\n      },\n      {\n        \"label\": \"女生自用\",\n        \"color\": \"#b21e82\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_group_link_guild\",\n    \"project_link\": \"nonebot-plugin-group-link-guild\",\n    \"author_id\": 54731914,\n    \"tags\": [\n      {\n        \"label\": \"QQ群\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"QQ频道\",\n        \"color\": \"#52ead5\"\n      },\n      {\n        \"label\": \"消息互通\",\n        \"color\": \"#50c545\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot-plugin-mcport\",\n    \"project_link\": \"nonebot-plugin-mcport\",\n    \"author_id\": 107346913,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_xdu_support\",\n    \"project_link\": \"nonebot-plugin-xdu-support\",\n    \"author_id\": 75836227,\n    \"tags\": [\n      {\n        \"label\": \"大学校园\",\n        \"color\": \"#52b5ea\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_zyk_lightNVL\",\n    \"project_link\": \"nonebot-plugin-zyk-lightNVL\",\n    \"author_id\": 110616928,\n    \"tags\": [\n      {\n        \"label\": \"轻小说\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_dog\",\n    \"project_link\": \"nonebot-plugin-dog\",\n    \"author_id\": 87823528,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_uuid\",\n    \"project_link\": \"nonebot-plugin-uuid\",\n    \"author_id\": 60338092,\n    \"tags\": [\n      {\n        \"label\": \"工具\",\n        \"color\": \"#39c5bb\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_naturel_gpt\",\n    \"project_link\": \"nonebot-plugin-naturel-gpt\",\n    \"author_id\": 57167362,\n    \"tags\": [\n      {\n        \"label\": \"GPT3\",\n        \"color\": \"#66ccff\"\n      },\n      {\n        \"label\": \"OpenAi\",\n        \"color\": \"#cc66ff\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_impact\",\n    \"project_link\": \"nonebot-plugin-impact\",\n    \"author_id\": 87489040,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_mcping\",\n    \"project_link\": \"nonebot-plugin-mcping\",\n    \"author_id\": 54731914,\n    \"tags\": [\n      {\n        \"label\": \"Minecraft\",\n        \"color\": \"#47d754\"\n      },\n      {\n        \"label\": \"服务器状态\",\n        \"color\": \"#d7cd47\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_b23\",\n    \"project_link\": \"nonebot-plugin-b23\",\n    \"author_id\": 61458340,\n    \"tags\": [\n      {\n        \"label\": \"bilibili\",\n        \"color\": \"#00aeec\"\n      },\n      {\n        \"label\": \"热搜\",\n        \"color\": \"#00aeec\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_autoreply\",\n    \"project_link\": \"nonebot-plugin-autoreply\",\n    \"author_id\": 59048777,\n    \"tags\": [\n      {\n        \"label\": \"自动回复\",\n        \"color\": \"#ea881e\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_setu_collection\",\n    \"project_link\": \"nonebot_plugin_setu_collection\",\n    \"author_id\": 51886078,\n    \"tags\": [\n      {\n        \"label\": \"LoliconAPI\",\n        \"color\": \"#5adba8\"\n      },\n      {\n        \"label\": \"色图\",\n        \"color\": \"#7ab2e1\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_groupmate_waifu\",\n    \"project_link\": \"nonebot-plugin-groupmate-waifu\",\n    \"author_id\": 51886078,\n    \"tags\": [\n      {\n        \"label\": \"娶群友\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_jrrp2\",\n    \"project_link\": \"nonebot-plugin-jrrp2\",\n    \"author_id\": 74699219,\n    \"tags\": [\n      {\n        \"label\": \"每日人品\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"jrrp\",\n        \"color\": \"#5290ea\"\n      },\n      {\n        \"label\": \"jrrp2\",\n        \"color\": \"#52bbea\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_dicky_pk\",\n    \"project_link\": \"nonebot-plugin-dicky-pk\",\n    \"author_id\": 107618388,\n    \"tags\": [\n      {\n        \"label\": \"群聊小游戏\",\n        \"color\": \"#ffd500\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_eventmonitor\",\n    \"project_link\": \"nonebot-plugin-eventmonitor\",\n    \"author_id\": 87823528,\n    \"tags\": [\n      {\n        \"label\": \"QQGroup\",\n        \"color\": \"#2885c0\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_whateat_pic\",\n    \"project_link\": \"nonebot-plugin-whateat-pic\",\n    \"author_id\": 106718176,\n    \"tags\": [\n      {\n        \"label\": \"吃什么\",\n        \"color\": \"#e4ea52\"\n      },\n      {\n        \"label\": \"喝什么\",\n        \"color\": \"#52ea8b\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_matcher_block\",\n    \"project_link\": \"nonebot-plugin-matcher-block\",\n    \"author_id\": 51886078,\n    \"tags\": [\n      {\n        \"label\": \"指令阻断\",\n        \"color\": \"#525fea\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_acm_reminder\",\n    \"project_link\": \"nonebot_plugin_acm_reminder\",\n    \"author_id\": 63897047,\n    \"tags\": [\n      {\n        \"label\": \"ACM\",\n        \"color\": \"#3b8b74\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_maimai\",\n    \"project_link\": \"nonebot-plugin-maimai\",\n    \"author_id\": 70925546,\n    \"tags\": [\n      {\n        \"label\": \"maimai\",\n        \"color\": \"#5262ea\"\n      },\n      {\n        \"label\": \"Alconna\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_all4one\",\n    \"project_link\": \"nonebot-plugin-all4one\",\n    \"author_id\": 50312681,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_gsabyss\",\n    \"project_link\": \"nonebot-plugin-gsabyss\",\n    \"author_id\": 22407052,\n    \"tags\": [\n      {\n        \"label\": \"Genshin\",\n        \"color\": \"#ffd49f\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_arktools\",\n    \"project_link\": \"nonebot-plugin-arktools\",\n    \"author_id\": 52584526,\n    \"tags\": [\n      {\n        \"label\": \"arknights\",\n        \"color\": \"#22bbff\"\n      },\n      {\n        \"label\": \"game\",\n        \"color\": \"#db905e\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"gartic_room\",\n    \"project_link\": \"nonebot-plugin-gartic-room\",\n    \"author_id\": 47290820,\n    \"tags\": [\n      {\n        \"label\": \"ayaka\",\n        \"color\": \"#e36306\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot-plugin-resolver\",\n    \"project_link\": \"nonebot-plugin-resolver\",\n    \"author_id\": 33365787,\n    \"tags\": [\n      {\n        \"label\": \"bilibili\",\n        \"color\": \"#f8a5c2\"\n      },\n      {\n        \"label\": \"tiktok\",\n        \"color\": \"#303952\"\n      },\n      {\n        \"label\": \"twitter\",\n        \"color\": \"#1b9cfc\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_bing_chat\",\n    \"project_link\": \"nonebot-plugin-bing-chat\",\n    \"author_id\": 69247286,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_saa\",\n    \"project_link\": \"nonebot-plugin-send-anything-anywhere\",\n    \"author_id\": 23295345,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_random_stereotypes\",\n    \"project_link\": \"nonebot-plugin-random-stereotypes\",\n    \"author_id\": 40910637,\n    \"tags\": [\n      {\n        \"label\": \"语录\",\n        \"color\": \"#6a6060\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_xiuxian_2\",\n    \"project_link\": \"nonebot-plugin-xiuxian-2\",\n    \"author_id\": 88731921,\n    \"tags\": [\n      {\n        \"label\": \"文游\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"修仙\",\n        \"color\": \"#4e9f9f\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_h2e\",\n    \"project_link\": \"nonebot-plugin-h2e\",\n    \"author_id\": 59423752,\n    \"tags\": [\n      {\n        \"label\": \"锻炼\",\n        \"color\": \"#da4a4a\"\n      },\n      {\n        \"label\": \"what2eat\",\n        \"color\": \"#99da4a\"\n      },\n      {\n        \"label\": \" how2exe\",\n        \"color\": \"#99da4a\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_oachat\",\n    \"project_link\": \"nonebot-plugin-oachat\",\n    \"author_id\": 59423752,\n    \"tags\": [\n      {\n        \"label\": \"OpenAI\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"GPT3\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"ChatBot\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_warframe_mode\",\n    \"project_link\": \"nonebot-plugin-warframe-mode\",\n    \"author_id\": 73402119,\n    \"tags\": [\n      {\n        \"label\": \"星际战甲\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"warframe\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_bf1_groptools\",\n    \"project_link\": \"nonebot-plugin-bf1-groptools\",\n    \"author_id\": 72740993,\n    \"tags\": [\n      {\n        \"label\": \"战地一\",\n        \"color\": \"#52eae4\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_afd\",\n    \"project_link\": \"nonebot-plugin-afd\",\n    \"author_id\": 54731914,\n    \"tags\": [\n      {\n        \"label\": \"爱发电\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"自动审核进群\",\n        \"color\": \"#52eae9\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_eventdone\",\n    \"project_link\": \"nonebot-plugin-eventdone\",\n    \"author_id\": 105444165,\n    \"tags\": [\n      {\n        \"label\": \"同意好友\",\n        \"color\": \"#ba2d2d\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_ncm_saying\",\n    \"project_link\": \"nonebot-plugin-ncm-saying\",\n    \"author_id\": 78636812,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_60s\",\n    \"project_link\": \"nonebot-plugin-60s\",\n    \"author_id\": 78636812,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_AutoRepeater\",\n    \"project_link\": \"nonebot-plugin-AutoRepeater\",\n    \"author_id\": 59276590,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_ai_timetable\",\n    \"project_link\": \"nonebot-plugin-ai-timetable\",\n    \"author_id\": 123555887,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_sanae\",\n    \"project_link\": \"nonebot-plugin-sanae\",\n    \"author_id\": 36219542,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_arkrecord\",\n    \"project_link\": \"nonebot-plugin-arkrecord\",\n    \"author_id\": 63400477,\n    \"tags\": [\n      {\n        \"label\": \"明日方舟 \",\n        \"color\": \"#c39191\"\n      },\n      {\n        \"label\": \"游戏\",\n        \"color\": \"#c39191\"\n      },\n      {\n        \"label\": \"抽卡\",\n        \"color\": \"#c39191\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_chatgpt_turbo\",\n    \"project_link\": \"nonebot-plugin-chatgpt-turbo\",\n    \"author_id\": 16055526,\n    \"tags\": [\n      {\n        \"label\": \"ChatGPT\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"OpenAI\",\n        \"color\": \"#52ea92\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_chatgpt_on_qq\",\n    \"project_link\": \"nonebot-plugin-chatgpt-on-qq\",\n    \"author_id\": 33772816,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_tuan_chatgpt\",\n    \"project_link\": \"nonebot-plugin-tuan-chatgpt\",\n    \"author_id\": 32624562,\n    \"tags\": [\n      {\n        \"label\": \"chat\",\n        \"color\": \"#ff9d97\"\n      },\n      {\n        \"label\": \"chatgpt\",\n        \"color\": \"#ff9d97\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_chatpdf\",\n    \"project_link\": \"nonebot-plugin-chatpdf\",\n    \"author_id\": 16055526,\n    \"tags\": [\n      {\n        \"label\": \"ChatGPT\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"ChatPDF\",\n        \"color\": \"#6c7abd\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_rimofun\",\n    \"project_link\": \"nonebot-plugin-rimofun\",\n    \"author_id\": 59048777,\n    \"tags\": [\n      {\n        \"label\": \"RimoChan\",\n        \"color\": \"#f3e5bf\"\n      },\n      {\n        \"label\": \"bnhhsh\",\n        \"color\": \"#ebbcc6\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_customemote\",\n    \"project_link\": \"nonebot-plugin-customemote\",\n    \"author_id\": 59276590,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_justsix\",\n    \"project_link\": \"nonebot-plugin-justsix\",\n    \"author_id\": 127737368,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_simulator_xiuxian\",\n    \"project_link\": \"nonebot-plugin-simulator-xiuxian\",\n    \"author_id\": 127736993,\n    \"tags\": [\n      {\n        \"label\": \"文游\",\n        \"color\": \"#4256da\"\n      },\n      {\n        \"label\": \"修仙1.0\",\n        \"color\": \"#1d1b1c\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_bracket\",\n    \"project_link\": \"nonebot-plugin-bracket\",\n    \"author_id\": 33149974,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_gshisbanner\",\n    \"project_link\": \"nonebot-plugin-gshisbanner\",\n    \"author_id\": 100580891,\n    \"tags\": [\n      {\n        \"label\": \"原神\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"卡池\",\n        \"color\": \"#52ea56\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_unoconv\",\n    \"project_link\": \"nonebot-plugin-unoconv\",\n    \"author_id\": 57753690,\n    \"tags\": [\n      {\n        \"label\": \"文件转换\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"pdf转换\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_apexranklookup\",\n    \"project_link\": \"nonebot-plugin-apexranklookup\",\n    \"author_id\": 22563214,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_randomnana\",\n    \"project_link\": \"nonebot-plugin-randomnana\",\n    \"author_id\": 56375835,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_quote\",\n    \"project_link\": \"nonebot-plugin-quote\",\n    \"author_id\": 32476024,\n    \"tags\": [\n      {\n        \"label\": \"语录\",\n        \"color\": \"#003f88\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_memes_api\",\n    \"project_link\": \"nonebot_plugin_memes_api\",\n    \"author_id\": 33149974,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_rrc\",\n    \"project_link\": \"nonebot-plugin-rrc\",\n    \"author_id\": 88731921,\n    \"tags\": [\n      {\n        \"label\": \"课堂\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"抽人\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_hotsearch\",\n    \"project_link\": \"nonebot-plugin-hotsearch\",\n    \"author_id\": 84057953,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_ai_interviewer\",\n    \"project_link\": \"nonebot-plugin-ai-interviewer\",\n    \"author_id\": 16055526,\n    \"tags\": [\n      {\n        \"label\": \"ChatGPT\",\n        \"color\": \"#4366eb\"\n      },\n      {\n        \"label\": \"模拟面试\",\n        \"color\": \"#af286f\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_chatglm\",\n    \"project_link\": \"nonebot-plugin-chatglm\",\n    \"author_id\": 34794409,\n    \"tags\": [\n      {\n        \"label\": \"Chatbot\",\n        \"color\": \"#4366eb\"\n      },\n      {\n        \"label\": \"ChatGLM\",\n        \"color\": \"#af286f\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_chatglm6b\",\n    \"project_link\": \"nonebot-plugin-chatglm6b\",\n    \"author_id\": 117292352,\n    \"tags\": [\n      {\n        \"label\": \"ChatGLM\",\n        \"color\": \"#52d6ea\"\n      },\n      {\n        \"label\": \"AI Chat\",\n        \"color\": \"#8e52ea\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_helloworld\",\n    \"project_link\": \"nonebot-plugin-helloworld\",\n    \"author_id\": 66513481,\n    \"tags\": [\n      {\n        \"label\": \"good first plugin\",\n        \"color\": \"#6c58f6\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_overbracket\",\n    \"project_link\": \"nonebot-plugin-overbracket\",\n    \"author_id\": 37037264,\n    \"tags\": [\n      {\n        \"label\": \"useless\",\n        \"color\": \"#0a930e\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_miao\",\n    \"project_link\": \"nonebot-plugin-miao\",\n    \"author_id\": 63870437,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_questionmark\",\n    \"project_link\": \"nonebot-plugin-questionmark\",\n    \"author_id\": 52584526,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_genshin_cos\",\n    \"project_link\": \"nonebot-plugin-genshin-cos\",\n    \"author_id\": 106718176,\n    \"tags\": [\n      {\n        \"label\": \"原神\",\n        \"color\": \"#f55400\"\n      },\n      {\n        \"label\": \"cos\",\n        \"color\": \"#00d0f5\"\n      },\n      {\n        \"label\": \"coser\",\n        \"color\": \"#eb1dd3\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_chatgpt_plus\",\n    \"project_link\": \"nonebot-plugin-chatgpt-plus\",\n    \"author_id\": 55268546,\n    \"tags\": [\n      {\n        \"label\": \"ChatGPT\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"GPT4\",\n        \"color\": \"#2ecf57\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_sayoroll\",\n    \"project_link\": \"nonebot-plugin-sayoroll\",\n    \"author_id\": 52590027,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_gw2\",\n    \"project_link\": \"nonebot-plugin-gw2\",\n    \"author_id\": 70925546,\n    \"tags\": [\n      {\n        \"label\": \"gw2\",\n        \"color\": \"#52ea5a\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_today_waifu\",\n    \"project_link\": \"nonebot-plugin-today-waifu\",\n    \"author_id\": 129576887,\n    \"tags\": [\n      {\n        \"label\": \"娱乐\",\n        \"color\": \"#eac752\"\n      },\n      {\n        \"label\": \"每日老婆\",\n        \"color\": \"#9beff6\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_sleep\",\n    \"project_link\": \"nonebot-plugin-sleep\",\n    \"author_id\": 52590027,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_api_paddle\",\n    \"project_link\": \"nonebot-api-paddle\",\n    \"author_id\": 69547456,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_chatppt\",\n    \"project_link\": \"nonebot-plugin-chatppt\",\n    \"author_id\": 16055526,\n    \"tags\": [\n      {\n        \"label\": \"ChatGPT\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_steam_game_status\",\n    \"project_link\": \"nonebot-plugin-steam-game-status\",\n    \"author_id\": 57703506,\n    \"tags\": [\n      {\n        \"label\": \"Steam\",\n        \"color\": \"#6690a5\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_bilichat\",\n    \"project_link\": \"nonebot-plugin-bilichat\",\n    \"author_id\": 40534114,\n    \"tags\": [\n      {\n        \"label\": \"哔哩哔哩\",\n        \"color\": \"#ffc8ea\"\n      },\n      {\n        \"label\": \"ChatGPT\",\n        \"color\": \"#75ffc0\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"GenshinUID\",\n    \"project_link\": \"nonebot-plugin-genshinuid\",\n    \"author_id\": 55526518,\n    \"tags\": [\n      {\n        \"label\": \"原神\",\n        \"color\": \"#000000\"\n      },\n      {\n        \"label\": \"早柚核心\",\n        \"color\": \"#7937a9\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_blive_danmaku\",\n    \"project_link\": \"nonebot-plugin-blive-danmaku\",\n    \"author_id\": 14540861,\n    \"tags\": [\n      {\n        \"label\": \"bilibili\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_clock\",\n    \"project_link\": \"nonebot-plugin-clock\",\n    \"author_id\": 57753690,\n    \"tags\": [\n      {\n        \"label\": \"闹钟\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_SDGPT\",\n    \"project_link\": \"nonebot-plugin-sdgpt\",\n    \"author_id\": 52259890,\n    \"tags\": [\n      {\n        \"label\": \"chatGPT\",\n        \"color\": \"#54b490\"\n      },\n      {\n        \"label\": \"novelai\",\n        \"color\": \"#f0dc4e\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_fuckyou\",\n    \"project_link\": \"nonebot-plugin-fuckyou\",\n    \"author_id\": 59048777,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_pokemonfusion\",\n    \"project_link\": \"nonebot-plugin-pokemonfusion\",\n    \"author_id\": 112180508,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"tatarubot2\",\n    \"project_link\": \"tatarubot2\",\n    \"author_id\": 44492123,\n    \"tags\": [\n      {\n        \"label\": \"FF14\",\n        \"color\": \"#5282ea\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_osuverify\",\n    \"project_link\": \"nonebot-plugin-osuverify\",\n    \"author_id\": 52590027,\n    \"tags\": [\n      {\n        \"label\": \"OSU\",\n        \"color\": \"#eb5d9b\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_bilifan\",\n    \"project_link\": \"nonebot-plugin-bilifan\",\n    \"author_id\": 70925546,\n    \"tags\": [\n      {\n        \"label\": \"bilibili\",\n        \"color\": \"#ec15c6\"\n      },\n      {\n        \"label\": \"Alconna\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_akinator\",\n    \"project_link\": \"nonebot-plugin-akinator\",\n    \"author_id\": 59048777,\n    \"tags\": [\n      {\n        \"label\": \"Akinator\",\n        \"color\": \"#6599fe\"\n      },\n      {\n        \"label\": \"网络天才\",\n        \"color\": \"#6599fe\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_rename\",\n    \"project_link\": \"nonebot-plugin-rename\",\n    \"author_id\": 100580891,\n    \"tags\": [\n      {\n        \"label\": \"群名片\",\n        \"color\": \"#466bed\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_appinsights\",\n    \"project_link\": \"nonebot-plugin-appinsights\",\n    \"author_id\": 41534161,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_brainfuck\",\n    \"project_link\": \"nonebot-plugin-brainfuck\",\n    \"author_id\": 19896796,\n    \"tags\": [\n      {\n        \"label\": \"dev\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"bf\",\n        \"color\": \"#529fea\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot-plugin-coderun\",\n    \"project_link\": \"nonebot-plugin-coderun\",\n    \"author_id\": 91423824,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_abot_place\",\n    \"project_link\": \"nonebot-plugin-abot-place\",\n    \"author_id\": 59153990,\n    \"tags\": [\n      {\n        \"label\": \"ABot\",\n        \"color\": \"#52aaea\"\n      },\n      {\n        \"label\": \"Place\",\n        \"color\": \"#ea6dda\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_megumin\",\n    \"project_link\": \"nonebot-plugin-megumin\",\n    \"author_id\": 99666950,\n    \"tags\": [\n      {\n        \"label\": \"小游戏\",\n        \"color\": \"#f9971c\"\n      },\n      {\n        \"label\": \"Explosion\",\n        \"color\": \"#f94b1c\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_manga_translator\",\n    \"project_link\": \"nonebot-plugin-manga-translator\",\n    \"author_id\": 123555887,\n    \"tags\": [\n      {\n        \"label\": \"漫画翻译\",\n        \"color\": \"#527bea\"\n      },\n      {\n        \"label\": \"图片翻译\",\n        \"color\": \"#c452ea\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_watermarker\",\n    \"project_link\": \"nonebot-plugin-watermarker\",\n    \"author_id\": 91937041,\n    \"tags\": [\n      {\n        \"label\": \"水印\",\n        \"color\": \"#52eae7\"\n      },\n      {\n        \"label\": \"图片水印\",\n        \"color\": \"#ef258f\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_starrail_calendar\",\n    \"project_link\": \"nonebot-plugin-starrail-calendar\",\n    \"author_id\": 17716585,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_wordle_help\",\n    \"project_link\": \"nonebot-plugin-wordle-help\",\n    \"author_id\": 87489040,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot-plugin-csgo-case-simulator\",\n    \"project_link\": \"nonebot-plugin-csgo-case-simulator\",\n    \"author_id\": 11494827,\n    \"tags\": [\n      {\n        \"label\": \"CSGO\",\n        \"color\": \"#47aeff\"\n      },\n      {\n        \"label\": \"开箱模拟器\",\n        \"color\": \"#ff4781\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_callapi\",\n    \"project_link\": \"nonebot-plugin-callapi\",\n    \"author_id\": 59048777,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_penguin\",\n    \"project_link\": \"nonebot-plugin-penguin\",\n    \"author_id\": 57004769,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_spark_gpt\",\n    \"project_link\": \"nonebot-plugin-spark-gpt\",\n    \"author_id\": 69547456,\n    \"tags\": [\n      {\n        \"label\": \"多来源语言GPT\",\n        \"color\": \"#5599ff\"\n      },\n      {\n        \"label\": \"多平台用户数据互通\",\n        \"color\": \"#ffff77\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_logpile\",\n    \"project_link\": \"nonebot-plugin-logpile\",\n    \"author_id\": 66513481,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_arkgacha\",\n    \"project_link\": \"nonebot-plugin-arkgacha\",\n    \"author_id\": 42648639,\n    \"tags\": [\n      {\n        \"label\": \"game\",\n        \"color\": \"#eaa852\"\n      },\n      {\n        \"label\": \"arknights\",\n        \"color\": \"#5276ea\"\n      },\n      {\n        \"label\": \"抽卡\",\n        \"color\": \"#c852ea\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_og\",\n    \"project_link\": \"nonebot-plugin-og\",\n    \"author_id\": 110453675,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_hoshino_sign\",\n    \"project_link\": \"nonebot-plugin-hoshino-sign\",\n    \"author_id\": 66541860,\n    \"tags\": [\n      {\n        \"label\": \"PCR\",\n        \"color\": \"#ff0000\"\n      },\n      {\n        \"label\": \"签到\",\n        \"color\": \"#008000\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_multincm\",\n    \"project_link\": \"nonebot-plugin-multincm\",\n    \"author_id\": 59048777,\n    \"tags\": [\n      {\n        \"label\": \"网易云\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"ncm\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_smallapi\",\n    \"project_link\": \"nonebot-plugin-smallapi\",\n    \"author_id\": 91947491,\n    \"tags\": [\n      {\n        \"label\": \"webapi\",\n        \"color\": \"#52ea7d\"\n      },\n      {\n        \"label\": \"api\",\n        \"color\": \"#52ea7d\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_p5generator\",\n    \"project_link\": \"nonebot-plugin-p5generator\",\n    \"author_id\": 58218656,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_sd_webui\",\n    \"project_link\": \"nonebot-plugin-sd-webui\",\n    \"author_id\": 49489433,\n    \"tags\": [\n      {\n        \"label\": \"sd\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_session\",\n    \"project_link\": \"nonebot_plugin_session\",\n    \"author_id\": 33149974,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_pluginupdatecheck\",\n    \"project_link\": \"nonebot-plugin-pluginupdatecheck\",\n    \"author_id\": 58218656,\n    \"tags\": [\n      {\n        \"label\": \"便携安装\",\n        \"color\": \"#e42828\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_stockhelper\",\n    \"project_link\": \"nonebot-plugin-stockhelper\",\n    \"author_id\": 77315378,\n    \"tags\": [\n      {\n        \"label\": \"股票\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_githubcard\",\n    \"project_link\": \"nonebot-plugin-githubcard\",\n    \"author_id\": 56375835,\n    \"tags\": [\n      {\n        \"label\": \" Github\",\n        \"color\": \"#171a21\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_lua\",\n    \"project_link\": \"nonebot-plugin-lua\",\n    \"author_id\": 50922489,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_cube\",\n    \"project_link\": \"nonebot-plugin-cube\",\n    \"author_id\": 109729945,\n    \"tags\": [\n      {\n        \"label\": \"魔方\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"sqlite3\",\n        \"color\": \"#c2ea52\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_homo_mathematician\",\n    \"project_link\": \"nonebot-plugin-homo-mathematician\",\n    \"author_id\": 87489040,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_puzzle\",\n    \"project_link\": \"nonebot-plugin-puzzle\",\n    \"author_id\": 109729945,\n    \"tags\": [\n      {\n        \"label\": \"数字华容道\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"拼图游戏\",\n        \"color\": \"#a7ea52\"\n      },\n      {\n        \"label\": \"puzzle\",\n        \"color\": \"#52eadd\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_herocard\",\n    \"project_link\": \"nonebot-plugin-herocard\",\n    \"author_id\": 41467241,\n    \"tags\": [\n      {\n        \"label\": \"文本提取\",\n        \"color\": \"#6bc6bf\"\n      },\n      {\n        \"label\": \"日语\",\n        \"color\": \"#53bbd8\"\n      },\n      {\n        \"label\": \"本子\",\n        \"color\": \"#eea1b1\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_nagabus\",\n    \"project_link\": \"nonebot-plugin-nagabus\",\n    \"author_id\": 17331698,\n    \"tags\": [\n      {\n        \"label\": \"日麻\",\n        \"color\": \"#b52ee1\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_random_draw\",\n    \"project_link\": \"nonebot-plugin-random-draw\",\n    \"author_id\": 40910637,\n    \"tags\": [\n      {\n        \"label\": \"随机\",\n        \"color\": \"#ffe0aa\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_stable_diffusion_diao\",\n    \"project_link\": \"nonebot-plugin-stable-diffusion-diao\",\n    \"author_id\": 126318917,\n    \"tags\": [\n      {\n        \"label\": \"AI绘图\",\n        \"color\": \"#eaaf52\"\n      },\n      {\n        \"label\": \"SD\",\n        \"color\": \"#eaaf52\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_escape_url\",\n    \"project_link\": \"nonebot-plugin-escape-url\",\n    \"author_id\": 17331698,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_twitter\",\n    \"project_link\": \"nonebot-plugin-twitter\",\n    \"author_id\": 57703506,\n    \"tags\": [\n      {\n        \"label\": \"twitter\",\n        \"color\": \"#29a8dc\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_pcrjjc\",\n    \"project_link\": \"nonebot-plugin-pcrjjc\",\n    \"author_id\": 46278371,\n    \"tags\": [\n      {\n        \"label\": \"公主连结\",\n        \"color\": \"#778a1e\"\n      },\n      {\n        \"label\": \"pcrjjc\",\n        \"color\": \"#778a1e\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_audiocraft\",\n    \"project_link\": \"nonebot-plugin-audiocraft\",\n    \"author_id\": 16055526,\n    \"tags\": [\n      {\n        \"label\": \"AI\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_warthunder_player_check\",\n    \"project_link\": \"nonebot-plugin-warthunder-player-check\",\n    \"author_id\": 110895144,\n    \"tags\": [\n      {\n        \"label\": \"Wathunder\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_follow_withdraw\",\n    \"project_link\": \"nonebot-plugin-follow-withdraw\",\n    \"author_id\": 63870437,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_ocgbot_v2\",\n    \"project_link\": \"nonebot-plugin-ocgbot-v2\",\n    \"author_id\": 76525116,\n    \"tags\": [\n      {\n        \"label\": \"游戏王\",\n        \"color\": \"#2ecbed\"\n      },\n      {\n        \"label\": \"ygo\",\n        \"color\": \"#f77117\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_helltide\",\n    \"project_link\": \"nonebot-plugin-helltide\",\n    \"author_id\": 2779686,\n    \"tags\": [\n      {\n        \"label\": \"helltide\",\n        \"color\": \"#ff0000\"\n      },\n      {\n        \"label\": \"diablo4\",\n        \"color\": \"#ff3300\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_userinfo\",\n    \"project_link\": \"nonebot_plugin_userinfo\",\n    \"author_id\": 33149974,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_poke\",\n    \"project_link\": \"nonebot-plugin-poke\",\n    \"author_id\": 70925546,\n    \"tags\": [\n      {\n        \"label\": \"v11\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_friends\",\n    \"project_link\": \"nonebot-plugin-friends\",\n    \"author_id\": 70925546,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_update\",\n    \"project_link\": \"nonebot-plugin-update\",\n    \"author_id\": 59153990,\n    \"tags\": [\n      {\n        \"label\": \"Nonebot\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"tool\",\n        \"color\": \"#527dea\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_cp_broadcast\",\n    \"project_link\": \"nonebot-plugin-cp-broadcast\",\n    \"author_id\": 103566602,\n    \"tags\": [\n      {\n        \"label\": \"算法竞赛\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"比赛播报\",\n        \"color\": \"#55ea52\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_cfassistant\",\n    \"project_link\": \"nonebot-plugin-cfassistant\",\n    \"author_id\": 24607145,\n    \"tags\": [\n      {\n        \"label\": \"ACM\",\n        \"color\": \"#52EACF\"\n      },\n      {\n        \"label\": \"Codeforces\",\n        \"color\": \"#52EACF\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_splatoon3_schedule\",\n    \"project_link\": \"nonebot-plugin-splatoon3-schedule\",\n    \"author_id\": 53274578,\n    \"tags\": [\n      {\n        \"label\": \"splatoon3\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_capoo\",\n    \"project_link\": \"nonebot-plugin-capoo\",\n    \"author_id\": 103566602,\n    \"tags\": [\n      {\n        \"label\": \"猫猫虫咖波\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_eventexpiry\",\n    \"project_link\": \"nonebot-plugin-eventexpiry\",\n    \"author_id\": 66513481,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_nobahpicture\",\n    \"project_link\": \"nonebot-plugin-nobahpicture\",\n    \"author_id\": 54620759,\n    \"tags\": [\n      {\n        \"label\": \"碧蓝档案\",\n        \"color\": \"#00defe\"\n      },\n      {\n        \"label\": \"蔚蓝档案\",\n        \"color\": \"#00defe\"\n      },\n      {\n        \"label\": \"涩图\",\n        \"color\": \"#00defe\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_blocker\",\n    \"project_link\": \"nonebot-plugin-blocker\",\n    \"author_id\": 41883458,\n    \"tags\": [\n      {\n        \"label\": \"Blocker\",\n        \"color\": \"#39c5bb\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_picture_api\",\n    \"project_link\": \"nonebot-plugin-picture-api\",\n    \"author_id\": 57926506,\n    \"tags\": [\n      {\n        \"label\": \"图片api\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_wenan\",\n    \"project_link\": \"nonebot-plugin-wenan\",\n    \"author_id\": 57926506,\n    \"tags\": [\n      {\n        \"label\": \"文案api\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_mongodb\",\n    \"project_link\": \"nonebot-plugin-mongodb\",\n    \"author_id\": 40534114,\n    \"tags\": [\n      {\n        \"label\": \"mongodb\",\n        \"color\": \"#30a340\"\n      },\n      {\n        \"label\": \"beanie\",\n        \"color\": \"#ff0a0a\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_templates\",\n    \"project_link\": \"nonebot-plugin-templates\",\n    \"author_id\": 69547456,\n    \"tags\": [\n      {\n        \"label\": \"模板渲染\",\n        \"color\": \"#eacd52\"\n      },\n      {\n        \"label\": \"图片生成\",\n        \"color\": \"#adea52\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_pokesomeone\",\n    \"project_link\": \"nonebot-plugin-pokesomeone\",\n    \"author_id\": 69547456,\n    \"tags\": [\n      {\n        \"label\": \"戳一戳\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_dall-e\",\n    \"project_link\": \"nonebot-plugin-dall-e\",\n    \"author_id\": 102890277,\n    \"tags\": [\n      {\n        \"label\": \"DALL-E\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"AI画图\",\n        \"color\": \"#52ea61\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_tempfile\",\n    \"project_link\": \"nonebot-plugin-tempfile\",\n    \"author_id\": 37037264,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_disconnect_notice\",\n    \"project_link\": \"nonebot-plugin-disconnect-notice\",\n    \"author_id\": 53274578,\n    \"tags\": [\n      {\n        \"label\": \"掉线通知\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"邮件\",\n        \"color\": \"#52eaa4\"\n      },\n      {\n        \"label\": \"通知\",\n        \"color\": \"#cbea52\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_BotMailNotice\",\n    \"project_link\": \"nonebot-plugin-botmailnotice\",\n    \"author_id\": 78628186,\n    \"tags\": [\n      {\n        \"label\": \"Mail\",\n        \"color\": \"#52e5ea\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_theworld\",\n    \"project_link\": \"nonebot-plugin-theworld\",\n    \"author_id\": 66513481,\n    \"tags\": [\n      {\n        \"label\": \"JOJO\",\n        \"color\": \"#75147c\"\n      },\n      {\n        \"label\": \"DIO\",\n        \"color\": \"#f9d849\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_nonememe\",\n    \"project_link\": \"nonebot-plugin-nonememe\",\n    \"author_id\": 59048777,\n    \"tags\": [\n      {\n        \"label\": \"NoneMeme\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_aujob\",\n    \"project_link\": \"nonebot-plugin-aujob\",\n    \"author_id\": 110767171,\n    \"tags\": [\n      {\n        \"label\": \"among us\",\n        \"color\": \"#48d5bf\"\n      },\n      {\n        \"label\": \"TOH\",\n        \"color\": \"#05c4f2\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_bind\",\n    \"project_link\": \"nonebot-plugin-bind\",\n    \"author_id\": 69547456,\n    \"tags\": [\n      {\n        \"label\": \"跨平台\",\n        \"color\": \"#5289ea\"\n      },\n      {\n        \"label\": \"账户绑定\",\n        \"color\": \"#6eb428\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_savepic\",\n    \"project_link\": \"nonebot-plugin-savepic\",\n    \"author_id\": 32036413,\n    \"tags\": [\n      {\n        \"label\": \"表情包\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_lostark_wandering_trader\",\n    \"project_link\": \"nonebot-plugin-lostark-wandering-trader\",\n    \"author_id\": 11713728,\n    \"tags\": [\n      {\n        \"label\": \"命运方舟\",\n        \"color\": \"#5289ea\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_csornament\",\n    \"project_link\": \"nonebot-plugin-csornament\",\n    \"author_id\": 129657153,\n    \"tags\": [\n      {\n        \"label\": \"CS:GO\",\n        \"color\": \"#047b97\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_mcversion\",\n    \"project_link\": \"nonebot-plugin-mcversion\",\n    \"author_id\": 110646806,\n    \"tags\": [\n      {\n        \"label\": \"版本\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"MC\",\n        \"color\": \"#52e7ea\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_muteme\",\n    \"project_link\": \"nonebot-plugin-muteme\",\n    \"author_id\": 96647974,\n    \"tags\": [\n      {\n        \"label\": \"禁言\",\n        \"color\": \"#e45b8d\"\n      },\n      {\n        \"label\": \"muteme\",\n        \"color\": \"#5bc2e4\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_jx3\",\n    \"project_link\": \"nonebot-plugin-jx3\",\n    \"author_id\": 61312850,\n    \"tags\": [\n      {\n        \"label\": \"剑网三\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"jx3\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_xinghuo_api\",\n    \"project_link\": \"nonebot-plugin-xinghuo-api\",\n    \"author_id\": 16055526,\n    \"tags\": [\n      {\n        \"label\": \"AI\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"Chat\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"dicergirl\",\n    \"project_link\": \"dicergirl\",\n    \"author_id\": 46275354,\n    \"tags\": [\n      {\n        \"label\": \"跑团\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_uvdiviner\",\n    \"project_link\": \"nonebot-plugin-uvdiviner\",\n    \"author_id\": 46275354,\n    \"tags\": [\n      {\n        \"label\": \"占卜\",\n        \"color\": \"#440e0e\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_push\",\n    \"project_link\": \"nonebot-plugin-push\",\n    \"author_id\": 44370805,\n    \"tags\": [\n      {\n        \"label\": \"邮件\",\n        \"color\": \"#91c0bd\"\n      },\n      {\n        \"label\": \"飞书\",\n        \"color\": \"#7aaccc\"\n      },\n      {\n        \"label\": \"推送\",\n        \"color\": \"#9e97c4\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_sudo\",\n    \"project_link\": \"nonebot-plugin-sudo\",\n    \"author_id\": 104149371,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_arxivRSS\",\n    \"project_link\": \"nonebot-plugin-arxivRSS\",\n    \"author_id\": 83060644,\n    \"tags\": [\n      {\n        \"label\": \"arxiv\",\n        \"color\": \"#f30f0f\"\n      },\n      {\n        \"label\": \"RSS\",\n        \"color\": \"#70a7d8\"\n      },\n      {\n        \"label\": \"论文\",\n        \"color\": \"#f541dc\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_anime_trace\",\n    \"project_link\": \"nonebot-plugin-anime-trace\",\n    \"author_id\": 53679884,\n    \"tags\": [\n      {\n        \"label\": \"AI\",\n        \"color\": \"#f541dc\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_send_message\",\n    \"project_link\": \"nonebot-plugin-send-message\",\n    \"author_id\": 99388013,\n    \"tags\": [\n      {\n        \"label\": \"传话\",\n        \"color\": \"#52ea75\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_requests\",\n    \"project_link\": \"nonebot-plugin-requests\",\n    \"author_id\": 41713304,\n    \"tags\": [\n      {\n        \"label\": \"requests\",\n        \"color\": \"#80bcc2\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_simple_httpcat\",\n    \"project_link\": \"nonebot-plugin-simple-httpcat\",\n    \"author_id\": 96647974,\n    \"tags\": [\n      {\n        \"label\": \"httpcat\",\n        \"color\": \"#5dbfc8\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_qrcode2\",\n    \"project_link\": \"nonebot-plugin-qrcode2\",\n    \"author_id\": 53679884,\n    \"tags\": [\n      {\n        \"label\": \"qrcode\",\n        \"color\": \"#f541dc\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_fakemsg\",\n    \"project_link\": \"nonebot-plugin-fakemsg\",\n    \"author_id\": 106718176,\n    \"tags\": [\n      {\n        \"label\": \"合并转发\",\n        \"color\": \"#1ae32c\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_alchelper\",\n    \"project_link\": \"nonebot-plugin-alchelper\",\n    \"author_id\": 42648639,\n    \"tags\": [\n      {\n        \"label\": \"Alconna\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_souti\",\n    \"project_link\": \"nonebot-plugin-souti\",\n    \"author_id\": 124417044,\n    \"tags\": [\n      {\n        \"label\": \"搜题\",\n        \"color\": \"#49e3d5\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_helper_plus\",\n    \"project_link\": \"nonebot-plugin-helper-plus\",\n    \"author_id\": 83159422,\n    \"tags\": [\n      {\n        \"label\": \"帮助\",\n        \"color\": \"#eae152\"\n      },\n      {\n        \"label\": \"命令控制\",\n        \"color\": \"#52ea5c\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_wearskirt\",\n    \"project_link\": \"nonebot-plugin-wearskirt\",\n    \"author_id\": 74490140,\n    \"tags\": [\n      {\n        \"label\": \"女装\",\n        \"color\": \"#ffc0cb\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_skland_arksign\",\n    \"project_link\": \"nonebot-plugin-skland-arksign\",\n    \"author_id\": 44727214,\n    \"tags\": [\n      {\n        \"label\": \"森空岛\",\n        \"color\": \"#c8eb21\"\n      },\n      {\n        \"label\": \"明日方舟\",\n        \"color\": \"#111111\"\n      },\n      {\n        \"label\": \"签到\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_group_whitelist\",\n    \"project_link\": \"nonebot-plugin-group-whitelist\",\n    \"author_id\": 121035454,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_ernie\",\n    \"project_link\": \"nonebot-plugin-ernie\",\n    \"author_id\": 39023047,\n    \"tags\": [\n      {\n        \"label\": \"文心一言\",\n        \"color\": \"#2e317c\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"TeenStudy\",\n    \"project_link\": \"TeenStudy\",\n    \"author_id\": 143619319,\n    \"tags\": [\n      {\n        \"label\": \"青年大学习\",\n        \"color\": \"#7aea52\"\n      },\n      {\n        \"label\": \"Web UI\",\n        \"color\": \"#52eaea\"\n      },\n      {\n        \"label\": \"多地区\",\n        \"color\": \"#dfea52\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_video_api\",\n    \"project_link\": \"nonebot-plugin-video-api\",\n    \"author_id\": 57926506,\n    \"tags\": [\n      {\n        \"label\": \"视频api\",\n        \"color\": \"#14d8de\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_ottohzys\",\n    \"project_link\": \"nonebot-plugin-ottohzys\",\n    \"author_id\": 59048777,\n    \"tags\": [\n      {\n        \"label\": \"otto\",\n        \"color\": \"#00a6ed\"\n      },\n      {\n        \"label\": \"电棍\",\n        \"color\": \"#00a6ed\"\n      },\n      {\n        \"label\": \"活字印刷\",\n        \"color\": \"#00a6ed\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_ability\",\n    \"project_link\": \"nonebot-plugin-ability\",\n    \"author_id\": 110453675,\n    \"tags\": [\n      {\n        \"label\": \"🔋\",\n        \"color\": \"#8bff00\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_agent\",\n    \"project_link\": \"nonebot-plugin-agent\",\n    \"author_id\": 56953648,\n    \"tags\": [\n      {\n        \"label\": \"agent\",\n        \"color\": \"#99ccff\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_blockwords\",\n    \"project_link\": \"nonebot-plugin-blockwords\",\n    \"author_id\": 50488999,\n    \"tags\": [\n      {\n        \"label\": \"word\",\n        \"color\": \"#99ccff\"\n      },\n      {\n        \"label\": \"屏蔽词\",\n        \"color\": \"#ffcc99\"\n      },\n      {\n        \"label\": \"blockwords\",\n        \"color\": \"#ffcc99\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_matchreminder\",\n    \"project_link\": \"nonebot-plugin-matchreminder\",\n    \"author_id\": 135496272,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_eop_ai\",\n    \"project_link\": \"nonebot_plugin_eop_ai\",\n    \"author_id\": 31379266,\n    \"tags\": [\n      {\n        \"label\": \"POE\",\n        \"color\": \"#bf40bf\"\n      },\n      {\n        \"label\": \"GPT\",\n        \"color\": \"#aaff00\"\n      },\n      {\n        \"label\": \"AI\",\n        \"color\": \"#fa5f55\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_playercheck\",\n    \"project_link\": \"nonebot-plugin-playercheck\",\n    \"author_id\": 38505121,\n    \"tags\": [\n      {\n        \"label\": \"音游\",\n        \"color\": \"#ec623c\"\n      },\n      {\n        \"label\": \"成分\",\n        \"color\": \"#ef5ec4\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_op_finder\",\n    \"project_link\": \"nonebot-plugin-op-finder\",\n    \"author_id\": 29861280,\n    \"tags\": [\n      {\n        \"label\": \"原神\",\n        \"color\": \"#00a0ea\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_morep_finder\",\n    \"project_link\": \"nonebot-plugin-morep-finder\",\n    \"author_id\": 29861280,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot-plugin-yesman\",\n    \"project_link\": \"nonebot-plugin-yesman\",\n    \"author_id\": 89695226,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_fgoavatarguess\",\n    \"project_link\": \"nonebot-plugin-fgoavatarguess\",\n    \"author_id\": 114396889,\n    \"tags\": [\n      {\n        \"label\": \"FGO\",\n        \"color\": \"#66cccc\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_vrchat\",\n    \"project_link\": \"nonebot-plugin-vrchat\",\n    \"author_id\": 70925546,\n    \"tags\": [\n      {\n        \"label\": \"VRChat\",\n        \"color\": \"#7aea53\"\n      },\n      {\n        \"label\": \"Alconna\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"i18n\",\n        \"color\": \"#5452ea\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_batitle\",\n    \"project_link\": \"nonebot-plugin-batitle\",\n    \"author_id\": 41883458,\n    \"tags\": [\n      {\n        \"label\": \"碧蓝档案\",\n        \"color\": \"#00d7fb\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_maimaidx\",\n    \"project_link\": \"nonebot-plugin-maimaidx\",\n    \"author_id\": 65105396,\n    \"tags\": [\n      {\n        \"label\": \"maimai\",\n        \"color\": \"#cea6ff\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_getbapics\",\n    \"project_link\": \"nonebot_plugin_getbapics\",\n    \"author_id\": 144128876,\n    \"tags\": [\n      {\n        \"label\": \"碧蓝档案\",\n        \"color\": \"#00defe\"\n      },\n      {\n        \"label\": \"BA\",\n        \"color\": \"#00defe\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_make_choice\",\n    \"project_link\": \"nonebot-plugin-make-choice\",\n    \"author_id\": 57581480,\n    \"tags\": [\n      {\n        \"label\": \"choice\",\n        \"color\": \"#77c5ff\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_scheduled_broadcast\",\n    \"project_link\": \"nonebot-plugin-scheduled-broadcast\",\n    \"author_id\": 33901373,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_any\",\n    \"project_link\": \"nonebot-plugin-any\",\n    \"author_id\": 81250368,\n    \"tags\": [\n      {\n        \"label\": \"跨平台\",\n        \"color\": \"#36d399\"\n      },\n      {\n        \"label\": \"统一\",\n        \"color\": \"#fa5f55\"\n      },\n      {\n        \"label\": \"工具库\",\n        \"color\": \"#52eadd\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_bertvits2\",\n    \"project_link\": \"nonebot-plugin-bertvits2\",\n    \"author_id\": 52267304,\n    \"tags\": [\n      {\n        \"label\": \"语音合成\",\n        \"color\": \"#9bf6ff\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_wake_on_lan\",\n    \"project_link\": \"nonebot-plugin-wake-on-lan\",\n    \"author_id\": 52388789,\n    \"tags\": [\n      {\n        \"label\": \"局域网唤醒\",\n        \"color\": \"#1e90ff\"\n      },\n      {\n        \"label\": \"WOL\",\n        \"color\": \"#4682b4\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_bingimagecreator\",\n    \"project_link\": \"nonebot-plugin-bingimagecreator\",\n    \"author_id\": 16055526,\n    \"tags\": [\n      {\n        \"label\": \"AI\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"绘图\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_user\",\n    \"project_link\": \"nonebot-plugin-user\",\n    \"author_id\": 5219550,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_shorturl\",\n    \"project_link\": \"nonebot-plugin-shorturl\",\n    \"author_id\": 14922941,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_filehost\",\n    \"project_link\": \"nonebot-plugin-filehost\",\n    \"author_id\": 32300164,\n    \"tags\": [],\n    \"is_official\": true\n  },\n  {\n    \"module_name\": \"nonebot_plugin_smms\",\n    \"project_link\": \"nonebot-plugin-smms\",\n    \"author_id\": 44370805,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_nsfw\",\n    \"project_link\": \"nonebot-plugin-nsfw\",\n    \"author_id\": 48091591,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_batarot\",\n    \"project_link\": \"nonebot-plugin-batarot\",\n    \"author_id\": 143330850,\n    \"tags\": [\n      {\n        \"label\": \"碧蓝档案\",\n        \"color\": \"#15a0df\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_longtu\",\n    \"project_link\": \"nonebot-plugin-longtu\",\n    \"author_id\": 143330850,\n    \"tags\": [\n      {\n        \"label\": \"龙图\",\n        \"color\": \"#e1122d\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_phigros\",\n    \"project_link\": \"nonebot-plugin-phigros\",\n    \"author_id\": 96647974,\n    \"tags\": [\n      {\n        \"label\": \"Phigros\",\n        \"color\": \"#b0339a\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_antimonkey\",\n    \"project_link\": \"nonebot-plugin-antimonkey\",\n    \"author_id\": 131594704,\n    \"tags\": [\n      {\n        \"label\": \"猴子\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"群管\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_waiter\",\n    \"project_link\": \"nonebot-plugin-waiter\",\n    \"author_id\": 42648639,\n    \"tags\": [\n      {\n        \"label\": \"waiter\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_imagemaster\",\n    \"project_link\": \"nonebot-plugin-imagemaster\",\n    \"author_id\": 131594704,\n    \"tags\": [\n      {\n        \"label\": \"修图\",\n        \"color\": \"#52d4ea\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_nikke\",\n    \"project_link\": \"nonebot-plugin-nikke\",\n    \"author_id\": 143330850,\n    \"tags\": [\n      {\n        \"label\": \"nikke\",\n        \"color\": \"#e111b7\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_mcpic\",\n    \"project_link\": \"nonebot-plugin-mcpic\",\n    \"author_id\": 77601125,\n    \"tags\": [\n      {\n        \"label\": \"Minecraft\",\n        \"color\": \"#3cb371\"\n      },\n      {\n        \"label\": \"MC\",\n        \"color\": \"#3cb371\"\n      },\n      {\n        \"label\": \"图片\",\n        \"color\": \"#3cb371\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_wx4\",\n    \"project_link\": \"nonebot-plugin-wx4\",\n    \"author_id\": 72342321,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_mypower\",\n    \"project_link\": \"nonebot-plugin-mypower\",\n    \"author_id\": 118712549,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_bard\",\n    \"project_link\": \"nonebot-plugin-bard\",\n    \"author_id\": 16055526,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_nekoimage\",\n    \"project_link\": \"nonebot-plugin-nekoimage\",\n    \"author_id\": 114645197,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_finallines\",\n    \"project_link\": \"nonebot-plugin-finallines\",\n    \"author_id\": 143330850,\n    \"tags\": [\n      {\n        \"label\": \"最终台词\",\n        \"color\": \"#052199\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_gemini\",\n    \"project_link\": \"nonebot-plugin-gemini\",\n    \"author_id\": 55650833,\n    \"tags\": [\n      {\n        \"label\": \"Gemini\",\n        \"color\": \"#3e8ffb\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"haruka_bot_red\",\n    \"project_link\": \"haruka_bot_red\",\n    \"author_id\": 80164656,\n    \"tags\": [\n      {\n        \"label\": \"bilibili\",\n        \"color\": \"#c83f3f\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_enatfrp\",\n    \"project_link\": \"nonebot-plugin-enatfrp\",\n    \"author_id\": 61458340,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_nezha\",\n    \"project_link\": \"nonebot-plugin-nezha\",\n    \"author_id\": 61458340,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_randpic\",\n    \"project_link\": \"nonebot-plugin-randpic\",\n    \"author_id\": 103566602,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_BAdrawcard\",\n    \"project_link\": \"nonebot-plugin-badrawcard\",\n    \"author_id\": 117559987,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_gpt\",\n    \"project_link\": \"nonebot-plugin-gpt\",\n    \"author_id\": 57703506,\n    \"tags\": [\n      {\n        \"label\": \"ChatGPT\",\n        \"color\": \"#50ec9d\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_easy_blacklist\",\n    \"project_link\": \"nonebot-plugin-easy-blacklist\",\n    \"author_id\": 99388013,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_reminder\",\n    \"project_link\": \"nonebot-plugin-reminder\",\n    \"author_id\": 38395332,\n    \"tags\": [\n      {\n        \"label\": \"scheduler\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_chikari_yinpa\",\n    \"project_link\": \"nonebot-plugin-chikari-yinpa\",\n    \"author_id\": 121878042,\n    \"tags\": [\n      {\n        \"label\": \"yinpa\",\n        \"color\": \"#ffff00\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_splatoon3_nso\",\n    \"project_link\": \"nonebot-plugin-splatoon3-nso\",\n    \"author_id\": 53274578,\n    \"tags\": [\n      {\n        \"label\": \"splatoon3\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_bf1marneserverlist\",\n    \"project_link\": \"nonebot-plugin-bf1marneserverlist\",\n    \"author_id\": 79032826,\n    \"tags\": [\n      {\n        \"label\": \"server\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_kawaii_status\",\n    \"project_link\": \"nonebot-plugin-kawaii-status\",\n    \"author_id\": 110453675,\n    \"tags\": [\n      {\n        \"label\": \"简约\",\n        \"color\": \"#54adff\"\n      },\n      {\n        \"label\": \"可爱\",\n        \"color\": \"#ffb3cc\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_vits_tts\",\n    \"project_link\": \"nonebot-plugin-vits-tts\",\n    \"author_id\": 109732988,\n    \"tags\": [\n      {\n        \"label\": \"VITS\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"TTS\",\n        \"color\": \"#52dbea\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_chatglm_plus\",\n    \"project_link\": \"nonebot-plugin-chatglm-plus\",\n    \"author_id\": 96647974,\n    \"tags\": [\n      {\n        \"label\": \"ChatGLM\",\n        \"color\": \"#73cccc\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_fishing\",\n    \"project_link\": \"nonebot-plugin-fishing\",\n    \"author_id\": 160833462,\n    \"tags\": [\n      {\n        \"label\": \"钓鱼\",\n        \"color\": \"#87cefa\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_a2s_query\",\n    \"project_link\": \"nonebot-plugin-a2s-query\",\n    \"author_id\": 81429435,\n    \"tags\": [\n      {\n        \"label\": \"游戏服务器\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"value\",\n        \"color\": \"#99ea52\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_dice_narrator\",\n    \"project_link\": \"nonebot-plugin-dice-narrator\",\n    \"author_id\": 57167362,\n    \"tags\": [\n      {\n        \"label\": \"GPT\",\n        \"color\": \"#29b752\"\n      },\n      {\n        \"label\": \"掷骰姬\",\n        \"color\": \"#c84b9d\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_steam_info\",\n    \"project_link\": \"nonebot-plugin-steam-info\",\n    \"author_id\": 55650833,\n    \"tags\": [\n      {\n        \"label\": \"Steam\",\n        \"color\": \"#14305e\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_orangejuice\",\n    \"project_link\": \"nonebot-plugin-orangejuice\",\n    \"author_id\": 49135577,\n    \"tags\": [\n      {\n        \"label\": \"百橙\",\n        \"color\": \"#ed6f00\"\n      },\n      {\n        \"label\": \"100OJ\",\n        \"color\": \"#ed6f00\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_md\",\n    \"project_link\": \"nonebot_plugin_md\",\n    \"author_id\": 70925546,\n    \"tags\": [\n      {\n        \"label\": \"muse dash\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_duel\",\n    \"project_link\": \"nonebot-plugin-duel\",\n    \"author_id\": 109732988,\n    \"tags\": [\n      {\n        \"label\": \"决斗\",\n        \"color\": \"#5be7d1\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_pallas_repeater\",\n    \"project_link\": \"nonebot-plugin-pallas-repeater\",\n    \"author_id\": 109732988,\n    \"tags\": [\n      {\n        \"label\": \"复读鸡\",\n        \"color\": \"#52eae7\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_humanaticstore\",\n    \"project_link\": \"nonebot-plugin-humanaticstore\",\n    \"author_id\": 160235071,\n    \"tags\": [\n      {\n        \"label\": \"config\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"配置工具\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_ghtiles\",\n    \"project_link\": \"nonebot-plugin-ghtiles\",\n    \"author_id\": 18106422,\n    \"tags\": [\n      {\n        \"label\": \"Github\",\n        \"color\": \"#2a2a2a\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_diffsinger\",\n    \"project_link\": \"nonebot-plugin-diffsinger\",\n    \"author_id\": 39423408,\n    \"tags\": [\n      {\n        \"label\": \"diffsinger\",\n        \"color\": \"#c24444\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_chikari_economy\",\n    \"project_link\": \"nonebot-plugin-chikari-economy\",\n    \"author_id\": 121878042,\n    \"tags\": [\n      {\n        \"label\": \"经济\",\n        \"color\": \"#adad73\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_auto_bot_selector\",\n    \"project_link\": \"nonebot-plugin-auto-bot-selector\",\n    \"author_id\": 40534114,\n    \"tags\": [\n      {\n        \"label\": \"多适配器\",\n        \"color\": \"#5280ea\"\n      },\n      {\n        \"label\": \"跨平台\",\n        \"color\": \"#5452ea\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_nai3\",\n    \"project_link\": \"nonebot-plugin-nai3\",\n    \"author_id\": 66541860,\n    \"tags\": [\n      {\n        \"label\": \"NovelAI\",\n        \"color\": \"#35f139\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_hx_yinying\",\n    \"project_link\": \"nonebot-plugin-hx-yinying\",\n    \"author_id\": 121207415,\n    \"tags\": [\n      {\n        \"label\": \"Chat\",\n        \"color\": \"#3b53ff\"\n      },\n      {\n        \"label\": \"幻歆！\",\n        \"color\": \"#0d7ccd\"\n      },\n      {\n        \"label\": \"银影！\",\n        \"color\": \"#2b4dae\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_fhl\",\n    \"project_link\": \"nonebot-plugin-fhl\",\n    \"author_id\": 158065462,\n    \"tags\": [\n      {\n        \"label\": \"飞花令\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_yinyu\",\n    \"project_link\": \"nonebot-plugin-yinyu\",\n    \"author_id\": 136897416,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_yinying_chat\",\n    \"project_link\": \"nonebot-plugin-yinying-chat\",\n    \"author_id\": 79129640,\n    \"tags\": [\n      {\n        \"label\": \"Furry\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"银影\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_dcqg_relay\",\n    \"project_link\": \"nonebot-plugin-dcqg-relay\",\n    \"author_id\": 35159351,\n    \"tags\": [\n      {\n        \"label\": \"消息互通\",\n        \"color\": \"#52beea\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_zsmeme\",\n    \"project_link\": \"nonebot-plugin-zsmeme\",\n    \"author_id\": 136897416,\n    \"tags\": [\n      {\n        \"label\": \"帕弥什\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_sanyao\",\n    \"project_link\": \"nonebot-plugin-sanyao\",\n    \"author_id\": 99971730,\n    \"tags\": [\n      {\n        \"label\": \"占卜\",\n        \"color\": \"#415656\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_cyberfurry\",\n    \"project_link\": \"nonebot-plugin-cyberfurry\",\n    \"author_id\": 106443696,\n    \"tags\": [\n      {\n        \"label\": \"幼龙云端\",\n        \"color\": \"#ccc719\"\n      },\n      {\n        \"label\": \"银影\",\n        \"color\": \"#af2af3\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_helpwithpic\",\n    \"project_link\": \"nonebot-plugin-helpwithpic\",\n    \"author_id\": 106443696,\n    \"tags\": [\n      {\n        \"label\": \"帮助\",\n        \"color\": \"#c61b1b\"\n      },\n      {\n        \"label\": \"图片生成\",\n        \"color\": \"#c61b1b\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_sticker_saver\",\n    \"project_link\": \"nonebot-plugin-sticker-saver\",\n    \"author_id\": 6457253,\n    \"tags\": [\n      {\n        \"label\": \"表情包\",\n        \"color\": \"#f1ce15\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_anime_downloader\",\n    \"project_link\": \"nonebot-plugin-anime-downloader\",\n    \"author_id\": 55650833,\n    \"tags\": [\n      {\n        \"label\": \"Anime\",\n        \"color\": \"#ff7474\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_with_ai_agents\",\n    \"project_link\": \"nonebot-plugin-with-ai-agents\",\n    \"author_id\": 49857339,\n    \"tags\": [\n      {\n        \"label\": \"AI 智能体\",\n        \"color\": \"#5dac81\"\n      },\n      {\n        \"label\": \"多平台模型\",\n        \"color\": \"#5dac81\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_RanFurryPic\",\n    \"project_link\": \"nonebot-plugin-RanFurryPic\",\n    \"author_id\": 97278930,\n    \"tags\": [\n      {\n        \"label\": \"furry\",\n        \"color\": \"#8cb9e3\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_furryfusion\",\n    \"project_link\": \"nonebot-plugin-furryfusion\",\n    \"author_id\": 97278930,\n    \"tags\": [\n      {\n        \"label\": \"furry\",\n        \"color\": \"#8cb9e3\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_mysticism\",\n    \"project_link\": \"nonebot-plugin-mysticism\",\n    \"author_id\": 32036413,\n    \"tags\": [\n      {\n        \"label\": \"占卜\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"tarot\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"神秘学\",\n        \"color\": \"#2d4168\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_tsugu_frontend\",\n    \"project_link\": \"nonebot-plugin-tsugu-frontend\",\n    \"author_id\": 55650833,\n    \"tags\": [\n      {\n        \"label\": \"BanGDream\",\n        \"color\": \"#e70050\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_kurogames\",\n    \"project_link\": \"nonebot-plugin-kurogames\",\n    \"author_id\": 36001297,\n    \"tags\": [\n      {\n        \"label\": \"战双帕弥什\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"鸣潮\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"库洛\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_valve_server_query\",\n    \"project_link\": \"nonebot-plugin-valve-server-query\",\n    \"author_id\": 98267824,\n    \"tags\": [\n      {\n        \"label\": \"valve\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"query\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"a2s\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_sparkapi\",\n    \"project_link\": \"nonebot-plugin-sparkapi\",\n    \"author_id\": 145911132,\n    \"tags\": [\n      {\n        \"label\": \"AI\",\n        \"color\": \"#00ff00\"\n      },\n      {\n        \"label\": \"星火\",\n        \"color\": \"#0000ff\"\n      },\n      {\n        \"label\": \"Chat\",\n        \"color\": \"#ff0000\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_tsugu_bangdream_bot\",\n    \"project_link\": \"nonebot-plugin-tsugu-bangdream-bot\",\n    \"author_id\": 68172940,\n    \"tags\": [\n      {\n        \"label\": \"tsugu\",\n        \"color\": \"#ffee88\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_calc24\",\n    \"project_link\": \"nonebot-plugin-calc24\",\n    \"author_id\": 139576615,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_nai3_bot\",\n    \"project_link\": \"nonebot-plugin-nai3-bot\",\n    \"author_id\": 16055526,\n    \"tags\": [\n      {\n        \"label\": \"NovelAI\",\n        \"color\": \"#52ea52\"\n      },\n      {\n        \"label\": \"NAI3\",\n        \"color\": \"#52ea52\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_dg_lab_play\",\n    \"project_link\": \"nonebot-plugin-dg-lab-play\",\n    \"author_id\": 63289359,\n    \"tags\": [\n      {\n        \"label\": \"dg-lab\",\n        \"color\": \"#fee99d\"\n      },\n      {\n        \"label\": \"dg-lab-v3\",\n        \"color\": \"#fee99d\"\n      },\n      {\n        \"label\": \"t:game\",\n        \"color\": \"#019bf1\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_authrespond\",\n    \"project_link\": \"nonebot-plugin-authrespond\",\n    \"author_id\": 106443696,\n    \"tags\": [\n      {\n        \"label\": \"黑名单\",\n        \"color\": \"#e81616\"\n      },\n      {\n        \"label\": \"cubplugins\",\n        \"color\": \"#28a5d1\"\n      },\n      {\n        \"label\": \"权限控制\",\n        \"color\": \"#c75d59\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_shutdown_hook\",\n    \"project_link\": \"nonebot-plugin-shutdown-hook\",\n    \"author_id\": 32418823,\n    \"tags\": [\n      {\n        \"label\": \"工具\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_plus_one\",\n    \"project_link\": \"nonebot-plugin-plus-one\",\n    \"author_id\": 49857339,\n    \"tags\": [\n      {\n        \"label\": \"复读姬\",\n        \"color\": \"#df3cda\"\n      },\n      {\n        \"label\": \"船新版本\",\n        \"color\": \"#28d98e\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_aising\",\n    \"project_link\": \"nonebot-plugin-aising\",\n    \"author_id\": 149048350,\n    \"tags\": [\n      {\n        \"label\": \"唱歌\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"NeuCoSVC\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_qqshell\",\n    \"project_link\": \"nonebot-plugin-qqshell\",\n    \"author_id\": 49857339,\n    \"tags\": [\n      {\n        \"label\": \"SSH\",\n        \"color\": \"#cd2a8f\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_lynchpined\",\n    \"project_link\": \"nonebot-plugin-lynchpined\",\n    \"author_id\": 30611816,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_gakuenImasCalculator\",\n    \"project_link\": \"nonebot-plugin-gakuenImasCalculator\",\n    \"author_id\": 54504721,\n    \"tags\": [\n      {\n        \"label\": \"游戏\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"工具\",\n        \"color\": \"#ea5f52\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_beauty_rater\",\n    \"project_link\": \"nonebot-plugin-beauty-rater\",\n    \"author_id\": 110453675,\n    \"tags\": [\n      {\n        \"label\": \"🌐\",\n        \"color\": \"#e7f4ff\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_xjie_weather\",\n    \"project_link\": \"nonebot-plugin-xjie-weather\",\n    \"author_id\": 139576615,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_wwgachalogs\",\n    \"project_link\": \"nonebot-plugin-wwgachalogs\",\n    \"author_id\": 61410850,\n    \"tags\": [\n      {\n        \"label\": \"鸣潮\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_saalc\",\n    \"project_link\": \"nonebot-plugin-saalc\",\n    \"author_id\": 57004769,\n    \"tags\": [\n      {\n        \"label\": \"SAA\",\n        \"color\": \"#17a5fe\"\n      },\n      {\n        \"label\": \"Alconna\",\n        \"color\": \"#fe9517\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_easymarkdown\",\n    \"project_link\": \"nonebot-plugin-easymarkdown\",\n    \"author_id\": 131594704,\n    \"tags\": [\n      {\n        \"label\": \"code\",\n        \"color\": \"#5284ea\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_multigpt\",\n    \"project_link\": \"nonebot-plugin-multigpt\",\n    \"author_id\": 135360021,\n    \"tags\": [\n      {\n        \"label\": \"GPT\",\n        \"color\": \"#52d7ea\"\n      },\n      {\n        \"label\": \"PPT\",\n        \"color\": \"#ea52c7\"\n      },\n      {\n        \"label\": \"多模\",\n        \"color\": \"#eac252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_mcsm\",\n    \"project_link\": \"nonebot-plugin-mcsm\",\n    \"author_id\": 98267824,\n    \"tags\": [\n      {\n        \"label\": \"MCSM\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"应用托管\",\n        \"color\": \"#5272ea\"\n      },\n      {\n        \"label\": \"服务器管理\",\n        \"color\": \"#4cc275\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_helldivers\",\n    \"project_link\": \"nonebot-plugin-helldivers\",\n    \"author_id\": 57581480,\n    \"tags\": [\n      {\n        \"label\": \"helldivers\",\n        \"color\": \"#ffd700\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_mahjong_hand_guess\",\n    \"project_link\": \"nonebot-plugin-mahjong-hand-guess\",\n    \"author_id\": 56375835,\n    \"tags\": [\n      {\n        \"label\": \"game\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_asmr\",\n    \"project_link\": \"nonebot-plugin-asmr\",\n    \"author_id\": 149048350,\n    \"tags\": [\n      {\n        \"label\": \"asmr\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"音声\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_eve_tool\",\n    \"project_link\": \"nonebot-plugin-eve-tool\",\n    \"author_id\": 95174933,\n    \"tags\": [\n      {\n        \"label\": \"game\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"eve\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_daily_task\",\n    \"project_link\": \"nonebot-plugin-daily-task\",\n    \"author_id\": 45627292,\n    \"tags\": [\n      {\n        \"label\": \"每日任务\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_biliforward\",\n    \"project_link\": \"nonebot-plugin-biliforward\",\n    \"author_id\": 61410850,\n    \"tags\": [\n      {\n        \"label\": \"bilibili\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"哔哩哔哩\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_obastatus\",\n    \"project_link\": \"nonebot-plugin-obastatus\",\n    \"author_id\": 63110083,\n    \"tags\": [\n      {\n        \"label\": \"BMCLAPI\",\n        \"color\": \"#5f82ba\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_dcqq_relay\",\n    \"project_link\": \"nonebot-plugin-dcqq-relay\",\n    \"author_id\": 35159351,\n    \"tags\": [\n      {\n        \"label\": \"消息互通\",\n        \"color\": \"#428fdb\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_ncupdate\",\n    \"project_link\": \"nonebot-plugin-ncupdate\",\n    \"author_id\": 118712549,\n    \"tags\": [\n      {\n        \"label\": \"NapCat\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_anymate\",\n    \"project_link\": \"nonebot-plugin-anymate\",\n    \"author_id\": 89532126,\n    \"tags\": [\n      {\n        \"label\": \"server\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"func\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_cfr2\",\n    \"project_link\": \"nonebot-plugin-cfr2\",\n    \"author_id\": 25610914,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_WWwiki\",\n    \"project_link\": \"nonebot-plugin-WWwiki\",\n    \"author_id\": 136897416,\n    \"tags\": [\n      {\n        \"label\": \"鸣潮\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"wiki\",\n        \"color\": \"#30af29\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_acgnshow\",\n    \"project_link\": \"nonebot-plugin-acgnshow\",\n    \"author_id\": 60691961,\n    \"tags\": [\n      {\n        \"label\": \"bilibili\",\n        \"color\": \"#f21010\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_runagain\",\n    \"project_link\": \"nonebot-plugin-runagain\",\n    \"author_id\": 37037264,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_wordle_simple\",\n    \"project_link\": \"nonebot-plugin-wordle-simple\",\n    \"author_id\": 59602626,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_daily_oil_price\",\n    \"project_link\": \"nonebot-plugin-daily-oil-price\",\n    \"author_id\": 16321862,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_game_torrent\",\n    \"project_link\": \"nonebot-plugin-game-torrent\",\n    \"author_id\": 106718176,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_weiweibot\",\n    \"project_link\": \"nonebot-plugin-weiweibot\",\n    \"author_id\": 85827122,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_autopush\",\n    \"project_link\": \"nonebot-plugin-autopush\",\n    \"author_id\": 104149371,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_exe_code\",\n    \"project_link\": \"nonebot-plugin-exe-code\",\n    \"author_id\": 69091901,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_system_command\",\n    \"project_link\": \"nonebot-plugin-system-command\",\n    \"author_id\": 107618388,\n    \"tags\": [\n      {\n        \"label\": \"shell\",\n        \"color\": \"#0078d4\"\n      },\n      {\n        \"label\": \"cmd\",\n        \"color\": \"#24292f\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_cogvideox\",\n    \"project_link\": \"nonebot-plugin-cogvideox\",\n    \"author_id\": 16055526,\n    \"tags\": [\n      {\n        \"label\": \"AI\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"视频生成\",\n        \"color\": \"#1a30b7\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_gpt_sovits\",\n    \"project_link\": \"nonebot-plugin-gpt-sovits\",\n    \"author_id\": 55650833,\n    \"tags\": [\n      {\n        \"label\": \"GPT-SoVITS\",\n        \"color\": \"#000000\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_wakatime\",\n    \"project_link\": \"nonebot-plugin-wakatime\",\n    \"author_id\": 110453675,\n    \"tags\": [\n      {\n        \"label\": \"WakaTime\",\n        \"color\": \"#000000\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_sunoai\",\n    \"project_link\": \"nonebot-plugin-sunoai\",\n    \"author_id\": 149048350,\n    \"tags\": [\n      {\n        \"label\": \"SunoAi\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"AI歌曲\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_web_bottle\",\n    \"project_link\": \"nonebot_plugin_web_bottle\",\n    \"author_id\": 107746729,\n    \"tags\": [\n      {\n        \"label\": \"漂流瓶\",\n        \"color\": \"#689dd0\"\n      },\n      {\n        \"label\": \"审核\",\n        \"color\": \"#e3567b\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_deer_pipe\",\n    \"project_link\": \"nonebot-plugin-deer-pipe\",\n    \"author_id\": 49141130,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_alist\",\n    \"project_link\": \"nonebot-plugin-alist\",\n    \"author_id\": 52738183,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_bili_fav_watcher\",\n    \"project_link\": \"nonebot-plugin-bili-fav-watcher\",\n    \"author_id\": 94740235,\n    \"tags\": [\n      {\n        \"label\": \"bilibili\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_essence_message\",\n    \"project_link\": \"nonebot-plugin-essence-message\",\n    \"author_id\": 57578111,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_ba_tools\",\n    \"project_link\": \"nonebot-plugin-ba-tools\",\n    \"author_id\": 144128876,\n    \"tags\": [\n      {\n        \"label\": \"蔚蓝档案\",\n        \"color\": \"#00fcf8\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_fakepic\",\n    \"project_link\": \"nonebot-plugin-fakepic\",\n    \"author_id\": 170833701,\n    \"tags\": [\n      {\n        \"label\": \"图片生成\",\n        \"color\": \"#3a82ff\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"pokepoke_miss\",\n    \"project_link\": \"pokepoke_miss\",\n    \"author_id\": 140364698,\n    \"tags\": [\n      {\n        \"label\": \"舞萌\",\n        \"color\": \"#f7fe0e\"\n      },\n      {\n        \"label\": \"戳一戳\",\n        \"color\": \"#e80606\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_ehentai_search\",\n    \"project_link\": \"nonebot-plugin-ehentai-search\",\n    \"author_id\": 61854722,\n    \"tags\": [\n      {\n        \"label\": \"ehentai\",\n        \"color\": \"#ffe700\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"mai2_pcount\",\n    \"project_link\": \"mai2_pcount\",\n    \"author_id\": 140364698,\n    \"tags\": [\n      {\n        \"label\": \"舞萌\",\n        \"color\": \"#eabf0b\"\n      },\n      {\n        \"label\": \"机厅\",\n        \"color\": \"#a60a25\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_dify\",\n    \"project_link\": \"nonebot-plugin-dify\",\n    \"author_id\": 1080807,\n    \"tags\": [\n      {\n        \"label\": \"LLM\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"Dify\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_looklike\",\n    \"project_link\": \"nonebot-plugin-looklike\",\n    \"author_id\": 107618388,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_wait_a_minute\",\n    \"project_link\": \"nonebot-plugin-wait-a-minute\",\n    \"author_id\": 51957264,\n    \"tags\": [\n      {\n        \"label\": \"hook\",\n        \"color\": \"#ff5349\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_repesix\",\n    \"project_link\": \"nonebot-plugin-repesix\",\n    \"author_id\": 107618388,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_liteyukibot\",\n    \"project_link\": \"nonebot-plugin-liteyukibot\",\n    \"author_id\": 79104275,\n    \"tags\": [\n      {\n        \"label\": \"liteyuki\",\n        \"color\": \"#d0e9ff\"\n      },\n      {\n        \"label\": \"轻雪\",\n        \"color\": \"#a2d8f4\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_mute\",\n    \"project_link\": \"nonebot_plugin_mute\",\n    \"author_id\": 140364698,\n    \"tags\": [\n      {\n        \"label\": \"禁言\",\n        \"color\": \"#ff0303\"\n      },\n      {\n        \"label\": \"娱乐\",\n        \"color\": \"#03fff2\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nekro_agent\",\n    \"project_link\": \"nekro-agent\",\n    \"author_id\": 57167362,\n    \"tags\": [\n      {\n        \"label\": \"Agent\",\n        \"color\": \"#ece349\"\n      },\n      {\n        \"label\": \"ChatAI\",\n        \"color\": \"#3cae69\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_lagrange\",\n    \"project_link\": \"nonebot_plugin_lagrange\",\n    \"author_id\": 90964775,\n    \"tags\": [\n      {\n        \"label\": \"Lagrange\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_mccheck\",\n    \"project_link\": \"nonebot-plugin-mccheck\",\n    \"author_id\": 104612722,\n    \"tags\": [\n      {\n        \"label\": \"Minecraft\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"i18n\",\n        \"color\": \"#39c5bb\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_lxns_maimai\",\n    \"project_link\": \"nonebot-plugin-lxns-maimai\",\n    \"author_id\": 110453675,\n    \"tags\": [\n      {\n        \"label\": \"maimai\",\n        \"color\": \"#228be6\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_sendpic\",\n    \"project_link\": \"nonebot-plugin-sendpic\",\n    \"author_id\": 162463571,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_yoyogame\",\n    \"project_link\": \"nonebot-plugin-yoyogame\",\n    \"author_id\": 91937041,\n    \"tags\": [\n      {\n        \"label\": \"game\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"多人游戏\",\n        \"color\": \"#73fe1f\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_tea_silencer\",\n    \"project_link\": \"nonebot-plugin-tea-silencer\",\n    \"author_id\": 99666950,\n    \"tags\": [\n      {\n        \"label\": \"拦截\",\n        \"color\": \"#fe7931\"\n      },\n      {\n        \"label\": \"屏蔽\",\n        \"color\": \"#31c8fe\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_avalon\",\n    \"project_link\": \"nonebot-plugin-avalon\",\n    \"author_id\": 49141130,\n    \"tags\": [\n      {\n        \"label\": \"game\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_werewolf\",\n    \"project_link\": \"nonebot-plugin-werewolf\",\n    \"author_id\": 69091901,\n    \"tags\": [\n      {\n        \"label\": \"game\",\n        \"color\": \"#16acf3\"\n      },\n      {\n        \"label\": \"狼人杀\",\n        \"color\": \"#f3161a\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_tarina_lang_turbo\",\n    \"project_link\": \"nonebot-plugin-tarina-lang-turbo\",\n    \"author_id\": 51957264,\n    \"tags\": [\n      {\n        \"label\": \"i18n\",\n        \"color\": \"#ea5f52\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot-plugin-ACGalaxy\",\n    \"project_link\": \"nonebot-plugin-ACGalaxy\",\n    \"author_id\": 77649130,\n    \"tags\": [\n      {\n        \"label\": \"bilibili\",\n        \"color\": \"#52eaea\"\n      },\n      {\n        \"label\": \"漫展\",\n        \"color\": \"#52ea65\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_QRrender\",\n    \"project_link\": \"nonebot-plugin-QRrender\",\n    \"author_id\": 162463571,\n    \"tags\": [\n      {\n        \"label\": \"二维码\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_weather_rank\",\n    \"project_link\": \"nonebot-plugin-weather-rank\",\n    \"author_id\": 144128876,\n    \"tags\": [\n      {\n        \"label\": \"weather\",\n        \"color\": \"#43f1ff\"\n      },\n      {\n        \"label\": \"天气\",\n        \"color\": \"#43f1ff\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_witff\",\n    \"project_link\": \"nonebot-plugin-witff\",\n    \"author_id\": 131526534,\n    \"tags\": [\n      {\n        \"label\": \"furry\",\n        \"color\": \"#ea5f52\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_logstream\",\n    \"project_link\": \"nonebot-plugin-logstream\",\n    \"author_id\": 171804402,\n    \"tags\": [\n      {\n        \"label\": \"log\",\n        \"color\": \"#41fae7\"\n      },\n      {\n        \"label\": \"SSE\",\n        \"color\": \"#1eefff\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_uninfo\",\n    \"project_link\": \"nonebot-plugin-uninfo\",\n    \"author_id\": 42648639,\n    \"tags\": [\n      {\n        \"label\": \"跨平台\",\n        \"color\": \"#5752ea\"\n      },\n      {\n        \"label\": \"用户数据\",\n        \"color\": \"#ea52d2\"\n      },\n      {\n        \"label\": \"群组频道数据\",\n        \"color\": \"#ea7d52\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_inspect\",\n    \"project_link\": \"nonebot-plugin-inspect\",\n    \"author_id\": 42648639,\n    \"tags\": [\n      {\n        \"label\": \"多平台适配\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_batch_withdrawal\",\n    \"project_link\": \"nonebot-plugin-batch-withdrawal\",\n    \"author_id\": 114509415,\n    \"tags\": [\n      {\n        \"label\": \"群管\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_yareminder\",\n    \"project_link\": \"nonebot-plugin-yareminder\",\n    \"author_id\": 88876404,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_calc_game\",\n    \"project_link\": \"nonebot-plugin-calc-game\",\n    \"author_id\": 143202058,\n    \"tags\": [\n      {\n        \"label\": \"game\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_fun_content\",\n    \"project_link\": \"nonebot-plugin-fun-content\",\n    \"author_id\": 119806180,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_pjsk_helper\",\n    \"project_link\": \"nonebot-plugin-pjsk-helper\",\n    \"author_id\": 140572469,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_color_see_see\",\n    \"project_link\": \"nonebot-plugin-color-see-see\",\n    \"author_id\": 80870777,\n    \"tags\": [\n      {\n        \"label\": \"game\",\n        \"color\": \"#97e7e1\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_githubmodels\",\n    \"project_link\": \"nonebot-plugin-githubmodels\",\n    \"author_id\": 122811297,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_beatsaberscore\",\n    \"project_link\": \"nonebot-plugin-beatsaberscore\",\n    \"author_id\": 172130062,\n    \"tags\": [\n      {\n        \"label\": \"Beat Saber\",\n        \"color\": \"#456df1\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_SimpleToWrite\",\n    \"project_link\": \"nonebot-plugin-SimpleToWrite\",\n    \"author_id\": 174641131,\n    \"tags\": [\n      {\n        \"label\": \"编写简化\",\n        \"color\": \"#104772\"\n      },\n      {\n        \"label\": \"小白向推荐\",\n        \"color\": \"#149581\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_nonechat\",\n    \"project_link\": \"nonebot-plugin-nonechat\",\n    \"author_id\": 144128876,\n    \"tags\": [\n      {\n        \"label\": \"LLM\",\n        \"color\": \"#52eacf\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_marshoai\",\n    \"project_link\": \"nonebot-plugin-marshoai\",\n    \"author_id\": 60691961,\n    \"tags\": [\n      {\n        \"label\": \"猫娘\",\n        \"color\": \"#e6a432\"\n      },\n      {\n        \"label\": \"AI\",\n        \"color\": \"#32c3e6\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_osu_match_monitor\",\n    \"project_link\": \"nonebot-plugin-osu-match-monitor\",\n    \"author_id\": 65720409,\n    \"tags\": [\n      {\n        \"label\": \"OSU\",\n        \"color\": \"#da6699\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_lolinfo\",\n    \"project_link\": \"nonebot-plugin-lolinfo\",\n    \"author_id\": 112923496,\n    \"tags\": [\n      {\n        \"label\": \"LOL\",\n        \"color\": \"#02ceff\"\n      },\n      {\n        \"label\": \"英雄联盟\",\n        \"color\": \"#ff02fb\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_bf5_grouptools\",\n    \"project_link\": \"nonebot_plugin_bf5_grouptools\",\n    \"author_id\": 90964775,\n    \"tags\": [\n      {\n        \"label\": \"战地五\",\n        \"color\": \"#529aea\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_mc_watcher\",\n    \"project_link\": \"nonebot_plugin_mc_watcher\",\n    \"author_id\": 90964775,\n    \"tags\": [\n      {\n        \"label\": \"Minecraft\",\n        \"color\": \"#d79f10\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_zxpm\",\n    \"project_link\": \"nonebot-plugin-zxpm\",\n    \"author_id\": 45528451,\n    \"tags\": [\n      {\n        \"label\": \"小真寻\",\n        \"color\": \"#fbe4e4\"\n      },\n      {\n        \"label\": \"多平台适配\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"插件管理\",\n        \"color\": \"#456df1\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_lingyi_chat\",\n    \"project_link\": \"nonebot-plugin-lingyi-chat\",\n    \"author_id\": 149694986,\n    \"tags\": [\n      {\n        \"label\": \"ChatGPT\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"AI\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_bfvsearch\",\n    \"project_link\": \"nonebot-plugin-bfvsearch\",\n    \"author_id\": 166974448,\n    \"tags\": [\n      {\n        \"label\": \"战地五\",\n        \"color\": \"#529aea\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_safeR18\",\n    \"project_link\": \"nonebot-plugin-safer18\",\n    \"author_id\": 91937041,\n    \"tags\": [\n      {\n        \"label\": \"图片\",\n        \"color\": \"#76ecfd\"\n      },\n      {\n        \"label\": \"R18\",\n        \"color\": \"#fd0004\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_updater\",\n    \"project_link\": \"nonebot-plugin-updater\",\n    \"author_id\": 144128876,\n    \"tags\": [\n      {\n        \"label\": \"插件更新\",\n        \"color\": \"#dd2200\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_npu\",\n    \"project_link\": \"nonebot-plugin-npu\",\n    \"author_id\": 91271243,\n    \"tags\": [\n      {\n        \"label\": \"西工大\",\n        \"color\": \"#66ccff\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_running_state\",\n    \"project_link\": \"nonebot-plugin-running-state\",\n    \"author_id\": 114509415,\n    \"tags\": [\n      {\n        \"label\": \"server\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_zxreport\",\n    \"project_link\": \"nonebot-plugin-zxreport\",\n    \"author_id\": 45528451,\n    \"tags\": [\n      {\n        \"label\": \"小真寻\",\n        \"color\": \"#fbe4e4\"\n      },\n      {\n        \"label\": \"多平台适配\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"日报\",\n        \"color\": \"#dd628b\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_buy\",\n    \"project_link\": \"nonebot-plugin-buy\",\n    \"author_id\": 93068569,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_nailongremove\",\n    \"project_link\": \"nonebot-plugin-nailongremove\",\n    \"author_id\": 218371791,\n    \"tags\": [\n      {\n        \"label\": \"图像分类模型\",\n        \"color\": \"#5269ea\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_pmhelp\",\n    \"project_link\": \"nonebot-plugin-pmhelp\",\n    \"author_id\": 110460087,\n    \"tags\": [\n      {\n        \"label\": \"帮助\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_jtj\",\n    \"project_link\": \"nonebot-plugin-jtj\",\n    \"author_id\": 93068569,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_BR\",\n    \"project_link\": \"nonebot_plugin_BR\",\n    \"author_id\": 70925546,\n    \"tags\": [\n      {\n        \"label\": \"游戏\",\n        \"color\": \"#fa0404\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_boom\",\n    \"project_link\": \"nonebot-plugin-boom\",\n    \"author_id\": 149694986,\n    \"tags\": [\n      {\n        \"label\": \"unsafe\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_picsetu\",\n    \"project_link\": \"nonebot-plugin-picsetu\",\n    \"author_id\": 114509415,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_gotify\",\n    \"project_link\": \"nonebot-plugin-gotify\",\n    \"author_id\": 79104275,\n    \"tags\": [\n      {\n        \"label\": \"gotify\",\n        \"color\": \"#6eddff\"\n      },\n      {\n        \"label\": \"通知推送\",\n        \"color\": \"#6eddff\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_voicemusic\",\n    \"project_link\": \"nonebot-plugin-voicemusic\",\n    \"author_id\": 93068569,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_fishspeech_tts\",\n    \"project_link\": \"nonebot-plugin-fishspeech-tts\",\n    \"author_id\": 106718176,\n    \"tags\": [\n      {\n        \"label\": \"TTS\",\n        \"color\": \"#23e907\"\n      },\n      {\n        \"label\": \"语音合成\",\n        \"color\": \"#e9297f\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_summary\",\n    \"project_link\": \"nonebot-plugin-summary\",\n    \"author_id\": 91937041,\n    \"tags\": [\n      {\n        \"label\": \"省流\",\n        \"color\": \"#57f99a\"\n      },\n      {\n        \"label\": \"AI\",\n        \"color\": \"#c0d048\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_ddrace\",\n    \"project_link\": \"nonebot-plugin-ddrace\",\n    \"author_id\": 60888755,\n    \"tags\": [\n      {\n        \"label\": \"game\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"DDNet\",\n        \"color\": \"#f39c12\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_mai_arcade\",\n    \"project_link\": \"nonebot-plugin-mai-arcade\",\n    \"author_id\": 114254083,\n    \"tags\": [\n      {\n        \"label\": \"maimai\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"arcade\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_prevent_withdrawal\",\n    \"project_link\": \"nonebot-prevent-withdrawal\",\n    \"author_id\": 114509415,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_bilimusic\",\n    \"project_link\": \"nonebot_plugin_bilimusic\",\n    \"author_id\": 90964775,\n    \"tags\": [\n      {\n        \"label\": \"bilibili\",\n        \"color\": \"#52d5ea\"\n      },\n      {\n        \"label\": \"music\",\n        \"color\": \"#ea52ca\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_mmm\",\n    \"project_link\": \"nonebot-plugin-mmm\",\n    \"author_id\": 61458340,\n    \"tags\": [\n      {\n        \"label\": \"人机合一\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_pong\",\n    \"project_link\": \"nonebot-plugin-pong\",\n    \"author_id\": 61458340,\n    \"tags\": [\n      {\n        \"label\": \"ping\",\n        \"color\": \"#ff0000\"\n      },\n      {\n        \"label\": \"pong\",\n        \"color\": \"#0000ff\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_partner_join\",\n    \"project_link\": \"nonebot-plugin-partner-join\",\n    \"author_id\": 114254083,\n    \"tags\": [\n      {\n        \"label\": \"maimai\",\n        \"color\": \"#a1b5e8\"\n      },\n      {\n        \"label\": \"picture\",\n        \"color\": \"#a1b5e8\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_zxpix\",\n    \"project_link\": \"nonebot-plugin-zxpix\",\n    \"author_id\": 45528451,\n    \"tags\": [\n      {\n        \"label\": \"小真寻\",\n        \"color\": \"#fbe4e4\"\n      },\n      {\n        \"label\": \"多平台适配\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"xp\",\n        \"color\": \"#e96ab5\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_impart\",\n    \"project_link\": \"nonebot-plugin-impart\",\n    \"author_id\": 114254083,\n    \"tags\": [\n      {\n        \"label\": \"impart\",\n        \"color\": \"#fa9650\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_wife\",\n    \"project_link\": \"nonebot-plugin-wife\",\n    \"author_id\": 107618388,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_comfyui\",\n    \"project_link\": \"nonebot-plugin-comfyui\",\n    \"author_id\": 126318917,\n    \"tags\": [\n      {\n        \"label\": \"comfyui\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"AI绘图\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_omb\",\n    \"project_link\": \"nonebot-plugin-omb\",\n    \"author_id\": 61458340,\n    \"tags\": [\n      {\n        \"label\": \"人机合一\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_leetcodeAPI_KHASA\",\n    \"project_link\": \"nonebot-plugin-leetcodeAPI-KHASA\",\n    \"author_id\": 178357384,\n    \"tags\": [\n      {\n        \"label\": \"leetcode\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_pypistats\",\n    \"project_link\": \"nonebot-plugin-pypistats\",\n    \"author_id\": 114509415,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_better_broadcast\",\n    \"project_link\": \"nonebot-plugin-better-broadcast\",\n    \"author_id\": 98072207,\n    \"tags\": [\n      {\n        \"label\": \"广播\",\n        \"color\": \"#1fd5ec\"\n      },\n      {\n        \"label\": \"broadcast\",\n        \"color\": \"#231fec\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_bililivedm\",\n    \"project_link\": \"nonebot-plugin-bililivedm\",\n    \"author_id\": 174641131,\n    \"tags\": [\n      {\n        \"label\": \"bilibili\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"全平台可用\",\n        \"color\": \"#232a7a\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_text_ban\",\n    \"project_link\": \"nonebot-plugin-text-ban\",\n    \"author_id\": 114509415,\n    \"tags\": [\n      {\n        \"label\": \"群管\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot-plugin-auto-sendlike\",\n    \"project_link\": \"nonebot-plugin-auto-sendlike\",\n    \"author_id\": 33365787,\n    \"tags\": [\n      {\n        \"label\": \"点赞\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"自动化\",\n        \"color\": \"#de7c7d\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_kindness_lesson\",\n    \"project_link\": \"nonebot-plugin-kindness-lesson\",\n    \"author_id\": 55650833,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_manageweb\",\n    \"project_link\": \"nonebot-plugin-manageweb\",\n    \"author_id\": 110460087,\n    \"tags\": [\n      {\n        \"label\": \"webui\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"插件更新\",\n        \"color\": \"#505d4d\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_api_scheduler\",\n    \"project_link\": \"nonebot-plugin-api-scheduler\",\n    \"author_id\": 68597153,\n    \"tags\": [\n      {\n        \"label\": \"scheduler\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"api\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_pam\",\n    \"project_link\": \"nonebot-plugin-pam\",\n    \"author_id\": 32036413,\n    \"tags\": [\n      {\n        \"label\": \"rule\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_zepplife\",\n    \"project_link\": \"nonebot-plugin-zepplife\",\n    \"author_id\": 137581352,\n    \"tags\": [\n      {\n        \"label\": \"运动\",\n        \"color\": \"#34cb0e\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_hyp\",\n    \"project_link\": \"nonebot-plugin-hyp\",\n    \"author_id\": 87823528,\n    \"tags\": [\n      {\n        \"label\": \"Hypixel\",\n        \"color\": \"#d5a446\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_zxwb\",\n    \"project_link\": \"nonebot-plugin-zxwb\",\n    \"author_id\": 45528451,\n    \"tags\": [\n      {\n        \"label\": \"小真寻\",\n        \"color\": \"#fbe4e4\"\n      },\n      {\n        \"label\": \"多平台适配\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"词条问答\",\n        \"color\": \"#527dea\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_resolver2\",\n    \"project_link\": \"nonebot-plugin-resolver2\",\n    \"author_id\": 64878354,\n    \"tags\": [\n      {\n        \"label\": \"音频\",\n        \"color\": \"#cb324f\"\n      },\n      {\n        \"label\": \"视频\",\n        \"color\": \"#cb324f\"\n      },\n      {\n        \"label\": \"bilibili\",\n        \"color\": \"#ec3658\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_water_geoup_stats\",\n    \"project_link\": \"nonebot-plugin-water-geoup-stats\",\n    \"author_id\": 114509415,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_pcr_sign\",\n    \"project_link\": \"nonebot-plugin-pcr-sign\",\n    \"author_id\": 80870777,\n    \"tags\": [\n      {\n        \"label\": \"PCR\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"签到\",\n        \"color\": \"#aeeaa8\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_emojilike\",\n    \"project_link\": \"nonebot-plugin-emojilike\",\n    \"author_id\": 64878354,\n    \"tags\": [\n      {\n        \"label\": \"赞\",\n        \"color\": \"#e8ec2a\"\n      },\n      {\n        \"label\": \"emoji\",\n        \"color\": \"#e5e861\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot-plugin-multimodal-gemini\",\n    \"project_link\": \"nonebot-plugin-multimodal-gemini\",\n    \"author_id\": 33365787,\n    \"tags\": [\n      {\n        \"label\": \"Gemini\",\n        \"color\": \"#368bd1\"\n      },\n      {\n        \"label\": \"AI\",\n        \"color\": \"#368bd1\"\n      },\n      {\n        \"label\": \"GPT\",\n        \"color\": \"#368bd1\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_csgomarket\",\n    \"project_link\": \"nonebot-plugin-csgomarket\",\n    \"author_id\": 188882430,\n    \"tags\": [\n      {\n        \"label\": \"CSGO\",\n        \"color\": \"#fb9f29\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_nailongmagic\",\n    \"project_link\": \"nonebot-plugin-nailongmagic\",\n    \"author_id\": 218371791,\n    \"tags\": [\n      {\n        \"label\": \"SD\",\n        \"color\": \"#3645e3\"\n      },\n      {\n        \"label\": \"nailong\",\n        \"color\": \"#edd81b\"\n      },\n      {\n        \"label\": \"func\",\n        \"color\": \"#43ed1b\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_amitabha\",\n    \"project_link\": \"nonebot-plugin-amitabha\",\n    \"author_id\": 53631287,\n    \"tags\": [\n      {\n        \"label\": \"念佛\",\n        \"color\": \"#fae1a9\"\n      },\n      {\n        \"label\": \"阿弥陀佛\",\n        \"color\": \"#fab6a9\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_ollama\",\n    \"project_link\": \"nonebot-plugin-ollama\",\n    \"author_id\": 189951286,\n    \"tags\": [\n      {\n        \"label\": \"LLM\",\n        \"color\": \"#da56f6\"\n      },\n      {\n        \"label\": \"AI\",\n        \"color\": \"#56f65f\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_picstatus_template_zhenxun\",\n    \"project_link\": \"nonebot-plugin-picstatus-template-zhenxun\",\n    \"author_id\": 59048777,\n    \"tags\": [\n      {\n        \"label\": \"PicStatus\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_arcaea_sticker\",\n    \"project_link\": \"nonebot-plugin-arcaea-sticker\",\n    \"author_id\": 106399011,\n    \"tags\": [\n      {\n        \"label\": \"Arcaea\",\n        \"color\": \"#1f1e33\"\n      },\n      {\n        \"label\": \"表情包\",\n        \"color\": \"#d560e2\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_simple_block\",\n    \"project_link\": \"nonebot-plugin-simple-block\",\n    \"author_id\": 98072207,\n    \"tags\": [\n      {\n        \"label\": \"阻断\",\n        \"color\": \"#1c999e\"\n      },\n      {\n        \"label\": \"调试\",\n        \"color\": \"#eda509\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_add_friends\",\n    \"project_link\": \"nonebot-plugin-add-friends\",\n    \"author_id\": 48401273,\n    \"tags\": [\n      {\n        \"label\": \"同意好友\",\n        \"color\": \"#04ff48\"\n      },\n      {\n        \"label\": \"加群邀请\",\n        \"color\": \"#0ecdf7\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_addons_manager\",\n    \"project_link\": \"nonebot-plugin-addons-manager\",\n    \"author_id\": 131855327,\n    \"tags\": [\n      {\n        \"label\": \"求生之路\",\n        \"color\": \"#ffff6e\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_flo_luck\",\n    \"project_link\": \"nonebot-plugin-flo-luck\",\n    \"author_id\": 188882430,\n    \"tags\": [\n      {\n        \"label\": \"jrrp\",\n        \"color\": \"#52eae7\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_auto_enter_group\",\n    \"project_link\": \"nonebot-plugin-auto-enter-group\",\n    \"author_id\": 99017826,\n    \"tags\": [\n      {\n        \"label\": \"群管\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_prometheus\",\n    \"project_link\": \"nonebot-plugin-prometheus\",\n    \"author_id\": 143583484,\n    \"tags\": [\n      {\n        \"label\": \"server\",\n        \"color\": \"#f94300\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_pjsekaihelper\",\n    \"project_link\": \"nonebot-plugin-pjsekaihelper\",\n    \"author_id\": 186144551,\n    \"tags\": [\n      {\n        \"label\": \"pjsk\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"世界计划\",\n        \"color\": \"#52ea5a\"\n      },\n      {\n        \"label\": \"音游\",\n        \"color\": \"#52c5ea\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_quark\",\n    \"project_link\": \"nonebot-plugin-quark\",\n    \"author_id\": 64878354,\n    \"tags\": [\n      {\n        \"label\": \"search\",\n        \"color\": \"#e1cfcf\"\n      },\n      {\n        \"label\": \"quark\",\n        \"color\": \"#cfbfbf\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_zxui\",\n    \"project_link\": \"nonebot-plugin-zxui\",\n    \"author_id\": 45528451,\n    \"tags\": [\n      {\n        \"label\": \"小真寻\",\n        \"color\": \"#fbe4e4\"\n      },\n      {\n        \"label\": \"WebUi\",\n        \"color\": \"#2bcaca\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_whats_talk_gemini\",\n    \"project_link\": \"nonebot-plugin-whats-talk-gemini\",\n    \"author_id\": 48401273,\n    \"tags\": [\n      {\n        \"label\": \"群聊总结\",\n        \"color\": \"#03f744\"\n      },\n      {\n        \"label\": \"AI\",\n        \"color\": \"#feee06\"\n      },\n      {\n        \"label\": \"Gemini\",\n        \"color\": \"#0609fe\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_liarsbar\",\n    \"project_link\": \"nonebot-plugin-liarsbar\",\n    \"author_id\": 101725770,\n    \"tags\": [\n      {\n        \"label\": \"game\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_number_detection\",\n    \"project_link\": \"nonebot-plugin-number-detection\",\n    \"author_id\": 114509415,\n    \"tags\": [\n      {\n        \"label\": \"群管\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_qbittorrent_manager\",\n    \"project_link\": \"nonebot-plugin-qbittorrent-manager\",\n    \"author_id\": 109544213,\n    \"tags\": [\n      {\n        \"label\": \"文件下载\",\n        \"color\": \"#4985d1\"\n      },\n      {\n        \"label\": \"跨平台\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_checkbpm\",\n    \"project_link\": \"nonebot-plugin-checkbpm\",\n    \"author_id\": 186144551,\n    \"tags\": [\n      {\n        \"label\": \"music\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"bpm\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"tool\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_apod\",\n    \"project_link\": \"nonebot-plugin-apod\",\n    \"author_id\": 122811297,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_envious\",\n    \"project_link\": \"nonebot-plugin-envious\",\n    \"author_id\": 64878354,\n    \"tags\": [\n      {\n        \"label\": \"羡慕\",\n        \"color\": \"#d84848\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_emlp_Lightweight\",\n    \"project_link\": \"nonebot-plugin-emlp-Lightweight\",\n    \"author_id\": 174641131,\n    \"tags\": [\n      {\n        \"label\": \"恶魔轮盘\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"高自由度\",\n        \"color\": \"#3b35ac\"\n      },\n      {\n        \"label\": \"全平台\",\n        \"color\": \"#41d7a7\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_summary_group\",\n    \"project_link\": \"nonebot-plugin-summary-group\",\n    \"author_id\": 85243954,\n    \"tags\": [\n      {\n        \"label\": \"群聊总结\",\n        \"color\": \"#b8e994\"\n      },\n      {\n        \"label\": \"AI\",\n        \"color\": \"#1289a7\"\n      },\n      {\n        \"label\": \"分析\",\n        \"color\": \"#0652dd\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_palworld\",\n    \"project_link\": \"nonebot-plugin-palworld\",\n    \"author_id\": 121207415,\n    \"tags\": [\n      {\n        \"label\": \"palworld\",\n        \"color\": \"#006cff\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_llm_jade\",\n    \"project_link\": \"nonebot-plugin-llm-jade\",\n    \"author_id\": 96647974,\n    \"tags\": [\n      {\n        \"label\": \"玉\",\n        \"color\": \"#1fe792\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_pictranslator\",\n    \"project_link\": \"nonebot-plugin-pictranslator\",\n    \"author_id\": 75929887,\n    \"tags\": [\n      {\n        \"label\": \"翻译\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"图片翻译\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_fortnite\",\n    \"project_link\": \"nonebot-plugin-fortnite\",\n    \"author_id\": 64878354,\n    \"tags\": [\n      {\n        \"label\": \"EPIC\",\n        \"color\": \"#0b0b11\"\n      },\n      {\n        \"label\": \"堡垒之夜\",\n        \"color\": \"#23a5c2\"\n      },\n      {\n        \"label\": \"Fortnite\",\n        \"color\": \"#23a5c2\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_neuro_draw\",\n    \"project_link\": \"nonebot-plugin-neuro-draw\",\n    \"author_id\": 55650833,\n    \"tags\": [\n      {\n        \"label\": \"Neuro-sama\",\n        \"color\": \"#ed5c5c\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_remind\",\n    \"project_link\": \"nonebot-plugin-remind\",\n    \"author_id\": 137194341,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_joke\",\n    \"project_link\": \"nonebot-plugin-joke\",\n    \"author_id\": 137194341,\n    \"tags\": [\n      {\n        \"label\": \"joke\",\n        \"color\": \"#0b48a9\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_group_file_admin\",\n    \"project_link\": \"nonebot-plugin-group-file-admin\",\n    \"author_id\": 114509415,\n    \"tags\": [\n      {\n        \"label\": \"群管\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_deepseek\",\n    \"project_link\": \"nonebot-plugin-deepseek\",\n    \"author_id\": 110453675,\n    \"tags\": [\n      {\n        \"label\": \"DeepSeek\",\n        \"color\": \"#4d6bfe\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_bot_tap\",\n    \"project_link\": \"nonebot-plugin-bot-tap\",\n    \"author_id\": 96647974,\n    \"tags\": [\n      {\n        \"label\": \"Bot\",\n        \"color\": \"#57a4ce\"\n      },\n      {\n        \"label\": \"管理\",\n        \"color\": \"#57ce66\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_track_anime\",\n    \"project_link\": \"nonebot-plugin-track-anime\",\n    \"author_id\": 113231410,\n    \"tags\": [\n      {\n        \"label\": \"追番工具\",\n        \"color\": \"#c78787\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_groups_aichat\",\n    \"project_link\": \"nonebot-plugin-groups-aichat\",\n    \"author_id\": 51886078,\n    \"tags\": [\n      {\n        \"label\": \"ChatGPT\",\n        \"color\": \"#33cc99\"\n      },\n      {\n        \"label\": \"Gemini\",\n        \"color\": \"#59fb51\"\n      },\n      {\n        \"label\": \"DeepSeek\",\n        \"color\": \"#3399aa\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_suggarchat\",\n    \"project_link\": \"nonebot-plugin-suggarchat\",\n    \"author_id\": 67693593,\n    \"tags\": [\n      {\n        \"label\": \"ChatBot\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"OpenAI\",\n        \"color\": \"#00ffe3\"\n      },\n      {\n        \"label\": \"聊天\",\n        \"color\": \"#0067ff\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_nbnhhsh_q\",\n    \"project_link\": \"nonebot-plugin-nbnhhsh-q\",\n    \"author_id\": 57782574,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_llmchat\",\n    \"project_link\": \"nonebot-plugin-llmchat\",\n    \"author_id\": 87348379,\n    \"tags\": [\n      {\n        \"label\": \"DeepSeek\",\n        \"color\": \"#45beff\"\n      },\n      {\n        \"label\": \"LLM\",\n        \"color\": \"#ff6dea\"\n      },\n      {\n        \"label\": \"ChatGPT\",\n        \"color\": \"#9eff6d\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_group_config\",\n    \"project_link\": \"nonebot-plugin-group-config\",\n    \"author_id\": 157892771,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_luoguluck\",\n    \"project_link\": \"nonebot-plugin-luoguluck\",\n    \"author_id\": 67693593,\n    \"tags\": [\n      {\n        \"label\": \"运势\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"占卜\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"洛谷\",\n        \"color\": \"#0950b8\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_vocu\",\n    \"project_link\": \"nonebot-plugin-vocu\",\n    \"author_id\": 64878354,\n    \"tags\": [\n      {\n        \"label\": \"TTS\",\n        \"color\": \"#4e61a5\"\n      },\n      {\n        \"label\": \"语音\",\n        \"color\": \"#667dd0\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_aitalk\",\n    \"project_link\": \"nonebot-plugin-aitalk\",\n    \"author_id\": 98072207,\n    \"tags\": [\n      {\n        \"label\": \"LLM\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"OpenAI\",\n        \"color\": \"#52dfea\"\n      },\n      {\n        \"label\": \"DeepSeek\",\n        \"color\": \"#9686bb\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_timed_nickname_updater\",\n    \"project_link\": \"nonebot-plugin-timed-nickname-updater\",\n    \"author_id\": 85243954,\n    \"tags\": [\n      {\n        \"label\": \"群昵称\",\n        \"color\": \"#2bcbba\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_meme_stickers\",\n    \"project_link\": \"nonebot-plugin-meme-stickers\",\n    \"author_id\": 59048777,\n    \"tags\": [\n      {\n        \"label\": \"PJSK\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"Arcaea\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"表情包\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_whois\",\n    \"project_link\": \"nonebot-plugin-whois\",\n    \"author_id\": 157226607,\n    \"tags\": [\n      {\n        \"label\": \"域名\",\n        \"color\": \"#14ff22\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_tangkiller\",\n    \"project_link\": \"nonebot-plugin-tangkiller\",\n    \"author_id\": 85243954,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_binsearch\",\n    \"project_link\": \"nonebot-plugin-binsearch\",\n    \"author_id\": 157226607,\n    \"tags\": [\n      {\n        \"label\": \"BankCard\",\n        \"color\": \"#37efff\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_chatgpt_api\",\n    \"project_link\": \"nonebot-plugin-chatgpt-api\",\n    \"author_id\": 66420814,\n    \"tags\": [\n      {\n        \"label\": \"ChatGPT\",\n        \"color\": \"#000000\"\n      },\n      {\n        \"label\": \"DeepSeek\",\n        \"color\": \"#4d6bfe\"\n      },\n      {\n        \"label\": \"OpenAI\",\n        \"color\": \"#01a47e\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_random_reply\",\n    \"project_link\": \"nonebot-plugin-random-reply\",\n    \"author_id\": 16055526,\n    \"tags\": [\n      {\n        \"label\": \"AI\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_aiochatllm\",\n    \"project_link\": \"nonebot-plugin-aiochatllm\",\n    \"author_id\": 176760093,\n    \"tags\": [\n      {\n        \"label\": \"LLM\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_guess_song\",\n    \"project_link\": \"nonebot-plugin-guess-song\",\n    \"author_id\": 116427400,\n    \"tags\": [\n      {\n        \"label\": \"maimai\",\n        \"color\": \"#17bcff\"\n      },\n      {\n        \"label\": \"音游\",\n        \"color\": \"#ff9122\"\n      },\n      {\n        \"label\": \"猜歌\",\n        \"color\": \"#ff2e5c\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_paminet_nodirtymsg\",\n    \"project_link\": \"nonebot-plugin-paminet-nodirtymsg\",\n    \"author_id\": 141894077,\n    \"tags\": [\n      {\n        \"label\": \"群管\",\n        \"color\": \"#ff0000\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_suggarex_cf\",\n    \"project_link\": \"nonebot-plugin-suggarex-cf\",\n    \"author_id\": 67693593,\n    \"tags\": [\n      {\n        \"label\": \"Suggar\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"CloudFlare\",\n        \"color\": \"#ced23c\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_llm_plugins_call\",\n    \"project_link\": \"nonebot-plugin-llm-plugins-call\",\n    \"author_id\": 16055526,\n    \"tags\": [\n      {\n        \"label\": \"AI\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"工具调用\",\n        \"color\": \"#586fd9\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_dingzhen\",\n    \"project_link\": \"nonebot-plugin-dingzhen\",\n    \"author_id\": 109138085,\n    \"tags\": [\n      {\n        \"label\": \"语音\",\n        \"color\": \"#126fdf\"\n      },\n      {\n        \"label\": \"丁真\",\n        \"color\": \"#df2f2f\"\n      },\n      {\n        \"label\": \"语音合成\",\n        \"color\": \"#0cdf4f\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_gold_price\",\n    \"project_link\": \"nonebot-plugin-gold-price\",\n    \"author_id\": 96228495,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_whoasked\",\n    \"project_link\": \"nonebot-plugin-whoasked\",\n    \"author_id\": 163709829,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_moellmchats\",\n    \"project_link\": \"nonebot-plugin-moellmchats\",\n    \"author_id\": 31814960,\n    \"tags\": [\n      {\n        \"label\": \"DeepSeek\",\n        \"color\": \"#3586ff\"\n      },\n      {\n        \"label\": \"LLM\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"ChatGPT\",\n        \"color\": \"#000000\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_jmdownloader\",\n    \"project_link\": \"nonebot-plugin-jmdownloader\",\n    \"author_id\": 74812967,\n    \"tags\": [\n      {\n        \"label\": \"禁漫\",\n        \"color\": \"#f3d667\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_jm\",\n    \"project_link\": \"nonebot-plugin-jm\",\n    \"author_id\": 85243954,\n    \"tags\": [\n      {\n        \"label\": \"禁漫\",\n        \"color\": \"#f3d667\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_zssm\",\n    \"project_link\": \"nonebot-plugin-zssm\",\n    \"author_id\": 59153990,\n    \"tags\": [\n      {\n        \"label\": \"func\",\n        \"color\": \"#961d1d\"\n      },\n      {\n        \"label\": \"ai\",\n        \"color\": \"#5bd46c\"\n      },\n      {\n        \"label\": \"deepseek\",\n        \"color\": \"#4b57ed\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_gemini_vision\",\n    \"project_link\": \"nonebot-plugin-gemini-vision\",\n    \"author_id\": 98764734,\n    \"tags\": [\n      {\n        \"label\": \"Gemini\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"vision\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"ai\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_oi_helper\",\n    \"project_link\": \"nonebot-plugin-oi-helper\",\n    \"author_id\": 78906287,\n    \"tags\": [\n      {\n        \"label\": \"ACM\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"OI\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_mclib\",\n    \"project_link\": \"nonebot_plugin_mclib\",\n    \"author_id\": 67693593,\n    \"tags\": [\n      {\n        \"label\": \"MC\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"Minecraft\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_furryyunhei\",\n    \"project_link\": \"nonebot-plugin-furryyunhei\",\n    \"author_id\": 193674838,\n    \"tags\": [\n      {\n        \"label\": \"Furry\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_testhkk\",\n    \"project_link\": \"nonebot-plugin-testhkk\",\n    \"author_id\": 157194530,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_error_report\",\n    \"project_link\": \"nonebot-plugin-error-report\",\n    \"author_id\": 121207415,\n    \"tags\": [\n      {\n        \"label\": \"跨平台\",\n        \"color\": \"#52b2ea\"\n      },\n      {\n        \"label\": \"Alconna\",\n        \"color\": \"#eabd52\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_asmr100\",\n    \"project_link\": \"nonebot-plugin-asmr100\",\n    \"author_id\": 95261137,\n    \"tags\": [\n      {\n        \"label\": \"asmr\",\n        \"color\": \"#52b4ea\"\n      },\n      {\n        \"label\": \"R18\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_skland\",\n    \"project_link\": \"nonebot-plugin-skland\",\n    \"author_id\": 80870777,\n    \"tags\": [\n      {\n        \"label\": \"森空岛\",\n        \"color\": \"#bfec00\"\n      },\n      {\n        \"label\": \"明日方舟\",\n        \"color\": \"#67fdfe\"\n      },\n      {\n        \"label\": \"终末地\",\n        \"color\": \"#e3d600\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_argot\",\n    \"project_link\": \"nonebot-plugin-argot\",\n    \"author_id\": 110453675,\n    \"tags\": [\n      {\n        \"label\": \"👁️\",\n        \"color\": \"#e74f88\"\n      },\n      {\n        \"label\": \"🤫\",\n        \"color\": \"#ffd8b8\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_github_release_notifier\",\n    \"project_link\": \"nonebot-plugin-github-release-notifier\",\n    \"author_id\": 80151962,\n    \"tags\": [\n      {\n        \"label\": \"Github\",\n        \"color\": \"#2a2a2a\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_bfvservermap\",\n    \"project_link\": \"nonebot-plugin-bfvservermap\",\n    \"author_id\": 163698589,\n    \"tags\": [\n      {\n        \"label\": \"战地五\",\n        \"color\": \"#529aea\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_tieba_monitor\",\n    \"project_link\": \"nonebot-plugin-tieba-monitor\",\n    \"author_id\": 104332832,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_anywhere_llm\",\n    \"project_link\": \"nonebot-plugin-anywhere-llm\",\n    \"author_id\": 57753690,\n    \"tags\": [\n      {\n        \"label\": \"DeepSeek\",\n        \"color\": \"#5852f7\"\n      },\n      {\n        \"label\": \"LLM\",\n        \"color\": \"#f75297\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_latex\",\n    \"project_link\": \"nonebot-plugin-latex\",\n    \"author_id\": 71250018,\n    \"tags\": [\n      {\n        \"label\": \"LaTeX\",\n        \"color\": \"#007af9\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_mcmod\",\n    \"project_link\": \"nonebot-plugin-mcmod\",\n    \"author_id\": 58966022,\n    \"tags\": [\n      {\n        \"label\": \"Minecraft\",\n        \"color\": \"#7d24d5\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_revolver\",\n    \"project_link\": \"nonebot-plugin-revolver\",\n    \"author_id\": 68112346,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_custom_face\",\n    \"project_link\": \"nonebot-plugin-custom-face\",\n    \"author_id\": 109930302,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_jmcomic\",\n    \"project_link\": \"nonebot-plugin-jmcomic\",\n    \"author_id\": 66541860,\n    \"tags\": [\n      {\n        \"label\": \"禁漫\",\n        \"color\": \"#f3d667\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_taygedo_helper\",\n    \"project_link\": \"nonebot-plugin-taygedo-helper\",\n    \"author_id\": 61410850,\n    \"tags\": [\n      {\n        \"label\": \"幻塔\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"塔吉多\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_ban_sticker\",\n    \"project_link\": \"nonebot-plugin-ban-sticker\",\n    \"author_id\": 57578111,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_hitokoto_plus\",\n    \"project_link\": \"nonebot-plugin-hitokoto-plus\",\n    \"author_id\": 163709829,\n    \"tags\": [\n      {\n        \"label\": \"一言\",\n        \"color\": \"#7d24d5\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_poker\",\n    \"project_link\": \"nonebot-plugin-poker\",\n    \"author_id\": 138541077,\n    \"tags\": [\n      {\n        \"label\": \"小游戏\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"对战\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_mhguesser\",\n    \"project_link\": \"nonebot-plugin-mhguesser\",\n    \"author_id\": 53973735,\n    \"tags\": [\n      {\n        \"label\": \"怪物猎人\",\n        \"color\": \"#b128a2\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_ciku\",\n    \"project_link\": \"nonebot-plugin-ciku\",\n    \"author_id\": 174641131,\n    \"tags\": [\n      {\n        \"label\": \"词库\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"小白推荐\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_noadpls\",\n    \"project_link\": \"nonebot-plugin-noadpls\",\n    \"author_id\": 60888755,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_sideload\",\n    \"project_link\": \"nonebot-plugin-sideload\",\n    \"author_id\": 96647974,\n    \"tags\": [\n      {\n        \"label\": \"Web\",\n        \"color\": \"#52eada\"\n      },\n      {\n        \"label\": \"聊天\",\n        \"color\": \"#85ea52\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_paper\",\n    \"project_link\": \"nonebot-plugin-paper\",\n    \"author_id\": 73932916,\n    \"tags\": [\n      {\n        \"label\": \"arxiv\",\n        \"color\": \"#1c1a17\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_dorodoro\",\n    \"project_link\": \"nonebot-plugin-dorodoro\",\n    \"author_id\": 93612024,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_qqdetail\",\n    \"project_link\": \"nonebot-plugin-qqdetail\",\n    \"author_id\": 144674902,\n    \"tags\": [\n      {\n        \"label\": \"qq\",\n        \"color\": \"#52eabc\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_multi_source_daily\",\n    \"project_link\": \"nonebot-plugin-multi-source-daily\",\n    \"author_id\": 32546670,\n    \"tags\": [\n      {\n        \"label\": \"日报\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"news\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_fix_qq_img_ssl\",\n    \"project_link\": \"nonebot-plugin-fix-qq-img-ssl\",\n    \"author_id\": 59048777,\n    \"tags\": [\n      {\n        \"label\": \"SSL\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"qq\",\n        \"color\": \"#52eabc\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_farm\",\n    \"project_link\": \"nonebot-plugin-farm\",\n    \"author_id\": 51057547,\n    \"tags\": [\n      {\n        \"label\": \"种地\",\n        \"color\": \"#90ee90\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_ehentai\",\n    \"project_link\": \"nonebot-plugin-ehentai\",\n    \"author_id\": 35657483,\n    \"tags\": [\n      {\n        \"label\": \"ehentai\",\n        \"color\": \"#e3e0d1\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_picmenu_next\",\n    \"project_link\": \"nonebot-plugin-picmenu-next\",\n    \"author_id\": 59048777,\n    \"tags\": [\n      {\n        \"label\": \"帮助\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"菜单\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_nyaturingtest\",\n    \"project_link\": \"nonebot-plugin-nyaturingtest\",\n    \"author_id\": 74781933,\n    \"tags\": [\n      {\n        \"label\": \"ChatBot\",\n        \"color\": \"#689fff\"\n      },\n      {\n        \"label\": \"QQ群\",\n        \"color\": \"#689fff\"\n      },\n      {\n        \"label\": \"LLM\",\n        \"color\": \"#689fff\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_clovers\",\n    \"project_link\": \"nonebot-plugin-clovers\",\n    \"author_id\": 51886078,\n    \"tags\": [\n      {\n        \"label\": \"clovers\",\n        \"color\": \"#00cc33\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_pokemonle\",\n    \"project_link\": \"nonebot-plugin-pokemonle\",\n    \"author_id\": 53973735,\n    \"tags\": [\n      {\n        \"label\": \"宝可梦\",\n        \"color\": \"#fff160\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_bfvplayerlist\",\n    \"project_link\": \"nonebot-plugin-bfvplayerlist\",\n    \"author_id\": 163698589,\n    \"tags\": [\n      {\n        \"label\": \"战地五\",\n        \"color\": \"#529aea\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_emojilike_automonkey\",\n    \"project_link\": \"nonebot-plugin-emojilike-automonkey\",\n    \"author_id\": 109930302,\n    \"tags\": [\n      {\n        \"label\": \"emoji\",\n        \"color\": \"#6677ff\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_df_armor_repair_simulator\",\n    \"project_link\": \"nonebot-plugin-df-armor-repair-simulator\",\n    \"author_id\": 109930302,\n    \"tags\": [\n      {\n        \"label\": \"tool\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_hacker_news\",\n    \"project_link\": \"nonebot-plugin-hacker-news\",\n    \"author_id\": 36318991,\n    \"tags\": [\n      {\n        \"label\": \"broadcast\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"news\",\n        \"color\": \"#f59a10\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_limiter\",\n    \"project_link\": \"nonebot-plugin-limiter\",\n    \"author_id\": 98752512,\n    \"tags\": [\n      {\n        \"label\": \"cooldown\",\n        \"color\": \"#527aea\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_zzzpanel\",\n    \"project_link\": \"nonebot-plugin-zzzpanel\",\n    \"author_id\": 174641131,\n    \"tags\": [\n      {\n        \"label\": \"米哈游\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"绝区零\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_NobleDuel\",\n    \"project_link\": \"nonebot-plugin-NobleDuel\",\n    \"author_id\": 105113722,\n    \"tags\": [\n      {\n        \"label\": \"贵族决斗\",\n        \"color\": \"#527dea\"\n      },\n      {\n        \"label\": \"恶魔轮盘\",\n        \"color\": \"#ea5255\"\n      },\n      {\n        \"label\": \"养成\",\n        \"color\": \"#82ea52\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_sunset_reminder\",\n    \"project_link\": \"nonebot-plugin-sunset-reminder\",\n    \"author_id\": 80151962,\n    \"tags\": [\n      {\n        \"label\": \"火烧云\",\n        \"color\": \"#ff9c00\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_binance\",\n    \"project_link\": \"nonebot-plugin-binance\",\n    \"author_id\": 96228495,\n    \"tags\": [\n      {\n        \"label\": \"binance\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"币安\",\n        \"color\": \"#e9ea52\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_orm\",\n    \"project_link\": \"nonebot-plugin-orm\",\n    \"author_id\": 45716046,\n    \"tags\": [],\n    \"is_official\": true\n  },\n  {\n    \"module_name\": \"nonebot_plugin_exdi\",\n    \"project_link\": \"nonebot-plugin-exdi\",\n    \"author_id\": 122149478,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_xibao\",\n    \"project_link\": \"nonebot-plugin-xibao\",\n    \"author_id\": 176733343,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_vv\",\n    \"project_link\": \"nonebot-plugin-vv\",\n    \"author_id\": 85243954,\n    \"tags\": [\n      {\n        \"label\": \"表情包\",\n        \"color\": \"#b9f6ca\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_deltaforce_simulator\",\n    \"project_link\": \"nonebot-plugin-deltaforce-simulator\",\n    \"author_id\": 16055526,\n    \"tags\": [\n      {\n        \"label\": \"三角洲\",\n        \"color\": \"#457a47\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_liteperm\",\n    \"project_link\": \"nonebot_plugin_liteperm\",\n    \"author_id\": 67693593,\n    \"tags\": [\n      {\n        \"label\": \"权限控制\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"管理\",\n        \"color\": \"#d7ae22\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_lazytea\",\n    \"project_link\": \"nonebot_plugin_lazytea\",\n    \"author_id\": 78413699,\n    \"tags\": [\n      {\n        \"label\": \"GUI\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"图形化\",\n        \"color\": \"#52e9ea\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_flomic\",\n    \"project_link\": \"nonebot-plugin-flomic\",\n    \"author_id\": 188882430,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_fishing2\",\n    \"project_link\": \"nonebot-plugin-fishing2\",\n    \"author_id\": 49135577,\n    \"tags\": [\n      {\n        \"label\": \"钓鱼\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_llm_helper\",\n    \"project_link\": \"nonebot-plugin-llm-helper\",\n    \"author_id\": 90964775,\n    \"tags\": [\n      {\n        \"label\": \"多平台适配\",\n        \"color\": \"#f16969\"\n      },\n      {\n        \"label\": \"帮助\",\n        \"color\": \"#43b1ce\"\n      },\n      {\n        \"label\": \"LLM\",\n        \"color\": \"#a2c36d\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_guess_disease\",\n    \"project_link\": \"nonebot-plugin-guess-disease\",\n    \"author_id\": 68677053,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_delta_helper\",\n    \"project_link\": \"nonebot-plugin-delta-helper\",\n    \"author_id\": 61410850,\n    \"tags\": [\n      {\n        \"label\": \"deltaforce\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"三角洲行动\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_awsmgmt\",\n    \"project_link\": \"nonebot-plugin-awsmgmt\",\n    \"author_id\": 20412597,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_lazytea_shell_extension\",\n    \"project_link\": \"nonebot_plugin_lazytea_shell_extension\",\n    \"author_id\": 78413699,\n    \"tags\": [\n      {\n        \"label\": \"GUI\",\n        \"color\": \"#53f047\"\n      },\n      {\n        \"label\": \"管理\",\n        \"color\": \"#fff29e\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_nmcweather\",\n    \"project_link\": \"nonebot-plugin-nmcweather\",\n    \"author_id\": 65002276,\n    \"tags\": [\n      {\n        \"label\": \"weather\",\n        \"color\": \"#05f73c\"\n      },\n      {\n        \"label\": \"天气\",\n        \"color\": \"#08f9c7\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_fuckfinalshell\",\n    \"project_link\": \"nonebot-plugin-fuckfinalshell\",\n    \"author_id\": 144674902,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_value\",\n    \"project_link\": \"nonebot-plugin-value\",\n    \"author_id\": 67693593,\n    \"tags\": [\n      {\n        \"label\": \"value\",\n        \"color\": \"#d10a0a\"\n      },\n      {\n        \"label\": \"货币\",\n        \"color\": \"#3c0eff\"\n      },\n      {\n        \"label\": \"经济\",\n        \"color\": \"#1b9f1d\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_mcplayer_render\",\n    \"project_link\": \"nonebot-plugin-mcplayer-render\",\n    \"author_id\": 49135577,\n    \"tags\": [\n      {\n        \"label\": \"Minecraft\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_alisten\",\n    \"project_link\": \"nonebot-plugin-alisten\",\n    \"author_id\": 5219550,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_simple_setu\",\n    \"project_link\": \"nonebot-plugin-simple-setu\",\n    \"author_id\": 178264759,\n    \"tags\": [\n      {\n        \"label\": \"pixiv\",\n        \"color\": \"#00f5ed\"\n      },\n      {\n        \"label\": \"色图\",\n        \"color\": \"#ed0823\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot-plugin-anipusher\",\n    \"project_link\": \"nonebot-plugin-anipusher\",\n    \"author_id\": 32594985,\n    \"tags\": [\n      {\n        \"label\": \"Emby\",\n        \"color\": \"#0b8a31\"\n      },\n      {\n        \"label\": \"AniRss\",\n        \"color\": \"#0b8a31\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_huaer_bot\",\n    \"project_link\": \"nonebot-plugin-huaer-bot\",\n    \"author_id\": 216365707,\n    \"tags\": [\n      {\n        \"label\": \"LLM\",\n        \"color\": \"#0583b3\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_sell_poor\",\n    \"project_link\": \"nonebot-plugin-sell-poor\",\n    \"author_id\": 96647974,\n    \"tags\": [\n      {\n        \"label\": \"卖弱\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"😭😭😭\",\n        \"color\": \"#52eaba\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_abs\",\n    \"project_link\": \"nonebot-plugin-abs\",\n    \"author_id\": 64878354,\n    \"tags\": [\n      {\n        \"label\": \"abstract\",\n        \"color\": \"#edf119\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_ImageLibrary\",\n    \"project_link\": \"nonebot-plugin-imagelibrary\",\n    \"author_id\": 126797731,\n    \"tags\": [\n      {\n        \"label\": \"image\",\n        \"color\": \"#7629c4\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_exhibitionism\",\n    \"project_link\": \"nonebot_plugin_exhibitionism\",\n    \"author_id\": 78413699,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_akashgen\",\n    \"project_link\": \"nonebot-plugin-akashgen\",\n    \"author_id\": 144674902,\n    \"tags\": [\n      {\n        \"label\": \"绘画\",\n        \"color\": \"#98f698\"\n      },\n      {\n        \"label\": \"Akash\",\n        \"color\": \"#a1eecf\"\n      },\n      {\n        \"label\": \"AI\",\n        \"color\": \"#78bbf0\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_figurine\",\n    \"project_link\": \"nonebot-plugin-figurine\",\n    \"author_id\": 99017826,\n    \"tags\": [\n      {\n        \"label\": \"figurine\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"手办\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_distributed_blacklist\",\n    \"project_link\": \"nonebot-plugin-distributed-blacklist\",\n    \"author_id\": 65720409,\n    \"tags\": [\n      {\n        \"label\": \"blacklist\",\n        \"color\": \"#000000\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_bafortune\",\n    \"project_link\": \"nonebot-plugin-bafortune\",\n    \"author_id\": 98072207,\n    \"tags\": [\n      {\n        \"label\": \"碧蓝档案\",\n        \"color\": \"#2b8dce\"\n      },\n      {\n        \"label\": \"今日运势\",\n        \"color\": \"#ce2b2b\"\n      },\n      {\n        \"label\": \"多平台适配\",\n        \"color\": \"#c2ce2b\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_repeat_checker\",\n    \"project_link\": \"nonebot-plugin-repeat-checker\",\n    \"author_id\": 56631400,\n    \"tags\": [\n      {\n        \"label\": \"复读\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_quark_autosave\",\n    \"project_link\": \"nonebot-plugin-quark-autosave\",\n    \"author_id\": 64878354,\n    \"tags\": [\n      {\n        \"label\": \"quark\",\n        \"color\": \"#593dc3\"\n      },\n      {\n        \"label\": \"夸克\",\n        \"color\": \"#6542eb\"\n      },\n      {\n        \"label\": \"网盘\",\n        \"color\": \"#235c84\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_who_is_spy\",\n    \"project_link\": \"nonebot-plugin-who-is-spy\",\n    \"author_id\": 144591143,\n    \"tags\": [\n      {\n        \"label\": \"game\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"游戏\",\n        \"color\": \"#ea52cd\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_dst_qq\",\n    \"project_link\": \"nonebot-plugin-dst-qq\",\n    \"author_id\": 145603392,\n    \"tags\": [\n      {\n        \"label\": \"server\",\n        \"color\": \"#52ea9d\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_fupan\",\n    \"project_link\": \"nonebot-plugin-fupan\",\n    \"author_id\": 867749,\n    \"tags\": [\n      {\n        \"label\": \"股票\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"打卡\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_algo\",\n    \"project_link\": \"nonebot-plugin-algo\",\n    \"author_id\": 188494207,\n    \"tags\": [\n      {\n        \"label\": \"算法竞赛\",\n        \"color\": \"#52c5ea\"\n      },\n      {\n        \"label\": \"ACM\",\n        \"color\": \"#eb0e31\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_llm_extension\",\n    \"project_link\": \"nonebot-plugin-llm-extension\",\n    \"author_id\": 110453675,\n    \"tags\": [\n      {\n        \"label\": \"🇦🇮\",\n        \"color\": \"#f0f0f0\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_htmlkit\",\n    \"project_link\": \"nonebot-plugin-htmlkit\",\n    \"author_id\": 50769752,\n    \"tags\": [\n      {\n        \"label\": \"模板渲染\",\n        \"color\": \"#a57021\"\n      },\n      {\n        \"label\": \"图片生成\",\n        \"color\": \"#74c817\"\n      },\n      {\n        \"label\": \"HTML渲染\",\n        \"color\": \"#199579\"\n      }\n    ],\n    \"is_official\": true\n  },\n  {\n    \"module_name\": \"nonebot_plugin_mhcodes\",\n    \"project_link\": \"nonebot-plugin-mhcodes\",\n    \"author_id\": 99017826,\n    \"tags\": [\n      {\n        \"label\": \"怪物猎人\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_parser\",\n    \"project_link\": \"nonebot-plugin-parser\",\n    \"author_id\": 64878354,\n    \"tags\": [\n      {\n        \"label\": \"图集\",\n        \"color\": \"#263bd5\"\n      },\n      {\n        \"label\": \"视频\",\n        \"color\": \"#152ac0\"\n      },\n      {\n        \"label\": \"解析\",\n        \"color\": \"#152ac0\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_kookcardmessage\",\n    \"project_link\": \"nonebot-plugin-kookcardmessage\",\n    \"author_id\": 174641131,\n    \"tags\": [\n      {\n        \"label\": \"kook\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_mcnews\",\n    \"project_link\": \"nonebot-plugin-mcnews\",\n    \"author_id\": 110646806,\n    \"tags\": [\n      {\n        \"label\": \"MC\",\n        \"color\": \"#52e7ea\"\n      },\n      {\n        \"label\": \"新闻\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_open_file\",\n    \"project_link\": \"nonebot-plugin-open-file\",\n    \"author_id\": 131272574,\n    \"tags\": [\n      {\n        \"label\": \"file\",\n        \"color\": \"#a50a0a\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_memory\",\n    \"project_link\": \"nonebot-plugin-memory\",\n    \"author_id\": 175703143,\n    \"tags\": [\n      {\n        \"label\": \"log\",\n        \"color\": \"#336daf\"\n      },\n      {\n        \"label\": \"func\",\n        \"color\": \"#5bd0a2\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_pxchat\",\n    \"project_link\": \"nonebot-plugin-pxchat\",\n    \"author_id\": 112293881,\n    \"tags\": [\n      {\n        \"label\": \"chat\",\n        \"color\": \"#52eab7\"\n      },\n      {\n        \"label\": \"deepseek\",\n        \"color\": \"#52ddea\"\n      },\n      {\n        \"label\": \"mcp\",\n        \"color\": \"#5292ea\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_daily_bing\",\n    \"project_link\": \"nonebot-plugin-daily-bing\",\n    \"author_id\": 122811297,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_ai_turtle_soup\",\n    \"project_link\": \"nonebot-plugin-ai-turtle-soup\",\n    \"author_id\": 68174188,\n    \"tags\": [\n      {\n        \"label\": \"群聊小游戏\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"海龟汤\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"AI\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_bili_helper\",\n    \"project_link\": \"nonebot-plugin-bili-helper\",\n    \"author_id\": 4216470,\n    \"tags\": [\n      {\n        \"label\": \"bilibili\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"B站\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_markdown2img\",\n    \"project_link\": \"nonebot-plugin-markdown2img\",\n    \"author_id\": 96008766,\n    \"tags\": [\n      {\n        \"label\": \"markdown\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"func\",\n        \"color\": \"#0fe16e\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_jrrp3\",\n    \"project_link\": \"nonebot-plugin-jrrp3\",\n    \"author_id\": 79314033,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_image_symmetry\",\n    \"project_link\": \"nonebot-plugin-image-symmetry\",\n    \"author_id\": 79314033,\n    \"tags\": [\n      {\n        \"label\": \"image\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_manosaba_memes\",\n    \"project_link\": \"nonebot-plugin-manosaba-memes\",\n    \"author_id\": 55650833,\n    \"tags\": [\n      {\n        \"label\": \"魔法少女的魔法审判\",\n        \"color\": \"#de7d92\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_course_schedule\",\n    \"project_link\": \"nonebot-plugin-course-schedule\",\n    \"author_id\": 49135577,\n    \"tags\": [\n      {\n        \"label\": \"课表\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_ipinfo\",\n    \"project_link\": \"nonebot-plugin-ipinfo\",\n    \"author_id\": 122811297,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_templates_draw\",\n    \"project_link\": \"nonebot-plugin-templates-draw\",\n    \"author_id\": 99017826,\n    \"tags\": [\n      {\n        \"label\": \"AI画图\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_manosoba_reply_generator\",\n    \"project_link\": \"nonebot-plugin-manosoba-reply-generator\",\n    \"author_id\": 111682952,\n    \"tags\": [\n      {\n        \"label\": \"func\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_anans_sketchbook\",\n    \"project_link\": \"nonebot-plugin-anans-sketchbook\",\n    \"author_id\": 64982342,\n    \"tags\": [\n      {\n        \"label\": \"魔法少女的魔女审判\",\n        \"color\": \"#d07e82\"\n      },\n      {\n        \"label\": \"夏目安安\",\n        \"color\": \"#6a5acd\"\n      },\n      {\n        \"label\": \"表情包\",\n        \"color\": \"#ca64dc\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_anan_say\",\n    \"project_link\": \"nonebot-plugin-anan-say\",\n    \"author_id\": 122149478,\n    \"tags\": [\n      {\n        \"label\": \"魔女审判\",\n        \"color\": \"#b10ba0\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_terralink\",\n    \"project_link\": \"nonebot-plugin-terralink\",\n    \"author_id\": 96228495,\n    \"tags\": [\n      {\n        \"label\": \"泰拉瑞亚\",\n        \"color\": \"#b6e161\"\n      },\n      {\n        \"label\": \"群服互通\",\n        \"color\": \"#669ed7\"\n      },\n      {\n        \"label\": \"Terraria\",\n        \"color\": \"#df6262\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_railwaytools\",\n    \"project_link\": \"nonebot-plugin-railwaytools\",\n    \"author_id\": 51502183,\n    \"tags\": [\n      {\n        \"label\": \"中国铁路\",\n        \"color\": \"#5287ea\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_omikuji\",\n    \"project_link\": \"nonebot-plugin-omikuji\",\n    \"author_id\": 67693593,\n    \"tags\": [\n      {\n        \"label\": \"御神签\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"运势\",\n        \"color\": \"#0bc2af\"\n      },\n      {\n        \"label\": \"占卜\",\n        \"color\": \"#811eee\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_rollpig\",\n    \"project_link\": \"nonebot-plugin-rollpig\",\n    \"author_id\": 30120610,\n    \"tags\": [\n      {\n        \"label\": \"pig\",\n        \"color\": \"#fdd7e4\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_instagram\",\n    \"project_link\": \"nonebot-plugin-instagram\",\n    \"author_id\": 49683326,\n    \"tags\": [\n      {\n        \"label\": \"instagram\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_mcserver_status_check\",\n    \"project_link\": \"nonebot-plugin-mcserver-status-check\",\n    \"author_id\": 153894603,\n    \"tags\": [\n      {\n        \"label\": \"Minecraft\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"MC\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_perithacus\",\n    \"project_link\": \"nonebot-plugin-pErithacus\",\n    \"author_id\": 21017431,\n    \"tags\": [\n      {\n        \"label\": \"chat\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"自动回复\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_quickreply\",\n    \"project_link\": \"nonebot-plugin-quickreply\",\n    \"author_id\": 104259619,\n    \"tags\": [\n      {\n        \"label\": \"工具\",\n        \"color\": \"#4de024\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_jimeng\",\n    \"project_link\": \"nonebot-plugin-jimeng\",\n    \"author_id\": 104259619,\n    \"tags\": [\n      {\n        \"label\": \"OpenAi\",\n        \"color\": \"#1d7374\"\n      },\n      {\n        \"label\": \"绘画\",\n        \"color\": \"#8b1bb4\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_boardgamehelper\",\n    \"project_link\": \"nonebot-plugin-boardgamehelper\",\n    \"author_id\": 60382099,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_ai_groupmate\",\n    \"project_link\": \"nonebot-plugin-ai-groupmate\",\n    \"author_id\": 30517062,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_trans_progress\",\n    \"project_link\": \"nonebot-plugin-trans-progress\",\n    \"author_id\": 99017826,\n    \"tags\": [\n      {\n        \"label\": \"汉化\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_word_censor\",\n    \"project_link\": \"nonebot-plugin-word-censor\",\n    \"author_id\": 99163726,\n    \"tags\": [\n      {\n        \"label\": \"rule\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"黑名单\",\n        \"color\": \"#000000\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_internet_outage\",\n    \"project_link\": \"nonebot-plugin-internet-outage\",\n    \"author_id\": 110646806,\n    \"tags\": [\n      {\n        \"label\": \"Cloudflare\",\n        \"color\": \"#f97316\"\n      },\n      {\n        \"label\": \"网络中断监测\",\n        \"color\": \"#dc2626\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_group_relay\",\n    \"project_link\": \"nonebot-plugin-group-relay\",\n    \"author_id\": 86188856,\n    \"tags\": [\n      {\n        \"label\": \"func\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_tavily\",\n    \"project_link\": \"nonebot-plugin-tavily\",\n    \"author_id\": 1080807,\n    \"tags\": [\n      {\n        \"label\": \"search\",\n        \"color\": \"#39c5bb\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_bf6_stats\",\n    \"project_link\": \"nonebot-plugin-bf6-stats\",\n    \"author_id\": 202926395,\n    \"tags\": [\n      {\n        \"label\": \"bf6\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_bili2mp4\",\n    \"project_link\": \"nonebot-plugin-bili2mp4\",\n    \"author_id\": 181480818,\n    \"tags\": [\n      {\n        \"label\": \"bilibili\",\n        \"color\": \"#ff6699\"\n      },\n      {\n        \"label\": \"mp4\",\n        \"color\": \"#ff6699\"\n      },\n      {\n        \"label\": \"小程序\",\n        \"color\": \"#ff6699\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_qqmusic_reco\",\n    \"project_link\": \"nonebot-plugin-qqmusic-reco\",\n    \"author_id\": 99163726,\n    \"tags\": [\n      {\n        \"label\": \"music\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"recommend\",\n        \"color\": \"#beeb0c\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_osugreek\",\n    \"project_link\": \"nonebot-plugin-osugreek\",\n    \"author_id\": 64720173,\n    \"tags\": [\n      {\n        \"label\": \"osu\",\n        \"color\": \"#ff66aa\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_uniconf\",\n    \"project_link\": \"nonebot-plugin-uniconf\",\n    \"author_id\": 67693593,\n    \"tags\": [\n      {\n        \"label\": \"config\",\n        \"color\": \"#0cccff\"\n      },\n      {\n        \"label\": \"配置文件\",\n        \"color\": \"#c3ff0c\"\n      },\n      {\n        \"label\": \"API\",\n        \"color\": \"#ffff00\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_dice_helper\",\n    \"project_link\": \"nonebot-plugin-dice-helper\",\n    \"author_id\": 30589360,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_mcpclient\",\n    \"project_link\": \"nonebot-plugin-mcpclient\",\n    \"author_id\": 1080807,\n    \"tags\": [\n      {\n        \"label\": \"mcp\",\n        \"color\": \"#243576\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_BitTorrents\",\n    \"project_link\": \"nonebot-plugin-bittorrents\",\n    \"author_id\": 98020024,\n    \"tags\": [\n      {\n        \"label\": \"func\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_personification\",\n    \"project_link\": \"nonebot-plugin-shiro-personification\",\n    \"author_id\": 114895516,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_shiro_web_console\",\n    \"project_link\": \"nonebot-plugin-shiro-web-console\",\n    \"author_id\": 114895516,\n    \"tags\": [\n      {\n        \"label\": \"WebUi\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_nodejsphira\",\n    \"project_link\": \"nonebot-plugin-nodejsphira\",\n    \"author_id\": 75435667,\n    \"tags\": [\n      {\n        \"label\": \"server\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_doroending\",\n    \"project_link\": \"nonebot-plugin-doroending\",\n    \"author_id\": 127853582,\n    \"tags\": [\n      {\n        \"label\": \"doro\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_auto_emojimix\",\n    \"project_link\": \"nonebot-plugin-auto-emojimix\",\n    \"author_id\": 74812967,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_peek\",\n    \"project_link\": \"nonebot-plugin-peek\",\n    \"author_id\": 74812967,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_rikka\",\n    \"project_link\": \"nonebot-plugin-rikka\",\n    \"author_id\": 72406624,\n    \"tags\": [\n      {\n        \"label\": \"maimai\",\n        \"color\": \"#60bacf\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_cardimg\",\n    \"project_link\": \"nonebot-plugin-cardimg\",\n    \"author_id\": 99666950,\n    \"tags\": [\n      {\n        \"label\": \"模板渲染\",\n        \"color\": \"#dec7ef\"\n      },\n      {\n        \"label\": \"图片生成\",\n        \"color\": \"#f8d8cf\"\n      },\n      {\n        \"label\": \"HTML渲染\",\n        \"color\": \"#c2dff2\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_mute_cat\",\n    \"project_link\": \"nonebot-plugin-mute-cat\",\n    \"author_id\": 199351962,\n    \"tags\": [\n      {\n        \"label\": \"工具\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"群管理\",\n        \"color\": \"#52eacf\"\n      },\n      {\n        \"label\": \"禁言\",\n        \"color\": \"#830daa\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot-plugin-trumpwatcher\",\n    \"project_link\": \"nonebot-plugin-trumpwatcher\",\n    \"author_id\": 42519315,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_mc_whitelist_controller\",\n    \"project_link\": \"nonebot-plugin-mc-whitelist-controller\",\n    \"author_id\": 51502183,\n    \"tags\": [\n      {\n        \"label\": \"Minecraft\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"MC\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"server\",\n        \"color\": \"#ea5252\"\n      }\n    ],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_nbnhhsh\",\n    \"project_link\": \"nonebot-plugin-nbnhhsh\",\n    \"author_id\": 24908800,\n    \"tags\": [],\n    \"is_official\": false\n  },\n  {\n    \"module_name\": \"nonebot_plugin_codex\",\n    \"project_link\": \"nonebot-plugin-codex\",\n    \"author_id\": 98325911,\n    \"tags\": [\n      {\n        \"label\": \"OpenAI\",\n        \"color\": \"#ea5252\"\n      },\n      {\n        \"label\": \"Codex\",\n        \"color\": \"#6fd8fc\"\n      },\n      {\n        \"label\": \"VibeCoding\",\n        \"color\": \"#74fc6f\"\n      }\n    ],\n    \"is_official\": false\n  },\n]\n"
  },
  {
    "path": "nonebot/__init__.py",
    "content": "\"\"\"本模块主要定义了 NoneBot 启动所需函数，供 bot 入口文件调用。\n\n## 快捷导入\n\n为方便使用，本模块从子模块导入了部分内容，以下内容可以直接通过本模块导入:\n\n- `on` => {ref}``on` <nonebot.plugin.on.on>`\n- `on_metaevent` => {ref}``on_metaevent` <nonebot.plugin.on.on_metaevent>`\n- `on_message` => {ref}``on_message` <nonebot.plugin.on.on_message>`\n- `on_notice` => {ref}``on_notice` <nonebot.plugin.on.on_notice>`\n- `on_request` => {ref}``on_request` <nonebot.plugin.on.on_request>`\n- `on_startswith` => {ref}``on_startswith` <nonebot.plugin.on.on_startswith>`\n- `on_endswith` => {ref}``on_endswith` <nonebot.plugin.on.on_endswith>`\n- `on_fullmatch` => {ref}``on_fullmatch` <nonebot.plugin.on.on_fullmatch>`\n- `on_keyword` => {ref}``on_keyword` <nonebot.plugin.on.on_keyword>`\n- `on_command` => {ref}``on_command` <nonebot.plugin.on.on_command>`\n- `on_shell_command` => {ref}``on_shell_command` <nonebot.plugin.on.on_shell_command>`\n- `on_regex` => {ref}``on_regex` <nonebot.plugin.on.on_regex>`\n- `on_type` => {ref}``on_type` <nonebot.plugin.on.on_type>`\n- `CommandGroup` => {ref}``CommandGroup` <nonebot.plugin.on.CommandGroup>`\n- `Matchergroup` => {ref}``MatcherGroup` <nonebot.plugin.on.MatcherGroup>`\n- `load_plugin` => {ref}``load_plugin` <nonebot.plugin.load.load_plugin>`\n- `load_plugins` => {ref}``load_plugins` <nonebot.plugin.load.load_plugins>`\n- `load_all_plugins` => {ref}``load_all_plugins` <nonebot.plugin.load.load_all_plugins>`\n- `load_from_json` => {ref}``load_from_json` <nonebot.plugin.load.load_from_json>`\n- `load_from_toml` => {ref}``load_from_toml` <nonebot.plugin.load.load_from_toml>`\n- `load_builtin_plugin` =>\n  {ref}``load_builtin_plugin` <nonebot.plugin.load.load_builtin_plugin>`\n- `load_builtin_plugins` =>\n  {ref}``load_builtin_plugins` <nonebot.plugin.load.load_builtin_plugins>`\n- `get_plugin` => {ref}``get_plugin` <nonebot.plugin.get_plugin>`\n- `get_plugin_by_module_name` =>\n  {ref}``get_plugin_by_module_name` <nonebot.plugin.get_plugin_by_module_name>`\n- `get_loaded_plugins` =>\n  {ref}``get_loaded_plugins` <nonebot.plugin.get_loaded_plugins>`\n- `get_available_plugin_names` =>\n  {ref}``get_available_plugin_names` <nonebot.plugin.get_available_plugin_names>`\n- `get_plugin_config` => {ref}``get_plugin_config` <nonebot.plugin.get_plugin_config>`\n- `require` => {ref}``require` <nonebot.plugin.load.require>`\n\nFrontMatter:\n    mdx:\n        format: md\n    sidebar_position: 0\n    description: nonebot 模块\n\"\"\"\n\nfrom importlib.metadata import version\nimport os\nfrom typing import Any, TypeVar, overload\n\nimport loguru\n\nfrom nonebot.adapters import Adapter, Bot\nfrom nonebot.compat import model_dump\nfrom nonebot.config import DOTENV_TYPE, Config, Env\nfrom nonebot.drivers import ASGIMixin, Driver, combine_driver\nfrom nonebot.log import logger as logger\nfrom nonebot.utils import escape_tag, resolve_dot_notation\n\ntry:\n    __version__ = version(\"nonebot2\")\nexcept Exception:  # pragma: no cover\n    __version__ = None\n\nA = TypeVar(\"A\", bound=Adapter)\n\n_driver: Driver | None = None\n\n\ndef get_driver() -> Driver:\n    \"\"\"获取全局 {ref}`nonebot.drivers.Driver` 实例。\n\n    可用于在计划任务的回调等情形中获取当前 {ref}`nonebot.drivers.Driver` 实例。\n\n    返回:\n        全局 {ref}`nonebot.drivers.Driver` 对象\n\n    异常:\n        ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化\n            ({ref}`nonebot.init <nonebot.init>` 尚未调用)\n\n    用法:\n        ```python\n        driver = nonebot.get_driver()\n        ```\n    \"\"\"\n    if _driver is None:\n        raise ValueError(\"NoneBot has not been initialized.\")\n    return _driver\n\n\n@overload\ndef get_adapter(name: str) -> Adapter:\n    \"\"\"\n    参数:\n        name: 适配器名称\n\n    返回:\n        指定名称的 {ref}`nonebot.adapters.Adapter` 对象\n    \"\"\"\n\n\n@overload\ndef get_adapter(name: type[A]) -> A:\n    \"\"\"\n    参数:\n        name: 适配器类型\n\n    返回:\n        指定类型的 {ref}`nonebot.adapters.Adapter` 对象\n    \"\"\"\n\n\ndef get_adapter(name: str | type[Adapter]) -> Adapter:\n    \"\"\"获取已注册的 {ref}`nonebot.adapters.Adapter` 实例。\n\n    异常:\n        ValueError: 指定的 {ref}`nonebot.adapters.Adapter` 未注册\n        ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化\n            ({ref}`nonebot.init <nonebot.init>` 尚未调用)\n\n    用法:\n        ```python\n        from nonebot.adapters.console import Adapter\n        adapter = nonebot.get_adapter(Adapter)\n        ```\n    \"\"\"\n    adapters = get_adapters()\n    target = name if isinstance(name, str) else name.get_name()\n    if target not in adapters:\n        raise ValueError(f\"Adapter {target} not registered.\")\n    return adapters[target]\n\n\ndef get_adapters() -> dict[str, Adapter]:\n    \"\"\"获取所有已注册的 {ref}`nonebot.adapters.Adapter` 实例。\n\n    返回:\n        所有 {ref}`nonebot.adapters.Adapter` 实例字典\n\n    异常:\n        ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化\n            ({ref}`nonebot.init <nonebot.init>` 尚未调用)\n\n    用法:\n        ```python\n        adapters = nonebot.get_adapters()\n        ```\n    \"\"\"\n    return get_driver()._adapters.copy()\n\n\ndef get_app() -> Any:\n    \"\"\"获取全局 {ref}`nonebot.drivers.ASGIMixin` 对应的 Server App 对象。\n\n    返回:\n        Server App 对象\n\n    异常:\n        AssertionError: 全局 Driver 对象不是 {ref}`nonebot.drivers.ASGIMixin` 类型\n        ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化\n            ({ref}`nonebot.init <nonebot.init>` 尚未调用)\n\n    用法:\n        ```python\n        app = nonebot.get_app()\n        ```\n    \"\"\"\n    driver = get_driver()\n    assert isinstance(driver, ASGIMixin), \"app object is only available for asgi driver\"\n    return driver.server_app\n\n\ndef get_asgi() -> Any:\n    \"\"\"获取全局 {ref}`nonebot.drivers.ASGIMixin` 对应的\n    [ASGI](https://asgi.readthedocs.io/) 对象。\n\n    返回:\n        ASGI 对象\n\n    异常:\n        AssertionError: 全局 Driver 对象不是 {ref}`nonebot.drivers.ASGIMixin` 类型\n        ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化\n            ({ref}`nonebot.init <nonebot.init>` 尚未调用)\n\n    用法:\n        ```python\n        asgi = nonebot.get_asgi()\n        ```\n    \"\"\"\n    driver = get_driver()\n    assert isinstance(driver, ASGIMixin), (\n        \"asgi object is only available for asgi driver\"\n    )\n    return driver.asgi\n\n\ndef get_bot(self_id: str | None = None) -> Bot:\n    \"\"\"获取一个连接到 NoneBot 的 {ref}`nonebot.adapters.Bot` 对象。\n\n    当提供 `self_id` 时，此函数是 `get_bots()[self_id]` 的简写；\n    当不提供时，返回一个 {ref}`nonebot.adapters.Bot`。\n\n    参数:\n        self_id: 用来识别 {ref}`nonebot.adapters.Bot` 的\n            {ref}`nonebot.adapters.Bot.self_id` 属性\n\n    返回:\n        {ref}`nonebot.adapters.Bot` 对象\n\n    异常:\n        KeyError: 对应 self_id 的 Bot 不存在\n        ValueError: 没有传入 self_id 且没有 Bot 可用\n        ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化\n            ({ref}`nonebot.init <nonebot.init>` 尚未调用)\n\n    用法:\n        ```python\n        assert nonebot.get_bot(\"12345\") == nonebot.get_bots()[\"12345\"]\n\n        another_unspecified_bot = nonebot.get_bot()\n        ```\n    \"\"\"\n    bots = get_bots()\n    if self_id is not None:\n        return bots[self_id]\n\n    for bot in bots.values():\n        return bot\n\n    raise ValueError(\"There are no bots to get.\")\n\n\ndef get_bots() -> dict[str, Bot]:\n    \"\"\"获取所有连接到 NoneBot 的 {ref}`nonebot.adapters.Bot` 对象。\n\n    返回:\n        一个以 {ref}`nonebot.adapters.Bot.self_id` 为键\n        {ref}`nonebot.adapters.Bot` 对象为值的字典\n\n    异常:\n        ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化\n            ({ref}`nonebot.init <nonebot.init>` 尚未调用)\n\n    用法:\n        ```python\n        bots = nonebot.get_bots()\n        ```\n    \"\"\"\n    return get_driver().bots\n\n\ndef _resolve_combine_expr(obj_str: str) -> type[Driver]:\n    drivers = obj_str.split(\"+\")\n    DriverClass = resolve_dot_notation(\n        drivers[0], \"Driver\", default_prefix=\"nonebot.drivers.\"\n    )\n    if len(drivers) == 1:\n        logger.trace(f\"Detected driver {DriverClass} with no mixins.\")\n        return DriverClass\n    mixins = [\n        resolve_dot_notation(mixin, \"Mixin\", default_prefix=\"nonebot.drivers.\")\n        for mixin in drivers[1:]\n    ]\n    logger.trace(f\"Detected driver {DriverClass} with mixins {mixins}.\")\n    return combine_driver(DriverClass, *mixins)\n\n\ndef _log_patcher(record: \"loguru.Record\"):\n    \"\"\"使用插件标识优化日志展示\"\"\"\n    record[\"name\"] = (\n        plugin.id_\n        if (module_name := record[\"name\"])\n        and (plugin := get_plugin_by_module_name(module_name))\n        else (module_name and module_name.split(\".\", maxsplit=1)[0])\n    )\n\n\ndef init(*, _env_file: DOTENV_TYPE | None = None, **kwargs: Any) -> None:\n    \"\"\"初始化 NoneBot 以及 全局 {ref}`nonebot.drivers.Driver` 对象。\n\n    NoneBot 将会从 .env 文件中读取环境信息，并使用相应的 env 文件配置。\n\n    也可以传入自定义的 `_env_file` 来指定 NoneBot 从该文件读取配置。\n\n    参数:\n        _env_file: 配置文件名，默认从 `.env.{env_name}` 中读取配置\n        kwargs: 任意变量，将会存储到 {ref}`nonebot.drivers.Driver.config` 对象里\n\n    用法:\n        ```python\n        nonebot.init(database=Database(...))\n        ```\n    \"\"\"\n    global _driver\n    if not _driver:\n        logger.success(\"NoneBot is initializing...\")\n        env = Env()\n        _env_file = _env_file or f\".env.{env.environment}\"\n        config = Config(\n            **kwargs,\n            _env_file=(\n                (\".env\", _env_file)\n                if isinstance(_env_file, (str, os.PathLike))\n                else _env_file\n            ),\n        )\n\n        logger.configure(\n            extra={\"nonebot_log_level\": config.log_level}, patcher=_log_patcher\n        )\n        logger.opt(colors=True).info(\n            f\"Current <y><b>Env: {escape_tag(env.environment)}</b></y>\"\n        )\n        logger.opt(colors=True).debug(\n            f\"Loaded <y><b>Config</b></y>: {escape_tag(str(model_dump(config)))}\"\n        )\n\n        DriverClass = _resolve_combine_expr(config.driver)\n        _driver = DriverClass(env, config)\n\n\ndef run(*args: Any, **kwargs: Any) -> None:\n    \"\"\"启动 NoneBot，即运行全局 {ref}`nonebot.drivers.Driver` 对象。\n\n    参数:\n        args: 传入 {ref}`nonebot.drivers.Driver.run` 的位置参数\n        kwargs: 传入 {ref}`nonebot.drivers.Driver.run` 的命名参数\n\n    用法:\n        ```python\n        nonebot.run(host=\"127.0.0.1\", port=8080)\n        ```\n    \"\"\"\n    logger.success(\"Running NoneBot...\")\n    get_driver().run(*args, **kwargs)\n\n\nfrom nonebot.plugin import CommandGroup as CommandGroup\nfrom nonebot.plugin import MatcherGroup as MatcherGroup\nfrom nonebot.plugin import get_available_plugin_names as get_available_plugin_names\nfrom nonebot.plugin import get_loaded_plugins as get_loaded_plugins\nfrom nonebot.plugin import get_plugin as get_plugin\nfrom nonebot.plugin import get_plugin_by_module_name as get_plugin_by_module_name\nfrom nonebot.plugin import get_plugin_config as get_plugin_config\nfrom nonebot.plugin import load_all_plugins as load_all_plugins\nfrom nonebot.plugin import load_builtin_plugin as load_builtin_plugin\nfrom nonebot.plugin import load_builtin_plugins as load_builtin_plugins\nfrom nonebot.plugin import load_from_json as load_from_json\nfrom nonebot.plugin import load_from_toml as load_from_toml\nfrom nonebot.plugin import load_plugin as load_plugin\nfrom nonebot.plugin import load_plugins as load_plugins\nfrom nonebot.plugin import on as on\nfrom nonebot.plugin import on_command as on_command\nfrom nonebot.plugin import on_endswith as on_endswith\nfrom nonebot.plugin import on_fullmatch as on_fullmatch\nfrom nonebot.plugin import on_keyword as on_keyword\nfrom nonebot.plugin import on_message as on_message\nfrom nonebot.plugin import on_metaevent as on_metaevent\nfrom nonebot.plugin import on_notice as on_notice\nfrom nonebot.plugin import on_regex as on_regex\nfrom nonebot.plugin import on_request as on_request\nfrom nonebot.plugin import on_shell_command as on_shell_command\nfrom nonebot.plugin import on_startswith as on_startswith\nfrom nonebot.plugin import on_type as on_type\nfrom nonebot.plugin import require as require\n"
  },
  {
    "path": "nonebot/adapters/__init__.py",
    "content": "\"\"\"本模块定义了协议适配基类，各协议请继承以下基类。\n\n使用 {ref}`nonebot.drivers.Driver.register_adapter` 注册适配器。\n\nFrontMatter:\n    mdx:\n        format: md\n    sidebar_position: 0\n    description: nonebot.adapters 模块\n\"\"\"\n\nfrom nonebot.internal.adapter import Adapter as Adapter\nfrom nonebot.internal.adapter import Bot as Bot\nfrom nonebot.internal.adapter import Event as Event\nfrom nonebot.internal.adapter import Message as Message\nfrom nonebot.internal.adapter import MessageSegment as MessageSegment\nfrom nonebot.internal.adapter import MessageTemplate as MessageTemplate\n\n__autodoc__ = {\n    \"Bot\": True,\n    \"Event\": True,\n    \"Adapter\": True,\n    \"Message\": True,\n    \"Message.__getitem__\": True,\n    \"Message.__contains__\": True,\n    \"Message._construct\": True,\n    \"MessageSegment\": True,\n    \"MessageSegment.__str__\": True,\n    \"MessageSegment.__add__\": True,\n    \"MessageTemplate\": True,\n}\n"
  },
  {
    "path": "nonebot/compat.py",
    "content": "\"\"\"本模块为 Pydantic 版本兼容层模块\n\n为兼容 Pydantic V1 与 V2 版本，定义了一系列兼容函数与类供使用。\n\nFrontMatter:\n    mdx:\n        format: md\n    sidebar_position: 16\n    description: nonebot.compat 模块\n\"\"\"\n\nfrom collections.abc import Callable, Generator\nfrom dataclasses import dataclass, is_dataclass\nfrom functools import cached_property, wraps\nfrom typing import (\n    TYPE_CHECKING,\n    Annotated,\n    Any,\n    Generic,\n    Literal,\n    Protocol,\n    TypeAlias,\n    TypeVar,\n    cast,\n    get_args,\n    get_origin,\n    overload,\n)\nfrom typing_extensions import ParamSpec, Self, is_typeddict\n\nfrom pydantic import VERSION, BaseModel\n\nfrom nonebot.typing import origin_is_annotated\n\nT = TypeVar(\"T\")\nP = ParamSpec(\"P\")\n\nPYDANTIC_V2 = int(VERSION.split(\".\", 1)[0]) == 2\n\nif TYPE_CHECKING:\n\n    class _CustomValidationClass(Protocol):\n        @classmethod\n        def __get_validators__(cls) -> Generator[Callable[..., Any], None, None]: ...\n\n    CVC = TypeVar(\"CVC\", bound=_CustomValidationClass)\n\n\nModelDumpIncEx: TypeAlias = (\n    set[int]\n    | set[str]\n    | dict[int, \"ModelDumpIncEx\"]\n    | dict[str, \"ModelDumpIncEx\"]\n    | None\n)\n\"\"\"Common include/exclude shape accepted by all supported pydantic versions.\"\"\"\n\n\n__all__ = (\n    \"DEFAULT_CONFIG\",\n    \"PYDANTIC_V2\",\n    \"ConfigDict\",\n    \"FieldInfo\",\n    \"LegacyUnionField\",\n    \"ModelField\",\n    \"PydanticUndefined\",\n    \"PydanticUndefinedType\",\n    \"Required\",\n    \"TypeAdapter\",\n    \"custom_validation\",\n    \"field_validator\",\n    \"model_config\",\n    \"model_dump\",\n    \"model_fields\",\n    \"model_validator\",\n    \"type_validate_json\",\n    \"type_validate_python\",\n)\n\n__autodoc__ = {\n    \"PydanticUndefined\": \"Pydantic Undefined object\",\n    \"PydanticUndefinedType\": \"Pydantic Undefined type\",\n}\n\n\nif PYDANTIC_V2:  # pragma: pydantic-v2\n    from pydantic import Field, GetCoreSchemaHandler\n    from pydantic import TypeAdapter as TypeAdapter\n    from pydantic import field_validator as field_validator\n    from pydantic import model_validator as model_validator\n    from pydantic._internal._repr import display_as_type\n    from pydantic.fields import FieldInfo as BaseFieldInfo\n    from pydantic_core import CoreSchema, core_schema\n\n    Required = Ellipsis\n    \"\"\"Alias of Ellipsis for compatibility with pydantic v1\"\"\"\n\n    # Export undefined type\n    from pydantic_core import PydanticUndefined as PydanticUndefined\n    from pydantic_core import PydanticUndefinedType as PydanticUndefinedType\n\n    # isort: split\n\n    # Export model config dict\n    from pydantic import ConfigDict as ConfigDict\n\n    DEFAULT_CONFIG = ConfigDict(extra=\"allow\", arbitrary_types_allowed=True)\n    \"\"\"Default config for validations\"\"\"\n\n    def _get_legacy_union_field(func: Callable[P, T]) -> Callable[P, T]:\n        @wraps(func)\n        def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:\n            kwargs[\"union_mode\"] = \"left_to_right\"\n            return func(*args, **kwargs)\n\n        return wrapper\n\n    LegacyUnionField = _get_legacy_union_field(Field)\n    LegacyUnionField.__doc__ = \"Mark field to use legacy left to right union mode\"\n\n    class FieldInfo(BaseFieldInfo):  # pyright: ignore[reportGeneralTypeIssues]\n        \"\"\"FieldInfo class with extra property for compatibility with pydantic v1\"\"\"\n\n        # make default can be positional argument\n        def __init__(self, default: Any = PydanticUndefined, **kwargs: Any) -> None:\n            super().__init__(default=default, **kwargs)\n\n        @property\n        def extra(self) -> dict[str, Any]:\n            \"\"\"Extra data that is not part of the standard pydantic fields.\n\n            For compatibility with pydantic v1.\n            \"\"\"\n            # extract extra data from attributes set except used slots\n            # we need to call super in advance due to\n            # comprehension not inlined in cpython < 3.12\n            # https://peps.python.org/pep-0709/\n            slots = super().__slots__\n            return {k: v for k, v in self._attributes_set.items() if k not in slots}\n\n        @classmethod\n        def _inherit_construct(\n            cls, field_info: BaseFieldInfo | None = None, **kwargs: Any\n        ) -> Self:\n            init_kwargs = {}\n            if field_info:\n                init_kwargs.update(field_info._attributes_set)\n            init_kwargs.update(kwargs)\n\n            instance = cls(**init_kwargs)\n            if field_info:\n                instance.metadata = field_info.metadata\n            return instance\n\n    @dataclass\n    class ModelField:\n        \"\"\"ModelField class for compatibility with pydantic v1\"\"\"\n\n        name: str\n        \"\"\"The name of the field.\"\"\"\n        annotation: Any\n        \"\"\"The annotation of the field.\"\"\"\n        field_info: FieldInfo\n        \"\"\"The FieldInfo of the field.\"\"\"\n\n        @classmethod\n        def _construct(cls, name: str, annotation: Any, field_info: FieldInfo) -> Self:\n            return cls(name, annotation, field_info)\n\n        @classmethod\n        def construct(\n            cls, name: str, annotation: Any, field_info: FieldInfo | None = None\n        ) -> Self:\n            \"\"\"Construct a ModelField from given infos.\"\"\"\n            return cls._construct(name, annotation, field_info or FieldInfo())\n\n        def __hash__(self) -> int:\n            # Each ModelField is unique for our purposes,\n            # to allow store them in a set.\n            return id(self)\n\n        @cached_property\n        def type_adapter(self) -> TypeAdapter:\n            \"\"\"TypeAdapter of the field.\n\n            Cache the TypeAdapter to avoid creating it multiple times.\n            Pydantic v2 uses too much cpu time to create TypeAdapter.\n\n            See: https://github.com/pydantic/pydantic/issues/9834\n            \"\"\"\n            return TypeAdapter(\n                Annotated[self.annotation, self.field_info],\n                config=None if self._annotation_has_config() else DEFAULT_CONFIG,\n            )\n\n        def _annotation_has_config(self) -> bool:\n            \"\"\"Check if the annotation has config.\n\n            TypeAdapter raise error when annotation has config\n            and given config is not None.\n            \"\"\"\n            type_is_annotated = origin_is_annotated(get_origin(self.annotation))\n            inner_type = (\n                get_args(self.annotation)[0] if type_is_annotated else self.annotation\n            )\n            try:\n                return (\n                    issubclass(inner_type, BaseModel)\n                    or is_dataclass(inner_type)\n                    or is_typeddict(inner_type)\n                )\n            except TypeError:\n                return False\n\n        def get_default(self) -> Any:\n            \"\"\"Get the default value of the field.\"\"\"\n            return self.field_info.get_default(call_default_factory=True)\n\n        def _type_display(self):\n            \"\"\"Get the display of the type of the field.\"\"\"\n            return display_as_type(self.annotation)\n\n        def validate_value(self, value: Any) -> Any:\n            \"\"\"Validate the value pass to the field.\"\"\"\n            return self.type_adapter.validate_python(value)\n\n    def model_fields(model: type[BaseModel]) -> list[ModelField]:\n        \"\"\"Get field list of a model.\"\"\"\n\n        return [\n            ModelField._construct(\n                name=name,\n                annotation=field_info.rebuild_annotation(),\n                field_info=FieldInfo._inherit_construct(field_info),\n            )\n            for name, field_info in model.model_fields.items()\n        ]\n\n    def model_config(model: type[BaseModel]) -> Any:\n        \"\"\"Get config of a model.\"\"\"\n        return model.model_config\n\n    def model_dump(\n        model: BaseModel,\n        include: ModelDumpIncEx = None,\n        exclude: ModelDumpIncEx = None,\n        by_alias: bool = False,\n        exclude_unset: bool = False,\n        exclude_defaults: bool = False,\n        exclude_none: bool = False,\n    ) -> dict[str, Any]:\n        return model.model_dump(\n            # Nested types cannot be inferred correctly\n            include=cast(Any, include),\n            exclude=cast(Any, exclude),\n            by_alias=by_alias,\n            exclude_unset=exclude_unset,\n            exclude_defaults=exclude_defaults,\n            exclude_none=exclude_none,\n        )\n\n    def type_validate_python(type_: type[T], data: Any) -> T:\n        \"\"\"Validate data with given type.\"\"\"\n        return TypeAdapter(type_).validate_python(data)\n\n    def type_validate_json(type_: type[T], data: str | bytes) -> T:\n        \"\"\"Validate JSON with given type.\"\"\"\n        return TypeAdapter(type_).validate_json(data)\n\n    def __get_pydantic_core_schema__(\n        cls: type[\"_CustomValidationClass\"],\n        source_type: Any,\n        handler: GetCoreSchemaHandler,\n    ) -> CoreSchema:\n        validators = list(cls.__get_validators__())\n        if len(validators) == 1:\n            return core_schema.no_info_plain_validator_function(validators[0])\n        return core_schema.chain_schema(\n            [core_schema.no_info_plain_validator_function(func) for func in validators]\n        )\n\n    def custom_validation(class_: type[\"CVC\"]) -> type[\"CVC\"]:\n        \"\"\"Use pydantic v1 like validator generator in pydantic v2\"\"\"\n\n        setattr(\n            class_,\n            \"__get_pydantic_core_schema__\",\n            classmethod(__get_pydantic_core_schema__),\n        )\n        return class_\n\nelse:  # pragma: pydantic-v1\n    from pydantic import BaseConfig as PydanticConfig\n    from pydantic import Extra, parse_obj_as, parse_raw_as, root_validator, validator\n    from pydantic.fields import FieldInfo as BaseFieldInfo\n    from pydantic.fields import ModelField as BaseModelField\n    from pydantic.schema import get_annotation_from_field_info\n\n    # isort: split\n\n    from pydantic.fields import Required as Required\n\n    # isort: split\n\n    from pydantic.fields import Undefined as PydanticUndefined\n    from pydantic.fields import UndefinedType as PydanticUndefinedType\n\n    class ConfigDict(PydanticConfig):\n        \"\"\"Config class that allow get value with default value.\"\"\"\n\n        @classmethod\n        def get(cls, field: str, default: Any = None) -> Any:\n            \"\"\"Get a config value.\"\"\"\n            return getattr(cls, field, default)\n\n    class DEFAULT_CONFIG(ConfigDict):\n        extra = Extra.allow\n        arbitrary_types_allowed = True\n\n    from pydantic.fields import Field as LegacyUnionField\n\n    class FieldInfo(BaseFieldInfo):\n        def __init__(self, default: Any = PydanticUndefined, **kwargs: Any):\n            # preprocess default value to make it compatible with pydantic v2\n            # when default is Required, set it to PydanticUndefined\n            if default is Required:\n                default = PydanticUndefined\n            super().__init__(default, **kwargs)\n\n        @classmethod\n        def _inherit_construct(\n            cls, field_info: BaseFieldInfo | None = None, **kwargs: Any\n        ):\n            if field_info:\n                init_kwargs = {\n                    s: getattr(field_info, s)\n                    for s in field_info.__slots__\n                    if s != \"extra\"\n                }\n                init_kwargs.update(field_info.extra)\n            else:\n                init_kwargs = {}\n            init_kwargs.update(kwargs)\n            return cls(**init_kwargs)\n\n    class ModelField(BaseModelField):\n        @classmethod\n        def _construct(cls, name: str, annotation: Any, field_info: FieldInfo) -> Self:\n            return cls(\n                name=name,\n                type_=annotation,\n                class_validators=None,\n                model_config=DEFAULT_CONFIG,\n                default=field_info.default,\n                default_factory=field_info.default_factory,\n                required=(\n                    field_info.default is PydanticUndefined\n                    and field_info.default_factory is None\n                ),\n                field_info=field_info,\n            )\n\n        @classmethod\n        def construct(\n            cls, name: str, annotation: Any, field_info: FieldInfo | None = None\n        ) -> Self:\n            \"\"\"Construct a ModelField from given infos.\n\n            Field annotation is preprocessed with field_info.\n            \"\"\"\n            if field_info is not None:\n                annotation = get_annotation_from_field_info(\n                    annotation, field_info, name\n                )\n            return cls._construct(name, annotation, field_info or FieldInfo())\n\n        def validate_value(self, value: Any) -> Any:\n            \"\"\"Validate the value pass to the field.\"\"\"\n            v, errs_ = self.validate(value, {}, loc=())\n            if errs_:\n                raise ValueError(value, self)\n            return v\n\n    class TypeAdapter(Generic[T]):\n        @overload\n        def __init__(\n            self,\n            type: type[T],\n            *,\n            config: ConfigDict | None = ...,\n        ) -> None: ...\n\n        @overload\n        def __init__(\n            self,\n            type: Any,\n            *,\n            config: ConfigDict | None = ...,\n        ) -> None: ...\n\n        def __init__(\n            self,\n            type: Any,\n            *,\n            config: ConfigDict | None = None,\n        ) -> None:\n            self.type = type\n            self.config = config\n\n        def validate_python(self, value: Any) -> T:\n            return type_validate_python(self.type, value)\n\n        def validate_json(self, value: str | bytes) -> T:\n            return type_validate_json(self.type, value)\n\n    @overload\n    def field_validator(\n        field: str,\n        /,\n        *fields: str,\n        mode: Literal[\"before\"],\n        check_fields: bool | None = None,\n    ): ...\n\n    @overload\n    def field_validator(\n        field: str,\n        /,\n        *fields: str,\n        mode: Literal[\"after\"] = ...,\n        check_fields: bool | None = None,\n    ): ...\n\n    def field_validator(\n        field: str,\n        /,\n        *fields: str,\n        mode: Literal[\"before\", \"after\"] = \"after\",\n        check_fields: bool | None = None,\n    ):\n        if mode == \"before\":\n            return validator(\n                field,\n                *fields,\n                pre=True,\n                check_fields=check_fields or True,\n                allow_reuse=True,\n            )\n        else:\n            return validator(\n                field, *fields, check_fields=check_fields or True, allow_reuse=True\n            )\n\n    def model_fields(model: type[BaseModel]) -> list[ModelField]:\n        \"\"\"Get field list of a model.\"\"\"\n\n        # construct the model field without preprocess to avoid error\n        return [\n            ModelField._construct(\n                name=model_field.name,\n                annotation=model_field.annotation,\n                field_info=FieldInfo._inherit_construct(model_field.field_info),\n            )\n            for model_field in model.__fields__.values()\n        ]\n\n    def model_config(model: type[BaseModel]) -> Any:\n        \"\"\"Get config of a model.\"\"\"\n        return model.__config__\n\n    def model_dump(\n        model: BaseModel,\n        include: ModelDumpIncEx = None,\n        exclude: ModelDumpIncEx = None,\n        by_alias: bool = False,\n        exclude_unset: bool = False,\n        exclude_defaults: bool = False,\n        exclude_none: bool = False,\n    ) -> dict[str, Any]:\n        return model.dict(\n            include=cast(Any, include),\n            exclude=cast(Any, exclude),\n            by_alias=by_alias,\n            exclude_unset=exclude_unset,\n            exclude_defaults=exclude_defaults,\n            exclude_none=exclude_none,\n        )\n\n    @overload\n    def model_validator(*, mode: Literal[\"before\"]): ...\n\n    @overload\n    def model_validator(*, mode: Literal[\"after\"]): ...\n\n    def model_validator(*, mode: Literal[\"before\", \"after\"]):\n        if mode == \"before\":\n            return root_validator(pre=True, allow_reuse=True)\n        else:\n            return root_validator(skip_on_failure=True, allow_reuse=True)\n\n    def type_validate_python(type_: type[T], data: Any) -> T:\n        \"\"\"Validate data with given type.\"\"\"\n        return parse_obj_as(type_, data)\n\n    def type_validate_json(type_: type[T], data: str | bytes) -> T:\n        \"\"\"Validate JSON with given type.\"\"\"\n        return parse_raw_as(type_, data)\n\n    def custom_validation(class_: type[\"CVC\"]) -> type[\"CVC\"]:\n        \"\"\"Do nothing in pydantic v1\"\"\"\n        return class_\n"
  },
  {
    "path": "nonebot/config.py",
    "content": "\"\"\"本模块定义了 NoneBot 本身运行所需的配置项。\n\nNoneBot 使用 [`pydantic`](https://pydantic-docs.helpmanual.io/) 以及\n[`python-dotenv`](https://saurabh-kumar.com/python-dotenv/) 来读取配置。\n\n配置项需符合特殊格式或 json 序列化格式\n详情见 [`pydantic Field Type`](https://pydantic-docs.helpmanual.io/usage/types/) 文档。\n\nFrontMatter:\n    mdx:\n        format: md\n    sidebar_position: 1\n    description: nonebot.config 模块\n\"\"\"\n\nimport abc\nfrom collections.abc import Mapping\nfrom datetime import timedelta\nfrom ipaddress import IPv4Address\nimport json\nimport os\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, Any, TypeAlias, get_args, get_origin\n\nfrom dotenv import dotenv_values\nfrom pydantic import BaseModel, Field\nfrom pydantic.networks import IPvAnyAddress\n\nfrom nonebot.compat import (\n    PYDANTIC_V2,\n    ConfigDict,\n    LegacyUnionField,\n    ModelField,\n    PydanticUndefined,\n    PydanticUndefinedType,\n    model_config,\n    model_fields,\n)\nfrom nonebot.log import logger\nfrom nonebot.typing import origin_is_union\nfrom nonebot.utils import deep_update, lenient_issubclass, type_is_complex\n\nDOTENV_TYPE: TypeAlias = Path | str | list[Path | str] | tuple[Path | str, ...]\n\nENV_FILE_SENTINEL = Path(\"\")\n\n\nclass SettingsError(ValueError): ...\n\n\nclass BaseSettingsSource(abc.ABC):\n    def __init__(self, settings_cls: type[BaseModel]) -> None:\n        self.settings_cls = settings_cls\n\n    @property\n    def config(self) -> \"SettingsConfig\":\n        return model_config(self.settings_cls)\n\n    @abc.abstractmethod\n    def __call__(self) -> dict[str, Any]:\n        raise NotImplementedError\n\n\nclass InitSettingsSource(BaseSettingsSource):\n    __slots__ = (\"init_kwargs\",)\n\n    def __init__(\n        self, settings_cls: type[BaseModel], init_kwargs: dict[str, Any]\n    ) -> None:\n        self.init_kwargs = init_kwargs\n        super().__init__(settings_cls)\n\n    def __call__(self) -> dict[str, Any]:\n        return self.init_kwargs\n\n    def __repr__(self) -> str:\n        return f\"InitSettingsSource(init_kwargs={self.init_kwargs!r})\"\n\n\nclass DotEnvSettingsSource(BaseSettingsSource):\n    def __init__(\n        self,\n        settings_cls: type[BaseModel],\n        env_file: DOTENV_TYPE | None,\n        env_file_encoding: str,\n        case_sensitive: bool | None = False,\n        env_nested_delimiter: str | None = None,\n    ) -> None:\n        super().__init__(settings_cls)\n        self.env_file = env_file\n        self.env_file_encoding = env_file_encoding\n        self.case_sensitive = case_sensitive\n        self.env_nested_delimiter = env_nested_delimiter\n\n    def _apply_case_sensitive(self, var_name: str) -> str:\n        return var_name if self.case_sensitive else var_name.lower()\n\n    def _field_is_complex(self, field: ModelField) -> tuple[bool, bool]:\n        if type_is_complex(field.annotation):\n            return True, False\n        elif origin_is_union(get_origin(field.annotation)) and any(\n            type_is_complex(arg) for arg in get_args(field.annotation)\n        ):\n            return True, True\n        return False, False\n\n    def _parse_env_vars(\n        self, env_vars: Mapping[str, str | None]\n    ) -> dict[str, str | None]:\n        return {\n            self._apply_case_sensitive(key): value for key, value in env_vars.items()\n        }\n\n    def _read_env_file(self, file_path: Path) -> dict[str, str | None]:\n        file_vars = dotenv_values(file_path, encoding=self.env_file_encoding)\n        return self._parse_env_vars(file_vars)\n\n    def _read_env_files(self) -> dict[str, str | None]:\n        env_files = self.env_file\n        if env_files is None:\n            return {}\n\n        if isinstance(env_files, (str, os.PathLike)):\n            env_files = [env_files]\n\n        dotenv_vars: dict[str, str | None] = {}\n        for env_file in env_files:\n            env_path = Path(env_file).expanduser()\n            if env_path.is_file():\n                dotenv_vars.update(self._read_env_file(env_path))\n        return dotenv_vars\n\n    def _next_field(self, field: ModelField | None, key: str) -> ModelField | None:\n        if not field or origin_is_union(get_origin(field.annotation)):\n            return None\n        elif field.annotation and lenient_issubclass(field.annotation, BaseModel):\n            for field in model_fields(field.annotation):\n                if field.name == key:\n                    return field\n        return None\n\n    def _explode_env_vars(\n        self,\n        field: ModelField,\n        env_vars: dict[str, str | None],\n        env_file_vars: dict[str, str | None],\n    ) -> dict[str, Any]:\n        if self.env_nested_delimiter is None:\n            return {}\n\n        prefix = f\"{field.name}{self.env_nested_delimiter}\"\n        result: dict[str, Any] = {}\n        for env_name, env_val in env_vars.items():\n            if not env_name.startswith(prefix):\n                continue\n\n            # delete from file vars when used\n            env_file_vars.pop(env_name, None)\n\n            _, *keys, last_key = env_name.split(self.env_nested_delimiter)\n            env_var = result\n            target_field: ModelField | None = field\n            for key in keys:\n                target_field = self._next_field(target_field, key)\n                env_var = env_var.setdefault(key, {})\n\n            target_field = self._next_field(target_field, last_key)\n            if target_field and env_val:\n                is_complex, allow_parse_failure = self._field_is_complex(target_field)\n                if is_complex:\n                    try:\n                        env_val = json.loads(env_val)\n                    except ValueError as e:\n                        if not allow_parse_failure:\n                            raise SettingsError(\n                                f'error parsing env var \"{env_name}\"'\n                            ) from e\n\n            env_var[last_key] = env_val\n\n        return result\n\n    def __call__(self) -> dict[str, Any]:\n        \"\"\"从环境变量和 dotenv 配置文件中读取配置项。\"\"\"\n\n        d: dict[str, Any] = {}\n\n        env_vars = self._parse_env_vars(os.environ)\n        env_file_vars = self._read_env_files()\n        env_vars = {**env_file_vars, **env_vars}\n\n        for field in model_fields(self.settings_cls):\n            field_name = field.name\n            env_name = self._apply_case_sensitive(field_name)\n            alias_name = field.field_info.alias\n            alias_env_name = (\n                None if alias_name is None else self._apply_case_sensitive(alias_name)\n            )\n\n            # pydantic use alias name to validate if exist\n            if alias_name is not None:\n                field_name = alias_name\n\n            # try get values from env vars\n            env_val = env_vars.get(env_name, PydanticUndefined)\n            alias_env_val = (\n                PydanticUndefined\n                if alias_env_name is None\n                else env_vars.get(alias_env_name, PydanticUndefined)\n            )\n            # alias env value has higher priority\n            env_val = (\n                env_val\n                if isinstance(alias_env_val, PydanticUndefinedType)\n                else alias_env_val\n            )\n            # delete from file vars when used\n            if env_name in env_file_vars:\n                del env_file_vars[env_name]\n            if alias_env_name is not None and alias_env_name in env_file_vars:\n                del env_file_vars[alias_env_name]\n\n            is_complex, allow_parse_failure = self._field_is_complex(field)\n            if is_complex:\n                if isinstance(env_val, PydanticUndefinedType):\n                    # field is complex but no value found so far, try explode_env_vars\n                    if env_val_built := self._explode_env_vars(\n                        field, env_vars, env_file_vars\n                    ):\n                        d[field_name] = env_val_built\n                elif env_val is None:\n                    d[field_name] = env_val\n                else:\n                    # field is complex and there's a value\n                    # decode that as JSON, then add explode_env_vars\n                    try:\n                        env_val = json.loads(env_val)\n                    except ValueError as e:\n                        if not allow_parse_failure:\n                            raise SettingsError(\n                                f'error parsing env var \"{env_name}\"'\n                            ) from e\n\n                    if isinstance(env_val, dict):\n                        # field value is a dict\n                        # try explode_env_vars to find more sub-values\n                        d[field_name] = deep_update(\n                            env_val,\n                            self._explode_env_vars(field, env_vars, env_file_vars),\n                        )\n                    else:\n                        d[field_name] = env_val\n            elif env_val is not PydanticUndefined:\n                # simplest case, field is not complex\n                # we only need to add the value if it was found\n                d[field_name] = env_val\n\n        # remain user custom config\n        for env_name in env_file_vars:\n            env_val = env_vars[env_name]\n            if env_val and (val_striped := env_val.strip()):\n                # there's a value, decode that as JSON\n                try:\n                    env_val = json.loads(val_striped)\n                except ValueError:\n                    logger.trace(\n                        \"Error while parsing JSON for \"\n                        f\"{env_name!r}={val_striped!r}. \"\n                        \"Assumed as string.\"\n                    )\n\n            # explode value when it's a nested dict\n            env_name, *nested_keys = env_name.split(self.env_nested_delimiter)\n            if nested_keys and (env_name not in d or isinstance(d[env_name], dict)):\n                result = {}\n                *keys, last_key = nested_keys\n                _tmp = result\n                for key in keys:\n                    _tmp = _tmp.setdefault(key, {})\n                _tmp[last_key] = env_val\n                d[env_name] = deep_update(d.get(env_name, {}), result)\n            elif not nested_keys:\n                d[env_name] = env_val\n\n        return d\n\n\nif PYDANTIC_V2:  # pragma: pydantic-v2\n\n    class SettingsConfig(ConfigDict, total=False):\n        env_file: DOTENV_TYPE | None\n        env_file_encoding: str\n        case_sensitive: bool\n        env_nested_delimiter: str | None\n\nelse:  # pragma: pydantic-v1\n\n    class SettingsConfig(ConfigDict):\n        env_file: DOTENV_TYPE | None\n        env_file_encoding: str\n        case_sensitive: bool\n        env_nested_delimiter: str | None\n\n\nclass BaseSettings(BaseModel):\n    if TYPE_CHECKING:\n        # dummy getattr for pylance checking, actually not used\n        def __getattr__(self, name: str) -> Any:  # pragma: no cover\n            return self.__dict__.get(name)\n\n    if PYDANTIC_V2:  # pragma: pydantic-v2\n        model_config = SettingsConfig(\n            extra=\"allow\",\n            env_file=\".env\",\n            env_file_encoding=\"utf-8\",\n            case_sensitive=False,\n            env_nested_delimiter=\"__\",\n        )\n    else:  # pragma: pydantic-v1\n\n        class Config(SettingsConfig):\n            extra = \"allow\"  # type: ignore\n            env_file = \".env\"\n            env_file_encoding = \"utf-8\"\n            case_sensitive = False\n            env_nested_delimiter = \"__\"\n\n    def __init__(\n        __settings_self__,  # pyright: ignore[reportSelfClsParameterName]\n        _env_file: DOTENV_TYPE | None = ENV_FILE_SENTINEL,\n        _env_file_encoding: str | None = None,\n        _env_nested_delimiter: str | None = None,\n        **values: Any,\n    ) -> None:\n        settings_config = model_config(__settings_self__.__class__)\n        env_file = (\n            _env_file\n            if _env_file is not ENV_FILE_SENTINEL\n            else settings_config.get(\"env_file\", (\".env\",))\n        )\n        env_file_encoding = (\n            _env_file_encoding\n            if _env_file_encoding is not None\n            else settings_config.get(\"env_file_encoding\", \"utf-8\")\n        )\n        env_nested_delimiter = (\n            _env_nested_delimiter\n            if _env_nested_delimiter is not None\n            else settings_config.get(\"env_nested_delimiter\", None)\n        )\n\n        super().__init__(\n            **__settings_self__._settings_build_values(\n                __settings_self__.__class__,\n                values,\n                env_file=env_file,\n                env_file_encoding=env_file_encoding,\n                env_nested_delimiter=env_nested_delimiter,\n            )\n        )\n\n        __settings_self__._env_file = env_file\n        __settings_self__._env_file_encoding = env_file_encoding\n        __settings_self__._env_nested_delimiter = env_nested_delimiter\n\n    @staticmethod\n    def _settings_build_values(\n        settings_cls: type[BaseModel],\n        init_kwargs: dict[str, Any],\n        env_file: DOTENV_TYPE | None,\n        env_file_encoding: str,\n        env_nested_delimiter: str | None,\n    ) -> dict[str, Any]:\n        init_settings = InitSettingsSource(settings_cls, init_kwargs=init_kwargs)\n        env_settings = DotEnvSettingsSource(\n            settings_cls,\n            env_file=env_file,\n            env_file_encoding=env_file_encoding,\n            env_nested_delimiter=env_nested_delimiter,\n        )\n        return deep_update(env_settings(), init_settings())\n\n\nclass Env(BaseSettings):\n    \"\"\"运行环境配置。大小写不敏感。\n\n    将会从 **环境变量** > **dotenv 配置文件** 的优先级读取环境信息。\n    \"\"\"\n\n    environment: str = \"prod\"\n    \"\"\"当前环境名。\n\n    NoneBot 将从 `.env.{environment}` 文件中加载配置。\n    \"\"\"\n\n\nclass Config(BaseSettings):\n    \"\"\"NoneBot 主要配置。大小写不敏感。\n\n    除了 NoneBot 的配置项外，还可以自行添加配置项到 `.env.{environment}` 文件中。\n    这些配置将会在 json 反序列化后一起带入 `Config` 类中。\n\n    配置方法参考: [配置](https://nonebot.dev/docs/appendices/config)\n    \"\"\"\n\n    if TYPE_CHECKING:\n        _env_file: DOTENV_TYPE | None = \".env\", \".env.prod\"\n\n    # nonebot configs\n    driver: str = \"~fastapi\"\n    \"\"\"NoneBot 运行所使用的 `Driver` 。继承自 {ref}`nonebot.drivers.Driver` 。\n\n    配置格式为 `<module>[:<Driver>][+<module>[:<Mixin>]]*`。\n\n    `~` 为 `nonebot.drivers.` 的缩写。\n\n    配置方法参考: [配置驱动器](https://nonebot.dev/docs/advanced/driver#%E9%85%8D%E7%BD%AE%E9%A9%B1%E5%8A%A8%E5%99%A8)\n    \"\"\"\n    host: IPvAnyAddress = IPv4Address(\"127.0.0.1\")  # type: ignore\n    \"\"\"NoneBot {ref}`nonebot.drivers.ReverseDriver` 服务端监听的 IP/主机名。\"\"\"\n    port: int = Field(default=8080, ge=1, le=65535)\n    \"\"\"NoneBot {ref}`nonebot.drivers.ReverseDriver` 服务端监听的端口。\"\"\"\n    log_level: int | str = LegacyUnionField(default=\"INFO\")\n    \"\"\"NoneBot 日志输出等级，可以为 `int` 类型等级或等级名称。\n\n    参考 [记录日志](https://nonebot.dev/docs/appendices/log)，[loguru 日志等级](https://loguru.readthedocs.io/en/stable/api/logger.html#levels)。\n\n    :::tip 提示\n    日志等级名称应为大写，如 `INFO`。\n    :::\n\n    用法:\n        ```conf\n        LOG_LEVEL=25\n        LOG_LEVEL=INFO\n        ```\n    \"\"\"\n\n    # bot connection configs\n    api_timeout: float | None = 30.0\n    \"\"\"API 请求超时时间，单位: 秒。\"\"\"\n\n    # bot runtime configs\n    superusers: set[str] = set()\n    \"\"\"机器人超级用户。\n\n    用法:\n        ```conf\n        SUPERUSERS=[\"12345789\"]\n        ```\n    \"\"\"\n    nickname: set[str] = set()\n    \"\"\"机器人昵称。\"\"\"\n    command_start: set[str] = {\"/\"}\n    \"\"\"命令的起始标记，用于判断一条消息是不是命令。\n\n    参考[命令响应规则](https://nonebot.dev/docs/advanced/matcher#command)。\n\n    用法:\n        ```conf\n        COMMAND_START=[\"/\", \"\"]\n        ```\n    \"\"\"\n    command_sep: set[str] = {\".\"}\n    \"\"\"命令的分隔标记，用于将文本形式的命令切分为元组（实际的命令名）。\n\n    参考[命令响应规则](https://nonebot.dev/docs/advanced/matcher#command)。\n\n    用法:\n        ```conf\n        COMMAND_SEP=[\".\"]\n        ```\n    \"\"\"\n    session_expire_timeout: timedelta = timedelta(minutes=2)\n    \"\"\"等待用户回复的超时时间。\n\n    用法:\n        ```conf\n        SESSION_EXPIRE_TIMEOUT=[-][DD]D[,][HH:MM:]SS[.ffffff]\n        SESSION_EXPIRE_TIMEOUT=[±]P[DD]DT[HH]H[MM]M[SS]S  # ISO 8601\n        ```\n    \"\"\"\n\n    # adapter configs\n    # adapter configs are defined in adapter/config.py\n\n    # custom configs\n    # custom configs can be assigned during nonebot.init\n    # or from env file using json loads\n\n    if PYDANTIC_V2:  # pragma: pydantic-v2\n        model_config = SettingsConfig(env_file=(\".env\", \".env.prod\"))\n    else:  # pragma: pydantic-v1\n\n        class Config(  # pyright: ignore[reportIncompatibleVariableOverride]\n            SettingsConfig\n        ):\n            env_file = \".env\", \".env.prod\"\n\n\n__autodoc__ = {\n    \"SettingsError\": False,\n    \"BaseSettingsSource\": False,\n    \"InitSettingsSource\": False,\n    \"DotEnvSettingsSource\": False,\n    \"SettingsConfig\": False,\n    \"BaseSettings\": False,\n}\n"
  },
  {
    "path": "nonebot/consts.py",
    "content": "\"\"\"本模块包含了 NoneBot 事件处理过程中使用到的常量。\n\nFrontMatter:\n    mdx:\n        format: md\n    sidebar_position: 9\n    description: nonebot.consts 模块\n\"\"\"\n\nimport os\nimport sys\nfrom typing import Literal\n\n# used by Matcher\nRECEIVE_KEY: Literal[\"_receive_{id}\"] = \"_receive_{id}\"\n\"\"\"`receive` 存储 key\"\"\"\nLAST_RECEIVE_KEY: Literal[\"_last_receive\"] = \"_last_receive\"\n\"\"\"`last_receive` 存储 key\"\"\"\nARG_KEY: Literal[\"{key}\"] = \"{key}\"\n\"\"\"`arg` 存储 key\"\"\"\nREJECT_TARGET: Literal[\"_current_target\"] = \"_current_target\"\n\"\"\"当前 `reject` 目标存储 key\"\"\"\nREJECT_CACHE_TARGET: Literal[\"_next_target\"] = \"_next_target\"\n\"\"\"下一个 `reject` 目标存储 key\"\"\"\nPAUSE_PROMPT_RESULT_KEY: Literal[\"_pause_result\"] = \"_pause_result\"\n\"\"\"`pause` prompt 发送结果存储 key\"\"\"\nREJECT_PROMPT_RESULT_KEY: Literal[\"_reject_{key}_result\"] = \"_reject_{key}_result\"\n\"\"\"`reject` prompt 发送结果存储 key\"\"\"\n\n# used by Rule\nPREFIX_KEY: Literal[\"_prefix\"] = \"_prefix\"\n\"\"\"命令前缀存储 key\"\"\"\n\nCMD_KEY: Literal[\"command\"] = \"command\"\n\"\"\"命令元组存储 key\"\"\"\nRAW_CMD_KEY: Literal[\"raw_command\"] = \"raw_command\"\n\"\"\"命令文本存储 key\"\"\"\nCMD_ARG_KEY: Literal[\"command_arg\"] = \"command_arg\"\n\"\"\"命令参数存储 key\"\"\"\nCMD_START_KEY: Literal[\"command_start\"] = \"command_start\"\n\"\"\"命令开头存储 key\"\"\"\nCMD_WHITESPACE_KEY: Literal[\"command_whitespace\"] = \"command_whitespace\"\n\"\"\"命令与参数间空白符存储 key\"\"\"\n\nSHELL_ARGS: Literal[\"_args\"] = \"_args\"\n\"\"\"shell 命令 parse 后参数字典存储 key\"\"\"\nSHELL_ARGV: Literal[\"_argv\"] = \"_argv\"\n\"\"\"shell 命令原始参数列表存储 key\"\"\"\n\nREGEX_MATCHED: Literal[\"_matched\"] = \"_matched\"\n\"\"\"正则匹配结果存储 key\"\"\"\nSTARTSWITH_KEY: Literal[\"_startswith\"] = \"_startswith\"\n\"\"\"响应触发前缀 key\"\"\"\nENDSWITH_KEY: Literal[\"_endswith\"] = \"_endswith\"\n\"\"\"响应触发后缀 key\"\"\"\nFULLMATCH_KEY: Literal[\"_fullmatch\"] = \"_fullmatch\"\n\"\"\"响应触发完整消息 key\"\"\"\nKEYWORD_KEY: Literal[\"_keyword\"] = \"_keyword\"\n\"\"\"响应触发关键字 key\"\"\"\n\nWINDOWS = sys.platform.startswith(\"win\") or (sys.platform == \"cli\" and os.name == \"nt\")\n"
  },
  {
    "path": "nonebot/dependencies/__init__.py",
    "content": "\"\"\"本模块模块实现了依赖注入的定义与处理。\n\nFrontMatter:\n    mdx:\n        format: md\n    sidebar_position: 0\n    description: nonebot.dependencies 模块\n\"\"\"\n\nimport abc\nfrom collections.abc import Awaitable, Callable, Iterable\nfrom dataclasses import dataclass, field\nfrom functools import partial\nimport inspect\nfrom typing import Any, Generic, TypeVar, cast\n\nimport anyio\nfrom exceptiongroup import BaseExceptionGroup, catch\n\nfrom nonebot.compat import FieldInfo, ModelField, PydanticUndefined\nfrom nonebot.exception import SkippedException\nfrom nonebot.log import logger\nfrom nonebot.typing import _DependentCallable\nfrom nonebot.utils import (\n    flatten_exception_group,\n    is_coroutine_callable,\n    run_coro_with_shield,\n    run_sync,\n)\n\nfrom .utils import check_field_type, get_typed_signature\n\nR = TypeVar(\"R\")\nT = TypeVar(\"T\", bound=\"Dependent\")\n\n\nclass Param(abc.ABC, FieldInfo):\n    \"\"\"依赖注入的基本单元 —— 参数。\n\n    继承自 `pydantic.fields.FieldInfo`，用于描述参数信息（不包括参数名）。\n    \"\"\"\n\n    def __init__(self, *args, validate: bool = False, **kwargs: Any) -> None:\n        super().__init__(*args, **kwargs)\n        self.validate = validate\n\n    @classmethod\n    def _check_param(\n        cls, param: inspect.Parameter, allow_types: tuple[type[\"Param\"], ...]\n    ) -> \"Param | None\":\n        return\n\n    @classmethod\n    def _check_parameterless(\n        cls, value: Any, allow_types: tuple[type[\"Param\"], ...]\n    ) -> \"Param | None\":\n        return\n\n    @abc.abstractmethod\n    async def _solve(self, **kwargs: Any) -> Any:\n        raise NotImplementedError\n\n    async def _check(self, **kwargs: Any) -> None:\n        return\n\n\n@dataclass(frozen=True)\nclass Dependent(Generic[R]):\n    \"\"\"依赖注入容器\n\n    参数:\n        call: 依赖注入的可调用对象，可以是任何 Callable 对象\n        pre_checkers: 依赖注入解析前的参数检查\n        params: 具名参数列表\n        parameterless: 匿名参数列表\n        allow_types: 允许的参数类型\n    \"\"\"\n\n    call: _DependentCallable[R]\n    params: tuple[ModelField, ...] = field(default_factory=tuple)\n    parameterless: tuple[Param, ...] = field(default_factory=tuple)\n\n    def __repr__(self) -> str:\n        if inspect.isfunction(self.call) or inspect.isclass(self.call):\n            call_str = self.call.__name__\n        else:\n            call_str = repr(self.call)\n        return (\n            f\"Dependent(call={call_str}\"\n            + (f\", parameterless={self.parameterless}\" if self.parameterless else \"\")\n            + \")\"\n        )\n\n    async def __call__(self, **kwargs: Any) -> R:\n        exception: BaseExceptionGroup[SkippedException] | None = None\n\n        def _handle_skipped(exc_group: BaseExceptionGroup[SkippedException]):\n            nonlocal exception\n            exception = exc_group\n            # raise one of the exceptions instead\n            excs = list(flatten_exception_group(exc_group))\n            logger.trace(f\"{self} skipped due to {excs}\")\n\n        with catch({SkippedException: _handle_skipped}):\n            # do pre-check\n            await self.check(**kwargs)\n\n            # solve param values\n            values = await self.solve(**kwargs)\n\n            # call function\n            if is_coroutine_callable(self.call):\n                return await cast(Callable[..., Awaitable[R]], self.call)(**values)\n            else:\n                return await run_sync(cast(Callable[..., R], self.call))(**values)\n\n        raise exception\n\n    @staticmethod\n    def parse_params(\n        call: _DependentCallable[R], allow_types: tuple[type[Param], ...]\n    ) -> tuple[ModelField, ...]:\n        fields: list[ModelField] = []\n        params = get_typed_signature(call).parameters.values()\n\n        for param in params:\n            if isinstance(param.default, Param):\n                field_info = param.default\n            else:\n                for allow_type in allow_types:\n                    if field_info := allow_type._check_param(param, allow_types):\n                        break\n                else:\n                    raise ValueError(\n                        f\"Unknown parameter {param.name} \"\n                        f\"for function {call} with type {param.annotation}\"\n                    )\n\n            annotation: Any = Any\n            if param.annotation is not param.empty:\n                annotation = param.annotation\n\n            fields.append(\n                ModelField.construct(\n                    name=param.name, annotation=annotation, field_info=field_info\n                )\n            )\n\n        return tuple(fields)\n\n    @staticmethod\n    def parse_parameterless(\n        parameterless: tuple[Any, ...], allow_types: tuple[type[Param], ...]\n    ) -> tuple[Param, ...]:\n        parameterless_params: list[Param] = []\n        for value in parameterless:\n            for allow_type in allow_types:\n                if param := allow_type._check_parameterless(value, allow_types):\n                    break\n            else:\n                raise ValueError(f\"Unknown parameterless {value}\")\n            parameterless_params.append(param)\n        return tuple(parameterless_params)\n\n    @classmethod\n    def parse(\n        cls,\n        *,\n        call: _DependentCallable[R],\n        parameterless: Iterable[Any] | None = None,\n        allow_types: Iterable[type[Param]],\n    ) -> \"Dependent[R]\":\n        allow_types = tuple(allow_types)\n\n        params = cls.parse_params(call, allow_types)\n        parameterless_params = (\n            ()\n            if parameterless is None\n            else cls.parse_parameterless(tuple(parameterless), allow_types)\n        )\n\n        return cls(call, params, parameterless_params)\n\n    async def check(self, **params: Any) -> None:\n        if self.parameterless:\n            async with anyio.create_task_group() as tg:\n                for param in self.parameterless:\n                    tg.start_soon(partial(param._check, **params))\n\n        if self.params:\n            async with anyio.create_task_group() as tg:\n                for param in self.params:\n                    tg.start_soon(\n                        partial(cast(Param, param.field_info)._check, **params)\n                    )\n\n    async def _solve_field(self, field: ModelField, params: dict[str, Any]) -> Any:\n        param = cast(Param, field.field_info)\n        value = await param._solve(**params)\n        if value is PydanticUndefined:\n            value = field.get_default()\n        v = check_field_type(field, value)\n        return v if param.validate else value\n\n    async def solve(self, **params: Any) -> dict[str, Any]:\n        # solve parameterless\n        for param in self.parameterless:\n            await param._solve(**params)\n\n        # solve param values\n        result: dict[str, Any] = {}\n        if not self.params:\n            return result\n\n        async def _solve_field(field: ModelField, params: dict[str, Any]) -> None:\n            value = await self._solve_field(field, params)\n            result[field.name] = value\n\n        async with anyio.create_task_group() as tg:\n            for field in self.params:\n                # shield the task to prevent cancellation\n                # when one of the tasks raises an exception\n                # this will improve the dependency cache reusability\n                tg.start_soon(run_coro_with_shield, _solve_field(field, params))\n\n        return result\n\n\n__autodoc__ = {\"CustomConfig\": False}\n"
  },
  {
    "path": "nonebot/dependencies/utils.py",
    "content": "\"\"\"\nFrontMatter:\n    mdx:\n        format: md\n    sidebar_position: 1\n    description: nonebot.dependencies.utils 模块\n\"\"\"\n\nfrom collections.abc import Callable\nimport inspect\nfrom typing import Any, ForwardRef, cast\nfrom typing_extensions import TypeAliasType\n\nfrom loguru import logger\n\nfrom nonebot.compat import ModelField\nfrom nonebot.exception import TypeMisMatch\nfrom nonebot.typing import evaluate_forwardref, is_type_alias_type\n\n\ndef get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:\n    \"\"\"获取可调用对象签名\"\"\"\n\n    signature = inspect.signature(call)\n    globalns = getattr(call, \"__globals__\", {})\n    typed_params = [\n        inspect.Parameter(\n            name=param.name,\n            kind=param.kind,\n            default=param.default,\n            annotation=get_typed_annotation(param, globalns),\n        )\n        for param in signature.parameters.values()\n    ]\n    return inspect.Signature(typed_params)\n\n\ndef get_typed_annotation(param: inspect.Parameter, globalns: dict[str, Any]) -> Any:\n    \"\"\"获取参数的类型注解\"\"\"\n\n    annotation = param.annotation\n    if isinstance(annotation, str):\n        annotation = ForwardRef(annotation)\n        try:\n            annotation = evaluate_forwardref(annotation, globalns, globalns)\n        except Exception as e:\n            logger.opt(colors=True, exception=e).warning(\n                f'Unknown ForwardRef[\"{param.annotation}\"] for parameter {param.name}'\n            )\n            return inspect.Parameter.empty\n    if is_type_alias_type(annotation):\n        # Python 3.12+ supports PEP 695 TypeAliasType\n        annotation = cast(TypeAliasType, annotation).__value__\n    return annotation\n\n\ndef check_field_type(field: ModelField, value: Any) -> Any:\n    \"\"\"检查字段类型是否匹配\"\"\"\n\n    try:\n        return field.validate_value(value)\n    except ValueError:\n        raise TypeMisMatch(field, value)\n"
  },
  {
    "path": "nonebot/drivers/__init__.py",
    "content": "\"\"\"本模块定义了驱动适配器基类。\n\n各驱动请继承以下基类。\n\nFrontMatter:\n    mdx:\n        format: md\n    sidebar_position: 0\n    description: nonebot.drivers 模块\n\"\"\"\n\nfrom nonebot.internal.driver import URL as URL\nfrom nonebot.internal.driver import ASGIMixin as ASGIMixin\nfrom nonebot.internal.driver import Cookies as Cookies\nfrom nonebot.internal.driver import Driver as Driver\nfrom nonebot.internal.driver import ForwardDriver as ForwardDriver\nfrom nonebot.internal.driver import ForwardMixin as ForwardMixin\nfrom nonebot.internal.driver import HTTPClientMixin as HTTPClientMixin\nfrom nonebot.internal.driver import HTTPClientSession as HTTPClientSession\nfrom nonebot.internal.driver import HTTPServerSetup as HTTPServerSetup\nfrom nonebot.internal.driver import HTTPVersion as HTTPVersion\nfrom nonebot.internal.driver import Mixin as Mixin\nfrom nonebot.internal.driver import Request as Request\nfrom nonebot.internal.driver import Response as Response\nfrom nonebot.internal.driver import ReverseDriver as ReverseDriver\nfrom nonebot.internal.driver import ReverseMixin as ReverseMixin\nfrom nonebot.internal.driver import Timeout as Timeout\nfrom nonebot.internal.driver import WebSocket as WebSocket\nfrom nonebot.internal.driver import WebSocketClientMixin as WebSocketClientMixin\nfrom nonebot.internal.driver import WebSocketServerSetup as WebSocketServerSetup\nfrom nonebot.internal.driver import combine_driver as combine_driver\n\n__autodoc__ = {\n    \"URL\": True,\n    \"Cookies\": True,\n    \"Request\": True,\n    \"Response\": True,\n    \"Timeout\": True,\n    \"WebSocket\": True,\n    \"HTTPVersion\": True,\n    \"Driver\": True,\n    \"Mixin\": True,\n    \"ForwardMixin\": True,\n    \"ForwardDriver\": True,\n    \"HTTPClientMixin\": True,\n    \"WebSocketClientMixin\": True,\n    \"ReverseMixin\": True,\n    \"ReverseDriver\": True,\n    \"ASGIMixin\": True,\n    \"combine_driver\": True,\n    \"HTTPServerSetup\": True,\n    \"WebSocketServerSetup\": True,\n}\n"
  },
  {
    "path": "nonebot/drivers/aiohttp.py",
    "content": "\"\"\"[AIOHTTP](https://aiohttp.readthedocs.io/en/stable/) 驱动适配器。\n\n```bash\nnb driver install aiohttp\n# 或者\npip install nonebot2[aiohttp]\n```\n\n:::tip 提示\n本驱动仅支持客户端连接\n:::\n\nFrontMatter:\n    mdx:\n        format: md\n    sidebar_position: 2\n    description: nonebot.drivers.aiohttp 模块\n\"\"\"\n\nfrom collections.abc import AsyncGenerator\nfrom contextlib import asynccontextmanager\nfrom typing import TYPE_CHECKING\nfrom typing_extensions import override\n\nfrom multidict import CIMultiDict\n\nfrom nonebot.drivers import (\n    URL,\n    HTTPClientMixin,\n    HTTPClientSession,\n    HTTPVersion,\n    Request,\n    Response,\n    WebSocketClientMixin,\n    combine_driver,\n)\nfrom nonebot.drivers import WebSocket as BaseWebSocket\nfrom nonebot.drivers.none import Driver as NoneDriver\nfrom nonebot.exception import WebSocketClosed\nfrom nonebot.internal.driver import (\n    Cookies,\n    CookieTypes,\n    HeaderTypes,\n    QueryTypes,\n    Timeout,\n    TimeoutTypes,\n)\n\ntry:\n    import aiohttp\nexcept ModuleNotFoundError as e:  # pragma: no cover\n    raise ImportError(\n        \"Please install aiohttp first to use this driver. \"\n        \"Install with pip: `pip install nonebot2[aiohttp]`\"\n    ) from e\n\n\nclass Session(HTTPClientSession):\n    @override\n    def __init__(\n        self,\n        params: QueryTypes = None,\n        headers: HeaderTypes = None,\n        cookies: CookieTypes = None,\n        version: str | HTTPVersion = HTTPVersion.H11,\n        timeout: TimeoutTypes = None,\n        proxy: str | None = None,\n    ):\n        self._client: aiohttp.ClientSession | None = None\n\n        self._params = URL.build(query=params).query if params is not None else None\n\n        self._headers = CIMultiDict(headers) if headers is not None else None\n        self._cookies = tuple(\n            (cookie.name, cookie.value)\n            for cookie in Cookies(cookies)\n            if cookie.value is not None\n        )\n\n        version = HTTPVersion(version)\n        if version == HTTPVersion.H10:\n            self._version = aiohttp.HttpVersion10\n        elif version == HTTPVersion.H11:\n            self._version = aiohttp.HttpVersion11\n        else:\n            raise RuntimeError(f\"Unsupported HTTP version: {version}\")\n\n        if isinstance(timeout, Timeout):\n            self._timeout = aiohttp.ClientTimeout(\n                total=timeout.total,\n                connect=timeout.connect,\n                sock_read=timeout.read,\n            )\n        else:\n            self._timeout = aiohttp.ClientTimeout(timeout)\n\n        self._proxy = proxy\n\n    @property\n    def client(self) -> aiohttp.ClientSession:\n        if self._client is None:\n            raise RuntimeError(\"Session is not initialized\")\n        return self._client\n\n    @override\n    async def request(self, setup: Request) -> Response:\n        if self._params:\n            url = setup.url.with_query({**self._params, **setup.url.query})\n        else:\n            url = setup.url\n\n        data = setup.data\n        if setup.files:\n            data = aiohttp.FormData(data or {}, quote_fields=False)\n            for name, file in setup.files:\n                data.add_field(name, file[1], content_type=file[2], filename=file[0])\n\n        cookies = (\n            (cookie.name, cookie.value)\n            for cookie in setup.cookies\n            if cookie.value is not None\n        )\n\n        if isinstance(setup.timeout, Timeout):\n            timeout = aiohttp.ClientTimeout(\n                total=setup.timeout.total,\n                connect=setup.timeout.connect,\n                sock_read=setup.timeout.read,\n            )\n        else:\n            timeout = aiohttp.ClientTimeout(setup.timeout)\n\n        async with await self.client.request(\n            setup.method,\n            url,\n            data=setup.content or data,\n            json=setup.json,\n            cookies=cookies,\n            headers=setup.headers,\n            proxy=setup.proxy or self._proxy,\n            timeout=timeout,\n        ) as response:\n            return Response(\n                response.status,\n                headers=response.headers.copy(),\n                content=await response.read(),\n                request=setup,\n            )\n\n    @override\n    async def stream_request(\n        self,\n        setup: Request,\n        *,\n        chunk_size: int = 1024,\n    ) -> AsyncGenerator[Response, None]:\n        if self._params:\n            url = setup.url.with_query({**self._params, **setup.url.query})\n        else:\n            url = setup.url\n\n        data = setup.data\n        if setup.files:\n            data = aiohttp.FormData(data or {}, quote_fields=False)\n            for name, file in setup.files:\n                data.add_field(name, file[1], content_type=file[2], filename=file[0])\n\n        cookies = (\n            (cookie.name, cookie.value)\n            for cookie in setup.cookies\n            if cookie.value is not None\n        )\n\n        if isinstance(setup.timeout, Timeout):\n            timeout = aiohttp.ClientTimeout(\n                total=setup.timeout.total,\n                connect=setup.timeout.connect,\n                sock_read=setup.timeout.read,\n            )\n        else:\n            timeout = aiohttp.ClientTimeout(setup.timeout)\n\n        async with self.client.request(\n            setup.method,\n            url,\n            data=setup.content or data,\n            json=setup.json,\n            cookies=cookies,\n            headers=setup.headers,\n            proxy=setup.proxy or self._proxy,\n            timeout=timeout,\n        ) as response:\n            response_headers = response.headers.copy()\n            async for chunk in response.content.iter_chunked(chunk_size):\n                yield Response(\n                    response.status,\n                    headers=response_headers,\n                    content=chunk,\n                    request=setup,\n                )\n\n    @override\n    async def setup(self) -> None:\n        if self._client is not None:\n            raise RuntimeError(\"Session has already been initialized\")\n        self._client = aiohttp.ClientSession(\n            cookies=self._cookies,\n            headers=self._headers,\n            version=self._version,\n            timeout=self._timeout,\n            trust_env=True,\n        )\n        await self._client.__aenter__()\n\n    @override\n    async def close(self) -> None:\n        try:\n            if self._client is not None:\n                await self._client.close()\n        finally:\n            self._client = None\n\n\nclass Mixin(HTTPClientMixin, WebSocketClientMixin):\n    \"\"\"AIOHTTP Mixin\"\"\"\n\n    @property\n    @override\n    def type(self) -> str:\n        return \"aiohttp\"\n\n    @override\n    async def request(self, setup: Request) -> Response:\n        async with self.get_session() as session:\n            return await session.request(setup)\n\n    @override\n    async def stream_request(\n        self,\n        setup: Request,\n        *,\n        chunk_size: int = 1024,\n    ) -> AsyncGenerator[Response, None]:\n        async with self.get_session() as session:\n            async for response in session.stream_request(setup, chunk_size=chunk_size):\n                yield response\n\n    @override\n    @asynccontextmanager\n    async def websocket(self, setup: Request) -> AsyncGenerator[\"WebSocket\", None]:\n        if setup.version == HTTPVersion.H10:\n            version = aiohttp.HttpVersion10\n        elif setup.version == HTTPVersion.H11:\n            version = aiohttp.HttpVersion11\n        else:\n            raise RuntimeError(f\"Unsupported HTTP version: {setup.version}\")\n\n        if isinstance(setup.timeout, Timeout):\n            timeout = aiohttp.ClientWSTimeout(\n                ws_receive=setup.timeout.read,  # type: ignore\n                ws_close=setup.timeout.total,  # type: ignore\n            )\n        else:\n            timeout = aiohttp.ClientWSTimeout(ws_close=setup.timeout or 10.0)  # type: ignore\n\n        async with aiohttp.ClientSession(version=version, trust_env=True) as session:\n            async with session.ws_connect(\n                setup.url,\n                method=setup.method,\n                timeout=timeout,\n                headers=setup.headers,\n                proxy=setup.proxy,\n            ) as ws:\n                yield WebSocket(request=setup, session=session, websocket=ws)\n\n    @override\n    def get_session(\n        self,\n        params: QueryTypes = None,\n        headers: HeaderTypes = None,\n        cookies: CookieTypes = None,\n        version: str | HTTPVersion = HTTPVersion.H11,\n        timeout: TimeoutTypes = None,\n        proxy: str | None = None,\n    ) -> Session:\n        return Session(\n            params=params,\n            headers=headers,\n            cookies=cookies,\n            version=version,\n            timeout=timeout,\n            proxy=proxy,\n        )\n\n\nclass WebSocket(BaseWebSocket):\n    \"\"\"AIOHTTP Websocket Wrapper\"\"\"\n\n    def __init__(\n        self,\n        *,\n        request: Request,\n        session: aiohttp.ClientSession,\n        websocket: aiohttp.ClientWebSocketResponse,\n    ):\n        super().__init__(request=request)\n        self.session = session\n        self.websocket = websocket\n\n    @property\n    @override\n    def closed(self):\n        return self.websocket.closed\n\n    @override\n    async def accept(self):\n        raise NotImplementedError\n\n    @override\n    async def close(self, code: int = 1000, reason: str = \"\"):\n        await self.websocket.close(code=code, message=reason.encode(\"utf-8\"))\n        await self.session.close()\n\n    async def _receive(self) -> aiohttp.WSMessage:\n        msg = await self.websocket.receive()\n        if msg.type in (\n            aiohttp.WSMsgType.CLOSE,\n            aiohttp.WSMsgType.CLOSING,\n            aiohttp.WSMsgType.CLOSED,\n        ):\n            raise WebSocketClosed(self.websocket.close_code or 1006)\n        return msg\n\n    @override\n    async def receive(self) -> str:\n        msg = await self._receive()\n        if msg.type not in (aiohttp.WSMsgType.TEXT, aiohttp.WSMsgType.BINARY):\n            raise TypeError(\n                f\"WebSocket received unexpected frame type: {msg.type}, {msg.data!r}\"\n            )\n        return msg.data\n\n    @override\n    async def receive_text(self) -> str:\n        msg = await self._receive()\n        if msg.type != aiohttp.WSMsgType.TEXT:\n            raise TypeError(\n                f\"WebSocket received unexpected frame type: {msg.type}, {msg.data!r}\"\n            )\n        return msg.data\n\n    @override\n    async def receive_bytes(self) -> bytes:\n        msg = await self._receive()\n        if msg.type != aiohttp.WSMsgType.BINARY:\n            raise TypeError(\n                f\"WebSocket received unexpected frame type: {msg.type}, {msg.data!r}\"\n            )\n        return msg.data\n\n    @override\n    async def send_text(self, data: str) -> None:\n        await self.websocket.send_str(data)\n\n    @override\n    async def send_bytes(self, data: bytes) -> None:\n        await self.websocket.send_bytes(data)\n\n\nif TYPE_CHECKING:\n\n    class Driver(Mixin, NoneDriver): ...\n\nelse:\n    Driver = combine_driver(NoneDriver, Mixin)\n    \"\"\"AIOHTTP Driver\"\"\"\n"
  },
  {
    "path": "nonebot/drivers/fastapi.py",
    "content": "\"\"\"[FastAPI](https://fastapi.tiangolo.com/) 驱动适配\n\n```bash\nnb driver install fastapi\n# 或者\npip install nonebot2[fastapi]\n```\n\n:::tip 提示\n本驱动仅支持服务端连接\n:::\n\nFrontMatter:\n    mdx:\n        format: md\n    sidebar_position: 1\n    description: nonebot.drivers.fastapi 模块\n\"\"\"\n\nimport contextlib\nfrom functools import wraps\nimport logging\nfrom typing import Any\nfrom typing_extensions import override\n\nfrom pydantic import BaseModel\n\nfrom nonebot.compat import model_dump, type_validate_python\nfrom nonebot.config import Config as NoneBotConfig\nfrom nonebot.config import Env\nfrom nonebot.drivers import ASGIMixin, HTTPServerSetup, WebSocketServerSetup\nfrom nonebot.drivers import Driver as BaseDriver\nfrom nonebot.drivers import Request as BaseRequest\nfrom nonebot.drivers import WebSocket as BaseWebSocket\nfrom nonebot.exception import WebSocketClosed\nfrom nonebot.internal.driver import FileTypes\n\ntry:\n    from fastapi import FastAPI, Request, UploadFile, status\n    from fastapi.responses import Response\n    from starlette.websockets import WebSocket, WebSocketDisconnect, WebSocketState\n    import uvicorn\nexcept ModuleNotFoundError as e:  # pragma: no cover\n    raise ImportError(\n        \"Please install FastAPI first to use this driver. \"\n        \"Install with pip: `pip install nonebot2[fastapi]`\"\n    ) from e\n\n\ndef catch_closed(func):\n    @wraps(func)\n    async def decorator(*args, **kwargs):\n        try:\n            return await func(*args, **kwargs)\n        except WebSocketDisconnect as e:\n            raise WebSocketClosed(e.code)\n        except KeyError:\n            raise TypeError(\"WebSocket received unexpected frame type\")\n\n    return decorator\n\n\nclass Config(BaseModel):\n    \"\"\"FastAPI 驱动框架设置，详情参考 FastAPI 文档\"\"\"\n\n    fastapi_openapi_url: str | None = None\n    \"\"\"`openapi.json` 地址，默认为 `None` 即关闭\"\"\"\n    fastapi_docs_url: str | None = None\n    \"\"\"`swagger` 地址，默认为 `None` 即关闭\"\"\"\n    fastapi_redoc_url: str | None = None\n    \"\"\"`redoc` 地址，默认为 `None` 即关闭\"\"\"\n    fastapi_include_adapter_schema: bool = True\n    \"\"\"是否包含适配器路由的 schema，默认为 `True`\"\"\"\n    fastapi_reload: bool = False\n    \"\"\"开启/关闭冷重载\"\"\"\n    fastapi_reload_dirs: list[str] | None = None\n    \"\"\"重载监控文件夹列表，默认为 uvicorn 默认值\"\"\"\n    fastapi_reload_delay: float = 0.25\n    \"\"\"重载延迟，默认为 uvicorn 默认值\"\"\"\n    fastapi_reload_includes: list[str] | None = None\n    \"\"\"要监听的文件列表，支持 glob pattern，默认为 uvicorn 默认值\"\"\"\n    fastapi_reload_excludes: list[str] | None = None\n    \"\"\"不要监听的文件列表，支持 glob pattern，默认为 uvicorn 默认值\"\"\"\n    fastapi_extra: dict[str, Any] = {}\n    \"\"\"传递给 `FastAPI` 的其他参数。\"\"\"\n\n\nclass Driver(BaseDriver, ASGIMixin):\n    \"\"\"FastAPI 驱动框架。\"\"\"\n\n    def __init__(self, env: Env, config: NoneBotConfig):\n        super().__init__(env, config)\n\n        self.fastapi_config: Config = type_validate_python(Config, model_dump(config))\n\n        self._server_app = FastAPI(\n            lifespan=self._lifespan_manager,\n            openapi_url=self.fastapi_config.fastapi_openapi_url,\n            docs_url=self.fastapi_config.fastapi_docs_url,\n            redoc_url=self.fastapi_config.fastapi_redoc_url,\n            **self.fastapi_config.fastapi_extra,\n        )\n\n    @property\n    @override\n    def type(self) -> str:\n        \"\"\"驱动名称: `fastapi`\"\"\"\n        return \"fastapi\"\n\n    @property\n    @override\n    def server_app(self) -> FastAPI:\n        \"\"\"`FastAPI APP` 对象\"\"\"\n        return self._server_app\n\n    @property\n    @override\n    def asgi(self) -> FastAPI:\n        \"\"\"`FastAPI APP` 对象\"\"\"\n        return self._server_app\n\n    @property\n    @override\n    def logger(self) -> logging.Logger:\n        \"\"\"fastapi 使用的 logger\"\"\"\n        return logging.getLogger(\"fastapi\")\n\n    @override\n    def setup_http_server(self, setup: HTTPServerSetup):\n        async def _handle(request: Request) -> Response:\n            return await self._handle_http(request, setup)\n\n        self._server_app.add_api_route(\n            setup.path.path,\n            _handle,\n            name=setup.name,\n            methods=[setup.method],\n            include_in_schema=self.fastapi_config.fastapi_include_adapter_schema,\n        )\n\n    @override\n    def setup_websocket_server(self, setup: WebSocketServerSetup) -> None:\n        async def _handle(websocket: WebSocket) -> None:\n            await self._handle_ws(websocket, setup)\n\n        self._server_app.add_api_websocket_route(\n            setup.path.path,\n            _handle,\n            name=setup.name,\n        )\n\n    @contextlib.asynccontextmanager\n    async def _lifespan_manager(self, app: FastAPI):\n        await self._lifespan.startup()\n        try:\n            yield\n        finally:\n            await self._lifespan.shutdown()\n\n    @override\n    def run(\n        self,\n        host: str | None = None,\n        port: int | None = None,\n        *args,\n        app: str | None = None,\n        **kwargs,\n    ):\n        \"\"\"使用 `uvicorn` 启动 FastAPI\"\"\"\n        super().run(host, port, app=app, **kwargs)\n        LOGGING_CONFIG = {\n            \"version\": 1,\n            \"disable_existing_loggers\": False,\n            \"handlers\": {\n                \"default\": {\n                    \"class\": \"nonebot.log.LoguruHandler\",\n                },\n            },\n            \"loggers\": {\n                \"uvicorn.error\": {\"handlers\": [\"default\"], \"level\": \"INFO\"},\n                \"uvicorn.access\": {\n                    \"handlers\": [\"default\"],\n                    \"level\": \"INFO\",\n                },\n            },\n        }\n        uvicorn.run(\n            app or self.server_app,  # type: ignore\n            host=host or str(self.config.host),\n            port=port or self.config.port,\n            reload=self.fastapi_config.fastapi_reload,\n            reload_dirs=self.fastapi_config.fastapi_reload_dirs,\n            reload_delay=self.fastapi_config.fastapi_reload_delay,\n            reload_includes=self.fastapi_config.fastapi_reload_includes,\n            reload_excludes=self.fastapi_config.fastapi_reload_excludes,\n            log_config=LOGGING_CONFIG,\n            **kwargs,\n        )\n\n    async def _handle_http(\n        self,\n        request: Request,\n        setup: HTTPServerSetup,\n    ) -> Response:\n        json: Any = None\n        with contextlib.suppress(Exception):\n            json = await request.json()\n\n        data: dict | None = None\n        files: list[tuple[str, FileTypes]] | None = None\n        with contextlib.suppress(Exception):\n            form = await request.form()\n            data = {}\n            files = []\n            for key, value in form.multi_items():\n                if isinstance(value, UploadFile):\n                    files.append(\n                        (key, (value.filename, value.file, value.content_type))\n                    )\n                else:\n                    data[key] = value\n\n        http_request = BaseRequest(\n            request.method,\n            str(request.url),\n            headers=request.headers.items(),\n            cookies=request.cookies,\n            content=await request.body(),\n            data=data,\n            json=json,\n            files=files,\n            version=request.scope[\"http_version\"],\n        )\n\n        response = await setup.handle_func(http_request)\n        return Response(\n            response.content, response.status_code, dict(response.headers.items())\n        )\n\n    async def _handle_ws(self, websocket: WebSocket, setup: WebSocketServerSetup):\n        request = BaseRequest(\n            \"GET\",\n            str(websocket.url),\n            headers=websocket.headers.items(),\n            cookies=websocket.cookies,\n            version=websocket.scope.get(\"http_version\", \"1.1\"),\n        )\n        ws = FastAPIWebSocket(\n            request=request,\n            websocket=websocket,\n        )\n\n        await setup.handle_func(ws)\n\n\nclass FastAPIWebSocket(BaseWebSocket):\n    \"\"\"FastAPI WebSocket Wrapper\"\"\"\n\n    @override\n    def __init__(self, *, request: BaseRequest, websocket: WebSocket):\n        super().__init__(request=request)\n        self.websocket = websocket\n\n    @property\n    @override\n    def closed(self) -> bool:\n        return (\n            self.websocket.client_state == WebSocketState.DISCONNECTED\n            or self.websocket.application_state == WebSocketState.DISCONNECTED\n        )\n\n    @override\n    async def accept(self) -> None:\n        await self.websocket.accept()\n\n    @override\n    async def close(\n        self, code: int = status.WS_1000_NORMAL_CLOSURE, reason: str = \"\"\n    ) -> None:\n        await self.websocket.close(code, reason)\n\n    @override\n    async def receive(self) -> str | bytes:\n        # assert self.websocket.application_state == WebSocketState.CONNECTED\n        msg = await self.websocket.receive()\n        if msg[\"type\"] == \"websocket.disconnect\":\n            raise WebSocketClosed(msg[\"code\"])\n        return msg[\"text\"] if \"text\" in msg else msg[\"bytes\"]\n\n    @override\n    @catch_closed\n    async def receive_text(self) -> str:\n        return await self.websocket.receive_text()\n\n    @override\n    @catch_closed\n    async def receive_bytes(self) -> bytes:\n        return await self.websocket.receive_bytes()\n\n    @override\n    async def send_text(self, data: str) -> None:\n        await self.websocket.send({\"type\": \"websocket.send\", \"text\": data})\n\n    @override\n    async def send_bytes(self, data: bytes) -> None:\n        await self.websocket.send({\"type\": \"websocket.send\", \"bytes\": data})\n\n\n__autodoc__ = {\"catch_closed\": False}\n"
  },
  {
    "path": "nonebot/drivers/httpx.py",
    "content": "\"\"\"[HTTPX](https://www.python-httpx.org/) 驱动适配\n\n```bash\nnb driver install httpx\n# 或者\npip install nonebot2[httpx]\n```\n\n:::tip 提示\n本驱动仅支持客户端 HTTP 连接\n:::\n\nFrontMatter:\n    mdx:\n        format: md\n    sidebar_position: 3\n    description: nonebot.drivers.httpx 模块\n\"\"\"\n\nfrom collections.abc import AsyncGenerator\nfrom typing import TYPE_CHECKING\nfrom typing_extensions import override\n\nfrom multidict import CIMultiDict\n\nfrom nonebot.drivers import (\n    URL,\n    HTTPClientMixin,\n    HTTPClientSession,\n    HTTPVersion,\n    Request,\n    Response,\n    combine_driver,\n)\nfrom nonebot.drivers.none import Driver as NoneDriver\nfrom nonebot.internal.driver import (\n    Cookies,\n    CookieTypes,\n    HeaderTypes,\n    QueryTypes,\n    Timeout,\n    TimeoutTypes,\n)\n\ntry:\n    import httpx\nexcept ModuleNotFoundError as e:  # pragma: no cover\n    raise ImportError(\n        \"Please install httpx first to use this driver. \"\n        \"Install with pip: `pip install nonebot2[httpx]`\"\n    ) from e\n\n\nclass Session(HTTPClientSession):\n    @override\n    def __init__(\n        self,\n        params: QueryTypes = None,\n        headers: HeaderTypes = None,\n        cookies: CookieTypes = None,\n        version: str | HTTPVersion = HTTPVersion.H11,\n        timeout: TimeoutTypes = None,\n        proxy: str | None = None,\n    ):\n        self._client: httpx.AsyncClient | None = None\n\n        self._params = (\n            tuple(URL.build(query=params).query.items()) if params is not None else None\n        )\n        self._headers = (\n            tuple(CIMultiDict(headers).items()) if headers is not None else None\n        )\n        self._cookies = Cookies(cookies)\n        self._version = HTTPVersion(version)\n\n        if isinstance(timeout, Timeout):\n            self._timeout = httpx.Timeout(\n                timeout=timeout.total,\n                connect=timeout.connect,\n                read=timeout.read,\n            )\n        else:\n            self._timeout = httpx.Timeout(timeout)\n\n        self._proxy = proxy\n\n    @property\n    def client(self) -> httpx.AsyncClient:\n        if self._client is None:\n            raise RuntimeError(\"Session is not initialized\")\n        return self._client\n\n    @override\n    async def request(self, setup: Request) -> Response:\n        if isinstance(setup.timeout, Timeout):\n            timeout = httpx.Timeout(\n                timeout=setup.timeout.total,\n                connect=setup.timeout.connect,\n                read=setup.timeout.read,\n            )\n        else:\n            timeout = httpx.Timeout(setup.timeout)\n\n        response = await self.client.request(\n            setup.method,\n            str(setup.url),\n            content=setup.content,\n            data=setup.data,\n            files=setup.files,\n            json=setup.json,\n            # ensure the params priority\n            params=setup.url.raw_query_string,\n            headers=tuple(setup.headers.items()),\n            cookies=setup.cookies.jar,\n            timeout=timeout,\n        )\n        return Response(\n            response.status_code,\n            headers=response.headers.multi_items(),\n            content=response.content,\n            request=setup,\n        )\n\n    @override\n    async def stream_request(\n        self,\n        setup: Request,\n        *,\n        chunk_size: int = 1024,\n    ) -> AsyncGenerator[Response, None]:\n        if isinstance(setup.timeout, Timeout):\n            timeout = httpx.Timeout(\n                timeout=setup.timeout.total,\n                connect=setup.timeout.connect,\n                read=setup.timeout.read,\n            )\n        else:\n            timeout = httpx.Timeout(setup.timeout)\n\n        async with self.client.stream(\n            setup.method,\n            str(setup.url),\n            content=setup.content,\n            data=setup.data,\n            files=setup.files,\n            json=setup.json,\n            # ensure the params priority\n            params=setup.url.raw_query_string,\n            headers=tuple(setup.headers.items()),\n            cookies=setup.cookies.jar,\n            timeout=timeout,\n        ) as response:\n            response_headers = response.headers.multi_items()\n            async for chunk in response.aiter_bytes(chunk_size=chunk_size):\n                yield Response(\n                    response.status_code,\n                    headers=response_headers,\n                    content=chunk,\n                    request=setup,\n                )\n\n    @override\n    async def setup(self) -> None:\n        if self._client is not None:\n            raise RuntimeError(\"Session has already been initialized\")\n        self._client = httpx.AsyncClient(\n            params=self._params,\n            headers=self._headers,\n            cookies=self._cookies.jar,\n            http2=self._version == HTTPVersion.H2,\n            proxy=self._proxy,\n            follow_redirects=True,\n        )\n        await self._client.__aenter__()\n\n    @override\n    async def close(self) -> None:\n        try:\n            if self._client is not None:\n                await self._client.aclose()\n        finally:\n            self._client = None\n\n\nclass Mixin(HTTPClientMixin):\n    \"\"\"HTTPX Mixin\"\"\"\n\n    @property\n    @override\n    def type(self) -> str:\n        return \"httpx\"\n\n    @override\n    async def request(self, setup: Request) -> Response:\n        async with self.get_session(\n            version=setup.version, proxy=setup.proxy\n        ) as session:\n            return await session.request(setup)\n\n    @override\n    async def stream_request(\n        self,\n        setup: Request,\n        *,\n        chunk_size: int = 1024,\n    ) -> AsyncGenerator[Response, None]:\n        async with self.get_session(\n            version=setup.version, proxy=setup.proxy\n        ) as session:\n            async for response in session.stream_request(setup, chunk_size=chunk_size):\n                yield response\n\n    @override\n    def get_session(\n        self,\n        params: QueryTypes = None,\n        headers: HeaderTypes = None,\n        cookies: CookieTypes = None,\n        version: str | HTTPVersion = HTTPVersion.H11,\n        timeout: TimeoutTypes = None,\n        proxy: str | None = None,\n    ) -> Session:\n        return Session(\n            params=params,\n            headers=headers,\n            cookies=cookies,\n            version=version,\n            timeout=timeout,\n            proxy=proxy,\n        )\n\n\nif TYPE_CHECKING:\n\n    class Driver(Mixin, NoneDriver): ...\n\nelse:\n    Driver = combine_driver(NoneDriver, Mixin)\n    \"\"\"HTTPX Driver\"\"\"\n"
  },
  {
    "path": "nonebot/drivers/none.py",
    "content": "\"\"\"None 驱动适配\n\n:::tip 提示\n本驱动不支持任何服务器或客户端连接\n:::\n\nFrontMatter:\n    mdx:\n        format: md\n    sidebar_position: 6\n    description: nonebot.drivers.none 模块\n\"\"\"\n\nimport signal\nfrom typing_extensions import override\n\nimport anyio\nfrom anyio.abc import TaskGroup\nfrom exceptiongroup import BaseExceptionGroup, catch\n\nfrom nonebot.config import Config, Env\nfrom nonebot.consts import WINDOWS\nfrom nonebot.drivers import Driver as BaseDriver\nfrom nonebot.log import logger\nfrom nonebot.utils import flatten_exception_group\n\nHANDLED_SIGNALS = (\n    signal.SIGINT,  # Unix signal 2. Sent by Ctrl+C.\n    signal.SIGTERM,  # Unix signal 15. Sent by `kill <pid>`.\n)\nif WINDOWS:  # pragma: py-win32\n    HANDLED_SIGNALS += (signal.SIGBREAK,)  # Windows signal 21. Sent by Ctrl+Break.\n\n\nclass Driver(BaseDriver):\n    \"\"\"None 驱动框架\"\"\"\n\n    def __init__(self, env: Env, config: Config):\n        super().__init__(env, config)\n\n        self.should_exit: anyio.Event = anyio.Event()\n        self.force_exit: anyio.Event = anyio.Event()\n\n    @property\n    @override\n    def type(self) -> str:\n        \"\"\"驱动名称: `none`\"\"\"\n        return \"none\"\n\n    @property\n    @override\n    def logger(self):\n        \"\"\"none driver 使用的 logger\"\"\"\n        return logger\n\n    @override\n    def run(self, *args, **kwargs):\n        \"\"\"启动 none driver\"\"\"\n        super().run(*args, **kwargs)\n        anyio.run(self._serve)\n\n    async def _serve(self):\n        async with anyio.create_task_group() as driver_tg:\n            driver_tg.start_soon(self._handle_signals)\n            driver_tg.start_soon(self._listen_force_exit, driver_tg)\n            driver_tg.start_soon(self._handle_lifespan, driver_tg)\n\n    async def _handle_signals(self):\n        try:\n            with anyio.open_signal_receiver(*HANDLED_SIGNALS) as signal_receiver:\n                async for sig in signal_receiver:\n                    self.exit(force=self.should_exit.is_set())\n        except NotImplementedError:\n            # Windows\n            for sig in HANDLED_SIGNALS:\n                signal.signal(sig, self._handle_legacy_signal)\n\n    # backport for Windows signal handling\n    def _handle_legacy_signal(self, sig, frame):\n        self.exit(force=self.should_exit.is_set())\n\n    async def _handle_lifespan(self, tg: TaskGroup):\n        try:\n            await self._startup()\n\n            if self.should_exit.is_set():\n                return\n\n            await self._listen_exit()\n\n            await self._shutdown()\n        finally:\n            tg.cancel_scope.cancel()\n\n    async def _startup(self):\n        def handle_exception(exc_group: BaseExceptionGroup[Exception]) -> None:\n            self.should_exit.set()\n\n            for exc in flatten_exception_group(exc_group):\n                logger.opt(colors=True, exception=exc).error(\n                    \"<r><bg #f8bbd0>Error occurred while running startup hook.\"\n                    \"</bg #f8bbd0></r>\"\n                )\n            logger.error(\n                \"<r><bg #f8bbd0>Application startup failed. Exiting.</bg #f8bbd0></r>\"\n            )\n\n        with catch({Exception: handle_exception}):\n            await self._lifespan.startup()\n\n        if not self.should_exit.is_set():\n            logger.info(\"Application startup completed.\")\n\n    async def _listen_exit(self, tg: TaskGroup | None = None):\n        await self.should_exit.wait()\n\n        if tg is not None:\n            tg.cancel_scope.cancel()\n\n    async def _shutdown(self):\n        logger.info(\"Shutting down\")\n        logger.info(\"Waiting for application shutdown. (CTRL+C to force quit)\")\n\n        error_occurred: bool = False\n\n        def handle_exception(exc_group: BaseExceptionGroup[Exception]) -> None:\n            nonlocal error_occurred\n\n            error_occurred = True\n\n            for exc in flatten_exception_group(exc_group):\n                logger.opt(colors=True, exception=exc).error(\n                    \"<r><bg #f8bbd0>Error occurred while running shutdown hook.\"\n                    \"</bg #f8bbd0></r>\"\n                )\n            logger.error(\n                \"<r><bg #f8bbd0>Application shutdown failed. Exiting.</bg #f8bbd0></r>\"\n            )\n\n        with catch({Exception: handle_exception}):\n            await self._lifespan.shutdown()\n\n        if not error_occurred:\n            logger.info(\"Application shutdown complete.\")\n\n    async def _listen_force_exit(self, tg: TaskGroup):\n        await self.force_exit.wait()\n        tg.cancel_scope.cancel()\n\n    def exit(self, force: bool = False):\n        \"\"\"退出 none driver\n\n        参数:\n            force: 强制退出\n        \"\"\"\n        if not self.should_exit.is_set():\n            self.should_exit.set()\n        if force:\n            self.force_exit.set()\n"
  },
  {
    "path": "nonebot/drivers/quart.py",
    "content": "\"\"\"[Quart](https://pgjones.gitlab.io/quart/index.html) 驱动适配\n\n```bash\nnb driver install quart\n# 或者\npip install nonebot2[quart]\n```\n\n:::tip 提示\n本驱动仅支持服务端连接\n:::\n\nFrontMatter:\n    mdx:\n        format: md\n    sidebar_position: 5\n    description: nonebot.drivers.quart 模块\n\"\"\"\n\nimport asyncio\nfrom functools import wraps\nfrom typing import Any, cast\nfrom typing_extensions import override\n\nfrom pydantic import BaseModel\n\nfrom nonebot.compat import model_dump, type_validate_python\nfrom nonebot.config import Config as NoneBotConfig\nfrom nonebot.config import Env\nfrom nonebot.drivers import ASGIMixin, HTTPServerSetup, WebSocketServerSetup\nfrom nonebot.drivers import Driver as BaseDriver\nfrom nonebot.drivers import Request as BaseRequest\nfrom nonebot.drivers import WebSocket as BaseWebSocket\nfrom nonebot.exception import WebSocketClosed\nfrom nonebot.internal.driver import FileTypes\n\ntry:\n    from quart import Quart, Request, Response\n    from quart import Websocket as QuartWebSocket\n    from quart import request as _request\n    from quart.ctx import WebsocketContext\n    from quart.datastructures import FileStorage\n    from quart.globals import websocket_ctx\n    import uvicorn\nexcept ModuleNotFoundError as e:  # pragma: no cover\n    raise ImportError(\n        \"Please install Quart first to use this driver. \"\n        \"Install with pip: `pip install nonebot2[quart]`\"\n    ) from e\n\n\ndef catch_closed(func):\n    @wraps(func)\n    async def decorator(*args, **kwargs):\n        try:\n            return await func(*args, **kwargs)\n        except asyncio.CancelledError:\n            raise WebSocketClosed(1000)\n\n    return decorator\n\n\nclass Config(BaseModel):\n    \"\"\"Quart 驱动框架设置\"\"\"\n\n    quart_reload: bool = False\n    \"\"\"开启/关闭冷重载\"\"\"\n    quart_reload_dirs: list[str] | None = None\n    \"\"\"重载监控文件夹列表，默认为 uvicorn 默认值\"\"\"\n    quart_reload_delay: float = 0.25\n    \"\"\"重载延迟，默认为 uvicorn 默认值\"\"\"\n    quart_reload_includes: list[str] | None = None\n    \"\"\"要监听的文件列表，支持 glob pattern，默认为 uvicorn 默认值\"\"\"\n    quart_reload_excludes: list[str] | None = None\n    \"\"\"不要监听的文件列表，支持 glob pattern，默认为 uvicorn 默认值\"\"\"\n    quart_extra: dict[str, Any] = {}\n    \"\"\"传递给 `Quart` 的其他参数。\"\"\"\n\n\nclass Driver(BaseDriver, ASGIMixin):\n    \"\"\"Quart 驱动框架\"\"\"\n\n    def __init__(self, env: Env, config: NoneBotConfig):\n        super().__init__(env, config)\n\n        self.quart_config = type_validate_python(Config, model_dump(config))\n\n        self._server_app = Quart(\n            self.__class__.__qualname__, **self.quart_config.quart_extra\n        )\n        self._server_app.before_serving(self._lifespan.startup)\n        self._server_app.after_serving(self._lifespan.shutdown)\n\n    @property\n    @override\n    def type(self) -> str:\n        \"\"\"驱动名称: `quart`\"\"\"\n        return \"quart\"\n\n    @property\n    @override\n    def server_app(self) -> Quart:\n        \"\"\"`Quart` 对象\"\"\"\n        return self._server_app\n\n    @property\n    @override\n    def asgi(self):\n        \"\"\"`Quart` 对象\"\"\"\n        return self._server_app\n\n    @property\n    @override\n    def logger(self):\n        \"\"\"Quart 使用的 logger\"\"\"\n        return self._server_app.logger\n\n    @override\n    def setup_http_server(self, setup: HTTPServerSetup):\n        async def _handle() -> Response:\n            return await self._handle_http(setup)\n\n        self._server_app.add_url_rule(\n            setup.path.path,\n            endpoint=setup.name,\n            methods=[setup.method],\n            view_func=_handle,\n        )\n\n    @override\n    def setup_websocket_server(self, setup: WebSocketServerSetup) -> None:\n        async def _handle() -> None:\n            return await self._handle_ws(setup)\n\n        self._server_app.add_websocket(\n            setup.path.path,\n            endpoint=setup.name,\n            view_func=_handle,\n        )\n\n    @override\n    def run(\n        self,\n        host: str | None = None,\n        port: int | None = None,\n        *args,\n        app: str | None = None,\n        **kwargs,\n    ):\n        \"\"\"使用 `uvicorn` 启动 Quart\"\"\"\n        super().run(host, port, app, **kwargs)\n        LOGGING_CONFIG = {\n            \"version\": 1,\n            \"disable_existing_loggers\": False,\n            \"handlers\": {\n                \"default\": {\n                    \"class\": \"nonebot.log.LoguruHandler\",\n                },\n            },\n            \"loggers\": {\n                \"uvicorn.error\": {\"handlers\": [\"default\"], \"level\": \"INFO\"},\n                \"uvicorn.access\": {\n                    \"handlers\": [\"default\"],\n                    \"level\": \"INFO\",\n                },\n            },\n        }\n        uvicorn.run(\n            app or self.server_app,  # type: ignore\n            host=host or str(self.config.host),\n            port=port or self.config.port,\n            reload=self.quart_config.quart_reload,\n            reload_dirs=self.quart_config.quart_reload_dirs,\n            reload_delay=self.quart_config.quart_reload_delay,\n            reload_includes=self.quart_config.quart_reload_includes,\n            reload_excludes=self.quart_config.quart_reload_excludes,\n            log_config=LOGGING_CONFIG,\n            **kwargs,\n        )\n\n    async def _handle_http(self, setup: HTTPServerSetup) -> Response:\n        request: Request = _request\n\n        json = await request.get_json() if request.is_json else None\n\n        data = await request.form\n        files_dict = await request.files\n        files: list[tuple[str, FileTypes]] = []\n        key: str\n        value: FileStorage\n        for key, value in files_dict.items():\n            files.append((key, (value.filename, value.stream, value.content_type)))\n\n        http_request = BaseRequest(\n            request.method,\n            request.url,\n            headers=list(request.headers.items()),\n            cookies=list(request.cookies.items()),\n            content=await request.get_data(\n                cache=False, as_text=False, parse_form_data=False\n            ),\n            data=data or None,\n            json=json,\n            files=files or None,\n            version=request.http_version,\n        )\n\n        response = await setup.handle_func(http_request)\n\n        return Response(\n            response.content or \"\",\n            response.status_code or 200,\n            headers=dict(response.headers),\n        )\n\n    async def _handle_ws(self, setup: WebSocketServerSetup) -> None:\n        ctx = cast(WebsocketContext, websocket_ctx.copy())\n        websocket = websocket_ctx.websocket\n\n        http_request = BaseRequest(\n            websocket.method,\n            websocket.url,\n            headers=list(websocket.headers.items()),\n            cookies=list(websocket.cookies.items()),\n            version=websocket.http_version,\n        )\n\n        ws = WebSocket(request=http_request, websocket_ctx=ctx)\n\n        await setup.handle_func(ws)\n\n\nclass WebSocket(BaseWebSocket):\n    \"\"\"Quart WebSocket Wrapper\"\"\"\n\n    def __init__(self, *, request: BaseRequest, websocket_ctx: WebsocketContext):\n        super().__init__(request=request)\n        self.websocket_ctx = websocket_ctx\n\n    @property\n    def websocket(self) -> QuartWebSocket:\n        return self.websocket_ctx.websocket\n\n    @property\n    @override\n    def closed(self):\n        # FIXME\n        return True\n\n    @override\n    async def accept(self):\n        await self.websocket.accept()\n\n    @override\n    async def close(self, code: int = 1000, reason: str = \"\"):\n        await self.websocket.close(code, reason)\n\n    @override\n    @catch_closed\n    async def receive(self) -> str | bytes:\n        return await self.websocket.receive()\n\n    @override\n    @catch_closed\n    async def receive_text(self) -> str:\n        msg = await self.websocket.receive()\n        if isinstance(msg, bytes):\n            raise TypeError(\"WebSocket received unexpected frame type: bytes\")\n        return msg\n\n    @override\n    @catch_closed\n    async def receive_bytes(self) -> bytes:\n        msg = await self.websocket.receive()\n        if isinstance(msg, str):\n            raise TypeError(\"WebSocket received unexpected frame type: str\")\n        return msg\n\n    @override\n    async def send_text(self, data: str):\n        await self.websocket.send(data)\n\n    @override\n    async def send_bytes(self, data: bytes):\n        await self.websocket.send(data)\n\n\n__autodoc__ = {\"catch_closed\": False}\n"
  },
  {
    "path": "nonebot/drivers/websockets.py",
    "content": "\"\"\"[websockets](https://websockets.readthedocs.io/) 驱动适配\n\n```bash\nnb driver install websockets\n# 或者\npip install nonebot2[websockets]\n```\n\n:::tip 提示\n本驱动仅支持客户端 WebSocket 连接\n:::\n\nFrontMatter:\n    mdx:\n        format: md\n    sidebar_position: 4\n    description: nonebot.drivers.websockets 模块\n\"\"\"\n\nfrom collections.abc import AsyncGenerator, Callable\nfrom contextlib import asynccontextmanager\nfrom functools import wraps\nimport logging\nfrom types import CoroutineType\nfrom typing import TYPE_CHECKING, Any, TypeVar\nfrom typing_extensions import ParamSpec, override\n\nfrom nonebot.drivers import Request, Timeout, WebSocketClientMixin, combine_driver\nfrom nonebot.drivers import WebSocket as BaseWebSocket\nfrom nonebot.drivers.none import Driver as NoneDriver\nfrom nonebot.exception import WebSocketClosed\nfrom nonebot.log import LoguruHandler\n\ntry:\n    from websockets import ClientConnection, ConnectionClosed, connect\nexcept ModuleNotFoundError as e:  # pragma: no cover\n    raise ImportError(\n        \"Please install websockets first to use this driver. \"\n        \"Install with pip: `pip install nonebot2[websockets]`\"\n    ) from e\n\nT = TypeVar(\"T\")\nP = ParamSpec(\"P\")\n\nlogger = logging.Logger(\"websockets.client\", \"INFO\")\nlogger.addHandler(LoguruHandler())\n\n\ndef catch_closed(\n    func: Callable[P, \"CoroutineType[Any, Any, T]\"],\n) -> Callable[P, \"CoroutineType[Any, Any, T]\"]:\n    @wraps(func)\n    async def decorator(*args: P.args, **kwargs: P.kwargs) -> T:\n        try:\n            return await func(*args, **kwargs)\n        except ConnectionClosed as e:\n            raise WebSocketClosed(e.code, e.reason)\n\n    return decorator\n\n\nclass Mixin(WebSocketClientMixin):\n    \"\"\"Websockets Mixin\"\"\"\n\n    @property\n    @override\n    def type(self) -> str:\n        return \"websockets\"\n\n    @override\n    @asynccontextmanager\n    async def websocket(self, setup: Request) -> AsyncGenerator[\"WebSocket\", None]:\n        if isinstance(setup.timeout, Timeout):\n            timeout = setup.timeout.total or setup.timeout.connect or setup.timeout.read\n        else:\n            timeout = setup.timeout\n\n        connection = connect(\n            str(setup.url),\n            additional_headers={**setup.headers, **setup.cookies.as_header(setup)},\n            proxy=setup.proxy if setup.proxy is not None else True,\n            open_timeout=timeout,\n        )\n        async with connection as ws:\n            yield WebSocket(request=setup, websocket=ws)\n\n\nclass WebSocket(BaseWebSocket):\n    \"\"\"Websockets WebSocket Wrapper\"\"\"\n\n    @override\n    def __init__(self, *, request: Request, websocket: ClientConnection):\n        super().__init__(request=request)\n        self.websocket = websocket\n\n    @property\n    @override\n    def closed(self) -> bool:\n        return self.websocket.close_code is not None\n\n    @override\n    async def accept(self):\n        raise NotImplementedError\n\n    @override\n    async def close(self, code: int = 1000, reason: str = \"\"):\n        await self.websocket.close(code, reason)\n\n    @override\n    @catch_closed\n    async def receive(self) -> str | bytes:\n        return await self.websocket.recv()\n\n    @override\n    @catch_closed\n    async def receive_text(self) -> str:\n        msg = await self.websocket.recv()\n        if isinstance(msg, bytes):\n            raise TypeError(\"WebSocket received unexpected frame type: bytes\")\n        return msg\n\n    @override\n    @catch_closed\n    async def receive_bytes(self) -> bytes:\n        msg = await self.websocket.recv()\n        if isinstance(msg, str):\n            raise TypeError(\"WebSocket received unexpected frame type: str\")\n        return msg\n\n    @override\n    async def send_text(self, data: str) -> None:\n        await self.websocket.send(data)\n\n    @override\n    async def send_bytes(self, data: bytes) -> None:\n        await self.websocket.send(data)\n\n\nif TYPE_CHECKING:\n\n    class Driver(Mixin, NoneDriver): ...\n\nelse:\n    Driver = combine_driver(NoneDriver, Mixin)\n    \"\"\"Websockets Driver\"\"\"\n"
  },
  {
    "path": "nonebot/exception.py",
    "content": "\"\"\"本模块包含了所有 NoneBot 运行时可能会抛出的异常。\n\n这些异常并非所有需要用户处理，在 NoneBot 内部运行时被捕获，并进行对应操作。\n\n```bash\nNoneBotException\n├── ParserExit\n├── ProcessException\n|   ├── IgnoredException\n|   ├── SkippedException\n|   |   └── TypeMisMatch\n|   ├── MockApiException\n|   └── StopPropagation\n├── MatcherException\n|   ├── PausedException\n|   ├── RejectedException\n|   └── FinishedException\n├── AdapterException\n|   ├── NoLogException\n|   ├── ApiNotAvailable\n|   ├── NetworkError\n|   └── ActionFailed\n└── DriverException\n    └── WebSocketClosed\n```\n\nFrontMatter:\n    mdx:\n        format: md\n    sidebar_position: 10\n    description: nonebot.exception 模块\n\"\"\"\n\nfrom typing import Any\n\nfrom nonebot.compat import ModelField\n\n\nclass NoneBotException(Exception):\n    \"\"\"所有 NoneBot 发生的异常基类。\"\"\"\n\n    def __str__(self) -> str:\n        return self.__repr__()\n\n\n# Rule Exception\nclass ParserExit(NoneBotException):\n    \"\"\"{ref}`nonebot.rule.shell_command` 处理消息失败时返回的异常。\"\"\"\n\n    def __init__(self, status: int = 0, message: str | None = None) -> None:\n        self.status = status\n        self.message = message\n\n    def __repr__(self) -> str:\n        return (\n            f\"ParserExit(status={self.status}\"\n            + (f\", message={self.message!r}\" if self.message else \"\")\n            + \")\"\n        )\n\n\n# Processor Exception\nclass ProcessException(NoneBotException):\n    \"\"\"事件处理过程中发生的异常基类。\"\"\"\n\n\nclass IgnoredException(ProcessException):\n    \"\"\"指示 NoneBot 应该忽略该事件。可由 PreProcessor 抛出。\n\n    参数:\n        reason: 忽略事件的原因\n    \"\"\"\n\n    def __init__(self, reason: Any) -> None:\n        self.reason: Any = reason\n\n    def __repr__(self) -> str:\n        return f\"IgnoredException(reason={self.reason!r})\"\n\n\nclass SkippedException(ProcessException):\n    \"\"\"指示 NoneBot 立即结束当前 `Dependent` 的运行。\n\n    例如，可以在 `Handler` 中通过 {ref}`nonebot.matcher.Matcher.skip` 抛出。\n\n    用法:\n        ```python\n        def always_skip():\n            Matcher.skip()\n\n        @matcher.handle()\n        async def handler(dependency = Depends(always_skip)):\n            # never run\n        ```\n    \"\"\"\n\n\nclass TypeMisMatch(SkippedException):\n    \"\"\"当前 `Handler` 的参数类型不匹配。\"\"\"\n\n    def __init__(self, param: ModelField, value: Any) -> None:\n        self.param: ModelField = param\n        self.value: Any = value\n\n    def __repr__(self) -> str:\n        return (\n            f\"TypeMisMatch(param={self.param.name}, \"\n            f\"type={self.param._type_display()}, value={self.value!r}>\"\n        )\n\n\nclass MockApiException(ProcessException):\n    \"\"\"指示 NoneBot 阻止本次 API 调用或修改本次调用返回值，并返回自定义内容。\n    可由 api hook 抛出。\n\n    参数:\n        result: 返回的内容\n    \"\"\"\n\n    def __init__(self, result: Any):\n        self.result = result\n\n    def __repr__(self) -> str:\n        return f\"MockApiException(result={self.result!r})\"\n\n\nclass StopPropagation(ProcessException):\n    \"\"\"指示 NoneBot 终止事件向下层传播。\n\n    在 {ref}`nonebot.matcher.Matcher.block` 为 `True`\n    或使用 {ref}`nonebot.matcher.Matcher.stop_propagation` 方法时抛出。\n\n    用法:\n        ```python\n        matcher = on_notice(block=True)\n        # 或者\n        @matcher.handle()\n        async def handler(matcher: Matcher):\n            matcher.stop_propagation()\n        ```\n    \"\"\"\n\n\n# Matcher Exceptions\nclass MatcherException(NoneBotException):\n    \"\"\"所有 Matcher 发生的异常基类。\"\"\"\n\n\nclass PausedException(MatcherException):\n    \"\"\"指示 NoneBot 结束当前 `Handler` 并等待下一条消息后继续下一个 `Handler`。\n    可用于用户输入新信息。\n\n    可以在 `Handler` 中通过 {ref}`nonebot.matcher.Matcher.pause` 抛出。\n\n    用法:\n        ```python\n        @matcher.handle()\n        async def handler():\n            await matcher.pause(\"some message\")\n        ```\n    \"\"\"\n\n\nclass RejectedException(MatcherException):\n    \"\"\"指示 NoneBot 结束当前 `Handler` 并等待下一条消息后重新运行当前 `Handler`。\n    可用于用户重新输入。\n\n    可以在 `Handler` 中通过 {ref}`nonebot.matcher.Matcher.reject` 抛出。\n\n    用法:\n        ```python\n        @matcher.handle()\n        async def handler():\n            await matcher.reject(\"some message\")\n        ```\n    \"\"\"\n\n\nclass FinishedException(MatcherException):\n    \"\"\"指示 NoneBot 结束当前 `Handler` 且后续 `Handler` 不再被运行。可用于结束用户会话。\n\n    可以在 `Handler` 中通过 {ref}`nonebot.matcher.Matcher.finish` 抛出。\n\n    用法:\n        ```python\n        @matcher.handle()\n        async def handler():\n            await matcher.finish(\"some message\")\n        ```\n    \"\"\"\n\n\n# Adapter Exceptions\nclass AdapterException(NoneBotException):\n    \"\"\"代表 `Adapter` 抛出的异常，所有的 `Adapter` 都要在内部继承自这个 `Exception`。\n\n    参数:\n        adapter_name: 标识 adapter\n    \"\"\"\n\n    def __init__(self, adapter_name: str, *args: object) -> None:\n        super().__init__(*args)\n        self.adapter_name: str = adapter_name\n\n\nclass NoLogException(AdapterException):\n    \"\"\"指示 NoneBot 对当前 `Event` 进行处理但不显示 Log 信息。\n\n    可在 {ref}`nonebot.adapters.Event.get_log_string` 时抛出\n    \"\"\"\n\n\nclass ApiNotAvailable(AdapterException):\n    \"\"\"在 API 连接不可用时抛出。\"\"\"\n\n\nclass NetworkError(AdapterException):\n    \"\"\"在网络出现问题时抛出，\n    如: API 请求地址不正确, API 请求无返回或返回状态非正常等。\n    \"\"\"\n\n\nclass ActionFailed(AdapterException):\n    \"\"\"API 请求成功返回数据，但 API 操作失败。\"\"\"\n\n\n# Driver Exceptions\nclass DriverException(NoneBotException):\n    \"\"\"`Driver` 抛出的异常基类。\"\"\"\n\n\nclass WebSocketClosed(DriverException):\n    \"\"\"WebSocket 连接已关闭。\"\"\"\n\n    def __init__(self, code: int, reason: str | None = None) -> None:\n        self.code = code\n        self.reason = reason\n\n    def __repr__(self) -> str:\n        return (\n            f\"WebSocketClosed(code={self.code}\"\n            + (f\", reason={self.reason!r}\" if self.reason else \"\")\n            + \")\"\n        )\n"
  },
  {
    "path": "nonebot/internal/__init__.py",
    "content": ""
  },
  {
    "path": "nonebot/internal/adapter/__init__.py",
    "content": "from .adapter import Adapter as Adapter\nfrom .bot import Bot as Bot\nfrom .event import Event as Event\nfrom .message import Message as Message\nfrom .message import MessageSegment as MessageSegment\nfrom .template import MessageTemplate as MessageTemplate\n"
  },
  {
    "path": "nonebot/internal/adapter/adapter.py",
    "content": "import abc\nfrom collections.abc import AsyncGenerator\nfrom contextlib import asynccontextmanager\nfrom typing import Any\n\nfrom nonebot.config import Config\nfrom nonebot.internal.driver import (\n    ASGIMixin,\n    Driver,\n    HTTPClientMixin,\n    HTTPServerSetup,\n    Request,\n    Response,\n    WebSocket,\n    WebSocketClientMixin,\n    WebSocketServerSetup,\n)\nfrom nonebot.internal.driver._lifespan import LIFESPAN_FUNC\n\nfrom .bot import Bot\n\n\nclass Adapter(abc.ABC):\n    \"\"\"协议适配器基类。\n\n    通常，在 Adapter 中编写协议通信相关代码，如: 建立通信连接、处理接收与发送 data 等。\n\n    参数:\n        driver: {ref}`nonebot.drivers.Driver` 实例\n        kwargs: 其他由 {ref}`nonebot.drivers.Driver.register_adapter` 传入的额外参数\n    \"\"\"\n\n    def __init__(self, driver: Driver, **kwargs: Any):\n        self.driver: Driver = driver\n        \"\"\"{ref}`nonebot.drivers.Driver` 实例\"\"\"\n        self.bots: dict[str, Bot] = {}\n        \"\"\"本协议适配器已建立连接的 {ref}`nonebot.adapters.Bot` 实例\"\"\"\n\n    def __repr__(self) -> str:\n        return f\"Adapter(name={self.get_name()!r})\"\n\n    @classmethod\n    @abc.abstractmethod\n    def get_name(cls) -> str:\n        \"\"\"当前协议适配器的名称\"\"\"\n        raise NotImplementedError\n\n    @property\n    def config(self) -> Config:\n        \"\"\"全局 NoneBot 配置\"\"\"\n        return self.driver.config\n\n    def bot_connect(self, bot: Bot) -> None:\n        \"\"\"告知 NoneBot 建立了一个新的 {ref}`nonebot.adapters.Bot` 连接。\n\n        当有新的 {ref}`nonebot.adapters.Bot` 实例连接建立成功时调用。\n\n        参数:\n            bot: {ref}`nonebot.adapters.Bot` 实例\n        \"\"\"\n        self.driver._bot_connect(bot)\n        self.bots[bot.self_id] = bot\n\n    def bot_disconnect(self, bot: Bot) -> None:\n        \"\"\"告知 NoneBot {ref}`nonebot.adapters.Bot` 连接已断开。\n\n        当有 {ref}`nonebot.adapters.Bot` 实例连接断开时调用。\n\n        参数:\n            bot: {ref}`nonebot.adapters.Bot` 实例\n        \"\"\"\n        if self.bots.pop(bot.self_id, None) is None:\n            raise RuntimeError(f\"{bot} not found in adapter {self.get_name()}\")\n        self.driver._bot_disconnect(bot)\n\n    def setup_http_server(self, setup: HTTPServerSetup):\n        \"\"\"设置一个 HTTP 服务器路由配置\"\"\"\n        if not isinstance(self.driver, ASGIMixin):\n            raise TypeError(\"Current driver does not support http server\")\n        self.driver.setup_http_server(setup)\n\n    def setup_websocket_server(self, setup: WebSocketServerSetup):\n        \"\"\"设置一个 WebSocket 服务器路由配置\"\"\"\n        if not isinstance(self.driver, ASGIMixin):\n            raise TypeError(\"Current driver does not support websocket server\")\n        self.driver.setup_websocket_server(setup)\n\n    async def request(self, setup: Request) -> Response:\n        \"\"\"进行一个 HTTP 客户端请求\"\"\"\n        if not isinstance(self.driver, HTTPClientMixin):\n            raise TypeError(\"Current driver does not support http client\")\n        return await self.driver.request(setup)\n\n    @asynccontextmanager\n    async def websocket(self, setup: Request) -> AsyncGenerator[WebSocket, None]:\n        \"\"\"建立一个 WebSocket 客户端连接请求\"\"\"\n        if not isinstance(self.driver, WebSocketClientMixin):\n            raise TypeError(\"Current driver does not support websocket client\")\n        async with self.driver.websocket(setup) as ws:\n            yield ws\n\n    def on_ready(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:\n        return self.driver._lifespan.on_ready(func)\n\n    @abc.abstractmethod\n    async def _call_api(self, bot: Bot, api: str, **data: Any) -> Any:\n        \"\"\"`Adapter` 实际调用 api 的逻辑实现函数，实现该方法以调用 api。\n\n        参数:\n            api: API 名称\n            data: API 数据\n        \"\"\"\n        raise NotImplementedError\n\n\n__autodoc__ = {\"Adapter._call_api\": True}\n"
  },
  {
    "path": "nonebot/internal/adapter/bot.py",
    "content": "import abc\nfrom functools import partial\nfrom typing import TYPE_CHECKING, Any, ClassVar, Protocol\n\nimport anyio\nfrom exceptiongroup import BaseExceptionGroup, catch\n\nfrom nonebot.config import Config\nfrom nonebot.exception import MockApiException\nfrom nonebot.log import logger\nfrom nonebot.typing import T_CalledAPIHook, T_CallingAPIHook\nfrom nonebot.utils import flatten_exception_group\n\nif TYPE_CHECKING:\n    from .adapter import Adapter\n    from .event import Event\n    from .message import Message, MessageSegment\n\n    class _ApiCall(Protocol):\n        async def __call__(self, **kwargs: Any) -> Any: ...\n\n\nclass Bot(abc.ABC):\n    \"\"\"Bot 基类。\n\n    用于处理上报消息，并提供 API 调用接口。\n\n    参数:\n        adapter: 协议适配器实例\n        self_id: 机器人 ID\n    \"\"\"\n\n    _calling_api_hook: ClassVar[set[T_CallingAPIHook]] = set()\n    \"\"\"call_api 时执行的函数\"\"\"\n    _called_api_hook: ClassVar[set[T_CalledAPIHook]] = set()\n    \"\"\"call_api 后执行的函数\"\"\"\n\n    def __init__(self, adapter: \"Adapter\", self_id: str):\n        self.adapter: \"Adapter\" = adapter\n        \"\"\"协议适配器实例\"\"\"\n        self.self_id: str = self_id\n        \"\"\"机器人 ID\"\"\"\n\n    def __repr__(self) -> str:\n        return f\"Bot(type={self.type!r}, self_id={self.self_id!r})\"\n\n    def __getattr__(self, name: str) -> \"_ApiCall\":\n        if name.startswith(\"__\") and name.endswith(\"__\"):\n            raise AttributeError(\n                f\"'{self.__class__.__name__}' object has no attribute '{name}'\"\n            )\n        return partial(self.call_api, name)\n\n    @property\n    def type(self) -> str:\n        \"\"\"协议适配器名称\"\"\"\n        return self.adapter.get_name()\n\n    @property\n    def config(self) -> Config:\n        \"\"\"全局 NoneBot 配置\"\"\"\n        return self.adapter.config\n\n    async def call_api(self, api: str, **data: Any) -> Any:\n        \"\"\"调用机器人 API 接口，可以通过该函数或直接通过 bot 属性进行调用\n\n        参数:\n            api: API 名称\n            data: API 数据\n\n        用法:\n            ```python\n            await bot.call_api(\"send_msg\", message=\"hello world\")\n            await bot.send_msg(message=\"hello world\")\n            ```\n        \"\"\"\n\n        result: Any = None\n        skip_calling_api: bool = False\n        exception: Exception | None = None\n\n        if self._calling_api_hook:\n            logger.debug(\"Running CallingAPI hooks...\")\n\n            def _handle_mock_api_exception(\n                exc_group: BaseExceptionGroup[MockApiException],\n            ) -> None:\n                nonlocal skip_calling_api, result\n\n                excs = [\n                    exc\n                    for exc in flatten_exception_group(exc_group)\n                    if isinstance(exc, MockApiException)\n                ]\n                if not excs:\n                    return\n                elif len(excs) > 1:\n                    logger.warning(\n                        \"Multiple hooks want to mock API result. Use the first one.\"\n                    )\n\n                skip_calling_api = True\n                result = excs[0].result\n\n                logger.debug(\n                    f\"Calling API {api} is cancelled. Return {result!r} instead.\"\n                )\n\n            def _handle_exception(exc_group: BaseExceptionGroup[Exception]) -> None:\n                for exc in flatten_exception_group(exc_group):\n                    logger.opt(colors=True, exception=exc).error(\n                        \"<r><bg #f8bbd0>Error when running CallingAPI hook. \"\n                        \"Running cancelled!</bg #f8bbd0></r>\"\n                    )\n\n            with catch(\n                {\n                    MockApiException: _handle_mock_api_exception,\n                    Exception: _handle_exception,\n                }\n            ):\n                async with anyio.create_task_group() as tg:\n                    for hook in self._calling_api_hook:\n                        tg.start_soon(hook, self, api, data)\n\n        if not skip_calling_api:\n            try:\n                result = await self.adapter._call_api(self, api, **data)\n            except Exception as e:\n                exception = e\n\n        if self._called_api_hook:\n            logger.debug(\"Running CalledAPI hooks...\")\n\n            def _handle_mock_api_exception(\n                exc_group: BaseExceptionGroup[MockApiException],\n            ) -> None:\n                nonlocal result, exception\n\n                excs = [\n                    exc\n                    for exc in flatten_exception_group(exc_group)\n                    if isinstance(exc, MockApiException)\n                ]\n                if not excs:\n                    return\n                elif len(excs) > 1:\n                    logger.warning(\n                        \"Multiple hooks want to mock API result. Use the first one.\"\n                    )\n\n                result = excs[0].result\n                exception = None\n                logger.debug(\n                    f\"Calling API {api} result is mocked. Return {result} instead.\"\n                )\n\n            def _handle_exception(exc_group: BaseExceptionGroup[Exception]) -> None:\n                for exc in flatten_exception_group(exc_group):\n                    logger.opt(colors=True, exception=exc).error(\n                        \"<r><bg #f8bbd0>Error when running CalledAPI hook. \"\n                        \"Running cancelled!</bg #f8bbd0></r>\"\n                    )\n\n            with catch(\n                {\n                    MockApiException: _handle_mock_api_exception,\n                    Exception: _handle_exception,\n                }\n            ):\n                async with anyio.create_task_group() as tg:\n                    for hook in self._called_api_hook:\n                        tg.start_soon(hook, self, exception, api, data, result)\n\n        if exception:\n            raise exception\n        return result\n\n    @abc.abstractmethod\n    async def send(\n        self,\n        event: \"Event\",\n        message: \"str | Message | MessageSegment\",\n        **kwargs: Any,\n    ) -> Any:\n        \"\"\"调用机器人基础发送消息接口\n\n        参数:\n            event: 上报事件\n            message: 要发送的消息\n            kwargs: 任意额外参数\n        \"\"\"\n        raise NotImplementedError\n\n    @classmethod\n    def on_calling_api(cls, func: T_CallingAPIHook) -> T_CallingAPIHook:\n        \"\"\"调用 api 预处理。\n\n        钩子函数参数:\n\n        - bot: 当前 bot 对象\n        - api: 调用的 api 名称\n        - data: api 调用的参数字典\n        \"\"\"\n        cls._calling_api_hook.add(func)\n        return func\n\n    @classmethod\n    def on_called_api(cls, func: T_CalledAPIHook) -> T_CalledAPIHook:\n        \"\"\"调用 api 后处理。\n\n        钩子函数参数:\n\n        - bot: 当前 bot 对象\n        - exception: 调用 api 时发生的错误\n        - api: 调用的 api 名称\n        - data: api 调用的参数字典\n        - result: api 调用的返回\n        \"\"\"\n        cls._called_api_hook.add(func)\n        return func\n"
  },
  {
    "path": "nonebot/internal/adapter/event.py",
    "content": "import abc\nfrom typing import Any, TypeVar\n\nfrom pydantic import BaseModel\n\nfrom nonebot.compat import PYDANTIC_V2, ConfigDict\nfrom nonebot.utils import DataclassEncoder\n\nfrom .message import Message\n\nE = TypeVar(\"E\", bound=\"Event\")\n\n\nclass Event(abc.ABC, BaseModel):\n    \"\"\"Event 基类。提供获取关键信息的方法，其余信息可直接获取。\"\"\"\n\n    if PYDANTIC_V2:  # pragma: pydantic-v2\n        model_config = ConfigDict(extra=\"allow\")\n    else:  # pragma: pydantic-v1\n\n        class Config(ConfigDict):\n            extra = \"allow\"  # type: ignore\n            json_encoders = {Message: DataclassEncoder}  # noqa: RUF012\n\n    if not PYDANTIC_V2:  # pragma: pydantic-v1\n\n        @classmethod\n        def validate(cls: type[\"E\"], value: Any) -> \"E\":\n            if isinstance(value, Event) and not isinstance(value, cls):\n                raise TypeError(f\"{value} is incompatible with Event type {cls}\")\n            return super().validate(value)\n\n    @abc.abstractmethod\n    def get_type(self) -> str:\n        \"\"\"获取事件类型的方法，类型通常为 NoneBot 内置的四种类型。\"\"\"\n        raise NotImplementedError\n\n    @abc.abstractmethod\n    def get_event_name(self) -> str:\n        \"\"\"获取事件名称的方法。\"\"\"\n        raise NotImplementedError\n\n    @abc.abstractmethod\n    def get_event_description(self) -> str:\n        \"\"\"获取事件描述的方法，通常为事件具体内容。\"\"\"\n        raise NotImplementedError\n\n    def __str__(self) -> str:\n        return f\"[{self.get_event_name()}]: {self.get_event_description()}\"\n\n    def get_log_string(self) -> str:\n        \"\"\"获取事件日志信息的方法。\n\n        通常你不需要修改这个方法，只有当希望 NoneBot 隐藏该事件日志时，\n        可以抛出 `NoLogException` 异常。\n\n        异常:\n            NoLogException: 希望 NoneBot 隐藏该事件日志\n        \"\"\"\n        return f\"[{self.get_event_name()}]: {self.get_event_description()}\"\n\n    @abc.abstractmethod\n    def get_user_id(self) -> str:\n        \"\"\"获取事件主体 id 的方法，通常是用户 id 。\"\"\"\n        raise NotImplementedError\n\n    @abc.abstractmethod\n    def get_session_id(self) -> str:\n        \"\"\"获取会话 id 的方法，用于判断当前事件属于哪一个会话，\n        通常是用户 id、群组 id 组合。\n        \"\"\"\n        raise NotImplementedError\n\n    @abc.abstractmethod\n    def get_message(self) -> \"Message\":\n        \"\"\"获取事件消息内容的方法。\"\"\"\n        raise NotImplementedError\n\n    def get_plaintext(self) -> str:\n        \"\"\"获取消息纯文本的方法。\n\n        通常不需要修改，默认通过 `get_message().extract_plain_text` 获取。\n        \"\"\"\n        return self.get_message().extract_plain_text()\n\n    @abc.abstractmethod\n    def is_tome(self) -> bool:\n        \"\"\"获取事件是否与机器人有关的方法。\"\"\"\n        raise NotImplementedError\n"
  },
  {
    "path": "nonebot/internal/adapter/message.py",
    "content": "import abc\nfrom collections.abc import Iterable\nfrom copy import deepcopy\nfrom dataclasses import asdict, dataclass, field\nfrom typing import (  # noqa: UP035\n    Any,\n    Generic,\n    SupportsIndex,\n    Type,\n    TypeVar,\n    overload,\n)\nfrom typing_extensions import Self\n\nfrom nonebot.compat import custom_validation, type_validate_python\n\nfrom .template import MessageTemplate\n\nTMS = TypeVar(\"TMS\", bound=\"MessageSegment\")\nTM = TypeVar(\"TM\", bound=\"Message\")\n\n\n@custom_validation\n@dataclass\nclass MessageSegment(abc.ABC, Generic[TM]):\n    \"\"\"消息段基类\"\"\"\n\n    type: str\n    \"\"\"消息段类型\"\"\"\n    data: dict[str, Any] = field(default_factory=dict)\n    \"\"\"消息段数据\"\"\"\n\n    @classmethod\n    @abc.abstractmethod\n    def get_message_class(cls) -> Type[TM]:  # noqa: UP006\n        \"\"\"获取消息数组类型\"\"\"\n        raise NotImplementedError\n\n    @abc.abstractmethod\n    def __str__(self) -> str:\n        \"\"\"该消息段所代表的 str，在命令匹配部分使用\"\"\"\n        raise NotImplementedError\n\n    def __len__(self) -> int:\n        return len(str(self))\n\n    def __ne__(  # pyright: ignore[reportIncompatibleMethodOverride]\n        self, other: Self\n    ) -> bool:\n        return not self == other\n\n    def __add__(self, other: str | Self | Iterable[Self]) -> TM:\n        return self.get_message_class()(self) + other\n\n    def __radd__(self, other: str | Self | Iterable[Self]) -> TM:\n        return self.get_message_class()(other) + self\n\n    @classmethod\n    def __get_validators__(cls):\n        yield cls._validate\n\n    @classmethod\n    def _validate(cls, value) -> Self:\n        if isinstance(value, cls):\n            return value\n        if isinstance(value, MessageSegment):\n            raise ValueError(f\"Type {type(value)} can not be converted to {cls}\")\n        if not isinstance(value, dict):\n            raise ValueError(f\"Expected dict for MessageSegment, got {type(value)}\")\n        if \"type\" not in value:\n            raise ValueError(\n                f\"Expected dict with 'type' for MessageSegment, got {value}\"\n            )\n        return cls(type=value[\"type\"], data=value.get(\"data\", {}))\n\n    def get(self, key: str, default: Any = None):\n        return asdict(self).get(key, default)\n\n    def keys(self):\n        return asdict(self).keys()\n\n    def values(self):\n        return asdict(self).values()\n\n    def items(self):\n        return asdict(self).items()\n\n    def join(self, iterable: Iterable[Self | TM]) -> TM:\n        return self.get_message_class()(self).join(iterable)\n\n    def copy(self) -> Self:\n        return deepcopy(self)\n\n    @abc.abstractmethod\n    def is_text(self) -> bool:\n        \"\"\"当前消息段是否为纯文本\"\"\"\n        raise NotImplementedError\n\n\n@custom_validation\nclass Message(list[TMS], abc.ABC):\n    \"\"\"消息序列\n\n    参数:\n        message: 消息内容\n    \"\"\"\n\n    def __init__(\n        self,\n        message: str | None | Iterable[TMS] | TMS = None,\n    ):\n        super().__init__()\n        if message is None:\n            return\n        elif isinstance(message, str):\n            self.extend(self._construct(message))\n        elif isinstance(message, MessageSegment):\n            self.append(message)\n        elif isinstance(message, Iterable):\n            self.extend(message)\n        else:\n            self.extend(self._construct(message))  # pragma: no cover\n\n    @classmethod\n    def template(cls, format_string: str | TM) -> MessageTemplate[Self]:\n        \"\"\"创建消息模板。\n\n        用法和 `str.format` 大致相同，支持以 `Message` 对象作为消息模板并输出消息对象。\n        并且提供了拓展的格式化控制符，\n        可以通过该消息类型的 `MessageSegment` 工厂方法创建消息。\n\n        参数:\n            format_string: 格式化模板\n\n        返回:\n            消息格式化器\n        \"\"\"\n        return MessageTemplate(format_string, cls)\n\n    @classmethod\n    @abc.abstractmethod\n    def get_segment_class(cls) -> type[TMS]:\n        \"\"\"获取消息段类型\"\"\"\n        raise NotImplementedError\n\n    def __str__(self) -> str:\n        return \"\".join(str(seg) for seg in self)\n\n    @classmethod\n    def __get_validators__(cls):\n        yield cls._validate\n\n    @classmethod\n    def _validate(cls, value) -> Self:\n        if isinstance(value, cls):\n            return value\n        elif isinstance(value, Message):\n            raise ValueError(f\"Type {type(value)} can not be converted to {cls}\")\n        elif isinstance(value, str):\n            pass\n        elif isinstance(value, dict):\n            value = type_validate_python(cls.get_segment_class(), value)\n        elif isinstance(value, Iterable):\n            value = [type_validate_python(cls.get_segment_class(), v) for v in value]\n        else:\n            raise ValueError(\n                f\"Expected str, dict or iterable for Message, got {type(value)}\"\n            )\n        return cls(value)\n\n    @staticmethod\n    @abc.abstractmethod\n    def _construct(msg: str) -> Iterable[TMS]:\n        \"\"\"构造消息数组\"\"\"\n        raise NotImplementedError\n\n    def __add__(  # pyright: ignore[reportIncompatibleMethodOverride]\n        self, other: str | TMS | Iterable[TMS]\n    ) -> Self:\n        result = self.copy()\n        result += other\n        return result\n\n    def __radd__(self, other: str | TMS | Iterable[TMS]) -> Self:\n        result = self.__class__(other)\n        return result + self\n\n    def __iadd__(self, other: str | TMS | Iterable[TMS]) -> Self:\n        if isinstance(other, str):\n            self.extend(self._construct(other))\n        elif isinstance(other, MessageSegment):\n            self.append(other)\n        elif isinstance(other, Iterable):\n            self.extend(other)\n        else:\n            raise TypeError(f\"Unsupported type {type(other)!r}\")\n        return self\n\n    @overload\n    def __getitem__(self, args: str) -> Self:\n        \"\"\"获取仅包含指定消息段类型的消息\n\n        参数:\n            args: 消息段类型\n\n        返回:\n            所有类型为 `args` 的消息段\n        \"\"\"\n\n    @overload\n    def __getitem__(self, args: tuple[str, int]) -> TMS:\n        \"\"\"索引指定类型的消息段\n\n        参数:\n            args: 消息段类型和索引\n\n        返回:\n            类型为 `args[0]` 的消息段第 `args[1]` 个\n        \"\"\"\n\n    @overload\n    def __getitem__(self, args: tuple[str, slice]) -> Self:\n        \"\"\"切片指定类型的消息段\n\n        参数:\n            args: 消息段类型和切片\n\n        返回:\n            类型为 `args[0]` 的消息段切片 `args[1]`\n        \"\"\"\n\n    @overload\n    def __getitem__(self, args: int) -> TMS:\n        \"\"\"索引消息段\n\n        参数:\n            args: 索引\n\n        返回:\n            第 `args` 个消息段\n        \"\"\"\n\n    @overload\n    def __getitem__(self, args: slice) -> Self:\n        \"\"\"切片消息段\n\n        参数:\n            args: 切片\n\n        返回:\n            消息切片 `args`\n        \"\"\"\n\n    def __getitem__(  # pyright: ignore[reportIncompatibleMethodOverride]\n        self,\n        args: str | tuple[str, int] | tuple[str, slice] | int | slice,\n    ) -> TMS | Self:\n        arg1, arg2 = args if isinstance(args, tuple) else (args, None)\n        if isinstance(arg1, int) and arg2 is None:\n            return super().__getitem__(arg1)\n        elif isinstance(arg1, slice) and arg2 is None:\n            return self.__class__(super().__getitem__(arg1))\n        elif isinstance(arg1, str) and arg2 is None:\n            return self.__class__(seg for seg in self if seg.type == arg1)\n        elif isinstance(arg1, str) and isinstance(arg2, int):\n            return [seg for seg in self if seg.type == arg1][arg2]\n        elif isinstance(arg1, str) and isinstance(arg2, slice):\n            return self.__class__([seg for seg in self if seg.type == arg1][arg2])\n        else:\n            raise ValueError(\"Incorrect arguments to slice\")  # pragma: no cover\n\n    def __contains__(  # pyright: ignore[reportIncompatibleMethodOverride]\n        self, value: TMS | str\n    ) -> bool:\n        \"\"\"检查消息段是否存在\n\n        参数:\n            value: 消息段或消息段类型\n        返回:\n            消息内是否存在给定消息段或给定类型的消息段\n        \"\"\"\n        if isinstance(value, str):\n            return next((seg for seg in self if seg.type == value), None) is not None\n        return super().__contains__(value)\n\n    def has(self, value: TMS | str) -> bool:\n        \"\"\"与 {ref}``__contains__` <nonebot.adapters.Message.__contains__>` 相同\"\"\"\n        return value in self\n\n    def index(self, value: TMS | str, *args: SupportsIndex) -> int:\n        \"\"\"索引消息段\n\n        参数:\n            value: 消息段或者消息段类型\n            arg: start 与 end\n\n        返回:\n            索引 index\n\n        异常:\n            ValueError: 消息段不存在\n        \"\"\"\n        if isinstance(value, str):\n            first_segment = next((seg for seg in self if seg.type == value), None)\n            if first_segment is None:\n                raise ValueError(f\"Segment with type {value!r} is not in message\")\n            return super().index(first_segment, *args)\n        return super().index(value, *args)\n\n    def get(self, type_: str, count: int | None = None) -> Self:\n        \"\"\"获取指定类型的消息段\n\n        参数:\n            type_: 消息段类型\n            count: 获取个数\n\n        返回:\n            构建的新消息\n        \"\"\"\n        if count is None:\n            return self[type_]\n\n        iterator, filtered = (\n            (seg for seg in self if seg.type == type_),\n            self.__class__(),\n        )\n        for _ in range(count):\n            seg = next(iterator, None)\n            if seg is None:\n                break\n            filtered.append(seg)\n        return filtered\n\n    def count(self, value: TMS | str) -> int:\n        \"\"\"计算指定消息段的个数\n\n        参数:\n            value: 消息段或消息段类型\n\n        返回:\n            个数\n        \"\"\"\n        return len(self[value]) if isinstance(value, str) else super().count(value)\n\n    def only(self, value: TMS | str) -> bool:\n        \"\"\"检查消息中是否仅包含指定消息段\n\n        参数:\n            value: 指定消息段或消息段类型\n\n        返回:\n            是否仅包含指定消息段\n        \"\"\"\n        if isinstance(value, str):\n            return all(seg.type == value for seg in self)\n        return all(seg == value for seg in self)\n\n    def append(  # pyright: ignore[reportIncompatibleMethodOverride]\n        self, obj: str | TMS\n    ) -> Self:\n        \"\"\"添加一个消息段到消息数组末尾。\n\n        参数:\n            obj: 要添加的消息段\n        \"\"\"\n        if isinstance(obj, MessageSegment):\n            super().append(obj)\n        elif isinstance(obj, str):\n            self.extend(self._construct(obj))\n        else:\n            raise ValueError(f\"Unexpected type: {type(obj)} {obj}\")  # pragma: no cover\n        return self\n\n    def extend(  # pyright: ignore[reportIncompatibleMethodOverride]\n        self, obj: Self | Iterable[TMS]\n    ) -> Self:\n        \"\"\"拼接一个消息数组或多个消息段到消息数组末尾。\n\n        参数:\n            obj: 要添加的消息数组\n        \"\"\"\n        for segment in obj:\n            self.append(segment)\n        return self\n\n    def join(self, iterable: Iterable[TMS | Self]) -> Self:\n        \"\"\"将多个消息连接并将自身作为分割\n\n        参数:\n            iterable: 要连接的消息\n\n        返回:\n            连接后的消息\n        \"\"\"\n        ret = self.__class__()\n        for index, msg in enumerate(iterable):\n            if index != 0:\n                ret.extend(self)\n            if isinstance(msg, MessageSegment):\n                ret.append(msg.copy())\n            else:\n                ret.extend(msg.copy())\n        return ret\n\n    def copy(self) -> Self:\n        \"\"\"深拷贝消息\"\"\"\n        return deepcopy(self)\n\n    def include(self, *types: str) -> Self:\n        \"\"\"过滤消息\n\n        参数:\n            types: 包含的消息段类型\n\n        返回:\n            新构造的消息\n        \"\"\"\n        return self.__class__(seg for seg in self if seg.type in types)\n\n    def exclude(self, *types: str) -> Self:\n        \"\"\"过滤消息\n\n        参数:\n            types: 不包含的消息段类型\n\n        返回:\n            新构造的消息\n        \"\"\"\n        return self.__class__(seg for seg in self if seg.type not in types)\n\n    def extract_plain_text(self) -> str:\n        \"\"\"提取消息内纯文本消息\"\"\"\n\n        return \"\".join(str(seg) for seg in self if seg.is_text())\n"
  },
  {
    "path": "nonebot/internal/adapter/template.py",
    "content": "from _string import formatter_field_name_split  # type: ignore\nfrom collections.abc import Callable, Mapping, Sequence\nimport functools\nfrom string import Formatter\nfrom typing import (\n    TYPE_CHECKING,\n    Any,\n    Generic,\n    TypeAlias,\n    TypeVar,\n    cast,\n    overload,\n)\n\nif TYPE_CHECKING:\n    from .message import Message, MessageSegment\n\n    def formatter_field_name_split(\n        field_name: str,\n    ) -> tuple[str, list[tuple[bool, str]]]: ...\n\n\nTM = TypeVar(\"TM\", bound=\"Message\")\nTF = TypeVar(\"TF\", str, \"Message\")\n\nFormatSpecFunc: TypeAlias = Callable[[Any], str]\nFormatSpecFunc_T = TypeVar(\"FormatSpecFunc_T\", bound=FormatSpecFunc)\n\n\nclass MessageTemplate(Formatter, Generic[TF]):\n    \"\"\"消息模板格式化实现类。\n\n    参数:\n        template: 模板\n        factory: 消息类型工厂，默认为 `str`\n        private_getattr: 是否允许在模板中访问私有属性，默认为 `False`\n    \"\"\"\n\n    @overload\n    def __init__(\n        self: \"MessageTemplate[str]\",\n        template: str,\n        factory: type[str] = str,\n        private_getattr: bool = False,\n    ) -> None: ...\n\n    @overload\n    def __init__(\n        self: \"MessageTemplate[TM]\",\n        template: str | TM,\n        factory: type[TM],\n        private_getattr: bool = False,\n    ) -> None: ...\n\n    def __init__(\n        self,\n        template: str | TM,\n        factory: type[str] | type[TM] = str,\n        private_getattr: bool = False,\n    ) -> None:\n        self.template: TF = template  # type: ignore\n        self.factory: type[TF] = factory  # type: ignore\n        self.format_specs: dict[str, FormatSpecFunc] = {}\n        self.private_getattr = private_getattr\n\n    def __repr__(self) -> str:\n        return f\"MessageTemplate({self.template!r}, factory={self.factory!r})\"\n\n    def add_format_spec(\n        self, spec: FormatSpecFunc_T, name: str | None = None\n    ) -> FormatSpecFunc_T:\n        name = name or spec.__name__\n        if name in self.format_specs:\n            raise ValueError(f\"Format spec {name} already exists!\")\n        self.format_specs[name] = spec\n        return spec\n\n    def format(  # pyright: ignore[reportIncompatibleMethodOverride]\n        self, *args, **kwargs\n    ) -> TF:\n        \"\"\"根据传入参数和模板生成消息对象\"\"\"\n        return self._format(args, kwargs)\n\n    def format_map(self, mapping: Mapping[str, Any]) -> TF:\n        \"\"\"根据传入字典和模板生成消息对象, 在传入字段名不是有效标识符时有用\"\"\"\n        return self._format([], mapping)\n\n    def _format(self, args: Sequence[Any], kwargs: Mapping[str, Any]) -> TF:\n        full_message = self.factory()\n        used_args, arg_index = set(), 0\n\n        if isinstance(self.template, str):\n            msg, arg_index = self._vformat(\n                self.template, args, kwargs, used_args, arg_index\n            )\n            full_message += msg\n        elif isinstance(self.template, self.factory):\n            template = cast(\"Message[MessageSegment]\", self.template)\n            for seg in template:\n                if not seg.is_text():\n                    full_message += seg\n                else:\n                    msg, arg_index = self._vformat(\n                        str(seg), args, kwargs, used_args, arg_index\n                    )\n                    full_message += msg\n        else:\n            raise TypeError(\"template must be a string or instance of Message!\")\n\n        self.check_unused_args(used_args, args, kwargs)\n        return cast(TF, full_message)\n\n    def vformat(  # pyright: ignore[reportIncompatibleMethodOverride]\n        self,\n        format_string: str,\n        args: Sequence[Any],\n        kwargs: Mapping[str, Any],\n    ) -> TF:\n        raise NotImplementedError(\"`vformat` has merged into `_format`\")\n\n    def _vformat(  # pyright: ignore[reportIncompatibleMethodOverride]\n        self,\n        format_string: str,\n        args: Sequence[Any],\n        kwargs: Mapping[str, Any],\n        used_args: set[int | str],\n        auto_arg_index: int = 0,\n    ) -> tuple[TF, int]:\n        results: list[Any] = [self.factory()]\n\n        for literal_text, field_name, format_spec, conversion in self.parse(\n            format_string\n        ):\n            # output the literal text\n            if literal_text:\n                results.append(literal_text)\n\n            # if there's a field, output it\n            if field_name is not None:\n                # this is some markup, find the object and do\n                #  the formatting\n\n                # handle arg indexing when empty field_names are given.\n                if field_name == \"\":\n                    if auto_arg_index is False:\n                        raise ValueError(\n                            \"cannot switch from manual field specification to \"\n                            \"automatic field numbering\"\n                        )\n                    field_name = str(auto_arg_index)\n                    auto_arg_index += 1\n                elif field_name.isdigit():\n                    if auto_arg_index:\n                        raise ValueError(\n                            \"cannot switch from manual field specification to \"\n                            \"automatic field numbering\"\n                        )\n                    # disable auto arg incrementing, if it gets\n                    # used later on, then an exception will be raised\n                    auto_arg_index = False\n\n                # given the field_name, find the object it references\n                #  and the argument it came from\n                obj, arg_used = self.get_field(field_name, args, kwargs)\n                used_args.add(arg_used)\n\n                # do any conversion on the resulting object\n                obj = self.convert_field(obj, conversion) if conversion else obj\n\n                # format the object and append to the result\n                formatted_text = (\n                    self.format_field(obj, format_spec) if format_spec else obj\n                )\n                results.append(formatted_text)\n\n        return functools.reduce(self._add, results), auto_arg_index\n\n    def get_field(\n        self, field_name: str, args: Sequence[Any], kwargs: Mapping[str, Any]\n    ) -> tuple[Any, int | str]:\n        first, rest = formatter_field_name_split(field_name)\n        obj = self.get_value(first, args, kwargs)\n\n        for is_attr, value in rest:\n            if not self.private_getattr and value.startswith(\"_\"):\n                raise ValueError(\"Cannot access private attribute\")\n            obj = getattr(obj, value) if is_attr else obj[value]\n\n        return obj, first\n\n    def format_field(self, value: Any, format_spec: str) -> Any:\n        formatter: FormatSpecFunc | None = self.format_specs.get(format_spec)\n        if formatter is None and not issubclass(self.factory, str):\n            segment_class: type[\"MessageSegment\"] = self.factory.get_segment_class()\n            method = getattr(segment_class, format_spec, None)\n            if callable(method) and not cast(str, method.__name__).startswith(\"_\"):\n                formatter = getattr(segment_class, format_spec)\n        return (\n            super().format_field(value, format_spec)\n            if formatter is None\n            else formatter(value)\n        )\n\n    def _add(self, a: Any, b: Any) -> Any:\n        try:\n            return a + b\n        except TypeError:\n            return a + str(b)\n"
  },
  {
    "path": "nonebot/internal/driver/__init__.py",
    "content": "from .abstract import ASGIMixin as ASGIMixin\nfrom .abstract import Driver as Driver\nfrom .abstract import ForwardDriver as ForwardDriver\nfrom .abstract import ForwardMixin as ForwardMixin\nfrom .abstract import HTTPClientMixin as HTTPClientMixin\nfrom .abstract import HTTPClientSession as HTTPClientSession\nfrom .abstract import Mixin as Mixin\nfrom .abstract import ReverseDriver as ReverseDriver\nfrom .abstract import ReverseMixin as ReverseMixin\nfrom .abstract import WebSocketClientMixin as WebSocketClientMixin\nfrom .combine import combine_driver as combine_driver\nfrom .model import URL as URL\nfrom .model import ContentTypes as ContentTypes\nfrom .model import Cookies as Cookies\nfrom .model import CookieTypes as CookieTypes\nfrom .model import DataTypes as DataTypes\nfrom .model import FileContent as FileContent\nfrom .model import FilesTypes as FilesTypes\nfrom .model import FileType as FileType\nfrom .model import FileTypes as FileTypes\nfrom .model import HeaderTypes as HeaderTypes\nfrom .model import HTTPServerSetup as HTTPServerSetup\nfrom .model import HTTPVersion as HTTPVersion\nfrom .model import QueryTypes as QueryTypes\nfrom .model import QueryVariable as QueryVariable\nfrom .model import RawURL as RawURL\nfrom .model import Request as Request\nfrom .model import Response as Response\nfrom .model import SimpleQuery as SimpleQuery\nfrom .model import Timeout as Timeout\nfrom .model import TimeoutTypes as TimeoutTypes\nfrom .model import WebSocket as WebSocket\nfrom .model import WebSocketServerSetup as WebSocketServerSetup\n"
  },
  {
    "path": "nonebot/internal/driver/_lifespan.py",
    "content": "from collections.abc import Awaitable, Callable, Iterable\nfrom types import TracebackType\nfrom typing import Any, TypeAlias, cast\n\nimport anyio\nfrom anyio.abc import TaskGroup\nfrom exceptiongroup import suppress\n\nfrom nonebot.utils import is_coroutine_callable, run_sync\n\nSYNC_LIFESPAN_FUNC: TypeAlias = Callable[[], Any]\nASYNC_LIFESPAN_FUNC: TypeAlias = Callable[[], Awaitable[Any]]\nLIFESPAN_FUNC: TypeAlias = SYNC_LIFESPAN_FUNC | ASYNC_LIFESPAN_FUNC\n\n\nclass Lifespan:\n    def __init__(self) -> None:\n        self._task_group: TaskGroup | None = None\n\n        self._startup_funcs: list[LIFESPAN_FUNC] = []\n        self._ready_funcs: list[LIFESPAN_FUNC] = []\n        self._shutdown_funcs: list[LIFESPAN_FUNC] = []\n\n    @property\n    def task_group(self) -> TaskGroup:\n        if self._task_group is None:\n            raise RuntimeError(\"Lifespan not started\")\n        return self._task_group\n\n    @task_group.setter\n    def task_group(self, task_group: TaskGroup) -> None:\n        if self._task_group is not None:\n            raise RuntimeError(\"Lifespan already started\")\n        self._task_group = task_group\n\n    def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:\n        self._startup_funcs.append(func)\n        return func\n\n    def on_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:\n        self._shutdown_funcs.append(func)\n        return func\n\n    def on_ready(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:\n        self._ready_funcs.append(func)\n        return func\n\n    @staticmethod\n    async def _run_lifespan_func(\n        funcs: Iterable[LIFESPAN_FUNC],\n    ) -> None:\n        for func in funcs:\n            if is_coroutine_callable(func):\n                await cast(ASYNC_LIFESPAN_FUNC, func)()\n            else:\n                await run_sync(cast(SYNC_LIFESPAN_FUNC, func))()\n\n    async def startup(self) -> None:\n        # create background task group\n        self.task_group = anyio.create_task_group()\n        await self.task_group.__aenter__()\n\n        # run startup funcs\n        if self._startup_funcs:\n            await self._run_lifespan_func(self._startup_funcs)\n\n        # run ready funcs\n        if self._ready_funcs:\n            await self._run_lifespan_func(self._ready_funcs)\n\n    async def shutdown(\n        self,\n        *,\n        exc_type: type[BaseException] | None = None,\n        exc_val: BaseException | None = None,\n        exc_tb: TracebackType | None = None,\n    ) -> None:\n        if self._shutdown_funcs:\n            # reverse shutdown funcs to ensure stack order\n            await self._run_lifespan_func(reversed(self._shutdown_funcs))\n\n        # shutdown background task group\n        self.task_group.cancel_scope.cancel()\n\n        with suppress(Exception):\n            await self.task_group.__aexit__(exc_type, exc_val, exc_tb)\n\n        self._task_group = None\n\n    async def __aenter__(self) -> None:\n        await self.startup()\n\n    async def __aexit__(\n        self,\n        exc_type: type[BaseException] | None,\n        exc_val: BaseException | None,\n        exc_tb: TracebackType | None,\n    ) -> None:\n        await self.shutdown(exc_type=exc_type, exc_val=exc_val, exc_tb=exc_tb)\n"
  },
  {
    "path": "nonebot/internal/driver/abstract.py",
    "content": "import abc\nfrom collections.abc import AsyncGenerator\nfrom contextlib import AsyncExitStack, asynccontextmanager\nfrom types import TracebackType\nfrom typing import TYPE_CHECKING, Any, ClassVar, TypeAlias\nfrom typing_extensions import Self\n\nfrom anyio import CancelScope, create_task_group\nfrom anyio.abc import TaskGroup\nfrom exceptiongroup import BaseExceptionGroup, catch\n\nfrom nonebot.config import Config, Env\nfrom nonebot.dependencies import Dependent\nfrom nonebot.exception import SkippedException\nfrom nonebot.internal.params import BotParam, DefaultParam, DependParam\nfrom nonebot.log import logger\nfrom nonebot.typing import (\n    T_BotConnectionHook,\n    T_BotDisconnectionHook,\n    T_DependencyCache,\n)\nfrom nonebot.utils import escape_tag, flatten_exception_group, run_coro_with_catch\n\nfrom ._lifespan import LIFESPAN_FUNC, Lifespan\nfrom .model import (\n    CookieTypes,\n    HeaderTypes,\n    HTTPServerSetup,\n    HTTPVersion,\n    QueryTypes,\n    Request,\n    Response,\n    TimeoutTypes,\n    WebSocket,\n    WebSocketServerSetup,\n)\n\nif TYPE_CHECKING:\n    from nonebot.internal.adapter import Adapter, Bot\n\n\nBOT_HOOK_PARAMS = [DependParam, BotParam, DefaultParam]\n\n\nclass Driver(abc.ABC):\n    \"\"\"驱动器基类。\n\n    驱动器控制框架的启动和停止，适配器的注册，以及机器人生命周期管理。\n\n    参数:\n        env: 包含环境信息的 Env 对象\n        config: 包含配置信息的 Config 对象\n    \"\"\"\n\n    _adapters: ClassVar[dict[str, \"Adapter\"]] = {}\n    \"\"\"已注册的适配器列表\"\"\"\n    _bot_connection_hook: ClassVar[set[Dependent[Any]]] = set()\n    \"\"\"Bot 连接建立时执行的函数\"\"\"\n    _bot_disconnection_hook: ClassVar[set[Dependent[Any]]] = set()\n    \"\"\"Bot 连接断开时执行的函数\"\"\"\n\n    def __init__(self, env: Env, config: Config):\n        self.env: str = env.environment\n        \"\"\"环境名称\"\"\"\n        self.config: Config = config\n        \"\"\"全局配置对象\"\"\"\n        self._bots: dict[str, \"Bot\"] = {}\n        self._lifespan = Lifespan()\n\n    def __repr__(self) -> str:\n        return (\n            f\"Driver(type={self.type!r}, \"\n            f\"adapters={len(self._adapters)}, bots={len(self._bots)})\"\n        )\n\n    @property\n    def bots(self) -> dict[str, \"Bot\"]:\n        \"\"\"获取当前所有已连接的 Bot\"\"\"\n        return self._bots\n\n    @property\n    def task_group(self) -> TaskGroup:\n        return self._lifespan.task_group\n\n    def register_adapter(self, adapter: type[\"Adapter\"], **kwargs) -> None:\n        \"\"\"注册一个协议适配器\n\n        参数:\n            adapter: 适配器类\n            kwargs: 其他传递给适配器的参数\n        \"\"\"\n        name = adapter.get_name()\n        if name in self._adapters:\n            logger.opt(colors=True).debug(\n                f'Adapter \"<y>{escape_tag(name)}</y>\" already exists'\n            )\n            return\n        self._adapters[name] = adapter(self, **kwargs)\n        logger.opt(colors=True).debug(\n            f'Succeeded to load adapter \"<y>{escape_tag(name)}</y>\"'\n        )\n\n    @property\n    @abc.abstractmethod\n    def type(self) -> str:\n        \"\"\"驱动类型名称\"\"\"\n        raise NotImplementedError\n\n    @property\n    @abc.abstractmethod\n    def logger(self):\n        \"\"\"驱动专属 logger 日志记录器\"\"\"\n        raise NotImplementedError\n\n    @abc.abstractmethod\n    def run(self, *args, **kwargs):\n        \"\"\"启动驱动框架\"\"\"\n        logger.opt(colors=True).success(\n            f\"<g>Loaded adapters: {escape_tag(', '.join(self._adapters))}</g>\"\n        )\n\n    def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:\n        \"\"\"注册一个启动时执行的函数\"\"\"\n        return self._lifespan.on_startup(func)\n\n    def on_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:\n        \"\"\"注册一个停止时执行的函数\"\"\"\n        return self._lifespan.on_shutdown(func)\n\n    @classmethod\n    def on_bot_connect(cls, func: T_BotConnectionHook) -> T_BotConnectionHook:\n        \"\"\"装饰一个函数使他在 bot 连接成功时执行。\n\n        钩子函数参数:\n\n        - bot: 当前连接上的 Bot 对象\n        \"\"\"\n        cls._bot_connection_hook.add(\n            Dependent[Any].parse(call=func, allow_types=BOT_HOOK_PARAMS)\n        )\n        return func\n\n    @classmethod\n    def on_bot_disconnect(cls, func: T_BotDisconnectionHook) -> T_BotDisconnectionHook:\n        \"\"\"装饰一个函数使他在 bot 连接断开时执行。\n\n        钩子函数参数:\n\n        - bot: 当前连接上的 Bot 对象\n        \"\"\"\n        cls._bot_disconnection_hook.add(\n            Dependent[Any].parse(call=func, allow_types=BOT_HOOK_PARAMS)\n        )\n        return func\n\n    def _bot_connect(self, bot: \"Bot\") -> None:\n        \"\"\"在连接成功后，调用该函数来注册 bot 对象\"\"\"\n        if bot.self_id in self._bots:\n            raise RuntimeError(f\"Duplicate bot connection with id {bot.self_id}\")\n        self._bots[bot.self_id] = bot\n\n        if not self._bot_connection_hook:\n            return\n\n        def handle_exception(exc_group: BaseExceptionGroup) -> None:\n            for exc in flatten_exception_group(exc_group):\n                logger.opt(colors=True, exception=exc).error(\n                    \"<r><bg #f8bbd0>\"\n                    \"Error when running WebSocketConnection hook:\"\n                    \"</bg #f8bbd0></r>\"\n                )\n\n        async def _run_hook(bot: \"Bot\") -> None:\n            dependency_cache: T_DependencyCache = {}\n            with CancelScope(shield=True), catch({Exception: handle_exception}):\n                async with AsyncExitStack() as stack, create_task_group() as tg:\n                    for hook in self._bot_connection_hook:\n                        tg.start_soon(\n                            run_coro_with_catch,\n                            hook(\n                                bot=bot, stack=stack, dependency_cache=dependency_cache\n                            ),\n                            (SkippedException,),\n                        )\n\n        self.task_group.start_soon(_run_hook, bot)\n\n    def _bot_disconnect(self, bot: \"Bot\") -> None:\n        \"\"\"在连接断开后，调用该函数来注销 bot 对象\"\"\"\n        if bot.self_id in self._bots:\n            del self._bots[bot.self_id]\n\n        if not self._bot_disconnection_hook:\n            return\n\n        def handle_exception(exc_group: BaseExceptionGroup) -> None:\n            for exc in flatten_exception_group(exc_group):\n                logger.opt(colors=True, exception=exc).error(\n                    \"<r><bg #f8bbd0>\"\n                    \"Error when running WebSocketDisConnection hook:\"\n                    \"</bg #f8bbd0></r>\"\n                )\n\n        async def _run_hook(bot: \"Bot\") -> None:\n            dependency_cache: T_DependencyCache = {}\n            # shield cancellation to ensure bot disconnect hooks are always run\n            with CancelScope(shield=True), catch({Exception: handle_exception}):\n                async with create_task_group() as tg, AsyncExitStack() as stack:\n                    for hook in self._bot_disconnection_hook:\n                        tg.start_soon(\n                            run_coro_with_catch,\n                            hook(\n                                bot=bot, stack=stack, dependency_cache=dependency_cache\n                            ),\n                            (SkippedException,),\n                        )\n\n        self.task_group.start_soon(_run_hook, bot)\n\n\nclass Mixin(abc.ABC):\n    \"\"\"可与其他驱动器共用的混入基类。\"\"\"\n\n    @property\n    @abc.abstractmethod\n    def type(self) -> str:\n        \"\"\"混入驱动类型名称\"\"\"\n        raise NotImplementedError\n\n\nclass ForwardMixin(Mixin):\n    \"\"\"客户端混入基类。\"\"\"\n\n\nclass ReverseMixin(Mixin):\n    \"\"\"服务端混入基类。\"\"\"\n\n\nclass HTTPClientSession(abc.ABC):\n    \"\"\"HTTP 客户端会话基类。\"\"\"\n\n    @abc.abstractmethod\n    def __init__(\n        self,\n        params: QueryTypes = None,\n        headers: HeaderTypes = None,\n        cookies: CookieTypes = None,\n        version: str | HTTPVersion = HTTPVersion.H11,\n        timeout: TimeoutTypes = None,\n        proxy: str | None = None,\n    ):\n        raise NotImplementedError\n\n    @abc.abstractmethod\n    async def request(self, setup: Request) -> Response:\n        \"\"\"发送一个 HTTP 请求\"\"\"\n        raise NotImplementedError\n\n    @abc.abstractmethod\n    async def stream_request(\n        self,\n        setup: Request,\n        *,\n        chunk_size: int = 1024,\n    ) -> AsyncGenerator[Response, None]:\n        \"\"\"发送一个 HTTP 流式请求\"\"\"\n        raise NotImplementedError\n        yield  # used for static type checking's generator detection\n\n    @abc.abstractmethod\n    async def setup(self) -> None:\n        \"\"\"初始化会话\"\"\"\n        raise NotImplementedError\n\n    @abc.abstractmethod\n    async def close(self) -> None:\n        \"\"\"关闭会话\"\"\"\n        raise NotImplementedError\n\n    async def __aenter__(self) -> Self:\n        await self.setup()\n        return self\n\n    async def __aexit__(\n        self,\n        exc_type: type[BaseException] | None,\n        exc: BaseException | None,\n        tb: TracebackType | None,\n    ) -> None:\n        await self.close()\n\n\nclass HTTPClientMixin(ForwardMixin):\n    \"\"\"HTTP 客户端混入基类。\"\"\"\n\n    @abc.abstractmethod\n    async def request(self, setup: Request) -> Response:\n        \"\"\"发送一个 HTTP 请求\"\"\"\n        raise NotImplementedError\n\n    @abc.abstractmethod\n    async def stream_request(\n        self,\n        setup: Request,\n        *,\n        chunk_size: int = 1024,\n    ) -> AsyncGenerator[Response, None]:\n        \"\"\"发送一个 HTTP 流式请求\"\"\"\n        raise NotImplementedError\n        yield  # used for static type checking's generator detection\n\n    @abc.abstractmethod\n    def get_session(\n        self,\n        params: QueryTypes = None,\n        headers: HeaderTypes = None,\n        cookies: CookieTypes = None,\n        version: str | HTTPVersion = HTTPVersion.H11,\n        timeout: TimeoutTypes = None,\n        proxy: str | None = None,\n    ) -> HTTPClientSession:\n        \"\"\"获取一个 HTTP 会话\"\"\"\n        raise NotImplementedError\n\n\nclass WebSocketClientMixin(ForwardMixin):\n    \"\"\"WebSocket 客户端混入基类。\"\"\"\n\n    @abc.abstractmethod\n    @asynccontextmanager\n    async def websocket(self, setup: Request) -> AsyncGenerator[WebSocket, None]:\n        \"\"\"发起一个 WebSocket 连接\"\"\"\n        raise NotImplementedError\n        yield  # used for static type checking's generator detection\n\n\nclass ASGIMixin(ReverseMixin):\n    \"\"\"ASGI 服务端基类。\n\n    将后端框架封装，以满足适配器使用。\n    \"\"\"\n\n    @property\n    @abc.abstractmethod\n    def server_app(self) -> Any:\n        \"\"\"驱动 APP 对象\"\"\"\n        raise NotImplementedError\n\n    @property\n    @abc.abstractmethod\n    def asgi(self) -> Any:\n        \"\"\"驱动 ASGI 对象\"\"\"\n        raise NotImplementedError\n\n    @abc.abstractmethod\n    def setup_http_server(self, setup: \"HTTPServerSetup\") -> None:\n        \"\"\"设置一个 HTTP 服务器路由配置\"\"\"\n        raise NotImplementedError\n\n    @abc.abstractmethod\n    def setup_websocket_server(self, setup: \"WebSocketServerSetup\") -> None:\n        \"\"\"设置一个 WebSocket 服务器路由配置\"\"\"\n        raise NotImplementedError\n\n\nForwardDriver: TypeAlias = ForwardMixin\n\"\"\"支持客户端请求的驱动器。\n\n**Deprecated**，请使用 {ref}`nonebot.drivers.ForwardMixin` 或其子类代替。\n\"\"\"\n\nReverseDriver: TypeAlias = ReverseMixin\n\"\"\"支持服务端请求的驱动器。\n\n**Deprecated**，请使用 {ref}`nonebot.drivers.ReverseMixin` 或其子类代替。\n\"\"\"\n"
  },
  {
    "path": "nonebot/internal/driver/combine.py",
    "content": "from typing import TYPE_CHECKING, TypeVar, overload\n\nfrom .abstract import Driver, Mixin\n\nD = TypeVar(\"D\", bound=\"Driver\")\n\nif TYPE_CHECKING:\n\n    class CombinedDriver(Driver, Mixin): ...\n\n\n@overload\ndef combine_driver(driver: type[D]) -> type[D]: ...\n\n\n@overload\ndef combine_driver(\n    driver: type[D], __m: type[Mixin], /, *mixins: type[Mixin]\n) -> type[\"CombinedDriver\"]: ...\n\n\ndef combine_driver(\n    driver: type[D], *mixins: type[Mixin]\n) -> type[D] | type[\"CombinedDriver\"]:\n    \"\"\"将一个驱动器和多个混入类合并。\"\"\"\n    # check first\n    if not issubclass(driver, Driver):\n        raise TypeError(\"`driver` must be subclass of Driver\")\n    if not all(issubclass(m, Mixin) for m in mixins):\n        raise TypeError(\"`mixins` must be subclass of Mixin\")\n\n    if not mixins:\n        return driver\n\n    def type_(self: \"CombinedDriver\") -> str:\n        return (\n            driver.type.__get__(self)  # type: ignore\n            + \"+\"\n            + \"+\".join(x.type.__get__(self) for x in mixins)  # type: ignore\n        )\n\n    return type(\"CombinedDriver\", (*mixins, driver), {\"type\": property(type_)})  # type: ignore\n"
  },
  {
    "path": "nonebot/internal/driver/model.py",
    "content": "import abc\nfrom collections.abc import Awaitable, Callable, Iterator, Mapping, MutableMapping\nfrom dataclasses import dataclass\nfrom enum import Enum\nfrom http.cookiejar import Cookie, CookieJar\nfrom typing import IO, Any, TypeAlias\nimport urllib.request\n\nfrom multidict import CIMultiDict\nfrom yarl import URL as URL\n\n\n@dataclass\nclass Timeout:\n    \"\"\"Request 超时配置。\"\"\"\n\n    total: float | None = None\n    connect: float | None = None\n    read: float | None = None\n\n\nRawURL: TypeAlias = tuple[bytes, bytes, int | None, bytes]\n\nSimpleQuery: TypeAlias = str | int | float\nQueryVariable: TypeAlias = SimpleQuery | list[SimpleQuery]\nQueryTypes: TypeAlias = (\n    None | str | Mapping[str, QueryVariable] | list[tuple[str, SimpleQuery]]\n)\n\nHeaderTypes: TypeAlias = (\n    None | CIMultiDict[str] | dict[str, str] | list[tuple[str, str]]\n)\n\nCookieTypes: TypeAlias = (\n    \"None | Cookies | CookieJar | dict[str, str] | list[tuple[str, str]]\"\n)\n\nContentTypes: TypeAlias = str | bytes | None\nDataTypes: TypeAlias = dict | None\nFileContent: TypeAlias = IO[bytes] | bytes\nFileType: TypeAlias = tuple[str | None, FileContent, str | None]\nFileTypes: TypeAlias = (\n    FileContent  # file (or bytes)\n    | tuple[str | None, FileContent]  # (filename, file (or bytes))\n    | FileType  # (filename, file (or bytes), content_type)\n)\nFilesTypes: TypeAlias = dict[str, FileTypes] | list[tuple[str, FileTypes]] | None\nTimeoutTypes: TypeAlias = float | Timeout | None\n\n\nclass HTTPVersion(Enum):\n    H10 = \"1.0\"\n    H11 = \"1.1\"\n    H2 = \"2\"\n\n\nclass Request:\n    def __init__(\n        self,\n        method: str | bytes,\n        url: \"URL | str | RawURL\",\n        *,\n        params: QueryTypes = None,\n        headers: HeaderTypes = None,\n        cookies: CookieTypes = None,\n        content: ContentTypes = None,\n        data: DataTypes = None,\n        json: Any = None,\n        files: FilesTypes = None,\n        version: str | HTTPVersion = HTTPVersion.H11,\n        timeout: TimeoutTypes = None,\n        proxy: str | None = None,\n    ):\n        # method\n        self.method: str = (\n            method.decode(\"ascii\").upper()\n            if isinstance(method, bytes)\n            else method.upper()\n        )\n        # http version\n        self.version: HTTPVersion = HTTPVersion(version)\n        # timeout\n        self.timeout: TimeoutTypes = timeout\n        # proxy\n        self.proxy: str | None = proxy\n\n        # url\n        if isinstance(url, tuple):\n            scheme, host, port, path = url\n            url = URL.build(\n                scheme=scheme.decode(\"ascii\"),\n                host=host.decode(\"ascii\"),\n                port=port,\n                path=path.decode(\"ascii\"),\n            )\n        else:\n            url = URL(url)\n\n        if params is not None:\n            url = url.update_query(params)\n        self.url: URL = url\n\n        # headers\n        self.headers: CIMultiDict[str] = (\n            CIMultiDict(headers) if headers is not None else CIMultiDict()\n        )\n        # cookies\n        self.cookies = Cookies(cookies)\n\n        # body\n        self.content: ContentTypes = content\n        self.data: DataTypes = data\n        self.json: Any = json\n        self.files: list[tuple[str, FileType]] | None = None\n        if files:\n            self.files = []\n            files_ = files.items() if isinstance(files, dict) else files\n            for name, file_info in files_:\n                if not isinstance(file_info, tuple):\n                    self.files.append((name, (name, file_info, None)))\n                elif len(file_info) == 2:\n                    self.files.append((name, (file_info[0], file_info[1], None)))\n                else:\n                    self.files.append((name, file_info))  # type: ignore\n\n    def __repr__(self) -> str:\n        return f\"{self.__class__.__name__}(method={self.method!r}, url='{self.url!s}')\"\n\n\nclass Response:\n    def __init__(\n        self,\n        status_code: int,\n        *,\n        headers: HeaderTypes = None,\n        content: ContentTypes = None,\n        request: Request | None = None,\n    ):\n        # status code\n        self.status_code: int = status_code\n\n        # headers\n        self.headers: CIMultiDict[str] = (\n            CIMultiDict(headers) if headers is not None else CIMultiDict()\n        )\n        # body\n        self.content: ContentTypes = content\n\n        # request\n        self.request: Request | None = request\n\n    def __repr__(self) -> str:\n        return f\"{self.__class__.__name__}(status_code={self.status_code!r})\"\n\n\nclass WebSocket(abc.ABC):\n    def __init__(self, *, request: Request):\n        self.request: Request = request\n\n    def __repr__(self) -> str:\n        return f\"{self.__class__.__name__}('{self.request.url!s}')\"\n\n    @property\n    @abc.abstractmethod\n    def closed(self) -> bool:\n        \"\"\"连接是否已经关闭\"\"\"\n        raise NotImplementedError\n\n    @abc.abstractmethod\n    async def accept(self) -> None:\n        \"\"\"接受 WebSocket 连接请求\"\"\"\n        raise NotImplementedError\n\n    @abc.abstractmethod\n    async def close(self, code: int = 1000, reason: str = \"\") -> None:\n        \"\"\"关闭 WebSocket 连接请求\"\"\"\n        raise NotImplementedError\n\n    @abc.abstractmethod\n    async def receive(self) -> str | bytes:\n        \"\"\"接收一条 WebSocket text/bytes 信息\"\"\"\n        raise NotImplementedError\n\n    @abc.abstractmethod\n    async def receive_text(self) -> str:\n        \"\"\"接收一条 WebSocket text 信息\"\"\"\n        raise NotImplementedError\n\n    @abc.abstractmethod\n    async def receive_bytes(self) -> bytes:\n        \"\"\"接收一条 WebSocket binary 信息\"\"\"\n        raise NotImplementedError\n\n    async def send(self, data: str | bytes) -> None:\n        \"\"\"发送一条 WebSocket text/bytes 信息\"\"\"\n        if isinstance(data, str):\n            await self.send_text(data)\n        elif isinstance(data, bytes):\n            await self.send_bytes(data)\n        else:\n            raise TypeError(\"WebSocker send method expects str or bytes!\")\n\n    @abc.abstractmethod\n    async def send_text(self, data: str) -> None:\n        \"\"\"发送一条 WebSocket text 信息\"\"\"\n        raise NotImplementedError\n\n    @abc.abstractmethod\n    async def send_bytes(self, data: bytes) -> None:\n        \"\"\"发送一条 WebSocket binary 信息\"\"\"\n        raise NotImplementedError\n\n\nclass Cookies(MutableMapping):\n    def __init__(self, cookies: CookieTypes = None) -> None:\n        self.jar: CookieJar = cookies if isinstance(cookies, CookieJar) else CookieJar()\n        if cookies is not None and not isinstance(cookies, CookieJar):\n            if isinstance(cookies, dict):\n                for key, value in cookies.items():\n                    self.set(key, value)\n            elif isinstance(cookies, list):\n                for key, value in cookies:\n                    self.set(key, value)\n            elif isinstance(cookies, Cookies):\n                for cookie in cookies.jar:\n                    self.jar.set_cookie(cookie)\n            else:\n                raise TypeError(f\"Cookies must be dict or list, not {type(cookies)}\")\n\n    def set(self, name: str, value: str, domain: str = \"\", path: str = \"/\") -> None:\n        cookie = Cookie(\n            version=0,\n            name=name,\n            value=value,\n            port=None,\n            port_specified=False,\n            domain=domain,\n            domain_specified=bool(domain),\n            domain_initial_dot=domain.startswith(\".\"),\n            path=path,\n            path_specified=bool(path),\n            secure=False,\n            expires=None,\n            discard=True,\n            comment=None,\n            comment_url=None,\n            rest={},\n            rfc2109=False,\n        )\n        self.jar.set_cookie(cookie)\n\n    def get(  # pyright: ignore[reportIncompatibleMethodOverride]\n        self,\n        name: str,\n        default: str | None = None,\n        domain: str | None = None,\n        path: str | None = None,\n    ) -> str | None:\n        value: str | None = None\n        for cookie in self.jar:\n            if (\n                cookie.name == name\n                and (domain is None or cookie.domain == domain)\n                and (path is None or cookie.path == path)\n            ):\n                if value is not None:\n                    message = f\"Multiple cookies exist with name={name}\"\n                    raise ValueError(message)\n                value = cookie.value\n\n        return default if value is None else value\n\n    def delete(\n        self, name: str, domain: str | None = None, path: str | None = None\n    ) -> None:\n        if domain is not None and path is not None:\n            return self.jar.clear(domain, path, name)\n\n        remove = [\n            cookie\n            for cookie in self.jar\n            if cookie.name == name\n            and (domain is None or cookie.domain == domain)\n            and (path is None or cookie.path == path)\n        ]\n\n        for cookie in remove:\n            self.jar.clear(cookie.domain, cookie.path, cookie.name)\n\n    def clear(self, domain: str | None = None, path: str | None = None) -> None:\n        self.jar.clear(domain, path)\n\n    def update(  # pyright: ignore[reportIncompatibleMethodOverride]\n        self, cookies: CookieTypes = None\n    ) -> None:\n        cookies = Cookies(cookies)\n        for cookie in cookies.jar:\n            self.jar.set_cookie(cookie)\n\n    def as_header(self, request: Request) -> dict[str, str]:\n        urllib_request = self._CookieCompatRequest(request)\n        self.jar.add_cookie_header(urllib_request)\n        return urllib_request.added_headers\n\n    def __setitem__(self, name: str, value: str) -> None:\n        return self.set(name, value)\n\n    def __getitem__(self, name: str) -> str:\n        value = self.get(name)\n        if value is None:\n            raise KeyError(name)\n        return value\n\n    def __delitem__(self, name: str) -> None:\n        return self.delete(name)\n\n    def __len__(self) -> int:\n        return len(self.jar)\n\n    def __iter__(self) -> Iterator[Cookie]:\n        return iter(self.jar)\n\n    def __repr__(self) -> str:\n        cookies_repr = \", \".join(\n            f\"Cookie({cookie.name}={cookie.value} for {cookie.domain})\"\n            for cookie in self.jar\n        )\n        return f\"{self.__class__.__name__}({cookies_repr})\"\n\n    class _CookieCompatRequest(urllib.request.Request):\n        def __init__(self, request: Request) -> None:\n            super().__init__(\n                url=str(request.url),\n                headers=dict(request.headers),\n                method=request.method,\n            )\n            self.request = request\n            self.added_headers: dict[str, str] = {}\n\n        def add_unredirected_header(  # pyright: ignore[reportIncompatibleMethodOverride]\n            self, key: str, value: str\n        ) -> None:\n            super().add_unredirected_header(key, value)\n            self.added_headers[key] = value\n\n\n@dataclass\nclass HTTPServerSetup:\n    \"\"\"HTTP 服务器路由配置。\"\"\"\n\n    path: URL  # path should not be absolute, check it by URL.is_absolute() == False\n    method: str\n    name: str\n    handle_func: Callable[[Request], Awaitable[Response]]\n\n\n@dataclass\nclass WebSocketServerSetup:\n    \"\"\"WebSocket 服务器路由配置。\"\"\"\n\n    path: URL  # path should not be absolute, check it by URL.is_absolute() == False\n    name: str\n    handle_func: Callable[[WebSocket], Awaitable[Any]]\n"
  },
  {
    "path": "nonebot/internal/matcher/__init__.py",
    "content": "from .manager import MatcherManager as MatcherManager\nfrom .provider import DEFAULT_PROVIDER_CLASS as DEFAULT_PROVIDER_CLASS\nfrom .provider import MatcherProvider as MatcherProvider\n\nmatchers = MatcherManager()\n\nfrom .matcher import Matcher as Matcher\nfrom .matcher import MatcherSource as MatcherSource\nfrom .matcher import current_bot as current_bot\nfrom .matcher import current_event as current_event\nfrom .matcher import current_handler as current_handler\nfrom .matcher import current_matcher as current_matcher\n"
  },
  {
    "path": "nonebot/internal/matcher/manager.py",
    "content": "from collections.abc import ItemsView, Iterator, KeysView, MutableMapping, ValuesView\nfrom typing import TYPE_CHECKING, TypeVar, overload\n\nfrom .provider import DEFAULT_PROVIDER_CLASS, MatcherProvider\n\nif TYPE_CHECKING:\n    from .matcher import Matcher\n\nT = TypeVar(\"T\")\n\n\nclass MatcherManager(MutableMapping[int, list[type[\"Matcher\"]]]):\n    \"\"\"事件响应器管理器\n\n    实现了常用字典操作，用于管理事件响应器。\n    \"\"\"\n\n    def __init__(self):\n        self.provider: MatcherProvider = DEFAULT_PROVIDER_CLASS({})\n\n    def __repr__(self) -> str:\n        return f\"MatcherManager(provider={self.provider!r})\"\n\n    def __contains__(self, o: object) -> bool:\n        return o in self.provider\n\n    def __iter__(self) -> Iterator[int]:\n        return iter(self.provider)\n\n    def __len__(self) -> int:\n        return len(self.provider)\n\n    def __getitem__(self, key: int) -> list[type[\"Matcher\"]]:\n        return self.provider[key]\n\n    def __setitem__(self, key: int, value: list[type[\"Matcher\"]]) -> None:\n        self.provider[key] = value\n\n    def __delitem__(self, key: int) -> None:\n        del self.provider[key]\n\n    def __eq__(self, other: object) -> bool:\n        return isinstance(other, MatcherManager) and self.provider == other.provider\n\n    def keys(self) -> KeysView[int]:\n        return self.provider.keys()\n\n    def values(self) -> ValuesView[list[type[\"Matcher\"]]]:\n        return self.provider.values()\n\n    def items(self) -> ItemsView[int, list[type[\"Matcher\"]]]:\n        return self.provider.items()\n\n    @overload\n    def get(self, key: int) -> list[type[\"Matcher\"]] | None: ...\n\n    @overload\n    def get(\n        self, key: int, default: list[type[\"Matcher\"]]\n    ) -> list[type[\"Matcher\"]]: ...\n\n    @overload\n    def get(self, key: int, default: T) -> list[type[\"Matcher\"]] | T: ...\n\n    def get(\n        self, key: int, default: T | None = None\n    ) -> list[type[\"Matcher\"]] | T | None:\n        return self.provider.get(key, default)\n\n    def pop(  # pyright: ignore[reportIncompatibleMethodOverride]\n        self, key: int\n    ) -> list[type[\"Matcher\"]]:\n        return self.provider.pop(key)\n\n    def popitem(self) -> tuple[int, list[type[\"Matcher\"]]]:\n        return self.provider.popitem()\n\n    def clear(self) -> None:\n        self.provider.clear()\n\n    def update(  # pyright: ignore[reportIncompatibleMethodOverride]\n        self, m: MutableMapping[int, list[type[\"Matcher\"]]], /\n    ) -> None:\n        self.provider.update(m)\n\n    def setdefault(\n        self, key: int, default: list[type[\"Matcher\"]]\n    ) -> list[type[\"Matcher\"]]:\n        return self.provider.setdefault(key, default)\n\n    def set_provider(self, provider_class: type[MatcherProvider]) -> None:\n        \"\"\"设置事件响应器存储器\n\n        参数:\n            provider_class: 事件响应器存储器类\n        \"\"\"\n        self.provider = provider_class(self.provider)\n"
  },
  {
    "path": "nonebot/internal/matcher/matcher.py",
    "content": "from collections.abc import Iterable\nfrom contextlib import AsyncExitStack, contextmanager\nfrom contextvars import ContextVar\nfrom dataclasses import dataclass\nfrom datetime import datetime, timedelta\nimport inspect\nfrom pathlib import Path\nimport sys\nfrom types import ModuleType\nfrom typing import (  # noqa: UP035\n    TYPE_CHECKING,\n    Any,\n    Callable,\n    ClassVar,\n    NoReturn,\n    Type,\n    TypeVar,\n    overload,\n)\nfrom typing_extensions import Self\nimport warnings\n\nfrom exceptiongroup import BaseExceptionGroup, catch\n\nfrom nonebot.consts import (\n    ARG_KEY,\n    LAST_RECEIVE_KEY,\n    PAUSE_PROMPT_RESULT_KEY,\n    RECEIVE_KEY,\n    REJECT_CACHE_TARGET,\n    REJECT_PROMPT_RESULT_KEY,\n    REJECT_TARGET,\n)\nfrom nonebot.dependencies import Dependent, Param\nfrom nonebot.exception import (\n    FinishedException,\n    PausedException,\n    RejectedException,\n    SkippedException,\n    StopPropagation,\n)\nfrom nonebot.internal.adapter import (\n    Bot,\n    Event,\n    Message,\n    MessageSegment,\n    MessageTemplate,\n)\nfrom nonebot.internal.params import (\n    ArgParam,\n    BotParam,\n    DefaultParam,\n    DependParam,\n    Depends,\n    EventParam,\n    MatcherParam,\n    StateParam,\n)\nfrom nonebot.internal.permission import Permission, User\nfrom nonebot.internal.rule import Rule\nfrom nonebot.log import logger\nfrom nonebot.typing import (\n    T_DependencyCache,\n    T_Handler,\n    T_PermissionUpdater,\n    T_State,\n    T_TypeUpdater,\n)\nfrom nonebot.utils import classproperty, flatten_exception_group\n\nfrom . import matchers\n\nif TYPE_CHECKING:\n    from nonebot.plugin import Plugin\n\nT = TypeVar(\"T\")\n\ncurrent_bot: ContextVar[Bot] = ContextVar(\"current_bot\")\ncurrent_event: ContextVar[Event] = ContextVar(\"current_event\")\ncurrent_matcher: ContextVar[\"Matcher\"] = ContextVar(\"current_matcher\")\ncurrent_handler: ContextVar[Dependent[Any]] = ContextVar(\"current_handler\")\n\n\n@dataclass\nclass MatcherSource:\n    \"\"\"Matcher 源代码上下文信息\"\"\"\n\n    plugin_id: str | None = None\n    \"\"\"事件响应器所在插件标识符\"\"\"\n    module_name: str | None = None\n    \"\"\"事件响应器所在插件模块的路径名\"\"\"\n    lineno: int | None = None\n    \"\"\"事件响应器所在行号\"\"\"\n\n    @property\n    def plugin(self) -> \"Plugin | None\":\n        \"\"\"事件响应器所在插件\"\"\"\n        from nonebot.plugin import get_plugin\n\n        if self.plugin_id is not None:\n            return get_plugin(self.plugin_id)\n\n    @property\n    def plugin_name(self) -> str | None:\n        \"\"\"事件响应器所在插件名\"\"\"\n        return self.plugin and self.plugin.name\n\n    @property\n    def module(self) -> ModuleType | None:\n        if self.module_name is not None:\n            return sys.modules.get(self.module_name)\n\n    @property\n    def file(self) -> Path | None:\n        if self.module is not None and (file := inspect.getsourcefile(self.module)):\n            return Path(file).absolute()\n\n\nclass MatcherMeta(type):\n    if TYPE_CHECKING:\n        type: str\n        _source: MatcherSource | None\n        module_name: str | None\n\n    def __repr__(self) -> str:\n        return (\n            f\"{self.__name__}(type={self.type!r}\"\n            + (f\", module={self.module_name}\" if self.module_name else \"\")\n            + (\n                f\", lineno={self._source.lineno}\"\n                if self._source and self._source.lineno is not None\n                else \"\"\n            )\n            + \")\"\n        )\n\n\nclass Matcher(metaclass=MatcherMeta):\n    \"\"\"事件响应器类\"\"\"\n\n    _source: ClassVar[MatcherSource | None] = None\n\n    type: ClassVar[str] = \"\"\n    \"\"\"事件响应器类型\"\"\"\n    rule: ClassVar[Rule] = Rule()\n    \"\"\"事件响应器匹配规则\"\"\"\n    permission: ClassVar[Permission] = Permission()\n    \"\"\"事件响应器触发权限\"\"\"\n    handlers: ClassVar[list[Dependent[Any]]] = []\n    \"\"\"事件响应器拥有的事件处理函数列表\"\"\"\n    priority: ClassVar[int] = 1\n    \"\"\"事件响应器优先级\"\"\"\n    block: bool = False\n    \"\"\"事件响应器是否阻止事件传播\"\"\"\n    temp: ClassVar[bool] = False\n    \"\"\"事件响应器是否为临时\"\"\"\n    expire_time: ClassVar[datetime | None] = None\n    \"\"\"事件响应器过期时间点\"\"\"\n\n    _default_state: ClassVar[T_State] = {}\n    \"\"\"事件响应器默认状态\"\"\"\n\n    _default_type_updater: ClassVar[Dependent[str] | None] = None\n    \"\"\"事件响应器类型更新函数\"\"\"\n    _default_permission_updater: ClassVar[Dependent[Permission] | None] = None\n    \"\"\"事件响应器权限更新函数\"\"\"\n\n    HANDLER_PARAM_TYPES: ClassVar[tuple[Type[Param], ...]] = (  # noqa: UP006\n        DependParam,\n        BotParam,\n        EventParam,\n        StateParam,\n        ArgParam,\n        MatcherParam,\n        DefaultParam,\n    )\n\n    def __init__(self):\n        self.remain_handlers: list[Dependent[Any]] = self.handlers.copy()\n        self.state = self._default_state.copy()\n\n    def __repr__(self) -> str:\n        return (\n            f\"{self.__class__.__name__}(type={self.type!r}\"\n            + (f\", module={self.module_name}\" if self.module_name else \"\")\n            + (\n                f\", lineno={self._source.lineno}\"\n                if self._source and self._source.lineno is not None\n                else \"\"\n            )\n            + \")\"\n        )\n\n    @classmethod\n    def new(\n        cls,\n        type_: str = \"\",\n        rule: Rule | None = None,\n        permission: Permission | None = None,\n        handlers: list[T_Handler | Dependent[Any]] | None = None,\n        temp: bool = False,\n        priority: int = 1,\n        block: bool = False,\n        *,\n        plugin: \"Plugin | None\" = None,\n        module: ModuleType | None = None,\n        source: MatcherSource | None = None,\n        expire_time: datetime | timedelta | None = None,\n        default_state: T_State | None = None,\n        default_type_updater: T_TypeUpdater | Dependent[str] | None = None,\n        default_permission_updater: T_PermissionUpdater\n        | Dependent[Permission]\n        | None = None,\n    ) -> Type[Self]:  # noqa: UP006\n        \"\"\"\n        创建一个新的事件响应器，并存储至 `matchers <#matchers>`_\n\n        参数:\n            type_: 事件响应器类型，与 `event.get_type()` 一致时触发，空字符串表示任意\n            rule: 匹配规则\n            permission: 权限\n            handlers: 事件处理函数列表\n            temp: 是否为临时事件响应器，即触发一次后删除\n            priority: 响应优先级\n            block: 是否阻止事件向更低优先级的响应器传播\n            plugin: **Deprecated.** 事件响应器所在插件\n            module: **Deprecated.** 事件响应器所在模块\n            source: 事件响应器源代码上下文信息\n            expire_time: 事件响应器最终有效时间点，过时即被删除\n            default_state: 默认状态 `state`\n            default_type_updater: 默认事件类型更新函数\n            default_permission_updater: 默认会话权限更新函数\n\n        返回:\n            Type[Matcher]: 新的事件响应器类\n        \"\"\"\n        if plugin is not None:\n            warnings.warn(\n                (\n                    \"Pass `plugin` context info to create Matcher is deprecated. \"\n                    \"Use `source` instead.\"\n                ),\n                DeprecationWarning,\n            )\n        if module is not None:\n            warnings.warn(\n                (\n                    \"Pass `module` context info to create Matcher is deprecated. \"\n                    \"Use `source` instead.\"\n                ),\n                DeprecationWarning,\n            )\n        source = source or (\n            MatcherSource(\n                plugin_id=plugin and plugin.id_,\n                module_name=module and module.__name__,\n            )\n            if plugin is not None or module is not None\n            else None\n        )\n\n        NewMatcher = type(\n            cls.__name__,\n            (cls,),\n            {\n                \"_source\": source,\n                \"type\": type_,\n                \"rule\": rule or Rule(),\n                \"permission\": permission or Permission(),\n                \"handlers\": (\n                    [\n                        (\n                            handler\n                            if isinstance(handler, Dependent)\n                            else Dependent[Any].parse(\n                                call=handler, allow_types=cls.HANDLER_PARAM_TYPES\n                            )\n                        )\n                        for handler in handlers\n                    ]\n                    if handlers\n                    else []\n                ),\n                \"temp\": temp,\n                \"expire_time\": (\n                    expire_time\n                    and (\n                        expire_time\n                        if isinstance(expire_time, datetime)\n                        else datetime.now() + expire_time\n                    )\n                ),\n                \"priority\": priority,\n                \"block\": block,\n                \"_default_state\": default_state or {},\n                \"_default_type_updater\": (\n                    default_type_updater\n                    and (\n                        default_type_updater\n                        if isinstance(default_type_updater, Dependent)\n                        else Dependent[str].parse(\n                            call=default_type_updater,\n                            allow_types=cls.HANDLER_PARAM_TYPES,\n                        )\n                    )\n                ),\n                \"_default_permission_updater\": (\n                    default_permission_updater\n                    and (\n                        default_permission_updater\n                        if isinstance(default_permission_updater, Dependent)\n                        else Dependent[Permission].parse(\n                            call=default_permission_updater,\n                            allow_types=cls.HANDLER_PARAM_TYPES,\n                        )\n                    )\n                ),\n            },\n        )\n\n        logger.trace(f\"Define new matcher {NewMatcher}\")\n\n        matchers[priority].append(NewMatcher)\n\n        return NewMatcher  # type: ignore\n\n    @classmethod\n    def destroy(cls) -> None:\n        \"\"\"销毁当前的事件响应器\"\"\"\n        matchers[cls.priority].remove(cls)\n\n    @classproperty\n    def plugin(cls) -> \"Plugin | None\":\n        \"\"\"事件响应器所在插件\"\"\"\n        return cls._source and cls._source.plugin\n\n    @classproperty\n    def plugin_id(cls) -> str | None:\n        \"\"\"事件响应器所在插件标识符\"\"\"\n        return cls._source and cls._source.plugin_id\n\n    @classproperty\n    def plugin_name(cls) -> str | None:\n        \"\"\"事件响应器所在插件名\"\"\"\n        return cls._source and cls._source.plugin_name\n\n    @classproperty\n    def module(cls) -> ModuleType | None:\n        \"\"\"事件响应器所在插件模块\"\"\"\n        return cls._source and cls._source.module\n\n    @classproperty\n    def module_name(cls) -> str | None:\n        \"\"\"事件响应器所在插件模块路径\"\"\"\n        return cls._source and cls._source.module_name\n\n    @classmethod\n    async def check_perm(\n        cls,\n        bot: Bot,\n        event: Event,\n        stack: AsyncExitStack | None = None,\n        dependency_cache: T_DependencyCache | None = None,\n    ) -> bool:\n        \"\"\"检查是否满足触发权限\n\n        参数:\n            bot: Bot 对象\n            event: 上报事件\n            stack: 异步上下文栈\n            dependency_cache: 依赖缓存\n\n        返回:\n            是否满足权限\n        \"\"\"\n        event_type = event.get_type()\n        return event_type == (cls.type or event_type) and await cls.permission(\n            bot, event, stack, dependency_cache\n        )\n\n    @classmethod\n    async def check_rule(\n        cls,\n        bot: Bot,\n        event: Event,\n        state: T_State,\n        stack: AsyncExitStack | None = None,\n        dependency_cache: T_DependencyCache | None = None,\n    ) -> bool:\n        \"\"\"检查是否满足匹配规则\n\n        参数:\n            bot: Bot 对象\n            event: 上报事件\n            state: 当前状态\n            stack: 异步上下文栈\n            dependency_cache: 依赖缓存\n\n        返回:\n            是否满足匹配规则\n        \"\"\"\n        event_type = event.get_type()\n        return event_type == (cls.type or event_type) and await cls.rule(\n            bot, event, state, stack, dependency_cache\n        )\n\n    @classmethod\n    def type_updater(cls, func: T_TypeUpdater) -> T_TypeUpdater:\n        \"\"\"装饰一个函数来更改当前事件响应器的默认响应事件类型更新函数\n\n        参数:\n            func: 响应事件类型更新函数\n        \"\"\"\n        cls._default_type_updater = Dependent[str].parse(\n            call=func, allow_types=cls.HANDLER_PARAM_TYPES\n        )\n        return func\n\n    @classmethod\n    def permission_updater(cls, func: T_PermissionUpdater) -> T_PermissionUpdater:\n        \"\"\"装饰一个函数来更改当前事件响应器的默认会话权限更新函数\n\n        参数:\n            func: 会话权限更新函数\n        \"\"\"\n        cls._default_permission_updater = Dependent[Permission].parse(\n            call=func, allow_types=cls.HANDLER_PARAM_TYPES\n        )\n        return func\n\n    @classmethod\n    def append_handler(\n        cls, handler: T_Handler, parameterless: Iterable[Any] | None = None\n    ) -> Dependent[Any]:\n        handler_ = Dependent[Any].parse(\n            call=handler,\n            parameterless=parameterless,\n            allow_types=cls.HANDLER_PARAM_TYPES,\n        )\n        cls.handlers.append(handler_)\n        return handler_\n\n    @classmethod\n    def handle(\n        cls, parameterless: Iterable[Any] | None = None\n    ) -> Callable[[T_Handler], T_Handler]:\n        \"\"\"装饰一个函数来向事件响应器直接添加一个处理函数\n\n        参数:\n            parameterless: 非参数类型依赖列表\n        \"\"\"\n\n        def _decorator(func: T_Handler) -> T_Handler:\n            cls.append_handler(func, parameterless=parameterless)\n            return func\n\n        return _decorator\n\n    @classmethod\n    def receive(\n        cls, id: str = \"\", parameterless: Iterable[Any] | None = None\n    ) -> Callable[[T_Handler], T_Handler]:\n        \"\"\"装饰一个函数来指示 NoneBot 在接收用户新的一条消息后继续运行该函数\n\n        参数:\n            id: 消息 ID\n            parameterless: 非参数类型依赖列表\n        \"\"\"\n\n        async def _receive(event: Event, matcher: \"Matcher\") -> None:\n            matcher.set_target(RECEIVE_KEY.format(id=id))\n            if matcher.get_target() == RECEIVE_KEY.format(id=id):\n                matcher.set_receive(id, event)\n                return\n            if matcher.get_receive(id, ...) is not ...:\n                return\n            await matcher.reject()\n\n        _parameterless = (Depends(_receive), *(parameterless or ()))\n\n        def _decorator(func: T_Handler) -> T_Handler:\n            if cls.handlers and cls.handlers[-1].call is func:\n                func_handler = cls.handlers[-1]\n                new_handler = Dependent(\n                    call=func_handler.call,\n                    params=func_handler.params,\n                    parameterless=Dependent.parse_parameterless(\n                        tuple(_parameterless), cls.HANDLER_PARAM_TYPES\n                    )\n                    + func_handler.parameterless,\n                )\n                cls.handlers[-1] = new_handler\n            else:\n                cls.append_handler(func, parameterless=_parameterless)\n\n            return func\n\n        return _decorator\n\n    @classmethod\n    def got(\n        cls,\n        key: str,\n        prompt: str | Message | MessageSegment | MessageTemplate | None = None,\n        parameterless: Iterable[Any] | None = None,\n    ) -> Callable[[T_Handler], T_Handler]:\n        \"\"\"装饰一个函数来指示 NoneBot 获取一个参数 `key`\n\n        当要获取的 `key` 不存在时接收用户新的一条消息再运行该函数，\n        如果 `key` 已存在则直接继续运行\n\n        参数:\n            key: 参数名\n            prompt: 在参数不存在时向用户发送的消息\n            parameterless: 非参数类型依赖列表\n        \"\"\"\n\n        async def _key_getter(event: Event, matcher: \"Matcher\"):\n            matcher.set_target(ARG_KEY.format(key=key))\n            if matcher.get_target() == ARG_KEY.format(key=key):\n                matcher.set_arg(key, event.get_message())\n                return\n            if matcher.get_arg(key, ...) is not ...:\n                return\n            await matcher.reject(prompt)\n\n        _parameterless = (Depends(_key_getter), *(parameterless or ()))\n\n        def _decorator(func: T_Handler) -> T_Handler:\n            if cls.handlers and cls.handlers[-1].call is func:\n                func_handler = cls.handlers[-1]\n                new_handler = Dependent(\n                    call=func_handler.call,\n                    params=func_handler.params,\n                    parameterless=Dependent.parse_parameterless(\n                        tuple(_parameterless), cls.HANDLER_PARAM_TYPES\n                    )\n                    + func_handler.parameterless,\n                )\n                cls.handlers[-1] = new_handler\n            else:\n                cls.append_handler(func, parameterless=_parameterless)\n\n            return func\n\n        return _decorator\n\n    @classmethod\n    async def send(\n        cls,\n        message: str | Message | MessageSegment | MessageTemplate,\n        **kwargs: Any,\n    ) -> Any:\n        \"\"\"发送一条消息给当前交互用户\n\n        参数:\n            message: 消息内容\n            kwargs: {ref}`nonebot.adapters.Bot.send` 的参数，\n                请参考对应 adapter 的 bot 对象 api\n        \"\"\"\n        bot = current_bot.get()\n        event = current_event.get()\n        if isinstance(message, MessageTemplate):\n            state = current_matcher.get().state\n            _message = message.format(**state)\n        else:\n            _message = message\n        return await bot.send(event=event, message=_message, **kwargs)\n\n    @classmethod\n    async def finish(\n        cls,\n        message: str | Message | MessageSegment | MessageTemplate | None = None,\n        **kwargs,\n    ) -> NoReturn:\n        \"\"\"发送一条消息给当前交互用户并结束当前事件响应器\n\n        参数:\n            message: 消息内容\n            kwargs: {ref}`nonebot.adapters.Bot.send` 的参数，\n                请参考对应 adapter 的 bot 对象 api\n        \"\"\"\n        if message is not None:\n            await cls.send(message, **kwargs)\n        raise FinishedException\n\n    @classmethod\n    async def pause(\n        cls,\n        prompt: str | Message | MessageSegment | MessageTemplate | None = None,\n        **kwargs,\n    ) -> NoReturn:\n        \"\"\"发送一条消息给当前交互用户并暂停事件响应器，在接收用户新的一条消息后继续下一个处理函数\n\n        参数:\n            prompt: 消息内容\n            kwargs: {ref}`nonebot.adapters.Bot.send` 的参数，\n                请参考对应 adapter 的 bot 对象 api\n        \"\"\"\n        try:\n            matcher = current_matcher.get()\n        except Exception:\n            matcher = None\n\n        if prompt is not None:\n            result = await cls.send(prompt, **kwargs)\n            if matcher is not None:\n                matcher.state[PAUSE_PROMPT_RESULT_KEY] = result\n        raise PausedException\n\n    @classmethod\n    async def reject(\n        cls,\n        prompt: str | Message | MessageSegment | MessageTemplate | None = None,\n        **kwargs,\n    ) -> NoReturn:\n        \"\"\"最近使用 `got` / `receive` 接收的消息不符合预期，\n        发送一条消息给当前交互用户并将当前事件处理流程中断在当前位置，在接收用户新的一个事件后从头开始执行当前处理函数\n\n        参数:\n            prompt: 消息内容\n            kwargs: {ref}`nonebot.adapters.Bot.send` 的参数，\n                请参考对应 adapter 的 bot 对象 api\n        \"\"\"\n        try:\n            matcher = current_matcher.get()\n            key = matcher.get_target()\n        except Exception:\n            matcher = None\n            key = None\n\n        key = REJECT_PROMPT_RESULT_KEY.format(key=key) if key is not None else None\n\n        if prompt is not None:\n            result = await cls.send(prompt, **kwargs)\n            if key is not None and matcher:\n                matcher.state[key] = result\n        raise RejectedException\n\n    @classmethod\n    async def reject_arg(\n        cls,\n        key: str,\n        prompt: str | Message | MessageSegment | MessageTemplate | None = None,\n        **kwargs,\n    ) -> NoReturn:\n        \"\"\"最近使用 `got` 接收的消息不符合预期，\n        发送一条消息给当前交互用户并将当前事件处理流程中断在当前位置，在接收用户新的一条消息后从头开始执行当前处理函数\n\n        参数:\n            key: 参数名\n            prompt: 消息内容\n            kwargs: {ref}`nonebot.adapters.Bot.send` 的参数，\n                请参考对应 adapter 的 bot 对象 api\n        \"\"\"\n        matcher = current_matcher.get()\n        arg_key = ARG_KEY.format(key=key)\n        matcher.set_target(arg_key)\n\n        if prompt is not None:\n            result = await cls.send(prompt, **kwargs)\n            matcher.state[REJECT_PROMPT_RESULT_KEY.format(key=arg_key)] = result\n        raise RejectedException\n\n    @classmethod\n    async def reject_receive(\n        cls,\n        id: str = \"\",\n        prompt: str | Message | MessageSegment | MessageTemplate | None = None,\n        **kwargs,\n    ) -> NoReturn:\n        \"\"\"最近使用 `receive` 接收的消息不符合预期，\n        发送一条消息给当前交互用户并将当前事件处理流程中断在当前位置，在接收用户新的一个事件后从头开始执行当前处理函数\n\n        参数:\n            id: 消息 id\n            prompt: 消息内容\n            kwargs: {ref}`nonebot.adapters.Bot.send` 的参数，\n                请参考对应 adapter 的 bot 对象 api\n        \"\"\"\n        matcher = current_matcher.get()\n        receive_key = RECEIVE_KEY.format(id=id)\n        matcher.set_target(receive_key)\n\n        if prompt is not None:\n            result = await cls.send(prompt, **kwargs)\n            matcher.state[REJECT_PROMPT_RESULT_KEY.format(key=receive_key)] = result\n        raise RejectedException\n\n    @classmethod\n    def skip(cls) -> NoReturn:\n        \"\"\"跳过当前事件处理函数，继续下一个处理函数\n\n        通常在事件处理函数的依赖中使用。\n        \"\"\"\n        raise SkippedException\n\n    @overload\n    def get_receive(self, id: str) -> Event | None: ...\n\n    @overload\n    def get_receive(self, id: str, default: T) -> Event | T: ...\n\n    def get_receive(self, id: str, default: T | None = None) -> Event | T | None:\n        \"\"\"获取一个 `receive` 事件\n\n        如果没有找到对应的事件，返回 `default` 值\n        \"\"\"\n        return self.state.get(RECEIVE_KEY.format(id=id), default)\n\n    def set_receive(self, id: str, event: Event) -> None:\n        \"\"\"设置一个 `receive` 事件\"\"\"\n        self.state[RECEIVE_KEY.format(id=id)] = event\n        self.state[LAST_RECEIVE_KEY] = event\n\n    @overload\n    def get_last_receive(self) -> Event | None: ...\n\n    @overload\n    def get_last_receive(self, default: T) -> Event | T: ...\n\n    def get_last_receive(self, default: T | None = None) -> Event | T | None:\n        \"\"\"获取最近一次 `receive` 事件\n\n        如果没有事件，返回 `default` 值\n        \"\"\"\n        return self.state.get(LAST_RECEIVE_KEY, default)\n\n    @overload\n    def get_arg(self, key: str) -> Message | None: ...\n\n    @overload\n    def get_arg(self, key: str, default: T) -> Message | T: ...\n\n    def get_arg(self, key: str, default: T | None = None) -> Message | T | None:\n        \"\"\"获取一个 `got` 消息\n\n        如果没有找到对应的消息，返回 `default` 值\n        \"\"\"\n        return self.state.get(ARG_KEY.format(key=key), default)\n\n    def set_arg(self, key: str, message: Message) -> None:\n        \"\"\"设置一个 `got` 消息\"\"\"\n        self.state[ARG_KEY.format(key=key)] = message\n\n    def set_target(self, target: str, cache: bool = True) -> None:\n        if cache:\n            self.state[REJECT_CACHE_TARGET] = target\n        else:\n            self.state[REJECT_TARGET] = target\n\n    @overload\n    def get_target(self) -> str | None: ...\n\n    @overload\n    def get_target(self, default: T) -> str | T: ...\n\n    def get_target(self, default: T | None = None) -> str | T | None:\n        return self.state.get(REJECT_TARGET, default)\n\n    def stop_propagation(self):\n        \"\"\"阻止事件传播\"\"\"\n        self.block = True\n\n    async def update_type(\n        self,\n        bot: Bot,\n        event: Event,\n        stack: AsyncExitStack | None = None,\n        dependency_cache: T_DependencyCache | None = None,\n    ) -> str:\n        updater = self.__class__._default_type_updater\n        return (\n            await updater(\n                bot=bot,\n                event=event,\n                state=self.state,\n                matcher=self,\n                stack=stack,\n                dependency_cache=dependency_cache,\n            )\n            if updater\n            else \"message\"\n        )\n\n    async def update_permission(\n        self,\n        bot: Bot,\n        event: Event,\n        stack: AsyncExitStack | None = None,\n        dependency_cache: T_DependencyCache | None = None,\n    ) -> Permission:\n        if updater := self.__class__._default_permission_updater:\n            return await updater(\n                bot=bot,\n                event=event,\n                state=self.state,\n                matcher=self,\n                stack=stack,\n                dependency_cache=dependency_cache,\n            )\n        return Permission(User.from_event(event, perm=self.permission))\n\n    async def resolve_reject(self):\n        handler = current_handler.get()\n        self.remain_handlers.insert(0, handler)\n        if REJECT_CACHE_TARGET in self.state:\n            self.state[REJECT_TARGET] = self.state[REJECT_CACHE_TARGET]\n\n    @contextmanager\n    def ensure_context(self, bot: Bot, event: Event):\n        b_t = current_bot.set(bot)\n        e_t = current_event.set(event)\n        m_t = current_matcher.set(self)\n        try:\n            yield\n        finally:\n            current_bot.reset(b_t)\n            current_event.reset(e_t)\n            current_matcher.reset(m_t)\n\n    async def simple_run(\n        self,\n        bot: Bot,\n        event: Event,\n        state: T_State,\n        stack: AsyncExitStack | None = None,\n        dependency_cache: T_DependencyCache | None = None,\n    ):\n        logger.trace(\n            f\"{self} run with incoming args: \"\n            f\"bot={bot}, event={event!r}, state={state!r}\"\n        )\n\n        def _handle_stop_propagation(exc_group: BaseExceptionGroup[StopPropagation]):\n            self.block = True\n\n        with self.ensure_context(bot, event):\n            try:\n                with catch({StopPropagation: _handle_stop_propagation}):\n                    # Refresh preprocess state\n                    self.state.update(state)\n\n                    while self.remain_handlers:\n                        handler = self.remain_handlers.pop(0)\n                        current_handler.set(handler)\n                        logger.debug(f\"Running handler {handler}\")\n\n                        def _handle_skipped(\n                            exc_group: BaseExceptionGroup[SkippedException],\n                        ):\n                            logger.debug(f\"Handler {handler} skipped\")\n\n                        with catch({SkippedException: _handle_skipped}):\n                            await handler(\n                                matcher=self,\n                                bot=bot,\n                                event=event,\n                                state=self.state,\n                                stack=stack,\n                                dependency_cache=dependency_cache,\n                            )\n            finally:\n                logger.info(f\"{self} running complete\")\n\n    # 运行handlers\n    async def run(\n        self,\n        bot: Bot,\n        event: Event,\n        state: T_State,\n        stack: AsyncExitStack | None = None,\n        dependency_cache: T_DependencyCache | None = None,\n    ):\n        exc: FinishedException | RejectedException | PausedException | None = None\n\n        def _handle_special_exception(\n            exc_group: BaseExceptionGroup[\n                FinishedException | RejectedException | PausedException\n            ],\n        ):\n            nonlocal exc\n            excs = list(flatten_exception_group(exc_group))\n            if len(excs) > 1:\n                logger.warning(\n                    \"Multiple session control exceptions occurred. \"\n                    \"NoneBot will choose the proper one.\"\n                )\n                finished_exc = next(\n                    (e for e in excs if isinstance(e, FinishedException)),\n                    None,\n                )\n                rejected_exc = next(\n                    (e for e in excs if isinstance(e, RejectedException)),\n                    None,\n                )\n                paused_exc = next(\n                    (e for e in excs if isinstance(e, PausedException)),\n                    None,\n                )\n                exc = finished_exc or rejected_exc or paused_exc\n            elif isinstance(\n                excs[0], (FinishedException, RejectedException, PausedException)\n            ):\n                exc = excs[0]\n\n        with catch(\n            {\n                (\n                    FinishedException,\n                    RejectedException,\n                    PausedException,\n                ): _handle_special_exception\n            }\n        ):\n            await self.simple_run(bot, event, state, stack, dependency_cache)\n\n        if isinstance(exc, FinishedException):\n            pass\n        elif isinstance(exc, RejectedException):\n            await self.resolve_reject()\n            type_ = await self.update_type(bot, event, stack, dependency_cache)\n            permission = await self.update_permission(\n                bot, event, stack, dependency_cache\n            )\n\n            self.new(\n                type_,\n                Rule(),\n                permission,\n                self.remain_handlers,\n                temp=True,\n                priority=0,\n                block=True,\n                source=self.__class__._source,\n                expire_time=bot.config.session_expire_timeout,\n                default_state=self.state,\n                default_type_updater=self.__class__._default_type_updater,\n                default_permission_updater=self.__class__._default_permission_updater,\n            )\n        elif isinstance(exc, PausedException):\n            type_ = await self.update_type(bot, event, stack, dependency_cache)\n            permission = await self.update_permission(\n                bot, event, stack, dependency_cache\n            )\n\n            self.new(\n                type_,\n                Rule(),\n                permission,\n                self.remain_handlers,\n                temp=True,\n                priority=0,\n                block=True,\n                source=self.__class__._source,\n                expire_time=bot.config.session_expire_timeout,\n                default_state=self.state,\n                default_type_updater=self.__class__._default_type_updater,\n                default_permission_updater=self.__class__._default_permission_updater,\n            )\n"
  },
  {
    "path": "nonebot/internal/matcher/provider.py",
    "content": "import abc\nfrom collections import defaultdict\nfrom collections.abc import Mapping, MutableMapping\nfrom typing import TYPE_CHECKING\n\nif TYPE_CHECKING:\n    from .matcher import Matcher\n\n\nclass MatcherProvider(abc.ABC, MutableMapping[int, list[type[\"Matcher\"]]]):\n    \"\"\"事件响应器存储器基类\n\n    参数:\n        matchers: 当前存储器中已有的事件响应器\n    \"\"\"\n\n    @abc.abstractmethod\n    def __init__(self, matchers: Mapping[int, list[type[\"Matcher\"]]]):\n        raise NotImplementedError\n\n\nclass _DictProvider(defaultdict[int, list[type[\"Matcher\"]]], MatcherProvider):  # type: ignore\n    def __init__(self, matchers: Mapping[int, list[type[\"Matcher\"]]]):\n        super().__init__(list, matchers)\n\n\nDEFAULT_PROVIDER_CLASS = _DictProvider\n\"\"\"默认存储器类型\"\"\"\n"
  },
  {
    "path": "nonebot/internal/params.py",
    "content": "from collections.abc import Callable\nfrom contextlib import AsyncExitStack, asynccontextmanager, contextmanager\nfrom enum import Enum\nimport inspect\nfrom typing import (\n    TYPE_CHECKING,\n    Annotated,\n    Any,\n    Literal,\n    cast,\n    get_args,\n    get_origin,\n)\nfrom typing_extensions import Self, override\n\nimport anyio\nfrom exceptiongroup import BaseExceptionGroup, catch\nfrom pydantic.fields import FieldInfo as PydanticFieldInfo\n\nfrom nonebot.compat import FieldInfo, ModelField, PydanticUndefined\nfrom nonebot.consts import ARG_KEY, REJECT_PROMPT_RESULT_KEY\nfrom nonebot.dependencies import Dependent, Param\nfrom nonebot.dependencies.utils import check_field_type\nfrom nonebot.exception import SkippedException\nfrom nonebot.typing import (\n    _STATE_FLAG,\n    T_DependencyCache,\n    T_Handler,\n    T_State,\n    origin_is_annotated,\n)\nfrom nonebot.utils import (\n    generic_check_issubclass,\n    get_name,\n    is_async_gen_callable,\n    is_coroutine_callable,\n    is_gen_callable,\n    run_sync,\n    run_sync_ctx_manager,\n)\n\nif TYPE_CHECKING:\n    from nonebot.adapters import Bot, Event, Message\n    from nonebot.matcher import Matcher\n\n\nclass DependsInner:\n    def __init__(\n        self,\n        dependency: T_Handler | None = None,\n        *,\n        use_cache: bool = True,\n        validate: bool | PydanticFieldInfo = False,\n    ) -> None:\n        self.dependency = dependency\n        self.use_cache = use_cache\n        self.validate = validate\n\n    def __repr__(self) -> str:\n        dep = get_name(self.dependency)\n        cache = \"\" if self.use_cache else \", use_cache=False\"\n        validate = f\", validate={self.validate}\" if self.validate else \"\"\n        return f\"DependsInner({dep}{cache}{validate})\"\n\n\ndef Depends(\n    dependency: T_Handler | None = None,\n    *,\n    use_cache: bool = True,\n    validate: bool | PydanticFieldInfo = False,\n) -> Any:\n    \"\"\"子依赖装饰器\n\n    参数:\n        dependency: 依赖函数。默认为参数的类型注释。\n        use_cache: 是否使用缓存。默认为 `True`。\n        validate: 是否使用 Pydantic 类型校验。默认为 `False`。\n\n    用法:\n        ```python\n        def depend_func() -> Any:\n            return ...\n\n        def depend_gen_func():\n            try:\n                yield ...\n            finally:\n                ...\n\n        async def handler(\n            param_name: Any = Depends(depend_func),\n            gen: Any = Depends(depend_gen_func),\n        ):\n            ...\n        ```\n    \"\"\"\n    return DependsInner(dependency, use_cache=use_cache, validate=validate)\n\n\nclass CacheState(str, Enum):\n    \"\"\"子依赖缓存状态\"\"\"\n\n    PENDING = \"PENDING\"\n    FINISHED = \"FINISHED\"\n\n\nclass DependencyCache:\n    \"\"\"子依赖结果缓存。\n\n    用于缓存子依赖的结果，以避免重复计算。\n    \"\"\"\n\n    def __init__(self):\n        self._state = CacheState.PENDING\n        self._result: Any = None\n        self._exception: BaseException | None = None\n        self._waiter = anyio.Event()\n\n    def done(self) -> bool:\n        return self._state == CacheState.FINISHED\n\n    def result(self) -> Any:\n        \"\"\"获取子依赖结果\"\"\"\n\n        if self._state != CacheState.FINISHED:\n            raise RuntimeError(\"Result is not ready\")\n\n        if self._exception is not None:\n            raise self._exception\n        return self._result\n\n    def exception(self) -> BaseException | None:\n        \"\"\"获取子依赖异常\"\"\"\n\n        if self._state != CacheState.FINISHED:\n            raise RuntimeError(\"Result is not ready\")\n\n        return self._exception\n\n    def set_result(self, result: Any) -> None:\n        \"\"\"设置子依赖结果\"\"\"\n\n        if self._state != CacheState.PENDING:\n            raise RuntimeError(f\"Cache state invalid: {self._state}\")\n\n        self._result = result\n        self._state = CacheState.FINISHED\n        self._waiter.set()\n\n    def set_exception(self, exception: BaseException) -> None:\n        \"\"\"设置子依赖异常\"\"\"\n\n        if self._state != CacheState.PENDING:\n            raise RuntimeError(f\"Cache state invalid: {self._state}\")\n\n        self._exception = exception\n        self._state = CacheState.FINISHED\n        self._waiter.set()\n\n    async def wait(self):\n        \"\"\"等待子依赖结果\"\"\"\n        await self._waiter.wait()\n        if self._state != CacheState.FINISHED:\n            raise RuntimeError(\"Invalid cache state\")\n\n        if self._exception is not None:\n            raise self._exception\n\n        return self._result\n\n\nclass DependParam(Param):\n    \"\"\"子依赖注入参数。\n\n    本注入解析所有子依赖注入，然后将它们的返回值作为参数值传递给父依赖。\n\n    本注入应该具有最高优先级，因此应该在其他参数之前检查。\n    \"\"\"\n\n    def __init__(\n        self, *args, dependent: Dependent[Any], use_cache: bool, **kwargs: Any\n    ) -> None:\n        super().__init__(*args, **kwargs)\n        self.dependent = dependent\n        self.use_cache = use_cache\n\n    def __repr__(self) -> str:\n        return f\"Depends({self.dependent}, use_cache={self.use_cache})\"\n\n    @classmethod\n    def _from_field(\n        cls,\n        sub_dependent: Dependent[Any],\n        use_cache: bool,\n        validate: bool | PydanticFieldInfo,\n    ) -> Self:\n        return cls._inherit_construct(\n            validate if isinstance(validate, PydanticFieldInfo) else None,\n            dependent=sub_dependent,\n            use_cache=use_cache,\n            validate=bool(validate),\n        )\n\n    @classmethod\n    @override\n    def _check_param(\n        cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...]\n    ) -> Self | None:\n        type_annotation, depends_inner = param.annotation, None\n        # extract type annotation and dependency from Annotated\n        if get_origin(param.annotation) is Annotated:\n            type_annotation, *extra_args = get_args(param.annotation)\n            depends_inner = next(\n                (x for x in reversed(extra_args) if isinstance(x, DependsInner)), None\n            )\n\n        # param default value takes higher priority\n        depends_inner = (\n            param.default if isinstance(param.default, DependsInner) else depends_inner\n        )\n        # not a dependent\n        if depends_inner is None:\n            return\n\n        dependency: T_Handler\n        # sub dependency is not specified, use type annotation\n        if depends_inner.dependency is None:\n            assert type_annotation is not inspect.Signature.empty, (\n                \"Dependency cannot be empty\"\n            )\n            dependency = type_annotation\n        else:\n            dependency = depends_inner.dependency\n        # parse sub dependency\n        sub_dependent = Dependent[Any].parse(\n            call=dependency,\n            allow_types=allow_types,\n        )\n\n        return cls._from_field(\n            sub_dependent, depends_inner.use_cache, depends_inner.validate\n        )\n\n    @classmethod\n    @override\n    def _check_parameterless(\n        cls, value: Any, allow_types: tuple[type[Param], ...]\n    ) -> \"Param | None\":\n        if isinstance(value, DependsInner):\n            assert value.dependency, \"Dependency cannot be empty\"\n            dependent = Dependent[Any].parse(\n                call=value.dependency, allow_types=allow_types\n            )\n            return cls._from_field(dependent, value.use_cache, value.validate)\n\n    @override\n    async def _solve(\n        self,\n        stack: AsyncExitStack | None = None,\n        dependency_cache: T_DependencyCache | None = None,\n        **kwargs: Any,\n    ) -> Any:\n        use_cache: bool = self.use_cache\n        dependency_cache = {} if dependency_cache is None else dependency_cache\n\n        sub_dependent = self.dependent\n        call = cast(Callable[..., Any], sub_dependent.call)\n\n        # solve sub dependency with current cache\n        exc: BaseExceptionGroup[SkippedException] | None = None\n\n        def _handle_skipped(exc_group: BaseExceptionGroup[SkippedException]):\n            nonlocal exc\n            exc = exc_group\n\n        with catch({SkippedException: _handle_skipped}):\n            sub_values = await sub_dependent.solve(\n                stack=stack,\n                dependency_cache=dependency_cache,\n                **kwargs,\n            )\n\n        if exc is not None:\n            raise exc\n\n        # run dependency function\n        if use_cache and call in dependency_cache:\n            return await dependency_cache[call].wait()\n\n        if is_gen_callable(call) or is_async_gen_callable(call):\n            assert isinstance(stack, AsyncExitStack), (\n                \"Generator dependency should be called in context\"\n            )\n            if is_gen_callable(call):\n                cm = run_sync_ctx_manager(contextmanager(call)(**sub_values))\n            else:\n                cm = asynccontextmanager(call)(**sub_values)\n\n            target = stack.enter_async_context(cm)\n        elif is_coroutine_callable(call):\n            target = call(**sub_values)\n        else:\n            target = run_sync(call)(**sub_values)\n\n        dependency_cache[call] = cache = DependencyCache()\n        try:\n            result = await target\n        except Exception as e:\n            cache.set_exception(e)\n            raise\n        except BaseException as e:\n            cache.set_exception(e)\n            # remove cache when base exception occurs\n            # e.g. CancelledError\n            dependency_cache.pop(call, None)\n            raise\n        else:\n            cache.set_result(result)\n            return result\n\n    @override\n    async def _check(self, **kwargs: Any) -> None:\n        # run sub dependent pre-checkers\n        await self.dependent.check(**kwargs)\n\n\nclass BotParam(Param):\n    \"\"\"{ref}`nonebot.adapters.Bot` 注入参数。\n\n    本注入解析所有类型为且仅为 {ref}`nonebot.adapters.Bot` 及其子类或 `None` 的参数。\n\n    为保证兼容性，本注入还会解析名为 `bot` 且没有类型注解的参数。\n    \"\"\"\n\n    def __init__(self, *args, checker: ModelField | None = None, **kwargs: Any) -> None:\n        super().__init__(*args, **kwargs)\n        self.checker = checker\n\n    def __repr__(self) -> str:\n        return (\n            \"BotParam(\"\n            + (repr(self.checker.annotation) if self.checker is not None else \"\")\n            + \")\"\n        )\n\n    @classmethod\n    @override\n    def _check_param(\n        cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...]\n    ) -> Self | None:\n        from nonebot.adapters import Bot\n\n        # param type is Bot(s) or subclass(es) of Bot or None\n        if generic_check_issubclass(param.annotation, Bot):\n            checker: ModelField | None = None\n            if param.annotation is not Bot:\n                checker = ModelField.construct(\n                    name=param.name, annotation=param.annotation, field_info=FieldInfo()\n                )\n            return cls(checker=checker)\n        # legacy: param is named \"bot\" and has no type annotation\n        elif param.annotation == param.empty and param.name == \"bot\":\n            return cls()\n\n    @override\n    async def _solve(  # pyright: ignore[reportIncompatibleMethodOverride]\n        self, bot: \"Bot\", **kwargs: Any\n    ) -> Any:\n        return bot\n\n    @override\n    async def _check(  # pyright: ignore[reportIncompatibleMethodOverride]\n        self, bot: \"Bot\", **kwargs: Any\n    ) -> None:\n        if self.checker is not None:\n            check_field_type(self.checker, bot)\n\n\nclass EventParam(Param):\n    \"\"\"{ref}`nonebot.adapters.Event` 注入参数\n\n    本注入解析所有类型为且仅为 {ref}`nonebot.adapters.Event` 及其子类或 `None` 的参数。\n\n    为保证兼容性，本注入还会解析名为 `event` 且没有类型注解的参数。\n    \"\"\"\n\n    def __init__(self, *args, checker: ModelField | None = None, **kwargs: Any) -> None:\n        super().__init__(*args, **kwargs)\n        self.checker = checker\n\n    def __repr__(self) -> str:\n        return (\n            \"EventParam(\"\n            + (repr(self.checker.annotation) if self.checker is not None else \"\")\n            + \")\"\n        )\n\n    @classmethod\n    @override\n    def _check_param(\n        cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...]\n    ) -> Self | None:\n        from nonebot.adapters import Event\n\n        # param type is Event(s) or subclass(es) of Event or None\n        if generic_check_issubclass(param.annotation, Event):\n            checker: ModelField | None = None\n            if param.annotation is not Event:\n                checker = ModelField.construct(\n                    name=param.name, annotation=param.annotation, field_info=FieldInfo()\n                )\n            return cls(checker=checker)\n        # legacy: param is named \"event\" and has no type annotation\n        elif param.annotation == param.empty and param.name == \"event\":\n            return cls()\n\n    @override\n    async def _solve(  # pyright: ignore[reportIncompatibleMethodOverride]\n        self, event: \"Event\", **kwargs: Any\n    ) -> Any:\n        return event\n\n    @override\n    async def _check(  # pyright: ignore[reportIncompatibleMethodOverride]\n        self, event: \"Event\", **kwargs: Any\n    ) -> Any:\n        if self.checker is not None:\n            check_field_type(self.checker, event)\n\n\nclass StateParam(Param):\n    \"\"\"事件处理状态注入参数\n\n    本注入解析所有类型为 `T_State` 的参数。\n\n    为保证兼容性，本注入还会解析名为 `state` 且没有类型注解的参数。\n    \"\"\"\n\n    def __repr__(self) -> str:\n        return \"StateParam()\"\n\n    @classmethod\n    @override\n    def _check_param(\n        cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...]\n    ) -> Self | None:\n        # param type is T_State\n        if origin_is_annotated(\n            get_origin(param.annotation)\n        ) and _STATE_FLAG in get_args(param.annotation):\n            return cls()\n        # legacy: param is named \"state\" and has no type annotation\n        elif param.annotation == param.empty and param.name == \"state\":\n            return cls()\n\n    @override\n    async def _solve(  # pyright: ignore[reportIncompatibleMethodOverride]\n        self, state: T_State, **kwargs: Any\n    ) -> Any:\n        return state\n\n\nclass MatcherParam(Param):\n    \"\"\"事件响应器实例注入参数\n\n    本注入解析所有类型为且仅为 {ref}`nonebot.matcher.Matcher` 及其子类或 `None` 的参数。\n\n    为保证兼容性，本注入还会解析名为 `matcher` 且没有类型注解的参数。\n    \"\"\"\n\n    def __init__(self, *args, checker: ModelField | None = None, **kwargs: Any) -> None:\n        super().__init__(*args, **kwargs)\n        self.checker = checker\n\n    def __repr__(self) -> str:\n        return (\n            \"MatcherParam(\"\n            + (repr(self.checker.annotation) if self.checker is not None else \"\")\n            + \")\"\n        )\n\n    @classmethod\n    @override\n    def _check_param(\n        cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...]\n    ) -> Self | None:\n        from nonebot.matcher import Matcher\n\n        # param type is Matcher(s) or subclass(es) of Matcher or None\n        if generic_check_issubclass(param.annotation, Matcher):\n            checker: ModelField | None = None\n            if param.annotation is not Matcher:\n                checker = ModelField.construct(\n                    name=param.name, annotation=param.annotation, field_info=FieldInfo()\n                )\n            return cls(checker=checker)\n        # legacy: param is named \"matcher\" and has no type annotation\n        elif param.annotation == param.empty and param.name == \"matcher\":\n            return cls()\n\n    @override\n    async def _solve(  # pyright: ignore[reportIncompatibleMethodOverride]\n        self, matcher: \"Matcher\", **kwargs: Any\n    ) -> Any:\n        return matcher\n\n    @override\n    async def _check(  # pyright: ignore[reportIncompatibleMethodOverride]\n        self, matcher: \"Matcher\", **kwargs: Any\n    ) -> Any:\n        if self.checker is not None:\n            check_field_type(self.checker, matcher)\n\n\nclass ArgInner:\n    def __init__(\n        self, key: str | None, type: Literal[\"message\", \"str\", \"plaintext\", \"prompt\"]\n    ) -> None:\n        self.key: str | None = key\n        self.type: Literal[\"message\", \"str\", \"plaintext\", \"prompt\"] = type\n\n    def __repr__(self) -> str:\n        return f\"ArgInner(key={self.key!r}, type={self.type!r})\"\n\n\ndef Arg(key: str | None = None) -> Any:\n    \"\"\"Arg 参数消息\"\"\"\n    return ArgInner(key, \"message\")\n\n\ndef ArgStr(key: str | None = None) -> str:\n    \"\"\"Arg 参数消息文本\"\"\"\n    return ArgInner(key, \"str\")  # type: ignore\n\n\ndef ArgPlainText(key: str | None = None) -> str:\n    \"\"\"Arg 参数消息纯文本\"\"\"\n    return ArgInner(key, \"plaintext\")  # type: ignore\n\n\ndef ArgPromptResult(key: str | None = None) -> Any:\n    \"\"\"`arg` prompt 发送结果\"\"\"\n    return ArgInner(key, \"prompt\")\n\n\nclass ArgParam(Param):\n    \"\"\"Arg 注入参数\n\n    本注入解析事件响应器操作 `got` 所获取的参数。\n\n    可以通过 `Arg`、`ArgStr`、`ArgPlainText` 等函数参数 `key` 指定获取的参数，\n    留空则会根据参数名称获取。\n    \"\"\"\n\n    def __init__(\n        self,\n        *args,\n        key: str,\n        type: Literal[\"message\", \"str\", \"plaintext\", \"prompt\"],\n        **kwargs: Any,\n    ) -> None:\n        super().__init__(*args, **kwargs)\n        self.key = key\n        self.type = type\n\n    def __repr__(self) -> str:\n        return f\"ArgParam(key={self.key!r}, type={self.type!r})\"\n\n    @classmethod\n    @override\n    def _check_param(\n        cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...]\n    ) -> Self | None:\n        if isinstance(param.default, ArgInner):\n            return cls(key=param.default.key or param.name, type=param.default.type)\n        elif get_origin(param.annotation) is Annotated:\n            for arg in get_args(param.annotation)[:0:-1]:\n                if isinstance(arg, ArgInner):\n                    return cls(key=arg.key or param.name, type=arg.type)\n\n    async def _solve(  # pyright: ignore[reportIncompatibleMethodOverride]\n        self, matcher: \"Matcher\", **kwargs: Any\n    ) -> Any:\n        if self.type == \"message\":\n            return self._solve_message(matcher)\n        elif self.type == \"str\":\n            return self._solve_str(matcher)\n        elif self.type == \"plaintext\":\n            return self._solve_plaintext(matcher)\n        elif self.type == \"prompt\":\n            return self._solve_prompt(matcher)\n        else:\n            raise ValueError(f\"Unknown Arg type: {self.type}\")\n\n    def _solve_message(self, matcher: \"Matcher\") -> \"Message | None\":\n        return matcher.get_arg(self.key)\n\n    def _solve_str(self, matcher: \"Matcher\") -> str | None:\n        message = matcher.get_arg(self.key)\n        return str(message) if message is not None else None\n\n    def _solve_plaintext(self, matcher: \"Matcher\") -> str | None:\n        message = matcher.get_arg(self.key)\n        return message.extract_plain_text() if message is not None else None\n\n    def _solve_prompt(self, matcher: \"Matcher\") -> Any | None:\n        return matcher.state.get(\n            REJECT_PROMPT_RESULT_KEY.format(key=ARG_KEY.format(key=self.key))\n        )\n\n\nclass ExceptionParam(Param):\n    \"\"\"{ref}`nonebot.message.run_postprocessor` 的异常注入参数\n\n    本注入解析所有类型为 `Exception` 或 `None` 的参数。\n\n    为保证兼容性，本注入还会解析名为 `exception` 且没有类型注解的参数。\n    \"\"\"\n\n    def __repr__(self) -> str:\n        return \"ExceptionParam()\"\n\n    @classmethod\n    @override\n    def _check_param(\n        cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...]\n    ) -> Self | None:\n        # param type is Exception(s) or subclass(es) of Exception or None\n        if generic_check_issubclass(param.annotation, Exception):\n            return cls()\n        # legacy: param is named \"exception\" and has no type annotation\n        elif param.annotation == param.empty and param.name == \"exception\":\n            return cls()\n\n    @override\n    async def _solve(self, exception: Exception | None = None, **kwargs: Any) -> Any:\n        return exception\n\n\nclass DefaultParam(Param):\n    \"\"\"默认值注入参数\n\n    本注入解析所有剩余未能解析且具有默认值的参数。\n\n    本注入参数应该具有最低优先级，因此应该在所有其他注入参数之后使用。\n    \"\"\"\n\n    def __repr__(self) -> str:\n        return f\"DefaultParam(default={self.default!r})\"\n\n    @classmethod\n    @override\n    def _check_param(\n        cls, param: inspect.Parameter, allow_types: tuple[type[Param], ...]\n    ) -> Self | None:\n        if param.default != param.empty:\n            return cls(default=param.default)\n\n    @override\n    async def _solve(self, **kwargs: Any) -> Any:\n        return PydanticUndefined\n\n\n__autodoc__ = {\n    \"DependsInner\": False,\n    \"StateInner\": False,\n    \"ArgInner\": False,\n}\n"
  },
  {
    "path": "nonebot/internal/permission.py",
    "content": "from contextlib import AsyncExitStack\nfrom typing import ClassVar, NoReturn\nfrom typing_extensions import Self\n\nimport anyio\n\nfrom nonebot.dependencies import Dependent\nfrom nonebot.exception import SkippedException\nfrom nonebot.typing import T_DependencyCache, T_PermissionChecker\nfrom nonebot.utils import run_coro_with_catch\n\nfrom .adapter import Bot, Event\nfrom .params import BotParam, DefaultParam, DependParam, EventParam, Param\n\n\nclass Permission:\n    \"\"\"{ref}`nonebot.matcher.Matcher` 权限类。\n\n    当事件传递时，在 {ref}`nonebot.matcher.Matcher` 运行前进行检查。\n\n    参数:\n        checkers: PermissionChecker\n\n    用法:\n        ```python\n        Permission(async_function) | sync_function\n        # 等价于\n        Permission(async_function, sync_function)\n        ```\n    \"\"\"\n\n    __slots__ = (\"checkers\",)\n\n    HANDLER_PARAM_TYPES: ClassVar[list[type[Param]]] = [\n        DependParam,\n        BotParam,\n        EventParam,\n        DefaultParam,\n    ]\n\n    def __init__(self, *checkers: T_PermissionChecker | Dependent[bool]) -> None:\n        self.checkers: set[Dependent[bool]] = {\n            (\n                checker\n                if isinstance(checker, Dependent)\n                else Dependent[bool].parse(\n                    call=checker, allow_types=self.HANDLER_PARAM_TYPES\n                )\n            )\n            for checker in checkers\n        }\n        \"\"\"存储 `PermissionChecker`\"\"\"\n\n    def __repr__(self) -> str:\n        return f\"Permission({', '.join(repr(checker) for checker in self.checkers)})\"\n\n    async def __call__(\n        self,\n        bot: Bot,\n        event: Event,\n        stack: AsyncExitStack | None = None,\n        dependency_cache: T_DependencyCache | None = None,\n    ) -> bool:\n        \"\"\"检查是否满足某个权限。\n\n        参数:\n            bot: Bot 对象\n            event: Event 对象\n            stack: 异步上下文栈\n            dependency_cache: 依赖缓存\n        \"\"\"\n        if not self.checkers:\n            return True\n\n        result = False\n\n        async def _run_checker(checker: Dependent[bool]) -> None:\n            nonlocal result\n            # calculate the result first to avoid data racing\n            is_passed = await run_coro_with_catch(\n                checker(\n                    bot=bot, event=event, stack=stack, dependency_cache=dependency_cache\n                ),\n                (SkippedException,),\n                False,\n            )\n            result |= is_passed\n\n        async with anyio.create_task_group() as tg:\n            for checker in self.checkers:\n                tg.start_soon(_run_checker, checker)\n\n        return result\n\n    def __and__(self, other: object) -> NoReturn:\n        raise RuntimeError(\"And operation between Permissions is not allowed.\")\n\n    def __or__(self, other: \"Permission | T_PermissionChecker | None\") -> \"Permission\":\n        if other is None:\n            return self\n        elif isinstance(other, Permission):\n            return Permission(*self.checkers, *other.checkers)\n        else:\n            return Permission(*self.checkers, other)\n\n    def __ror__(self, other: \"Permission | T_PermissionChecker | None\") -> \"Permission\":\n        if other is None:\n            return self\n        elif isinstance(other, Permission):\n            return Permission(*other.checkers, *self.checkers)\n        else:\n            return Permission(other, *self.checkers)\n\n\nclass User:\n    \"\"\"检查当前事件是否属于指定会话。\n\n    参数:\n        users: 会话 ID 元组\n        perm: 需同时满足的权限\n    \"\"\"\n\n    __slots__ = (\"perm\", \"users\")\n\n    def __init__(self, users: tuple[str, ...], perm: Permission | None = None) -> None:\n        self.users = users\n        self.perm = perm\n\n    def __repr__(self) -> str:\n        return (\n            f\"User(users={self.users}\"\n            + (f\", permission={self.perm})\" if self.perm else \"\")\n            + \")\"\n        )\n\n    async def __call__(self, bot: Bot, event: Event) -> bool:\n        try:\n            session = event.get_session_id()\n        except Exception:\n            return False\n        return bool(\n            session in self.users and (self.perm is None or await self.perm(bot, event))\n        )\n\n    @classmethod\n    def _clean_permission(cls, perm: Permission) -> Permission | None:\n        if len(perm.checkers) == 1 and isinstance(\n            user_perm := next(iter(perm.checkers)).call, cls\n        ):\n            return user_perm.perm\n        return perm\n\n    @classmethod\n    def from_event(cls, event: Event, perm: Permission | None = None) -> Self:\n        \"\"\"从事件中获取会话 ID。\n\n        如果 `perm` 中仅有 `User` 类型的权限检查函数，则会去除原有的会话 ID 限制。\n\n        参数:\n            event: Event 对象\n            perm: 需同时满足的权限\n        \"\"\"\n        return cls((event.get_session_id(),), perm=perm and cls._clean_permission(perm))\n\n    @classmethod\n    def from_permission(cls, *users: str, perm: Permission | None = None) -> Self:\n        \"\"\"指定会话与权限。\n\n        如果 `perm` 中仅有 `User` 类型的权限检查函数，则会去除原有的会话 ID 限制。\n\n        参数:\n            users: 会话白名单\n            perm: 需同时满足的权限\n        \"\"\"\n        return cls(users, perm=perm and cls._clean_permission(perm))\n\n\ndef USER(*users: str, perm: Permission | None = None):\n    \"\"\"匹配当前事件属于指定会话。\n\n    如果 `perm` 中仅有 `User` 类型的权限检查函数，则会去除原有检查函数的会话 ID 限制。\n\n    参数:\n        user: 会话白名单\n        perm: 需要同时满足的权限\n    \"\"\"\n\n    return Permission(User.from_permission(*users, perm=perm))\n"
  },
  {
    "path": "nonebot/internal/rule.py",
    "content": "from contextlib import AsyncExitStack\nfrom typing import ClassVar, NoReturn\n\nimport anyio\nfrom exceptiongroup import BaseExceptionGroup, catch\n\nfrom nonebot.dependencies import Dependent\nfrom nonebot.exception import SkippedException\nfrom nonebot.typing import T_DependencyCache, T_RuleChecker, T_State\n\nfrom .adapter import Bot, Event\nfrom .params import BotParam, DefaultParam, DependParam, EventParam, Param, StateParam\n\n\nclass Rule:\n    \"\"\"{ref}`nonebot.matcher.Matcher` 规则类。\n\n    当事件传递时，在 {ref}`nonebot.matcher.Matcher` 运行前进行检查。\n\n    参数:\n        *checkers: RuleChecker\n\n    用法:\n        ```python\n        Rule(async_function) & sync_function\n        # 等价于\n        Rule(async_function, sync_function)\n        ```\n    \"\"\"\n\n    __slots__ = (\"checkers\",)\n\n    HANDLER_PARAM_TYPES: ClassVar[list[type[Param]]] = [\n        DependParam,\n        BotParam,\n        EventParam,\n        StateParam,\n        DefaultParam,\n    ]\n\n    def __init__(self, *checkers: T_RuleChecker | Dependent[bool]) -> None:\n        self.checkers: set[Dependent[bool]] = {\n            (\n                checker\n                if isinstance(checker, Dependent)\n                else Dependent[bool].parse(\n                    call=checker, allow_types=self.HANDLER_PARAM_TYPES\n                )\n            )\n            for checker in checkers\n        }\n        \"\"\"存储 `RuleChecker`\"\"\"\n\n    def __repr__(self) -> str:\n        return f\"Rule({', '.join(repr(checker) for checker in self.checkers)})\"\n\n    async def __call__(\n        self,\n        bot: Bot,\n        event: Event,\n        state: T_State,\n        stack: AsyncExitStack | None = None,\n        dependency_cache: T_DependencyCache | None = None,\n    ) -> bool:\n        \"\"\"检查是否符合所有规则\n\n        参数:\n            bot: Bot 对象\n            event: Event 对象\n            state: 当前 State\n            stack: 异步上下文栈\n            dependency_cache: 依赖缓存\n        \"\"\"\n        if not self.checkers:\n            return True\n\n        result = True\n\n        def _handle_skipped_exception(\n            exc_group: BaseExceptionGroup[SkippedException],\n        ) -> None:\n            nonlocal result\n            result = False\n\n        async def _run_checker(checker: Dependent[bool]) -> None:\n            nonlocal result\n            # calculate the result first to avoid data racing\n            is_passed = await checker(\n                bot=bot,\n                event=event,\n                state=state,\n                stack=stack,\n                dependency_cache=dependency_cache,\n            )\n            result &= is_passed\n\n        with catch({SkippedException: _handle_skipped_exception}):\n            async with anyio.create_task_group() as tg:\n                for checker in self.checkers:\n                    tg.start_soon(_run_checker, checker)\n\n        return result\n\n    def __and__(self, other: \"Rule | T_RuleChecker | None\") -> \"Rule\":\n        if other is None:\n            return self\n        elif isinstance(other, Rule):\n            return Rule(*self.checkers, *other.checkers)\n        else:\n            return Rule(*self.checkers, other)\n\n    def __rand__(self, other: \"Rule | T_RuleChecker | None\") -> \"Rule\":\n        if other is None:\n            return self\n        elif isinstance(other, Rule):\n            return Rule(*other.checkers, *self.checkers)\n        else:\n            return Rule(other, *self.checkers)\n\n    def __or__(self, other: object) -> NoReturn:\n        raise RuntimeError(\"Or operation between rules is not allowed.\")\n"
  },
  {
    "path": "nonebot/log.py",
    "content": "\"\"\"本模块定义了 NoneBot 的日志记录 Logger。\n\nNoneBot 使用 [`loguru`][loguru] 来记录日志信息。\n\n自定义 logger 请参考 [自定义日志](https://nonebot.dev/docs/appendices/log)\n以及 [`loguru`][loguru] 文档。\n\n[loguru]: https://github.com/Delgan/loguru\n\nFrontMatter:\n    mdx:\n        format: md\n    sidebar_position: 7\n    description: nonebot.log 模块\n\"\"\"\n\nimport inspect\nimport logging\nimport sys\nfrom typing import TYPE_CHECKING\n\nimport loguru\n\nif TYPE_CHECKING:\n    # avoid sphinx autodoc resolve annotation failed\n    # because loguru module do not have `Logger` class actually\n    from loguru import Logger, Record\n\n# logger = logging.getLogger(\"nonebot\")\nlogger: \"Logger\" = loguru.logger\n\"\"\"NoneBot 日志记录器对象。\n\n默认信息:\n\n- 格式: `[%(asctime)s %(name)s] %(levelname)s: %(message)s`\n- 等级: `INFO` ，根据 `config.log_level` 配置改变\n- 输出: 输出至 stdout\n\n用法:\n    ```python\n    from nonebot.log import logger\n    ```\n\"\"\"\n\n# default_handler = logging.StreamHandler(sys.stdout)\n# default_handler.setFormatter(\n#     logging.Formatter(\"[%(asctime)s %(name)s] %(levelname)s: %(message)s\"))\n# logger.addHandler(default_handler)\n\n\n# https://loguru.readthedocs.io/en/stable/overview.html#entirely-compatible-with-standard-logging\nclass LoguruHandler(logging.Handler):  # pragma: no cover\n    \"\"\"logging 与 loguru 之间的桥梁，将 logging 的日志转发到 loguru。\"\"\"\n\n    def emit(self, record: logging.LogRecord):\n        try:\n            level = logger.level(record.levelname).name\n        except ValueError:\n            level = record.levelno\n\n        frame, depth = inspect.currentframe(), 0\n        while frame and (depth == 0 or frame.f_code.co_filename == logging.__file__):\n            frame = frame.f_back\n            depth += 1\n\n        logger.opt(depth=depth, exception=record.exc_info).log(\n            level, record.getMessage()\n        )\n\n\ndef default_filter(record: \"Record\"):\n    \"\"\"默认的日志过滤器，根据 `config.log_level` 配置改变日志等级。\"\"\"\n    log_level = record[\"extra\"].get(\"nonebot_log_level\", \"INFO\")\n    levelno = logger.level(log_level).no if isinstance(log_level, str) else log_level\n    return record[\"level\"].no >= levelno\n\n\ndefault_format: str = (\n    \"<g>{time:MM-DD HH:mm:ss}</g> \"\n    \"[<lvl>{level}</lvl>] \"\n    \"<c><u>{name}</u></c> | \"\n    # \"<c>{function}:{line}</c>| \"\n    \"{message}\"\n)\n\"\"\"默认日志格式\"\"\"\n\nlogger.remove()\nlogger_id = logger.add(\n    sys.stdout,\n    level=0,\n    diagnose=False,\n    filter=default_filter,\n    format=default_format,\n)\n\"\"\"默认日志处理器 id\"\"\"\n\n__autodoc__ = {\"logger_id\": False}\n"
  },
  {
    "path": "nonebot/matcher.py",
    "content": "\"\"\"本模块实现事件响应器的创建与运行，并提供一些快捷方法来帮助用户更好的与机器人进行对话。\n\nFrontMatter:\n    mdx:\n        format: md\n    sidebar_position: 3\n    description: nonebot.matcher 模块\n\"\"\"\n\nfrom nonebot.internal.matcher import DEFAULT_PROVIDER_CLASS as DEFAULT_PROVIDER_CLASS\nfrom nonebot.internal.matcher import Matcher as Matcher\nfrom nonebot.internal.matcher import MatcherManager as MatcherManager\nfrom nonebot.internal.matcher import MatcherProvider as MatcherProvider\nfrom nonebot.internal.matcher import MatcherSource as MatcherSource\nfrom nonebot.internal.matcher import current_bot as current_bot\nfrom nonebot.internal.matcher import current_event as current_event\nfrom nonebot.internal.matcher import current_handler as current_handler\nfrom nonebot.internal.matcher import current_matcher as current_matcher\nfrom nonebot.internal.matcher import matchers as matchers\n\n__autodoc__ = {\n    \"Matcher\": True,\n    \"matchers\": True,\n    \"MatcherManager\": True,\n    \"MatcherProvider\": True,\n    \"DEFAULT_PROVIDER_CLASS\": True,\n}\n"
  },
  {
    "path": "nonebot/message.py",
    "content": "\"\"\"本模块定义了事件处理主要流程。\n\nNoneBot 内部处理并按优先级分发事件给所有事件响应器，提供了多个插槽以进行事件的预处理等。\n\nFrontMatter:\n    mdx:\n        format: md\n    sidebar_position: 2\n    description: nonebot.message 模块\n\"\"\"\n\nfrom collections.abc import Callable\nimport contextlib\nfrom contextlib import AsyncExitStack\nfrom datetime import datetime\nfrom typing import TYPE_CHECKING, Any\n\nimport anyio\nfrom exceptiongroup import BaseExceptionGroup, catch\n\nfrom nonebot.dependencies import Dependent\nfrom nonebot.exception import (\n    IgnoredException,\n    NoLogException,\n    SkippedException,\n    StopPropagation,\n)\nfrom nonebot.internal.params import (\n    ArgParam,\n    BotParam,\n    DefaultParam,\n    DependParam,\n    EventParam,\n    ExceptionParam,\n    MatcherParam,\n    StateParam,\n)\nfrom nonebot.log import logger\nfrom nonebot.matcher import Matcher, matchers\nfrom nonebot.rule import TrieRule\nfrom nonebot.typing import (\n    T_DependencyCache,\n    T_EventPostProcessor,\n    T_EventPreProcessor,\n    T_RunPostProcessor,\n    T_RunPreProcessor,\n    T_State,\n)\nfrom nonebot.utils import (\n    escape_tag,\n    flatten_exception_group,\n    run_coro_with_catch,\n    run_coro_with_shield,\n)\n\nif TYPE_CHECKING:\n    from nonebot.adapters import Bot, Event\n\n_event_preprocessors: set[Dependent[Any]] = set()\n_event_postprocessors: set[Dependent[Any]] = set()\n_run_preprocessors: set[Dependent[Any]] = set()\n_run_postprocessors: set[Dependent[Any]] = set()\n\nEVENT_PCS_PARAMS = (\n    DependParam,\n    BotParam,\n    EventParam,\n    StateParam,\n    DefaultParam,\n)\nRUN_PREPCS_PARAMS = (\n    DependParam,\n    BotParam,\n    EventParam,\n    StateParam,\n    ArgParam,\n    MatcherParam,\n    DefaultParam,\n)\nRUN_POSTPCS_PARAMS = (\n    DependParam,\n    ExceptionParam,\n    BotParam,\n    EventParam,\n    StateParam,\n    ArgParam,\n    MatcherParam,\n    DefaultParam,\n)\n\n\ndef event_preprocessor(func: T_EventPreProcessor) -> T_EventPreProcessor:\n    \"\"\"事件预处理。\n\n    装饰一个函数，使它在每次接收到事件并分发给各响应器之前执行。\n    \"\"\"\n    _event_preprocessors.add(\n        Dependent[Any].parse(call=func, allow_types=EVENT_PCS_PARAMS)\n    )\n    return func\n\n\ndef event_postprocessor(func: T_EventPostProcessor) -> T_EventPostProcessor:\n    \"\"\"事件后处理。\n\n    装饰一个函数，使它在每次接收到事件并分发给各响应器之后执行。\n    \"\"\"\n    _event_postprocessors.add(\n        Dependent[Any].parse(call=func, allow_types=EVENT_PCS_PARAMS)\n    )\n    return func\n\n\ndef run_preprocessor(func: T_RunPreProcessor) -> T_RunPreProcessor:\n    \"\"\"运行预处理。\n\n    装饰一个函数，使它在每次事件响应器运行前执行。\n    \"\"\"\n    _run_preprocessors.add(\n        Dependent[Any].parse(call=func, allow_types=RUN_PREPCS_PARAMS)\n    )\n    return func\n\n\ndef run_postprocessor(func: T_RunPostProcessor) -> T_RunPostProcessor:\n    \"\"\"运行后处理。\n\n    装饰一个函数，使它在每次事件响应器运行后执行。\n    \"\"\"\n    _run_postprocessors.add(\n        Dependent[Any].parse(call=func, allow_types=RUN_POSTPCS_PARAMS)\n    )\n    return func\n\n\ndef _handle_ignored_exception(\n    msg: str,\n) -> Callable[[BaseExceptionGroup[IgnoredException]], None]:\n    def _handle(exc_group: BaseExceptionGroup[IgnoredException]) -> None:\n        logger.opt(colors=True).info(msg)\n\n    return _handle\n\n\ndef _handle_exception(msg: str) -> Callable[[BaseExceptionGroup[Exception]], None]:\n    def _handle(exc_group: BaseExceptionGroup[Exception]) -> None:\n        for exc in flatten_exception_group(exc_group):\n            logger.opt(colors=True, exception=exc).error(msg)\n\n    return _handle\n\n\nasync def _apply_event_preprocessors(\n    bot: \"Bot\",\n    event: \"Event\",\n    state: T_State,\n    stack: AsyncExitStack | None = None,\n    dependency_cache: T_DependencyCache | None = None,\n    show_log: bool = True,\n) -> bool:\n    \"\"\"运行事件预处理。\n\n    参数:\n        bot: Bot 对象\n        event: Event 对象\n        state: 会话状态\n        stack: 异步上下文栈\n        dependency_cache: 依赖缓存\n        show_log: 是否显示日志\n\n    返回:\n        是否继续处理事件\n    \"\"\"\n    if not _event_preprocessors:\n        return True\n\n    if show_log:\n        logger.debug(\"Running PreProcessors...\")\n\n    with catch(\n        {\n            IgnoredException: _handle_ignored_exception(\n                f\"Event {escape_tag(event.get_event_name())} is <b>ignored</b>\"\n            ),\n            Exception: _handle_exception(\n                \"<r><bg #f8bbd0>Error when running EventPreProcessors. \"\n                \"Event ignored!</bg #f8bbd0></r>\"\n            ),\n        }\n    ):\n        async with anyio.create_task_group() as tg:\n            for proc in _event_preprocessors:\n                tg.start_soon(\n                    run_coro_with_catch,\n                    proc(\n                        bot=bot,\n                        event=event,\n                        state=state,\n                        stack=stack,\n                        dependency_cache=dependency_cache,\n                    ),\n                    (SkippedException,),\n                )\n\n        return True\n\n    return False\n\n\nasync def _apply_event_postprocessors(\n    bot: \"Bot\",\n    event: \"Event\",\n    state: T_State,\n    stack: AsyncExitStack | None = None,\n    dependency_cache: T_DependencyCache | None = None,\n    show_log: bool = True,\n) -> None:\n    \"\"\"运行事件后处理。\n\n    参数:\n        bot: Bot 对象\n        event: Event 对象\n        state: 会话状态\n        stack: 异步上下文栈\n        dependency_cache: 依赖缓存\n        show_log: 是否显示日志\n    \"\"\"\n    if not _event_postprocessors:\n        return\n\n    if show_log:\n        logger.debug(\"Running PostProcessors...\")\n\n    with catch(\n        {\n            Exception: _handle_exception(\n                \"<r><bg #f8bbd0>Error when running EventPostProcessors</bg #f8bbd0></r>\"\n            )\n        }\n    ):\n        async with anyio.create_task_group() as tg:\n            for proc in _event_postprocessors:\n                tg.start_soon(\n                    run_coro_with_catch,\n                    proc(\n                        bot=bot,\n                        event=event,\n                        state=state,\n                        stack=stack,\n                        dependency_cache=dependency_cache,\n                    ),\n                    (SkippedException,),\n                )\n\n\nasync def _apply_run_preprocessors(\n    bot: \"Bot\",\n    event: \"Event\",\n    state: T_State,\n    matcher: Matcher,\n    stack: AsyncExitStack | None = None,\n    dependency_cache: T_DependencyCache | None = None,\n) -> bool:\n    \"\"\"运行事件响应器运行前处理。\n\n    参数:\n        bot: Bot 对象\n        event: Event 对象\n        state: 会话状态\n        matcher: 事件响应器\n        stack: 异步上下文栈\n        dependency_cache: 依赖缓存\n\n    返回:\n        是否继续处理事件\n    \"\"\"\n    if not _run_preprocessors:\n        return True\n\n    # ensure matcher function can be correctly called\n    with (\n        matcher.ensure_context(bot, event),\n        catch(\n            {\n                IgnoredException: _handle_ignored_exception(\n                    f\"{matcher} running is <b>cancelled</b>\"\n                ),\n                Exception: _handle_exception(\n                    \"<r><bg #f8bbd0>Error when running RunPreProcessors. \"\n                    \"Running cancelled!</bg #f8bbd0></r>\"\n                ),\n            }\n        ),\n    ):\n        async with anyio.create_task_group() as tg:\n            for proc in _run_preprocessors:\n                tg.start_soon(\n                    run_coro_with_catch,\n                    proc(\n                        matcher=matcher,\n                        bot=bot,\n                        event=event,\n                        state=state,\n                        stack=stack,\n                        dependency_cache=dependency_cache,\n                    ),\n                    (SkippedException,),\n                )\n\n        return True\n\n    return False\n\n\nasync def _apply_run_postprocessors(\n    bot: \"Bot\",\n    event: \"Event\",\n    matcher: Matcher,\n    exception: Exception | None = None,\n    stack: AsyncExitStack | None = None,\n    dependency_cache: T_DependencyCache | None = None,\n) -> None:\n    \"\"\"运行事件响应器运行后处理。\n\n    参数:\n        bot: Bot 对象\n        event: Event 对象\n        matcher: 事件响应器\n        exception: 事件响应器运行异常\n        stack: 异步上下文栈\n        dependency_cache: 依赖缓存\n    \"\"\"\n    if not _run_postprocessors:\n        return\n\n    with (\n        matcher.ensure_context(bot, event),\n        catch(\n            {\n                Exception: _handle_exception(\n                    \"<r><bg #f8bbd0>Error when running RunPostProcessors\"\n                    \"</bg #f8bbd0></r>\"\n                )\n            }\n        ),\n    ):\n        async with anyio.create_task_group() as tg:\n            for proc in _run_postprocessors:\n                tg.start_soon(\n                    run_coro_with_catch,\n                    proc(\n                        matcher=matcher,\n                        exception=exception,\n                        bot=bot,\n                        event=event,\n                        state=matcher.state,\n                        stack=stack,\n                        dependency_cache=dependency_cache,\n                    ),\n                    (SkippedException,),\n                )\n\n\nasync def _check_matcher(\n    Matcher: type[Matcher],\n    bot: \"Bot\",\n    event: \"Event\",\n    state: T_State,\n    stack: AsyncExitStack | None = None,\n    dependency_cache: T_DependencyCache | None = None,\n) -> bool:\n    \"\"\"检查事件响应器是否符合运行条件。\n\n    请注意，过时的事件响应器将被**销毁**。对于未过时的事件响应器，将会一次检查其响应类型、权限和规则。\n\n    参数:\n        Matcher: 要检查的事件响应器\n        bot: Bot 对象\n        event: Event 对象\n        state: 会话状态\n        stack: 异步上下文栈\n        dependency_cache: 依赖缓存\n\n    返回:\n        bool: 是否符合运行条件\n    \"\"\"\n    if Matcher.expire_time and datetime.now() > Matcher.expire_time:\n        with contextlib.suppress(Exception):\n            Matcher.destroy()\n        return False\n\n    try:\n        if not await Matcher.check_perm(bot, event, stack, dependency_cache):\n            logger.trace(f\"Permission conditions not met for {Matcher}\")\n            return False\n    except Exception as e:\n        logger.opt(colors=True, exception=e).error(\n            f\"<r><bg #f8bbd0>Permission check failed for {Matcher}.</bg #f8bbd0></r>\"\n        )\n        return False\n\n    try:\n        if not await Matcher.check_rule(bot, event, state, stack, dependency_cache):\n            logger.trace(f\"Rule conditions not met for {Matcher}\")\n            return False\n    except Exception as e:\n        logger.opt(colors=True, exception=e).error(\n            f\"<r><bg #f8bbd0>Rule check failed for {Matcher}.</bg #f8bbd0></r>\"\n        )\n        return False\n\n    return True\n\n\nasync def _run_matcher(\n    Matcher: type[Matcher],\n    bot: \"Bot\",\n    event: \"Event\",\n    state: T_State,\n    stack: AsyncExitStack | None = None,\n    dependency_cache: T_DependencyCache | None = None,\n) -> None:\n    \"\"\"运行事件响应器。\n\n    临时事件响应器将在运行前被**销毁**。\n\n    参数:\n        Matcher: 事件响应器\n        bot: Bot 对象\n        event: Event 对象\n        state: 会话状态\n        stack: 异步上下文栈\n        dependency_cache: 依赖缓存\n\n    异常:\n        StopPropagation: 阻止事件继续传播\n    \"\"\"\n    logger.info(f\"Event will be handled by {Matcher}\")\n\n    if Matcher.temp:\n        with contextlib.suppress(Exception):\n            Matcher.destroy()\n\n    matcher = Matcher()\n\n    if not await _apply_run_preprocessors(\n        bot=bot,\n        event=event,\n        state=state,\n        matcher=matcher,\n        stack=stack,\n        dependency_cache=dependency_cache,\n    ):\n        return\n\n    exception = None\n\n    logger.debug(f\"Running {matcher}\")\n\n    try:\n        await matcher.run(bot, event, state, stack, dependency_cache)\n    except Exception as e:\n        logger.opt(colors=True, exception=e).error(\n            f\"<r><bg #f8bbd0>Running {matcher} failed.</bg #f8bbd0></r>\"\n        )\n        exception = e\n\n    await _apply_run_postprocessors(\n        bot=bot,\n        event=event,\n        matcher=matcher,\n        exception=exception,\n        stack=stack,\n        dependency_cache=dependency_cache,\n    )\n\n    if matcher.block:\n        raise StopPropagation\n\n\nasync def check_and_run_matcher(\n    Matcher: type[Matcher],\n    bot: \"Bot\",\n    event: \"Event\",\n    state: T_State,\n    stack: AsyncExitStack | None = None,\n    dependency_cache: T_DependencyCache | None = None,\n) -> None:\n    \"\"\"检查并运行事件响应器。\n\n    参数:\n        Matcher: 事件响应器\n        bot: Bot 对象\n        event: Event 对象\n        state: 会话状态\n        stack: 异步上下文栈\n        dependency_cache: 依赖缓存\n    \"\"\"\n    if not await _check_matcher(\n        Matcher=Matcher,\n        bot=bot,\n        event=event,\n        state=state,\n        stack=stack,\n        dependency_cache=dependency_cache,\n    ):\n        return\n\n    await _run_matcher(\n        Matcher=Matcher,\n        bot=bot,\n        event=event,\n        state=state,\n        stack=stack,\n        dependency_cache=dependency_cache,\n    )\n\n\nasync def handle_event(bot: \"Bot\", event: \"Event\") -> None:\n    \"\"\"处理一个事件。调用该函数以实现分发事件。\n\n    参数:\n        bot: Bot 对象\n        event: Event 对象\n\n    用法:\n        ```python\n        driver.task_group.start_soon(handle_event, bot, event)\n        ```\n    \"\"\"\n    show_log = True\n    log_msg = f\"<m>{escape_tag(bot.type)} {escape_tag(bot.self_id)}</m> | \"\n    try:\n        log_msg += event.get_log_string()\n    except NoLogException:\n        show_log = False\n    if show_log:\n        logger.opt(colors=True).success(log_msg)\n\n    state: dict[Any, Any] = {}\n    dependency_cache: T_DependencyCache = {}\n\n    # create event scope context\n    async with AsyncExitStack() as stack:\n        if not await _apply_event_preprocessors(\n            bot=bot,\n            event=event,\n            state=state,\n            stack=stack,\n            dependency_cache=dependency_cache,\n        ):\n            return\n\n        # Trie Match\n        try:\n            TrieRule.get_value(bot, event, state)\n        except Exception as e:\n            logger.opt(colors=True, exception=e).warning(\n                \"Error while parsing command for event\"\n            )\n\n        break_flag = False\n\n        def _handle_stop_propagation(exc_group: BaseExceptionGroup) -> None:\n            nonlocal break_flag\n\n            break_flag = True\n            logger.debug(\"Stop event propagation\")\n\n        # iterate through all priority until stop propagation\n        for priority in sorted(matchers.keys()):\n            if break_flag:\n                break\n\n            if show_log:\n                logger.debug(f\"Checking for matchers in priority {priority}...\")\n\n            if not (priority_matchers := matchers[priority]):\n                continue\n\n            with catch(\n                {\n                    StopPropagation: _handle_stop_propagation,\n                    Exception: _handle_exception(\n                        \"<r><bg #f8bbd0>Error when checking Matcher.</bg #f8bbd0></r>\"\n                    ),\n                }\n            ):\n                async with anyio.create_task_group() as tg:\n                    for matcher in priority_matchers:\n                        tg.start_soon(\n                            run_coro_with_shield,\n                            check_and_run_matcher(\n                                matcher,\n                                bot,\n                                event,\n                                state.copy(),\n                                stack,\n                                dependency_cache,\n                            ),\n                        )\n\n        if show_log:\n            logger.debug(\"Checking for matchers completed\")\n\n        await _apply_event_postprocessors(bot, event, state, stack, dependency_cache)\n"
  },
  {
    "path": "nonebot/params.py",
    "content": "\"\"\"本模块定义了依赖注入的各类参数。\n\nFrontMatter:\n    mdx:\n        format: md\n    sidebar_position: 4\n    description: nonebot.params 模块\n\"\"\"\n\nfrom collections.abc import Callable\nfrom re import Match\nfrom typing import Any, Literal, overload\n\nfrom nonebot.adapters import Event, Message, MessageSegment\nfrom nonebot.consts import (\n    CMD_ARG_KEY,\n    CMD_KEY,\n    CMD_START_KEY,\n    CMD_WHITESPACE_KEY,\n    ENDSWITH_KEY,\n    FULLMATCH_KEY,\n    KEYWORD_KEY,\n    PAUSE_PROMPT_RESULT_KEY,\n    PREFIX_KEY,\n    RAW_CMD_KEY,\n    RECEIVE_KEY,\n    REGEX_MATCHED,\n    REJECT_PROMPT_RESULT_KEY,\n    SHELL_ARGS,\n    SHELL_ARGV,\n    STARTSWITH_KEY,\n)\nfrom nonebot.internal.params import Arg as Arg\nfrom nonebot.internal.params import ArgParam as ArgParam\nfrom nonebot.internal.params import ArgPlainText as ArgPlainText\nfrom nonebot.internal.params import ArgPromptResult as ArgPromptResult\nfrom nonebot.internal.params import ArgStr as ArgStr\nfrom nonebot.internal.params import BotParam as BotParam\nfrom nonebot.internal.params import DefaultParam as DefaultParam\nfrom nonebot.internal.params import DependParam as DependParam\nfrom nonebot.internal.params import Depends as Depends\nfrom nonebot.internal.params import EventParam as EventParam\nfrom nonebot.internal.params import ExceptionParam as ExceptionParam\nfrom nonebot.internal.params import MatcherParam as MatcherParam\nfrom nonebot.internal.params import StateParam as StateParam\nfrom nonebot.matcher import Matcher\nfrom nonebot.typing import T_State\n\n\nasync def _event_type(event: Event) -> str:\n    return event.get_type()\n\n\ndef EventType() -> str:\n    \"\"\"{ref}`nonebot.adapters.Event` 类型参数\"\"\"\n    return Depends(_event_type)\n\n\nasync def _event_message(event: Event) -> Message:\n    return event.get_message()\n\n\ndef EventMessage() -> Any:\n    \"\"\"{ref}`nonebot.adapters.Event` 消息参数\"\"\"\n    return Depends(_event_message)\n\n\nasync def _event_plain_text(event: Event) -> str:\n    return event.get_plaintext()\n\n\ndef EventPlainText() -> str:\n    \"\"\"{ref}`nonebot.adapters.Event` 纯文本消息参数\"\"\"\n    return Depends(_event_plain_text)\n\n\nasync def _event_to_me(event: Event) -> bool:\n    return event.is_tome()\n\n\ndef EventToMe() -> bool:\n    \"\"\"{ref}`nonebot.adapters.Event` `to_me` 参数\"\"\"\n    return Depends(_event_to_me)\n\n\ndef _command(state: T_State) -> Message:\n    return state[PREFIX_KEY][CMD_KEY]\n\n\ndef Command() -> tuple[str, ...]:\n    \"\"\"消息命令元组\"\"\"\n    return Depends(_command)\n\n\ndef _raw_command(state: T_State) -> Message:\n    return state[PREFIX_KEY][RAW_CMD_KEY]\n\n\ndef RawCommand() -> str:\n    \"\"\"消息命令文本\"\"\"\n    return Depends(_raw_command)\n\n\ndef _command_arg(state: T_State) -> Message:\n    return state[PREFIX_KEY][CMD_ARG_KEY]\n\n\ndef CommandArg() -> Any:\n    \"\"\"消息命令参数\"\"\"\n    return Depends(_command_arg)\n\n\ndef _command_start(state: T_State) -> str:\n    return state[PREFIX_KEY][CMD_START_KEY]\n\n\ndef CommandStart() -> str:\n    \"\"\"消息命令开头\"\"\"\n    return Depends(_command_start)\n\n\ndef _command_whitespace(state: T_State) -> str:\n    return state[PREFIX_KEY][CMD_WHITESPACE_KEY]\n\n\ndef CommandWhitespace() -> str:\n    \"\"\"消息命令与参数之间的空白\"\"\"\n    return Depends(_command_whitespace)\n\n\ndef _shell_command_args(state: T_State) -> Any:\n    return state[SHELL_ARGS]  # Namespace or ParserExit\n\n\ndef ShellCommandArgs() -> Any:\n    \"\"\"shell 命令解析后的参数字典\"\"\"\n    return Depends(_shell_command_args, use_cache=False)\n\n\ndef _shell_command_argv(state: T_State) -> list[str | MessageSegment]:\n    return state[SHELL_ARGV]\n\n\ndef ShellCommandArgv() -> Any:\n    \"\"\"shell 命令原始参数列表\"\"\"\n    return Depends(_shell_command_argv, use_cache=False)\n\n\ndef _regex_matched(state: T_State) -> Match[str]:\n    return state[REGEX_MATCHED]\n\n\ndef RegexMatched() -> Match[str]:\n    \"\"\"正则匹配结果\"\"\"\n    return Depends(_regex_matched, use_cache=False)\n\n\ndef _regex_str(\n    groups: tuple[str | int, ...],\n) -> Callable[[T_State], str | tuple[str | Any, ...] | Any]:\n    def _regex_str_dependency(\n        state: T_State,\n    ) -> str | tuple[str | Any, ...] | Any:\n        return _regex_matched(state).group(*groups)\n\n    return _regex_str_dependency\n\n\n@overload\ndef RegexStr(group: Literal[0] = 0, /) -> str: ...\n\n\n@overload\ndef RegexStr(group: str | int, /) -> str | Any: ...\n\n\n@overload\ndef RegexStr(\n    group1: str | int, group2: str | int, /, *groups: str | int\n) -> tuple[str | Any, ...]: ...\n\n\ndef RegexStr(*groups: str | int) -> str | tuple[str | Any, ...] | Any:\n    \"\"\"正则匹配结果文本\"\"\"\n    return Depends(_regex_str(groups), use_cache=False)\n\n\ndef _regex_group(state: T_State) -> tuple[Any, ...]:\n    return _regex_matched(state).groups()\n\n\ndef RegexGroup() -> tuple[Any, ...]:\n    \"\"\"正则匹配结果 group 元组\"\"\"\n    return Depends(_regex_group, use_cache=False)\n\n\ndef _regex_dict(state: T_State) -> dict[str, Any]:\n    return _regex_matched(state).groupdict()\n\n\ndef RegexDict() -> dict[str, Any]:\n    \"\"\"正则匹配结果 group 字典\"\"\"\n    return Depends(_regex_dict, use_cache=False)\n\n\ndef _startswith(state: T_State) -> str:\n    return state[STARTSWITH_KEY]\n\n\ndef Startswith() -> str:\n    \"\"\"响应触发前缀\"\"\"\n    return Depends(_startswith, use_cache=False)\n\n\ndef _endswith(state: T_State) -> str:\n    return state[ENDSWITH_KEY]\n\n\ndef Endswith() -> str:\n    \"\"\"响应触发后缀\"\"\"\n    return Depends(_endswith, use_cache=False)\n\n\ndef _fullmatch(state: T_State) -> str:\n    return state[FULLMATCH_KEY]\n\n\ndef Fullmatch() -> str:\n    \"\"\"响应触发完整消息\"\"\"\n    return Depends(_fullmatch, use_cache=False)\n\n\ndef _keyword(state: T_State) -> str:\n    return state[KEYWORD_KEY]\n\n\ndef Keyword() -> str:\n    \"\"\"响应触发关键字\"\"\"\n    return Depends(_keyword, use_cache=False)\n\n\ndef Received(id: str | None = None, default: Any = None) -> Any:\n    \"\"\"`receive` 事件参数\"\"\"\n\n    def _received(matcher: \"Matcher\") -> Any:\n        return matcher.get_receive(id or \"\", default)\n\n    return Depends(_received, use_cache=False)\n\n\ndef LastReceived(default: Any = None) -> Any:\n    \"\"\"`last_receive` 事件参数\"\"\"\n\n    def _last_received(matcher: \"Matcher\") -> Any:\n        return matcher.get_last_receive(default)\n\n    return Depends(_last_received, use_cache=False)\n\n\ndef ReceivePromptResult(id: str | None = None) -> Any:\n    \"\"\"`receive` prompt 发送结果\"\"\"\n\n    def _receive_prompt_result(matcher: \"Matcher\") -> Any:\n        return matcher.state.get(\n            REJECT_PROMPT_RESULT_KEY.format(key=RECEIVE_KEY.format(id=id))\n        )\n\n    return Depends(_receive_prompt_result, use_cache=False)\n\n\ndef PausePromptResult() -> Any:\n    \"\"\"`pause` prompt 发送结果\"\"\"\n\n    def _pause_prompt_result(matcher: \"Matcher\") -> Any:\n        return matcher.state.get(PAUSE_PROMPT_RESULT_KEY)\n\n    return Depends(_pause_prompt_result, use_cache=False)\n\n\n__autodoc__ = {\n    \"Arg\": True,\n    \"ArgStr\": True,\n    \"Depends\": True,\n    \"ArgParam\": True,\n    \"BotParam\": True,\n    \"EventParam\": True,\n    \"StateParam\": True,\n    \"DependParam\": True,\n    \"ArgPlainText\": True,\n    \"DefaultParam\": True,\n    \"MatcherParam\": True,\n    \"ExceptionParam\": True,\n    \"ArgPromptResult\": True,\n}\n"
  },
  {
    "path": "nonebot/permission.py",
    "content": "\"\"\"本模块是 {ref}`nonebot.matcher.Matcher.permission` 的类型定义。\n\n每个{ref}`事件响应器 <nonebot.matcher.Matcher>`\n拥有一个 {ref}`nonebot.permission.Permission`，其中是 `PermissionChecker` 的集合。\n只要有一个 `PermissionChecker` 检查结果为 `True` 时就会继续运行。\n\nFrontMatter:\n    mdx:\n        format: md\n    sidebar_position: 6\n    description: nonebot.permission 模块\n\"\"\"\n\nfrom nonebot.adapters import Bot, Event\nfrom nonebot.internal.permission import USER as USER\nfrom nonebot.internal.permission import Permission as Permission\nfrom nonebot.internal.permission import User as User\nfrom nonebot.params import EventType\n\n\nclass Message:\n    \"\"\"检查是否为消息事件\"\"\"\n\n    __slots__ = ()\n\n    def __repr__(self) -> str:\n        return \"Message()\"\n\n    async def __call__(self, type: str = EventType()) -> bool:\n        return type == \"message\"\n\n\nclass Notice:\n    \"\"\"检查是否为通知事件\"\"\"\n\n    __slots__ = ()\n\n    def __repr__(self) -> str:\n        return \"Notice()\"\n\n    async def __call__(self, type: str = EventType()) -> bool:\n        return type == \"notice\"\n\n\nclass Request:\n    \"\"\"检查是否为请求事件\"\"\"\n\n    __slots__ = ()\n\n    def __repr__(self) -> str:\n        return \"Request()\"\n\n    async def __call__(self, type: str = EventType()) -> bool:\n        return type == \"request\"\n\n\nclass MetaEvent:\n    \"\"\"检查是否为元事件\"\"\"\n\n    __slots__ = ()\n\n    def __repr__(self) -> str:\n        return \"MetaEvent()\"\n\n    async def __call__(self, type: str = EventType()) -> bool:\n        return type == \"meta_event\"\n\n\nMESSAGE: Permission = Permission(Message())\n\"\"\"匹配任意 `message` 类型事件\n\n仅在需要同时捕获不同类型事件时使用，优先使用 message type 的 Matcher。\n\"\"\"\nNOTICE: Permission = Permission(Notice())\n\"\"\"匹配任意 `notice` 类型事件\n\n仅在需要同时捕获不同类型事件时使用，优先使用 notice type 的 Matcher。\n\"\"\"\nREQUEST: Permission = Permission(Request())\n\"\"\"匹配任意 `request` 类型事件\n\n仅在需要同时捕获不同类型事件时使用，优先使用 request type 的 Matcher。\n\"\"\"\nMETAEVENT: Permission = Permission(MetaEvent())\n\"\"\"匹配任意 `meta_event` 类型事件\n\n仅在需要同时捕获不同类型事件时使用，优先使用 meta_event type 的 Matcher。\n\"\"\"\n\n\nclass SuperUser:\n    \"\"\"检查当前事件是否是消息事件且属于超级管理员\"\"\"\n\n    __slots__ = ()\n\n    def __repr__(self) -> str:\n        return \"Superuser()\"\n\n    async def __call__(self, bot: Bot, event: Event) -> bool:\n        try:\n            user_id = event.get_user_id()\n        except Exception:\n            return False\n        return (\n            f\"{bot.adapter.get_name().split(maxsplit=1)[0].lower()}:{user_id}\"\n            in bot.config.superusers\n            or user_id in bot.config.superusers  # 兼容旧配置\n        )\n\n\nSUPERUSER: Permission = Permission(SuperUser())\n\"\"\"匹配任意超级用户事件\"\"\"\n\n__autodoc__ = {\n    \"Permission\": True,\n    \"Permission.__call__\": True,\n    \"User\": True,\n    \"USER\": True,\n}\n"
  },
  {
    "path": "nonebot/plugin/__init__.py",
    "content": "\"\"\"本模块为 NoneBot 插件开发提供便携的定义函数。\n\n## 快捷导入\n\n为方便使用，本模块从子模块导入了部分内容，以下内容可以直接通过本模块导入:\n\n- `on` => {ref}``on` <nonebot.plugin.on.on>`\n- `on_metaevent` => {ref}``on_metaevent` <nonebot.plugin.on.on_metaevent>`\n- `on_message` => {ref}``on_message` <nonebot.plugin.on.on_message>`\n- `on_notice` => {ref}``on_notice` <nonebot.plugin.on.on_notice>`\n- `on_request` => {ref}``on_request` <nonebot.plugin.on.on_request>`\n- `on_startswith` => {ref}``on_startswith` <nonebot.plugin.on.on_startswith>`\n- `on_endswith` => {ref}``on_endswith` <nonebot.plugin.on.on_endswith>`\n- `on_fullmatch` => {ref}``on_fullmatch` <nonebot.plugin.on.on_fullmatch>`\n- `on_keyword` => {ref}``on_keyword` <nonebot.plugin.on.on_keyword>`\n- `on_command` => {ref}``on_command` <nonebot.plugin.on.on_command>`\n- `on_shell_command` => {ref}``on_shell_command` <nonebot.plugin.on.on_shell_command>`\n- `on_regex` => {ref}``on_regex` <nonebot.plugin.on.on_regex>`\n- `on_type` => {ref}``on_type` <nonebot.plugin.on.on_type>`\n- `CommandGroup` => {ref}``CommandGroup` <nonebot.plugin.on.CommandGroup>`\n- `Matchergroup` => {ref}``MatcherGroup` <nonebot.plugin.on.MatcherGroup>`\n- `load_plugin` => {ref}``load_plugin` <nonebot.plugin.load.load_plugin>`\n- `load_plugins` => {ref}``load_plugins` <nonebot.plugin.load.load_plugins>`\n- `load_all_plugins` => {ref}``load_all_plugins` <nonebot.plugin.load.load_all_plugins>`\n- `load_from_json` => {ref}``load_from_json` <nonebot.plugin.load.load_from_json>`\n- `load_from_toml` => {ref}``load_from_toml` <nonebot.plugin.load.load_from_toml>`\n- `load_builtin_plugin` =>\n  {ref}``load_builtin_plugin` <nonebot.plugin.load.load_builtin_plugin>`\n- `load_builtin_plugins` =>\n  {ref}``load_builtin_plugins` <nonebot.plugin.load.load_builtin_plugins>`\n- `require` => {ref}``require` <nonebot.plugin.load.require>`\n- `PluginMetadata` => {ref}``PluginMetadata` <nonebot.plugin.model.PluginMetadata>`\n\nFrontMatter:\n    mdx:\n        format: md\n    sidebar_position: 0\n    description: nonebot.plugin 模块\n\"\"\"\n\nfrom contextvars import ContextVar\nfrom itertools import chain\nfrom types import ModuleType\nfrom typing import TypeVar\n\nfrom pydantic import BaseModel\n\nfrom nonebot import get_driver\nfrom nonebot.compat import model_dump, type_validate_python\nfrom nonebot.config import BaseSettings\n\nC = TypeVar(\"C\", bound=BaseModel)\n\n_plugins: dict[str, \"Plugin\"] = {}\n_managers: list[\"PluginManager\"] = []\n_current_plugin: ContextVar[\"Plugin | None\"] = ContextVar(\n    \"_current_plugin\", default=None\n)\n\n\ndef _module_name_to_plugin_name(module_name: str) -> str:\n    return module_name.rsplit(\".\", 1)[-1]\n\n\ndef _controlled_modules() -> dict[str, str]:\n    return {\n        plugin_id: module_name\n        for manager in _managers\n        for plugin_id, module_name in manager.controlled_modules.items()\n    }\n\n\ndef _find_parent_plugin_id(\n    module_name: str, controlled_modules: dict[str, str] | None = None\n) -> str | None:\n    if controlled_modules is None:\n        controlled_modules = _controlled_modules()\n    available = {\n        module_name: plugin_id for plugin_id, module_name in controlled_modules.items()\n    }\n    while \".\" in module_name:\n        module_name, _ = module_name.rsplit(\".\", 1)\n        if module_name in available:\n            return available[module_name]\n\n\ndef _module_name_to_plugin_id(\n    module_name: str, controlled_modules: dict[str, str] | None = None\n) -> str:\n    plugin_name = _module_name_to_plugin_name(module_name)\n    if parent_plugin_id := _find_parent_plugin_id(module_name, controlled_modules):\n        return f\"{parent_plugin_id}:{plugin_name}\"\n    return plugin_name\n\n\ndef _new_plugin(\n    module_name: str, module: ModuleType, manager: \"PluginManager\"\n) -> \"Plugin\":\n    plugin_id = _module_name_to_plugin_id(module_name)\n    if plugin_id in _plugins:\n        raise RuntimeError(\n            f\"Plugin {plugin_id} already exists! Check your plugin name.\"\n        )\n\n    parent_plugin_id = _find_parent_plugin_id(module_name)\n    if parent_plugin_id is not None and parent_plugin_id not in _plugins:\n        raise RuntimeError(\n            f\"Parent plugin {parent_plugin_id} must \"\n            f\"be loaded before loading {plugin_id}.\"\n        )\n    parent_plugin = _plugins[parent_plugin_id] if parent_plugin_id is not None else None\n\n    plugin = Plugin(\n        name=_module_name_to_plugin_name(module_name),\n        module=module,\n        module_name=module_name,\n        manager=manager,\n        parent_plugin=parent_plugin,\n    )\n    if parent_plugin:\n        parent_plugin.sub_plugins.add(plugin)\n\n    _plugins[plugin_id] = plugin\n    return plugin\n\n\ndef _revert_plugin(plugin: \"Plugin\") -> None:\n    if plugin.id_ not in _plugins:\n        raise RuntimeError(\"Plugin not found!\")\n    del _plugins[plugin.id_]\n    if parent_plugin := plugin.parent_plugin:\n        parent_plugin.sub_plugins.discard(plugin)\n\n\ndef get_plugin(plugin_id: str) -> \"Plugin | None\":\n    \"\"\"获取已经导入的某个插件。\n\n    如果为 `load_plugins` 文件夹导入的插件，则为文件(夹)名。\n\n    如果为嵌套的子插件，标识符为 `父插件标识符:子插件文件(夹)名`。\n\n    参数:\n        plugin_id: 插件标识符，即 {ref}`nonebot.plugin.model.Plugin.id_`。\n    \"\"\"\n    return _plugins.get(plugin_id)\n\n\ndef get_plugin_by_module_name(module_name: str) -> \"Plugin | None\":\n    \"\"\"通过模块名获取已经导入的某个插件。\n\n    如果提供的模块名为某个插件的子模块，同样会返回该插件。\n\n    参数:\n        module_name: 模块名，即 {ref}`nonebot.plugin.model.Plugin.module_name`。\n    \"\"\"\n    loaded = {plugin.module_name: plugin for plugin in _plugins.values()}\n    has_parent = True\n    while has_parent:\n        if module_name in loaded:\n            return loaded[module_name]\n        module_name, *has_parent = module_name.rsplit(\".\", 1)\n\n\ndef get_loaded_plugins() -> set[\"Plugin\"]:\n    \"\"\"获取当前已导入的所有插件。\"\"\"\n    return set(_plugins.values())\n\n\ndef get_available_plugin_names() -> set[str]:\n    \"\"\"获取当前所有可用的插件标识符（包含尚未加载的插件）。\"\"\"\n    return {*chain.from_iterable(manager.available_plugins for manager in _managers)}\n\n\ndef get_plugin_config(config: type[C]) -> C:\n    \"\"\"从全局配置获取当前插件需要的配置项。\"\"\"\n    global_config = get_driver().config\n    return type_validate_python(\n        config,\n        BaseSettings._settings_build_values(\n            config,\n            model_dump(global_config),\n            env_file=global_config._env_file,\n            env_file_encoding=global_config._env_file_encoding,\n            env_nested_delimiter=global_config._env_nested_delimiter,\n        ),\n    )\n\n\nfrom .load import inherit_supported_adapters as inherit_supported_adapters\nfrom .load import load_all_plugins as load_all_plugins\nfrom .load import load_builtin_plugin as load_builtin_plugin\nfrom .load import load_builtin_plugins as load_builtin_plugins\nfrom .load import load_from_json as load_from_json\nfrom .load import load_from_toml as load_from_toml\nfrom .load import load_plugin as load_plugin\nfrom .load import load_plugins as load_plugins\nfrom .load import require as require\nfrom .manager import PluginManager\nfrom .model import Plugin as Plugin\nfrom .model import PluginMetadata as PluginMetadata\nfrom .on import CommandGroup as CommandGroup\nfrom .on import MatcherGroup as MatcherGroup\nfrom .on import on as on\nfrom .on import on_command as on_command\nfrom .on import on_endswith as on_endswith\nfrom .on import on_fullmatch as on_fullmatch\nfrom .on import on_keyword as on_keyword\nfrom .on import on_message as on_message\nfrom .on import on_metaevent as on_metaevent\nfrom .on import on_notice as on_notice\nfrom .on import on_regex as on_regex\nfrom .on import on_request as on_request\nfrom .on import on_shell_command as on_shell_command\nfrom .on import on_startswith as on_startswith\nfrom .on import on_type as on_type\n"
  },
  {
    "path": "nonebot/plugin/load.py",
    "content": "\"\"\"本模块定义插件加载接口。\n\nFrontMatter:\n    mdx:\n        format: md\n    sidebar_position: 1\n    description: nonebot.plugin.load 模块\n\"\"\"\n\nfrom collections.abc import Iterable\nfrom itertools import chain\nimport json\nfrom pathlib import Path\nfrom types import ModuleType\n\nfrom nonebot.log import logger\nfrom nonebot.utils import path_to_module_name\n\nfrom . import _managers, _module_name_to_plugin_id, get_plugin\nfrom .manager import PluginManager\nfrom .model import Plugin\n\ntry:  # pragma: py-gte-311\n    import tomllib  # pyright: ignore[reportMissingImports]\nexcept ModuleNotFoundError:  # pragma: py-lt-311\n    import tomli as tomllib  # pyright: ignore[reportMissingImports]\n\n\ndef load_plugin(module_path: str | Path) -> Plugin | None:\n    \"\"\"加载单个插件，可以是本地插件或是通过 `pip` 安装的插件。\n\n    参数:\n        module_path: 插件名称 `path.to.your.plugin`\n            或插件路径 `pathlib.Path(path/to/your/plugin)`\n    \"\"\"\n    module_path = (\n        path_to_module_name(module_path)\n        if isinstance(module_path, Path)\n        else module_path\n    )\n    manager = PluginManager([module_path])\n    _managers.append(manager)\n    return manager.load_plugin(module_path)\n\n\ndef load_plugins(*plugin_dir: str) -> set[Plugin]:\n    \"\"\"导入文件夹下多个插件，以 `_` 开头的插件不会被导入!\n\n    参数:\n        plugin_dir: 文件夹路径\n    \"\"\"\n    manager = PluginManager(search_path=plugin_dir)\n    _managers.append(manager)\n    return manager.load_all_plugins()\n\n\ndef load_all_plugins(\n    module_path: Iterable[str], plugin_dir: Iterable[str]\n) -> set[Plugin]:\n    \"\"\"导入指定列表中的插件以及指定目录下多个插件，以 `_` 开头的插件不会被导入!\n\n    参数:\n        module_path: 指定插件集合\n        plugin_dir: 指定文件夹路径集合\n    \"\"\"\n    manager = PluginManager(module_path, plugin_dir)\n    _managers.append(manager)\n    return manager.load_all_plugins()\n\n\ndef load_from_json(file_path: str, encoding: str = \"utf-8\") -> set[Plugin]:\n    \"\"\"导入指定 json 文件中的 `plugins` 以及 `plugin_dirs` 下多个插件。\n    以 `_` 开头的插件不会被导入!\n\n    参数:\n        file_path: 指定 json 文件路径\n        encoding: 指定 json 文件编码\n\n    用法:\n        ```json title=plugins.json\n        {\n            \"plugins\": [\"some_plugin\"],\n            \"plugin_dirs\": [\"some_dir\"]\n        }\n        ```\n\n        ```python\n        nonebot.load_from_json(\"plugins.json\")\n        ```\n    \"\"\"\n    with open(file_path, encoding=encoding) as f:\n        data = json.load(f)\n    if not isinstance(data, dict):\n        raise TypeError(\"json file must contains a dict!\")\n    plugins = data.get(\"plugins\")\n    plugin_dirs = data.get(\"plugin_dirs\")\n    assert isinstance(plugins, list), \"plugins must be a list of plugin name\"\n    assert isinstance(plugin_dirs, list), \"plugin_dirs must be a list of directories\"\n    return load_all_plugins(set(plugins), set(plugin_dirs))\n\n\ndef load_from_toml(file_path: str, encoding: str = \"utf-8\") -> set[Plugin]:\n    \"\"\"导入指定 toml 文件 `[tool.nonebot]` 中的\n    `plugins` 以及 `plugin_dirs` 下多个插件。\n    以 `_` 开头的插件不会被导入!\n\n    参数:\n        file_path: 指定 toml 文件路径\n        encoding: 指定 toml 文件编码\n\n    用法:\n        新格式:\n\n        ```toml title=pyproject.toml\n        [tool.nonebot]\n        plugin_dirs = [\"some_dir\"]\n\n        [tool.nonebot.plugins]\n        some-store-plugin = [\"some_store_plugin\"]\n        \"@local\" = [\"some_local_plugin\"]\n        ```\n\n        旧格式:\n\n        ```toml title=pyproject.toml\n        [tool.nonebot]\n        plugins = [\"some_plugin\"]\n        plugin_dirs = [\"some_dir\"]\n        ```\n\n        ```python\n        nonebot.load_from_toml(\"pyproject.toml\")\n        ```\n    \"\"\"\n    with open(file_path, encoding=encoding) as f:\n        data = tomllib.loads(f.read())\n\n    nonebot_data = data.get(\"tool\", {}).get(\"nonebot\")\n    if nonebot_data is None:\n        raise ValueError(\"Cannot find '[tool.nonebot]' in given toml file!\")\n    if not isinstance(nonebot_data, dict):\n        raise TypeError(\"'[tool.nonebot]' must be a Table!\")\n    plugins = nonebot_data.get(\"plugins\", {})\n    plugin_dirs = nonebot_data.get(\"plugin_dirs\", [])\n    assert isinstance(plugins, (list, dict)), (\n        \"plugins must be a list or a dict of plugin name\"\n    )\n    assert isinstance(plugin_dirs, list), \"plugin_dirs must be a list of directories\"\n    if isinstance(plugins, list):\n        logger.warning(\"Legacy project format found! Upgrade with `nb upgrade-format`.\")\n    return load_all_plugins(\n        set(\n            chain.from_iterable(plugins.values())\n            if isinstance(plugins, dict)\n            else plugins\n        ),\n        plugin_dirs,\n    )\n\n\ndef load_builtin_plugin(name: str) -> Plugin | None:\n    \"\"\"导入 NoneBot 内置插件。\n\n    参数:\n        name: 插件名称\n    \"\"\"\n    return load_plugin(f\"nonebot.plugins.{name}\")\n\n\ndef load_builtin_plugins(*plugins: str) -> set[Plugin]:\n    \"\"\"导入多个 NoneBot 内置插件。\n\n    参数:\n        plugins: 插件名称列表\n    \"\"\"\n    return load_all_plugins([f\"nonebot.plugins.{p}\" for p in plugins], [])\n\n\ndef _find_manager_by_name(name: str) -> PluginManager | None:\n    for manager in reversed(_managers):\n        if (\n            name in manager.controlled_modules\n            or name in manager.controlled_modules.values()\n        ):\n            return manager\n\n\ndef require(name: str) -> ModuleType:\n    \"\"\"声明依赖插件。\n\n    参数:\n        name: 插件模块名或插件标识符，仅在已声明插件的情况下可使用标识符。\n\n    异常:\n        RuntimeError: 插件无法加载\n    \"\"\"\n    if \".\" in name:\n        # name is a module name\n        plugin = get_plugin(_module_name_to_plugin_id(name))\n    else:\n        # name is a plugin id or simple module name (equals to plugin id)\n        plugin = get_plugin(name)\n\n    # if plugin not loaded\n    if plugin is None:\n        # plugin already declared, module name / plugin id\n        if manager := _find_manager_by_name(name):\n            plugin = manager.load_plugin(name)\n\n        # plugin not declared, try to declare and load it\n        else:\n            plugin = load_plugin(name)\n\n    if plugin is None:\n        raise RuntimeError(f'Cannot load plugin \"{name}\"!')\n    return plugin.module\n\n\ndef inherit_supported_adapters(*names: str) -> set[str] | None:\n    \"\"\"获取已加载插件的适配器支持状态集合。\n\n    如果传入了多个插件名称，返回值会自动取交集。\n\n    参数:\n        names: 插件名称列表。\n\n    异常:\n        RuntimeError: 插件未加载\n        ValueError: 插件缺少元数据\n    \"\"\"\n    final_supported: set[str] | None = None\n\n    for name in names:\n        plugin = get_plugin(_module_name_to_plugin_id(name))\n        if plugin is None:\n            raise RuntimeError(\n                f'Plugin \"{name}\" is not loaded! You should require it first.'\n            )\n        meta = plugin.metadata\n        if meta is None:\n            raise ValueError(f'Plugin \"{name}\" has no metadata!')\n\n        if (raw := meta.supported_adapters) is None:\n            continue\n\n        support = {\n            f\"nonebot.adapters.{adapter[1:]}\" if adapter.startswith(\"~\") else adapter\n            for adapter in raw\n        }\n\n        final_supported = (\n            support if final_supported is None else (final_supported & support)\n        )\n\n    return final_supported\n"
  },
  {
    "path": "nonebot/plugin/manager.py",
    "content": "\"\"\"本模块实现插件加载流程。\n\n参考: [import hooks](https://docs.python.org/3/reference/import.html#import-hooks), [PEP302](https://www.python.org/dev/peps/pep-0302/)\n\nFrontMatter:\n    mdx:\n        format: md\n    sidebar_position: 5\n    description: nonebot.plugin.manager 模块\n\"\"\"\n\nfrom collections.abc import Iterable, Sequence\nimport importlib\nfrom importlib.abc import MetaPathFinder\nfrom importlib.machinery import PathFinder, SourceFileLoader\nfrom itertools import chain\nfrom pathlib import Path\nimport pkgutil\nimport sys\nfrom types import ModuleType\n\nfrom nonebot.log import logger\nfrom nonebot.utils import escape_tag, path_to_module_name\n\nfrom . import (\n    _current_plugin,\n    _managers,\n    _module_name_to_plugin_id,\n    _new_plugin,\n    _revert_plugin,\n)\nfrom .model import Plugin, PluginMetadata\n\n\nclass PluginManager:\n    \"\"\"插件管理器。\n\n    参数:\n        plugins: 独立插件模块名集合。\n        search_path: 插件搜索路径（文件夹），相对于当前工作目录。\n    \"\"\"\n\n    def __init__(\n        self,\n        plugins: Iterable[str] | None = None,\n        search_path: Iterable[str] | None = None,\n    ):\n        # simple plugin not in search path\n        self.plugins: set[str] = set(plugins or [])\n        self.search_path: set[str] = set(search_path or [])\n\n        # cache plugins\n        self._third_party_plugin_ids: dict[str, str] = {}\n        self._searched_plugin_ids: dict[str, str] = {}\n        self._prepare_plugins()\n\n    def __repr__(self) -> str:\n        return f\"PluginManager(available_plugins={self.controlled_modules})\"\n\n    @property\n    def third_party_plugins(self) -> set[str]:\n        \"\"\"返回所有独立插件标识符。\"\"\"\n        return set(self._third_party_plugin_ids.keys())\n\n    @property\n    def searched_plugins(self) -> set[str]:\n        \"\"\"返回已搜索到的插件标识符。\"\"\"\n        return set(self._searched_plugin_ids.keys())\n\n    @property\n    def available_plugins(self) -> set[str]:\n        \"\"\"返回当前插件管理器中可用的插件标识符。\"\"\"\n        return self.third_party_plugins | self.searched_plugins\n\n    @property\n    def controlled_modules(self) -> dict[str, str]:\n        \"\"\"返回当前插件管理器中控制的插件标识符与模块路径映射字典。\"\"\"\n        return dict(\n            chain(\n                self._third_party_plugin_ids.items(), self._searched_plugin_ids.items()\n            )\n        )\n\n    def _previous_controlled_modules(self) -> dict[str, str]:\n        _pre_managers: list[PluginManager]\n        if self in _managers:\n            _pre_managers = _managers[: _managers.index(self)]\n        else:\n            _pre_managers = _managers[:]\n\n        return {\n            plugin_id: module_name\n            for manager in _pre_managers\n            for plugin_id, module_name in manager.controlled_modules.items()\n        }\n\n    def _prepare_plugins(self) -> set[str]:\n        \"\"\"搜索插件并缓存插件名称。\"\"\"\n        # get all previous ready to load plugins\n        previous_plugin_ids = self._previous_controlled_modules()\n\n        # if self not in global managers, merge self's controlled modules\n        def get_controlled_modules():\n            return (\n                previous_plugin_ids\n                if self in _managers\n                else {**previous_plugin_ids, **self.controlled_modules}\n            )\n\n        # check third party plugins\n        for plugin in self.plugins:\n            plugin_id = _module_name_to_plugin_id(plugin, get_controlled_modules())\n            if (\n                plugin_id in self._third_party_plugin_ids\n                or plugin_id in previous_plugin_ids\n            ):\n                raise RuntimeError(\n                    f\"Plugin already exists: {plugin_id}! Check your plugin name\"\n                )\n            self._third_party_plugin_ids[plugin_id] = plugin\n\n        # check plugins in search path\n        for module_info in pkgutil.iter_modules(self.search_path):\n            # ignore if startswith \"_\"\n            if module_info.name.startswith(\"_\"):\n                continue\n\n            if not (\n                module_spec := module_info.module_finder.find_spec(\n                    module_info.name, None\n                )\n            ):\n                continue\n\n            if not module_spec.origin:\n                continue\n\n            # get module name from path, pkgutil does not return the actual module name\n            module_path = Path(module_spec.origin).resolve()\n            module_name = path_to_module_name(module_path)\n            plugin_id = _module_name_to_plugin_id(module_name, get_controlled_modules())\n\n            if (\n                plugin_id in previous_plugin_ids\n                or plugin_id in self._third_party_plugin_ids\n                or plugin_id in self._searched_plugin_ids\n            ):\n                raise RuntimeError(\n                    f\"Plugin already exists: {plugin_id}! Check your plugin name\"\n                )\n\n            self._searched_plugin_ids[plugin_id] = module_name\n\n        return self.available_plugins\n\n    def load_plugin(self, name: str) -> Plugin | None:\n        \"\"\"加载指定插件。\n\n        可以使用完整插件模块名或者插件标识符加载。\n\n        参数:\n            name: 插件名称或插件标识符。\n        \"\"\"\n\n        try:\n            # load using plugin id\n            if name in self._third_party_plugin_ids:\n                module = importlib.import_module(self._third_party_plugin_ids[name])\n            elif name in self._searched_plugin_ids:\n                module = importlib.import_module(self._searched_plugin_ids[name])\n            # load using module name\n            elif (\n                name in self._third_party_plugin_ids.values()\n                or name in self._searched_plugin_ids.values()\n            ):\n                module = importlib.import_module(name)\n            else:\n                raise RuntimeError(f\"Plugin not found: {name}! Check your plugin name\")\n\n            if (\n                plugin := getattr(module, \"__plugin__\", None)\n            ) is None or not isinstance(plugin, Plugin):\n                raise RuntimeError(\n                    f\"Module {module.__name__} is not loaded as a plugin! \"\n                    f\"Make sure not to import it before loading.\"\n                )\n            logger.opt(colors=True).success(\n                f'Succeeded to load plugin \"<y>{escape_tag(plugin.id_)}</y>\"'\n                + (\n                    f' from \"<m>{escape_tag(plugin.module_name)}</m>\"'\n                    if plugin.module_name != plugin.id_\n                    else \"\"\n                )\n            )\n            return plugin\n        except Exception as e:\n            logger.opt(colors=True, exception=e).error(\n                f'<r><bg #f8bbd0>Failed to import \"{escape_tag(name)}\"</bg #f8bbd0></r>'\n            )\n\n    def load_all_plugins(self) -> set[Plugin]:\n        \"\"\"加载所有可用插件。\"\"\"\n\n        return set(\n            filter(None, (self.load_plugin(name) for name in self.available_plugins))\n        )\n\n\nclass PluginFinder(MetaPathFinder):\n    def find_spec(\n        self,\n        fullname: str,\n        path: Sequence[str] | None,\n        target: ModuleType | None = None,\n    ):\n        if _managers:\n            module_spec = PathFinder.find_spec(fullname, path, target)\n            if not module_spec:\n                return\n            module_origin = module_spec.origin\n            if not module_origin:\n                return\n\n            for manager in reversed(_managers):\n                if fullname in manager.controlled_modules.values():\n                    module_spec.loader = PluginLoader(manager, fullname, module_origin)\n                    return module_spec\n        return\n\n\nclass PluginLoader(SourceFileLoader):\n    def __init__(self, manager: PluginManager, fullname: str, path: str) -> None:\n        self.manager = manager\n        self.loaded = False\n        super().__init__(fullname, path)\n\n    def create_module(self, spec) -> ModuleType | None:\n        if self.name in sys.modules:\n            self.loaded = True\n            return sys.modules[self.name]\n        # return None to use default module creation\n        return super().create_module(spec)\n\n    def exec_module(self, module: ModuleType) -> None:\n        if self.loaded:\n            return\n\n        # create plugin before executing\n        plugin = _new_plugin(self.name, module, self.manager)\n        setattr(module, \"__plugin__\", plugin)\n\n        # enter plugin context\n        _plugin_token = _current_plugin.set(plugin)\n\n        try:\n            super().exec_module(module)\n        except Exception:\n            _revert_plugin(plugin)\n            raise\n        finally:\n            # leave plugin context\n            _current_plugin.reset(_plugin_token)\n\n        # get plugin metadata\n        metadata: PluginMetadata | None = getattr(module, \"__plugin_meta__\", None)\n        plugin.metadata = metadata\n\n        return\n\n\nsys.meta_path.insert(0, PluginFinder())\n"
  },
  {
    "path": "nonebot/plugin/model.py",
    "content": "\"\"\"本模块定义插件相关信息。\n\nFrontMatter:\n    mdx:\n        format: md\n    sidebar_position: 3\n    description: nonebot.plugin.model 模块\n\"\"\"\n\nimport contextlib\nfrom dataclasses import dataclass, field\nfrom types import ModuleType\nfrom typing import TYPE_CHECKING, Any, Type  # noqa: UP035\n\nfrom pydantic import BaseModel\n\nfrom nonebot.matcher import Matcher\nfrom nonebot.utils import resolve_dot_notation\n\nif TYPE_CHECKING:\n    from nonebot.adapters import Adapter\n\n    from .manager import PluginManager\n\n\n@dataclass(eq=False)\nclass PluginMetadata:\n    \"\"\"插件元信息，由插件编写者提供\"\"\"\n\n    name: str\n    \"\"\"插件名称\"\"\"\n    description: str\n    \"\"\"插件功能介绍\"\"\"\n    usage: str\n    \"\"\"插件使用方法\"\"\"\n    type: str | None = None\n    \"\"\"插件类型，用于商店分类\"\"\"\n    homepage: str | None = None\n    \"\"\"插件主页\"\"\"\n    config: Type[BaseModel] | None = None  # noqa: UP006\n    \"\"\"插件配置项\"\"\"\n    supported_adapters: set[str] | None = None\n    \"\"\"插件支持的适配器模块路径\n\n    格式为 `<module>[:<Adapter>]`，`~` 为 `nonebot.adapters.` 的缩写。\n\n    `None` 表示支持**所有适配器**。\n    \"\"\"\n    extra: dict[Any, Any] = field(default_factory=dict)\n    \"\"\"插件额外信息，可由插件编写者自由扩展定义\"\"\"\n\n    def get_supported_adapters(self) -> set[Type[\"Adapter\"]] | None:  # noqa: UP006\n        \"\"\"获取当前已安装的插件支持适配器类列表\"\"\"\n        if self.supported_adapters is None:\n            return None\n\n        adapters = set()\n        for adapter in self.supported_adapters:\n            with contextlib.suppress(ModuleNotFoundError, AttributeError):\n                adapters.add(\n                    resolve_dot_notation(adapter, \"Adapter\", \"nonebot.adapters.\")\n                )\n        return adapters\n\n\n@dataclass(eq=False)\nclass Plugin:\n    \"\"\"存储插件信息\"\"\"\n\n    name: str\n    \"\"\"插件名称，NoneBot 使用 文件/文件夹 名称作为插件名称\"\"\"\n    module: ModuleType\n    \"\"\"插件模块对象\"\"\"\n    module_name: str\n    \"\"\"点分割模块路径\"\"\"\n    manager: \"PluginManager\"\n    \"\"\"导入该插件的插件管理器\"\"\"\n    matcher: set[type[Matcher]] = field(default_factory=set)\n    \"\"\"插件加载时定义的 `Matcher`\"\"\"\n    parent_plugin: \"Plugin | None\" = None\n    \"\"\"父插件\"\"\"\n    sub_plugins: set[\"Plugin\"] = field(default_factory=set)\n    \"\"\"子插件集合\"\"\"\n    metadata: PluginMetadata | None = None\n    \"\"\"插件元信息\"\"\"\n\n    @property\n    def id_(self) -> str:\n        \"\"\"插件索引标识\"\"\"\n        return (\n            f\"{self.parent_plugin.id_}:{self.name}\" if self.parent_plugin else self.name\n        )\n"
  },
  {
    "path": "nonebot/plugin/on.py",
    "content": "\"\"\"本模块定义事件响应器便携定义函数。\n\nFrontMatter:\n    mdx:\n        format: md\n    sidebar_position: 2\n    description: nonebot.plugin.on 模块\n\"\"\"\n\nfrom datetime import datetime, timedelta\nimport inspect\nimport re\nfrom types import ModuleType\nfrom typing import Any\nimport warnings\n\nfrom nonebot.adapters import Event\nfrom nonebot.dependencies import Dependent\nfrom nonebot.matcher import Matcher, MatcherSource\nfrom nonebot.permission import Permission\nfrom nonebot.rule import (\n    ArgumentParser,\n    Rule,\n    command,\n    endswith,\n    fullmatch,\n    is_type,\n    keyword,\n    regex,\n    shell_command,\n    startswith,\n)\nfrom nonebot.typing import T_Handler, T_PermissionChecker, T_RuleChecker, T_State\n\nfrom . import get_plugin_by_module_name\nfrom .manager import _current_plugin\nfrom .model import Plugin\n\n\ndef store_matcher(matcher: type[Matcher]) -> None:\n    \"\"\"存储一个事件响应器到插件。\n\n    参数:\n        matcher: 事件响应器\n    \"\"\"\n    # only store the matcher defined when plugin loading\n    if plugin := _current_plugin.get():\n        plugin.matcher.add(matcher)\n\n\ndef get_matcher_plugin(depth: int = 1) -> Plugin | None:  # pragma: no cover\n    \"\"\"获取事件响应器定义所在插件。\n\n    **Deprecated**, 请使用 {ref}`nonebot.plugin.on.get_matcher_source` 获取信息。\n\n    参数:\n        depth: 调用栈深度\n    \"\"\"\n    warnings.warn(\n        \"`get_matcher_plugin` is deprecated, please use `get_matcher_source` instead\",\n        DeprecationWarning,\n    )\n    return (source := get_matcher_source(depth + 1)) and source.plugin\n\n\ndef get_matcher_module(depth: int = 1) -> ModuleType | None:  # pragma: no cover\n    \"\"\"获取事件响应器定义所在模块。\n\n    **Deprecated**, 请使用 {ref}`nonebot.plugin.on.get_matcher_source` 获取信息。\n\n    参数:\n        depth: 调用栈深度\n    \"\"\"\n    warnings.warn(\n        \"`get_matcher_module` is deprecated, please use `get_matcher_source` instead\",\n        DeprecationWarning,\n    )\n    return (source := get_matcher_source(depth + 1)) and source.module\n\n\ndef get_matcher_source(depth: int = 0) -> MatcherSource | None:\n    \"\"\"获取事件响应器定义所在源码信息。\n\n    参数:\n        depth: 调用栈深度\n    \"\"\"\n    current_frame = inspect.currentframe()\n    if current_frame is None:\n        return None\n\n    frame = current_frame\n    d = depth + 1\n    while d > 0:\n        frame = frame.f_back\n        if frame is None:\n            raise ValueError(\"Depth out of range\")\n        d -= 1\n\n    module_name = (module := inspect.getmodule(frame)) and module.__name__\n\n    # matcher defined when plugin loading\n    plugin: Plugin | None = _current_plugin.get()\n    # matcher defined when plugin running\n    if plugin is None and module_name:\n        plugin = get_plugin_by_module_name(module_name)\n\n    return MatcherSource(\n        plugin_id=plugin and plugin.id_,\n        module_name=module_name,\n        lineno=frame.f_lineno,\n    )\n\n\ndef on(\n    type: str = \"\",\n    rule: Rule | T_RuleChecker | None = None,\n    permission: Permission | T_PermissionChecker | None = None,\n    *,\n    handlers: list[T_Handler | Dependent[Any]] | None = None,\n    temp: bool = False,\n    expire_time: datetime | timedelta | None = None,\n    priority: int = 1,\n    block: bool = False,\n    state: T_State | None = None,\n    _depth: int = 0,\n) -> type[Matcher]:\n    \"\"\"注册一个基础事件响应器，可自定义类型。\n\n    参数:\n        type: 事件响应器类型\n        rule: 事件响应规则\n        permission: 事件响应权限\n        handlers: 事件处理函数列表\n        temp: 是否为临时事件响应器（仅执行一次）\n        expire_time: 事件响应器最终有效时间点，过时即被删除\n        priority: 事件响应器优先级\n        block: 是否阻止事件向更低优先级传递\n        state: 默认 state\n    \"\"\"\n    matcher = Matcher.new(\n        type,\n        Rule() & rule,\n        Permission() | permission,\n        temp=temp,\n        expire_time=expire_time,\n        priority=priority,\n        block=block,\n        handlers=handlers,\n        source=get_matcher_source(_depth + 1),\n        default_state=state,\n    )\n    store_matcher(matcher)\n    return matcher\n\n\ndef on_metaevent(*args, _depth: int = 0, **kwargs) -> type[Matcher]:\n    \"\"\"注册一个元事件响应器。\n\n    参数:\n        rule: 事件响应规则\n        permission: 事件响应权限\n        handlers: 事件处理函数列表\n        temp: 是否为临时事件响应器（仅执行一次）\n        expire_time: 事件响应器最终有效时间点，过时即被删除\n        priority: 事件响应器优先级\n        block: 是否阻止事件向更低优先级传递\n        state: 默认 state\n    \"\"\"\n    return on(\"meta_event\", *args, **kwargs, _depth=_depth + 1)\n\n\ndef on_message(*args, _depth: int = 0, **kwargs) -> type[Matcher]:\n    \"\"\"注册一个消息事件响应器。\n\n    参数:\n        rule: 事件响应规则\n        permission: 事件响应权限\n        handlers: 事件处理函数列表\n        temp: 是否为临时事件响应器（仅执行一次）\n        expire_time: 事件响应器最终有效时间点，过时即被删除\n        priority: 事件响应器优先级\n        block: 是否阻止事件向更低优先级传递\n        state: 默认 state\n    \"\"\"\n    kwargs.setdefault(\"block\", True)\n    return on(\"message\", *args, **kwargs, _depth=_depth + 1)\n\n\ndef on_notice(*args, _depth: int = 0, **kwargs) -> type[Matcher]:\n    \"\"\"注册一个通知事件响应器。\n\n    参数:\n        rule: 事件响应规则\n        permission: 事件响应权限\n        handlers: 事件处理函数列表\n        temp: 是否为临时事件响应器（仅执行一次）\n        expire_time: 事件响应器最终有效时间点，过时即被删除\n        priority: 事件响应器优先级\n        block: 是否阻止事件向更低优先级传递\n        state: 默认 state\n    \"\"\"\n    return on(\"notice\", *args, **kwargs, _depth=_depth + 1)\n\n\ndef on_request(*args, _depth: int = 0, **kwargs) -> type[Matcher]:\n    \"\"\"注册一个请求事件响应器。\n\n    参数:\n        rule: 事件响应规则\n        permission: 事件响应权限\n        handlers: 事件处理函数列表\n        temp: 是否为临时事件响应器（仅执行一次）\n        expire_time: 事件响应器最终有效时间点，过时即被删除\n        priority: 事件响应器优先级\n        block: 是否阻止事件向更低优先级传递\n        state: 默认 state\n    \"\"\"\n    return on(\"request\", *args, **kwargs, _depth=_depth + 1)\n\n\ndef on_startswith(\n    msg: str | tuple[str, ...],\n    rule: Rule | T_RuleChecker | None = None,\n    ignorecase: bool = False,\n    _depth: int = 0,\n    **kwargs,\n) -> type[Matcher]:\n    \"\"\"注册一个消息事件响应器，并且当消息的**文本部分**以指定内容开头时响应。\n\n    参数:\n        msg: 指定消息开头内容\n        rule: 事件响应规则\n        ignorecase: 是否忽略大小写\n        permission: 事件响应权限\n        handlers: 事件处理函数列表\n        temp: 是否为临时事件响应器（仅执行一次）\n        expire_time: 事件响应器最终有效时间点，过时即被删除\n        priority: 事件响应器优先级\n        block: 是否阻止事件向更低优先级传递\n        state: 默认 state\n    \"\"\"\n    return on_message(startswith(msg, ignorecase) & rule, **kwargs, _depth=_depth + 1)\n\n\ndef on_endswith(\n    msg: str | tuple[str, ...],\n    rule: Rule | T_RuleChecker | None = None,\n    ignorecase: bool = False,\n    _depth: int = 0,\n    **kwargs,\n) -> type[Matcher]:\n    \"\"\"注册一个消息事件响应器，并且当消息的**文本部分**以指定内容结尾时响应。\n\n    参数:\n        msg: 指定消息结尾内容\n        rule: 事件响应规则\n        ignorecase: 是否忽略大小写\n        permission: 事件响应权限\n        handlers: 事件处理函数列表\n        temp: 是否为临时事件响应器（仅执行一次）\n        expire_time: 事件响应器最终有效时间点，过时即被删除\n        priority: 事件响应器优先级\n        block: 是否阻止事件向更低优先级传递\n        state: 默认 state\n    \"\"\"\n    return on_message(endswith(msg, ignorecase) & rule, **kwargs, _depth=_depth + 1)\n\n\ndef on_fullmatch(\n    msg: str | tuple[str, ...],\n    rule: Rule | T_RuleChecker | None = None,\n    ignorecase: bool = False,\n    _depth: int = 0,\n    **kwargs,\n) -> type[Matcher]:\n    \"\"\"注册一个消息事件响应器，并且当消息的**文本部分**与指定内容完全一致时响应。\n\n    参数:\n        msg: 指定消息全匹配内容\n        rule: 事件响应规则\n        ignorecase: 是否忽略大小写\n        permission: 事件响应权限\n        handlers: 事件处理函数列表\n        temp: 是否为临时事件响应器（仅执行一次）\n        expire_time: 事件响应器最终有效时间点，过时即被删除\n        priority: 事件响应器优先级\n        block: 是否阻止事件向更低优先级传递\n        state: 默认 state\n    \"\"\"\n    return on_message(fullmatch(msg, ignorecase) & rule, **kwargs, _depth=_depth + 1)\n\n\ndef on_keyword(\n    keywords: set[str],\n    rule: Rule | T_RuleChecker | None = None,\n    _depth: int = 0,\n    **kwargs,\n) -> type[Matcher]:\n    \"\"\"注册一个消息事件响应器，并且当消息纯文本部分包含关键词时响应。\n\n    参数:\n        keywords: 关键词列表\n        rule: 事件响应规则\n        permission: 事件响应权限\n        handlers: 事件处理函数列表\n        temp: 是否为临时事件响应器（仅执行一次）\n        expire_time: 事件响应器最终有效时间点，过时即被删除\n        priority: 事件响应器优先级\n        block: 是否阻止事件向更低优先级传递\n        state: 默认 state\n    \"\"\"\n    return on_message(keyword(*keywords) & rule, **kwargs, _depth=_depth + 1)\n\n\ndef on_command(\n    cmd: str | tuple[str, ...],\n    rule: Rule | T_RuleChecker | None = None,\n    aliases: set[str | tuple[str, ...]] | None = None,\n    force_whitespace: str | bool | None = None,\n    _depth: int = 0,\n    **kwargs,\n) -> type[Matcher]:\n    \"\"\"注册一个消息事件响应器，并且当消息以指定命令开头时响应。\n\n    命令匹配规则参考: `命令形式匹配 <rule.md#command-command>`_\n\n    参数:\n        cmd: 指定命令内容\n        rule: 事件响应规则\n        aliases: 命令别名\n        force_whitespace: 是否强制命令后必须有指定空白符\n        permission: 事件响应权限\n        handlers: 事件处理函数列表\n        temp: 是否为临时事件响应器（仅执行一次）\n        expire_time: 事件响应器最终有效时间点，过时即被删除\n        priority: 事件响应器优先级\n        block: 是否阻止事件向更低优先级传递\n        state: 默认 state\n    \"\"\"\n\n    commands = {cmd} | (aliases or set())\n    kwargs.setdefault(\"block\", False)\n    return on_message(\n        command(*commands, force_whitespace=force_whitespace) & rule,\n        **kwargs,\n        _depth=_depth + 1,\n    )\n\n\ndef on_shell_command(\n    cmd: str | tuple[str, ...],\n    rule: Rule | T_RuleChecker | None = None,\n    aliases: set[str | tuple[str, ...]] | None = None,\n    parser: ArgumentParser | None = None,\n    _depth: int = 0,\n    **kwargs,\n) -> type[Matcher]:\n    \"\"\"注册一个支持 `shell_like` 解析参数的命令消息事件响应器。\n\n    与普通的 `on_command` 不同的是，在添加 `parser` 参数时, 响应器会自动处理消息。\n\n    可以通过 {ref}`nonebot.params.ShellCommandArgv` 获取原始参数列表，\n    通过 {ref}`nonebot.params.ShellCommandArgs` 获取解析后的参数字典。\n\n    参数:\n        cmd: 指定命令内容\n        rule: 事件响应规则\n        aliases: 命令别名\n        parser: `nonebot.rule.ArgumentParser` 对象\n        permission: 事件响应权限\n        handlers: 事件处理函数列表\n        temp: 是否为临时事件响应器（仅执行一次）\n        expire_time: 事件响应器最终有效时间点，过时即被删除\n        priority: 事件响应器优先级\n        block: 是否阻止事件向更低优先级传递\n        state: 默认 state\n    \"\"\"\n\n    commands = {cmd} | (aliases or set())\n    return on_message(\n        shell_command(*commands, parser=parser) & rule,\n        **kwargs,\n        _depth=_depth + 1,\n    )\n\n\ndef on_regex(\n    pattern: str,\n    flags: int | re.RegexFlag = 0,\n    rule: Rule | T_RuleChecker | None = None,\n    _depth: int = 0,\n    **kwargs,\n) -> type[Matcher]:\n    \"\"\"注册一个消息事件响应器，并且当消息匹配正则表达式时响应。\n\n    命令匹配规则参考: `正则匹配 <rule.md#regex-regex-flags-0>`_\n\n    参数:\n        pattern: 正则表达式\n        flags: 正则匹配标志\n        rule: 事件响应规则\n        permission: 事件响应权限\n        handlers: 事件处理函数列表\n        temp: 是否为临时事件响应器（仅执行一次）\n        expire_time: 事件响应器最终有效时间点，过时即被删除\n        priority: 事件响应器优先级\n        block: 是否阻止事件向更低优先级传递\n        state: 默认 state\n    \"\"\"\n    return on_message(regex(pattern, flags) & rule, **kwargs, _depth=_depth + 1)\n\n\ndef on_type(\n    types: type[Event] | tuple[type[Event], ...],\n    rule: Rule | T_RuleChecker | None = None,\n    *,\n    _depth: int = 0,\n    **kwargs,\n) -> type[Matcher]:\n    \"\"\"注册一个事件响应器，并且当事件为指定类型时响应。\n\n    参数:\n        types: 事件类型\n        rule: 事件响应规则\n        permission: 事件响应权限\n        handlers: 事件处理函数列表\n        temp: 是否为临时事件响应器（仅执行一次）\n        expire_time: 事件响应器最终有效时间点，过时即被删除\n        priority: 事件响应器优先级\n        block: 是否阻止事件向更低优先级传递\n        state: 默认 state\n    \"\"\"\n    event_types = types if isinstance(types, tuple) else (types,)\n    return on(rule=is_type(*event_types) & rule, **kwargs, _depth=_depth + 1)\n\n\nclass _Group:\n    def __init__(self, **kwargs):\n        \"\"\"创建一个事件响应器组合，参数为默认值，与 `on` 一致\"\"\"\n        self.matchers: list[type[Matcher]] = []\n        \"\"\"组内事件响应器列表\"\"\"\n        self.base_kwargs: dict[str, Any] = kwargs\n        \"\"\"其他传递给 `on` 的参数默认值\"\"\"\n\n    def _get_final_kwargs(\n        self, update: dict[str, Any], *, exclude: set[str] | None = None\n    ) -> dict[str, Any]:\n        \"\"\"获取最终传递给 `on` 的参数\n\n        参数:\n            update: 更新的关键字参数\n            exclude: 需要排除的参数\n        \"\"\"\n        final_kwargs = self.base_kwargs.copy()\n        final_kwargs.update(update)\n        if exclude:\n            for key in exclude:\n                final_kwargs.pop(key, None)\n        final_kwargs[\"_depth\"] = 1\n        return final_kwargs\n\n\nclass CommandGroup(_Group):\n    \"\"\"命令组，用于声明一组有相同名称前缀的命令。\n\n    参数:\n        cmd: 指定命令内容\n        prefix_aliases: 是否影响命令别名，给命令别名加前缀\n        rule: 事件响应规则\n        permission: 事件响应权限\n        handlers: 事件处理函数列表\n        temp: 是否为临时事件响应器（仅执行一次）\n        expire_time: 事件响应器最终有效时间点，过时即被删除\n        priority: 事件响应器优先级\n        block: 是否阻止事件向更低优先级传递\n        state: 默认 state\n    \"\"\"\n\n    def __init__(\n        self, cmd: str | tuple[str, ...], prefix_aliases: bool = False, **kwargs\n    ):\n        \"\"\"命令前缀\"\"\"\n        super().__init__(**kwargs)\n        self.basecmd: tuple[str, ...] = (cmd,) if isinstance(cmd, str) else cmd\n        self.base_kwargs.pop(\"aliases\", None)\n        self.prefix_aliases = prefix_aliases\n\n    def __repr__(self) -> str:\n        return f\"CommandGroup(cmd={self.basecmd}, matchers={len(self.matchers)})\"\n\n    def command(self, cmd: str | tuple[str, ...], **kwargs) -> type[Matcher]:\n        \"\"\"注册一个新的命令。新参数将会覆盖命令组默认值\n\n        参数:\n            cmd: 指定命令内容\n            aliases: 命令别名\n            force_whitespace: 是否强制命令后必须有指定空白符\n            rule: 事件响应规则\n            permission: 事件响应权限\n            handlers: 事件处理函数列表\n            temp: 是否为临时事件响应器（仅执行一次）\n            expire_time: 事件响应器最终有效时间点，过时即被删除\n            priority: 事件响应器优先级\n            block: 是否阻止事件向更低优先级传递\n            state: 默认 state\n        \"\"\"\n        sub_cmd = (cmd,) if isinstance(cmd, str) else cmd\n        cmd = self.basecmd + sub_cmd\n        if self.prefix_aliases and (aliases := kwargs.get(\"aliases\")):\n            kwargs[\"aliases\"] = {\n                self.basecmd + ((alias,) if isinstance(alias, str) else alias)\n                for alias in aliases\n            }\n        matcher = on_command(cmd, **self._get_final_kwargs(kwargs))\n        self.matchers.append(matcher)\n        return matcher\n\n    def shell_command(self, cmd: str | tuple[str, ...], **kwargs) -> type[Matcher]:\n        \"\"\"注册一个新的 `shell_like` 命令。新参数将会覆盖命令组默认值\n\n        参数:\n            cmd: 指定命令内容\n            rule: 事件响应规则\n            aliases: 命令别名\n            parser: `nonebot.rule.ArgumentParser` 对象\n            permission: 事件响应权限\n            handlers: 事件处理函数列表\n            temp: 是否为临时事件响应器（仅执行一次）\n            expire_time: 事件响应器最终有效时间点，过时即被删除\n            priority: 事件响应器优先级\n            block: 是否阻止事件向更低优先级传递\n            state: 默认 state\n        \"\"\"\n        sub_cmd = (cmd,) if isinstance(cmd, str) else cmd\n        cmd = self.basecmd + sub_cmd\n        if self.prefix_aliases and (aliases := kwargs.get(\"aliases\")):\n            kwargs[\"aliases\"] = {\n                self.basecmd + ((alias,) if isinstance(alias, str) else alias)\n                for alias in aliases\n            }\n        matcher = on_shell_command(cmd, **self._get_final_kwargs(kwargs))\n        self.matchers.append(matcher)\n        return matcher\n\n\nclass MatcherGroup(_Group):\n    \"\"\"事件响应器组合，统一管理。为 `Matcher` 创建提供默认属性。\"\"\"\n\n    def __repr__(self) -> str:\n        return f\"MatcherGroup(matchers={len(self.matchers)})\"\n\n    def on(self, **kwargs) -> type[Matcher]:\n        \"\"\"注册一个基础事件响应器，可自定义类型。\n\n        参数:\n            type: 事件响应器类型\n            rule: 事件响应规则\n            permission: 事件响应权限\n            handlers: 事件处理函数列表\n            temp: 是否为临时事件响应器（仅执行一次）\n            expire_time: 事件响应器最终有效时间点，过时即被删除\n            priority: 事件响应器优先级\n            block: 是否阻止事件向更低优先级传递\n            state: 默认 state\n        \"\"\"\n        matcher = on(**self._get_final_kwargs(kwargs))\n        self.matchers.append(matcher)\n        return matcher\n\n    def on_metaevent(self, **kwargs) -> type[Matcher]:\n        \"\"\"注册一个元事件响应器。\n\n        参数:\n            rule: 事件响应规则\n            permission: 事件响应权限\n            handlers: 事件处理函数列表\n            temp: 是否为临时事件响应器（仅执行一次）\n            expire_time: 事件响应器最终有效时间点，过时即被删除\n            priority: 事件响应器优先级\n            block: 是否阻止事件向更低优先级传递\n            state: 默认 state\n        \"\"\"\n        final_kwargs = self._get_final_kwargs(kwargs, exclude={\"type\", \"permission\"})\n        matcher = on_metaevent(**final_kwargs)\n        self.matchers.append(matcher)\n        return matcher\n\n    def on_message(self, **kwargs) -> type[Matcher]:\n        \"\"\"注册一个消息事件响应器。\n\n        参数:\n            rule: 事件响应规则\n            permission: 事件响应权限\n            handlers: 事件处理函数列表\n            temp: 是否为临时事件响应器（仅执行一次）\n            expire_time: 事件响应器最终有效时间点，过时即被删除\n            priority: 事件响应器优先级\n            block: 是否阻止事件向更低优先级传递\n            state: 默认 state\n        \"\"\"\n        final_kwargs = self._get_final_kwargs(kwargs, exclude={\"type\"})\n        matcher = on_message(**final_kwargs)\n        self.matchers.append(matcher)\n        return matcher\n\n    def on_notice(self, **kwargs) -> type[Matcher]:\n        \"\"\"注册一个通知事件响应器。\n\n        参数:\n            rule: 事件响应规则\n            permission: 事件响应权限\n            handlers: 事件处理函数列表\n            temp: 是否为临时事件响应器（仅执行一次）\n            expire_time: 事件响应器最终有效时间点，过时即被删除\n            priority: 事件响应器优先级\n            block: 是否阻止事件向更低优先级传递\n            state: 默认 state\n        \"\"\"\n        final_kwargs = self._get_final_kwargs(kwargs, exclude={\"type\", \"permission\"})\n        matcher = on_notice(**final_kwargs)\n        self.matchers.append(matcher)\n        return matcher\n\n    def on_request(self, **kwargs) -> type[Matcher]:\n        \"\"\"注册一个请求事件响应器。\n\n        参数:\n            rule: 事件响应规则\n            permission: 事件响应权限\n            handlers: 事件处理函数列表\n            temp: 是否为临时事件响应器（仅执行一次）\n            expire_time: 事件响应器最终有效时间点，过时即被删除\n            priority: 事件响应器优先级\n            block: 是否阻止事件向更低优先级传递\n            state: 默认 state\n        \"\"\"\n        final_kwargs = self._get_final_kwargs(kwargs, exclude={\"type\", \"permission\"})\n        matcher = on_request(**final_kwargs)\n        self.matchers.append(matcher)\n        return matcher\n\n    def on_startswith(self, msg: str | tuple[str, ...], **kwargs) -> type[Matcher]:\n        \"\"\"注册一个消息事件响应器，并且当消息的**文本部分**以指定内容开头时响应。\n\n        参数:\n            msg: 指定消息开头内容\n            ignorecase: 是否忽略大小写\n            rule: 事件响应规则\n            permission: 事件响应权限\n            handlers: 事件处理函数列表\n            temp: 是否为临时事件响应器（仅执行一次）\n            expire_time: 事件响应器最终有效时间点，过时即被删除\n            priority: 事件响应器优先级\n            block: 是否阻止事件向更低优先级传递\n            state: 默认 state\n        \"\"\"\n        final_kwargs = self._get_final_kwargs(kwargs, exclude={\"type\"})\n        matcher = on_startswith(msg, **final_kwargs)\n        self.matchers.append(matcher)\n        return matcher\n\n    def on_endswith(self, msg: str | tuple[str, ...], **kwargs) -> type[Matcher]:\n        \"\"\"注册一个消息事件响应器，并且当消息的**文本部分**以指定内容结尾时响应。\n\n        参数:\n            msg: 指定消息结尾内容\n            ignorecase: 是否忽略大小写\n            rule: 事件响应规则\n            permission: 事件响应权限\n            handlers: 事件处理函数列表\n            temp: 是否为临时事件响应器（仅执行一次）\n            expire_time: 事件响应器最终有效时间点，过时即被删除\n            priority: 事件响应器优先级\n            block: 是否阻止事件向更低优先级传递\n            state: 默认 state\n        \"\"\"\n        final_kwargs = self._get_final_kwargs(kwargs, exclude={\"type\"})\n        matcher = on_endswith(msg, **final_kwargs)\n        self.matchers.append(matcher)\n        return matcher\n\n    def on_fullmatch(self, msg: str | tuple[str, ...], **kwargs) -> type[Matcher]:\n        \"\"\"注册一个消息事件响应器，并且当消息的**文本部分**与指定内容完全一致时响应。\n\n        参数:\n            msg: 指定消息全匹配内容\n            rule: 事件响应规则\n            ignorecase: 是否忽略大小写\n            permission: 事件响应权限\n            handlers: 事件处理函数列表\n            temp: 是否为临时事件响应器（仅执行一次）\n            expire_time: 事件响应器最终有效时间点，过时即被删除\n            priority: 事件响应器优先级\n            block: 是否阻止事件向更低优先级传递\n            state: 默认 state\n        \"\"\"\n        final_kwargs = self._get_final_kwargs(kwargs, exclude={\"type\"})\n        matcher = on_fullmatch(msg, **final_kwargs)\n        self.matchers.append(matcher)\n        return matcher\n\n    def on_keyword(self, keywords: set[str], **kwargs) -> type[Matcher]:\n        \"\"\"注册一个消息事件响应器，并且当消息纯文本部分包含关键词时响应。\n\n        参数:\n            keywords: 关键词列表\n            rule: 事件响应规则\n            permission: 事件响应权限\n            handlers: 事件处理函数列表\n            temp: 是否为临时事件响应器（仅执行一次）\n            expire_time: 事件响应器最终有效时间点，过时即被删除\n            priority: 事件响应器优先级\n            block: 是否阻止事件向更低优先级传递\n            state: 默认 state\n        \"\"\"\n        final_kwargs = self._get_final_kwargs(kwargs, exclude={\"type\"})\n        matcher = on_keyword(keywords, **final_kwargs)\n        self.matchers.append(matcher)\n        return matcher\n\n    def on_command(\n        self,\n        cmd: str | tuple[str, ...],\n        aliases: set[str | tuple[str, ...]] | None = None,\n        force_whitespace: str | bool | None = None,\n        **kwargs,\n    ) -> type[Matcher]:\n        \"\"\"注册一个消息事件响应器，并且当消息以指定命令开头时响应。\n\n        命令匹配规则参考: `命令形式匹配 <rule.md#command-command>`_\n\n        参数:\n            cmd: 指定命令内容\n            aliases: 命令别名\n            force_whitespace: 是否强制命令后必须有指定空白符\n            rule: 事件响应规则\n            permission: 事件响应权限\n            handlers: 事件处理函数列表\n            temp: 是否为临时事件响应器（仅执行一次）\n            expire_time: 事件响应器最终有效时间点，过时即被删除\n            priority: 事件响应器优先级\n            block: 是否阻止事件向更低优先级传递\n            state: 默认 state\n        \"\"\"\n        final_kwargs = self._get_final_kwargs(kwargs, exclude={\"type\"})\n        matcher = on_command(\n            cmd, aliases=aliases, force_whitespace=force_whitespace, **final_kwargs\n        )\n        self.matchers.append(matcher)\n        return matcher\n\n    def on_shell_command(\n        self,\n        cmd: str | tuple[str, ...],\n        aliases: set[str | tuple[str, ...]] | None = None,\n        parser: ArgumentParser | None = None,\n        **kwargs,\n    ) -> type[Matcher]:\n        \"\"\"注册一个支持 `shell_like` 解析参数的命令消息事件响应器。\n\n        与普通的 `on_command` 不同的是，在添加 `parser` 参数时, 响应器会自动处理消息。\n\n        可以通过 {ref}`nonebot.params.ShellCommandArgv` 获取原始参数列表，\n        通过 {ref}`nonebot.params.ShellCommandArgs` 获取解析后的参数字典。\n\n        参数:\n            cmd: 指定命令内容\n            aliases: 命令别名\n            parser: `nonebot.rule.ArgumentParser` 对象\n            rule: 事件响应规则\n            permission: 事件响应权限\n            handlers: 事件处理函数列表\n            temp: 是否为临时事件响应器（仅执行一次）\n            expire_time: 事件响应器最终有效时间点，过时即被删除\n            priority: 事件响应器优先级\n            block: 是否阻止事件向更低优先级传递\n            state: 默认 state\n        \"\"\"\n        final_kwargs = self._get_final_kwargs(kwargs, exclude={\"type\"})\n        matcher = on_shell_command(cmd, aliases=aliases, parser=parser, **final_kwargs)\n        self.matchers.append(matcher)\n        return matcher\n\n    def on_regex(\n        self, pattern: str, flags: int | re.RegexFlag = 0, **kwargs\n    ) -> type[Matcher]:\n        \"\"\"注册一个消息事件响应器，并且当消息匹配正则表达式时响应。\n\n        命令匹配规则参考: `正则匹配 <rule.md#regex-regex-flags-0>`_\n\n        参数:\n            pattern: 正则表达式\n            flags: 正则匹配标志\n            rule: 事件响应规则\n            permission: 事件响应权限\n            handlers: 事件处理函数列表\n            temp: 是否为临时事件响应器（仅执行一次）\n            expire_time: 事件响应器最终有效时间点，过时即被删除\n            priority: 事件响应器优先级\n            block: 是否阻止事件向更低优先级传递\n            state: 默认 state\n        \"\"\"\n        final_kwargs = self._get_final_kwargs(kwargs, exclude={\"type\"})\n        matcher = on_regex(pattern, flags=flags, **final_kwargs)\n        self.matchers.append(matcher)\n        return matcher\n\n    def on_type(\n        self, types: type[Event] | tuple[type[Event]], **kwargs\n    ) -> type[Matcher]:\n        \"\"\"注册一个事件响应器，并且当事件为指定类型时响应。\n\n        参数:\n            types: 事件类型\n            rule: 事件响应规则\n            permission: 事件响应权限\n            handlers: 事件处理函数列表\n            temp: 是否为临时事件响应器（仅执行一次）\n            expire_time: 事件响应器最终有效时间点，过时即被删除\n            priority: 事件响应器优先级\n            block: 是否阻止事件向更低优先级传递\n            state: 默认 state\n        \"\"\"\n        final_kwargs = self._get_final_kwargs(kwargs, exclude={\"type\"})\n        matcher = on_type(types, **final_kwargs)\n        self.matchers.append(matcher)\n        return matcher\n"
  },
  {
    "path": "nonebot/plugin/on.pyi",
    "content": "from datetime import datetime, timedelta\nimport re\nfrom types import ModuleType\nfrom typing import Any\n\nfrom nonebot.adapters import Event\nfrom nonebot.dependencies import Dependent\nfrom nonebot.matcher import Matcher, MatcherSource\nfrom nonebot.permission import Permission\nfrom nonebot.rule import ArgumentParser, Rule\nfrom nonebot.typing import T_Handler, T_PermissionChecker, T_RuleChecker, T_State\n\nfrom .model import Plugin\n\ndef store_matcher(matcher: type[Matcher]) -> None: ...\ndef get_matcher_plugin(depth: int = ...) -> Plugin | None: ...\ndef get_matcher_module(depth: int = ...) -> ModuleType | None: ...\ndef get_matcher_source(depth: int = ...) -> MatcherSource | None: ...\ndef on(\n    type: str = \"\",\n    rule: Rule | T_RuleChecker | None = ...,\n    permission: Permission | T_PermissionChecker | None = ...,\n    *,\n    handlers: list[T_Handler | Dependent[Any]] | None = ...,\n    temp: bool = ...,\n    expire_time: datetime | timedelta | None = ...,\n    priority: int = ...,\n    block: bool = ...,\n    state: T_State | None = ...,\n) -> type[Matcher]: ...\ndef on_metaevent(\n    rule: Rule | T_RuleChecker | None = ...,\n    permission: Permission | T_PermissionChecker | None = ...,\n    *,\n    handlers: list[T_Handler | Dependent[Any]] | None = ...,\n    temp: bool = ...,\n    expire_time: datetime | timedelta | None = ...,\n    priority: int = ...,\n    block: bool = ...,\n    state: T_State | None = ...,\n) -> type[Matcher]: ...\ndef on_message(\n    rule: Rule | T_RuleChecker | None = ...,\n    permission: Permission | T_PermissionChecker | None = ...,\n    *,\n    handlers: list[T_Handler | Dependent[Any]] | None = ...,\n    temp: bool = ...,\n    expire_time: datetime | timedelta | None = ...,\n    priority: int = ...,\n    block: bool = ...,\n    state: T_State | None = ...,\n) -> type[Matcher]: ...\ndef on_notice(\n    rule: Rule | T_RuleChecker | None = ...,\n    permission: Permission | T_PermissionChecker | None = ...,\n    *,\n    handlers: list[T_Handler | Dependent[Any]] | None = ...,\n    temp: bool = ...,\n    expire_time: datetime | timedelta | None = ...,\n    priority: int = ...,\n    block: bool = ...,\n    state: T_State | None = ...,\n) -> type[Matcher]: ...\ndef on_request(\n    rule: Rule | T_RuleChecker | None = ...,\n    permission: Permission | T_PermissionChecker | None = ...,\n    *,\n    handlers: list[T_Handler | Dependent[Any]] | None = ...,\n    temp: bool = ...,\n    expire_time: datetime | timedelta | None = ...,\n    priority: int = ...,\n    block: bool = ...,\n    state: T_State | None = ...,\n) -> type[Matcher]: ...\ndef on_startswith(\n    msg: str | tuple[str, ...],\n    rule: Rule | T_RuleChecker | None = ...,\n    ignorecase: bool = ...,\n    *,\n    permission: Permission | T_PermissionChecker | None = ...,\n    handlers: list[T_Handler | Dependent[Any]] | None = ...,\n    temp: bool = ...,\n    expire_time: datetime | timedelta | None = ...,\n    priority: int = ...,\n    block: bool = ...,\n    state: T_State | None = ...,\n) -> type[Matcher]: ...\ndef on_endswith(\n    msg: str | tuple[str, ...],\n    rule: Rule | T_RuleChecker | None = ...,\n    ignorecase: bool = ...,\n    *,\n    permission: Permission | T_PermissionChecker | None = ...,\n    handlers: list[T_Handler | Dependent[Any]] | None = ...,\n    temp: bool = ...,\n    expire_time: datetime | timedelta | None = ...,\n    priority: int = ...,\n    block: bool = ...,\n    state: T_State | None = ...,\n) -> type[Matcher]: ...\ndef on_fullmatch(\n    msg: str | tuple[str, ...],\n    rule: Rule | T_RuleChecker | None = ...,\n    ignorecase: bool = ...,\n    *,\n    permission: Permission | T_PermissionChecker | None = ...,\n    handlers: list[T_Handler | Dependent[Any]] | None = ...,\n    temp: bool = ...,\n    expire_time: datetime | timedelta | None = ...,\n    priority: int = ...,\n    block: bool = ...,\n    state: T_State | None = ...,\n) -> type[Matcher]: ...\ndef on_keyword(\n    keywords: set[str],\n    rule: Rule | T_RuleChecker | None = ...,\n    *,\n    permission: Permission | T_PermissionChecker | None = ...,\n    handlers: list[T_Handler | Dependent[Any]] | None = ...,\n    temp: bool = ...,\n    expire_time: datetime | timedelta | None = ...,\n    priority: int = ...,\n    block: bool = ...,\n    state: T_State | None = ...,\n) -> type[Matcher]: ...\ndef on_command(\n    cmd: str | tuple[str, ...],\n    rule: Rule | T_RuleChecker | None = ...,\n    aliases: set[str | tuple[str, ...]] | None = ...,\n    force_whitespace: str | bool | None = ...,\n    *,\n    permission: Permission | T_PermissionChecker | None = ...,\n    handlers: list[T_Handler | Dependent[Any]] | None = ...,\n    temp: bool = ...,\n    expire_time: datetime | timedelta | None = ...,\n    priority: int = ...,\n    block: bool = ...,\n    state: T_State | None = ...,\n) -> type[Matcher]: ...\ndef on_shell_command(\n    cmd: str | tuple[str, ...],\n    rule: Rule | T_RuleChecker | None = ...,\n    aliases: set[str | tuple[str, ...]] | None = ...,\n    parser: ArgumentParser | None = ...,\n    *,\n    permission: Permission | T_PermissionChecker | None = ...,\n    handlers: list[T_Handler | Dependent[Any]] | None = ...,\n    temp: bool = ...,\n    expire_time: datetime | timedelta | None = ...,\n    priority: int = ...,\n    block: bool = ...,\n    state: T_State | None = ...,\n) -> type[Matcher]: ...\ndef on_regex(\n    pattern: str,\n    flags: int | re.RegexFlag = ...,\n    rule: Rule | T_RuleChecker | None = ...,\n    *,\n    permission: Permission | T_PermissionChecker | None = ...,\n    handlers: list[T_Handler | Dependent[Any]] | None = ...,\n    temp: bool = ...,\n    expire_time: datetime | timedelta | None = ...,\n    priority: int = ...,\n    block: bool = ...,\n    state: T_State | None = ...,\n) -> type[Matcher]: ...\ndef on_type(\n    types: type[Event] | tuple[type[Event], ...],\n    rule: Rule | T_RuleChecker | None = ...,\n    *,\n    permission: Permission | T_PermissionChecker | None = ...,\n    handlers: list[T_Handler | Dependent[Any]] | None = ...,\n    temp: bool = ...,\n    expire_time: datetime | timedelta | None = ...,\n    priority: int = ...,\n    block: bool = ...,\n    state: T_State | None = ...,\n) -> type[Matcher]: ...\n\nclass _Group:\n    matchers: list[type[Matcher]] = ...\n    base_kwargs: dict[str, Any] = ...\n    def _get_final_kwargs(\n        self, update: dict[str, Any], *, exclude: set[str] | None = None\n    ) -> dict[str, Any]: ...\n\nclass CommandGroup(_Group):\n    basecmd: tuple[str, ...] = ...\n    prefix_aliases: bool = ...\n    def __init__(\n        self,\n        cmd: str | tuple[str, ...],\n        prefix_aliases: bool = ...,\n        *,\n        rule: Rule | T_RuleChecker | None = ...,\n        permission: Permission | T_PermissionChecker | None = ...,\n        handlers: list[T_Handler | Dependent[Any]] | None = ...,\n        temp: bool = ...,\n        expire_time: datetime | timedelta | None = ...,\n        priority: int = ...,\n        block: bool = ...,\n        state: T_State | None = ...,\n    ): ...\n    def command(\n        self,\n        cmd: str | tuple[str, ...],\n        *,\n        rule: Rule | T_RuleChecker | None = ...,\n        aliases: set[str | tuple[str, ...]] | None = ...,\n        force_whitespace: str | bool | None = ...,\n        permission: Permission | T_PermissionChecker | None = ...,\n        handlers: list[T_Handler | Dependent[Any]] | None = ...,\n        temp: bool = ...,\n        expire_time: datetime | timedelta | None = ...,\n        priority: int = ...,\n        block: bool = ...,\n        state: T_State | None = ...,\n    ) -> type[Matcher]: ...\n    def shell_command(\n        self,\n        cmd: str | tuple[str, ...],\n        *,\n        rule: Rule | T_RuleChecker | None = ...,\n        aliases: set[str | tuple[str, ...]] | None = ...,\n        parser: ArgumentParser | None = ...,\n        permission: Permission | T_PermissionChecker | None = ...,\n        handlers: list[T_Handler | Dependent[Any]] | None = ...,\n        temp: bool = ...,\n        expire_time: datetime | timedelta | None = ...,\n        priority: int = ...,\n        block: bool = ...,\n        state: T_State | None = ...,\n    ) -> type[Matcher]: ...\n\nclass MatcherGroup(_Group):\n    def __init__(\n        self,\n        *,\n        type: str = ...,\n        rule: Rule | T_RuleChecker | None = ...,\n        permission: Permission | T_PermissionChecker | None = ...,\n        handlers: list[T_Handler | Dependent[Any]] | None = ...,\n        temp: bool = ...,\n        expire_time: datetime | timedelta | None = ...,\n        priority: int = ...,\n        block: bool = ...,\n        state: T_State | None = ...,\n    ): ...\n    def on(\n        self,\n        *,\n        type: str = ...,\n        rule: Rule | T_RuleChecker | None = ...,\n        permission: Permission | T_PermissionChecker | None = ...,\n        handlers: list[T_Handler | Dependent[Any]] | None = ...,\n        temp: bool = ...,\n        expire_time: datetime | timedelta | None = ...,\n        priority: int = ...,\n        block: bool = ...,\n        state: T_State | None = ...,\n    ) -> type[Matcher]: ...\n    def on_metaevent(\n        self,\n        *,\n        rule: Rule | T_RuleChecker | None = ...,\n        permission: Permission | T_PermissionChecker | None = ...,\n        handlers: list[T_Handler | Dependent[Any]] | None = ...,\n        temp: bool = ...,\n        expire_time: datetime | timedelta | None = ...,\n        priority: int = ...,\n        block: bool = ...,\n        state: T_State | None = ...,\n    ) -> type[Matcher]: ...\n    def on_message(\n        self,\n        *,\n        rule: Rule | T_RuleChecker | None = ...,\n        permission: Permission | T_PermissionChecker | None = ...,\n        handlers: list[T_Handler | Dependent[Any]] | None = ...,\n        temp: bool = ...,\n        expire_time: datetime | timedelta | None = ...,\n        priority: int = ...,\n        block: bool = ...,\n        state: T_State | None = ...,\n    ) -> type[Matcher]: ...\n    def on_notice(\n        self,\n        *,\n        rule: Rule | T_RuleChecker | None = ...,\n        permission: Permission | T_PermissionChecker | None = ...,\n        handlers: list[T_Handler | Dependent[Any]] | None = ...,\n        temp: bool = ...,\n        expire_time: datetime | timedelta | None = ...,\n        priority: int = ...,\n        block: bool = ...,\n        state: T_State | None = ...,\n    ) -> type[Matcher]: ...\n    def on_request(\n        self,\n        *,\n        rule: Rule | T_RuleChecker | None = ...,\n        permission: Permission | T_PermissionChecker | None = ...,\n        handlers: list[T_Handler | Dependent[Any]] | None = ...,\n        temp: bool = ...,\n        expire_time: datetime | timedelta | None = ...,\n        priority: int = ...,\n        block: bool = ...,\n        state: T_State | None = ...,\n    ) -> type[Matcher]: ...\n    def on_startswith(\n        self,\n        msg: str | tuple[str, ...],\n        *,\n        ignorecase: bool = ...,\n        rule: Rule | T_RuleChecker | None = ...,\n        permission: Permission | T_PermissionChecker | None = ...,\n        handlers: list[T_Handler | Dependent[Any]] | None = ...,\n        temp: bool = ...,\n        expire_time: datetime | timedelta | None = ...,\n        priority: int = ...,\n        block: bool = ...,\n        state: T_State | None = ...,\n    ) -> type[Matcher]: ...\n    def on_endswith(\n        self,\n        msg: str | tuple[str, ...],\n        *,\n        ignorecase: bool = ...,\n        rule: Rule | T_RuleChecker | None = ...,\n        permission: Permission | T_PermissionChecker | None = ...,\n        handlers: list[T_Handler | Dependent[Any]] | None = ...,\n        temp: bool = ...,\n        expire_time: datetime | timedelta | None = ...,\n        priority: int = ...,\n        block: bool = ...,\n        state: T_State | None = ...,\n    ) -> type[Matcher]: ...\n    def on_fullmatch(\n        self,\n        msg: str | tuple[str, ...],\n        *,\n        ignorecase: bool = ...,\n        rule: Rule | T_RuleChecker | None = ...,\n        permission: Permission | T_PermissionChecker | None = ...,\n        handlers: list[T_Handler | Dependent[Any]] | None = ...,\n        temp: bool = ...,\n        expire_time: datetime | timedelta | None = ...,\n        priority: int = ...,\n        block: bool = ...,\n        state: T_State | None = ...,\n    ) -> type[Matcher]: ...\n    def on_keyword(\n        self,\n        keywords: set[str],\n        *,\n        rule: Rule | T_RuleChecker | None = ...,\n        permission: Permission | T_PermissionChecker | None = ...,\n        handlers: list[T_Handler | Dependent[Any]] | None = ...,\n        temp: bool = ...,\n        expire_time: datetime | timedelta | None = ...,\n        priority: int = ...,\n        block: bool = ...,\n        state: T_State | None = ...,\n    ) -> type[Matcher]: ...\n    def on_command(\n        self,\n        cmd: str | tuple[str, ...],\n        aliases: set[str | tuple[str, ...]] | None = ...,\n        force_whitespace: str | bool | None = ...,\n        *,\n        rule: Rule | T_RuleChecker | None = ...,\n        permission: Permission | T_PermissionChecker | None = ...,\n        handlers: list[T_Handler | Dependent[Any]] | None = ...,\n        temp: bool = ...,\n        expire_time: datetime | timedelta | None = ...,\n        priority: int = ...,\n        block: bool = ...,\n        state: T_State | None = ...,\n    ) -> type[Matcher]: ...\n    def on_shell_command(\n        self,\n        cmd: str | tuple[str, ...],\n        aliases: set[str | tuple[str, ...]] | None = ...,\n        parser: ArgumentParser | None = ...,\n        *,\n        rule: Rule | T_RuleChecker | None = ...,\n        permission: Permission | T_PermissionChecker | None = ...,\n        handlers: list[T_Handler | Dependent[Any]] | None = ...,\n        temp: bool = ...,\n        expire_time: datetime | timedelta | None = ...,\n        priority: int = ...,\n        block: bool = ...,\n        state: T_State | None = ...,\n    ) -> type[Matcher]: ...\n    def on_regex(\n        self,\n        pattern: str,\n        flags: int | re.RegexFlag = ...,\n        *,\n        rule: Rule | T_RuleChecker | None = ...,\n        permission: Permission | T_PermissionChecker | None = ...,\n        handlers: list[T_Handler | Dependent[Any]] | None = ...,\n        temp: bool = ...,\n        expire_time: datetime | timedelta | None = ...,\n        priority: int = ...,\n        block: bool = ...,\n        state: T_State | None = ...,\n    ) -> type[Matcher]: ...\n    def on_type(\n        self,\n        types: type[Event] | tuple[type[Event]],\n        *,\n        rule: Rule | T_RuleChecker | None = ...,\n        permission: Permission | T_PermissionChecker | None = ...,\n        handlers: list[T_Handler | Dependent[Any]] | None = ...,\n        temp: bool = ...,\n        expire_time: datetime | timedelta | None = ...,\n        priority: int = ...,\n        block: bool = ...,\n        state: T_State | None = ...,\n    ) -> type[Matcher]: ...\n"
  },
  {
    "path": "nonebot/plugins/echo.py",
    "content": "from nonebot import on_command\nfrom nonebot.adapters import Message\nfrom nonebot.params import CommandArg\nfrom nonebot.plugin import PluginMetadata\nfrom nonebot.rule import to_me\n\n__plugin_meta__ = PluginMetadata(\n    name=\"echo\",\n    description=\"重复你说的话\",\n    usage=\"/echo [text]\",\n    type=\"application\",\n    homepage=\"https://github.com/nonebot/nonebot2/blob/master/nonebot/plugins/echo.py\",\n    config=None,\n    supported_adapters=None,\n)\n\necho = on_command(\"echo\", to_me())\n\n\n@echo.handle()\nasync def handle_echo(message: Message = CommandArg()):\n    if any((not seg.is_text()) or str(seg) for seg in message):\n        await echo.send(message=message)\n"
  },
  {
    "path": "nonebot/plugins/single_session.py",
    "content": "from collections.abc import AsyncGenerator\n\nfrom nonebot.adapters import Event\nfrom nonebot.message import IgnoredException, event_preprocessor\nfrom nonebot.params import Depends\nfrom nonebot.plugin import PluginMetadata\n\n__plugin_meta__ = PluginMetadata(\n    name=\"唯一会话\",\n    description=\"限制同一会话内同时只能运行一个响应器\",\n    usage=\"加载插件后自动生效\",\n    type=\"application\",\n    homepage=\"https://github.com/nonebot/nonebot2/blob/master/nonebot/plugins/single_session.py\",\n    config=None,\n    supported_adapters=None,\n)\n\n_running_matcher: dict[str, int] = {}\n\n\nasync def matcher_mutex(event: Event) -> AsyncGenerator[bool, None]:\n    result = False\n    try:\n        session_id = event.get_session_id()\n    except Exception:\n        yield result\n    else:\n        current_event_id = id(event)\n        if event_id := _running_matcher.get(session_id):\n            result = event_id != current_event_id\n        else:\n            _running_matcher[session_id] = current_event_id\n        yield result\n        if not result:\n            del _running_matcher[session_id]\n\n\n@event_preprocessor\nasync def preprocess(mutex: bool = Depends(matcher_mutex)):\n    if mutex:\n        raise IgnoredException(\"Another matcher running\")\n"
  },
  {
    "path": "nonebot/py.typed",
    "content": ""
  },
  {
    "path": "nonebot/rule.py",
    "content": "\"\"\"本模块是 {ref}`nonebot.matcher.Matcher.rule` 的类型定义。\n\n每个{ref}`事件响应器 <nonebot.matcher.Matcher>`拥有一个\n{ref}`nonebot.rule.Rule`，其中是 `RuleChecker` 的集合。\n只有当所有 `RuleChecker` 检查结果为 `True` 时继续运行。\n\nFrontMatter:\n    mdx:\n        format: md\n    sidebar_position: 5\n    description: nonebot.rule 模块\n\"\"\"\n\nfrom argparse import Action, ArgumentError\nfrom argparse import ArgumentParser as ArgParser\nfrom argparse import Namespace as Namespace\nfrom collections.abc import Sequence\nfrom contextvars import ContextVar\nfrom gettext import gettext\nfrom itertools import chain, product\nimport re\nimport shlex\nfrom typing import (\n    IO,\n    TYPE_CHECKING,\n    NamedTuple,\n    TypedDict,\n    TypeVar,\n    cast,\n    overload,\n)\n\nfrom pygtrie import CharTrie\n\nfrom nonebot import get_driver\nfrom nonebot.adapters import Bot, Event, Message, MessageSegment\nfrom nonebot.consts import (\n    CMD_ARG_KEY,\n    CMD_KEY,\n    CMD_START_KEY,\n    CMD_WHITESPACE_KEY,\n    ENDSWITH_KEY,\n    FULLMATCH_KEY,\n    KEYWORD_KEY,\n    PREFIX_KEY,\n    RAW_CMD_KEY,\n    REGEX_MATCHED,\n    SHELL_ARGS,\n    SHELL_ARGV,\n    STARTSWITH_KEY,\n)\nfrom nonebot.exception import ParserExit\nfrom nonebot.internal.rule import Rule as Rule\nfrom nonebot.log import logger\nfrom nonebot.params import Command, CommandArg, CommandWhitespace, EventToMe\nfrom nonebot.typing import T_State\n\nT = TypeVar(\"T\")\n\n\nclass CMD_RESULT(TypedDict):\n    command: tuple[str, ...] | None\n    raw_command: str | None\n    command_arg: Message | None\n    command_start: str | None\n    command_whitespace: str | None\n\n\nclass TRIE_VALUE(NamedTuple):\n    command_start: str\n    command: tuple[str, ...]\n\n\nparser_message: ContextVar[str] = ContextVar(\"parser_message\")\n\n\nclass TrieRule:\n    prefix: CharTrie = CharTrie()\n\n    @classmethod\n    def add_prefix(cls, prefix: str, value: TRIE_VALUE) -> None:\n        if prefix in cls.prefix:\n            logger.warning(f'Duplicated prefix rule \"{prefix}\"')\n            return\n        cls.prefix[prefix] = value\n\n    @classmethod\n    def get_value(cls, bot: Bot, event: Event, state: T_State) -> CMD_RESULT:\n        prefix = CMD_RESULT(\n            command=None,\n            raw_command=None,\n            command_arg=None,\n            command_start=None,\n            command_whitespace=None,\n        )\n        state[PREFIX_KEY] = prefix\n        if event.get_type() != \"message\":\n            return prefix\n\n        message = event.get_message()\n        message_seg: MessageSegment = message[0]\n        if message_seg.is_text():\n            segment_text = str(message_seg).lstrip()\n            if pf := cls.prefix.longest_prefix(segment_text):\n                value: TRIE_VALUE = pf.value\n                prefix[RAW_CMD_KEY] = pf.key\n                prefix[CMD_START_KEY] = value.command_start\n                prefix[CMD_KEY] = value.command\n\n                msg = message.copy()\n                msg.pop(0)\n\n                # check whitespace\n                arg_str = segment_text[len(pf.key) :]\n                arg_str_stripped = arg_str.lstrip()\n                # check next segment until arg detected or no text remain\n                while not arg_str_stripped and msg and msg[0].is_text():\n                    arg_str += str(msg.pop(0))\n                    arg_str_stripped = arg_str.lstrip()\n\n                has_arg = arg_str_stripped or msg\n                if (\n                    has_arg\n                    and (stripped_len := len(arg_str) - len(arg_str_stripped)) > 0\n                ):\n                    prefix[CMD_WHITESPACE_KEY] = arg_str[:stripped_len]\n\n                # construct command arg\n                if arg_str_stripped:\n                    new_message = msg.__class__(arg_str_stripped)\n                    for new_segment in reversed(new_message):\n                        msg.insert(0, new_segment)\n                prefix[CMD_ARG_KEY] = msg\n\n        return prefix\n\n\nclass StartswithRule:\n    \"\"\"检查消息纯文本是否以指定字符串开头。\n\n    参数:\n        msg: 指定消息开头字符串元组\n        ignorecase: 是否忽略大小写\n    \"\"\"\n\n    __slots__ = (\"ignorecase\", \"msg\")\n\n    def __init__(self, msg: tuple[str, ...], ignorecase: bool = False):\n        self.msg = msg\n        self.ignorecase = ignorecase\n\n    def __repr__(self) -> str:\n        return f\"Startswith(msg={self.msg}, ignorecase={self.ignorecase})\"\n\n    def __eq__(self, other: object) -> bool:\n        return (\n            isinstance(other, StartswithRule)\n            and frozenset(self.msg) == frozenset(other.msg)\n            and self.ignorecase == other.ignorecase\n        )\n\n    def __hash__(self) -> int:\n        return hash((frozenset(self.msg), self.ignorecase))\n\n    async def __call__(self, event: Event, state: T_State) -> bool:\n        try:\n            text = event.get_plaintext()\n        except Exception:\n            return False\n        if match := re.match(\n            f\"^(?:{'|'.join(re.escape(prefix) for prefix in self.msg)})\",\n            text,\n            re.IGNORECASE if self.ignorecase else 0,\n        ):\n            state[STARTSWITH_KEY] = match.group()\n            return True\n        return False\n\n\ndef startswith(msg: str | tuple[str, ...], ignorecase: bool = False) -> Rule:\n    \"\"\"匹配消息纯文本开头。\n\n    参数:\n        msg: 指定消息开头字符串元组\n        ignorecase: 是否忽略大小写\n    \"\"\"\n    if isinstance(msg, str):\n        msg = (msg,)\n\n    return Rule(StartswithRule(msg, ignorecase))\n\n\nclass EndswithRule:\n    \"\"\"检查消息纯文本是否以指定字符串结尾。\n\n    参数:\n        msg: 指定消息结尾字符串元组\n        ignorecase: 是否忽略大小写\n    \"\"\"\n\n    __slots__ = (\"ignorecase\", \"msg\")\n\n    def __init__(self, msg: tuple[str, ...], ignorecase: bool = False):\n        self.msg = msg\n        self.ignorecase = ignorecase\n\n    def __repr__(self) -> str:\n        return f\"Endswith(msg={self.msg}, ignorecase={self.ignorecase})\"\n\n    def __eq__(self, other: object) -> bool:\n        return (\n            isinstance(other, EndswithRule)\n            and frozenset(self.msg) == frozenset(other.msg)\n            and self.ignorecase == other.ignorecase\n        )\n\n    def __hash__(self) -> int:\n        return hash((frozenset(self.msg), self.ignorecase))\n\n    async def __call__(self, event: Event, state: T_State) -> bool:\n        try:\n            text = event.get_plaintext()\n        except Exception:\n            return False\n        if match := re.search(\n            f\"(?:{'|'.join(re.escape(suffix) for suffix in self.msg)})$\",\n            text,\n            re.IGNORECASE if self.ignorecase else 0,\n        ):\n            state[ENDSWITH_KEY] = match.group()\n            return True\n        return False\n\n\ndef endswith(msg: str | tuple[str, ...], ignorecase: bool = False) -> Rule:\n    \"\"\"匹配消息纯文本结尾。\n\n    参数:\n        msg: 指定消息开头字符串元组\n        ignorecase: 是否忽略大小写\n    \"\"\"\n    if isinstance(msg, str):\n        msg = (msg,)\n\n    return Rule(EndswithRule(msg, ignorecase))\n\n\nclass FullmatchRule:\n    \"\"\"检查消息纯文本是否与指定字符串全匹配。\n\n    参数:\n        msg: 指定消息全匹配字符串元组\n        ignorecase: 是否忽略大小写\n    \"\"\"\n\n    __slots__ = (\"ignorecase\", \"msg\")\n\n    def __init__(self, msg: tuple[str, ...], ignorecase: bool = False):\n        self.msg = tuple(map(str.casefold, msg) if ignorecase else msg)\n        self.ignorecase = ignorecase\n\n    def __repr__(self) -> str:\n        return f\"Fullmatch(msg={self.msg}, ignorecase={self.ignorecase})\"\n\n    def __eq__(self, other: object) -> bool:\n        return (\n            isinstance(other, FullmatchRule)\n            and frozenset(self.msg) == frozenset(other.msg)\n            and self.ignorecase == other.ignorecase\n        )\n\n    def __hash__(self) -> int:\n        return hash((frozenset(self.msg), self.ignorecase))\n\n    async def __call__(self, event: Event, state: T_State) -> bool:\n        try:\n            text = event.get_plaintext()\n        except Exception:\n            return False\n        if not text:\n            return False\n        text = text.casefold() if self.ignorecase else text\n        if text in self.msg:\n            state[FULLMATCH_KEY] = text\n            return True\n        return False\n\n\ndef fullmatch(msg: str | tuple[str, ...], ignorecase: bool = False) -> Rule:\n    \"\"\"完全匹配消息。\n\n    参数:\n        msg: 指定消息全匹配字符串元组\n        ignorecase: 是否忽略大小写\n    \"\"\"\n    if isinstance(msg, str):\n        msg = (msg,)\n\n    return Rule(FullmatchRule(msg, ignorecase))\n\n\nclass KeywordsRule:\n    \"\"\"检查消息纯文本是否包含指定关键字。\n\n    参数:\n        keywords: 指定关键字元组\n    \"\"\"\n\n    __slots__ = (\"keywords\",)\n\n    def __init__(self, *keywords: str):\n        self.keywords = keywords\n\n    def __repr__(self) -> str:\n        return f\"Keywords(keywords={self.keywords})\"\n\n    def __eq__(self, other: object) -> bool:\n        return isinstance(other, KeywordsRule) and frozenset(\n            self.keywords\n        ) == frozenset(other.keywords)\n\n    def __hash__(self) -> int:\n        return hash(frozenset(self.keywords))\n\n    async def __call__(self, event: Event, state: T_State) -> bool:\n        try:\n            text = event.get_plaintext()\n        except Exception:\n            return False\n        if not text:\n            return False\n        if key := next((k for k in self.keywords if k in text), None):\n            state[KEYWORD_KEY] = key\n            return True\n        return False\n\n\ndef keyword(*keywords: str) -> Rule:\n    \"\"\"匹配消息纯文本关键词。\n\n    参数:\n        keywords: 指定关键字元组\n    \"\"\"\n\n    return Rule(KeywordsRule(*keywords))\n\n\nclass CommandRule:\n    \"\"\"检查消息是否为指定命令。\n\n    参数:\n        cmds: 指定命令元组列表\n        force_whitespace: 是否强制命令后必须有指定空白符\n    \"\"\"\n\n    __slots__ = (\"cmds\", \"force_whitespace\")\n\n    def __init__(\n        self,\n        cmds: list[tuple[str, ...]],\n        force_whitespace: str | bool | None = None,\n    ):\n        self.cmds = tuple(cmds)\n        self.force_whitespace = force_whitespace\n\n    def __repr__(self) -> str:\n        return f\"Command(cmds={self.cmds})\"\n\n    def __eq__(self, other: object) -> bool:\n        return isinstance(other, CommandRule) and frozenset(self.cmds) == frozenset(\n            other.cmds\n        )\n\n    def __hash__(self) -> int:\n        return hash((frozenset(self.cmds),))\n\n    async def __call__(\n        self,\n        cmd: tuple[str, ...] | None = Command(),\n        cmd_arg: Message | None = CommandArg(),\n        cmd_whitespace: str | None = CommandWhitespace(),\n    ) -> bool:\n        if cmd not in self.cmds:\n            return False\n        if self.force_whitespace is None or not cmd_arg:\n            return True\n        if isinstance(self.force_whitespace, str):\n            return self.force_whitespace == cmd_whitespace\n        return self.force_whitespace == (cmd_whitespace is not None)\n\n\ndef command(\n    *cmds: str | tuple[str, ...],\n    force_whitespace: str | bool | None = None,\n) -> Rule:\n    \"\"\"匹配消息命令。\n\n    根据配置里提供的 {ref}``command_start` <nonebot.config.Config.command_start>`,\n    {ref}``command_sep` <nonebot.config.Config.command_sep>` 判断消息是否为命令。\n\n    可以通过 {ref}`nonebot.params.Command` 获取匹配成功的命令（例: `(\"test\",)`），\n    通过 {ref}`nonebot.params.RawCommand` 获取匹配成功的原始命令文本（例: `\"/test\"`），\n    通过 {ref}`nonebot.params.CommandArg` 获取匹配成功的命令参数。\n\n    参数:\n        cmds: 命令文本或命令元组\n        force_whitespace: 是否强制命令后必须有指定空白符\n\n    用法:\n        使用默认 `command_start`, `command_sep` 配置情况下：\n\n        命令 `(\"test\",)` 可以匹配: `/test` 开头的消息\n        命令 `(\"test\", \"sub\")` 可以匹配: `/test.sub` 开头的消息\n\n    :::tip 提示\n    命令内容与后续消息间无需空格!\n    :::\n    \"\"\"\n\n    config = get_driver().config\n    command_start = config.command_start\n    command_sep = config.command_sep\n    commands: list[tuple[str, ...]] = []\n    for command in cmds:\n        if isinstance(command, str):\n            command = (command,)\n\n        commands.append(command)\n\n        if len(command) == 1:\n            for start in command_start:\n                TrieRule.add_prefix(f\"{start}{command[0]}\", TRIE_VALUE(start, command))\n        else:\n            for start, sep in product(command_start, command_sep):\n                TrieRule.add_prefix(\n                    f\"{start}{sep.join(command)}\", TRIE_VALUE(start, command)\n                )\n\n    return Rule(CommandRule(commands, force_whitespace))\n\n\nclass ArgumentParser(ArgParser):\n    \"\"\"`shell_like` 命令参数解析器，解析出错时不会退出程序。\n\n    支持 {ref}`nonebot.adapters.Message` 富文本解析。\n\n    用法:\n        用法与 `argparse.ArgumentParser` 相同，\n        参考文档: [argparse](https://docs.python.org/3/library/argparse.html)\n    \"\"\"\n\n    if TYPE_CHECKING:\n\n        @overload\n        def parse_known_args(\n            self,\n            args: Sequence[str | MessageSegment] | None = None,\n            namespace: None = None,\n        ) -> tuple[Namespace, list[str | MessageSegment]]: ...\n\n        @overload\n        def parse_known_args(\n            self, args: Sequence[str | MessageSegment] | None, namespace: T\n        ) -> tuple[T, list[str | MessageSegment]]: ...\n\n        @overload\n        def parse_known_args(\n            self, *, namespace: T\n        ) -> tuple[T, list[str | MessageSegment]]: ...\n\n        def parse_known_args(  # pyright: ignore[reportIncompatibleMethodOverride]\n            self,\n            args: Sequence[str | MessageSegment] | None = None,\n            namespace: T | None = None,\n        ) -> tuple[Namespace | T, list[str | MessageSegment]]: ...\n\n    @overload\n    def parse_args(\n        self,\n        args: Sequence[str | MessageSegment] | None = None,\n        namespace: None = None,\n    ) -> Namespace: ...\n\n    @overload\n    def parse_args(\n        self, args: Sequence[str | MessageSegment] | None, namespace: T\n    ) -> T: ...\n\n    @overload\n    def parse_args(self, *, namespace: T) -> T: ...\n\n    def parse_args(\n        self,\n        args: Sequence[str | MessageSegment] | None = None,\n        namespace: T | None = None,\n    ) -> Namespace | T:\n        result, argv = self.parse_known_args(args, namespace)\n        if argv:\n            msg = gettext(\"unrecognized arguments: %s\")\n            self.error(msg % \" \".join(map(str, argv)))\n        return cast(Namespace | T, result)\n\n    def _parse_optional(\n        self, arg_string: str | MessageSegment\n    ) -> tuple[Action | None, str, str | None] | None:\n        return (\n            super()._parse_optional(arg_string) if isinstance(arg_string, str) else None\n        )\n\n    def _print_message(self, message: str, file: IO[str] | None = None):  # type: ignore\n        if (msg := parser_message.get(None)) is not None:\n            parser_message.set(msg + message)\n        else:\n            super()._print_message(message, file)\n\n    def exit(self, status: int = 0, message: str | None = None):\n        if message:\n            self._print_message(message)\n        raise ParserExit(status=status, message=parser_message.get(None))\n\n\nclass ShellCommandRule:\n    \"\"\"检查消息是否为指定 shell 命令。\n\n    参数:\n        cmds: 指定命令元组列表\n        parser: 可选参数解析器\n    \"\"\"\n\n    __slots__ = (\"cmds\", \"parser\")\n\n    def __init__(self, cmds: list[tuple[str, ...]], parser: ArgumentParser | None):\n        self.cmds = tuple(cmds)\n        self.parser = parser\n\n    def __repr__(self) -> str:\n        return f\"ShellCommand(cmds={self.cmds}, parser={self.parser})\"\n\n    def __eq__(self, other: object) -> bool:\n        return (\n            isinstance(other, ShellCommandRule)\n            and frozenset(self.cmds) == frozenset(other.cmds)\n            and self.parser is other.parser\n        )\n\n    def __hash__(self) -> int:\n        return hash((frozenset(self.cmds), self.parser))\n\n    async def __call__(\n        self,\n        state: T_State,\n        cmd: tuple[str, ...] | None = Command(),\n        msg: Message | None = CommandArg(),\n    ) -> bool:\n        if cmd not in self.cmds or msg is None:\n            return False\n\n        try:\n            state[SHELL_ARGV] = list(\n                chain.from_iterable(\n                    shlex.split(str(seg))\n                    if cast(MessageSegment, seg).is_text()\n                    else (seg,)\n                    for seg in msg\n                )\n            )\n        except Exception as e:\n            # set SHELL_ARGV to none indicating shlex error\n            state[SHELL_ARGV] = None\n            # ensure SHELL_ARGS is set to ParserExit if parser is provided\n            if self.parser:\n                state[SHELL_ARGS] = ParserExit(status=2, message=str(e))\n            return True\n\n        if self.parser:\n            t = parser_message.set(\"\")\n            try:\n                args = self.parser.parse_args(state[SHELL_ARGV])\n                state[SHELL_ARGS] = args\n            except ArgumentError as e:\n                state[SHELL_ARGS] = ParserExit(status=2, message=str(e))\n            except ParserExit as e:\n                state[SHELL_ARGS] = e\n            finally:\n                parser_message.reset(t)\n        return True\n\n\ndef shell_command(\n    *cmds: str | tuple[str, ...], parser: ArgumentParser | None = None\n) -> Rule:\n    \"\"\"匹配 `shell_like` 形式的消息命令。\n\n    根据配置里提供的 {ref}``command_start` <nonebot.config.Config.command_start>`,\n    {ref}``command_sep` <nonebot.config.Config.command_sep>` 判断消息是否为命令。\n\n    可以通过 {ref}`nonebot.params.Command` 获取匹配成功的命令\n    （例: `(\"test\",)`），\n    通过 {ref}`nonebot.params.RawCommand` 获取匹配成功的原始命令文本\n    （例: `\"/test\"`），\n    通过 {ref}`nonebot.params.ShellCommandArgv` 获取解析前的参数列表\n    （例: `[\"arg\", \"-h\"]`），\n    通过 {ref}`nonebot.params.ShellCommandArgs` 获取解析后的参数字典\n    （例: `{\"arg\": \"arg\", \"h\": True}`）。\n\n    :::caution 警告\n    如果参数解析失败，则通过 {ref}`nonebot.params.ShellCommandArgs`\n    获取的将是 {ref}`nonebot.exception.ParserExit` 异常。\n    :::\n\n    参数:\n        cmds: 命令文本或命令元组\n        parser: {ref}`nonebot.rule.ArgumentParser` 对象\n\n    用法:\n        使用默认 `command_start`, `command_sep` 配置，更多示例参考\n        [argparse](https://docs.python.org/3/library/argparse.html) 标准库文档。\n\n        ```python\n        from nonebot.rule import ArgumentParser\n\n        parser = ArgumentParser()\n        parser.add_argument(\"-a\", action=\"store_true\")\n\n        rule = shell_command(\"ls\", parser=parser)\n        ```\n\n    :::tip 提示\n    命令内容与后续消息间无需空格!\n    :::\n    \"\"\"\n    if parser is not None and not isinstance(parser, ArgumentParser):\n        raise TypeError(\"`parser` must be an instance of nonebot.rule.ArgumentParser\")\n\n    config = get_driver().config\n    command_start = config.command_start\n    command_sep = config.command_sep\n    commands: list[tuple[str, ...]] = []\n    for command in cmds:\n        if isinstance(command, str):\n            command = (command,)\n\n        commands.append(command)\n\n        if len(command) == 1:\n            for start in command_start:\n                TrieRule.add_prefix(f\"{start}{command[0]}\", TRIE_VALUE(start, command))\n        else:\n            for start, sep in product(command_start, command_sep):\n                TrieRule.add_prefix(\n                    f\"{start}{sep.join(command)}\", TRIE_VALUE(start, command)\n                )\n\n    return Rule(ShellCommandRule(commands, parser))\n\n\nclass RegexRule:\n    \"\"\"检查消息字符串是否符合指定正则表达式。\n\n    参数:\n        regex: 正则表达式\n        flags: 正则表达式标记\n    \"\"\"\n\n    __slots__ = (\"flags\", \"regex\")\n\n    def __init__(self, regex: str, flags: int = 0):\n        self.regex = regex\n        self.flags = flags\n\n    def __repr__(self) -> str:\n        return f\"Regex(regex={self.regex!r}, flags={self.flags})\"\n\n    def __eq__(self, other: object) -> bool:\n        return (\n            isinstance(other, RegexRule)\n            and self.regex == other.regex\n            and self.flags == other.flags\n        )\n\n    def __hash__(self) -> int:\n        return hash((self.regex, self.flags))\n\n    async def __call__(self, event: Event, state: T_State) -> bool:\n        try:\n            msg = event.get_message()\n        except Exception:\n            return False\n        if matched := re.search(self.regex, str(msg), self.flags):\n            state[REGEX_MATCHED] = matched\n            return True\n        else:\n            return False\n\n\ndef regex(regex: str, flags: int | re.RegexFlag = 0) -> Rule:\n    \"\"\"匹配符合正则表达式的消息字符串。\n\n    可以通过 {ref}`nonebot.params.RegexStr` 获取匹配成功的字符串，\n    通过 {ref}`nonebot.params.RegexGroup` 获取匹配成功的 group 元组，\n    通过 {ref}`nonebot.params.RegexDict` 获取匹配成功的 group 字典。\n\n    参数:\n        regex: 正则表达式\n        flags: 正则表达式标记\n\n    :::tip 提示\n    正则表达式匹配使用 search 而非 match，如需从头匹配请使用 `r\"^xxx\"` 来确保匹配开头\n    :::\n\n    :::tip 提示\n    正则表达式匹配使用 `EventMessage` 的 `str` 字符串，\n    而非 `EventMessage` 的 `PlainText` 纯文本字符串\n    :::\n    \"\"\"\n\n    return Rule(RegexRule(regex, flags))\n\n\nclass ToMeRule:\n    \"\"\"检查事件是否与机器人有关。\"\"\"\n\n    __slots__ = ()\n\n    def __repr__(self) -> str:\n        return \"ToMe()\"\n\n    def __eq__(self, other: object) -> bool:\n        return isinstance(other, ToMeRule)\n\n    def __hash__(self) -> int:\n        return hash((self.__class__,))\n\n    async def __call__(self, to_me: bool = EventToMe()) -> bool:\n        return to_me\n\n\ndef to_me() -> Rule:\n    \"\"\"匹配与机器人有关的事件。\"\"\"\n\n    return Rule(ToMeRule())\n\n\nclass IsTypeRule:\n    \"\"\"检查事件类型是否为指定类型。\"\"\"\n\n    __slots__ = (\"types\",)\n\n    def __init__(self, *types: type[Event]):\n        self.types = types\n\n    def __repr__(self) -> str:\n        return f\"IsType(types={tuple(type.__name__ for type in self.types)})\"\n\n    def __eq__(self, other: object) -> bool:\n        return isinstance(other, IsTypeRule) and self.types == other.types\n\n    def __hash__(self) -> int:\n        return hash((self.types,))\n\n    async def __call__(self, event: Event) -> bool:\n        return isinstance(event, self.types)\n\n\ndef is_type(*types: type[Event]) -> Rule:\n    \"\"\"匹配事件类型。\n\n    参数:\n        types: 事件类型\n    \"\"\"\n\n    return Rule(IsTypeRule(*types))\n\n\n__autodoc__ = {\n    \"Rule\": True,\n    \"Rule.__call__\": True,\n    \"TrieRule\": False,\n    \"ArgumentParser.exit\": False,\n    \"ArgumentParser.parse_args\": False,\n}\n"
  },
  {
    "path": "nonebot/typing.py",
    "content": "\"\"\"本模块定义了 NoneBot 模块中共享的一些类型。\n\n使用 Python 的 Type Hint 语法，\n参考 [`PEP 484`](https://www.python.org/dev/peps/pep-0484/),\n[`PEP 526`](https://www.python.org/dev/peps/pep-0526/) 和\n[`typing`](https://docs.python.org/3/library/typing.html)。\n\nFrontMatter:\n    mdx:\n        format: md\n    sidebar_position: 11\n    description: nonebot.typing 模块\n\"\"\"\n\nimport sys\nimport types\nimport typing as t\nfrom typing import TYPE_CHECKING, TypeAlias, TypeVar, get_args, get_origin\nimport typing_extensions as t_ext\nfrom typing_extensions import ParamSpec, override\nimport warnings\n\nif TYPE_CHECKING:\n    from nonebot.adapters import Bot\n    from nonebot.internal.params import DependencyCache\n    from nonebot.permission import Permission\n\nT = TypeVar(\"T\")\nP = ParamSpec(\"P\")\n\nT_Wrapped: TypeAlias = t.Callable[P, T]\n\n\ndef overrides(InterfaceClass: object):\n    \"\"\"标记一个方法为父类 interface 的 implement\"\"\"\n\n    warnings.warn(\n        \"overrides is deprecated and will be removed in a future version, \"\n        \"use @typing_extensions.override instead. \"\n        \"See [PEP 698](https://peps.python.org/pep-0698/) for more details.\",\n        DeprecationWarning,\n    )\n    return override\n\n\ndef type_has_args(type_: type[t.Any]) -> bool:\n    return isinstance(type_, (t._GenericAlias, types.GenericAlias, types.UnionType))  # type: ignore\n\n\ndef origin_is_union(origin: type[t.Any] | None) -> bool:\n    return origin is t.Union or origin is types.UnionType\n\n\ndef origin_is_literal(origin: type[t.Any] | None) -> bool:\n    \"\"\"判断是否是 Literal 类型\"\"\"\n    return origin is t.Literal or origin is t_ext.Literal\n\n\ndef _literal_values(type_: type[t.Any]) -> tuple[t.Any, ...]:\n    return get_args(type_)\n\n\ndef all_literal_values(type_: type[t.Any]) -> list[t.Any]:\n    \"\"\"获取 Literal 类型包含的所有值\"\"\"\n    if not origin_is_literal(get_origin(type_)):\n        return [type_]\n\n    return [x for value in _literal_values(type_) for x in all_literal_values(value)]\n\n\ndef origin_is_annotated(origin: type[t.Any] | None) -> bool:\n    \"\"\"判断是否是 Annotated 类型\"\"\"\n    return origin is t_ext.Annotated\n\n\nNONE_TYPES = {None, type(None), t.Literal[None], t_ext.Literal[None], types.NoneType}  # noqa: PYI061\n\n\ndef is_none_type(type_: type[t.Any]) -> bool:\n    \"\"\"判断是否是 None 类型\"\"\"\n    return type_ in NONE_TYPES\n\n\nif sys.version_info < (3, 12):\n\n    def is_type_alias_type(type_: type[t.Any]) -> bool:\n        \"\"\"判断是否是 TypeAliasType 类型\"\"\"\n        return isinstance(type_, t_ext.TypeAliasType)\n\nelse:\n\n    def is_type_alias_type(type_: type[t.Any]) -> bool:\n        return isinstance(type_, (t.TypeAliasType, t_ext.TypeAliasType))\n\n\ndef evaluate_forwardref(\n    ref: t.ForwardRef, globalns: dict[str, t.Any], localns: dict[str, t.Any]\n) -> t.Any:\n    # Python 3.13/3.12.4+ made `recursive_guard` a kwarg,\n    # so name it explicitly to avoid:\n    # TypeError: ForwardRef._evaluate()\n    # missing 1 required keyword-only argument: 'recursive_guard'\n    return ref._evaluate(globalns, localns, recursive_guard=frozenset())\n\n\n# state\n# use annotated flag to avoid ForwardRef recreate generic type (py >= 3.11)\nclass StateFlag:\n    def __repr__(self) -> str:\n        return \"StateFlag()\"\n\n\n_STATE_FLAG = StateFlag()\n\nT_State: TypeAlias = t.Annotated[dict[t.Any, t.Any], _STATE_FLAG]\n\"\"\"事件处理状态 State 类型\"\"\"\n\n_DependentCallable: TypeAlias = t.Callable[..., T] | t.Callable[..., t.Awaitable[T]]\n\n# driver hooks\nT_BotConnectionHook: TypeAlias = _DependentCallable[t.Any]\n\"\"\"Bot 连接建立时钩子函数\n\n依赖参数:\n\n- DependParam: 子依赖参数\n- BotParam: Bot 对象\n- DefaultParam: 带有默认值的参数\n\"\"\"\nT_BotDisconnectionHook: TypeAlias = _DependentCallable[t.Any]\n\"\"\"Bot 连接断开时钩子函数\n\n依赖参数:\n\n- DependParam: 子依赖参数\n- BotParam: Bot 对象\n- DefaultParam: 带有默认值的参数\n\"\"\"\n\n# api hooks\nT_CallingAPIHook: TypeAlias = t.Callable[\n    [\"Bot\", str, dict[str, t.Any]], t.Awaitable[t.Any]\n]\n\"\"\"`bot.call_api` 钩子函数\"\"\"\nT_CalledAPIHook: TypeAlias = t.Callable[\n    [\"Bot\", Exception | None, str, dict[str, t.Any], t.Any], t.Awaitable[t.Any]\n]\n\"\"\"`bot.call_api` 后执行的函数，参数分别为 bot, exception, api, data, result\"\"\"\n\n# event hooks\nT_EventPreProcessor: TypeAlias = _DependentCallable[t.Any]\n\"\"\"事件预处理函数 EventPreProcessor 类型\n\n依赖参数:\n\n- DependParam: 子依赖参数\n- BotParam: Bot 对象\n- EventParam: Event 对象\n- StateParam: State 对象\n- DefaultParam: 带有默认值的参数\n\"\"\"\nT_EventPostProcessor: TypeAlias = _DependentCallable[t.Any]\n\"\"\"事件后处理函数 EventPostProcessor 类型\n\n依赖参数:\n\n- DependParam: 子依赖参数\n- BotParam: Bot 对象\n- EventParam: Event 对象\n- StateParam: State 对象\n- DefaultParam: 带有默认值的参数\n\"\"\"\n\n# matcher run hooks\nT_RunPreProcessor: TypeAlias = _DependentCallable[t.Any]\n\"\"\"事件响应器运行前预处理函数 RunPreProcessor 类型\n\n依赖参数:\n\n- DependParam: 子依赖参数\n- BotParam: Bot 对象\n- EventParam: Event 对象\n- StateParam: State 对象\n- MatcherParam: Matcher 对象\n- DefaultParam: 带有默认值的参数\n\"\"\"\nT_RunPostProcessor: TypeAlias = _DependentCallable[t.Any]\n\"\"\"事件响应器运行后后处理函数 RunPostProcessor 类型\n\n依赖参数:\n\n- DependParam: 子依赖参数\n- BotParam: Bot 对象\n- EventParam: Event 对象\n- StateParam: State 对象\n- MatcherParam: Matcher 对象\n- ExceptionParam: 异常对象（可能为 None）\n- DefaultParam: 带有默认值的参数\n\"\"\"\n\n# rule, permission\nT_RuleChecker: TypeAlias = _DependentCallable[bool]\n\"\"\"RuleChecker 即判断是否响应事件的处理函数。\n\n依赖参数:\n\n- DependParam: 子依赖参数\n- BotParam: Bot 对象\n- EventParam: Event 对象\n- StateParam: State 对象\n- DefaultParam: 带有默认值的参数\n\"\"\"\nT_PermissionChecker: TypeAlias = _DependentCallable[bool]\n\"\"\"PermissionChecker 即判断事件是否满足权限的处理函数。\n\n依赖参数:\n\n- DependParam: 子依赖参数\n- BotParam: Bot 对象\n- EventParam: Event 对象\n- DefaultParam: 带有默认值的参数\n\"\"\"\n\nT_Handler: TypeAlias = _DependentCallable[t.Any]\n\"\"\"Handler 处理函数。\"\"\"\nT_TypeUpdater: TypeAlias = _DependentCallable[str]\n\"\"\"TypeUpdater 在 Matcher.pause, Matcher.reject 时被运行，用于更新响应的事件类型。\n默认会更新为 `message`。\n\n依赖参数:\n\n- DependParam: 子依赖参数\n- BotParam: Bot 对象\n- EventParam: Event 对象\n- StateParam: State 对象\n- MatcherParam: Matcher 对象\n- DefaultParam: 带有默认值的参数\n\"\"\"\nT_PermissionUpdater: TypeAlias = _DependentCallable[\"Permission\"]\n\"\"\"PermissionUpdater 在 Matcher.pause, Matcher.reject 时被运行，用于更新会话对象权限。\n默认会更新为当前事件的触发对象。\n\n依赖参数:\n\n- DependParam: 子依赖参数\n- BotParam: Bot 对象\n- EventParam: Event 对象\n- StateParam: State 对象\n- MatcherParam: Matcher 对象\n- DefaultParam: 带有默认值的参数\n\"\"\"\nT_DependencyCache: TypeAlias = dict[_DependentCallable[t.Any], \"DependencyCache\"]\n\"\"\"依赖缓存, 用于存储依赖函数的返回值\"\"\"\n"
  },
  {
    "path": "nonebot/utils.py",
    "content": "\"\"\"本模块包含了 NoneBot 的一些工具函数\n\nFrontMatter:\n    mdx:\n        format: md\n    sidebar_position: 8\n    description: nonebot.utils 模块\n\"\"\"\n\nfrom collections import deque\nfrom collections.abc import (\n    AsyncGenerator,\n    Callable,\n    Coroutine,\n    Generator,\n    Mapping,\n    Sequence,\n)\nimport contextlib\nfrom contextlib import AbstractContextManager, asynccontextmanager\nimport dataclasses\nfrom functools import partial, wraps\nimport importlib\nimport inspect\nimport json\nfrom pathlib import Path\nimport re\nfrom typing import (\n    Any,\n    Generic,\n    TypeVar,\n    get_args,\n    get_origin,\n    overload,\n)\nfrom typing_extensions import ParamSpec, override\n\nimport anyio\nimport anyio.to_thread\nfrom exceptiongroup import BaseExceptionGroup, catch\nfrom pydantic import BaseModel\n\nfrom nonebot.log import logger\nfrom nonebot.typing import (\n    all_literal_values,\n    is_none_type,\n    origin_is_literal,\n    origin_is_union,\n    type_has_args,\n)\n\nP = ParamSpec(\"P\")\nR = TypeVar(\"R\")\nT = TypeVar(\"T\")\nK = TypeVar(\"K\")\nV = TypeVar(\"V\")\nE = TypeVar(\"E\", bound=BaseException)\n\n\ndef escape_tag(s: str) -> str:\n    \"\"\"用于记录带颜色日志时转义 `<tag>` 类型特殊标签\n\n    参考: [loguru color 标签](https://loguru.readthedocs.io/en/stable/api/logger.html#color)\n\n    参数:\n        s: 需要转义的字符串\n    \"\"\"\n    return re.sub(r\"</?((?:[fb]g\\s)?[^<>\\s]*)>\", r\"\\\\\\g<0>\", s)\n\n\ndef deep_update(\n    mapping: dict[K, Any], *updating_mappings: dict[K, Any]\n) -> dict[K, Any]:\n    \"\"\"深度更新合并字典\"\"\"\n    updated_mapping = mapping.copy()\n    for updating_mapping in updating_mappings:\n        for k, v in updating_mapping.items():\n            if (\n                k in updated_mapping\n                and isinstance(updated_mapping[k], dict)\n                and isinstance(v, dict)\n            ):\n                updated_mapping[k] = deep_update(updated_mapping[k], v)\n            else:\n                updated_mapping[k] = v\n    return updated_mapping\n\n\ndef lenient_issubclass(\n    cls: Any, class_or_tuple: type[Any] | tuple[type[Any], ...]\n) -> bool:\n    \"\"\"检查 cls 是否是 class_or_tuple 中的一个类型子类并忽略类型错误。\"\"\"\n    try:\n        return isinstance(cls, type) and issubclass(cls, class_or_tuple)\n    except TypeError:\n        return False\n\n\ndef generic_check_issubclass(\n    cls: Any, class_or_tuple: type[Any] | tuple[type[Any], ...]\n) -> bool:\n    \"\"\"检查 cls 是否是 class_or_tuple 中的一个类型子类。\n\n    特别的：\n\n    - 如果 cls 是 `typing.TypeVar` 类型，\n      则会检查其 `__bound__` 或 `__constraints__`\n      是否是 class_or_tuple 中一个类型的子类或 None。\n    - 如果 cls 是 `typing.Union` 或 `types.UnionType` 类型，\n      则会检查其中的所有类型是否是 class_or_tuple 中一个类型的子类或 None。\n    - 如果 cls 是 `typing.Literal` 类型，\n      则会检查其中的所有值是否是 class_or_tuple 中一个类型的实例。\n    - 如果 cls 是 `typing.List`、`typing.Dict` 等泛型类型，\n      则会检查其原始类型是否是 class_or_tuple 中一个类型的子类。\n    \"\"\"\n    # if the target is a TypeVar, we check it first\n    if isinstance(cls, TypeVar):\n        if cls.__constraints__:\n            return all(\n                is_none_type(type_) or generic_check_issubclass(type_, class_or_tuple)\n                for type_ in cls.__constraints__\n            )\n        elif cls.__bound__:\n            return generic_check_issubclass(cls.__bound__, class_or_tuple)\n        return False\n    # elif the target is not a generic type, we check it directly\n    elif not type_has_args(cls):\n        with contextlib.suppress(TypeError):\n            return issubclass(cls, class_or_tuple)\n\n    origin = get_origin(cls)\n    if origin_is_union(origin):\n        return all(\n            is_none_type(type_) or generic_check_issubclass(type_, class_or_tuple)\n            for type_ in get_args(cls)\n        )\n    elif origin_is_literal(origin):\n        return all(\n            is_none_type(value) or isinstance(value, class_or_tuple)\n            for value in all_literal_values(cls)\n        )\n    # ensure generic List, Dict can be checked\n    elif origin:\n        # avoid class check error (typing.Final, typing.ClassVar, etc...)\n        try:\n            return issubclass(origin, class_or_tuple)\n        except TypeError:\n            return False\n    return False\n\n\ndef type_is_complex(type_: type[Any]) -> bool:\n    \"\"\"检查 type_ 是否是复杂类型\"\"\"\n    origin = get_origin(type_)\n    return _type_is_complex_inner(type_) or _type_is_complex_inner(origin)\n\n\ndef _type_is_complex_inner(type_: type[Any] | None) -> bool:\n    if lenient_issubclass(type_, (str, bytes)):\n        return False\n\n    return lenient_issubclass(\n        type_, (BaseModel, Mapping, Sequence, tuple, set, frozenset, deque)\n    ) or dataclasses.is_dataclass(type_)\n\n\ndef is_coroutine_callable(call: Callable[..., Any]) -> bool:\n    \"\"\"检查 call 是否是一个 callable 协程函数\"\"\"\n    if inspect.isroutine(call):\n        return inspect.iscoroutinefunction(call)\n    if inspect.isclass(call):\n        return False\n    func_ = getattr(call, \"__call__\", None)\n    return inspect.iscoroutinefunction(func_)\n\n\ndef is_gen_callable(call: Callable[..., Any]) -> bool:\n    \"\"\"检查 call 是否是一个生成器函数\"\"\"\n    if inspect.isgeneratorfunction(call):\n        return True\n    func_ = getattr(call, \"__call__\", None)\n    return inspect.isgeneratorfunction(func_)\n\n\ndef is_async_gen_callable(call: Callable[..., Any]) -> bool:\n    \"\"\"检查 call 是否是一个异步生成器函数\"\"\"\n    if inspect.isasyncgenfunction(call):\n        return True\n    func_ = getattr(call, \"__call__\", None)\n    return inspect.isasyncgenfunction(func_)\n\n\ndef run_sync(call: Callable[P, R]) -> Callable[P, Coroutine[None, None, R]]:\n    \"\"\"一个用于包装 sync function 为 async function 的装饰器\n\n    参数:\n        call: 被装饰的同步函数\n    \"\"\"\n\n    @wraps(call)\n    async def _wrapper(*args: P.args, **kwargs: P.kwargs) -> R:\n        return await anyio.to_thread.run_sync(\n            partial(call, *args, **kwargs), abandon_on_cancel=True\n        )\n\n    return _wrapper\n\n\n@asynccontextmanager\nasync def run_sync_ctx_manager(\n    cm: AbstractContextManager[T],\n) -> AsyncGenerator[T, None]:\n    \"\"\"一个用于包装 sync context manager 为 async context manager 的执行函数\"\"\"\n    try:\n        yield await run_sync(cm.__enter__)()\n    except Exception as e:\n        ok = await run_sync(cm.__exit__)(type(e), e, None)\n        if not ok:\n            raise e\n    else:\n        await run_sync(cm.__exit__)(None, None, None)\n\n\n@overload\nasync def run_coro_with_catch(\n    coro: Coroutine[Any, Any, T],\n    exc: tuple[type[Exception], ...],\n    return_on_err: None = None,\n) -> T | None: ...\n\n\n@overload\nasync def run_coro_with_catch(\n    coro: Coroutine[Any, Any, T],\n    exc: tuple[type[Exception], ...],\n    return_on_err: R,\n) -> T | R: ...\n\n\nasync def run_coro_with_catch(\n    coro: Coroutine[Any, Any, T],\n    exc: tuple[type[Exception], ...],\n    return_on_err: R | None = None,\n) -> T | R | None:\n    \"\"\"运行协程并当遇到指定异常时返回指定值。\n\n    参数:\n        coro: 要运行的协程\n        exc: 要捕获的异常\n        return_on_err: 当发生异常时返回的值\n\n    返回:\n        协程的返回值或发生异常时的指定值\n    \"\"\"\n\n    with catch({exc: lambda exc_group: None}):\n        return await coro\n\n    return return_on_err\n\n\nasync def run_coro_with_shield(coro: Coroutine[Any, Any, T]) -> T:\n    \"\"\"运行协程并在取消时屏蔽取消异常。\n\n    参数:\n        coro: 要运行的协程\n\n    返回:\n        协程的返回值\n    \"\"\"\n\n    with anyio.CancelScope(shield=True):\n        return await coro\n\n    raise RuntimeError(\"This should not happen\")\n\n\ndef flatten_exception_group(\n    exc_group: BaseExceptionGroup[E],\n) -> Generator[E, None, None]:\n    for exc in exc_group.exceptions:\n        if isinstance(exc, BaseExceptionGroup):\n            yield from flatten_exception_group(exc)\n        else:\n            yield exc\n\n\ndef get_name(obj: Any) -> str:\n    \"\"\"获取对象的名称\"\"\"\n    if inspect.isfunction(obj) or inspect.isclass(obj):\n        return obj.__name__\n    return obj.__class__.__name__\n\n\ndef path_to_module_name(path: Path) -> str:\n    \"\"\"转换路径为模块名\"\"\"\n    rel_path = path.resolve().relative_to(Path.cwd().resolve())\n    if rel_path.stem == \"__init__\":\n        return \".\".join(rel_path.parts[:-1])\n    else:\n        return \".\".join((*rel_path.parts[:-1], rel_path.stem))\n\n\ndef resolve_dot_notation(\n    obj_str: str, default_attr: str, default_prefix: str | None = None\n) -> Any:\n    \"\"\"解析并导入点分表示法的对象\"\"\"\n    modulename, _, cls = obj_str.partition(\":\")\n    if default_prefix is not None and modulename.startswith(\"~\"):\n        modulename = default_prefix + modulename[1:]\n    module = importlib.import_module(modulename)\n    if not cls:\n        return getattr(module, default_attr)\n    instance = module\n    for attr_str in cls.split(\".\"):\n        instance = getattr(instance, attr_str)\n    return instance\n\n\nclass classproperty(Generic[T]):\n    \"\"\"类属性装饰器\"\"\"\n\n    def __init__(self, func: Callable[[Any], T]) -> None:\n        self.func = func\n\n    def __get__(self, instance: Any, owner: type[Any] | None = None) -> T:\n        return self.func(type(instance) if owner is None else owner)\n\n\nclass DataclassEncoder(json.JSONEncoder):\n    \"\"\"可以序列化 {ref}`nonebot.adapters.Message`(List[Dataclass]) 的 `JSONEncoder`\"\"\"\n\n    @override\n    def default(self, o):\n        if dataclasses.is_dataclass(o):\n            return {f.name: getattr(o, f.name) for f in dataclasses.fields(o)}\n        return super().default(o)\n\n\ndef logger_wrapper(logger_name: str):\n    \"\"\"用于打印 adapter 的日志。\n\n    参数:\n        logger_name: adapter 的名称\n\n    返回:\n        日志记录函数\n\n        日志记录函数的参数:\n\n        - level: 日志等级\n        - message: 日志信息\n        - exception: 异常信息\n    \"\"\"\n\n    def log(level: str, message: str, exception: Exception | None = None):\n        logger.opt(colors=True, exception=exception).log(\n            level, f\"<m>{escape_tag(logger_name)}</m> | {message}\"\n        )\n\n    return log\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"root\",\n  \"private\": true,\n  \"packageManager\": \"yarn@1.22.22\",\n  \"workspaces\": [\n    \"website\"\n  ],\n  \"scripts\": {\n    \"archive\": \"yarn workspace nonebot docusaurus docs:version\",\n    \"build\": \"yarn workspace nonebot build\",\n    \"build:plugin\": \"cross-env BASE_URL='/website/' yarn workspace nonebot build\",\n    \"start\": \"yarn workspace nonebot start\",\n    \"serve\": \"yarn workspace nonebot serve\",\n    \"clear\": \"yarn workspace nonebot clear\",\n    \"prettier\": \"prettier --config ./.prettierrc --write \\\"./website/\\\"\",\n    \"lint\": \"yarn lint:js && yarn lint:style\",\n    \"lint:js\": \"eslint --cache --report-unused-disable-directives \\\"**/*.{js,jsx,ts,tsx,mjs}\\\"\",\n    \"lint:js:fix\": \"eslint --cache --report-unused-disable-directives --fix \\\"**/*.{js,jsx,ts,tsx,mjs}\\\"\",\n    \"lint:style\": \"stylelint \\\"**/*.css\\\"\",\n    \"lint:style:fix\": \"stylelint --fix \\\"**/*.css\\\"\",\n    \"pyright\": \"pyright\"\n  },\n  \"devDependencies\": {\n    \"@typescript-eslint/eslint-plugin\": \"^5.62.0\",\n    \"@typescript-eslint/parser\": \"^5.62.0\",\n    \"cross-env\": \"^7.0.3\",\n    \"eslint\": \"^8.48.0\",\n    \"eslint-config-airbnb\": \"^19.0.4\",\n    \"eslint-config-prettier\": \"^8.8.0\",\n    \"eslint-plugin-import\": \"^2.27.5\",\n    \"eslint-plugin-jsx-a11y\": \"^6.7.1\",\n    \"eslint-plugin-react\": \"^7.32.2\",\n    \"eslint-plugin-react-hooks\": \"^4.6.0\",\n    \"eslint-plugin-regexp\": \"^1.15.0\",\n    \"prettier\": \"^3.0.3\",\n    \"pyright\": \"1.1.393\",\n    \"stylelint\": \"^15.10.3\",\n    \"stylelint-config-standard\": \"^34.0.0\",\n    \"stylelint-prettier\": \"^4.0.2\"\n  }\n}\n"
  },
  {
    "path": "packages/nonebot-plugin-docs/README.md",
    "content": "<p align=\"center\">\n  <a href=\"https://nonebot.dev/\"><img src=\"https://nonebot.dev/logo.png\" width=\"200\" height=\"200\" alt=\"nonebot\"></a>\n</p>\n\n<div align=\"center\">\n\n# nonebot-plugin-docs\n\n_✨ NoneBot 本地文档插件 ✨_\n\n</div>\n\n<p align=\"center\">\n  <a href=\"https://raw.githubusercontent.com/nonebot/nonebot2/master/LICENSE\">\n    <img src=\"https://img.shields.io/github/license/nonebot/nonebot2.svg\" alt=\"license\">\n  </a>\n  <a href=\"https://pypi.python.org/pypi/nonebot-plugin-docs\">\n    <img src=\"https://img.shields.io/pypi/v/nonebot-plugin-docs.svg\" alt=\"pypi\">\n  </a>\n  <img src=\"https://img.shields.io/badge/python-3.9+-blue.svg\" alt=\"python\">\n</p>\n\n## 使用方式\n\n加载插件并启动 Bot ，在浏览器内打开 `http://host:port/website/`。\n\n具体网址会在控制台内输出。\n"
  },
  {
    "path": "packages/nonebot-plugin-docs/nonebot_plugin_docs/__init__.py",
    "content": "import importlib\n\nimport nonebot\nfrom nonebot.log import logger\nfrom nonebot.plugin import PluginMetadata\n\n__plugin_meta__ = PluginMetadata(\n    name=\"NoneBot 离线文档\",\n    description=\"在本地查看 NoneBot 文档\",\n    usage=\"启动机器人后访问 http://localhost:port/website/ 查看文档\",\n    type=\"application\",\n    homepage=\"https://github.com/nonebot/nonebot2/blob/master/packages/nonebot-plugin-docs\",\n    config=None,\n    supported_adapters=None,\n)\n\n\ndef init():\n    driver = nonebot.get_driver()\n    try:\n        _module = importlib.import_module(\n            f\"nonebot_plugin_docs.drivers.{driver.type.split('+')[0]}\"\n        )\n    except ImportError:\n        logger.warning(f\"Driver {driver.type} not supported\")\n        return\n    register_route = getattr(_module, \"register_route\")\n    register_route(driver)\n    host = str(driver.config.host)\n    port = driver.config.port\n    if host in {\"0.0.0.0\", \"127.0.0.1\"}:\n        host = \"localhost\"\n    logger.opt(colors=True).info(\n        f\"Nonebot docs will be running at: <b><u>http://{host}:{port}/website/</u></b>\"\n    )\n\n\ninit()\n"
  },
  {
    "path": "packages/nonebot-plugin-docs/nonebot_plugin_docs/drivers/fastapi.py",
    "content": "from pathlib import Path\n\nfrom fastapi.staticfiles import StaticFiles\n\nfrom nonebot.drivers.fastapi import Driver\n\n\ndef register_route(driver: Driver):\n    app = driver.server_app\n\n    static_path = str((Path(__file__).parent / \"..\" / \"dist\").resolve())\n\n    app.mount(\"/website\", StaticFiles(directory=static_path, html=True), name=\"docs\")\n"
  },
  {
    "path": "packages/nonebot-plugin-docs/pyproject.toml",
    "content": "[project]\nname = \"nonebot-plugin-docs\"\nversion = \"2.0.0\"\ndescription = \"View NoneBot2 Docs Locally\"\nauthors = [{ name = \"yanyongyu\", email = \"yyy@nonebot.dev\" }]\nreadme = \"README.md\"\nlicense = \"MIT\"\nkeywords = [\"nonebot\", \"nonebot2\", \"docs\"]\nrequires-python = \">=3.9, <4.0\"\ndependencies = [\"nonebot2 >=2.0.0, <3.0.0\"]\n\n[project.urls]\nHomepage = \"https://github.com/nonebot/nonebot2/blob/master/packages/nonebot-plugin-docs\"\nRepository = \"https://github.com/nonebot/nonebot2\"\n\n[dependency-groups]\ndev = []\n\n[tool.uv.build-backend]\nmodule-root = \"\"\n\n[build-system]\nrequires = [\"uv_build >=0.8.3, <0.9.0\"]\nbuild-backend = \"uv_build\"\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[project]\nname = \"nonebot2\"\nversion = \"2.4.4\"\ndescription = \"An asynchronous python bot framework.\"\nauthors = [{ name = \"yanyongyu\", email = \"yyy@nonebot.dev\" }]\nlicense = \"MIT\"\nreadme = \"README.md\"\nkeywords = [\"bot\", \"qq\", \"qqbot\", \"mirai\", \"coolq\"]\nclassifiers = [\n  \"Development Status :: 5 - Production/Stable\",\n  \"Framework :: Robot Framework\",\n  \"Framework :: Robot Framework :: Library\",\n  \"Operating System :: OS Independent\",\n  \"Programming Language :: Python :: 3\",\n]\nrequires-python = \">=3.10, <4.0\"\ndependencies = [\n  \"yarl >=1.7.2, <2.0.0\",\n  \"anyio >=4.4.0, <5.0.0\",\n  \"loguru >=0.6.0, <1.0.0\",\n  \"pygtrie >=2.4.1, <3.0.0\",\n  \"exceptiongroup >=1.2.2, <2.0.0\",\n  \"python-dotenv >=0.21.0, <2.0.0\",\n  \"typing-extensions >=4.6.0, <5.0.0\",\n  \"tomli >=2.0.1, <3.0.0; python_version < '3.11'\",\n  \"pydantic >=1.10.0, <3.0.0, !=2.5.0, !=2.5.1, !=2.10.0, !=2.10.1\",\n]\n\n[project.optional-dependencies]\nwebsockets = [\"websockets >=15.0\"]\nhttpx = [\"httpx[http2] >=0.26.0, <1.0.0\"]\naiohttp = [\"aiohttp[speedups] >=3.11.0, <4.0.0\"]\nquart = [\"Quart >=0.18.0, <1.0.0\", \"uvicorn[standard] >=0.20.0, <1.0.0\"]\nfastapi = [\"fastapi >=0.93.0, <1.0.0\", \"uvicorn[standard] >=0.20.0, <1.0.0\"]\nall = [\n  \"websockets >=15.0\",\n  \"fastapi >=0.93.0, <1.0.0\",\n  \"httpx[http2] >=0.26.0, <1.0.0\",\n  \"aiohttp[speedups] >=3.11.0, <4.0.0\",\n  \"uvicorn[standard] >=0.20.0, <1.0.0\",\n]\n\n[dependency-groups]\ndev = [\n  { include-group = \"test\" },\n  { include-group = \"docs\" },\n  \"ruff >=0.14.0, <0.15.0\",\n  \"nonemoji >=0.1.2, <0.2.0\",\n  \"pre-commit >=4.0.0, <5.0.0\",\n]\ntest = [\n  \"trio >=0.27.0\",\n  \"nonebug >=0.4.1, <0.5.0\",\n  \"wsproto >=1.2.0, <2.0.0\",\n  \"werkzeug >=2.3.6, <4.0.0\",\n  \"pytest-cov >=7.0.0, <8.0.0\",\n  \"pytest-xdist >=3.0.2, <4.0.0\",\n  \"coverage-conditional-plugin >=0.9.0, <0.10.0\",\n]\ndocs = [\"nb-autodoc >=1.0.4, <2.0.0\"]\npydantic-v1 = [\"pydantic >=1.10.0, <2.0.0\"]\npydantic-v2 = [\"pydantic >=2.0.0, <3.0.0\"]\n\n[project.urls]\nHomepage = \"https://nonebot.dev/\"\nRepository = \"https://github.com/nonebot/nonebot2\"\nDocumentation = \"https://nonebot.dev/\"\n\"Bug Tracker\" = \"https://github.com/nonebot/nonebot2/issues\"\nChangelog = \"https://nonebot.dev/changelog\"\nFunding = \"https://afdian.com/@nonebot\"\n\n[tool.uv]\nrequired-version = \">=0.8.0\"\nconflicts = [[{ group = \"pydantic-v1\" }, { group = \"pydantic-v2\" }]]\n\n[tool.uv.build-backend]\nmodule-name = \"nonebot\"\nmodule-root = \"\"\n\n[tool.pytest.ini_options]\naddopts = \"--cov=nonebot --cov-report=term-missing\"\nfilterwarnings = [\"error\", \"ignore::DeprecationWarning\"]\n\n[tool.ruff]\nline-length = 88\n\n[tool.ruff.format]\nline-ending = \"lf\"\n\n[tool.ruff.lint]\nselect = [\n  \"F\",     # Pyflakes\n  \"W\",     # pycodestyle warnings\n  \"E\",     # pycodestyle errors\n  \"I\",     # isort\n  \"UP\",    # pyupgrade\n  \"ASYNC\", # flake8-async\n  \"C4\",    # flake8-comprehensions\n  \"T10\",   # flake8-debugger\n  \"T20\",   # flake8-print\n  \"PYI\",   # flake8-pyi\n  \"PT\",    # flake8-pytest-style\n  \"Q\",     # flake8-quotes\n  \"TID\",   # flake8-tidy-imports\n  \"RUF\",   # Ruff-specific rules\n]\nignore = [\n  \"E402\",   # module-import-not-at-top-of-file\n  \"UP037\",  # quoted-annotation\n  \"RUF001\", # ambiguous-unicode-character-string\n  \"RUF002\", # ambiguous-unicode-character-docstring\n  \"RUF003\", # ambiguous-unicode-character-comment\n]\n\n[tool.ruff.lint.isort]\nforce-sort-within-sections = true\nknown-first-party = [\"nonebot\", \"tests/*\"]\nextra-standard-library = [\"typing_extensions\"]\n\n[tool.ruff.lint.flake8-pytest-style]\nfixture-parentheses = false\nmark-parentheses = false\n\n[tool.ruff.lint.pyupgrade]\nkeep-runtime-typing = true\n\n[tool.pyright]\npythonVersion = \"3.10\"\npythonPlatform = \"All\"\ndefineConstant = { PYDANTIC_V2 = true }\nexecutionEnvironments = [\n  { root = \"./tests/python_3_12\", pythonVersion = \"3.12\", extraPaths = [\n    \"./\",\n  ] },\n  { root = \"./tests\", extraPaths = [\n    \"./\",\n  ] },\n  { root = \"./\" },\n]\n\ntypeCheckingMode = \"standard\"\nreportShadowedImports = false\ndisableBytesTypePromotions = true\n\n[build-system]\nrequires = [\"uv_build >=0.8.3, <0.10.0\"]\nbuild-backend = \"uv_build\"\n"
  },
  {
    "path": "scripts/build-api-docs.sh",
    "content": "#!/usr/bin/env bash\n\nset -e\n\n# cd to the root of the project\ncd \"$(dirname \"$0\")/..\"\n\nnb-autodoc nonebot \\\n  -s nonebot.plugins \\\n  -u nonebot.internal \\\n  -u nonebot.internal.*\ncp -r ./build/nonebot/* ./website/docs/api/\nyarn prettier\n"
  },
  {
    "path": "scripts/run-tests.sh",
    "content": "#!/usr/bin/env bash\n\n# cd to the root of the tests\ncd \"$(dirname \"$0\")/../tests\"\n\n# Run the tests\npytest -n auto --cov-append --cov-report xml --junitxml=./junit.xml $@\n"
  },
  {
    "path": "scripts/setup-envs.sh",
    "content": "#!/usr/bin/env bash\n\necho \"Setting up dev environment\"\nuv sync --all-extras && uv run pre-commit install && yarn install\n"
  },
  {
    "path": "tests/.coveragerc",
    "content": "[run]\nplugins =\n  coverage_conditional_plugin\n\n[report]\nexclude_lines =\n  pragma: no cover\n  def __repr__\n  def __str__\n  @(typing\\.)?overload\n  if (typing\\.)?TYPE_CHECKING( is True)?:\n  @(abc\\.)?abstractmethod\n  raise NotImplementedError\n  warnings\\.warn\n  ^\\.\\.\\.$\n  pass\n  if __name__ == .__main__.:\n\n[coverage_conditional_plugin]\nrules =\n  \"sys_platform != 'win32'\": py-win32\n  \"sys_platform != 'linux'\": py-linux\n  \"sys_platform != 'darwin'\": py-darwin\n  \"sys_version_info < (3, 11)\": py-gte-311\n  \"sys_version_info >= (3, 11)\": py-lt-311\n  \"package_version('pydantic') < (2,)\": pydantic-v2\n  \"package_version('pydantic') >= (2,)\": pydantic-v1\n"
  },
  {
    "path": "tests/bad_plugins/bad_plugin.py",
    "content": "import nonebot\n\nplugin = nonebot.get_plugin(\"bad_plugin\")\nassert plugin\n\nx = 1 / 0\n"
  },
  {
    "path": "tests/conftest.py",
    "content": "from collections.abc import Callable, Generator\nfrom functools import wraps\nimport os\nfrom pathlib import Path\nimport sys\nimport threading\nfrom types import EllipsisType\nfrom typing import TYPE_CHECKING, TypeVar\nfrom typing_extensions import ParamSpec\n\nfrom nonebug import NONEBOT_INIT_KWARGS\nimport pytest\nfrom werkzeug.serving import BaseWSGIServer, make_server\n\nfrom fake_server import request_handler\nimport nonebot\nfrom nonebot import _resolve_combine_expr\nfrom nonebot.config import Env\nfrom nonebot.drivers import URL, Driver\n\nos.environ[\"CONFIG_FROM_ENV\"] = '{\"test\": \"test\"}'\nos.environ[\"CONFIG_OVERRIDE\"] = \"new\"\n\nif TYPE_CHECKING:\n    from nonebot.plugin import Plugin\n\nP = ParamSpec(\"P\")\nR = TypeVar(\"R\")\n\ncollect_ignore = [\"plugins/\", \"dynamic/\", \"bad_plugins/\"]\n\n\ndef pytest_configure(config: pytest.Config) -> None:\n    config.stash[NONEBOT_INIT_KWARGS] = {\"config_from_init\": \"init\"}\n\n\n@pytest.fixture(name=\"driver\")\ndef load_driver(request: pytest.FixtureRequest) -> Driver:\n    driver_name = getattr(request, \"param\", None)\n    global_driver = nonebot.get_driver()\n    if driver_name is None:\n        return global_driver\n\n    DriverClass = _resolve_combine_expr(driver_name)\n    return DriverClass(Env(environment=global_driver.env), global_driver.config)\n\n\n@pytest.fixture(scope=\"session\", params=[pytest.param(\"asyncio\"), pytest.param(\"trio\")])\ndef anyio_backend(request: pytest.FixtureRequest):\n    return request.param\n\n\ndef run_once(func: Callable[P, R]) -> Callable[P, R]:\n    result: R | EllipsisType = ...\n\n    @wraps(func)\n    def _wrapper(*args: P.args, **kwargs: P.kwargs) -> R:\n        nonlocal result\n        if result is not ...:\n            return result\n\n        result = func(*args, **kwargs)\n        return result\n\n    return _wrapper\n\n\n@pytest.fixture(scope=\"session\", autouse=True)\n@run_once\ndef load_plugin(anyio_backend, nonebug_init: None) -> set[\"Plugin\"]:\n    # preload global plugins\n    plugins: set[\"Plugin\"] = set()\n    plugins |= nonebot.load_plugins(str(Path(__file__).parent / \"plugins\"))\n    if sys.version_info >= (3, 12):\n        # preload python 3.12 plugins\n        plugins |= nonebot.load_plugins(\n            str(Path(__file__).parent / \"python_3_12\" / \"plugins\")\n        )\n    return plugins\n\n\n@pytest.fixture(scope=\"session\", autouse=True)\n@run_once\ndef load_builtin_plugin(anyio_backend, nonebug_init: None) -> set[\"Plugin\"]:\n    # preload builtin plugins\n    return nonebot.load_builtin_plugins(\"echo\", \"single_session\")\n\n\n@pytest.fixture(scope=\"session\", autouse=True)\ndef server() -> Generator[BaseWSGIServer, None, None]:\n    server = make_server(\"127.0.0.1\", 0, app=request_handler)\n    thread = threading.Thread(target=server.serve_forever)\n    thread.start()\n    try:\n        yield server\n    finally:\n        server.shutdown()\n        thread.join()\n\n\n@pytest.fixture(scope=\"session\")\ndef server_url(server: BaseWSGIServer) -> URL:\n    return URL(f\"http://{server.host}:{server.port}\")\n"
  },
  {
    "path": "tests/dynamic/manager.py",
    "content": ""
  },
  {
    "path": "tests/dynamic/path.py",
    "content": ""
  },
  {
    "path": "tests/dynamic/require_not_declared.py",
    "content": ""
  },
  {
    "path": "tests/dynamic/require_not_loaded/__init__.py",
    "content": ""
  },
  {
    "path": "tests/dynamic/require_not_loaded/subplugin1.py",
    "content": ""
  },
  {
    "path": "tests/dynamic/require_not_loaded/subplugin2.py",
    "content": ""
  },
  {
    "path": "tests/dynamic/simple.py",
    "content": ""
  },
  {
    "path": "tests/fake_server.py",
    "content": "import base64\nimport json\nimport socket\nfrom typing import TypeVar\n\nfrom werkzeug import Request, Response\nfrom werkzeug.datastructures import MultiDict\nfrom wsproto import ConnectionType, WSConnection\nfrom wsproto.events import (\n    AcceptConnection,\n    BytesMessage,\n    CloseConnection,\n    Ping,\n    TextMessage,\n)\nfrom wsproto.events import Request as WSRequest\nfrom wsproto.frame_protocol import CloseReason\n\nK = TypeVar(\"K\")\nV = TypeVar(\"V\")\n\n\ndef json_safe(string, content_type=\"application/octet-stream\") -> str:\n    try:\n        string = string.decode(\"utf-8\")\n        json.dumps(string)\n        return string\n    except (ValueError, TypeError):\n        return b\"\".join(\n            [\n                b\"data:\",\n                content_type.encode(\"utf-8\"),\n                b\";base64,\",\n                base64.b64encode(string),\n            ]\n        ).decode(\"utf-8\")\n\n\ndef flattern(d: \"MultiDict[K, V]\") -> dict[K, V | list[V]]:\n    return {k: v[0] if len(v) == 1 else v for k, v in d.to_dict(flat=False).items()}\n\n\ndef http_echo(request: Request) -> Response:\n    try:\n        _json = json.loads(request.data.decode(\"utf-8\"))\n    except (ValueError, TypeError):\n        _json = None\n\n    return Response(\n        json.dumps(\n            {\n                \"url\": request.url,\n                \"method\": request.method,\n                \"origin\": request.headers.get(\"X-Forwarded-For\", request.remote_addr),\n                \"headers\": flattern(\n                    MultiDict((k, v) for k, v in request.headers.items())\n                ),\n                \"args\": flattern(request.args),\n                \"form\": flattern(request.form),\n                \"data\": json_safe(request.data),\n                \"json\": _json,\n                \"files\": flattern(\n                    MultiDict(\n                        (\n                            k,\n                            json_safe(\n                                v.read(),\n                                request.files[k].content_type\n                                or \"application/octet-stream\",\n                            ),\n                        )\n                        for k, v in request.files.items()\n                    )\n                ),\n            }\n        ),\n        status=200,\n        content_type=\"application/json\",\n    )\n\n\ndef websocket_echo(request: Request) -> Response:\n    stream = request.environ[\"werkzeug.socket\"]\n\n    ws = WSConnection(ConnectionType.SERVER)\n\n    in_data = b\"GET %s HTTP/1.1\\r\\n\" % request.path.encode(\"utf-8\")\n    for header, value in request.headers.items():\n        in_data += f\"{header}: {value}\\r\\n\".encode()\n    in_data += b\"\\r\\n\"\n\n    ws.receive_data(in_data)\n\n    running: bool = True\n    while True:\n        out_data = b\"\"\n\n        for event in ws.events():\n            if isinstance(event, WSRequest):\n                out_data += ws.send(AcceptConnection())\n            elif isinstance(event, CloseConnection):\n                out_data += ws.send(event.response())\n                running = False\n            elif isinstance(event, Ping):\n                out_data += ws.send(event.response())\n            elif isinstance(event, TextMessage):\n                if event.data == \"quit\":\n                    out_data += ws.send(\n                        CloseConnection(CloseReason.NORMAL_CLOSURE, \"bye\")\n                    )\n                    running = False\n                else:\n                    out_data += ws.send(TextMessage(data=event.data))\n            elif isinstance(event, BytesMessage):\n                if event.data == b\"quit\":\n                    out_data += ws.send(\n                        CloseConnection(CloseReason.NORMAL_CLOSURE, \"bye\")\n                    )\n                    running = False\n                else:\n                    out_data += ws.send(BytesMessage(data=event.data))\n\n        if out_data:\n            stream.send(out_data)\n\n        if not running:\n            break\n\n        in_data = stream.recv(4096)\n        ws.receive_data(in_data)\n\n    stream.shutdown(socket.SHUT_RDWR)\n    return Response(\"\", status=204)\n\n\n@Request.application\ndef request_handler(request: Request) -> Response:\n    if request.headers.get(\"Connection\") == \"Upgrade\":\n        return websocket_echo(request)\n    else:\n        return http_echo(request)\n"
  },
  {
    "path": "tests/plugins/_hidden.py",
    "content": "import pytest\n\npytest.fail(\"should not be imported\")\n"
  },
  {
    "path": "tests/plugins/export.py",
    "content": "def test():\n    return \"export\"\n"
  },
  {
    "path": "tests/plugins/matcher/__init__.py",
    "content": "from pathlib import Path\n\nfrom nonebot import load_plugins\n\n_sub_plugins = set()\n\n_sub_plugins |= load_plugins(str(Path(__file__).parent))\n"
  },
  {
    "path": "tests/plugins/matcher/matcher_expire.py",
    "content": "from datetime import datetime, timedelta\n\nfrom nonebot.matcher import Matcher\n\ntest_temp_matcher = Matcher.new(\"test\", temp=True)\ntest_datetime_matcher = Matcher.new(\n    \"test\", expire_time=datetime.now() - timedelta(seconds=1)\n)\ntest_timedelta_matcher = Matcher.new(\"test\", expire_time=timedelta(seconds=-1))\n"
  },
  {
    "path": "tests/plugins/matcher/matcher_info.py",
    "content": "from nonebot import on\n\nmatcher = on(\"message\", temp=False, expire_time=None, priority=1, block=True)\n"
  },
  {
    "path": "tests/plugins/matcher/matcher_permission.py",
    "content": "from nonebot.matcher import Matcher\nfrom nonebot.permission import USER, Permission\n\ndefault_permission = Permission()\nnew_permission = Permission()\n\ntest_permission_updater = Matcher.new(permission=default_permission)\n\ntest_user_permission_updater = Matcher.new(\n    permission=USER(\"test\", perm=default_permission)\n)\n\ntest_custom_updater = Matcher.new(permission=default_permission)\n\n\n@test_custom_updater.permission_updater\nasync def _() -> Permission:\n    return new_permission\n"
  },
  {
    "path": "tests/plugins/matcher/matcher_process.py",
    "content": "from nonebot import on_message\nfrom nonebot.adapters import Event, Message\nfrom nonebot.matcher import Matcher\nfrom nonebot.params import ArgStr, EventMessage, LastReceived, Received\n\ntest_handle = on_message()\n\n\n@test_handle.handle()\nasync def handle():\n    await test_handle.finish(\"send\", at_sender=True)\n\n\ntest_got = on_message()\n\n\n@test_got.got(\"key1\", \"prompt key1\")\n@test_got.got(\"key2\", \"prompt key2\")\nasync def got(key1: str = ArgStr(), key2: str = ArgStr()):\n    if key2 == \"text\":\n        await test_got.reject(\"reject\", at_sender=True)\n\n    assert key1 == \"text\"\n    assert key2 == \"text_next\"\n\n\ntest_receive = on_message()\n\n\n@test_receive.receive()\n@test_receive.receive(\"receive\")\nasync def receive(\n    x: Event = Received(\"receive\"), y: Event = LastReceived(), z: Event = Received()\n):\n    assert str(x.get_message()) == \"text\"\n    assert str(z.get_message()) == \"text\"\n    assert x is y\n    await test_receive.pause(\"pause\", at_sender=True)\n\n\ntest_combine = on_message()\n\n\n@test_combine.got(\"a\")\n@test_combine.receive()\n@test_combine.got(\"b\")\nasync def combine(a: str = ArgStr(), b: str = ArgStr(), r: Event = Received()):\n    if a == \"text\":\n        await test_combine.reject_arg(\"a\")\n    elif b == \"text\":\n        await test_combine.reject_arg(\"b\")\n    elif str(r.get_message()) == \"text\":\n        await test_combine.reject_receive()\n\n    assert a == \"text_next\"\n    assert b == \"text_next\"\n    assert str(r.get_message()) == \"text_next\"\n\n\ntest_preset = on_message()\n\n\n@test_preset.handle()\nasync def preset(matcher: Matcher, message: Message = EventMessage()):\n    matcher.set_arg(\"a\", message)\n\n\n@test_preset.got(\"a\")\n@test_preset.got(\"b\")\nasync def reject_preset(a: str = ArgStr(), b: str = ArgStr()):\n    if a == \"text\":\n        await test_preset.reject_arg(\"a\")\n\n    assert a == \"text_next\"\n    assert b == \"text\"\n\n\ntest_overload = on_message()\n\n\nclass FakeEvent(Event): ...\n\n\n@test_overload.got(\"a\")\nasync def overload(event: FakeEvent):\n    await test_overload.reject_arg(\"a\")\n\n\n@test_overload.handle()\nasync def finish():\n    await test_overload.finish()\n\n\ntest_destroy = on_message()\n"
  },
  {
    "path": "tests/plugins/matcher/matcher_type.py",
    "content": "from nonebot.matcher import Matcher\n\ntest_type_updater = Matcher.new(type_=\"test\")\n\ntest_custom_updater = Matcher.new(type_=\"test\")\n\n\n@test_custom_updater.type_updater\nasync def _() -> str:\n    return \"custom\"\n"
  },
  {
    "path": "tests/plugins/metadata.py",
    "content": "from pydantic import BaseModel\n\nfrom nonebot.adapters import Adapter\nfrom nonebot.plugin import PluginMetadata\n\n\nclass Config(BaseModel):\n    custom: str = \"\"\n\n\nclass FakeAdapter(Adapter): ...\n\n\n__plugin_meta__ = PluginMetadata(\n    name=\"测试插件\",\n    description=\"测试插件元信息\",\n    usage=\"无法使用\",\n    type=\"application\",\n    homepage=\"https://nonebot.dev\",\n    config=Config,\n    supported_adapters={\"~onebot.v11\", \"plugins.metadata:FakeAdapter\"},\n    extra={\"author\": \"NoneBot\"},\n)\n"
  },
  {
    "path": "tests/plugins/metadata_2.py",
    "content": "from nonebot.plugin import PluginMetadata\n\n__plugin_meta__ = PluginMetadata(\n    name=\"测试插件2\",\n    description=\"测试继承适配器\",\n    usage=\"无法使用\",\n    type=\"application\",\n    homepage=\"https://nonebot.dev\",\n    supported_adapters={\"~onebot.v11\", \"~onebot.v12\"},\n    extra={\"author\": \"NoneBot\"},\n)\n"
  },
  {
    "path": "tests/plugins/metadata_3.py",
    "content": "from nonebot.plugin import PluginMetadata\n\n__plugin_meta__ = PluginMetadata(\n    name=\"测试插件3\",\n    description=\"测试继承适配器, 使用内置适配器全名\",\n    usage=\"无法使用\",\n    type=\"application\",\n    homepage=\"https://nonebot.dev\",\n    supported_adapters={\n        \"nonebot.adapters.onebot.v11\",\n        \"nonebot.adapters.onebot.v12\",\n        \"~qq\",\n    },\n    extra={\"author\": \"NoneBot\"},\n)\n"
  },
  {
    "path": "tests/plugins/nested/__init__.py",
    "content": "from pathlib import Path\n\nfrom nonebot.plugin import PluginManager, _managers\n\nmanager = PluginManager(\n    search_path=[str((Path(__file__).parent / \"plugins\").resolve())]\n)\n_managers.append(manager)\n\n# test load nested plugin with require\nmanager.load_plugin(\"plugins.nested.plugins.nested_subplugin\")\nmanager.load_plugin(\"nested:nested_subplugin2\")\n"
  },
  {
    "path": "tests/plugins/nested/plugins/nested_subplugin.py",
    "content": "from .nested_subplugin2 import a  # noqa: F401\n"
  },
  {
    "path": "tests/plugins/nested/plugins/nested_subplugin2.py",
    "content": "a = \"required by another subplugin\"\n"
  },
  {
    "path": "tests/plugins/param/__init__.py",
    "content": "from pathlib import Path\n\nfrom nonebot import load_plugins\n\n_sub_plugins = set()\n\n_sub_plugins |= load_plugins(str(Path(__file__).parent))\n"
  },
  {
    "path": "tests/plugins/param/param_arg.py",
    "content": "from typing import Annotated, Any\n\nfrom nonebot.adapters import Message\nfrom nonebot.params import Arg, ArgPlainText, ArgPromptResult, ArgStr\n\n\nasync def arg(key: Message = Arg()) -> Message:\n    return key\n\n\nasync def arg_str(key: str = ArgStr()) -> str:\n    return key\n\n\nasync def arg_plain_text(key: str = ArgPlainText()) -> str:\n    return key\n\n\nasync def annotated_arg(key: Annotated[Message, Arg()]) -> Message:\n    return key\n\n\nasync def annotated_arg_str(key: Annotated[str, ArgStr()]) -> str:\n    return key\n\n\nasync def annotated_arg_plain_text(key: Annotated[str, ArgPlainText()]) -> str:\n    return key\n\n\nasync def annotated_arg_prompt_result(key: Annotated[Any, ArgPromptResult()]) -> Any:\n    return key\n\n\n# test dependency priority\nasync def annotated_prior_arg(key: Annotated[str, ArgStr(\"foo\")] = ArgPlainText()):\n    return key\n\n\nasync def annotated_multi_arg(\n    key: Annotated[Annotated[str, ArgStr(\"foo\")], ArgPlainText()],\n):\n    return key\n"
  },
  {
    "path": "tests/plugins/param/param_bot.py",
    "content": "from typing import TypeVar\n\nfrom nonebot.adapters import Bot\n\n\nasync def get_bot(b: Bot) -> Bot:\n    return b\n\n\nasync def postpone_bot(b: \"Bot\") -> Bot:\n    return b\n\n\nasync def legacy_bot(bot):\n    return bot\n\n\nasync def not_legacy_bot(bot: int): ...\n\n\nclass FooBot(Bot): ...\n\n\nasync def sub_bot(b: FooBot) -> FooBot:\n    return b\n\n\nclass BarBot(Bot): ...\n\n\nasync def union_bot(b: FooBot | BarBot) -> FooBot | BarBot:\n    return b\n\n\nB = TypeVar(\"B\", bound=Bot)\n\n\nasync def generic_bot(b: B) -> B:\n    return b\n\n\nCB = TypeVar(\"CB\", Bot, None)\n\n\nasync def generic_bot_none(b: CB) -> CB:\n    return b\n\n\nasync def not_bot(b: int | Bot): ...\n"
  },
  {
    "path": "tests/plugins/param/param_default.py",
    "content": "async def default(value: int = 1) -> int:\n    return value\n"
  },
  {
    "path": "tests/plugins/param/param_depend.py",
    "content": "from dataclasses import dataclass\nfrom typing import Annotated\n\nimport anyio\nfrom pydantic import Field\n\nfrom nonebot import on_message\nfrom nonebot.adapters import Bot\nfrom nonebot.params import Depends\n\ntest_depends = on_message()\n\nrunned = []\n\n\ndef dependency():\n    runned.append(1)\n    return 1\n\n\ndef parameterless():\n    assert len(runned) == 0\n    runned.append(1)\n\n\ndef gen_sync():\n    yield 1\n\n\nasync def gen_async():\n    yield 2\n\n\n@dataclass\nclass ClassDependency:\n    x: int = Depends(gen_sync)\n    y: int = Depends(gen_async)\n\n\nclass FooBot(Bot): ...\n\n\nasync def sub_bot(b: FooBot) -> FooBot:\n    return b\n\n\n# test parameterless\n@test_depends.handle(parameterless=[Depends(parameterless)])\nasync def depends(x: int = Depends(dependency)):\n    # test dependency\n    return x\n\n\n@test_depends.handle()\nasync def depends_cache(y: int = Depends(dependency, use_cache=True)):\n    # test cache\n    return y\n\n\n# test class dependency\nasync def class_depend(c: ClassDependency = Depends()):\n    return c\n\n\n# test annotated dependency\nasync def annotated_depend(x: Annotated[int, Depends(dependency)]):\n    return x\n\n\n# test annotated class dependency\nasync def annotated_class_depend(c: Annotated[ClassDependency, Depends()]):\n    return c\n\n\n# test dependency priority\nasync def annotated_prior_depend(\n    x: Annotated[int, Depends(lambda: 2)] = Depends(dependency),\n):\n    return x\n\n\nasync def annotated_multi_depend(\n    x: Annotated[Annotated[int, Depends(lambda: 2)], Depends(dependency)],\n):\n    return x\n\n\n# test sub dependency type mismatch\nasync def sub_type_mismatch(b: FooBot = Depends(sub_bot)):\n    return b\n\n\n# test type validate\nasync def validate(x: int = Depends(lambda: \"1\", validate=True)):\n    return x\n\n\nasync def validate_fail(x: int = Depends(lambda: \"not_number\", validate=True)):\n    return x\n\n\n# test FieldInfo validate\nasync def validate_field(x: int = Depends(lambda: \"1\", validate=Field(gt=0))):\n    return x\n\n\nasync def validate_field_fail(x: int = Depends(lambda: \"0\", validate=Field(gt=0))):\n    return x\n\n\nasync def _dep():\n    await anyio.sleep(1)\n    return 1\n\n\ndef _dep_mismatch():\n    return 1\n\n\nasync def cache_exception_func1(\n    dep: int = Depends(_dep),\n    mismatch: dict = Depends(_dep_mismatch),\n):\n    raise RuntimeError(\"Never reach here\")\n\n\nasync def cache_exception_func2(\n    dep: int = Depends(_dep),\n    match: int = Depends(_dep_mismatch),\n):\n    return dep\n"
  },
  {
    "path": "tests/plugins/param/param_event.py",
    "content": "from typing import TypeVar\n\nfrom nonebot.adapters import Event, Message\nfrom nonebot.params import EventMessage, EventPlainText, EventToMe, EventType\n\n\nasync def event(e: Event) -> Event:\n    return e\n\n\nasync def postpone_event(e: \"Event\") -> Event:\n    return e\n\n\nasync def legacy_event(event):\n    return event\n\n\nasync def not_legacy_event(event: int): ...\n\n\nclass FooEvent(Event): ...\n\n\nasync def sub_event(e: FooEvent) -> FooEvent:\n    return e\n\n\nclass BarEvent(Event): ...\n\n\nasync def union_event(e: FooEvent | BarEvent) -> FooEvent | BarEvent:\n    return e\n\n\nE = TypeVar(\"E\", bound=Event)\n\n\nasync def generic_event(e: E) -> E:\n    return e\n\n\nCE = TypeVar(\"CE\", Event, None)\n\n\nasync def generic_event_none(e: CE) -> CE:\n    return e\n\n\nasync def not_event(e: int | Event): ...\n\n\nasync def event_type(t: str = EventType()) -> str:\n    return t\n\n\nasync def event_message(msg: Message = EventMessage()) -> Message:\n    return msg\n\n\nasync def event_plain_text(text: str = EventPlainText()) -> str:\n    return text\n\n\nasync def event_to_me(to_me: bool = EventToMe()) -> bool:\n    return to_me\n"
  },
  {
    "path": "tests/plugins/param/param_exception.py",
    "content": "async def exc(e: Exception, x: ValueError | TypeError) -> Exception:\n    assert e == x\n    return e\n\n\nasync def legacy_exc(exception) -> Exception:\n    return exception\n"
  },
  {
    "path": "tests/plugins/param/param_matcher.py",
    "content": "from typing import Any, TypeVar\n\nfrom nonebot.adapters import Event\nfrom nonebot.matcher import Matcher\nfrom nonebot.params import (\n    LastReceived,\n    PausePromptResult,\n    Received,\n    ReceivePromptResult,\n)\n\n\nasync def matcher(m: Matcher) -> Matcher:\n    return m\n\n\nasync def postpone_matcher(m: \"Matcher\") -> Matcher:\n    return m\n\n\nasync def legacy_matcher(matcher):\n    return matcher\n\n\nasync def not_legacy_matcher(matcher: int): ...\n\n\nclass FooMatcher(Matcher): ...\n\n\nasync def sub_matcher(m: FooMatcher) -> FooMatcher:\n    return m\n\n\nclass BarMatcher(Matcher): ...\n\n\nasync def union_matcher(\n    m: FooMatcher | BarMatcher,\n) -> FooMatcher | BarMatcher:\n    return m\n\n\nM = TypeVar(\"M\", bound=Matcher)\n\n\nasync def generic_matcher(m: M) -> M:\n    return m\n\n\nCM = TypeVar(\"CM\", Matcher, None)\n\n\nasync def generic_matcher_none(m: CM) -> CM:\n    return m\n\n\nasync def not_matcher(m: int | Matcher): ...\n\n\nasync def receive(e: Event = Received(\"test\")) -> Event:\n    return e\n\n\nasync def last_receive(e: Event = LastReceived()) -> Event:\n    return e\n\n\nasync def receive_prompt_result(result: Any = ReceivePromptResult(\"test\")) -> Any:\n    return result\n\n\nasync def pause_prompt_result(result: Any = PausePromptResult()) -> Any:\n    return result\n"
  },
  {
    "path": "tests/plugins/param/param_state.py",
    "content": "from re import Match\n\nfrom nonebot.adapters import Message\nfrom nonebot.params import (\n    Command,\n    CommandArg,\n    CommandStart,\n    CommandWhitespace,\n    Endswith,\n    Fullmatch,\n    Keyword,\n    RawCommand,\n    RegexDict,\n    RegexGroup,\n    RegexMatched,\n    RegexStr,\n    ShellCommandArgs,\n    ShellCommandArgv,\n    Startswith,\n)\nfrom nonebot.typing import T_State\n\n\nasync def state(x: T_State) -> T_State:\n    return x\n\n\nasync def postpone_state(x: \"T_State\") -> T_State:\n    return x\n\n\nasync def legacy_state(state):\n    return state\n\n\nasync def not_legacy_state(state: int): ...\n\n\nasync def command(cmd: tuple[str, ...] = Command()) -> tuple[str, ...]:\n    return cmd\n\n\nasync def raw_command(raw_cmd: str = RawCommand()) -> str:\n    return raw_cmd\n\n\nasync def command_arg(cmd_arg: Message = CommandArg()) -> Message:\n    return cmd_arg\n\n\nasync def command_start(start: str = CommandStart()) -> str:\n    return start\n\n\nasync def command_whitespace(whitespace: str = CommandWhitespace()) -> str:\n    return whitespace\n\n\nasync def shell_command_args(\n    shell_command_args: dict = ShellCommandArgs(),\n) -> dict:\n    return shell_command_args\n\n\nasync def shell_command_argv(\n    shell_command_argv: list[str] = ShellCommandArgv(),\n) -> list[str]:\n    return shell_command_argv\n\n\nasync def regex_dict(regex_dict: dict = RegexDict()) -> dict:\n    return regex_dict\n\n\nasync def regex_group(regex_group: tuple = RegexGroup()) -> tuple:\n    return regex_group\n\n\nasync def regex_matched(regex_matched: Match[str] = RegexMatched()) -> Match[str]:\n    return regex_matched\n\n\nasync def regex_str(\n    entire: str = RegexStr(),\n    type_: str = RegexStr(\"type\"),\n    second: str = RegexStr(2),\n    groups: tuple[str, ...] = RegexStr(1, \"arg\"),\n) -> tuple[str, str, str, tuple[str, ...]]:\n    return entire, type_, second, groups\n\n\nasync def startswith(startswith: str = Startswith()) -> str:\n    return startswith\n\n\nasync def endswith(endswith: str = Endswith()) -> str:\n    return endswith\n\n\nasync def fullmatch(fullmatch: str = Fullmatch()) -> str:\n    return fullmatch\n\n\nasync def keyword(keyword: str = Keyword()) -> str:\n    return keyword\n"
  },
  {
    "path": "tests/plugins/param/priority.py",
    "content": "from nonebot.adapters import Bot, Event, Message\nfrom nonebot.matcher import Matcher\nfrom nonebot.params import Arg, Depends\nfrom nonebot.typing import T_State\n\n\ndef dependency():\n    return 1\n\n\nasync def complex_priority(\n    sub: int = Depends(dependency),\n    bot: Bot | None = None,\n    event: Event | None = None,\n    state: T_State = {},\n    matcher: Matcher | None = None,\n    arg: Message = Arg(),\n    exception: Exception | None = None,\n    default: int = 1,\n): ...\n"
  },
  {
    "path": "tests/plugins/plugin/__init__.py",
    "content": "from . import matchers as matchers\n"
  },
  {
    "path": "tests/plugins/plugin/matchers.py",
    "content": "from datetime import datetime, timezone\n\nfrom nonebot import (\n    CommandGroup,\n    MatcherGroup,\n    on,\n    on_command,\n    on_endswith,\n    on_fullmatch,\n    on_keyword,\n    on_message,\n    on_metaevent,\n    on_notice,\n    on_regex,\n    on_request,\n    on_shell_command,\n    on_startswith,\n    on_type,\n)\nfrom nonebot.adapters import Event\nfrom nonebot.matcher import Matcher\n\n\nasync def rule() -> bool:\n    return True\n\n\nasync def permission() -> bool:\n    return True\n\n\nasync def handler():\n    return\n\n\nexpire_time = datetime.now(timezone.utc)\npriority = 100\nstate = {\"test\": \"test\"}\n\n\nmatcher_on = on(\n    \"test\",\n    rule=rule,\n    permission=permission,\n    handlers=[handler],\n    temp=True,\n    expire_time=expire_time,\n    priority=priority,\n    block=True,\n    state=state,\n)\n\n\ndef matcher_on_factory() -> type[Matcher]:\n    return on(\n        \"test\",\n        rule=rule,\n        permission=permission,\n        handlers=[handler],\n        temp=True,\n        expire_time=expire_time,\n        priority=priority,\n        block=True,\n        state=state,\n    )\n\n\nmatcher_on_metaevent = on_metaevent(\n    rule=rule,\n    handlers=[handler],\n    temp=True,\n    expire_time=expire_time,\n    priority=priority,\n    block=True,\n    state=state,\n)\n\n\nmatcher_on_message = on_message(\n    rule=rule,\n    permission=permission,\n    handlers=[handler],\n    temp=True,\n    expire_time=expire_time,\n    priority=priority,\n    block=True,\n    state=state,\n)\n\n\nmatcher_on_notice = on_notice(\n    rule=rule,\n    handlers=[handler],\n    temp=True,\n    expire_time=expire_time,\n    priority=priority,\n    block=True,\n    state=state,\n)\n\n\nmatcher_on_request = on_request(\n    rule=rule,\n    handlers=[handler],\n    temp=True,\n    expire_time=expire_time,\n    priority=priority,\n    block=True,\n    state=state,\n)\n\n\nmatcher_on_startswith = on_startswith(\n    \"test\",\n    rule=rule,\n    permission=permission,\n    handlers=[handler],\n    temp=True,\n    expire_time=expire_time,\n    priority=priority,\n    block=True,\n    state=state,\n)\n\n\nmatcher_on_endswith = on_endswith(\n    \"test\",\n    rule=rule,\n    permission=permission,\n    handlers=[handler],\n    temp=True,\n    expire_time=expire_time,\n    priority=priority,\n    block=True,\n    state=state,\n)\n\n\nmatcher_on_fullmatch = on_fullmatch(\n    \"test\",\n    rule=rule,\n    permission=permission,\n    handlers=[handler],\n    temp=True,\n    expire_time=expire_time,\n    priority=priority,\n    block=True,\n    state=state,\n)\n\n\nmatcher_on_keyword = on_keyword(\n    {\"test\"},\n    rule=rule,\n    permission=permission,\n    handlers=[handler],\n    temp=True,\n    expire_time=expire_time,\n    priority=priority,\n    block=True,\n    state=state,\n)\n\n\nmatcher_on_command = on_command(\n    \"test\",\n    rule=rule,\n    permission=permission,\n    handlers=[handler],\n    temp=True,\n    expire_time=expire_time,\n    priority=priority,\n    block=True,\n    state=state,\n)\n\n\nmatcher_on_shell_command = on_shell_command(\n    \"test\",\n    rule=rule,\n    permission=permission,\n    handlers=[handler],\n    temp=True,\n    expire_time=expire_time,\n    priority=priority,\n    block=True,\n    state=state,\n)\n\n\nmatcher_on_regex = on_regex(\n    \"test\",\n    rule=rule,\n    permission=permission,\n    handlers=[handler],\n    temp=True,\n    expire_time=expire_time,\n    priority=priority,\n    block=True,\n    state=state,\n)\n\n\nclass TestEvent(Event): ...\n\n\nmatcher_on_type = on_type(\n    TestEvent,\n    rule=rule,\n    permission=permission,\n    handlers=[handler],\n    temp=True,\n    expire_time=expire_time,\n    priority=priority,\n    block=True,\n    state=state,\n)\n\n\ncmd_group = CommandGroup(\n    \"prefix\",\n    rule=rule,\n    permission=permission,\n    handlers=[handler],\n    temp=True,\n    expire_time=expire_time,\n    priority=priority,\n    block=True,\n    state=state,\n)\nmatcher_prefix_cmd = cmd_group.command(\"sub\", aliases={\"help\", (\"help\", \"foo\")})\nmatcher_prefix_shell_cmd = cmd_group.shell_command(\n    \"sub\", aliases={\"help\", (\"help\", \"foo\")}\n)\n\n\ncmd_group_prefix_aliases = CommandGroup(\n    \"prefix\",\n    prefix_aliases=True,\n    rule=rule,\n    permission=permission,\n    handlers=[handler],\n    temp=True,\n    expire_time=expire_time,\n    priority=priority,\n    block=True,\n    state=state,\n)\nmatcher_prefix_aliases_cmd = cmd_group_prefix_aliases.command(\n    \"sub\", aliases={\"help\", (\"help\", \"foo\")}\n)\nmatcher_prefix_aliases_shell_cmd = cmd_group_prefix_aliases.shell_command(\n    \"sub\", aliases={\"help\", (\"help\", \"foo\")}\n)\n\n\nmatcher_group = MatcherGroup(\n    rule=rule,\n    permission=permission,\n    handlers=[handler],\n    temp=True,\n    expire_time=expire_time,\n    priority=priority,\n    block=True,\n    state=state,\n)\nmatcher_group_on = matcher_group.on(type=\"test\")\nmatcher_group_on_metaevent = matcher_group.on_metaevent()\nmatcher_group_on_message = matcher_group.on_message()\nmatcher_group_on_notice = matcher_group.on_notice()\nmatcher_group_on_request = matcher_group.on_request()\nmatcher_group_on_startswith = matcher_group.on_startswith(\"test\")\nmatcher_group_on_endswith = matcher_group.on_endswith(\"test\")\nmatcher_group_on_fullmatch = matcher_group.on_fullmatch(\"test\")\nmatcher_group_on_keyword = matcher_group.on_keyword({\"test\"})\nmatcher_group_on_command = matcher_group.on_command(\"test\")\nmatcher_group_on_shell_command = matcher_group.on_shell_command(\"test\")\nmatcher_group_on_regex = matcher_group.on_regex(\"test\")\nmatcher_group_on_type = matcher_group.on_type(TestEvent)\n"
  },
  {
    "path": "tests/plugins/require.py",
    "content": "from nonebot import require\n\ntest_require = require(\"export\").test\n\nfrom plugins.export import test\n\nassert test is test_require, \"Export Require Error\"\nassert test() == \"export\", \"Export Require Error\"\n"
  },
  {
    "path": "tests/plugins.empty.toml",
    "content": ""
  },
  {
    "path": "tests/plugins.invalid.json",
    "content": "[]\n"
  },
  {
    "path": "tests/plugins.invalid.toml",
    "content": "[tool]\nnonebot = []\n"
  },
  {
    "path": "tests/plugins.json",
    "content": "{\n  \"plugins\": [],\n  \"plugin_dirs\": []\n}\n"
  },
  {
    "path": "tests/plugins.legacy.toml",
    "content": "[tool.nonebot]\nplugins = []\nplugin_dirs = []\n"
  },
  {
    "path": "tests/plugins.toml",
    "content": "[tool.nonebot]\nplugin_dirs = []\n\n[tool.nonebot.plugins]\n\"@local\" = []\n"
  },
  {
    "path": "tests/pyproject.toml",
    "content": "[tool.ruff]\nextend = \"../pyproject.toml\"\n\n[tool.ruff.lint.isort]\nknown-first-party = [\"nonebot\", \"fake_server\", \"utils\"]\n"
  },
  {
    "path": "tests/python_3_12/plugins/aliased_param/__init__.py",
    "content": "from pathlib import Path\n\nfrom nonebot import load_plugins\n\n_sub_plugins = set()\n\n_sub_plugins |= load_plugins(str(Path(__file__).parent))\n"
  },
  {
    "path": "tests/python_3_12/plugins/aliased_param/param_arg.py",
    "content": "from typing import Annotated\n\nfrom nonebot.adapters import Message\nfrom nonebot.params import Arg\n\ntype AliasedArg = Annotated[Message, Arg()]\n\n\nasync def aliased_arg(key: AliasedArg) -> Message:\n    return key\n"
  },
  {
    "path": "tests/python_3_12/plugins/aliased_param/param_bot.py",
    "content": "from nonebot.adapters import Bot\n\ntype AliasedBot = Bot\n\n\nasync def get_aliased_bot(b: AliasedBot) -> Bot:\n    return b\n"
  },
  {
    "path": "tests/python_3_12/plugins/aliased_param/param_depend.py",
    "content": "from typing import Annotated\n\nfrom nonebot import on_message\nfrom nonebot.params import Depends\n\ntest_depends = on_message()\n\nrunned = []\n\n\ndef dependency():\n    runned.append(1)\n    return 1\n\n\ntype AliasedDepends = Annotated[int, Depends(dependency)]\n\n\n@test_depends.handle()\nasync def aliased_depends(x: AliasedDepends):\n    return x\n"
  },
  {
    "path": "tests/python_3_12/plugins/aliased_param/param_event.py",
    "content": "from nonebot.adapters import Event\n\ntype AliasedEvent = Event\n\n\nasync def aliased_event(e: AliasedEvent) -> Event:\n    return e\n"
  },
  {
    "path": "tests/python_3_12/plugins/aliased_param/param_exception.py",
    "content": "type AliasedException = Exception\n\n\nasync def aliased_exc(e: AliasedException) -> Exception:\n    return e\n"
  },
  {
    "path": "tests/python_3_12/plugins/aliased_param/param_matcher.py",
    "content": "from nonebot.matcher import Matcher\n\ntype AliasedMatcher = Matcher\n\n\nasync def aliased_matcher(m: AliasedMatcher) -> Matcher:\n    return m\n"
  },
  {
    "path": "tests/python_3_12/plugins/aliased_param/param_state.py",
    "content": "from nonebot.typing import T_State\n\ntype AliasedState = T_State\n\n\nasync def aliased_state(x: AliasedState) -> T_State:\n    return x\n"
  },
  {
    "path": "tests/python_3_12/pyproject.toml",
    "content": "[tool.ruff]\nextend = \"../pyproject.toml\"\ntarget-version = \"py312\"\n"
  },
  {
    "path": "tests/test_adapters/test_adapter.py",
    "content": "from contextlib import asynccontextmanager\n\nfrom nonebug import App\nimport pytest\n\nfrom nonebot.adapters import Bot\nfrom nonebot.drivers import (\n    URL,\n    Driver,\n    HTTPServerSetup,\n    Request,\n    Response,\n    WebSocket,\n    WebSocketServerSetup,\n)\nfrom utils import FakeAdapter\n\n\n@pytest.mark.anyio\nasync def test_adapter_connect(app: App, driver: Driver):\n    last_connect_bot: Bot | None = None\n    last_disconnect_bot: Bot | None = None\n\n    def _fake_bot_connect(bot: Bot):\n        nonlocal last_connect_bot\n        last_connect_bot = bot\n\n    def _fake_bot_disconnect(bot: Bot):\n        nonlocal last_disconnect_bot\n        last_disconnect_bot = bot\n\n    with pytest.MonkeyPatch.context() as m:\n        m.setattr(driver, \"_bot_connect\", _fake_bot_connect)\n        m.setattr(driver, \"_bot_disconnect\", _fake_bot_disconnect)\n\n        adapter = FakeAdapter(driver)\n\n        async with app.test_api() as ctx:\n            bot = ctx.create_bot(adapter=adapter)\n            assert last_connect_bot is bot\n            assert adapter.bots[bot.self_id] is bot\n\n        assert last_disconnect_bot is bot\n        assert bot.self_id not in adapter.bots\n\n\n@pytest.mark.parametrize(\n    \"driver\",\n    [\n        pytest.param(\"nonebot.drivers.fastapi:Driver\", id=\"fastapi\"),\n        pytest.param(\"nonebot.drivers.quart:Driver\", id=\"quart\"),\n        pytest.param(\n            \"nonebot.drivers.httpx:Driver\",\n            id=\"httpx\",\n            marks=pytest.mark.xfail(\n                reason=\"not a server\", raises=TypeError, strict=True\n            ),\n        ),\n        pytest.param(\n            \"nonebot.drivers.websockets:Driver\",\n            id=\"websockets\",\n            marks=pytest.mark.xfail(\n                reason=\"not a server\", raises=TypeError, strict=True\n            ),\n        ),\n        pytest.param(\n            \"nonebot.drivers.aiohttp:Driver\",\n            id=\"aiohttp\",\n            marks=pytest.mark.xfail(\n                reason=\"not a server\", raises=TypeError, strict=True\n            ),\n        ),\n    ],\n    indirect=True,\n)\ndef test_adapter_server(driver: Driver):\n    last_http_setup: HTTPServerSetup | None = None\n    last_ws_setup: WebSocketServerSetup | None = None\n\n    def _fake_setup_http_server(setup: HTTPServerSetup):\n        nonlocal last_http_setup\n        last_http_setup = setup\n\n    def _fake_setup_websocket_server(setup: WebSocketServerSetup):\n        nonlocal last_ws_setup\n        last_ws_setup = setup\n\n    with pytest.MonkeyPatch.context() as m:\n        m.setattr(driver, \"setup_http_server\", _fake_setup_http_server, raising=False)\n        m.setattr(\n            driver,\n            \"setup_websocket_server\",\n            _fake_setup_websocket_server,\n            raising=False,\n        )\n\n        async def handle_http(request: Request):\n            return Response(200, content=\"test\")\n\n        async def handle_ws(ws: WebSocket): ...\n\n        adapter = FakeAdapter(driver)\n\n        setup = HTTPServerSetup(URL(\"/test\"), \"GET\", \"test\", handle_http)\n        adapter.setup_http_server(setup)\n        assert last_http_setup is setup\n\n        setup = WebSocketServerSetup(URL(\"/test\"), \"test\", handle_ws)\n        adapter.setup_websocket_server(setup)\n        assert last_ws_setup is setup\n\n\n@pytest.mark.anyio\n@pytest.mark.parametrize(\n    \"driver\",\n    [\n        pytest.param(\n            \"nonebot.drivers.fastapi:Driver\",\n            id=\"fastapi\",\n            marks=pytest.mark.xfail(\n                reason=\"not a http client\", raises=TypeError, strict=True\n            ),\n        ),\n        pytest.param(\n            \"nonebot.drivers.quart:Driver\",\n            id=\"quart\",\n            marks=pytest.mark.xfail(\n                reason=\"not a http client\", raises=TypeError, strict=True\n            ),\n        ),\n        pytest.param(\"nonebot.drivers.httpx:Driver\", id=\"httpx\"),\n        pytest.param(\n            \"nonebot.drivers.websockets:Driver\",\n            id=\"websockets\",\n            marks=pytest.mark.xfail(\n                reason=\"not a http client\", raises=TypeError, strict=True\n            ),\n        ),\n        pytest.param(\"nonebot.drivers.aiohttp:Driver\", id=\"aiohttp\"),\n    ],\n    indirect=True,\n)\nasync def test_adapter_http_client(driver: Driver):\n    last_request: Request | None = None\n\n    async def _fake_request(request: Request):\n        nonlocal last_request\n        last_request = request\n\n    with pytest.MonkeyPatch.context() as m:\n        m.setattr(driver, \"request\", _fake_request, raising=False)\n\n        adapter = FakeAdapter(driver)\n\n        request = Request(\"GET\", URL(\"/test\"))\n        await adapter.request(request)\n        assert last_request is request\n\n\n@pytest.mark.anyio\n@pytest.mark.parametrize(\n    \"driver\",\n    [\n        pytest.param(\n            \"nonebot.drivers.fastapi:Driver\",\n            id=\"fastapi\",\n            marks=pytest.mark.xfail(\n                reason=\"not a websocket client\", raises=TypeError, strict=True\n            ),\n        ),\n        pytest.param(\n            \"nonebot.drivers.quart:Driver\",\n            id=\"quart\",\n            marks=pytest.mark.xfail(\n                reason=\"not a websocket client\", raises=TypeError, strict=True\n            ),\n        ),\n        pytest.param(\n            \"nonebot.drivers.httpx:Driver\",\n            id=\"httpx\",\n            marks=pytest.mark.xfail(\n                reason=\"not a websocket client\", raises=TypeError, strict=True\n            ),\n        ),\n        pytest.param(\"nonebot.drivers.websockets:Driver\", id=\"websockets\"),\n        pytest.param(\"nonebot.drivers.aiohttp:Driver\", id=\"aiohttp\"),\n    ],\n    indirect=True,\n)\nasync def test_adapter_websocket_client(driver: Driver):\n    _fake_ws = object()\n    _last_request: Request | None = None\n\n    @asynccontextmanager\n    async def _fake_websocket(setup: Request):\n        nonlocal _last_request\n        _last_request = setup\n        yield _fake_ws\n\n    with pytest.MonkeyPatch.context() as m:\n        m.setattr(driver, \"websocket\", _fake_websocket, raising=False)\n\n        adapter = FakeAdapter(driver)\n\n        request = Request(\"GET\", URL(\"/test\"))\n        async with adapter.websocket(request) as ws:\n            assert _last_request is request\n            assert ws is _fake_ws\n"
  },
  {
    "path": "tests/test_adapters/test_bot.py",
    "content": "from typing import Any\n\nimport anyio\nfrom nonebug import App\nimport pytest\n\nfrom nonebot.adapters import Bot\nfrom nonebot.exception import MockApiException\n\n\n@pytest.mark.anyio\nasync def test_bot_call_api(app: App):\n    async with app.test_api() as ctx:\n        bot = ctx.create_bot()\n        ctx.should_call_api(\"test\", {}, True)\n        result = await bot.call_api(\"test\")\n\n    assert result is True\n\n    async with app.test_api() as ctx:\n        bot = ctx.create_bot()\n        ctx.should_call_api(\"test\", {}, exception=RuntimeError(\"test\"))\n        with pytest.raises(RuntimeError, match=\"test\"):\n            await bot.call_api(\"test\")\n\n\n@pytest.mark.anyio\nasync def test_bot_calling_api_hook_simple(app: App):\n    runned: bool = False\n\n    async def calling_api_hook(bot: Bot, api: str, data: dict[str, Any]):\n        nonlocal runned\n        runned = True\n\n    hooks = set()\n\n    with pytest.MonkeyPatch.context() as m:\n        m.setattr(Bot, \"_calling_api_hook\", hooks)\n\n        Bot.on_calling_api(calling_api_hook)\n\n        assert hooks == {calling_api_hook}\n\n        async with app.test_api() as ctx:\n            bot = ctx.create_bot()\n            ctx.should_call_api(\"test\", {}, True)\n            result = await bot.call_api(\"test\")\n\n        assert runned is True\n        assert result is True\n\n\n@pytest.mark.anyio\nasync def test_bot_calling_api_hook_mock(app: App):\n    runned: bool = False\n\n    async def calling_api_hook(bot: Bot, api: str, data: dict[str, Any]):\n        nonlocal runned\n        runned = True\n\n        raise MockApiException(False)\n\n    hooks = set()\n\n    with pytest.MonkeyPatch.context() as m:\n        m.setattr(Bot, \"_calling_api_hook\", hooks)\n\n        Bot.on_calling_api(calling_api_hook)\n\n        assert hooks == {calling_api_hook}\n\n        async with app.test_api() as ctx:\n            bot = ctx.create_bot()\n            result = await bot.call_api(\"test\")\n\n        assert runned is True\n        assert result is False\n\n\n@pytest.mark.anyio\nasync def test_bot_calling_api_hook_multi_mock(app: App):\n    runned1: bool = False\n    runned2: bool = False\n    event = anyio.Event()\n\n    async def calling_api_hook1(bot: Bot, api: str, data: dict[str, Any]):\n        nonlocal runned1\n        runned1 = True\n        event.set()\n\n        raise MockApiException(1)\n\n    async def calling_api_hook2(bot: Bot, api: str, data: dict[str, Any]):\n        nonlocal runned2\n        runned2 = True\n        with anyio.fail_after(1):\n            await event.wait()\n\n        raise MockApiException(2)\n\n    hooks = set()\n\n    with pytest.MonkeyPatch.context() as m:\n        m.setattr(Bot, \"_calling_api_hook\", hooks)\n\n        Bot.on_calling_api(calling_api_hook1)\n        Bot.on_calling_api(calling_api_hook2)\n\n        assert hooks == {calling_api_hook1, calling_api_hook2}\n\n        async with app.test_api() as ctx:\n            bot = ctx.create_bot()\n            result = await bot.call_api(\"test\")\n\n        assert runned1 is True\n        assert runned2 is True\n        assert result == 1\n\n\n@pytest.mark.anyio\nasync def test_bot_called_api_hook_simple(app: App):\n    runned: bool = False\n\n    async def called_api_hook(\n        bot: Bot,\n        exception: Exception | None,\n        api: str,\n        data: dict[str, Any],\n        result: Any,\n    ):\n        nonlocal runned\n        runned = True\n\n    hooks = set()\n\n    with pytest.MonkeyPatch.context() as m:\n        m.setattr(Bot, \"_called_api_hook\", hooks)\n\n        Bot.on_called_api(called_api_hook)\n\n        assert hooks == {called_api_hook}\n\n        async with app.test_api() as ctx:\n            bot = ctx.create_bot()\n            ctx.should_call_api(\"test\", {}, True)\n            result = await bot.call_api(\"test\")\n\n        assert runned is True\n        assert result is True\n\n\n@pytest.mark.anyio\nasync def test_bot_called_api_hook_mock(app: App):\n    runned: bool = False\n\n    async def called_api_hook(\n        bot: Bot,\n        exception: Exception | None,\n        api: str,\n        data: dict[str, Any],\n        result: Any,\n    ):\n        nonlocal runned\n        runned = True\n\n        raise MockApiException(False)\n\n    hooks = set()\n\n    with pytest.MonkeyPatch.context() as m:\n        m.setattr(Bot, \"_called_api_hook\", hooks)\n\n        Bot.on_called_api(called_api_hook)\n\n        assert hooks == {called_api_hook}\n\n        async with app.test_api() as ctx:\n            bot = ctx.create_bot()\n            ctx.should_call_api(\"test\", {}, True)\n            result = await bot.call_api(\"test\")\n\n        assert runned is True\n        assert result is False\n\n        runned = False\n\n        async with app.test_api() as ctx:\n            bot = ctx.create_bot()\n            ctx.should_call_api(\"test\", {}, exception=RuntimeError(\"test\"))\n            result = await bot.call_api(\"test\")\n\n        assert runned is True\n        assert result is False\n\n\n@pytest.mark.anyio\nasync def test_bot_called_api_hook_multi_mock(app: App):\n    runned1: bool = False\n    runned2: bool = False\n    event = anyio.Event()\n\n    async def called_api_hook1(\n        bot: Bot,\n        exception: Exception | None,\n        api: str,\n        data: dict[str, Any],\n        result: Any,\n    ):\n        nonlocal runned1\n        runned1 = True\n        event.set()\n\n        raise MockApiException(1)\n\n    async def called_api_hook2(\n        bot: Bot,\n        exception: Exception | None,\n        api: str,\n        data: dict[str, Any],\n        result: Any,\n    ):\n        nonlocal runned2\n        runned2 = True\n        with anyio.fail_after(1):\n            await event.wait()\n\n        raise MockApiException(2)\n\n    hooks = set()\n\n    with pytest.MonkeyPatch.context() as m:\n        m.setattr(Bot, \"_called_api_hook\", hooks)\n\n        Bot.on_called_api(called_api_hook1)\n        Bot.on_called_api(called_api_hook2)\n\n        assert hooks == {called_api_hook1, called_api_hook2}\n\n        async with app.test_api() as ctx:\n            bot = ctx.create_bot()\n            ctx.should_call_api(\"test\", {}, True)\n            result = await bot.call_api(\"test\")\n\n        assert runned1 is True\n        assert runned2 is True\n        assert result == 1\n"
  },
  {
    "path": "tests/test_adapters/test_message.py",
    "content": "from pydantic import ValidationError\nimport pytest\n\nfrom nonebot.adapters import Message, MessageSegment\nfrom nonebot.compat import type_validate_python\nfrom utils import FakeMessage, FakeMessageSegment\n\n\ndef test_segment_data():\n    assert len(FakeMessageSegment.text(\"text\")) == 4\n    assert FakeMessageSegment.text(\"text\").get(\"data\") == {\"text\": \"text\"}\n    assert list(FakeMessageSegment.text(\"text\").keys()) == [\"type\", \"data\"]\n    assert list(FakeMessageSegment.text(\"text\").values()) == [\"text\", {\"text\": \"text\"}]\n    assert list(FakeMessageSegment.text(\"text\").items()) == [\n        (\"type\", \"text\"),\n        (\"data\", {\"text\": \"text\"}),\n    ]\n\n\ndef test_segment_equal():\n    assert FakeMessageSegment(\"text\", {\"text\": \"text\"}) == FakeMessageSegment(\n        \"text\", {\"text\": \"text\"}\n    )\n    assert FakeMessageSegment(\"text\", {\"text\": \"text\"}) != FakeMessageSegment(\n        \"text\", {\"text\": \"other\"}\n    )\n    assert FakeMessageSegment(\"text\", {\"text\": \"text\"}) != FakeMessageSegment(\n        \"other\", {\"text\": \"text\"}\n    )\n\n\ndef test_segment_add():\n    assert FakeMessageSegment.text(\"text\") + FakeMessageSegment.text(\n        \"text\"\n    ) == FakeMessage([FakeMessageSegment.text(\"text\"), FakeMessageSegment.text(\"text\")])\n\n    assert FakeMessageSegment.text(\"text\") + \"text\" == FakeMessage(\n        [FakeMessageSegment.text(\"text\"), FakeMessageSegment.text(\"text\")]\n    )\n\n    assert (\n        FakeMessageSegment.text(\"text\") + FakeMessage([FakeMessageSegment.text(\"text\")])\n    ) == FakeMessage([FakeMessageSegment.text(\"text\"), FakeMessageSegment.text(\"text\")])\n\n    assert \"text\" + FakeMessageSegment.text(\"text\") == FakeMessage(\n        [FakeMessageSegment.text(\"text\"), FakeMessageSegment.text(\"text\")]\n    )\n\n\ndef test_segment_validate():\n    assert type_validate_python(\n        FakeMessageSegment,\n        {\"type\": \"text\", \"data\": {\"text\": \"text\"}, \"extra\": \"should be ignored\"},\n    ) == FakeMessageSegment.text(\"text\")\n    with pytest.raises(ValidationError):\n        type_validate_python(\n            type(\"FakeMessageSegment2\", (MessageSegment,), {}),\n            FakeMessageSegment.text(\"text\"),\n        )\n\n    with pytest.raises(ValidationError):\n        type_validate_python(FakeMessageSegment, \"some str\")\n\n    with pytest.raises(ValidationError):\n        type_validate_python(FakeMessageSegment, {\"data\": {}})\n\n\ndef test_segment_join():\n    seg = FakeMessageSegment.text(\"test\")\n    iterable = [\n        FakeMessageSegment.text(\"first\"),\n        FakeMessage(\n            [FakeMessageSegment.text(\"second\"), FakeMessageSegment.text(\"third\")]\n        ),\n    ]\n\n    assert seg.join(iterable) == FakeMessage(\n        [\n            FakeMessageSegment.text(\"first\"),\n            FakeMessageSegment.text(\"test\"),\n            FakeMessageSegment.text(\"second\"),\n            FakeMessageSegment.text(\"third\"),\n        ]\n    )\n\n\ndef test_segment_copy():\n    origin = FakeMessageSegment.text(\"text\")\n    copy = origin.copy()\n    assert origin is not copy\n    assert origin == copy\n\n\ndef test_message_add():\n    assert (\n        FakeMessage([FakeMessageSegment.text(\"text\")]) + FakeMessageSegment.text(\"text\")\n    ) == FakeMessage([FakeMessageSegment.text(\"text\"), FakeMessageSegment.text(\"text\")])\n\n    assert FakeMessage([FakeMessageSegment.text(\"text\")]) + \"text\" == FakeMessage(\n        [FakeMessageSegment.text(\"text\"), FakeMessageSegment.text(\"text\")]\n    )\n\n    assert (\n        FakeMessage([FakeMessageSegment.text(\"text\")])\n        + FakeMessage([FakeMessageSegment.text(\"text\")])\n    ) == FakeMessage([FakeMessageSegment.text(\"text\"), FakeMessageSegment.text(\"text\")])\n\n    assert \"text\" + FakeMessage([FakeMessageSegment.text(\"text\")]) == FakeMessage(\n        [FakeMessageSegment.text(\"text\"), FakeMessageSegment.text(\"text\")]\n    )\n\n    msg = FakeMessage([FakeMessageSegment.text(\"text\")])\n    msg += FakeMessageSegment.text(\"text\")\n    assert msg == FakeMessage(\n        [FakeMessageSegment.text(\"text\"), FakeMessageSegment.text(\"text\")]\n    )\n\n\ndef test_message_getitem():\n    message = FakeMessage(\n        [\n            FakeMessageSegment.text(\"test\"),\n            FakeMessageSegment.image(\"test2\"),\n            FakeMessageSegment.image(\"test3\"),\n            FakeMessageSegment.text(\"test4\"),\n        ]\n    )\n\n    assert message[0] == FakeMessageSegment.text(\"test\")\n\n    assert message[:2] == FakeMessage(\n        [FakeMessageSegment.text(\"test\"), FakeMessageSegment.image(\"test2\")]\n    )\n\n    assert message[\"image\"] == FakeMessage(\n        [FakeMessageSegment.image(\"test2\"), FakeMessageSegment.image(\"test3\")]\n    )\n\n    assert message[\"image\", 0] == FakeMessageSegment.image(\"test2\")\n    assert message[\"image\", 0:2] == message[\"image\"]\n\n    assert message.index(message[0]) == 0\n    assert message.index(\"image\") == 1\n\n    assert message.get(\"image\") == message[\"image\"]\n    assert message.get(\"image\", 114514) == message[\"image\"]\n    assert message.get(\"image\", 1) == FakeMessage([message[\"image\", 0]])\n\n    assert message.count(\"image\") == 2\n\n\ndef test_message_validate():\n    assert type_validate_python(FakeMessage, FakeMessage([])) == FakeMessage([])\n\n    with pytest.raises(ValidationError):\n        type_validate_python(type(\"FakeMessage2\", (Message,), {}), FakeMessage([]))\n\n    assert type_validate_python(FakeMessage, \"text\") == FakeMessage(\n        [FakeMessageSegment.text(\"text\")]\n    )\n\n    assert type_validate_python(\n        FakeMessage, {\"type\": \"text\", \"data\": {\"text\": \"text\"}}\n    ) == FakeMessage([FakeMessageSegment.text(\"text\")])\n\n    assert type_validate_python(\n        FakeMessage,\n        [FakeMessageSegment.text(\"text\"), {\"type\": \"text\", \"data\": {\"text\": \"text\"}}],\n    ) == FakeMessage([FakeMessageSegment.text(\"text\"), FakeMessageSegment.text(\"text\")])\n\n    with pytest.raises(ValidationError):\n        type_validate_python(FakeMessage, object())\n\n\ndef test_message_contains():\n    message = FakeMessage(\n        [\n            FakeMessageSegment.text(\"test\"),\n            FakeMessageSegment.image(\"test2\"),\n            FakeMessageSegment.image(\"test3\"),\n            FakeMessageSegment.text(\"test4\"),\n        ]\n    )\n\n    assert message.has(FakeMessageSegment.text(\"test\")) is True\n    assert FakeMessageSegment.text(\"test\") in message\n    assert message.has(\"image\") is True\n    assert \"image\" in message\n\n    assert message.has(FakeMessageSegment.text(\"foo\")) is False\n    assert FakeMessageSegment.text(\"foo\") not in message\n    assert message.has(\"foo\") is False\n    assert \"foo\" not in message\n\n    assert not bool(FakeMessageSegment.text(\"\"))\n    msg_with_empty_seg = FakeMessage([FakeMessageSegment.text(\"\")])\n    assert msg_with_empty_seg.has(\"text\") is True\n    assert \"text\" in msg_with_empty_seg\n\n\ndef test_message_only():\n    message = FakeMessage(\n        [\n            FakeMessageSegment.text(\"test\"),\n            FakeMessageSegment.text(\"test2\"),\n        ]\n    )\n\n    assert message.only(\"text\") is True\n    assert message.only(FakeMessageSegment.text(\"test\")) is False\n\n    message = FakeMessage(\n        [\n            FakeMessageSegment.text(\"test\"),\n            FakeMessageSegment.image(\"test2\"),\n            FakeMessageSegment.image(\"test3\"),\n            FakeMessageSegment.text(\"test4\"),\n        ]\n    )\n\n    assert message.only(\"text\") is False\n\n    message = FakeMessage(\n        [\n            FakeMessageSegment.text(\"test\"),\n            FakeMessageSegment.text(\"test\"),\n        ]\n    )\n\n    assert message.only(FakeMessageSegment.text(\"test\")) is True\n\n\ndef test_message_join():\n    msg = FakeMessage([FakeMessageSegment.text(\"test\")])\n    iterable = [\n        FakeMessageSegment.text(\"first\"),\n        FakeMessage(\n            [FakeMessageSegment.text(\"second\"), FakeMessageSegment.text(\"third\")]\n        ),\n    ]\n\n    assert msg.join(iterable) == FakeMessage(\n        [\n            FakeMessageSegment.text(\"first\"),\n            FakeMessageSegment.text(\"test\"),\n            FakeMessageSegment.text(\"second\"),\n            FakeMessageSegment.text(\"third\"),\n        ]\n    )\n\n\ndef test_message_include():\n    message = FakeMessage(\n        [\n            FakeMessageSegment.text(\"test\"),\n            FakeMessageSegment.image(\"test2\"),\n            FakeMessageSegment.image(\"test3\"),\n            FakeMessageSegment.text(\"test4\"),\n        ]\n    )\n\n    assert message.include(\"text\") == FakeMessage(\n        [\n            FakeMessageSegment.text(\"test\"),\n            FakeMessageSegment.text(\"test4\"),\n        ]\n    )\n\n\ndef test_message_exclude():\n    message = FakeMessage(\n        [\n            FakeMessageSegment.text(\"test\"),\n            FakeMessageSegment.image(\"test2\"),\n            FakeMessageSegment.image(\"test3\"),\n            FakeMessageSegment.text(\"test4\"),\n        ]\n    )\n\n    assert message.exclude(\"image\") == FakeMessage(\n        [\n            FakeMessageSegment.text(\"test\"),\n            FakeMessageSegment.text(\"test4\"),\n        ]\n    )\n"
  },
  {
    "path": "tests/test_adapters/test_template.py",
    "content": "import pytest\n\nfrom nonebot.adapters import MessageTemplate\nfrom utils import FakeMessage, FakeMessageSegment, escape_text\n\n\ndef test_template_basis():\n    template = MessageTemplate(\"{key:.3%}\")\n    formatted = template.format(key=0.123456789)\n    assert formatted == \"12.346%\"\n\n\ndef test_template_message():\n    template = FakeMessage.template(\"{a:custom}{b:text}{c:image}/{d}\")\n\n    @template.add_format_spec\n    def custom(input: str) -> str:\n        return f\"{input}-custom!\"\n\n    with pytest.raises(ValueError, match=\"already exists\"):\n        template.add_format_spec(custom)\n\n    format_args = {\n        \"a\": \"custom\",\n        \"b\": \"text\",\n        \"c\": \"https://example.com/test\",\n        \"d\": 114,\n    }\n    formatted = template.format(**format_args)\n\n    assert template.format_map(format_args) == formatted\n    assert formatted.extract_plain_text() == \"custom-custom!text/114\"\n    assert str(formatted) == \"custom-custom!text[fake:image]/114\"\n\n\ndef test_rich_template_message():\n    pic1, pic2, pic3 = (\n        FakeMessageSegment.image(\"file:///pic1.jpg\"),\n        FakeMessageSegment.image(\"file:///pic2.jpg\"),\n        FakeMessageSegment.image(\"file:///pic3.jpg\"),\n    )\n\n    template = FakeMessage.template(\"{}{}\" + pic2 + \"{}\")\n\n    result = template.format(pic1, \"[fake:image]\", pic3)\n\n    assert result[\"image\"] == FakeMessage([pic1, pic2, pic3])\n    assert str(result) == (\n        \"[fake:image]\" + escape_text(\"[fake:image]\") + \"[fake:image]\" + \"[fake:image]\"\n    )\n\n\ndef test_message_injection():\n    template = FakeMessage.template(\"{name}Is Bad\")\n    message = template.format(name=\"[fake:image]\")\n\n    assert message.extract_plain_text() == escape_text(\"[fake:image]Is Bad\")\n\n\ndef test_malformed_template():\n    positive_template = FakeMessage.template(\"{a}{b}\")\n    message = positive_template.format(a=\"a\", b=\"b\")\n    assert message.extract_plain_text() == \"ab\"\n\n    malformed_template = FakeMessage.template(\"{a.__init__}\")\n    with pytest.raises(ValueError, match=\"private attribute\"):\n        message = malformed_template.format(a=\"a\")\n\n    malformed_template = FakeMessage.template(\"{a[__builtins__]}\")\n    with pytest.raises(ValueError, match=\"private attribute\"):\n        message = malformed_template.format(a=globals())\n\n    malformed_template = MessageTemplate(\n        \"{a[__builtins__][__import__]}{b.__init__}\", private_getattr=True\n    )\n    message = malformed_template.format(a=globals(), b=\"b\")\n"
  },
  {
    "path": "tests/test_broadcast.py",
    "content": "import sys\n\nfrom nonebug import App\nimport pytest\n\nfrom nonebot import on_message\nfrom nonebot.adapters import Bot, Event\nfrom nonebot.exception import IgnoredException\nfrom nonebot.log import default_filter, default_format, logger\nfrom nonebot.matcher import Matcher\nimport nonebot.message as message\nfrom nonebot.message import (\n    event_postprocessor,\n    event_preprocessor,\n    run_postprocessor,\n    run_preprocessor,\n)\nfrom nonebot.params import Depends\nfrom nonebot.typing import T_State\nfrom utils import make_fake_event\n\n\nasync def _dependency() -> int:\n    return 1\n\n\n@pytest.mark.anyio\nasync def test_event_preprocessor(app: App, monkeypatch: pytest.MonkeyPatch):\n    with monkeypatch.context() as m:\n        m.setattr(message, \"_event_preprocessors\", set())\n\n        runned = False\n\n        @event_preprocessor\n        async def test_preprocessor(\n            bot: Bot,\n            event: Event,\n            state: T_State,\n            sub: int = Depends(_dependency),\n            default: int = 1,\n        ):\n            nonlocal runned\n            runned = True\n\n        assert test_preprocessor in {\n            dependent.call for dependent in message._event_preprocessors\n        }\n\n        with app.provider.context({}):\n            matcher = on_message()\n\n            async with app.test_matcher(matcher) as ctx:\n                bot = ctx.create_bot()\n                event = make_fake_event()()\n                ctx.receive_event(bot, event)\n\n        assert runned, \"event_preprocessor should runned\"\n\n\n@pytest.mark.anyio\nasync def test_event_preprocessor_ignore(app: App, monkeypatch: pytest.MonkeyPatch):\n    with monkeypatch.context() as m:\n        m.setattr(message, \"_event_preprocessors\", set())\n\n        @event_preprocessor\n        async def test_preprocessor():\n            raise IgnoredException(\"pass\")\n\n        assert test_preprocessor in {\n            dependent.call for dependent in message._event_preprocessors\n        }\n\n        runned = False\n\n        async def handler():\n            nonlocal runned\n            runned = True\n\n        with app.provider.context({}):\n            matcher = on_message(handlers=[handler])\n\n            async with app.test_matcher(matcher) as ctx:\n                bot = ctx.create_bot()\n                event = make_fake_event()()\n                ctx.receive_event(bot, event)\n\n        assert not runned, \"matcher should not runned\"\n\n\n@pytest.mark.anyio\nasync def test_event_preprocessor_exception(\n    app: App, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]\n):\n    with monkeypatch.context() as m:\n        m.setattr(message, \"_event_preprocessors\", set())\n\n        @event_preprocessor\n        async def test_preprocessor():\n            raise RuntimeError(\"test\")\n\n        assert test_preprocessor in {\n            dependent.call for dependent in message._event_preprocessors\n        }\n\n        runned = False\n\n        async def handler():\n            nonlocal runned\n            runned = True\n\n        handler_id = logger.add(\n            sys.stdout,\n            level=0,\n            diagnose=False,\n            filter=default_filter,\n            format=default_format,\n        )\n\n        try:\n            with app.provider.context({}):\n                matcher = on_message(handlers=[handler])\n\n                async with app.test_matcher(matcher) as ctx:\n                    bot = ctx.create_bot()\n                    event = make_fake_event()()\n                    ctx.receive_event(bot, event)\n        finally:\n            logger.remove(handler_id)\n\n        assert not runned, \"matcher should not runned\"\n        assert \"RuntimeError: test\" in capsys.readouterr().out\n\n\n@pytest.mark.anyio\nasync def test_event_postprocessor(app: App, monkeypatch: pytest.MonkeyPatch):\n    with monkeypatch.context() as m:\n        m.setattr(message, \"_event_postprocessors\", set())\n\n        runned = False\n\n        @event_postprocessor\n        async def test_postprocessor(\n            bot: Bot,\n            event: Event,\n            state: T_State,\n            sub: int = Depends(_dependency),\n            default: int = 1,\n        ):\n            nonlocal runned\n            runned = True\n\n        assert test_postprocessor in {\n            dependent.call for dependent in message._event_postprocessors\n        }\n\n        with app.provider.context({}):\n            matcher = on_message()\n\n            async with app.test_matcher(matcher) as ctx:\n                bot = ctx.create_bot()\n                event = make_fake_event()()\n                ctx.receive_event(bot, event)\n\n        assert runned, \"event_postprocessor should runned\"\n\n\n@pytest.mark.anyio\nasync def test_event_postprocessor_exception(\n    app: App, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]\n):\n    with monkeypatch.context() as m:\n        m.setattr(message, \"_event_postprocessors\", set())\n\n        @event_postprocessor\n        async def test_postprocessor():\n            raise RuntimeError(\"test\")\n\n        assert test_postprocessor in {\n            dependent.call for dependent in message._event_postprocessors\n        }\n\n        handler_id = logger.add(\n            sys.stdout,\n            level=0,\n            diagnose=False,\n            filter=default_filter,\n            format=default_format,\n        )\n\n        try:\n            with app.provider.context({}):\n                matcher = on_message()\n\n                async with app.test_matcher(matcher) as ctx:\n                    bot = ctx.create_bot()\n                    event = make_fake_event()()\n                    ctx.receive_event(bot, event)\n        finally:\n            logger.remove(handler_id)\n\n        assert \"RuntimeError: test\" in capsys.readouterr().out\n\n\n@pytest.mark.anyio\nasync def test_run_preprocessor(app: App, monkeypatch: pytest.MonkeyPatch):\n    with monkeypatch.context() as m:\n        m.setattr(message, \"_run_preprocessors\", set())\n\n        runned = False\n\n        @run_preprocessor\n        async def test_preprocessor(\n            bot: Bot,\n            event: Event,\n            state: T_State,\n            matcher: Matcher,\n            sub: int = Depends(_dependency),\n            default: int = 1,\n        ):\n            nonlocal runned\n            runned = True\n\n            await matcher.send(\"test\")\n\n        assert test_preprocessor in {\n            dependent.call for dependent in message._run_preprocessors\n        }\n\n        with app.provider.context({}):\n            matcher = on_message()\n\n            async with app.test_matcher(matcher) as ctx:\n                bot = ctx.create_bot()\n                event = make_fake_event()()\n                ctx.receive_event(bot, event)\n                ctx.should_call_send(event, \"test\", True, bot=bot)\n\n        assert runned, \"run_preprocessor should runned\"\n\n\n@pytest.mark.anyio\nasync def test_run_preprocessor_ignore(app: App, monkeypatch: pytest.MonkeyPatch):\n    with monkeypatch.context() as m:\n        m.setattr(message, \"_run_preprocessors\", set())\n\n        @run_preprocessor\n        async def test_preprocessor():\n            raise IgnoredException(\"pass\")\n\n        assert test_preprocessor in {\n            dependent.call for dependent in message._run_preprocessors\n        }\n\n        runned = False\n\n        async def handler():\n            nonlocal runned\n            runned = True\n\n        with app.provider.context({}):\n            matcher = on_message(handlers=[handler])\n\n            async with app.test_matcher(matcher) as ctx:\n                bot = ctx.create_bot()\n                event = make_fake_event()()\n                ctx.receive_event(bot, event)\n\n        assert not runned, \"matcher should not runned\"\n\n\n@pytest.mark.anyio\nasync def test_run_preprocessor_exception(\n    app: App, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]\n):\n    with monkeypatch.context() as m:\n        m.setattr(message, \"_run_preprocessors\", set())\n\n        @run_preprocessor\n        async def test_preprocessor():\n            raise RuntimeError(\"test\")\n\n        assert test_preprocessor in {\n            dependent.call for dependent in message._run_preprocessors\n        }\n\n        runned = False\n\n        async def handler():\n            nonlocal runned\n            runned = True\n\n        handler_id = logger.add(\n            sys.stdout,\n            level=0,\n            diagnose=False,\n            filter=default_filter,\n            format=default_format,\n        )\n\n        try:\n            with app.provider.context({}):\n                matcher = on_message(handlers=[handler])\n\n                async with app.test_matcher(matcher) as ctx:\n                    bot = ctx.create_bot()\n                    event = make_fake_event()()\n                    ctx.receive_event(bot, event)\n        finally:\n            logger.remove(handler_id)\n\n        assert not runned, \"matcher should not runned\"\n        assert \"RuntimeError: test\" in capsys.readouterr().out\n\n\n@pytest.mark.anyio\nasync def test_run_postprocessor(app: App, monkeypatch: pytest.MonkeyPatch):\n    with monkeypatch.context() as m:\n        m.setattr(message, \"_run_postprocessors\", set())\n\n        runned = False\n\n        @run_postprocessor\n        async def test_postprocessor(\n            bot: Bot,\n            event: Event,\n            state: T_State,\n            matcher: Matcher,\n            exception: Exception | None,\n            sub: int = Depends(_dependency),\n            default: int = 1,\n        ):\n            nonlocal runned\n            runned = True\n\n            await matcher.send(\"test\")\n\n        assert test_postprocessor in {\n            dependent.call for dependent in message._run_postprocessors\n        }\n\n        with app.provider.context({}):\n            matcher = on_message()\n\n            async with app.test_matcher(matcher) as ctx:\n                bot = ctx.create_bot()\n                event = make_fake_event()()\n                ctx.receive_event(bot, event)\n                ctx.should_call_send(event, \"test\", True, bot=bot)\n\n        assert runned, \"run_postprocessor should runned\"\n\n\n@pytest.mark.anyio\nasync def test_run_postprocessor_exception(\n    app: App, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]\n):\n    with monkeypatch.context() as m:\n        m.setattr(message, \"_run_postprocessors\", set())\n\n        @run_postprocessor\n        async def test_postprocessor():\n            raise RuntimeError(\"test\")\n\n        assert test_postprocessor in {\n            dependent.call for dependent in message._run_postprocessors\n        }\n\n        handler_id = logger.add(\n            sys.stdout,\n            level=0,\n            diagnose=False,\n            filter=default_filter,\n            format=default_format,\n        )\n\n        try:\n            with app.provider.context({}):\n                matcher = on_message()\n\n                async with app.test_matcher(matcher) as ctx:\n                    bot = ctx.create_bot()\n                    event = make_fake_event()()\n                    ctx.receive_event(bot, event)\n        finally:\n            logger.remove(handler_id)\n\n        assert \"RuntimeError: test\" in capsys.readouterr().out\n"
  },
  {
    "path": "tests/test_compat.py",
    "content": "from dataclasses import dataclass\nfrom typing import Annotated, Any\n\nfrom pydantic import BaseModel, ValidationError\nimport pytest\n\nfrom nonebot.compat import (\n    DEFAULT_CONFIG,\n    FieldInfo,\n    PydanticUndefined,\n    Required,\n    TypeAdapter,\n    custom_validation,\n    field_validator,\n    model_dump,\n    model_validator,\n    type_validate_json,\n    type_validate_python,\n)\n\n\ndef test_default_config():\n    assert DEFAULT_CONFIG.get(\"extra\") == \"allow\"\n    assert DEFAULT_CONFIG.get(\"arbitrary_types_allowed\") is True\n\n\ndef test_field_info():\n    # required should be convert to PydanticUndefined\n    assert FieldInfo(Required).default is PydanticUndefined\n\n    # field info should allow extra attributes\n    assert FieldInfo(test=\"test\").extra[\"test\"] == \"test\"\n\n\ndef test_field_validator():\n    class TestModel(BaseModel):\n        foo: int\n        bar: str\n\n        @field_validator(\"foo\")\n        @classmethod\n        def test_validator(cls, v: Any) -> Any:\n            if v > 0:\n                return v\n            raise ValueError(\"test must be greater than 0\")\n\n        @field_validator(\"bar\", mode=\"before\")\n        @classmethod\n        def test_validator_before(cls, v: Any) -> Any:\n            if not isinstance(v, str):\n                v = str(v)\n            return v\n\n    assert type_validate_python(TestModel, {\"foo\": 1, \"bar\": \"test\"}).foo == 1\n    assert type_validate_python(TestModel, {\"foo\": 1, \"bar\": 123}).bar == \"123\"\n\n    with pytest.raises(ValidationError):\n        TestModel(foo=0, bar=\"test\")\n\n\ndef test_type_adapter():\n    t = TypeAdapter(Annotated[int, FieldInfo(ge=1)])\n\n    assert t.validate_python(2) == 2\n\n    with pytest.raises(ValidationError):\n        t.validate_python(0)\n\n    assert t.validate_json(\"2\") == 2\n\n    with pytest.raises(ValidationError):\n        t.validate_json(\"0\")\n\n\ndef test_model_dump():\n    class NestedModel(BaseModel):\n        hidden: int\n        shown: int\n\n    class TestModel(BaseModel):\n        test1: int\n        test2: int\n        nested: NestedModel\n        items: list[NestedModel]\n\n    model = TestModel(\n        test1=1,\n        test2=2,\n        nested=NestedModel(hidden=3, shown=4),\n        items=[NestedModel(hidden=5, shown=6)],\n    )\n\n    assert model_dump(model, include={\"test1\"}) == {\"test1\": 1}\n    assert model_dump(model, exclude={\"test1\"}) == {\n        \"test2\": 2,\n        \"nested\": {\"hidden\": 3, \"shown\": 4},\n        \"items\": [{\"hidden\": 5, \"shown\": 6}],\n    }\n    assert model_dump(model, exclude={\"nested\": {\"hidden\"}}) == {\n        \"test1\": 1,\n        \"test2\": 2,\n        \"nested\": {\"shown\": 4},\n        \"items\": [{\"hidden\": 5, \"shown\": 6}],\n    }\n    assert model_dump(model, exclude={\"items\": {\"__all__\": {\"hidden\"}}}) == {\n        \"test1\": 1,\n        \"test2\": 2,\n        \"nested\": {\"hidden\": 3, \"shown\": 4},\n        \"items\": [{\"shown\": 6}],\n    }\n\n\ndef test_model_validator():\n    class TestModel(BaseModel):\n        foo: int\n        bar: str\n\n        @model_validator(mode=\"before\")\n        @classmethod\n        def test_validator_before(cls, data: Any) -> Any:\n            if isinstance(data, dict):\n                if \"foo\" not in data:\n                    data[\"foo\"] = 1\n            return data\n\n        @model_validator(mode=\"after\")\n        @classmethod\n        def test_validator_after(cls, data: Any) -> Any:\n            if isinstance(data, dict):\n                if data[\"bar\"] == \"test\":\n                    raise ValueError(\"bar should not be test\")\n            elif data.bar == \"test\":\n                raise ValueError(\"bar should not be test\")\n            return data\n\n    assert type_validate_python(TestModel, {\"bar\": \"aaa\"}).foo == 1\n\n    with pytest.raises(ValidationError):\n        type_validate_python(TestModel, {\"foo\": 1, \"bar\": \"test\"})\n\n\ndef test_custom_validation():\n    called = []\n\n    @custom_validation\n    @dataclass\n    class TestModel:\n        test: int\n\n        @classmethod\n        def __get_validators__(cls):\n            yield cls._validate_1\n            yield cls._validate_2\n\n        @classmethod\n        def _validate_1(cls, v: Any) -> Any:\n            called.append(1)\n            return v\n\n        @classmethod\n        def _validate_2(cls, v: Any) -> Any:\n            called.append(2)\n            return cls(test=v[\"test\"])\n\n    assert type_validate_python(TestModel, {\"test\": 1}) == TestModel(test=1)\n    assert called == [1, 2]\n\n\ndef test_validate_json():\n    class TestModel(BaseModel):\n        test1: int\n        test2: str\n        test3: bool\n        test4: dict\n        test5: list\n        test6: int | None\n\n    assert type_validate_json(\n        TestModel,\n        \"{\"\n        '  \"test1\": 1,'\n        '  \"test2\": \"2\",'\n        '  \"test3\": true,'\n        '  \"test4\": {},'\n        '  \"test5\": [],'\n        '  \"test6\": null'\n        \"}\",\n    ) == TestModel(test1=1, test2=\"2\", test3=True, test4={}, test5=[], test6=None)\n"
  },
  {
    "path": "tests/test_config.py",
    "content": "from typing import TYPE_CHECKING\n\nfrom pydantic import BaseModel, Field\nimport pytest\n\nfrom nonebot.compat import PYDANTIC_V2, LegacyUnionField\nfrom nonebot.config import DOTENV_TYPE, BaseSettings, SettingsConfig, SettingsError\n\n\nclass Simple(BaseModel):\n    a: int = 0\n    b: int = 0\n    c: dict = {}\n    complex: list = []\n\n\nclass Example(BaseSettings):\n    if TYPE_CHECKING:\n        _env_file: DOTENV_TYPE | None = \".env\", \".env.example\"\n        _env_nested_delimiter: str | None = \"__\"\n\n    if PYDANTIC_V2:\n        model_config = SettingsConfig(\n            env_file=(\".env\", \".env.example\"), env_nested_delimiter=\"__\"\n        )\n    else:\n\n        class Config(  # pyright: ignore[reportIncompatibleVariableOverride]\n            SettingsConfig\n        ):\n            env_file = \".env\", \".env.example\"\n            env_nested_delimiter = \"__\"\n\n    simple: str = \"\"\n    int_str: int | str = LegacyUnionField(default=\"\")\n    complex: list[int] = Field(default=[1])\n    complex_none: list[int] | None = None\n    complex_union: int | list[int] = 1\n    nested: Simple = Simple()\n    nested_inner: Simple = Simple()\n    aliased_simple: str = Field(default=\"\", alias=\"alias_simple\")\n\n\nclass ExampleWithoutDelimiter(Example):\n    if PYDANTIC_V2:\n        model_config = SettingsConfig(env_nested_delimiter=None)\n    else:\n\n        class Config(  # pyright: ignore[reportIncompatibleVariableOverride]\n            SettingsConfig\n        ):\n            env_nested_delimiter = None\n\n\ndef test_config_no_env():\n    config = Example(_env_file=None)\n    assert config.simple == \"\"\n    with pytest.raises(AttributeError):\n        config.common_config\n\n\ndef test_config_with_env():\n    config = Example(_env_file=(\".env\", \".env.example\"))\n    assert config.simple == \"simple\"\n\n    assert config.int_str == 123\n\n    assert config.complex == [1, 2, 3]\n\n    assert config.complex_none is None\n\n    assert config.complex_union == [1, 2, 3]\n\n    assert config.nested.a == 1\n    assert config.nested.b == 2\n    assert config.nested.c == {\"c\": \"3\"}\n    assert config.nested.complex == [1, 2, 3]\n    with pytest.raises(AttributeError):\n        config.nested__b\n    with pytest.raises(AttributeError):\n        config.nested__c__c\n    with pytest.raises(AttributeError):\n        config.nested__complex\n\n    assert config.nested_inner.a == 1\n    assert config.nested_inner.b == 2\n    with pytest.raises(AttributeError):\n        config.nested_inner__a\n    with pytest.raises(AttributeError):\n        config.nested_inner__b\n\n    assert config.aliased_simple == \"aliased_simple\"\n\n    assert config.common_config == \"common\"\n\n    assert config.other_simple == \"simple\"\n\n    assert config.other_nested == {\"a\": 1, \"b\": 2}\n    with pytest.raises(AttributeError):\n        config.other_nested__b\n\n    assert config.other_nested_inner == {\"a\": 1, \"b\": 2}\n    with pytest.raises(AttributeError):\n        config.other_nested_inner__a\n    with pytest.raises(AttributeError):\n        config.other_nested_inner__b\n\n\ndef test_config_error_env():\n    with pytest.MonkeyPatch().context() as m:\n        m.setenv(\"COMPLEX\", \"not json\")\n\n        with pytest.raises(SettingsError):\n            Example(_env_file=(\".env\", \".env.example\"))\n\n\ndef test_config_without_delimiter():\n    config = ExampleWithoutDelimiter()\n    assert config.nested.a == 1\n    assert config.nested.b == 0\n    assert config.nested__b == 2\n    assert config.nested.c == {}\n    assert config.nested__c__c == 3\n    assert config.nested.complex == []\n    assert config.nested__complex == [1, 2, 3]\n\n    assert config.nested_inner.a == 0\n    assert config.nested_inner.b == 0\n\n    assert config.other_nested == {\"a\": 1}\n    assert config.other_nested__b == 2\n\n    with pytest.raises(AttributeError):\n        config.other_nested_inner\n    assert config.other_nested_inner__a == 1\n    assert config.other_nested_inner__b == 2\n"
  },
  {
    "path": "tests/test_driver.py",
    "content": "from http.cookies import SimpleCookie\nimport json\nfrom typing import Any\n\nfrom aiohttp import ClientSession, ClientWebSocketResponse, WSMessage, WSMsgType\nimport anyio\nfrom nonebug import App\nimport pytest\n\nfrom nonebot.adapters import Bot\nfrom nonebot.dependencies import Dependent\nfrom nonebot.drivers import (\n    URL,\n    ASGIMixin,\n    Driver,\n    HTTPClientMixin,\n    HTTPServerSetup,\n    Request,\n    Response,\n    Timeout,\n    WebSocket,\n    WebSocketClientMixin,\n    WebSocketServerSetup,\n)\nfrom nonebot.drivers.aiohttp import WebSocket as AiohttpWebSocket\nfrom nonebot.exception import WebSocketClosed\nfrom nonebot.params import Depends\nfrom utils import FakeAdapter\n\n\n@pytest.mark.anyio\n@pytest.mark.parametrize(\n    \"driver\", [pytest.param(\"nonebot.drivers.none:Driver\", id=\"none\")], indirect=True\n)\nasync def test_lifespan(driver: Driver):\n    adapter = FakeAdapter(driver)\n\n    start_log = []\n    ready_log = []\n    shutdown_log = []\n\n    @driver.on_startup\n    async def _startup1():\n        assert start_log == []\n        start_log.append(1)\n\n    @driver.on_startup\n    async def _startup2():\n        assert start_log == [1]\n        start_log.append(2)\n\n    @adapter.on_ready\n    def _ready1():\n        assert start_log == [1, 2]\n        assert ready_log == []\n        ready_log.append(1)\n\n    @adapter.on_ready\n    def _ready2():\n        assert ready_log == [1]\n        ready_log.append(2)\n\n    @driver.on_shutdown\n    async def _shutdown1():\n        assert shutdown_log == [2]\n        shutdown_log.append(1)\n\n    @driver.on_shutdown\n    async def _shutdown2():\n        assert shutdown_log == []\n        shutdown_log.append(2)\n\n    async with driver._lifespan:\n        assert start_log == [1, 2]\n        assert ready_log == [1, 2]\n\n    assert shutdown_log == [2, 1]\n\n\n@pytest.mark.anyio\n@pytest.mark.parametrize(\n    \"driver\",\n    [\n        pytest.param(\"nonebot.drivers.fastapi:Driver\", id=\"fastapi\"),\n        pytest.param(\"nonebot.drivers.quart:Driver\", id=\"quart\"),\n    ],\n    indirect=True,\n)\nasync def test_http_server(app: App, driver: Driver):\n    assert isinstance(driver, ASGIMixin)\n\n    async def _handle_http(request: Request) -> Response:\n        assert request.content in (b\"test\", \"test\")\n        return Response(200, content=\"test\")\n\n    http_setup = HTTPServerSetup(URL(\"/http_test\"), \"POST\", \"http_test\", _handle_http)\n    driver.setup_http_server(http_setup)\n\n    async with app.test_server(driver.asgi) as ctx:\n        client = ctx.get_client()\n        response = await client.post(\"/http_test\", data=\"test\")\n        assert response.status_code == 200\n        assert response.text == \"test\"\n\n    await anyio.sleep(1)\n\n\n@pytest.mark.anyio\n@pytest.mark.parametrize(\n    \"driver\",\n    [\n        pytest.param(\"nonebot.drivers.fastapi:Driver\", id=\"fastapi\"),\n        pytest.param(\"nonebot.drivers.quart:Driver\", id=\"quart\"),\n    ],\n    indirect=True,\n)\nasync def test_websocket_server(app: App, driver: Driver):\n    assert isinstance(driver, ASGIMixin)\n\n    async def _handle_ws(ws: WebSocket) -> None:\n        await ws.accept()\n        data = await ws.receive()\n        assert data == \"ping\"\n        await ws.send(\"pong\")\n\n        data = await ws.receive()\n        assert data == b\"ping\"\n        await ws.send(b\"pong\")\n\n        data = await ws.receive_text()\n        assert data == \"ping\"\n        await ws.send(\"pong\")\n\n        data = await ws.receive_bytes()\n        assert data == b\"ping\"\n        await ws.send(b\"pong\")\n\n        with pytest.raises(WebSocketClosed, match=r\"code=1000\"):\n            await ws.receive()\n\n    ws_setup = WebSocketServerSetup(URL(\"/ws_test\"), \"ws_test\", _handle_ws)\n    driver.setup_websocket_server(ws_setup)\n\n    async with app.test_server(driver.asgi) as ctx:\n        client = ctx.get_client()\n\n        async with client.websocket_connect(\"/ws_test\") as ws:\n            await ws.send_text(\"ping\")\n            assert await ws.receive_text() == \"pong\"\n            await ws.send_bytes(b\"ping\")\n            assert await ws.receive_bytes() == b\"pong\"\n\n            await ws.send_text(\"ping\")\n            assert await ws.receive_text() == \"pong\"\n\n            await ws.send_bytes(b\"ping\")\n            assert await ws.receive_bytes() == b\"pong\"\n\n            await ws.close(code=1000)\n\n    await anyio.sleep(1)\n\n\n@pytest.mark.anyio\n@pytest.mark.parametrize(\n    \"driver\",\n    [\n        pytest.param(\"nonebot.drivers.fastapi:Driver\", id=\"fastapi\"),\n        pytest.param(\"nonebot.drivers.quart:Driver\", id=\"quart\"),\n    ],\n    indirect=True,\n)\nasync def test_cross_context(app: App, driver: Driver):\n    assert isinstance(driver, ASGIMixin)\n\n    ws: WebSocket | None = None\n    ws_ready = anyio.Event()\n    ws_should_close = anyio.Event()\n\n    # create a background task before the ws connection established\n    async def background_task():\n        try:\n            await ws_ready.wait()\n            assert ws is not None\n\n            await ws.send(\"ping\")\n            data = await ws.receive()\n            assert data == \"pong\"\n        finally:\n            ws_should_close.set()\n\n    async def _handle_ws(websocket: WebSocket) -> None:\n        nonlocal ws\n        await websocket.accept()\n        ws = websocket\n        ws_ready.set()\n\n        await ws_should_close.wait()\n        await websocket.close()\n\n    ws_setup = WebSocketServerSetup(URL(\"/ws_test\"), \"ws_test\", _handle_ws)\n    driver.setup_websocket_server(ws_setup)\n\n    async with anyio.create_task_group() as tg, app.test_server(driver.asgi) as ctx:\n        tg.start_soon(background_task)\n\n        client = ctx.get_client()\n\n        async with client.websocket_connect(\"/ws_test\") as websocket:\n            try:\n                data = await websocket.receive_text()\n                assert data == \"ping\"\n                await websocket.send_text(\"pong\")\n            except Exception as e:\n                if not e.args or \"websocket.close\" not in str(e.args[0]):\n                    raise\n\n    await anyio.sleep(1)\n\n\n@pytest.mark.anyio\n@pytest.mark.parametrize(\n    \"driver\",\n    [\n        pytest.param(\"nonebot.drivers.httpx:Driver\", id=\"httpx\"),\n        pytest.param(\"nonebot.drivers.aiohttp:Driver\", id=\"aiohttp\"),\n    ],\n    indirect=True,\n)\nasync def test_http_client(driver: Driver, server_url: URL):\n    assert isinstance(driver, HTTPClientMixin)\n\n    # simple post with query, headers, cookies and content\n    request = Request(\n        \"POST\",\n        server_url,\n        params={\"param\": \"test\"},\n        headers={\"X-Test\": \"test\"},\n        cookies={\"session\": \"test\"},\n        content=\"test\",\n        timeout=Timeout(total=4, connect=2, read=2),\n    )\n    response = await driver.request(request)\n    assert server_url.host is not None\n    request_raw_url = Request(\n        \"POST\",\n        (\n            server_url.scheme.encode(\"ascii\"),\n            server_url.host.encode(\"ascii\"),\n            server_url.port,\n            server_url.path.encode(\"ascii\"),\n        ),\n        params={\"param\": \"test\"},\n        headers={\"X-Test\": \"test\"},\n        cookies={\"session\": \"test\"},\n        content=\"test\",\n        timeout=Timeout(total=4, connect=2, read=2),\n    )\n    assert request.url == request_raw_url.url, (\n        \"request.url should be equal to request_raw_url.url\"\n    )\n    assert response.status_code == 200\n    assert response.content\n    data = json.loads(response.content)\n    assert data[\"method\"] == \"POST\"\n    assert data[\"args\"] == {\"param\": \"test\"}\n    assert data[\"headers\"].get(\"X-Test\") == \"test\"\n    assert data[\"headers\"].get(\"Cookie\") == \"session=test\"\n    assert data[\"data\"] == \"test\"\n\n    # post with data body\n    request = Request(\"POST\", server_url, data={\"form\": \"test\"})\n    response = await driver.request(request)\n    assert response.status_code == 200\n    assert response.content\n    data = json.loads(response.content)\n    assert data[\"method\"] == \"POST\"\n    assert data[\"form\"] == {\"form\": \"test\"}\n\n    # post with json body\n    request = Request(\"POST\", server_url, json={\"json\": \"test\"})\n    response = await driver.request(request)\n    assert response.status_code == 200\n    assert response.content\n    data = json.loads(response.content)\n    assert data[\"method\"] == \"POST\"\n    assert data[\"json\"] == {\"json\": \"test\"}\n\n    # post with files and form data\n    request = Request(\n        \"POST\",\n        server_url,\n        data={\"form\": \"test\"},\n        files=[\n            (\"test1\", b\"test\"),\n            (\"test2\", (\"test.txt\", b\"test\")),\n            (\"test3\", (\"test.txt\", b\"test\", \"text/plain\")),\n        ],\n    )\n    response = await driver.request(request)\n    assert response.status_code == 200\n    assert response.content\n    data = json.loads(response.content)\n    assert data[\"method\"] == \"POST\"\n    assert data[\"form\"] == {\"form\": \"test\"}\n    assert data[\"files\"] == {\n        \"test1\": \"test\",\n        \"test2\": \"test\",\n        \"test3\": \"test\",\n    }, \"file parsing error\"\n\n    # post stream request with query, headers, cookies and content\n    request = Request(\n        \"POST\",\n        server_url,\n        params={\"param\": \"stream\"},\n        headers={\"X-Test\": \"stream\"},\n        cookies={\"session\": \"stream\"},\n        content=\"stream_test\" * 1024,\n        timeout=Timeout(total=4, connect=2, read=2),\n    )\n    chunks = []\n    async for resp in driver.stream_request(request, chunk_size=4):\n        assert resp.status_code == 200\n        assert resp.content\n        chunks.append(resp.content)\n    assert all(len(chunk) == 4 for chunk in chunks[:-1])\n    data = json.loads(b\"\".join(chunks))\n    assert data[\"method\"] == \"POST\"\n    assert data[\"args\"] == {\"param\": \"stream\"}\n    assert data[\"headers\"].get(\"X-Test\") == \"stream\"\n    assert data[\"headers\"].get(\"Cookie\") == \"session=stream\"\n    assert data[\"data\"] == \"stream_test\" * 1024\n\n    # post stream request with data body\n    request = Request(\"POST\", server_url, data={\"form\": \"test\"})\n    chunks = []\n    async for resp in driver.stream_request(request, chunk_size=4):\n        assert resp.status_code == 200\n        assert resp.content\n        chunks.append(resp.content)\n    assert all(len(chunk) == 4 for chunk in chunks[:-1])\n    data = json.loads(b\"\".join(chunks))\n    assert data[\"method\"] == \"POST\"\n    assert data[\"form\"] == {\"form\": \"test\"}\n\n    # post stream request with json body\n    request = Request(\"POST\", server_url, json={\"json\": \"test\"})\n    chunks = []\n    async for resp in driver.stream_request(request, chunk_size=4):\n        assert resp.status_code == 200\n        assert resp.content\n        chunks.append(resp.content)\n    assert all(len(chunk) == 4 for chunk in chunks[:-1])\n    data = json.loads(b\"\".join(chunks))\n    assert data[\"method\"] == \"POST\"\n    assert data[\"json\"] == {\"json\": \"test\"}\n\n    # post stream request with files and form data\n    request = Request(\n        \"POST\",\n        server_url,\n        data={\"form\": \"test\"},\n        files=[\n            (\"test1\", b\"test\"),\n            (\"test2\", (\"test.txt\", b\"test\")),\n            (\"test3\", (\"test.txt\", b\"test\", \"text/plain\")),\n        ],\n    )\n    chunks = []\n    async for resp in driver.stream_request(request, chunk_size=4):\n        assert response.status_code == 200\n        assert response.content\n        chunks.append(resp.content)\n    assert all(len(chunk) == 4 for chunk in chunks[:-1])\n    data = json.loads(b\"\".join(chunks))\n    assert data[\"method\"] == \"POST\"\n    assert data[\"form\"] == {\"form\": \"test\"}\n    assert data[\"files\"] == {\n        \"test1\": \"test\",\n        \"test2\": \"test\",\n        \"test3\": \"test\",\n    }, \"file parsing error\"\n\n    await anyio.sleep(1)\n\n\n@pytest.mark.anyio\n@pytest.mark.parametrize(\n    \"driver\",\n    [\n        pytest.param(\"nonebot.drivers.httpx:Driver\", id=\"httpx\"),\n        pytest.param(\"nonebot.drivers.aiohttp:Driver\", id=\"aiohttp\"),\n    ],\n    indirect=True,\n)\nasync def test_http_client_session(driver: Driver, server_url: URL):\n    assert isinstance(driver, HTTPClientMixin)\n\n    session = driver.get_session(\n        params={\"session\": \"test\"},\n        headers={\"X-Session\": \"test\"},\n        cookies={\"session\": \"test\"},\n    )\n    request = Request(\"GET\", server_url)\n    with pytest.raises(RuntimeError):\n        await session.request(request)\n\n    with pytest.raises(RuntimeError):  # noqa: PT012\n        async with session:\n            async with session:\n                ...\n\n    async with session as session:\n        # simple post with query, headers, cookies and content\n        request = Request(\n            \"POST\",\n            server_url,\n            params={\"param\": \"test\"},\n            headers={\"X-Test\": \"test\"},\n            cookies={\"cookie\": \"test\"},\n            content=\"test\",\n            timeout=Timeout(total=4, connect=2, read=2),\n        )\n        response = await session.request(request)\n        assert response.status_code == 200\n        assert response.content\n        data = json.loads(response.content)\n        assert data[\"method\"] == \"POST\"\n        assert data[\"args\"] == {\"session\": \"test\", \"param\": \"test\"}\n        assert data[\"headers\"].get(\"X-Session\") == \"test\"\n        assert data[\"headers\"].get(\"X-Test\") == \"test\"\n        assert {\n            key: cookie.value\n            for key, cookie in SimpleCookie(data[\"headers\"].get(\"Cookie\")).items()\n        } == {\n            \"session\": \"test\",\n            \"cookie\": \"test\",\n        }\n        assert data[\"data\"] == \"test\"\n\n        # post with data body\n        request = Request(\"POST\", server_url, data={\"form\": \"test\"})\n        response = await session.request(request)\n        assert response.status_code == 200\n        assert response.content\n        data = json.loads(response.content)\n        assert data[\"method\"] == \"POST\"\n        assert data[\"args\"] == {\"session\": \"test\"}\n        assert data[\"headers\"].get(\"X-Session\") == \"test\"\n        assert {\n            key: cookie.value\n            for key, cookie in SimpleCookie(data[\"headers\"].get(\"Cookie\")).items()\n        } == {\"session\": \"test\"}\n        assert data[\"form\"] == {\"form\": \"test\"}\n\n        # post with json body\n        request = Request(\"POST\", server_url, json={\"json\": \"test\"})\n        response = await session.request(request)\n        assert response.status_code == 200\n        assert response.content\n        data = json.loads(response.content)\n        assert data[\"method\"] == \"POST\"\n        assert data[\"args\"] == {\"session\": \"test\"}\n        assert data[\"headers\"].get(\"X-Session\") == \"test\"\n        assert {\n            key: cookie.value\n            for key, cookie in SimpleCookie(data[\"headers\"].get(\"Cookie\")).items()\n        } == {\"session\": \"test\"}\n        assert data[\"json\"] == {\"json\": \"test\"}\n\n        # post with files and form data\n        request = Request(\n            \"POST\",\n            server_url,\n            data={\"form\": \"test\"},\n            files=[\n                (\"test1\", b\"test\"),\n                (\"test2\", (\"test.txt\", b\"test\")),\n                (\"test3\", (\"test.txt\", b\"test\", \"text/plain\")),\n            ],\n        )\n        response = await session.request(request)\n        assert response.status_code == 200\n        assert response.content\n        data = json.loads(response.content)\n        assert data[\"method\"] == \"POST\"\n        assert data[\"args\"] == {\"session\": \"test\"}\n        assert data[\"headers\"].get(\"X-Session\") == \"test\"\n        assert {\n            key: cookie.value\n            for key, cookie in SimpleCookie(data[\"headers\"].get(\"Cookie\")).items()\n        } == {\"session\": \"test\"}\n        assert data[\"form\"] == {\"form\": \"test\"}\n        assert data[\"files\"] == {\n            \"test1\": \"test\",\n            \"test2\": \"test\",\n            \"test3\": \"test\",\n        }, \"file parsing error\"\n\n        # post stream request with query, headers, cookies and content\n        request = Request(\n            \"POST\",\n            server_url,\n            params={\"param\": \"stream\"},\n            headers={\"X-Test\": \"stream\"},\n            cookies={\"cookie\": \"stream\"},\n            content=\"stream_test\" * 1024,\n            timeout=Timeout(total=4, connect=2, read=2),\n        )\n        chunks = []\n        async for resp in session.stream_request(request, chunk_size=4):\n            assert resp.status_code == 200\n            assert resp.content\n            chunks.append(resp.content)\n        assert all(len(chunk) == 4 for chunk in chunks[:-1])\n        data = json.loads(b\"\".join(chunks))\n        assert data[\"method\"] == \"POST\"\n        assert data[\"args\"] == {\"session\": \"test\", \"param\": \"stream\"}\n        assert data[\"headers\"].get(\"X-Session\") == \"test\"\n        assert data[\"headers\"].get(\"X-Test\") == \"stream\"\n        assert {\n            key: cookie.value\n            for key, cookie in SimpleCookie(data[\"headers\"].get(\"Cookie\")).items()\n        } == {\"session\": \"test\", \"cookie\": \"stream\"}\n        assert data[\"data\"] == \"stream_test\" * 1024\n\n        # post stream request with data body\n        request = Request(\"POST\", server_url, data={\"form\": \"test\"})\n        chunks = []\n        async for resp in session.stream_request(request, chunk_size=4):\n            assert resp.status_code == 200\n            assert resp.content\n            chunks.append(resp.content)\n        assert all(len(chunk) == 4 for chunk in chunks[:-1])\n        data = json.loads(b\"\".join(chunks))\n        assert data[\"method\"] == \"POST\"\n        assert data[\"args\"] == {\"session\": \"test\"}\n        assert data[\"headers\"].get(\"X-Session\") == \"test\"\n        assert {\n            key: cookie.value\n            for key, cookie in SimpleCookie(data[\"headers\"].get(\"Cookie\")).items()\n        } == {\"session\": \"test\"}\n        assert data[\"form\"] == {\"form\": \"test\"}\n\n        # post stream request with json body\n        request = Request(\"POST\", server_url, json={\"json\": \"test\"})\n        chunks = []\n        async for resp in session.stream_request(request, chunk_size=4):\n            assert resp.status_code == 200\n            assert resp.content\n            chunks.append(resp.content)\n        assert all(len(chunk) == 4 for chunk in chunks[:-1])\n        data = json.loads(b\"\".join(chunks))\n        assert data[\"method\"] == \"POST\"\n        assert data[\"args\"] == {\"session\": \"test\"}\n        assert data[\"headers\"].get(\"X-Session\") == \"test\"\n        assert {\n            key: cookie.value\n            for key, cookie in SimpleCookie(data[\"headers\"].get(\"Cookie\")).items()\n        } == {\"session\": \"test\"}\n        assert data[\"json\"] == {\"json\": \"test\"}\n\n        # post stream request with files and form data\n        request = Request(\n            \"POST\",\n            server_url,\n            data={\"form\": \"test\"},\n            files=[\n                (\"test1\", b\"test\"),\n                (\"test2\", (\"test.txt\", b\"test\")),\n                (\"test3\", (\"test.txt\", b\"test\", \"text/plain\")),\n            ],\n        )\n        chunks = []\n        async for resp in session.stream_request(request, chunk_size=4):\n            assert resp.status_code == 200\n            assert resp.content\n            chunks.append(resp.content)\n        assert all(len(chunk) == 4 for chunk in chunks[:-1])\n        data = json.loads(b\"\".join(chunks))\n        assert data[\"method\"] == \"POST\"\n        assert data[\"args\"] == {\"session\": \"test\"}\n        assert data[\"headers\"].get(\"X-Session\") == \"test\"\n        assert {\n            key: cookie.value\n            for key, cookie in SimpleCookie(data[\"headers\"].get(\"Cookie\")).items()\n        } == {\"session\": \"test\"}\n        assert data[\"form\"] == {\"form\": \"test\"}\n        assert data[\"files\"] == {\n            \"test1\": \"test\",\n            \"test2\": \"test\",\n            \"test3\": \"test\",\n        }, \"file parsing error\"\n\n    await anyio.sleep(1)\n\n\n@pytest.mark.anyio\n@pytest.mark.parametrize(\n    \"driver\",\n    [\n        pytest.param(\"nonebot.drivers.websockets:Driver\", id=\"websockets\"),\n        pytest.param(\"nonebot.drivers.aiohttp:Driver\", id=\"aiohttp\"),\n    ],\n    indirect=True,\n)\nasync def test_websocket_client(driver: Driver, server_url: URL):\n    assert isinstance(driver, WebSocketClientMixin)\n\n    request = Request(\"GET\", server_url.with_scheme(\"ws\"))\n    async with driver.websocket(request) as ws:\n        await ws.send(\"test\")\n        assert await ws.receive() == \"test\"\n\n        await ws.send(b\"test\")\n        assert await ws.receive() == b\"test\"\n\n        await ws.send_text(\"test\")\n        assert await ws.receive_text() == \"test\"\n\n        await ws.send_bytes(b\"test\")\n        assert await ws.receive_bytes() == b\"test\"\n\n        await ws.send(\"quit\")\n        with pytest.raises(WebSocketClosed, match=r\"code=1000\"):\n            await ws.receive()\n\n    await anyio.sleep(1)\n\n\n@pytest.mark.anyio\n@pytest.mark.parametrize(\n    (\"msg_type\"),\n    [\n        pytest.param(\"CLOSE\", id=\"aiohttp-close\"),\n        pytest.param(\"CLOSING\", id=\"aiohttp-closing\"),\n        pytest.param(\"CLOSED\", id=\"aiohttp-closed\"),\n    ],\n)\nasync def test_aiohttp_websocket_close_frame(msg_type: str) -> None:\n    class DummyWS(ClientWebSocketResponse):\n        def __init__(self) -> None:\n            pass\n\n        @property\n        def close_code(self) -> None:\n            return None\n\n        @property\n        def closed(self) -> bool:\n            return True\n\n        async def receive(self, timeout: float | None = None) -> WSMessage:  # noqa: ASYNC109\n            return WSMessage(type=WSMsgType[msg_type], data=None, extra=None)\n\n    async with ClientSession() as session:\n        ws = AiohttpWebSocket(\n            request=Request(\"GET\", \"ws://example.com\"),\n            session=session,\n            websocket=DummyWS(),\n        )\n\n        with pytest.raises(WebSocketClosed, match=r\"code=1006\"):\n            await ws.receive()\n\n\n@pytest.mark.parametrize(\n    (\"driver\", \"driver_type\"),\n    [\n        pytest.param(\n            \"nonebot.drivers.fastapi:Driver+nonebot.drivers.aiohttp:Mixin\",\n            \"fastapi+aiohttp\",\n            id=\"fastapi+aiohttp\",\n        ),\n        pytest.param(\n            \"~httpx:Driver+~websockets\",\n            \"none+httpx+websockets\",\n            id=\"httpx+websockets\",\n        ),\n    ],\n    indirect=[\"driver\"],\n)\ndef test_combine_driver(driver: Driver, driver_type: str):\n    assert driver.type == driver_type\n\n\n@pytest.mark.anyio\nasync def test_bot_connect_hook(app: App, driver: Driver):\n    with pytest.MonkeyPatch.context() as m:\n        conn_hooks: set[Dependent[Any]] = set()\n        disconn_hooks: set[Dependent[Any]] = set()\n        m.setattr(Driver, \"_bot_connection_hook\", conn_hooks)\n        m.setattr(Driver, \"_bot_disconnection_hook\", disconn_hooks)\n\n        conn_should_be_called = False\n        disconn_should_be_called = False\n        dependency_should_be_run = False\n        dependency_should_be_cleaned = False\n\n        async def dependency():\n            nonlocal dependency_should_be_run, dependency_should_be_cleaned\n            dependency_should_be_run = True\n            try:\n                yield 1\n            finally:\n                dependency_should_be_cleaned = True\n\n        @driver.on_bot_connect\n        async def conn_hook(foo: Bot, dep: int = Depends(dependency), default: int = 1):\n            nonlocal conn_should_be_called\n\n            if foo is not bot:\n                pytest.fail(\"on_bot_connect hook called with wrong bot\")\n            if dep != 1:\n                pytest.fail(\"on_bot_connect hook called with wrong dependency\")\n            if default != 1:\n                pytest.fail(\"on_bot_connect hook called with wrong default value\")\n\n            conn_should_be_called = True\n\n        @driver.on_bot_disconnect\n        async def disconn_hook(\n            foo: Bot, dep: int = Depends(dependency), default: int = 1\n        ):\n            nonlocal disconn_should_be_called\n\n            if foo is not bot:\n                pytest.fail(\"on_bot_disconnect hook called with wrong bot\")\n            if dep != 1:\n                pytest.fail(\"on_bot_connect hook called with wrong dependency\")\n            if default != 1:\n                pytest.fail(\"on_bot_connect hook called with wrong default value\")\n\n            disconn_should_be_called = True\n\n        if conn_hook not in {hook.call for hook in conn_hooks}:  # type: ignore\n            pytest.fail(\"on_bot_connect hook not registered\")\n        if disconn_hook not in {hook.call for hook in disconn_hooks}:  # type: ignore\n            pytest.fail(\"on_bot_disconnect hook not registered\")\n\n        async with app.test_api() as ctx:\n            bot = ctx.create_bot()\n\n        await anyio.sleep(1)\n\n        if not conn_should_be_called:\n            pytest.fail(\"on_bot_connect hook not called\")\n        if not disconn_should_be_called:\n            pytest.fail(\"on_bot_disconnect hook not called\")\n        if not dependency_should_be_run:\n            pytest.fail(\"dependency not run\")\n        if not dependency_should_be_cleaned:\n            pytest.fail(\"dependency not cleaned\")\n"
  },
  {
    "path": "tests/test_echo.py",
    "content": "from nonebug import App\nimport pytest\n\nfrom utils import FakeMessage, FakeMessageSegment, make_fake_event\n\n\n@pytest.mark.anyio\nasync def test_echo(app: App):\n    from nonebot.plugins.echo import echo\n\n    async with app.test_matcher(echo) as ctx:\n        bot = ctx.create_bot()\n\n        message = FakeMessage(\"/echo 123\")\n        event = make_fake_event(_message=message)()\n        ctx.receive_event(bot, event)\n        ctx.should_call_send(event, FakeMessage(\"123\"), True, bot=bot)\n\n        message = FakeMessageSegment.text(\"/echo 123\") + FakeMessageSegment.image(\n            \"test\"\n        )\n        event = make_fake_event(_message=message)()\n        ctx.receive_event(bot, event)\n        ctx.should_call_send(\n            event,\n            FakeMessageSegment.text(\"123\") + FakeMessageSegment.image(\"test\"),\n            True,\n            bot=bot,\n        )\n\n        message = FakeMessage(\"/echo\")\n        event = make_fake_event(_message=message)()\n        ctx.receive_event(bot, event)\n"
  },
  {
    "path": "tests/test_init.py",
    "content": "from nonebug import App\nimport pytest\n\nimport nonebot\nfrom nonebot import (\n    get_adapter,\n    get_adapters,\n    get_app,\n    get_asgi,\n    get_bot,\n    get_bots,\n    get_driver,\n)\nfrom nonebot.drivers import ASGIMixin, Driver, ReverseDriver\n\n\ndef test_init():\n    env = nonebot.get_driver().env\n    assert env == \"test\"\n\n    config = nonebot.get_driver().config\n    assert config.nickname == {\"test\"}\n    assert config.superusers == {\"test\", \"fake:faketest\"}\n    assert config.api_timeout is None\n\n    assert config.simple_none is None\n    assert config.config_from_env == {\"test\": \"test\"}\n    assert config.config_override == \"new\"\n    assert config.config_from_init == \"init\"\n    assert config.common_config == \"common\"\n    assert config.common_override == \"new\"\n    assert config.nested_dict == {\"a\": 1, \"b\": 2, \"c\": {\"d\": 3}}\n    assert config.nested_missing_dict == {\"a\": 1, \"b\": {\"c\": 2}}\n    assert config.not_nested == \"some string\"\n\n\ndef test_get_driver(monkeypatch: pytest.MonkeyPatch):\n    with monkeypatch.context() as m:\n        m.setattr(nonebot, \"_driver\", None)\n        with pytest.raises(ValueError, match=\"initialized\"):\n            get_driver()\n\n\ndef test_get_asgi():\n    driver = get_driver()\n    assert isinstance(driver, ReverseDriver)\n    assert isinstance(driver, ASGIMixin)\n    assert get_asgi() == driver.asgi\n\n\ndef test_get_app():\n    driver = get_driver()\n    assert isinstance(driver, ReverseDriver)\n    assert isinstance(driver, ASGIMixin)\n    assert get_app() == driver.server_app\n\n\n@pytest.mark.anyio\nasync def test_get_adapter(app: App, monkeypatch: pytest.MonkeyPatch):\n    async with app.test_api() as ctx:\n        adapter = ctx.create_adapter()\n        adapter_name = adapter.get_name()\n\n        with monkeypatch.context() as m:\n            m.setattr(Driver, \"_adapters\", {adapter_name: adapter})\n            assert get_adapters() == {adapter_name: adapter}\n            assert get_adapter(adapter_name) is adapter\n            assert get_adapter(adapter.__class__) is adapter\n            with pytest.raises(ValueError, match=\"registered\"):\n                get_adapter(\"not exist\")\n\n\ndef test_run(monkeypatch: pytest.MonkeyPatch):\n    runned = False\n\n    def mock_run(*args, **kwargs):\n        nonlocal runned\n        runned = True\n        assert args == (\"arg\",)\n        assert kwargs == {\"kwarg\": \"kwarg\"}\n\n    driver = get_driver()\n\n    with monkeypatch.context() as m:\n        m.setattr(driver, \"run\", mock_run)\n        nonebot.run(\"arg\", kwarg=\"kwarg\")\n\n    assert runned\n\n\ndef test_get_bot(app: App, monkeypatch: pytest.MonkeyPatch):\n    driver = get_driver()\n\n    with pytest.raises(ValueError, match=\"no bots\"):\n        get_bot()\n\n    with monkeypatch.context() as m:\n        m.setattr(driver, \"_bots\", {\"test\": \"test\"})\n        assert get_bot() == \"test\"\n        assert get_bot(\"test\") == \"test\"\n        assert get_bots() == {\"test\": \"test\"}\n"
  },
  {
    "path": "tests/test_matcher/test_matcher.py",
    "content": "from pathlib import Path\nimport sys\n\nfrom nonebug import App\nimport pytest\n\nfrom nonebot import get_plugin\nfrom nonebot.matcher import Matcher, matchers\nfrom nonebot.message import _check_matcher, check_and_run_matcher\nfrom nonebot.permission import Permission, User\nfrom nonebot.rule import Rule\nfrom utils import FakeMessage, make_fake_event\n\n\ndef test_matcher_info(app: App):\n    from plugins.matcher.matcher_info import matcher\n\n    assert issubclass(matcher, Matcher)\n    assert matcher.type == \"message\"\n    assert matcher.priority == 1\n    assert matcher.temp is False\n    assert matcher.expire_time is None\n    assert matcher.block is True\n\n    assert matcher._source\n\n    assert matcher._source.module_name == \"plugins.matcher.matcher_info\"\n    assert matcher.module is sys.modules[\"plugins.matcher.matcher_info\"]\n    assert matcher.module_name == \"plugins.matcher.matcher_info\"\n\n    assert matcher._source.plugin_id == \"matcher:matcher_info\"\n    assert matcher._source.plugin_name == \"matcher_info\"\n    assert matcher.plugin is get_plugin(\"matcher:matcher_info\")\n    assert matcher.plugin_id == \"matcher:matcher_info\"\n    assert matcher.plugin_name == \"matcher_info\"\n\n    assert (\n        matcher._source.file\n        == (Path(__file__).parent.parent / \"plugins/matcher/matcher_info.py\").absolute()\n    )\n\n    assert matcher._source.lineno == 3\n\n\n@pytest.mark.anyio\nasync def test_matcher_check(app: App):\n    async def falsy():\n        return False\n\n    async def truthy():\n        return True\n\n    async def error():\n        raise RuntimeError\n\n    event = make_fake_event(_type=\"test\")()\n    with app.provider.context({}):\n        test_perm_falsy = Matcher.new(permission=Permission(falsy))\n        async with app.test_api() as ctx:\n            bot = ctx.create_bot()\n            assert await _check_matcher(test_perm_falsy, bot, event, {}) is False\n\n        test_perm_truthy = Matcher.new(permission=Permission(truthy))\n        async with app.test_api() as ctx:\n            bot = ctx.create_bot()\n            assert await _check_matcher(test_perm_truthy, bot, event, {}) is True\n\n        test_perm_error = Matcher.new(permission=Permission(error))\n        async with app.test_api() as ctx:\n            bot = ctx.create_bot()\n            assert await _check_matcher(test_perm_error, bot, event, {}) is False\n\n        test_rule_falsy = Matcher.new(rule=Rule(falsy))\n        async with app.test_api() as ctx:\n            bot = ctx.create_bot()\n            assert await _check_matcher(test_rule_falsy, bot, event, {}) is False\n\n        test_rule_truthy = Matcher.new(rule=Rule(truthy))\n        async with app.test_api() as ctx:\n            bot = ctx.create_bot()\n            assert await _check_matcher(test_rule_truthy, bot, event, {}) is True\n\n        test_rule_error = Matcher.new(rule=Rule(error))\n        async with app.test_api() as ctx:\n            bot = ctx.create_bot()\n            assert await _check_matcher(test_rule_error, bot, event, {}) is False\n\n\n@pytest.mark.anyio\nasync def test_matcher_handle(app: App):\n    from plugins.matcher.matcher_process import test_handle\n\n    message = FakeMessage(\"text\")\n    event = make_fake_event(_message=message)()\n\n    assert len(test_handle.handlers) == 1\n    async with app.test_matcher(test_handle) as ctx:\n        bot = ctx.create_bot()\n        ctx.receive_event(bot, event)\n        ctx.should_call_send(event, \"send\", \"result\", at_sender=True)\n        ctx.should_finished()\n\n\n@pytest.mark.anyio\nasync def test_matcher_got(app: App):\n    from plugins.matcher.matcher_process import test_got\n\n    message = FakeMessage(\"text\")\n    event = make_fake_event(_message=message)()\n    message_next = FakeMessage(\"text_next\")\n    event_next = make_fake_event(_message=message_next)()\n\n    assert len(test_got.handlers) == 1\n    async with app.test_matcher(test_got) as ctx:\n        bot = ctx.create_bot()\n        ctx.receive_event(bot, event)\n        ctx.should_call_send(event, \"prompt key1\", \"result1\")\n        ctx.receive_event(bot, event)\n        ctx.should_call_send(event, \"prompt key2\", \"result2\")\n        ctx.receive_event(bot, event)\n        ctx.should_call_send(event, \"reject\", \"result3\", at_sender=True)\n        ctx.should_rejected()\n        ctx.receive_event(bot, event_next)\n\n\n@pytest.mark.anyio\nasync def test_matcher_receive(app: App):\n    from plugins.matcher.matcher_process import test_receive\n\n    message = FakeMessage(\"text\")\n    event = make_fake_event(_message=message)()\n\n    assert len(test_receive.handlers) == 1\n    async with app.test_matcher(test_receive) as ctx:\n        bot = ctx.create_bot()\n        ctx.receive_event(bot, event)\n        ctx.receive_event(bot, event)\n        ctx.receive_event(bot, event)\n        ctx.should_call_send(event, \"pause\", \"result\", at_sender=True)\n        ctx.should_paused()\n\n\n@pytest.mark.anyio\nasync def test_matcher_combine(app: App):\n    from plugins.matcher.matcher_process import test_combine\n\n    message = FakeMessage(\"text\")\n    event = make_fake_event(_message=message)()\n    message_next = FakeMessage(\"text_next\")\n    event_next = make_fake_event(_message=message_next)()\n\n    assert len(test_combine.handlers) == 1\n    async with app.test_matcher(test_combine) as ctx:\n        bot = ctx.create_bot()\n        ctx.receive_event(bot, event)\n        ctx.receive_event(bot, event)\n        ctx.receive_event(bot, event)\n        ctx.should_rejected()\n        ctx.receive_event(bot, event_next)\n        ctx.should_rejected()\n        ctx.receive_event(bot, event_next)\n        ctx.should_rejected()\n        ctx.receive_event(bot, event_next)\n\n\n@pytest.mark.anyio\nasync def test_matcher_preset(app: App):\n    from plugins.matcher.matcher_process import test_preset\n\n    message = FakeMessage(\"text\")\n    event = make_fake_event(_message=message)()\n    message_next = FakeMessage(\"text_next\")\n    event_next = make_fake_event(_message=message_next)()\n\n    assert len(test_preset.handlers) == 2\n    async with app.test_matcher(test_preset) as ctx:\n        bot = ctx.create_bot()\n        ctx.receive_event(bot, event)\n        ctx.receive_event(bot, event)\n        ctx.should_rejected()\n        ctx.receive_event(bot, event_next)\n\n\n@pytest.mark.anyio\nasync def test_matcher_overload(app: App):\n    from plugins.matcher.matcher_process import test_overload\n\n    message = FakeMessage(\"text\")\n    event = make_fake_event(_message=message)()\n\n    assert len(test_overload.handlers) == 2\n    async with app.test_matcher(test_overload) as ctx:\n        bot = ctx.create_bot()\n        ctx.receive_event(bot, event)\n        ctx.should_finished()\n\n\n@pytest.mark.anyio\nasync def test_matcher_destroy(app: App):\n    from plugins.matcher.matcher_process import test_destroy\n\n    async with app.test_matcher(test_destroy):\n        assert len(matchers) == 1\n        assert len(matchers[test_destroy.priority]) == 1\n        assert matchers[test_destroy.priority][0] is test_destroy\n\n        test_destroy.destroy()\n\n        assert len(matchers[test_destroy.priority]) == 0\n\n\n@pytest.mark.anyio\nasync def test_type_updater(app: App):\n    from plugins.matcher.matcher_type import test_custom_updater, test_type_updater\n\n    event = make_fake_event()()\n\n    assert test_type_updater.type == \"test\"\n    async with app.test_api() as ctx:\n        bot = ctx.create_bot()\n        matcher = test_type_updater()\n        new_type = await matcher.update_type(bot, event)\n        assert new_type == \"message\"\n\n    assert test_custom_updater.type == \"test\"\n    async with app.test_api() as ctx:\n        bot = ctx.create_bot()\n        matcher = test_custom_updater()\n        new_type = await matcher.update_type(bot, event)\n        assert new_type == \"custom\"\n\n\n@pytest.mark.anyio\nasync def test_default_permission_updater(app: App):\n    from plugins.matcher.matcher_permission import (\n        default_permission,\n        test_permission_updater,\n    )\n\n    event = make_fake_event(_session_id=\"test\")()\n\n    assert test_permission_updater.permission is default_permission\n    async with app.test_api() as ctx:\n        bot = ctx.create_bot()\n        matcher = test_permission_updater()\n        new_perm = await matcher.update_permission(bot, event)\n        assert len(new_perm.checkers) == 1\n        checker = next(iter(new_perm.checkers)).call\n        assert isinstance(checker, User)\n        assert checker.users == (\"test\",)\n        assert checker.perm is default_permission\n\n\n@pytest.mark.anyio\nasync def test_user_permission_updater(app: App):\n    from plugins.matcher.matcher_permission import (\n        default_permission,\n        test_user_permission_updater,\n    )\n\n    event = make_fake_event(_session_id=\"test\")()\n    user_permission = next(iter(test_user_permission_updater.permission.checkers)).call\n    assert isinstance(user_permission, User)\n    assert user_permission.perm is default_permission\n    async with app.test_api() as ctx:\n        bot = ctx.create_bot()\n        matcher = test_user_permission_updater()\n        new_perm = await matcher.update_permission(bot, event)\n        assert len(new_perm.checkers) == 1\n        checker = next(iter(new_perm.checkers)).call\n        assert isinstance(checker, User)\n        assert checker.users == (\"test\",)\n        assert checker.perm is default_permission\n\n\n@pytest.mark.anyio\nasync def test_custom_permission_updater(app: App):\n    from plugins.matcher.matcher_permission import (\n        default_permission,\n        new_permission,\n        test_custom_updater,\n    )\n\n    event = make_fake_event(_session_id=\"test\")()\n    assert test_custom_updater.permission is default_permission\n    async with app.test_api() as ctx:\n        bot = ctx.create_bot()\n        matcher = test_custom_updater()\n        new_perm = await matcher.update_permission(bot, event)\n        assert new_perm is new_permission\n\n\n@pytest.mark.anyio\nasync def test_run(app: App):\n    with app.provider.context({}):\n        assert not matchers\n        event = make_fake_event()()\n\n        async def reject():\n            await Matcher.reject()\n\n        test_reject = Matcher.new(handlers=[reject])\n\n        async with app.test_api() as ctx:\n            bot = ctx.create_bot()\n            await test_reject().run(bot, event, {})\n            assert len(matchers[0]) == 1\n            assert len(matchers[0][0].handlers) == 1\n\n        del matchers[0]\n\n        async def pause():\n            await Matcher.pause()\n\n        test_pause = Matcher.new(handlers=[pause])\n\n        async with app.test_api() as ctx:\n            bot = ctx.create_bot()\n            await test_pause().run(bot, event, {})\n            assert len(matchers[0]) == 1\n            assert len(matchers[0][0].handlers) == 0\n\n\n@pytest.mark.anyio\nasync def test_temp(app: App):\n    from plugins.matcher.matcher_expire import test_temp_matcher\n\n    event = make_fake_event(_type=\"test\")()\n    with app.provider.context({test_temp_matcher.priority: [test_temp_matcher]}):\n        async with app.test_api() as ctx:\n            bot = ctx.create_bot()\n            assert test_temp_matcher in matchers[test_temp_matcher.priority]\n            await check_and_run_matcher(test_temp_matcher, bot, event, {})\n            assert test_temp_matcher not in matchers[test_temp_matcher.priority]\n\n\n@pytest.mark.anyio\nasync def test_datetime_expire(app: App):\n    from plugins.matcher.matcher_expire import test_datetime_matcher\n\n    event = make_fake_event()()\n    with app.provider.context(\n        {test_datetime_matcher.priority: [test_datetime_matcher]}\n    ):\n        async with app.test_matcher(test_datetime_matcher) as ctx:\n            bot = ctx.create_bot()\n            assert test_datetime_matcher in matchers[test_datetime_matcher.priority]\n            await check_and_run_matcher(test_datetime_matcher, bot, event, {})\n            assert test_datetime_matcher not in matchers[test_datetime_matcher.priority]\n\n\n@pytest.mark.anyio\nasync def test_timedelta_expire(app: App):\n    from plugins.matcher.matcher_expire import test_timedelta_matcher\n\n    event = make_fake_event()()\n    with app.provider.context(\n        {test_timedelta_matcher.priority: [test_timedelta_matcher]}\n    ):\n        async with app.test_api() as ctx:\n            bot = ctx.create_bot()\n            assert test_timedelta_matcher in matchers[test_timedelta_matcher.priority]\n            await check_and_run_matcher(test_timedelta_matcher, bot, event, {})\n            assert (\n                test_timedelta_matcher not in matchers[test_timedelta_matcher.priority]\n            )\n"
  },
  {
    "path": "tests/test_matcher/test_provider.py",
    "content": "from nonebug import App\n\nfrom nonebot.matcher import DEFAULT_PROVIDER_CLASS, matchers\n\n\ndef test_manager(app: App):\n    try:\n        default_provider = matchers.provider\n        matchers.set_provider(DEFAULT_PROVIDER_CLASS)\n        assert default_provider == matchers.provider\n    finally:\n        matchers.provider = app.provider\n"
  },
  {
    "path": "tests/test_param.py",
    "content": "from contextlib import suppress\nimport re\nimport sys\n\nfrom exceptiongroup import BaseExceptionGroup\nfrom nonebug import App\nimport pytest\n\nfrom nonebot.consts import (\n    ARG_KEY,\n    CMD_ARG_KEY,\n    CMD_KEY,\n    CMD_START_KEY,\n    CMD_WHITESPACE_KEY,\n    ENDSWITH_KEY,\n    FULLMATCH_KEY,\n    KEYWORD_KEY,\n    PREFIX_KEY,\n    RAW_CMD_KEY,\n    RECEIVE_KEY,\n    REGEX_MATCHED,\n    SHELL_ARGS,\n    SHELL_ARGV,\n    STARTSWITH_KEY,\n)\nfrom nonebot.dependencies import Dependent\nfrom nonebot.exception import PausedException, RejectedException, TypeMisMatch\nfrom nonebot.matcher import Matcher\nfrom nonebot.params import (\n    ArgParam,\n    BotParam,\n    DefaultParam,\n    DependParam,\n    EventParam,\n    ExceptionParam,\n    MatcherParam,\n    StateParam,\n)\nfrom utils import FakeMessage, make_fake_event\n\nUNKNOWN_PARAM = \"Unknown parameter\"\n\n\n@pytest.mark.anyio\n@pytest.mark.xfail(\n    ((3, 13) <= sys.version_info < (3, 13, 8))\n    or ((3, 14) <= sys.version_info < (3, 14, 1)),\n    reason=\"CPython Bug, see python/cpython#137317, python/cpython#137862\",\n)\nasync def test_depend(app: App):\n    from plugins.param.param_depend import (\n        ClassDependency,\n        annotated_class_depend,\n        annotated_depend,\n        annotated_multi_depend,\n        annotated_prior_depend,\n        cache_exception_func1,\n        cache_exception_func2,\n        class_depend,\n        depends,\n        runned,\n        sub_type_mismatch,\n        test_depends,\n        validate,\n        validate_fail,\n        validate_field,\n        validate_field_fail,\n    )\n\n    async with app.test_dependent(depends, allow_types=[DependParam]) as ctx:\n        ctx.should_return(1)\n\n    assert len(runned) == 1\n    assert runned[0] == 1\n\n    runned.clear()\n\n    async with app.test_matcher(test_depends) as ctx:\n        bot = ctx.create_bot()\n        event_next = make_fake_event()()\n        ctx.receive_event(bot, event_next)\n\n    assert runned == [1, 1]\n\n    runned.clear()\n\n    async with app.test_dependent(class_depend, allow_types=[DependParam]) as ctx:\n        ctx.should_return(ClassDependency(x=1, y=2))\n\n    async with app.test_dependent(annotated_depend, allow_types=[DependParam]) as ctx:\n        ctx.should_return(1)\n\n    async with app.test_dependent(\n        annotated_prior_depend, allow_types=[DependParam]\n    ) as ctx:\n        ctx.should_return(1)\n\n    async with app.test_dependent(\n        annotated_multi_depend, allow_types=[DependParam]\n    ) as ctx:\n        ctx.should_return(1)\n\n    assert runned == [1, 1, 1]\n\n    runned.clear()\n\n    async with app.test_dependent(\n        annotated_class_depend, allow_types=[DependParam]\n    ) as ctx:\n        ctx.should_return(ClassDependency(x=1, y=2))\n\n    with pytest.raises((TypeMisMatch, BaseExceptionGroup)) as exc_info:  # noqa: PT012\n        async with app.test_dependent(\n            sub_type_mismatch, allow_types=[DependParam, BotParam]\n        ) as ctx:\n            bot = ctx.create_bot()\n            ctx.pass_params(bot=bot)\n\n    if isinstance(exc_info.value, BaseExceptionGroup):\n        assert exc_info.group_contains(TypeMisMatch)\n\n    async with app.test_dependent(validate, allow_types=[DependParam]) as ctx:\n        ctx.should_return(1)\n\n    with pytest.raises((TypeMisMatch, BaseExceptionGroup)) as exc_info:\n        async with app.test_dependent(validate_fail, allow_types=[DependParam]) as ctx:\n            ...\n\n    if isinstance(exc_info.value, BaseExceptionGroup):\n        assert exc_info.group_contains(TypeMisMatch)\n\n    async with app.test_dependent(validate_field, allow_types=[DependParam]) as ctx:\n        ctx.should_return(1)\n\n    with pytest.raises((TypeMisMatch, BaseExceptionGroup)) as exc_info:\n        async with app.test_dependent(\n            validate_field_fail, allow_types=[DependParam]\n        ) as ctx:\n            ...\n\n    if isinstance(exc_info.value, BaseExceptionGroup):\n        assert exc_info.group_contains(TypeMisMatch)\n\n    # test cache reuse when exception raised\n    dependency_cache = {}\n    with pytest.raises((TypeMisMatch, BaseExceptionGroup)) as exc_info:\n        async with app.test_dependent(\n            cache_exception_func1, allow_types=[DependParam]\n        ) as ctx:\n            ctx.pass_params(dependency_cache=dependency_cache)\n\n    if isinstance(exc_info.value, BaseExceptionGroup):\n        assert exc_info.group_contains(TypeMisMatch)\n\n    # dependency solve tasks should be shielded even if one of them raises an exception\n    assert len(dependency_cache) == 2\n\n    async with app.test_dependent(\n        cache_exception_func2, allow_types=[DependParam]\n    ) as ctx:\n        ctx.pass_params(dependency_cache=dependency_cache)\n        ctx.should_return(1)\n\n\n@pytest.mark.anyio\n@pytest.mark.skipif(\n    sys.version_info < (3, 12), reason=\"TypeAlias requires Python 3.12 or higher\"\n)\nasync def test_aliased_depend(app: App):\n    from python_3_12.plugins.aliased_param.param_depend import aliased_depends, runned\n\n    async with app.test_dependent(aliased_depends, allow_types=[DependParam]) as ctx:\n        ctx.should_return(1)\n\n    assert len(runned) == 1\n    assert runned[0] == 1\n\n    runned.clear()\n\n\n@pytest.mark.anyio\nasync def test_bot(app: App):\n    from plugins.param.param_bot import (\n        FooBot,\n        generic_bot,\n        generic_bot_none,\n        get_bot,\n        legacy_bot,\n        not_bot,\n        not_legacy_bot,\n        postpone_bot,\n        sub_bot,\n        union_bot,\n    )\n\n    async with app.test_dependent(get_bot, allow_types=[BotParam]) as ctx:\n        bot = ctx.create_bot()\n        ctx.pass_params(bot=bot)\n        ctx.should_return(bot)\n\n    async with app.test_dependent(postpone_bot, allow_types=[BotParam]) as ctx:\n        bot = ctx.create_bot()\n        ctx.pass_params(bot=bot)\n        ctx.should_return(bot)\n\n    async with app.test_dependent(legacy_bot, allow_types=[BotParam]) as ctx:\n        bot = ctx.create_bot()\n        ctx.pass_params(bot=bot)\n        ctx.should_return(bot)\n\n    with pytest.raises(ValueError, match=UNKNOWN_PARAM):\n        app.test_dependent(not_legacy_bot, allow_types=[BotParam])\n\n    async with app.test_dependent(sub_bot, allow_types=[BotParam]) as ctx:\n        bot = ctx.create_bot(base=FooBot)\n        ctx.pass_params(bot=bot)\n        ctx.should_return(bot)\n\n    with pytest.raises((TypeMisMatch, BaseExceptionGroup)) as exc_info:  # noqa: PT012\n        async with app.test_dependent(sub_bot, allow_types=[BotParam]) as ctx:\n            bot = ctx.create_bot()\n            ctx.pass_params(bot=bot)\n\n    if isinstance(exc_info.value, BaseExceptionGroup):\n        assert exc_info.group_contains(TypeMisMatch)\n\n    async with app.test_dependent(union_bot, allow_types=[BotParam]) as ctx:\n        bot = ctx.create_bot(base=FooBot)\n        ctx.pass_params(bot=bot)\n        ctx.should_return(bot)\n\n    async with app.test_dependent(generic_bot, allow_types=[BotParam]) as ctx:\n        bot = ctx.create_bot()\n        ctx.pass_params(bot=bot)\n        ctx.should_return(bot)\n\n    async with app.test_dependent(generic_bot_none, allow_types=[BotParam]) as ctx:\n        bot = ctx.create_bot()\n        ctx.pass_params(bot=bot)\n        ctx.should_return(bot)\n\n    with pytest.raises(ValueError, match=UNKNOWN_PARAM):\n        app.test_dependent(not_bot, allow_types=[BotParam])\n\n\n@pytest.mark.anyio\n@pytest.mark.skipif(\n    sys.version_info < (3, 12), reason=\"TypeAlias requires Python 3.12 or higher\"\n)\nasync def test_aliased_bot(app: App):\n    from python_3_12.plugins.aliased_param.param_bot import get_aliased_bot\n\n    async with app.test_dependent(get_aliased_bot, allow_types=[BotParam]) as ctx:\n        bot = ctx.create_bot()\n        ctx.pass_params(bot=bot)\n        ctx.should_return(bot)\n\n\n@pytest.mark.anyio\nasync def test_event(app: App):\n    from plugins.param.param_event import (\n        FooEvent,\n        event,\n        event_message,\n        event_plain_text,\n        event_to_me,\n        event_type,\n        generic_event,\n        generic_event_none,\n        legacy_event,\n        not_event,\n        not_legacy_event,\n        postpone_event,\n        sub_event,\n        union_event,\n    )\n\n    fake_message = FakeMessage(\"text\")\n    fake_event = make_fake_event(_message=fake_message)()\n    fake_fooevent = make_fake_event(_base=FooEvent)()\n\n    async with app.test_dependent(event, allow_types=[EventParam]) as ctx:\n        ctx.pass_params(event=fake_event)\n        ctx.should_return(fake_event)\n\n    async with app.test_dependent(postpone_event, allow_types=[EventParam]) as ctx:\n        ctx.pass_params(event=fake_event)\n        ctx.should_return(fake_event)\n\n    async with app.test_dependent(legacy_event, allow_types=[EventParam]) as ctx:\n        ctx.pass_params(event=fake_event)\n        ctx.should_return(fake_event)\n\n    with pytest.raises(ValueError, match=UNKNOWN_PARAM):\n        app.test_dependent(not_legacy_event, allow_types=[EventParam])\n\n    async with app.test_dependent(sub_event, allow_types=[EventParam]) as ctx:\n        ctx.pass_params(event=fake_fooevent)\n        ctx.should_return(fake_fooevent)\n\n    with pytest.raises((TypeMisMatch, BaseExceptionGroup)) as exc_info:\n        async with app.test_dependent(sub_event, allow_types=[EventParam]) as ctx:\n            ctx.pass_params(event=fake_event)\n\n    if isinstance(exc_info.value, BaseExceptionGroup):\n        assert exc_info.group_contains(TypeMisMatch)\n\n    async with app.test_dependent(union_event, allow_types=[EventParam]) as ctx:\n        ctx.pass_params(event=fake_fooevent)\n        ctx.should_return(fake_fooevent)\n\n    async with app.test_dependent(generic_event, allow_types=[EventParam]) as ctx:\n        ctx.pass_params(event=fake_event)\n        ctx.should_return(fake_event)\n\n    async with app.test_dependent(generic_event_none, allow_types=[EventParam]) as ctx:\n        ctx.pass_params(event=fake_event)\n        ctx.should_return(fake_event)\n\n    with pytest.raises(ValueError, match=UNKNOWN_PARAM):\n        app.test_dependent(not_event, allow_types=[EventParam])\n\n    async with app.test_dependent(\n        event_type, allow_types=[EventParam, DependParam]\n    ) as ctx:\n        ctx.pass_params(event=fake_event)\n        ctx.should_return(fake_event.get_type())\n\n    async with app.test_dependent(\n        event_message, allow_types=[EventParam, DependParam]\n    ) as ctx:\n        ctx.pass_params(event=fake_event)\n        ctx.should_return(fake_event.get_message())\n\n    async with app.test_dependent(\n        event_plain_text, allow_types=[EventParam, DependParam]\n    ) as ctx:\n        ctx.pass_params(event=fake_event)\n        ctx.should_return(fake_event.get_plaintext())\n\n    async with app.test_dependent(\n        event_to_me, allow_types=[EventParam, DependParam]\n    ) as ctx:\n        ctx.pass_params(event=fake_event)\n        ctx.should_return(fake_event.is_tome())\n\n\n@pytest.mark.anyio\n@pytest.mark.skipif(\n    sys.version_info < (3, 12), reason=\"TypeAlias requires Python 3.12 or higher\"\n)\nasync def test_aliased_event(app: App):\n    from python_3_12.plugins.aliased_param.param_event import aliased_event\n\n    fake_message = FakeMessage(\"text\")\n    fake_event = make_fake_event(_message=fake_message)()\n\n    async with app.test_dependent(aliased_event, allow_types=[EventParam]) as ctx:\n        ctx.pass_params(event=fake_event)\n        ctx.should_return(fake_event)\n\n\n@pytest.mark.anyio\nasync def test_state(app: App):\n    from plugins.param.param_state import (\n        command,\n        command_arg,\n        command_start,\n        command_whitespace,\n        endswith,\n        fullmatch,\n        keyword,\n        legacy_state,\n        not_legacy_state,\n        postpone_state,\n        raw_command,\n        regex_dict,\n        regex_group,\n        regex_matched,\n        regex_str,\n        shell_command_args,\n        shell_command_argv,\n        startswith,\n        state,\n    )\n\n    fake_message = FakeMessage(\"text\")\n    fake_matched = re.match(r\"\\[cq:(?P<type>.*?),(?P<arg>.*?)\\]\", \"[cq:test,arg=value]\")\n    fake_state = {\n        PREFIX_KEY: {\n            CMD_KEY: (\"cmd\",),\n            RAW_CMD_KEY: \"/cmd\",\n            CMD_START_KEY: \"/\",\n            CMD_ARG_KEY: fake_message,\n            CMD_WHITESPACE_KEY: \" \",\n        },\n        SHELL_ARGV: [\"-h\"],\n        SHELL_ARGS: {\"help\": True},\n        REGEX_MATCHED: fake_matched,\n        STARTSWITH_KEY: \"startswith\",\n        ENDSWITH_KEY: \"endswith\",\n        FULLMATCH_KEY: \"fullmatch\",\n        KEYWORD_KEY: \"keyword\",\n    }\n\n    async with app.test_dependent(state, allow_types=[StateParam]) as ctx:\n        ctx.pass_params(state=fake_state)\n        ctx.should_return(fake_state)\n\n    async with app.test_dependent(postpone_state, allow_types=[StateParam]) as ctx:\n        ctx.pass_params(state=fake_state)\n        ctx.should_return(fake_state)\n\n    async with app.test_dependent(legacy_state, allow_types=[StateParam]) as ctx:\n        ctx.pass_params(state=fake_state)\n        ctx.should_return(fake_state)\n\n    with pytest.raises(ValueError, match=UNKNOWN_PARAM):\n        app.test_dependent(not_legacy_state, allow_types=[StateParam])\n\n    async with app.test_dependent(\n        command, allow_types=[StateParam, DependParam]\n    ) as ctx:\n        ctx.pass_params(state=fake_state)\n        ctx.should_return(fake_state[PREFIX_KEY][CMD_KEY])\n\n    async with app.test_dependent(\n        raw_command, allow_types=[StateParam, DependParam]\n    ) as ctx:\n        ctx.pass_params(state=fake_state)\n        ctx.should_return(fake_state[PREFIX_KEY][RAW_CMD_KEY])\n\n    async with app.test_dependent(\n        command_arg, allow_types=[StateParam, DependParam]\n    ) as ctx:\n        ctx.pass_params(state=fake_state)\n        ctx.should_return(fake_state[PREFIX_KEY][CMD_ARG_KEY])\n\n    async with app.test_dependent(\n        command_start, allow_types=[StateParam, DependParam]\n    ) as ctx:\n        ctx.pass_params(state=fake_state)\n        ctx.should_return(fake_state[PREFIX_KEY][CMD_START_KEY])\n\n    async with app.test_dependent(\n        command_whitespace, allow_types=[StateParam, DependParam]\n    ) as ctx:\n        ctx.pass_params(state=fake_state)\n        ctx.should_return(fake_state[PREFIX_KEY][CMD_WHITESPACE_KEY])\n\n    async with app.test_dependent(\n        shell_command_argv, allow_types=[StateParam, DependParam]\n    ) as ctx:\n        ctx.pass_params(state=fake_state)\n        ctx.should_return(fake_state[SHELL_ARGV])\n\n    async with app.test_dependent(\n        shell_command_args, allow_types=[StateParam, DependParam]\n    ) as ctx:\n        ctx.pass_params(state=fake_state)\n        ctx.should_return(fake_state[SHELL_ARGS])\n\n    async with app.test_dependent(\n        regex_matched, allow_types=[StateParam, DependParam]\n    ) as ctx:\n        ctx.pass_params(state=fake_state)\n        ctx.should_return(fake_state[REGEX_MATCHED])\n\n    async with app.test_dependent(\n        regex_str, allow_types=[StateParam, DependParam]\n    ) as ctx:\n        ctx.pass_params(state=fake_state)\n        ctx.should_return(\n            (\"[cq:test,arg=value]\", \"test\", \"arg=value\", (\"test\", \"arg=value\"))\n        )\n\n    async with app.test_dependent(\n        regex_group, allow_types=[StateParam, DependParam]\n    ) as ctx:\n        ctx.pass_params(state=fake_state)\n        ctx.should_return((\"test\", \"arg=value\"))\n\n    async with app.test_dependent(\n        regex_dict, allow_types=[StateParam, DependParam]\n    ) as ctx:\n        ctx.pass_params(state=fake_state)\n        ctx.should_return({\"type\": \"test\", \"arg\": \"arg=value\"})\n\n    async with app.test_dependent(\n        startswith, allow_types=[StateParam, DependParam]\n    ) as ctx:\n        ctx.pass_params(state=fake_state)\n        ctx.should_return(fake_state[STARTSWITH_KEY])\n\n    async with app.test_dependent(\n        endswith, allow_types=[StateParam, DependParam]\n    ) as ctx:\n        ctx.pass_params(state=fake_state)\n        ctx.should_return(fake_state[ENDSWITH_KEY])\n\n    async with app.test_dependent(\n        fullmatch, allow_types=[StateParam, DependParam]\n    ) as ctx:\n        ctx.pass_params(state=fake_state)\n        ctx.should_return(fake_state[FULLMATCH_KEY])\n\n    async with app.test_dependent(\n        keyword, allow_types=[StateParam, DependParam]\n    ) as ctx:\n        ctx.pass_params(state=fake_state)\n        ctx.should_return(fake_state[KEYWORD_KEY])\n\n\n@pytest.mark.anyio\n@pytest.mark.skipif(\n    sys.version_info < (3, 12), reason=\"TypeAlias requires Python 3.12 or higher\"\n)\nasync def test_aliased_state(app: App):\n    from python_3_12.plugins.aliased_param.param_state import aliased_state\n\n    fake_message = FakeMessage(\"text\")\n    fake_matched = re.match(r\"\\[cq:(?P<type>.*?),(?P<arg>.*?)\\]\", \"[cq:test,arg=value]\")\n    fake_state = {\n        PREFIX_KEY: {\n            CMD_KEY: (\"cmd\",),\n            RAW_CMD_KEY: \"/cmd\",\n            CMD_START_KEY: \"/\",\n            CMD_ARG_KEY: fake_message,\n            CMD_WHITESPACE_KEY: \" \",\n        },\n        SHELL_ARGV: [\"-h\"],\n        SHELL_ARGS: {\"help\": True},\n        REGEX_MATCHED: fake_matched,\n        STARTSWITH_KEY: \"startswith\",\n        ENDSWITH_KEY: \"endswith\",\n        FULLMATCH_KEY: \"fullmatch\",\n        KEYWORD_KEY: \"keyword\",\n    }\n\n    async with app.test_dependent(aliased_state, allow_types=[StateParam]) as ctx:\n        ctx.pass_params(state=fake_state)\n        ctx.should_return(fake_state)\n\n\n@pytest.mark.anyio\nasync def test_matcher(app: App):\n    from plugins.param.param_matcher import (\n        FooMatcher,\n        generic_matcher,\n        generic_matcher_none,\n        last_receive,\n        legacy_matcher,\n        matcher,\n        not_legacy_matcher,\n        not_matcher,\n        pause_prompt_result,\n        postpone_matcher,\n        receive,\n        receive_prompt_result,\n        sub_matcher,\n        union_matcher,\n    )\n\n    fake_matcher = Matcher()\n    foo_matcher = FooMatcher()\n\n    async with app.test_dependent(matcher, allow_types=[MatcherParam]) as ctx:\n        ctx.pass_params(matcher=fake_matcher)\n        ctx.should_return(fake_matcher)\n\n    async with app.test_dependent(postpone_matcher, allow_types=[MatcherParam]) as ctx:\n        ctx.pass_params(matcher=fake_matcher)\n        ctx.should_return(fake_matcher)\n\n    async with app.test_dependent(legacy_matcher, allow_types=[MatcherParam]) as ctx:\n        ctx.pass_params(matcher=fake_matcher)\n        ctx.should_return(fake_matcher)\n\n    with pytest.raises(ValueError, match=UNKNOWN_PARAM):\n        app.test_dependent(not_legacy_matcher, allow_types=[MatcherParam])\n\n    async with app.test_dependent(sub_matcher, allow_types=[MatcherParam]) as ctx:\n        ctx.pass_params(matcher=foo_matcher)\n        ctx.should_return(foo_matcher)\n\n    with pytest.raises((TypeMisMatch, BaseExceptionGroup)) as exc_info:\n        async with app.test_dependent(sub_matcher, allow_types=[MatcherParam]) as ctx:\n            ctx.pass_params(matcher=fake_matcher)\n\n    if isinstance(exc_info.value, BaseExceptionGroup):\n        assert exc_info.group_contains(TypeMisMatch)\n\n    async with app.test_dependent(union_matcher, allow_types=[MatcherParam]) as ctx:\n        ctx.pass_params(matcher=foo_matcher)\n        ctx.should_return(foo_matcher)\n\n    async with app.test_dependent(generic_matcher, allow_types=[MatcherParam]) as ctx:\n        ctx.pass_params(matcher=fake_matcher)\n        ctx.should_return(fake_matcher)\n\n    async with app.test_dependent(\n        generic_matcher_none, allow_types=[MatcherParam]\n    ) as ctx:\n        ctx.pass_params(matcher=fake_matcher)\n        ctx.should_return(fake_matcher)\n\n    with pytest.raises(ValueError, match=UNKNOWN_PARAM):\n        app.test_dependent(not_matcher, allow_types=[MatcherParam])\n\n    event = make_fake_event()()\n    fake_matcher.set_receive(\"test\", event)\n    event_next = make_fake_event()()\n    fake_matcher.set_receive(\"\", event_next)\n\n    async with app.test_dependent(\n        receive, allow_types=[MatcherParam, DependParam]\n    ) as ctx:\n        ctx.pass_params(matcher=fake_matcher)\n        ctx.should_return(event)\n\n    async with app.test_dependent(\n        last_receive, allow_types=[MatcherParam, DependParam]\n    ) as ctx:\n        ctx.pass_params(matcher=fake_matcher)\n        ctx.should_return(event_next)\n\n    fake_matcher.set_target(RECEIVE_KEY.format(id=\"test\"), cache=False)\n\n    async with app.test_api() as ctx:\n        bot = ctx.create_bot()\n        ctx.should_call_send(event, \"test\", result=True, bot=bot)\n        with fake_matcher.ensure_context(bot, event):\n            with suppress(RejectedException):\n                await fake_matcher.reject(\"test\")\n\n    async with app.test_dependent(\n        receive_prompt_result, allow_types=[MatcherParam, DependParam]\n    ) as ctx:\n        ctx.pass_params(matcher=fake_matcher)\n        ctx.should_return(True)\n\n    async with app.test_api() as ctx:\n        bot = ctx.create_bot()\n        ctx.should_call_send(event, \"test\", result=False, bot=bot)\n        with fake_matcher.ensure_context(bot, event):\n            fake_matcher.set_target(\"test\")\n            with suppress(PausedException):\n                await fake_matcher.pause(\"test\")\n\n    async with app.test_dependent(\n        pause_prompt_result, allow_types=[MatcherParam, DependParam]\n    ) as ctx:\n        ctx.pass_params(matcher=fake_matcher)\n        ctx.should_return(False)\n\n\n@pytest.mark.anyio\n@pytest.mark.skipif(\n    sys.version_info < (3, 12), reason=\"TypeAlias requires Python 3.12 or higher\"\n)\nasync def test_aliased_matcher(app: App):\n    from python_3_12.plugins.aliased_param.param_matcher import aliased_matcher\n\n    fake_matcher = Matcher()\n\n    async with app.test_dependent(aliased_matcher, allow_types=[MatcherParam]) as ctx:\n        ctx.pass_params(matcher=fake_matcher)\n        ctx.should_return(fake_matcher)\n\n\n@pytest.mark.anyio\nasync def test_arg(app: App):\n    from plugins.param.param_arg import (\n        annotated_arg,\n        annotated_arg_plain_text,\n        annotated_arg_prompt_result,\n        annotated_arg_str,\n        annotated_multi_arg,\n        annotated_prior_arg,\n        arg,\n        arg_plain_text,\n        arg_str,\n    )\n\n    matcher = Matcher()\n    event = make_fake_event()()\n    message = FakeMessage(\"text\")\n    matcher.set_arg(\"key\", message)\n\n    async with app.test_dependent(arg, allow_types=[ArgParam]) as ctx:\n        ctx.pass_params(matcher=matcher)\n        ctx.should_return(message)\n\n    async with app.test_dependent(arg_str, allow_types=[ArgParam]) as ctx:\n        ctx.pass_params(matcher=matcher)\n        ctx.should_return(str(message))\n\n    async with app.test_dependent(arg_plain_text, allow_types=[ArgParam]) as ctx:\n        ctx.pass_params(matcher=matcher)\n        ctx.should_return(message.extract_plain_text())\n\n    async with app.test_dependent(annotated_arg, allow_types=[ArgParam]) as ctx:\n        ctx.pass_params(matcher=matcher)\n        ctx.should_return(message)\n\n    async with app.test_dependent(annotated_arg_str, allow_types=[ArgParam]) as ctx:\n        ctx.pass_params(matcher=matcher)\n        ctx.should_return(str(message))\n\n    async with app.test_dependent(\n        annotated_arg_plain_text, allow_types=[ArgParam]\n    ) as ctx:\n        ctx.pass_params(matcher=matcher)\n        ctx.should_return(message.extract_plain_text())\n\n    matcher.set_target(ARG_KEY.format(key=\"key\"), cache=False)\n\n    async with app.test_api() as ctx:\n        bot = ctx.create_bot()\n        ctx.should_call_send(event, \"test\", result=\"arg\", bot=bot)\n        with matcher.ensure_context(bot, event):\n            with suppress(RejectedException):\n                await matcher.reject(\"test\")\n\n    async with app.test_dependent(\n        annotated_arg_prompt_result, allow_types=[ArgParam]\n    ) as ctx:\n        ctx.pass_params(matcher=matcher)\n        ctx.should_return(\"arg\")\n\n    async with app.test_dependent(annotated_multi_arg, allow_types=[ArgParam]) as ctx:\n        ctx.pass_params(matcher=matcher)\n        ctx.should_return(message.extract_plain_text())\n\n    async with app.test_dependent(annotated_prior_arg, allow_types=[ArgParam]) as ctx:\n        ctx.pass_params(matcher=matcher)\n        ctx.should_return(message.extract_plain_text())\n\n\n@pytest.mark.anyio\n@pytest.mark.skipif(\n    sys.version_info < (3, 12), reason=\"TypeAlias requires Python 3.12 or higher\"\n)\nasync def test_aliased_arg(app: App):\n    from python_3_12.plugins.aliased_param.param_arg import aliased_arg\n\n    matcher = Matcher()\n    message = FakeMessage(\"text\")\n    matcher.set_arg(\"key\", message)\n\n    async with app.test_dependent(aliased_arg, allow_types=[ArgParam]) as ctx:\n        ctx.pass_params(matcher=matcher)\n        ctx.should_return(message)\n\n\n@pytest.mark.anyio\nasync def test_exception(app: App):\n    from plugins.param.param_exception import exc, legacy_exc\n\n    exception = ValueError(\"test\")\n\n    async with app.test_dependent(exc, allow_types=[ExceptionParam]) as ctx:\n        ctx.pass_params(exception=exception)\n        ctx.should_return(exception)\n\n    async with app.test_dependent(legacy_exc, allow_types=[ExceptionParam]) as ctx:\n        ctx.pass_params(exception=exception)\n        ctx.should_return(exception)\n\n\n@pytest.mark.anyio\n@pytest.mark.skipif(\n    sys.version_info < (3, 12), reason=\"TypeAlias requires Python 3.12 or higher\"\n)\nasync def test_aliased_exception(app: App):\n    from python_3_12.plugins.aliased_param.param_exception import aliased_exc\n\n    exception = ValueError(\"test\")\n\n    async with app.test_dependent(aliased_exc, allow_types=[ExceptionParam]) as ctx:\n        ctx.pass_params(exception=exception)\n        ctx.should_return(exception)\n\n\n@pytest.mark.anyio\nasync def test_default(app: App):\n    from plugins.param.param_default import default\n\n    async with app.test_dependent(default, allow_types=[DefaultParam]) as ctx:\n        ctx.should_return(1)\n\n\ndef test_priority():\n    from plugins.param.priority import complex_priority\n\n    dependent = Dependent[None].parse(\n        call=complex_priority,\n        allow_types=[\n            DependParam,\n            BotParam,\n            EventParam,\n            StateParam,\n            MatcherParam,\n            ArgParam,\n            ExceptionParam,\n            DefaultParam,\n        ],\n    )\n    for param in dependent.params:\n        if param.name == \"sub\":\n            assert isinstance(param.field_info, DependParam)\n        elif param.name == \"bot\":\n            assert isinstance(param.field_info, BotParam)\n        elif param.name == \"event\":\n            assert isinstance(param.field_info, EventParam)\n        elif param.name == \"state\":\n            assert isinstance(param.field_info, StateParam)\n        elif param.name == \"matcher\":\n            assert isinstance(param.field_info, MatcherParam)\n        elif param.name == \"arg\":\n            assert isinstance(param.field_info, ArgParam)\n        elif param.name == \"exception\":\n            assert isinstance(param.field_info, ExceptionParam)\n        elif param.name == \"default\":\n            assert isinstance(param.field_info, DefaultParam)\n        else:\n            raise ValueError(f\"unknown param {param.name}\")\n"
  },
  {
    "path": "tests/test_permission.py",
    "content": "from nonebug import App\nimport pytest\n\nfrom nonebot.exception import SkippedException\nfrom nonebot.permission import (\n    MESSAGE,\n    METAEVENT,\n    NOTICE,\n    REQUEST,\n    SUPERUSER,\n    USER,\n    Message,\n    MetaEvent,\n    Notice,\n    Permission,\n    Request,\n    SuperUser,\n    User,\n)\nfrom utils import make_fake_event\n\n\n@pytest.mark.anyio\nasync def test_permission(app: App):\n    async def falsy():\n        return False\n\n    async def truthy():\n        return True\n\n    async def skipped() -> bool:\n        raise SkippedException\n\n    def _is_eq(a: Permission, b: Permission) -> bool:\n        return {d.call for d in a.checkers} == {d.call for d in b.checkers}\n\n    assert _is_eq(Permission(truthy) | None, Permission(truthy))\n    assert _is_eq(Permission(truthy) | falsy, Permission(truthy, falsy))\n    assert _is_eq(Permission(truthy) | Permission(falsy), Permission(truthy, falsy))\n\n    assert _is_eq(None | Permission(truthy), Permission(truthy))\n    assert _is_eq(truthy | Permission(falsy), Permission(truthy, falsy))\n\n    event = make_fake_event()()\n\n    async with app.test_api() as ctx:\n        bot = ctx.create_bot()\n        assert await Permission(falsy)(bot, event) is False\n        assert await Permission(truthy)(bot, event) is True\n        assert await Permission(skipped)(bot, event) is False\n        assert await Permission(truthy, falsy)(bot, event) is True\n        assert await Permission(truthy, skipped)(bot, event) is True\n\n\n@pytest.mark.anyio\n@pytest.mark.parametrize((\"type\", \"expected\"), [(\"message\", True), (\"notice\", False)])\nasync def test_message(type: str, expected: bool):\n    dependent = next(iter(MESSAGE.checkers))\n    checker = dependent.call\n\n    assert isinstance(checker, Message)\n\n    event = make_fake_event(_type=type)()\n    assert await dependent(event=event) == expected\n\n\n@pytest.mark.anyio\n@pytest.mark.parametrize((\"type\", \"expected\"), [(\"message\", False), (\"notice\", True)])\nasync def test_notice(type: str, expected: bool):\n    dependent = next(iter(NOTICE.checkers))\n    checker = dependent.call\n\n    assert isinstance(checker, Notice)\n\n    event = make_fake_event(_type=type)()\n    assert await dependent(event=event) == expected\n\n\n@pytest.mark.anyio\n@pytest.mark.parametrize((\"type\", \"expected\"), [(\"message\", False), (\"request\", True)])\nasync def test_request(type: str, expected: bool):\n    dependent = next(iter(REQUEST.checkers))\n    checker = dependent.call\n\n    assert isinstance(checker, Request)\n\n    event = make_fake_event(_type=type)()\n    assert await dependent(event=event) == expected\n\n\n@pytest.mark.anyio\n@pytest.mark.parametrize(\n    (\"type\", \"expected\"), [(\"message\", False), (\"meta_event\", True)]\n)\nasync def test_metaevent(type: str, expected: bool):\n    dependent = next(iter(METAEVENT.checkers))\n    checker = dependent.call\n\n    assert isinstance(checker, MetaEvent)\n\n    event = make_fake_event(_type=type)()\n    assert await dependent(event=event) == expected\n\n\n@pytest.mark.anyio\n@pytest.mark.parametrize(\n    (\"type\", \"user_id\", \"expected\"),\n    [\n        (\"message\", \"test\", True),\n        (\"message\", \"foo\", False),\n        (\"message\", \"faketest\", True),\n        (\"message\", None, False),\n        (\"notice\", \"test\", True),\n    ],\n)\nasync def test_superuser(app: App, type: str, user_id: str, expected: bool):\n    dependent = next(iter(SUPERUSER.checkers))\n    checker = dependent.call\n\n    assert isinstance(checker, SuperUser)\n\n    event = make_fake_event(_type=type, _user_id=user_id)()\n\n    async with app.test_api() as ctx:\n        bot = ctx.create_bot()\n        assert await dependent(bot=bot, event=event) == expected\n\n\n@pytest.mark.anyio\n@pytest.mark.parametrize(\n    (\"session_ids\", \"session_id\", \"expected\"),\n    [\n        ((\"user\", \"foo\"), \"user\", True),\n        ((\"user\", \"foo\"), \"bar\", False),\n        ((\"user\", \"foo\"), None, False),\n    ],\n)\nasync def test_user(\n    app: App, session_ids: tuple[str, ...], session_id: str | None, expected: bool\n):\n    dependent = next(iter(USER(*session_ids).checkers))\n    checker = dependent.call\n\n    assert isinstance(checker, User)\n\n    event = make_fake_event(_session_id=session_id)()\n\n    async with app.test_api() as ctx:\n        bot = ctx.create_bot()\n        assert await dependent(bot=bot, event=event) == expected\n"
  },
  {
    "path": "tests/test_plugin/test_get.py",
    "content": "from pydantic import BaseModel, Field\nimport pytest\n\nimport nonebot\nfrom nonebot.plugin import PluginManager, _managers\n\n\ndef test_get_plugin():\n    # check simple plugin\n    plugin = nonebot.get_plugin(\"export\")\n    assert plugin\n    assert plugin.id_ == \"export\"\n    assert plugin.name == \"export\"\n    assert plugin.module_name == \"plugins.export\"\n\n    # check sub plugin\n    plugin = nonebot.get_plugin(\"nested:nested_subplugin\")\n    assert plugin\n    assert plugin.id_ == \"nested:nested_subplugin\"\n    assert plugin.name == \"nested_subplugin\"\n    assert plugin.module_name == \"plugins.nested.plugins.nested_subplugin\"\n\n\ndef test_get_plugin_by_module_name():\n    # check get plugin by exact module name\n    plugin = nonebot.get_plugin_by_module_name(\"plugins.nested\")\n    assert plugin\n    assert plugin.id_ == \"nested\"\n    assert plugin.name == \"nested\"\n    assert plugin.module_name == \"plugins.nested\"\n\n    # check get plugin by sub module name\n    plugin = nonebot.get_plugin_by_module_name(\"plugins.nested.utils\")\n    assert plugin\n    assert plugin.id_ == \"nested\"\n    assert plugin.name == \"nested\"\n    assert plugin.module_name == \"plugins.nested\"\n\n    # check get plugin by sub plugin exact module name\n    plugin = nonebot.get_plugin_by_module_name(\n        \"plugins.nested.plugins.nested_subplugin\"\n    )\n    assert plugin\n    assert plugin.id_ == \"nested:nested_subplugin\"\n    assert plugin.name == \"nested_subplugin\"\n    assert plugin.module_name == \"plugins.nested.plugins.nested_subplugin\"\n\n\ndef test_get_available_plugin():\n    old_managers = _managers.copy()\n    _managers.clear()\n    try:\n        _managers.append(PluginManager([\"plugins.export\", \"plugin.require\"]))\n\n        # check get available plugins\n        plugin_ids = nonebot.get_available_plugin_names()\n        assert plugin_ids == {\"export\", \"require\"}\n    finally:\n        _managers.clear()\n        _managers.extend(old_managers)\n\n\ndef test_get_plugin_config():\n    class Config(BaseModel):\n        plugin_config: int\n\n    # check get plugin config\n    config = nonebot.get_plugin_config(Config)\n    assert isinstance(config, Config)\n    assert config.plugin_config == 1\n\n\ndef test_get_plugin_config_with_env(monkeypatch: pytest.MonkeyPatch):\n    monkeypatch.setenv(\"PLUGIN_CONFIG_ONE\", \"no_dummy_val\")\n    monkeypatch.setenv(\"PLUGIN_SUB_CONFIG__TWO\", \"two\")\n    monkeypatch.setenv(\"PLUGIN_CFG_THREE\", \"33\")\n    monkeypatch.setenv(\"CONFIG_FROM_INIT\", \"impossible\")\n\n    class SubConfig(BaseModel):\n        two: str = \"dummy_val\"\n\n    class Config(BaseModel):\n        plugin_config: int\n        plugin_config_one: str = \"dummy_val\"\n        plugin_sub_config: SubConfig = Field(default_factory=SubConfig)\n        plugin_config_three: int = Field(default=3, alias=\"plugin_cfg_three\")\n        config_from_init: str = \"dummy_val\"\n\n    config = nonebot.get_plugin_config(Config)\n    assert config.plugin_config == 1\n    assert config.plugin_config_one == \"no_dummy_val\"\n    assert config.plugin_sub_config.two == \"two\"\n    assert config.plugin_config_three == 33\n    assert config.config_from_init == \"init\"\n"
  },
  {
    "path": "tests/test_plugin/test_load.py",
    "content": "from collections.abc import Callable\nfrom dataclasses import asdict\nfrom functools import wraps\nfrom pathlib import Path\nimport sys\nfrom typing import TypeVar\nfrom typing_extensions import ParamSpec\n\nimport pytest\n\nimport nonebot\nfrom nonebot.plugin import (\n    Plugin,\n    PluginManager,\n    _managers,\n    _plugins,\n    inherit_supported_adapters,\n)\n\nP = ParamSpec(\"P\")\nR = TypeVar(\"R\")\n\n\ndef _recover(func: Callable[P, R]) -> Callable[P, R]:\n    @wraps(func)\n    def _wrapper(*args: P.args, **kwargs: P.kwargs) -> R:\n        origin_managers = _managers.copy()\n        origin_plugins = _plugins.copy()\n        try:\n            return func(*args, **kwargs)\n        finally:\n            _managers.clear()\n            _managers.extend(origin_managers)\n            _plugins.clear()\n            _plugins.update(origin_plugins)\n\n    return _wrapper\n\n\n@_recover\ndef test_load_plugin():\n    # check regular\n    assert nonebot.load_plugin(\"dynamic.simple\")\n\n    # check path\n    assert nonebot.load_plugin(Path(\"dynamic/path.py\"))\n\n    # check not found\n    assert nonebot.load_plugin(\"some_plugin_not_exist\") is None\n\n\ndef test_load_plugins(load_plugin: set[Plugin], load_builtin_plugin: set[Plugin]):\n    loaded_plugins = {\n        plugin for plugin in nonebot.get_loaded_plugins() if not plugin.parent_plugin\n    }\n    assert loaded_plugins >= load_plugin | load_builtin_plugin\n\n    # check simple plugin\n    assert \"plugins.export\" in sys.modules\n    assert \"plugin._hidden\" not in sys.modules\n\n    # check sub plugin\n    plugin = nonebot.get_plugin(\"nested:nested_subplugin\")\n    assert plugin\n    assert \"plugins.nested.plugins.nested_subplugin\" in sys.modules\n    assert plugin.parent_plugin is nonebot.get_plugin(\"nested\")\n\n    # check load again\n    with pytest.raises(RuntimeError):\n        PluginManager(plugins=[\"plugins.export\"]).load_all_plugins()\n    with pytest.raises(RuntimeError):\n        PluginManager(search_path=[\"plugins\"]).load_all_plugins()\n\n\ndef test_load_nested_plugin():\n    parent_plugin = nonebot.get_plugin(\"nested\")\n    sub_plugin = nonebot.get_plugin(\"nested:nested_subplugin\")\n    sub_plugin2 = nonebot.get_plugin(\"nested:nested_subplugin2\")\n    assert parent_plugin\n    assert sub_plugin\n    assert sub_plugin2\n    assert sub_plugin.parent_plugin is parent_plugin\n    assert sub_plugin2.parent_plugin is parent_plugin\n    assert parent_plugin.sub_plugins == {sub_plugin, sub_plugin2}\n\n\n@_recover\ndef test_load_json():\n    nonebot.load_from_json(\"./plugins.json\")\n\n    with pytest.raises(TypeError):\n        nonebot.load_from_json(\"./plugins.invalid.json\")\n\n\n@_recover\ndef test_load_toml():\n    nonebot.load_from_toml(\"./plugins.legacy.toml\")\n\n    nonebot.load_from_toml(\"./plugins.toml\")\n\n    with pytest.raises(ValueError, match=\"Cannot find\"):\n        nonebot.load_from_toml(\"./plugins.empty.toml\")\n\n    with pytest.raises(TypeError):\n        nonebot.load_from_toml(\"./plugins.invalid.toml\")\n\n\n@_recover\ndef test_bad_plugin():\n    nonebot.load_plugins(\"bad_plugins\")\n\n    assert nonebot.get_plugin(\"bad_plugin\") is None\n\n\n@_recover\ndef test_require_loaded(monkeypatch: pytest.MonkeyPatch):\n    def _patched_find(name: str):\n        pytest.fail(\"require existing plugin should not call find_manager_by_name\")\n\n    with monkeypatch.context() as m:\n        m.setattr(\"nonebot.plugin.load._find_manager_by_name\", _patched_find)\n\n        # require use module name\n        nonebot.require(\"plugins.export\")\n        # require use plugin id\n        nonebot.require(\"export\")\n        nonebot.require(\"nested:nested_subplugin\")\n\n\n@_recover\ndef test_require_not_loaded(monkeypatch: pytest.MonkeyPatch):\n    pm = PluginManager([\"dynamic.require_not_loaded\"], [\"dynamic/require_not_loaded/\"])\n    _managers.append(pm)\n    num_managers = len(_managers)\n\n    origin_load = PluginManager.load_plugin\n\n    def _patched_load(self: PluginManager, name: str):\n        assert self is pm\n        return origin_load(self, name)\n\n    with monkeypatch.context() as m:\n        m.setattr(PluginManager, \"load_plugin\", _patched_load)\n\n        # require standalone plugin\n        nonebot.require(\"dynamic.require_not_loaded\")\n        # require searched plugin\n        nonebot.require(\"dynamic.require_not_loaded.subplugin1\")\n        nonebot.require(\"require_not_loaded:subplugin2\")\n\n    assert len(_managers) == num_managers\n\n\n@_recover\ndef test_require_not_declared():\n    num_managers = len(_managers)\n\n    nonebot.require(\"dynamic.require_not_declared\")\n\n    assert len(_managers) == num_managers + 1\n    assert _managers[-1].plugins == {\"dynamic.require_not_declared\"}\n\n\n@_recover\ndef test_require_not_found():\n    with pytest.raises(RuntimeError):\n        nonebot.require(\"some_plugin_not_exist\")\n\n\ndef test_plugin_metadata():\n    from plugins.metadata import Config, FakeAdapter\n\n    plugin = nonebot.get_plugin(\"metadata\")\n    assert plugin\n    assert plugin.metadata\n    assert asdict(plugin.metadata) == {\n        \"name\": \"测试插件\",\n        \"description\": \"测试插件元信息\",\n        \"usage\": \"无法使用\",\n        \"type\": \"application\",\n        \"homepage\": \"https://nonebot.dev\",\n        \"config\": Config,\n        \"supported_adapters\": {\"~onebot.v11\", \"plugins.metadata:FakeAdapter\"},\n        \"extra\": {\"author\": \"NoneBot\"},\n    }\n\n    assert plugin.metadata.get_supported_adapters() == {FakeAdapter}\n\n\ndef test_inherit_supported_adapters_not_found():\n    with pytest.raises(RuntimeError):\n        inherit_supported_adapters(\"some_plugin_not_exist\")\n\n    with pytest.raises(ValueError, match=\"has no metadata!\"):\n        inherit_supported_adapters(\"export\")\n\n\n@pytest.mark.parametrize(\n    (\"inherit_plugins\", \"expected\"),\n    [\n        ((\"echo\",), None),\n        (\n            (\"metadata\",),\n            {\n                \"nonebot.adapters.onebot.v11\",\n                \"plugins.metadata:FakeAdapter\",\n            },\n        ),\n        (\n            (\"metadata_2\",),\n            {\n                \"nonebot.adapters.onebot.v11\",\n                \"nonebot.adapters.onebot.v12\",\n            },\n        ),\n        (\n            (\"metadata_3\",),\n            {\n                \"nonebot.adapters.onebot.v11\",\n                \"nonebot.adapters.onebot.v12\",\n                \"nonebot.adapters.qq\",\n            },\n        ),\n        (\n            (\"metadata\", \"metadata_2\"),\n            {\n                \"nonebot.adapters.onebot.v11\",\n            },\n        ),\n        (\n            (\"metadata\", \"metadata_3\"),\n            {\n                \"nonebot.adapters.onebot.v11\",\n            },\n        ),\n        (\n            (\"metadata_2\", \"metadata_3\"),\n            {\n                \"nonebot.adapters.onebot.v11\",\n                \"nonebot.adapters.onebot.v12\",\n            },\n        ),\n        (\n            (\"metadata\", \"metadata_2\", \"metadata_3\"),\n            {\n                \"nonebot.adapters.onebot.v11\",\n            },\n        ),\n        (\n            (\"metadata\", \"echo\"),\n            {\n                \"nonebot.adapters.onebot.v11\",\n                \"plugins.metadata:FakeAdapter\",\n            },\n        ),\n        (\n            (\"metadata\", \"metadata_2\", \"echo\"),\n            {\n                \"nonebot.adapters.onebot.v11\",\n            },\n        ),\n    ],\n)\ndef test_inherit_supported_adapters_combine(\n    inherit_plugins: tuple[str], expected: set[str]\n):\n    assert inherit_supported_adapters(*inherit_plugins) == expected\n"
  },
  {
    "path": "tests/test_plugin/test_manager.py",
    "content": "from nonebot.plugin import PluginManager, _managers\n\n\ndef test_load_plugin_name():\n    m = PluginManager(plugins=[\"dynamic.manager\"])\n    try:\n        _managers.append(m)\n\n        # load by plugin id\n        module1 = m.load_plugin(\"manager\")\n        # load by module name\n        module2 = m.load_plugin(\"dynamic.manager\")\n        assert module1\n        assert module2\n        assert module1 is module2\n    finally:\n        _managers.remove(m)\n"
  },
  {
    "path": "tests/test_plugin/test_on.py",
    "content": "from collections.abc import Callable\n\nimport pytest\n\nimport nonebot\nfrom nonebot.adapters import Event\nfrom nonebot.matcher import Matcher, matchers\nfrom nonebot.rule import (\n    CommandRule,\n    EndswithRule,\n    FullmatchRule,\n    IsTypeRule,\n    KeywordsRule,\n    RegexRule,\n    ShellCommandRule,\n    StartswithRule,\n)\nfrom nonebot.typing import T_RuleChecker\n\n\n@pytest.mark.parametrize(\n    (\"matcher_name\", \"pre_rule_factory\", \"has_permission\"),\n    [\n        pytest.param(\"matcher_on\", None, True),\n        pytest.param(\"matcher_on_metaevent\", None, False),\n        pytest.param(\"matcher_on_message\", None, True),\n        pytest.param(\"matcher_on_notice\", None, False),\n        pytest.param(\"matcher_on_request\", None, False),\n        pytest.param(\n            \"matcher_on_startswith\", lambda e: StartswithRule((\"test\",)), True\n        ),\n        pytest.param(\"matcher_on_endswith\", lambda e: EndswithRule((\"test\",)), True),\n        pytest.param(\"matcher_on_fullmatch\", lambda e: FullmatchRule((\"test\",)), True),\n        pytest.param(\"matcher_on_keyword\", lambda e: KeywordsRule(\"test\"), True),\n        pytest.param(\"matcher_on_command\", lambda e: CommandRule([(\"test\",)]), True),\n        pytest.param(\n            \"matcher_on_shell_command\",\n            lambda e: ShellCommandRule([(\"test\",)], None),\n            True,\n        ),\n        pytest.param(\"matcher_on_regex\", lambda e: RegexRule(\"test\"), True),\n        pytest.param(\"matcher_on_type\", lambda e: IsTypeRule(e), True),\n        pytest.param(\n            \"matcher_prefix_cmd\",\n            lambda e: CommandRule([(\"prefix\", \"sub\"), (\"help\",), (\"help\", \"foo\")]),\n            True,\n        ),\n        pytest.param(\n            \"matcher_prefix_shell_cmd\",\n            lambda e: ShellCommandRule(\n                [(\"prefix\", \"sub\"), (\"help\",), (\"help\", \"foo\")], None\n            ),\n            True,\n        ),\n        pytest.param(\n            \"matcher_prefix_aliases_cmd\",\n            lambda e: CommandRule(\n                [(\"prefix\", \"sub\"), (\"prefix\", \"help\"), (\"prefix\", \"help\", \"foo\")]\n            ),\n            True,\n        ),\n        pytest.param(\n            \"matcher_prefix_aliases_shell_cmd\",\n            lambda e: ShellCommandRule(\n                [(\"prefix\", \"sub\"), (\"prefix\", \"help\"), (\"prefix\", \"help\", \"foo\")], None\n            ),\n            True,\n        ),\n        pytest.param(\"matcher_group_on\", None, True),\n        pytest.param(\"matcher_group_on_metaevent\", None, False),\n        pytest.param(\"matcher_group_on_message\", None, True),\n        pytest.param(\"matcher_group_on_notice\", None, False),\n        pytest.param(\"matcher_group_on_request\", None, False),\n        pytest.param(\n            \"matcher_group_on_startswith\",\n            lambda e: StartswithRule((\"test\",)),\n            True,\n        ),\n        pytest.param(\n            \"matcher_group_on_endswith\",\n            lambda e: EndswithRule((\"test\",)),\n            True,\n        ),\n        pytest.param(\n            \"matcher_group_on_fullmatch\",\n            lambda e: FullmatchRule((\"test\",)),\n            True,\n        ),\n        pytest.param(\"matcher_group_on_keyword\", lambda e: KeywordsRule(\"test\"), True),\n        pytest.param(\n            \"matcher_group_on_command\",\n            lambda e: CommandRule([(\"test\",)]),\n            True,\n        ),\n        pytest.param(\n            \"matcher_group_on_shell_command\",\n            lambda e: ShellCommandRule([(\"test\",)], None),\n            True,\n        ),\n        pytest.param(\"matcher_group_on_regex\", lambda e: RegexRule(\"test\"), True),\n        pytest.param(\"matcher_group_on_type\", lambda e: IsTypeRule(e), True),\n    ],\n)\ndef test_on(\n    matcher_name: str,\n    pre_rule_factory: Callable[[type[Event]], T_RuleChecker] | None,\n    has_permission: bool,\n):\n    import plugins.plugin.matchers as module\n    from plugins.plugin.matchers import (\n        TestEvent,\n        expire_time,\n        handler,\n        permission,\n        priority,\n        rule,\n        state,\n    )\n\n    matcher = getattr(module, matcher_name)\n    assert issubclass(matcher, Matcher), f\"{matcher_name} should be a Matcher\"\n\n    pre_rule = pre_rule_factory(TestEvent) if pre_rule_factory else None\n\n    plugin = nonebot.get_plugin(\"plugin\")\n    assert plugin, \"plugin should be loaded\"\n\n    assert {dependent.call for dependent in matcher.rule.checkers} == (\n        {pre_rule, rule} if pre_rule else {rule}\n    )\n    if has_permission:\n        assert {dependent.call for dependent in matcher.permission.checkers} == {\n            permission\n        }\n    else:\n        assert not matcher.permission.checkers\n    assert [dependent.call for dependent in matcher.handlers] == [handler]\n    assert matcher.temp is True\n    assert matcher.expire_time == expire_time\n    assert matcher in matchers[priority]\n    assert matcher.block is True\n    assert matcher._default_state == state\n\n    assert matcher.plugin is plugin\n    assert matcher in plugin.matcher\n    assert matcher.module is module\n    assert matcher.plugin_id == \"plugin\"\n    assert matcher.plugin_name == \"plugin\"\n    assert matcher.module_name == \"plugins.plugin.matchers\"\n\n\ndef test_runtime_on():\n    import plugins.plugin.matchers as module\n    from plugins.plugin.matchers import matcher_on_factory\n\n    matcher = matcher_on_factory()\n\n    plugin = nonebot.get_plugin(\"plugin\")\n    assert plugin, \"plugin should be loaded\"\n\n    try:\n        assert matcher.plugin is plugin\n        assert matcher not in plugin.matcher\n        assert matcher.module is module\n        assert matcher.plugin_id == \"plugin\"\n        assert matcher.plugin_name == \"plugin\"\n        assert matcher.module_name == \"plugins.plugin.matchers\"\n    finally:\n        matcher.destroy()\n"
  },
  {
    "path": "tests/test_rule.py",
    "content": "import re\nfrom re import Match\n\nfrom nonebug import App\nimport pytest\n\nfrom nonebot.consts import (\n    CMD_ARG_KEY,\n    CMD_KEY,\n    CMD_WHITESPACE_KEY,\n    ENDSWITH_KEY,\n    FULLMATCH_KEY,\n    KEYWORD_KEY,\n    PREFIX_KEY,\n    REGEX_MATCHED,\n    SHELL_ARGS,\n    SHELL_ARGV,\n    STARTSWITH_KEY,\n)\nfrom nonebot.exception import ParserExit, SkippedException\nfrom nonebot.rule import (\n    CMD_RESULT,\n    TRIE_VALUE,\n    ArgumentParser,\n    CommandRule,\n    EndswithRule,\n    FullmatchRule,\n    IsTypeRule,\n    KeywordsRule,\n    Namespace,\n    RegexRule,\n    Rule,\n    ShellCommandRule,\n    StartswithRule,\n    ToMeRule,\n    TrieRule,\n    command,\n    endswith,\n    fullmatch,\n    is_type,\n    keyword,\n    regex,\n    shell_command,\n    startswith,\n    to_me,\n)\nfrom nonebot.typing import T_State\nfrom utils import FakeMessage, FakeMessageSegment, make_fake_event\n\n\n@pytest.mark.anyio\nasync def test_rule(app: App):\n    async def falsy():\n        return False\n\n    async def truthy():\n        return True\n\n    async def skipped() -> bool:\n        raise SkippedException\n\n    def _is_eq(a: Rule, b: Rule) -> bool:\n        return {d.call for d in a.checkers} == {d.call for d in b.checkers}\n\n    assert _is_eq(Rule(truthy) & None, Rule(truthy))\n    assert _is_eq(Rule(truthy) & falsy, Rule(truthy, falsy))\n    assert _is_eq(Rule(truthy) & Rule(falsy), Rule(truthy, falsy))\n\n    assert _is_eq(None & Rule(truthy), Rule(truthy))\n    assert _is_eq(truthy & Rule(falsy), Rule(truthy, falsy))\n\n    event = make_fake_event()()\n\n    async with app.test_api() as ctx:\n        bot = ctx.create_bot()\n        assert await Rule(falsy)(bot, event, {}) is False\n        assert await Rule(truthy)(bot, event, {}) is True\n        assert await Rule(skipped)(bot, event, {}) is False\n        assert await Rule(truthy, falsy)(bot, event, {}) is False\n        assert await Rule(truthy, skipped)(bot, event, {}) is False\n\n\n@pytest.mark.anyio\nasync def test_trie(app: App):\n    TrieRule.add_prefix(\"/fake-prefix\", TRIE_VALUE(\"/\", (\"fake-prefix\",)))\n\n    async with app.test_api() as ctx:\n        bot = ctx.create_bot()\n        message = FakeMessage(\"/fake-prefix some args\")\n        event = make_fake_event(_message=message)()\n        state = {}\n        TrieRule.get_value(bot, event, state)\n        assert state[PREFIX_KEY] == CMD_RESULT(\n            command=(\"fake-prefix\",),\n            raw_command=\"/fake-prefix\",\n            command_arg=FakeMessage(\"some args\"),\n            command_start=\"/\",\n            command_whitespace=\" \",\n        )\n\n        message = FakeMessageSegment.text(\"/fake-prefix \") + FakeMessageSegment.image(\n            \"fake url\"\n        )\n        event = make_fake_event(_message=message)()\n        state = {}\n        TrieRule.get_value(bot, event, state)\n        assert state[PREFIX_KEY] == CMD_RESULT(\n            command=(\"fake-prefix\",),\n            raw_command=\"/fake-prefix\",\n            command_arg=FakeMessage(FakeMessageSegment.image(\"fake url\")),\n            command_start=\"/\",\n            command_whitespace=\" \",\n        )\n\n        message = FakeMessageSegment.text(\"/fake-prefix \") + FakeMessageSegment.text(\n            \" some args\"\n        )\n        event = make_fake_event(_message=message)()\n        state = {}\n        TrieRule.get_value(bot, event, state)\n        assert state[PREFIX_KEY] == CMD_RESULT(\n            command=(\"fake-prefix\",),\n            raw_command=\"/fake-prefix\",\n            command_arg=FakeMessage(\"some args\"),\n            command_start=\"/\",\n            command_whitespace=\"  \",\n        )\n\n        message = (\n            FakeMessageSegment.text(\"/fake-prefix \")\n            + FakeMessageSegment.text(\"    \")\n            + FakeMessageSegment.text(\" some args\")\n        )\n        event = make_fake_event(_message=message)()\n        state = {}\n        TrieRule.get_value(bot, event, state)\n        assert state[PREFIX_KEY] == CMD_RESULT(\n            command=(\"fake-prefix\",),\n            raw_command=\"/fake-prefix\",\n            command_arg=FakeMessage(\"some args\"),\n            command_start=\"/\",\n            command_whitespace=\"      \",\n        )\n\n    del TrieRule.prefix[\"/fake-prefix\"]\n\n\n@pytest.mark.anyio\n@pytest.mark.parametrize(\n    (\"msg\", \"ignorecase\", \"type\", \"text\", \"expected\"),\n    [\n        (\"prefix\", False, \"message\", \"prefix_\", True),\n        (\"prefix\", False, \"message\", \"Prefix_\", False),\n        (\"prefix\", True, \"message\", \"prefix_\", True),\n        (\"prefix\", True, \"message\", \"Prefix_\", True),\n        (\"prefix\", False, \"message\", \"prefoo\", False),\n        (\"prefix\", False, \"message\", \"fooprefix\", False),\n        (\"prefix\", False, \"message\", None, False),\n        ((\"prefix\", \"foo\"), False, \"message\", \"fooprefix\", True),\n        (\"prefix\", False, \"notice\", \"prefix\", True),\n        (\"prefix\", False, \"notice\", \"foo\", False),\n    ],\n)\nasync def test_startswith(\n    msg: str | tuple[str, ...],\n    ignorecase: bool,\n    type: str,\n    text: str | None,\n    expected: bool,\n):\n    test_startswith = startswith(msg, ignorecase)\n    dependent = next(iter(test_startswith.checkers))\n    checker = dependent.call\n\n    msg = (msg,) if isinstance(msg, str) else msg\n\n    assert isinstance(checker, StartswithRule)\n    assert checker.msg == msg\n    assert checker.ignorecase == ignorecase\n\n    message = text if text is None else FakeMessage(text)\n    event = make_fake_event(_type=type, _message=message)()\n    for prefix in msg:\n        state = {STARTSWITH_KEY: prefix}\n        assert await dependent(event=event, state=state) == expected\n\n\n@pytest.mark.anyio\n@pytest.mark.parametrize(\n    (\"msg\", \"ignorecase\", \"type\", \"text\", \"expected\"),\n    [\n        (\"suffix\", False, \"message\", \"_suffix\", True),\n        (\"suffix\", False, \"message\", \"_Suffix\", False),\n        (\"suffix\", True, \"message\", \"_suffix\", True),\n        (\"suffix\", True, \"message\", \"_Suffix\", True),\n        (\"suffix\", False, \"message\", \"suffoo\", False),\n        (\"suffix\", False, \"message\", \"suffixfoo\", False),\n        (\"suffix\", False, \"message\", None, False),\n        ((\"suffix\", \"foo\"), False, \"message\", \"suffixfoo\", True),\n        (\"suffix\", False, \"notice\", \"suffix\", True),\n        (\"suffix\", False, \"notice\", \"foo\", False),\n    ],\n)\nasync def test_endswith(\n    msg: str | tuple[str, ...],\n    ignorecase: bool,\n    type: str,\n    text: str | None,\n    expected: bool,\n):\n    test_endswith = endswith(msg, ignorecase)\n    dependent = next(iter(test_endswith.checkers))\n    checker = dependent.call\n\n    msg = (msg,) if isinstance(msg, str) else msg\n\n    assert isinstance(checker, EndswithRule)\n    assert checker.msg == msg\n    assert checker.ignorecase == ignorecase\n\n    message = text if text is None else FakeMessage(text)\n    event = make_fake_event(_type=type, _message=message)()\n    for suffix in msg:\n        state = {ENDSWITH_KEY: suffix}\n        assert await dependent(event=event, state=state) == expected\n\n\n@pytest.mark.anyio\n@pytest.mark.parametrize(\n    (\"msg\", \"ignorecase\", \"type\", \"text\", \"expected\"),\n    [\n        (\"fullmatch\", False, \"message\", \"fullmatch\", True),\n        (\"fullmatch\", False, \"message\", \"Fullmatch\", False),\n        (\"fullmatch\", True, \"message\", \"fullmatch\", True),\n        (\"fullmatch\", True, \"message\", \"Fullmatch\", True),\n        (\"fullmatch\", False, \"message\", \"fullfoo\", False),\n        (\"fullmatch\", False, \"message\", \"_fullmatch_\", False),\n        (\"fullmatch\", False, \"message\", None, False),\n        ((\"fullmatch\", \"foo\"), False, \"message\", \"fullmatchfoo\", False),\n        (\"fullmatch\", False, \"notice\", \"fullmatch\", True),\n        (\"fullmatch\", False, \"notice\", \"foo\", False),\n    ],\n)\nasync def test_fullmatch(\n    msg: str | tuple[str, ...],\n    ignorecase: bool,\n    type: str,\n    text: str | None,\n    expected: bool,\n):\n    test_fullmatch = fullmatch(msg, ignorecase)\n    dependent = next(iter(test_fullmatch.checkers))\n    checker = dependent.call\n\n    msg = (msg,) if isinstance(msg, str) else msg\n\n    assert isinstance(checker, FullmatchRule)\n    assert checker.msg == msg\n    assert checker.ignorecase == ignorecase\n\n    message = text if text is None else FakeMessage(text)\n    event = make_fake_event(_type=type, _message=message)()\n    for full in msg:\n        state = {FULLMATCH_KEY: full}\n        assert await dependent(event=event, state=state) == expected\n\n\n@pytest.mark.anyio\n@pytest.mark.parametrize(\n    (\"kws\", \"type\", \"text\", \"expected\"),\n    [\n        ((\"key\",), \"message\", \"_key_\", True),\n        ((\"key\", \"foo\"), \"message\", \"_foo_\", True),\n        ((\"key\",), \"message\", None, False),\n        ((\"key\",), \"message\", \"foo\", False),\n        ((\"key\",), \"notice\", \"_key_\", True),\n        ((\"key\",), \"notice\", \"foo\", False),\n    ],\n)\nasync def test_keyword(\n    kws: tuple[str, ...],\n    type: str,\n    text: str | None,\n    expected: bool,\n):\n    test_keyword = keyword(*kws)\n    dependent = next(iter(test_keyword.checkers))\n    checker = dependent.call\n\n    assert isinstance(checker, KeywordsRule)\n    assert checker.keywords == kws\n\n    message = text if text is None else FakeMessage(text)\n    event = make_fake_event(_type=type, _message=message)()\n    for kw in kws:\n        state = {KEYWORD_KEY: kw}\n        assert await dependent(event=event, state=state) == expected\n\n\n@pytest.mark.anyio\n@pytest.mark.parametrize(\n    (\"cmds\", \"force_whitespace\", \"cmd\", \"whitespace\", \"arg_text\", \"expected\"),\n    [\n        # command tests\n        (((\"help\",),), None, (\"help\",), None, None, True),\n        (((\"help\",),), None, (\"foo\",), None, None, False),\n        (((\"help\", \"foo\"),), None, (\"help\", \"foo\"), None, None, True),\n        (((\"help\", \"foo\"),), None, (\"help\", \"bar\"), None, None, False),\n        (((\"help\",), (\"foo\",)), None, (\"help\",), None, None, True),\n        (((\"help\",), (\"foo\",)), None, (\"bar\",), None, None, False),\n        # whitespace tests\n        (((\"help\",),), True, (\"help\",), \" \", \"arg\", True),\n        (((\"help\",),), True, (\"help\",), None, \"arg\", False),\n        (((\"help\",),), True, (\"help\",), None, None, True),\n        (((\"help\",),), False, (\"help\",), \" \", \"arg\", False),\n        (((\"help\",),), False, (\"help\",), None, \"arg\", True),\n        (((\"help\",),), False, (\"help\",), None, None, True),\n        (((\"help\",),), \" \", (\"help\",), \" \", \"arg\", True),\n        (((\"help\",),), \" \", (\"help\",), \"\\n\", \"arg\", False),\n        (((\"help\",),), \" \", (\"help\",), None, \"arg\", False),\n        (((\"help\",),), \" \", (\"help\",), None, None, True),\n    ],\n)\nasync def test_command(\n    cmds: tuple[tuple[str, ...]],\n    force_whitespace: str | bool | None,\n    cmd: tuple[str, ...],\n    whitespace: str | None,\n    arg_text: str | None,\n    expected: bool,\n):\n    test_command = command(*cmds, force_whitespace=force_whitespace)\n    dependent = next(iter(test_command.checkers))\n    checker = dependent.call\n\n    assert isinstance(checker, CommandRule)\n    assert checker.cmds == cmds\n\n    arg = arg_text if arg_text is None else FakeMessage(arg_text)\n    state = {\n        PREFIX_KEY: {CMD_KEY: cmd, CMD_WHITESPACE_KEY: whitespace, CMD_ARG_KEY: arg}\n    }\n    assert await dependent(state=state) == expected\n\n\n@pytest.mark.anyio\nasync def test_shell_command():\n    state: T_State\n    CMD = (\"test\",)\n    Message = FakeMessage\n    MessageSegment = Message.get_segment_class()\n\n    test_not_cmd = shell_command(CMD)\n    dependent = next(iter(test_not_cmd.checkers))\n    checker = dependent.call\n    assert isinstance(checker, ShellCommandRule)\n    message = Message()\n    event = make_fake_event(_message=message)()\n    state = {PREFIX_KEY: {CMD_KEY: (\"not\",), CMD_ARG_KEY: message}}\n    assert not await dependent(event=event, state=state)\n\n    test_no_parser = shell_command(CMD)\n    dependent = next(iter(test_no_parser.checkers))\n    checker = dependent.call\n    assert isinstance(checker, ShellCommandRule)\n    message = Message()\n    event = make_fake_event(_message=message)()\n    state = {PREFIX_KEY: {CMD_KEY: CMD, CMD_ARG_KEY: message}}\n    assert await dependent(event=event, state=state)\n    assert state[SHELL_ARGV] == []\n    assert SHELL_ARGS not in state\n\n    test_lexical_error = shell_command(CMD)\n    dependent = next(iter(test_lexical_error.checkers))\n    checker = dependent.call\n    assert isinstance(checker, ShellCommandRule)\n    message = Message(\"-a '1\")\n    event = make_fake_event(_message=message)()\n    state = {PREFIX_KEY: {CMD_KEY: CMD, CMD_ARG_KEY: message}}\n    assert await dependent(event=event, state=state)\n    assert state[SHELL_ARGV] is None\n\n    parser = ArgumentParser(\"test\")\n    parser.add_argument(\"-a\", required=True)\n\n    test_lexical_error_with_parser = shell_command(CMD, parser=ArgumentParser(\"test\"))\n    dependent = next(iter(test_lexical_error_with_parser.checkers))\n    checker = dependent.call\n    assert isinstance(checker, ShellCommandRule)\n    message = Message(\"-a '1\")\n    event = make_fake_event(_message=message)()\n    state = {PREFIX_KEY: {CMD_KEY: CMD, CMD_ARG_KEY: message}}\n    assert await dependent(event=event, state=state)\n    assert state[SHELL_ARGV] is None\n    assert isinstance(state[SHELL_ARGS], ParserExit)\n    assert state[SHELL_ARGS].status != 0\n\n    test_simple_parser = shell_command(CMD, parser=parser)\n    dependent = next(iter(test_simple_parser.checkers))\n    checker = dependent.call\n    assert isinstance(checker, ShellCommandRule)\n    message = Message(\"-a 1\")\n    event = make_fake_event(_message=message)()\n    state = {PREFIX_KEY: {CMD_KEY: CMD, CMD_ARG_KEY: message}}\n    assert await dependent(event=event, state=state)\n    assert state[SHELL_ARGV] == [\"-a\", \"1\"]\n    assert state[SHELL_ARGS] == Namespace(a=\"1\")\n\n    test_parser_help = shell_command(CMD, parser=parser)\n    dependent = next(iter(test_parser_help.checkers))\n    checker = dependent.call\n    assert isinstance(checker, ShellCommandRule)\n    message = Message(\"-h\")\n    event = make_fake_event(_message=message)()\n    state = {PREFIX_KEY: {CMD_KEY: CMD, CMD_ARG_KEY: message}}\n    assert await dependent(event=event, state=state)\n    assert state[SHELL_ARGV] == [\"-h\"]\n    assert isinstance(state[SHELL_ARGS], ParserExit)\n    assert state[SHELL_ARGS].status == 0\n    assert state[SHELL_ARGS].message == parser.format_help()\n\n    test_parser_error = shell_command(CMD, parser=parser)\n    dependent = next(iter(test_parser_error.checkers))\n    checker = dependent.call\n    assert isinstance(checker, ShellCommandRule)\n    message = Message()\n    event = make_fake_event(_message=message)()\n    state = {PREFIX_KEY: {CMD_KEY: CMD, CMD_ARG_KEY: message}}\n    assert await dependent(event=event, state=state)\n    assert state[SHELL_ARGV] == []\n    assert isinstance(state[SHELL_ARGS], ParserExit)\n    assert state[SHELL_ARGS].status != 0\n    assert state[SHELL_ARGS].message.startswith(parser.format_usage() + \"test: error:\")\n\n    test_parser_remain_args = shell_command(CMD, parser=parser)\n    dependent = next(iter(test_parser_remain_args.checkers))\n    checker = dependent.call\n    assert isinstance(checker, ShellCommandRule)\n    message = MessageSegment.text(\"-a 1 2\") + MessageSegment.image(\"test\")\n    event = make_fake_event(_message=message)()\n    state = {PREFIX_KEY: {CMD_KEY: CMD, CMD_ARG_KEY: message}}\n    assert await dependent(event=event, state=state)\n    assert state[SHELL_ARGV] == [\"-a\", \"1\", \"2\", MessageSegment.image(\"test\")]\n    assert isinstance(state[SHELL_ARGS], ParserExit)\n    assert state[SHELL_ARGS].status != 0\n    assert state[SHELL_ARGS].message.startswith(parser.format_usage() + \"test: error:\")\n\n    test_message_parser = shell_command(CMD, parser=parser)\n    dependent = next(iter(test_message_parser.checkers))\n    checker = dependent.call\n    assert isinstance(checker, ShellCommandRule)\n    message = MessageSegment.text(\"-a\") + MessageSegment.image(\"test\")\n    event = make_fake_event(_message=message)()\n    state = {PREFIX_KEY: {CMD_KEY: CMD, CMD_ARG_KEY: message}}\n    assert await dependent(event=event, state=state)\n    assert state[SHELL_ARGV] == [\"-a\", MessageSegment.image(\"test\")]\n    assert state[SHELL_ARGS] == Namespace(a=MessageSegment.image(\"test\"))\n\n    parser = ArgumentParser(\"test\", exit_on_error=False)\n    parser.add_argument(\"-a\", required=True)\n\n    test_not_exit = shell_command(CMD, parser=parser)\n    dependent = next(iter(test_not_exit.checkers))\n    checker = dependent.call\n    assert isinstance(checker, ShellCommandRule)\n    message = Message()\n    event = make_fake_event(_message=message)()\n    state = {PREFIX_KEY: {CMD_KEY: CMD, CMD_ARG_KEY: message}}\n    assert await dependent(event=event, state=state)\n    assert state[SHELL_ARGV] == []\n    assert isinstance(state[SHELL_ARGS], ParserExit)\n    assert state[SHELL_ARGS].status != 0\n\n\n@pytest.mark.anyio\n@pytest.mark.parametrize(\n    (\"pattern\", \"type\", \"text\", \"expected\", \"matched\"),\n    [\n        (\n            r\"(?P<key>key\\d)\",\n            \"message\",\n            \"_key1_\",\n            True,\n            re.search(r\"(?P<key>key\\d)\", \"_key1_\"),\n        ),\n        (r\"foo\", \"message\", None, False, None),\n        (r\"foo\", \"notice\", \"foo\", True, re.search(r\"foo\", \"foo\")),\n        (r\"foo\", \"notice\", \"bar\", False, None),\n    ],\n)\nasync def test_regex(\n    pattern: str,\n    type: str,\n    text: str | None,\n    expected: bool,\n    matched: Match[str] | None,\n):\n    test_regex = regex(pattern)\n    dependent = next(iter(test_regex.checkers))\n    checker = dependent.call\n\n    assert isinstance(checker, RegexRule)\n    assert checker.regex == pattern\n\n    message = text if text is None else FakeMessage(text)\n    event = make_fake_event(_type=type, _message=message)()\n    state = {}\n    assert await dependent(event=event, state=state) == expected\n    result: Match[str] | None = state.get(REGEX_MATCHED)\n    if matched is None:\n        assert result is None\n    else:\n        assert isinstance(result, Match)\n        assert result.group() == matched.group()\n        assert result.span() == matched.span()\n\n\n@pytest.mark.anyio\n@pytest.mark.parametrize(\"expected\", [True, False])\nasync def test_to_me(expected: bool):\n    test_to_me = to_me()\n    dependent = next(iter(test_to_me.checkers))\n    checker = dependent.call\n\n    assert isinstance(checker, ToMeRule)\n\n    event = make_fake_event(_to_me=expected)()\n    assert await dependent(event=event) == expected\n\n\n@pytest.mark.anyio\nasync def test_is_type():\n    Event1 = make_fake_event()\n    Event2 = make_fake_event()\n    Event3 = make_fake_event()\n\n    test_type = is_type(Event1, Event2)\n    dependent = next(iter(test_type.checkers))\n    checker = dependent.call\n\n    assert isinstance(checker, IsTypeRule)\n\n    event = Event1()\n    assert await dependent(event=event)\n\n    event = Event3()\n    assert not await dependent(event=event)\n"
  },
  {
    "path": "tests/test_single_session.py",
    "content": "from contextlib import asynccontextmanager\n\nimport pytest\n\nfrom utils import make_fake_event\n\n\n@pytest.mark.anyio\nasync def test_matcher_mutex():\n    from nonebot.plugins.single_session import _running_matcher, matcher_mutex\n\n    am = asynccontextmanager(matcher_mutex)\n    event = make_fake_event()()\n    event_1 = make_fake_event()()\n    event_2 = make_fake_event(_session_id=\"test1\")()\n    event_3 = make_fake_event(_session_id=None)()\n\n    async with am(event) as ctx:\n        assert ctx is False\n    assert not _running_matcher\n\n    async with am(event) as ctx:\n        async with am(event_1) as ctx_1:\n            assert ctx is False\n            assert ctx_1 is True\n    assert not _running_matcher\n\n    async with am(event) as ctx:\n        async with am(event_2) as ctx_2:\n            assert ctx is False\n            assert ctx_2 is False\n    assert not _running_matcher\n\n    async with am(event_3) as ctx_3:\n        assert ctx_3 is False\n    assert not _running_matcher\n"
  },
  {
    "path": "tests/test_utils.py",
    "content": "import json\nfrom typing import ClassVar, Dict, List, Literal, TypeVar, Union  # noqa: UP035\n\nfrom nonebot.utils import (\n    DataclassEncoder,\n    escape_tag,\n    generic_check_issubclass,\n    is_async_gen_callable,\n    is_coroutine_callable,\n    is_gen_callable,\n)\nfrom utils import FakeMessage, FakeMessageSegment\n\n\ndef test_loguru_escape_tag():\n    assert escape_tag(\"<red>red</red>\") == r\"\\<red>red\\</red>\"\n    assert escape_tag(\"<fg #fff>white</fg #fff>\") == r\"\\<fg #fff>white\\</fg #fff>\"\n    assert escape_tag(\"<fg\\n#fff>white</fg\\n#fff>\") == \"\\\\<fg\\n#fff>white\\\\</fg\\n#fff>\"\n    assert escape_tag(\"<bg #fff>white</bg #fff>\") == r\"\\<bg #fff>white\\</bg #fff>\"\n    assert escape_tag(\"<bg\\n#fff>white</bg\\n#fff>\") == \"\\\\<bg\\n#fff>white\\\\</bg\\n#fff>\"\n\n\ndef test_generic_check_issubclass():\n    assert generic_check_issubclass(int, (int, float))\n    assert not generic_check_issubclass(str, (int, float))\n    assert generic_check_issubclass(Union[int, float, None], (int, float))  # noqa: UP007\n    assert generic_check_issubclass(int | float | None, (int, float))\n    assert generic_check_issubclass(Literal[1, 2, 3], int)\n    assert not generic_check_issubclass(Literal[1, 2, \"3\"], int)\n    assert generic_check_issubclass(List[int], list)  # noqa: UP006\n    assert generic_check_issubclass(Dict[str, int], dict)  # noqa: UP006\n    assert generic_check_issubclass(list[int], list)\n    assert generic_check_issubclass(dict[str, int], dict)\n    assert not generic_check_issubclass(ClassVar[int], int)\n    assert generic_check_issubclass(TypeVar(\"T\", int, float), (int, float))\n    assert generic_check_issubclass(TypeVar(\"T\", bound=int), (int, float))\n\n\ndef test_is_coroutine_callable():\n    async def test1(): ...\n\n    def test2(): ...\n\n    class TestClass1:\n        async def __call__(self): ...\n\n    class TestClass2:\n        def __call__(self): ...\n\n    assert is_coroutine_callable(test1)\n    assert not is_coroutine_callable(test2)\n    assert not is_coroutine_callable(TestClass1)\n    assert is_coroutine_callable(TestClass1())\n    assert not is_coroutine_callable(TestClass2)\n\n\ndef test_is_gen_callable():\n    def test1():\n        yield\n\n    async def test2():\n        yield\n\n    def test3(): ...\n\n    class TestClass1:\n        def __call__(self):\n            yield\n\n    class TestClass2:\n        async def __call__(self):\n            yield\n\n    class TestClass3:\n        def __call__(self): ...\n\n    assert is_gen_callable(test1)\n    assert not is_gen_callable(test2)\n    assert not is_gen_callable(test3)\n    assert is_gen_callable(TestClass1())\n    assert not is_gen_callable(TestClass2())\n    assert not is_gen_callable(TestClass3())\n\n\ndef test_is_async_gen_callable():\n    async def test1():\n        yield\n\n    def test2():\n        yield\n\n    async def test3(): ...\n\n    class TestClass1:\n        async def __call__(self):\n            yield\n\n    class TestClass2:\n        def __call__(self):\n            yield\n\n    class TestClass3:\n        async def __call__(self): ...\n\n    assert is_async_gen_callable(test1)\n    assert not is_async_gen_callable(test2)\n    assert not is_async_gen_callable(test3)\n    assert is_async_gen_callable(TestClass1())\n    assert not is_async_gen_callable(TestClass2())\n    assert not is_async_gen_callable(TestClass3())\n\n\ndef test_dataclass_encoder():\n    simple = json.dumps(\"123\", cls=DataclassEncoder)\n    assert simple == '\"123\"'\n\n    ms = FakeMessageSegment.nested(FakeMessage(FakeMessageSegment.text(\"text\")))\n    s = json.dumps(ms, cls=DataclassEncoder)\n    assert s == (\n        \"{\"\n        '\"type\": \"node\", '\n        '\"data\": {\"content\": [{\"type\": \"text\", \"data\": {\"text\": \"text\"}}]}'\n        \"}\"\n    )\n"
  },
  {
    "path": "tests/utils.py",
    "content": "from collections.abc import Iterable, Mapping\nfrom typing_extensions import override\n\nfrom pydantic import create_model\n\nfrom nonebot.adapters import Adapter, Bot, Event, Message, MessageSegment\n\n\ndef escape_text(s: str, *, escape_comma: bool = True) -> str:\n    s = s.replace(\"&\", \"&amp;\").replace(\"[\", \"&#91;\").replace(\"]\", \"&#93;\")\n    if escape_comma:\n        s = s.replace(\",\", \"&#44;\")\n    return s\n\n\nclass FakeAdapter(Adapter):\n    @classmethod\n    @override\n    def get_name(cls) -> str:\n        return \"fake\"\n\n    @override\n    async def _call_api(self, bot: Bot, api: str, **data):\n        raise NotImplementedError\n\n\nclass FakeMessageSegment(MessageSegment[\"FakeMessage\"]):\n    @classmethod\n    @override\n    def get_message_class(cls):\n        return FakeMessage\n\n    @override\n    def __str__(self) -> str:\n        return self.data[\"text\"] if self.type == \"text\" else f\"[fake:{self.type}]\"\n\n    @classmethod\n    def text(cls, text: str):\n        return cls(\"text\", {\"text\": text})\n\n    @staticmethod\n    def image(url: str):\n        return FakeMessageSegment(\"image\", {\"url\": url})\n\n    @staticmethod\n    def nested(content: \"FakeMessage\"):\n        return FakeMessageSegment(\"node\", {\"content\": content})\n\n    @override\n    def is_text(self) -> bool:\n        return self.type == \"text\"\n\n\nclass FakeMessage(Message[FakeMessageSegment]):\n    @classmethod\n    @override\n    def get_segment_class(cls):\n        return FakeMessageSegment\n\n    @staticmethod\n    @override\n    def _construct(msg: str | Iterable[Mapping]):\n        if isinstance(msg, str):\n            yield FakeMessageSegment.text(msg)\n        else:\n            for seg in msg:\n                yield FakeMessageSegment(**seg)\n        return\n\n    @override\n    def __add__(self, other: str | FakeMessageSegment | Iterable[FakeMessageSegment]):\n        other = escape_text(other) if isinstance(other, str) else other\n        return super().__add__(other)\n\n\ndef make_fake_event(\n    _base: type[Event] | None = None,\n    _type: str = \"message\",\n    _name: str = \"test\",\n    _description: str = \"test\",\n    _user_id: str | None = \"test\",\n    _session_id: str | None = \"test\",\n    _message: Message | None = None,\n    _to_me: bool = True,\n    **fields,\n) -> type[Event]:\n    Base = _base or Event\n\n    class FakeEvent(Base):\n        @override\n        def get_type(self) -> str:\n            return _type\n\n        @override\n        def get_event_name(self) -> str:\n            return _name\n\n        @override\n        def get_event_description(self) -> str:\n            return _description\n\n        @override\n        def get_user_id(self) -> str:\n            if _user_id is not None:\n                return _user_id\n            raise NotImplementedError\n\n        @override\n        def get_session_id(self) -> str:\n            if _session_id is not None:\n                return _session_id\n            raise NotImplementedError\n\n        @override\n        def get_message(self) -> \"Message\":\n            if _message is not None:\n                return _message\n            raise NotImplementedError\n\n        @override\n        def is_tome(self) -> bool:\n            return _to_me\n\n    return create_model(\"FakeEvent\", __base__=FakeEvent, **fields)\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"lib\": [\"ESNext\"],\n    \"module\": \"NodeNext\",\n    \"declaration\": true,\n    \"declarationMap\": false,\n    \"sourceMap\": false,\n    \"jsx\": \"react-native\",\n    \"noEmit\": true,\n\n    /* Strict Type-Checking Options */\n    \"strict\": true,\n    \"strictNullChecks\": true,\n    \"strictFunctionTypes\": true,\n    \"strictBindCallApply\": true,\n    \"strictPropertyInitialization\": true,\n    \"noImplicitThis\": true,\n    \"alwaysStrict\": true,\n\n    /* Additional Checks */\n    // \"noUnusedLocals\": false, // ensured by eslint, should not block compilation\n    // \"noImplicitReturns\": true,\n    // \"noFallthroughCasesInSwitch\": true,\n\n    /* Disabled on purpose (handled by ESLint, should not block compilation) */\n    \"noUnusedParameters\": false,\n\n    /* Module Resolution Options */\n    \"moduleResolution\": \"nodenext\",\n    \"allowSyntheticDefaultImports\": true,\n    \"esModuleInterop\": true,\n    \"isolatedModules\": true,\n\n    /* Advanced Options */\n    \"resolveJsonModule\": true,\n    \"skipLibCheck\": true, // @types/webpack and webpack/types.d.ts are not the same thing\n\n    /* Use tslib */\n    \"importHelpers\": true,\n    \"noEmitHelpers\": true\n  },\n  \"include\": [\"./**/.eslintrc.js\", \"./**/.stylelintrc.js\"],\n  \"exclude\": [\"node_modules\", \"**/lib/**/*\"]\n}\n"
  },
  {
    "path": "website/docs/README.md",
    "content": "---\nsidebar_position: 0\nid: index\nslug: /\n---\n\n# 概览\n\nNoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架（下称 NoneBot），它基于 Python 的类型注解和异步优先特性（兼容同步），能够为你的需求实现提供便捷灵活的支持。同时，NoneBot 拥有大量的开发者为其开发插件，用户无需编写任何代码，仅需完成环境配置及插件安装，就可以正常使用 NoneBot。\n\n需要注意的是，NoneBot 仅支持 **Python 3.9 以上版本**\n\n## 特色\n\n### 异步优先\n\nNoneBot 基于 Python [asyncio](https://docs.python.org/zh-cn/3/library/asyncio.html) / [trio](https://trio.readthedocs.io/en/stable/) 编写，并在异步机制的基础上进行了一定程度的同步函数兼容。\n\n### 完整的类型注解\n\nNoneBot 参考 [PEP 484](https://www.python.org/dev/peps/pep-0484/) 等 PEP 完整实现了类型注解，通过 Pyright（Pylance） 检查。配合编辑器的类型推导功能，能将绝大多数的 Bug 杜绝在编辑器中（[编辑器支持](./editor-support)）。\n\n### 开箱即用\n\nNoneBot 提供了使用便捷、具有交互式功能的命令行工具--`nb-cli`，使得用户初次接触 NoneBot 时更容易上手。使用方法请阅读本文档[指南](./quick-start.mdx)以及 [CLI 文档](https://cli.nonebot.dev/)。\n\n### 插件系统\n\n插件系统是 NoneBot 的核心，通过它可以实现机器人的模块化以及功能扩展，便于维护和管理。\n\n### 依赖注入系统\n\nNoneBot 采用了一套自行定义的依赖注入系统，可以让事件的处理过程更加的简洁、清晰，增加代码的可读性，减少代码冗余。\n\n#### 什么是依赖注入\n\n[**『依赖注入』**](https://zh.wikipedia.org/wiki/%E6%8E%A7%E5%88%B6%E5%8F%8D%E8%BD%AC)意思是，在编程中，有一种方法可以让你的代码声明它工作和使用所需要的东西，即**『依赖』**。\n\n系统（在这里是指 NoneBot）将负责做任何需要的事情，为你的代码提供这些必要依赖（即**『注入』**依赖性）\n\n这在你有以下情形的需求时非常有用：\n\n- 这部分代码拥有共享的逻辑（同样的代码逻辑多次重复）\n- 共享数据库以及网络请求连接会话\n  - 比如 `httpx.AsyncClient`、`aiohttp.ClientSession` 和 `sqlalchemy.Session`\n- 机器人用户权限检查以及认证\n- 还有更多...\n\n它在完成上述工作的同时，还能尽量减少代码的耦合和重复\n"
  },
  {
    "path": "website/docs/advanced/adapter.md",
    "content": "---\nsidebar_position: 1\ndescription: 注册适配器与指定平台交互\n\noptions:\n  menu:\n    - category: advanced\n      weight: 20\n---\n\n# 使用适配器\n\n适配器 (Adapter) 是机器人与平台交互的核心桥梁，它负责在驱动器和机器人插件之间转换与传递消息。\n\n## 适配器功能与组成\n\n适配器通常有两种功能，分别是**接收事件**和**调用平台接口**。其中，接收事件是指将驱动器收到的事件消息转换为 NoneBot 定义的事件模型，然后交由机器人插件处理；调用平台接口是指将机器人插件调用平台接口的数据转换为平台指定的格式，然后交由驱动器发送，并接收接口返回数据。\n\n为了实现这两种功能，适配器通常由四个部分组成：\n\n- **Adapter**：负责转换事件和调用接口，正确创建 Bot 对象并注册到 NoneBot 中。\n- **Bot**：负责存储平台机器人相关信息，并提供回复事件的方法。\n- **Event**：负责定义事件内容，以及事件主体对象。\n- **Message**：负责正确序列化消息，以便机器人插件处理。\n\n## 注册适配器\n\n在使用适配器之前，我们需要先将适配器注册到驱动器中，这样适配器就可以通过驱动器接收事件和调用接口了。我们以 Console 适配器为例，来看看如何注册适配器：\n\n```python {2,5} title=bot.py\nimport nonebot\nfrom nonebot.adapters.console import Adapter\n\ndriver = nonebot.get_driver()\ndriver.register_adapter(Adapter)\n```\n\n我们首先需要从适配器模块中导入所需要的适配器类，然后通过驱动器的 `register_adapter` 方法将适配器注册到驱动器中即可。如果我们需要多平台支持，可以多次调用 `register_adapter` 方法来注册多个适配器。\n\n## 获取已注册的适配器\n\nNoneBot 提供了 `get_adapter` 方法来获取已注册的适配器，我们可以通过适配器的名称或类型来获取指定的适配器实例：\n\n```python\nimport nonebot\nfrom nonebot.adapters.console import Adapter\n\nadapters = nonebot.get_adapters()\nconsole_adapter = nonebot.get_adapter(Adapter)\nconsole_adapter = nonebot.get_adapter(Adapter.get_name())\n```\n\n## 获取 Bot 对象\n\n当前所有适配器已连接的 Bot 对象可以通过 `get_bots` 方法获取，这是一个以机器人 ID 为键的字典：\n\n```python\nimport nonebot\n\nbots = nonebot.get_bots()\n```\n\n我们也可以通过 `get_bot` 方法获取指定 ID 的 Bot 对象。如果省略 ID 参数，将会返回所有 Bot 中的第一个：\n\n```python\nimport nonebot\n\nbot = nonebot.get_bot(\"bot_id\")\n```\n\n如果需要获取指定适配器连接的 Bot 对象，我们可以通过适配器的 `bots` 属性获取，这也是一个以机器人 ID 为键的字典：\n\n```python\nimport nonebot\nfrom nonebot.adapters.console import Adapter\n\nconsole_adapter = nonebot.get_adapter(Adapter)\nbots = console_adapter.bots\n```\n\nBot 对象都具有一个 `self_id` 属性，它是机器人的唯一 ID，由适配器填写，通常为机器人的帐号 ID 或者 APP ID。\n\n## 获取事件通用信息\n\n适配器的所有事件模型均继承自 `Event` 基类，在[事件类型与重载](../appendices/overload.md)一节中，我们也提到了如何使用基类抽象方法来获取事件通用信息。基类能提供如下信息：\n\n### 事件类型\n\n事件类型通常为 `meta_event`、`message`、`notice`、`request`。\n\n```python\ntype: str = event.get_type()\n```\n\n### 事件名称\n\n事件名称由适配器定义，通常用于日志记录。\n\n```python\nname: str = event.get_event_name()\n```\n\n### 事件描述\n\n事件描述由适配器定义，通常用于日志记录。\n\n```python\ndescription: str = event.get_event_description()\n```\n\n### 事件日志字符串\n\n事件日志字符串由事件名称和事件描述组成，用于日志记录。\n\n```python\nlog: str = event.get_log_string()\n```\n\n### 事件主体 ID\n\n事件主体 ID 通常为机器人用户 ID。\n\n```python\nuser_id: str = event.get_user_id()\n```\n\n### 事件会话 ID\n\n事件会话 ID 通常为机器人用户 ID 与群聊/频道 ID 组合而成。\n\n```python\nsession_id: str = event.get_session_id()\n```\n\n### 事件消息\n\n如果事件包含消息，则可以通过该方法获取，否则会产生异常。\n\n```python\nmessage: Message = event.get_message()\n```\n\n### 事件纯文本消息\n\n通常为事件消息的纯文本内容，如果事件不包含消息，则会产生异常。\n\n```python\ntext: str = event.get_plaintext()\n```\n\n### 事件是否与机器人有关\n\n由适配器实现的判断，通常将事件目标主体为机器人、消息中包含“@机器人”或以“机器人的昵称”开始视为与机器人有关。\n\n```python\nis_tome: bool = event.is_tome()\n```\n\n## 更多\n\n官方支持的适配器和社区贡献的适配器均可在[商店](/store/adapters)中查看。如果你想要开发自己的适配器，可以参考[开发文档](../developer/adapter-writing.md)。欢迎通过商店发布你的适配器。\n"
  },
  {
    "path": "website/docs/advanced/dependency.mdx",
    "content": "---\nsidebar_position: 6\ndescription: 通过依赖注入获取上下文信息\n\noptions:\n  menu:\n    - category: advanced\n      weight: 70\n---\n\n# 依赖注入\n\nimport Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\n\n在事件处理流程中，事件响应器具有自己独立的上下文，例如：当前的事件、机器人等信息。在 NoneBot 中，这些信息通过依赖注入的方式提供给事件处理函数，可以让代码更加整洁可读、提升复用能力。\n\n在了解如何使用依赖注入获取上下文信息之前，我们需要先了解两个概念：\n\n- `Dependent`：使用依赖注入的函数或其他任意可调用对象。如：事件处理函数、自定义的依赖函数等。\n- `Dependency`：依赖注入的对象。如：当前事件、机器人等。\n\n在之前的文档中，我们已经多次使用了依赖注入来获取事件信息。通过对函数参数依照一定规则填写类型注解，即可获得想要的上下文信息。任何一个事件处理函数在添加到事件处理流程时，都会根据一定规则提前将其解析成一个 `Dependent` 对象，方便运行时进行注入。如果遇到无法解析的参数，将会抛出 `ValueError(\"Unknown parameter\")` 的异常。整个依赖注入系统可以分为两部分：\n\n- 参数解析\n  - 依据一定规则解析函数参数，识别 `Dependency` 依赖。\n  - 生成 `Dependent` 对象。\n- 执行\n  - 根据已经解析的 `Dependency` 依赖，执行调用。\n  - 将所有 `Dependency` 的返回值根据参数名传入并调用 `Dependent` 。\n\n:::danger 警告\n在依赖注入中，类型注解是非常重要的，因为它不仅可以决定依赖注入的对象，还可以触发[重载机制](../appendices/overload.md#重载)。如果类型注解与实际获得数据类型不一致，将会跳过当前 `Dependent` 对象（即事件处理函数）。\n:::\n\n:::tip 提示\n如果对于依赖注入的解析流程有疑问，可以调整[日志等级配置项](../appendices/config.mdx#log-level)为 `TRACE`，查看依赖解析日志。\n:::\n\n## 同步支持\n\n对于依赖注入系统中的 `Dependent` 或者 `Dependency` 对象，均支持同步类型的函数或可调用对象。例如：\n\n```python {6,10}\nfrom nonebot import on_command\nfrom nonebot.params import Depends\n\nmatcher = on_command(\"foo\")\n\ndef dependency() -> str:\n    return \"something\"\n\n@matcher.handle()\ndef _(result: str = Depends(dependency)):\n    ...\n```\n\n## 非依赖参数\n\n在依赖注入解析中，任何无法解析的参数如果带有默认值，将会被视为非依赖参数。这些参数在依赖运行时将不会被注入而使用函数默认值。例如：\n\n```python\nasync def _(foo: str = \"bar\"): ...\n```\n\n## 类型依赖注入\n\n这一类的依赖注入仅需要在函数参数中添加对应的类型注解即可。\n\n### Bot\n\n获取当前事件的 Bot 对象。\n\n通过标注参数为 `Bot` 类型，或者一系列 `Bot` 类型，即可获取到当前事件的 Bot 对象。为兼容性考虑，如果参数名为 `bot` 且无类型注解，也会视为 Bot 依赖注入。\n\nBot 依赖注入支持重载（即：可以标注参数为子类型）且具有[重载优先检查权](../appendices/overload.md#重载)。\n\n<Tabs groupId=\"python\">\n  <TabItem value=\"3.10\" label=\"Python 3.10+\" default>\n\n```python\nfrom nonebot.adapters import Bot\nfrom nonebot.adapters.console import Bot as ConsoleBot\nfrom nonebot.adapters.onebot.v11 import Bot as OneBotV11Bot\n\nasync def _(foo: Bot): ...\nasync def _(foo: ConsoleBot | OneBotV11Bot): ...\nasync def _(bot): ...  # 兼容性处理\n```\n\n  </TabItem>\n  <TabItem value=\"3.9\" label=\"Python 3.9\">\n\n```python\nfrom typing import Union\n\nfrom nonebot.adapters import Bot\nfrom nonebot.adapters.console import Bot as ConsoleBot\nfrom nonebot.adapters.onebot.v11 import Bot as OneBotV11Bot\n\nasync def _(foo: Bot): ...\nasync def _(foo: Union[ConsoleBot, OneBotV11Bot]): ...\nasync def _(bot): ...  # 兼容性处理\n```\n\n  </TabItem>\n</Tabs>\n\n### Event\n\n获取当前事件。\n\n通过标注参数为 `Event` 类型，或者一系列 `Event` 类型，即可获取到当前事件。为兼容性考虑，如果参数名为 `event` 且无类型注解，也会视为 Event 依赖注入。\n\nEvent 依赖注入支持重载（即：可以标注参数为子类型）且具有[重载优先检查权](../appendices/overload.md#重载)。\n\n<Tabs groupId=\"python\">\n  <TabItem value=\"3.10\" label=\"Python 3.10+\" default>\n\n```python\nfrom nonebot.adapters import Event\nfrom nonebot.adapters.onebot.v11 import PrivateMessageEvent, GroupMessageEvent\n\nasync def _(foo: Event): ...\nasync def _(foo: PrivateMessageEvent | GroupMessageEvent): ...\nasync def _(event): ...  # 兼容性处理\n```\n\n  </TabItem>\n  <TabItem value=\"3.9\" label=\"Python 3.9\">\n\n```python\nfrom typing import Union\n\nfrom nonebot.adapters import Event\nfrom nonebot.adapters.onebot.v11 import PrivateMessageEvent, GroupMessageEvent\n\nasync def _(foo: Event): ...\nasync def _(foo: Union[PrivateMessageEvent, GroupMessageEvent]): ...\nasync def _(event): ...  # 兼容性处理\n```\n\n  </TabItem>\n</Tabs>\n\n### State\n\n获取当前[会话状态](../appendices/session-state.md)。\n\n通过标注参数为 `T_State` 类型，即可获取到当前会话状态。为兼容性考虑，如果参数名为 `state` 且无类型注解，也会视为 State 依赖注入。\n\n```python\nfrom nonebot.typing import T_State\n\nasync def _(foo: T_State): ...\n```\n\n### Matcher\n\n获取当前事件响应器实例。常用于使用[事件响应器操作](../appendices/session-control.mdx)。\n\n通过标注参数为 `Matcher` 类型，或者一系列 `Matcher` 类型，即可获取到当前事件。为兼容性考虑，如果参数名为 `matcher` 且无类型注解，也会视为 Matcher 依赖注入。\n\nMatcher 依赖注入支持重载（即：可以标注参数为子类型）且具有[重载优先检查权](../appendices/overload.md#重载)。\n\n```python\nfrom nonebot.matcher import Matcher\n\nasync def _(foo: Matcher): ...\nasync def _(matcher): ...  # 兼容性处理\n```\n\n### Exception\n\n获取事件响应器运行中抛出的异常。该依赖注入目前仅在事件响应器运行后处理 Hook 中可用。\n\n通过标注参数为异常类型，或者一系列异常类型，即可获取到事件响应器运行中抛出的异常。\n\n<Tabs groupId=\"python\">\n  <TabItem value=\"3.10\" label=\"Python 3.10+\" default>\n\n```python {5,8}\nfrom nonebot.message import run_postprocessor\nfrom nonebot.exception import ActionFailed, NetworkError\n\n@run_postprocessor\nasync def _(e: Exception): ...\n\n@run_postprocessor\nasync def _(e: ActionFailed | NetworkError): ...\n```\n\n  </TabItem>\n  <TabItem value=\"3.9\" label=\"Python 3.9\">\n\n```python {6,9}\nfrom typing import Union\nfrom nonebot.message import run_postprocessor\nfrom nonebot.exception import ActionFailed, NetworkError\n\n@run_postprocessor\nasync def _(e: Exception): ...\n\n@run_postprocessor\nasync def _(e: Union[ActionFailed, NetworkError]): ...\n```\n\n  </TabItem>\n</Tabs>\n\n## 子依赖\n\n在依赖注入系统中，我们可以定义一个子依赖，来执行自定义的操作，提高代码复用性以及处理性能。\n\n### 定义子依赖\n\n子依赖使用 `Depends` 标记进行定义，其参数即依赖的函数或可调用对象，同样会被解析为 `Dependent` 对象，将会在依赖注入期间执行。我们来看一个例子：\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {5,15}\nfrom typing import Annotated\n\nfrom nonebot import on_command\nfrom nonebot.adapters import Event\nfrom nonebot.params import Depends\n\ntest = on_command(\"test\")\n\nasync def check(event: Event) -> Event:\n    if event.get_user_id() in BLACKLIST:\n        await test.finish()\n    return event\n\n@test.handle()\nasync def _(event: Annotated[Event, Depends(check)]):\n    ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {3,13}\nfrom nonebot import on_command\nfrom nonebot.adapters import Event\nfrom nonebot.params import Depends\n\ntest = on_command(\"test\")\n\nasync def check(event: Event) -> Event:\n    if event.get_user_id() in BLACKLIST:\n        await test.finish()\n    return event\n\n@test.handle()\nasync def _(event: Event = Depends(check)):\n    ...\n```\n\n  </TabItem>\n</Tabs>\n\n在上面的代码中，我们使用 `Depends` 标记定义了一个子依赖 `check`。它判断事件主体用户是否在黑名单中，如果在，则直接结束事件处理流程。如果不在，则返回事件对象，以便事件处理函数可以继续执行。\n\n通过将 `Depends` 包裹的子依赖作为参数的默认值，我们就可以在执行事件处理函数之前执行子依赖，并将其返回值作为参数传入事件处理函数。子依赖和普通的事件处理函数并没有区别，同样可以使用依赖注入，并且可以返回任何类型的值。但需要注意的是，如果事件处理函数参数的类型注解与子依赖返回值的类型**不一致**，将会触发[重载](../appendices/overload.md)而跳过当前事件处理函数。\n\n特别的，我们可以为 `Dependent` 对象定义一系列前置子依赖，它们会在参数执行前被顺序执行，且返回值将会被忽略，例如：\n\n```python {11}\nfrom nonebot import on_command\nfrom nonebot.adapters import Event\nfrom nonebot.params import Depends\n\ntest = on_command(\"test\")\n\nasync def check(event: Event):\n    if event.get_user_id() in BLACKLIST:\n        await test.finish()\n\n@test.handle(parameterless=[Depends(check)])\nasync def _():\n    ...\n```\n\n### 依赖缓存\n\nNoneBot 在执行子依赖时，会将其返回值缓存起来。当我们在使用子依赖时，`Depends` 具有一个参数 `use_cache`，默认为 `True`。此时在事件处理流程中，多次使用同一个子依赖时，将会使用缓存中的结果而不会重复执行。这在很多情景中非常有用，例如：\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {7}\nimport random\nfrom typing import Annotated\n\nasync def random_result() -> int:\n    return random.randint(1, 100)\n\nasync def _(x: Annotated[int, Depends(random_result)]):\n    print(x)\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {6}\nimport random\n\nasync def random_result() -> int:\n    return random.randint(1, 100)\n\nasync def _(x: int = Depends(random_result)):\n    print(x)\n```\n\n  </TabItem>\n</Tabs>\n\n此时，在同一事件处理流程中，这个随机函数的返回值将会保持一致。如果我们希望每次都重新执行子依赖，可以将 `use_cache` 设置为 `False`。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {7}\nimport random\nfrom typing import Annotated\n\nasync def random_result() -> int:\n    return random.randint(1, 100)\n\nasync def _(x: Annotated[int, Depends(random_result, use_cache=False)]):\n    print(x)\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {6}\nimport random\n\nasync def random_result() -> int:\n    return random.randint(1, 100)\n\nasync def _(x: int = Depends(random_result, use_cache=False)):\n    print(x)\n```\n\n  </TabItem>\n</Tabs>\n\n:::tip 提示\n缓存的生命周期与当前接收到的事件相同。接收到事件后，子依赖在首次执行时缓存，在该事件处理完成后，缓存就会被清除。\n:::\n\n### 类型转换与校验\n\n在依赖注入系统中，我们可以对子依赖的返回值进行自动类型转换与校验。这个功能由 Pydantic 支持，因此我们通过参数类型注解自动使用 Pydantic 支持的类型转换。例如：\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {6,9}\nfrom typing import Annotated\n\nfrom nonebot.params import Depends\nfrom nonebot.adapters import Event\n\ndef get_user_id(event: Event) -> str:\n    return event.get_user_id()\n\nasync def _(user_id: Annotated[int, Depends(get_user_id, validate=True)]):\n    print(user_id)\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {4,7}\nfrom nonebot.params import Depends\nfrom nonebot.adapters import Event\n\ndef get_user_id(event: Event) -> str:\n    return event.get_user_id()\n\nasync def _(user_id: int = Depends(get_user_id, validate=True)):\n    print(user_id)\n```\n\n  </TabItem>\n</Tabs>\n\n在进行类型自动转换的同时，Pydantic 还支持对数据进行更多的限制，如：大于、小于、长度等。使用方法如下：\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {7,10}\nfrom typing import Annotated\n\nfrom pydantic import Field\nfrom nonebot.params import Depends\nfrom nonebot.adapters import Event\n\ndef get_user_id(event: Event) -> str:\n    return event.get_user_id()\n\nasync def _(user_id: Annotated[int, Depends(get_user_id, validate=Field(gt=100))]):\n    print(user_id)\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {5,8}\nfrom pydantic import Field\nfrom nonebot.params import Depends\nfrom nonebot.adapters import Event\n\ndef get_user_id(event: Event) -> str:\n    return event.get_user_id()\n\nasync def _(user_id: int = Depends(get_user_id, validate=Field(gt=100))):\n    print(user_id)\n```\n\n  </TabItem>\n</Tabs>\n\n### 类作为依赖\n\n在前面的事例中，我们使用了函数作为子依赖。实际上，我们还可以使用类作为依赖。当我们在实例化一个类的时候，其实我们就在调用它，类本身也是一个可调用对象。例如：\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {16}\nfrom typing import Annotated\nfrom dataclasses import dataclass\n\nfrom nonebot.params import Depends\nfrom nonebot.adapters import Event\nfrom nonebot.typing import T_State\n\ndef get_context(state: T_State) -> dict:\n    return state.setdefault(\"context\", {})\n\n@dataclass\nclass ClassDependency:\n    event: Event\n    context: dict = Depends(get_context)\n\nasync def _(data: Annotated[ClassDependency, Depends(ClassDependency)]):\n    print(data.event, data.context)\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {15}\nfrom dataclasses import dataclass\n\nfrom nonebot.params import Depends\nfrom nonebot.adapters import Event\nfrom nonebot.typing import T_State\n\ndef get_context(state: T_State) -> dict:\n    return state.setdefault(\"context\", {})\n\n@dataclass\nclass ClassDependency:\n    event: Event\n    context: dict = Depends(get_context)\n\nasync def _(data: ClassDependency = Depends(ClassDependency)):\n    print(data.event, data.context)\n```\n\n  </TabItem>\n</Tabs>\n\n可以看到，我们使用 `dataclass` 定义了一个类。由于这个类的 `__init__` 方法可以被依赖注入系统解析，因此，我们可以将其作为子依赖进行声明。特别地，对于类依赖，`Depends` 的参数可以为空，NoneBot 将会使用参数的类型注解进行解析与推断：\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python\nfrom typing import Annotated\n\nasync def _(data: Annotated[ClassDependency, Depends()]):\n    print(data.event, data.context)\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python\nasync def _(data: ClassDependency = Depends()):\n    print(data.event, data.context)\n```\n\n  </TabItem>\n</Tabs>\n\n### 生成器作为依赖\n\nNoneBot 的依赖注入支持依赖项在事件处理流程结束后进行一些额外的工作，比如数据库 session 或者网络 IO 的关闭，互斥锁的解锁等等。同时，由于[依赖缓存](#依赖缓存)的存在，我们可以通过这种方式来实现共享一个 session 等功能。\n\n要实现上述功能，我们可以用生成器函数作为依赖项，我们用 `yield` 关键字取代 `return` 关键字，并在 `yield` 之后进行额外的工作。\n\n我们可以看下述代码段, 使用 `httpx.AsyncClient` 异步网络 IO，并在事件处理流程中共用一个 client：\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {15}\nfrom typing import Annotated\nfrom collections.abc import AsyncGenerator\n\nimport httpx\nfrom nonebot.params import Depends\n\nasync def get_client() -> AsyncGenerator[httpx.AsyncClient, None]:\n    try:\n        async with httpx.AsyncClient() as client:\n            yield client\n    finally:\n        # 在这里进行额外的工作\n\n\n@test.handle()\nasync def _(x: Annotated[httpx.AsyncClient, Depends(get_client)]):\n    resp = await x.get(\"https://nonebot.dev\")\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {15}\nfrom collections.abc import AsyncGenerator\n\nimport httpx\nfrom nonebot.params import Depends\n\nasync def get_client() -> AsyncGenerator[httpx.AsyncClient, None]:\n    try:\n        async with httpx.AsyncClient() as client:\n            yield client\n    finally:\n        # 在这里进行额外的工作\n\n\n@test.handle()\nasync def _(x: httpx.AsyncClient = Depends(get_client)):\n    resp = await x.get(\"https://nonebot.dev\")\n```\n\n  </TabItem>\n</Tabs>\n\n:::caution 注意\n生成器作为依赖时，其中只能进行一次 `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 内部就使用了这两个装饰器。\n:::\n\n### 可调用对象作为依赖\n\n在 Python 里，为类定义 `__call__` 方法就可以使得这个类的实例成为一个可调用对象。因此，我们也可以将定义了 `__call__` 方法的类的实例作为依赖。事实上，NoneBot 的[内置响应规则](./matcher.md#内置响应规则)就广泛使用了这种方式，以 `is_type` 规则为例：\n\n```python\nfrom nonebot.adapters import Event\n\nclass IsTypeRule:\n    def __init__(self, *types: type[Event]):\n        self.types = types\n\n    async def __call__(self, event: Event) -> bool:\n        return isinstance(event, self.types)\n```\n\n我们在使用 `is_type` 时，即实例化了 `IsTypeRule` 类，然后将实例作为响应规则依赖项传入。\n\n## 其他依赖注入\n\n这一类的依赖注入通常基于子依赖编写，为我们开发者提供更方便的途径获取上下文信息。\n\n### EventType\n\n获取当前事件的类型。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot.params import EventType\n\nasync def _(foo: Annotated[str, EventType()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {3}\nfrom nonebot.params import EventType\n\nasync def _(foo: str = EventType()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### EventMessage\n\n获取当前事件的消息。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {5}\nfrom typing import Annotated\nfrom nonebot.adapters import Message\nfrom nonebot.params import EventMessage\n\nasync def _(foo: Annotated[Message, EventMessage()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {4}\nfrom nonebot.adapters import Message\nfrom nonebot.params import EventMessage\n\nasync def _(foo: Message = EventMessage()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### EventPlainText\n\n获取当前事件的消息纯文本部分。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot.params import EventPlainText\n\nasync def _(foo: Annotated[str, EventPlainText()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {3}\nfrom nonebot.params import EventPlainText\n\nasync def _(foo: str = EventPlainText()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### EventToMe\n\n获取当前事件是否与机器人相关。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot.params import EventToMe\n\nasync def _(foo: Annotated[bool, EventToMe()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {3}\nfrom nonebot.params import EventToMe\n\nasync def _(foo: bool = EventToMe()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### Command\n\n获取当前命令型消息的元组形式命令名。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot.params import Command\n\nasync def _(foo: Annotated[tuple[str, ...], Command()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {4}\nfrom nonebot.params import Command\n\nasync def _(foo: tuple[str, ...] = Command()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n:::tip 提示\n命令详情只能在**触发命令型事件响应器时**获取。如果在事件处理后续流程中获取，则会获取到不同的值。\n:::\n\n### RawCommand\n\n获取当前命令型消息的文本形式命令名。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot.params import RawCommand\n\nasync def _(foo: Annotated[str, RawCommand()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {3}\nfrom nonebot.params import RawCommand\n\nasync def _(foo: str = RawCommand()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n:::tip 提示\n命令详情只能在**触发命令型事件响应器时**获取。如果在事件处理后续流程中获取，则会获取到不同的值。\n:::\n\n### CommandArg\n\n获取命令型消息命令后跟随的参数。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {5}\nfrom typing import Annotated\nfrom nonebot.adapters import Message\nfrom nonebot.params import CommandArg\n\nasync def _(foo: Annotated[Message, CommandArg()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {4}\nfrom nonebot.adapters import Message\nfrom nonebot.params import CommandArg\n\nasync def _(foo: Message = CommandArg()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n:::tip 提示\n命令详情只能在**触发命令型事件响应器时**获取。如果在事件处理后续流程中获取，则会获取到不同的值。\n:::\n\n### CommandStart\n\n获取命令型消息命令前缀。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot.params import CommandStart\n\nasync def _(foo: Annotated[str, CommandStart()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {3}\nfrom nonebot.params import CommandStart\n\nasync def _(foo: str = CommandStart()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n:::tip 提示\n命令详情只能在**触发命令型事件响应器时**获取。如果在事件处理后续流程中获取，则会获取到不同的值。\n:::\n\n### CommandWhitespace\n\n获取命令型消息命令与参数间空白符。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot.params import CommandWhitespace\n\nasync def _(foo: Annotated[str, CommandWhitespace()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {3}\nfrom nonebot.params import CommandWhitespace\n\nasync def _(foo: str = CommandWhitespace()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n:::tip 提示\n命令详情只能在**触发命令型事件响应器时**获取。如果在事件处理后续流程中获取，则会获取到不同的值。\n:::\n\n### ShellCommandArgv\n\n获取 shell 命令解析前的参数列表，列表中可能包含文本字符串和富文本消息段（如：图片）。当词法解析出错的时候，返回值将为 `None`。通过重载机制即可处理两种不同的情况。\n\n<Tabs groupId=\"python\">\n  <TabItem value=\"3.10\" label=\"Python 3.10+\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot import on_shell_command\nfrom nonebot.params import ShellCommandArgv\n\nmatcher = on_shell_command(\"cmd\")\n\n# 解析失败\n@matcher.handle()\nasync def _(foo: Annotated[None, ShellCommandArgv()]): ...\n\n# 解析成功\n@matcher.handle()\nasync def _(foo: Annotated[list[str | MessageSegment], ShellCommandArgv()]): ...\n```\n\n```python {4}\nfrom nonebot import on_shell_command\nfrom nonebot.params import ShellCommandArgv\n\nmatcher = on_shell_command(\"cmd\")\n\n# 解析失败\n@matcher.handle()\nasync def _(foo: None = ShellCommandArgv()): ...\n\n# 解析成功\n@matcher.handle()\nasync def _(foo: list[str | MessageSegment] = ShellCommandArgv()): ...\n```\n\n  </TabItem>\n  <TabItem value=\"3.9\" label=\"Python 3.9\">\n\n```python {4}\nfrom typing import Union, Annotated\nfrom nonebot import on_shell_command\nfrom nonebot.params import ShellCommandArgv\n\nmatcher = on_shell_command(\"cmd\")\n\n# 解析失败\n@matcher.handle()\nasync def _(foo: Annotated[None, ShellCommandArgv()]): ...\n\n# 解析成功\n@matcher.handle()\nasync def _(foo: Annotated[list[Union[str, MessageSegment]], ShellCommandArgv()]): ...\n```\n\n```python {4}\nfrom typing import Union\nfrom nonebot import on_shell_command\nfrom nonebot.params import ShellCommandArgv\n\nmatcher = on_shell_command(\"cmd\")\n\n# 解析失败\n@matcher.handle()\nasync def _(foo: None = ShellCommandArgv()): ...\n\n# 解析成功\n@matcher.handle()\nasync def _(foo: list[Union[str, MessageSegment]] = ShellCommandArgv()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### ShellCommandArgs\n\n获取 shell 命令解析后的参数 Namespace，支持 MessageSegment 富文本（如：图片）。\n\n:::tip 提示\n如果参数解析成功，则为 parser 返回的 Namespace；如果参数解析失败，则为 [`ParserExit`](../api/exception.md#ParserExit) 异常，并携带错误码与错误信息。在前置词法解析失败时，返回值也为 [`ParserExit`](../api/exception.md#ParserExit) 异常。通过重载机制即可处理两种不同的情况。\n\n由于 `ArgumentParser` 在解析到 `--help` 参数时也会抛出异常，这种情况下错误码为 `0` 且错误信息即为帮助信息。\n:::\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {14,22}\nfrom typing import Annotated\n\nfrom nonebot import on_shell_command\nfrom nonebot.exception import ParserExit\nfrom nonebot.params import ShellCommandArgs\nfrom nonebot.rule import Namespace, ArgumentParser\n\nparser = ArgumentParser(\"demo\")\n# parser.add_argument ...\nmatcher = on_shell_command(\"cmd\", parser=parser)\n\n# 解析失败\n@matcher.handle()\nasync def _(foo: Annotated[ParserExit, ShellCommandArgs()]):\n    if foo.status == 0:\n        foo.message  # help message\n    else:\n        foo.message  # error message\n\n# 解析成功\n@matcher.handle()\nasync def _(foo: Annotated[Namespace, ShellCommandArgs()]):\n    arg_dict = vars(foo)\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {12,20}\nfrom nonebot import on_shell_command\nfrom nonebot.exception import ParserExit\nfrom nonebot.params import ShellCommandArgs\nfrom nonebot.rule import Namespace, ArgumentParser\n\nparser = ArgumentParser(\"demo\")\n# parser.add_argument ...\nmatcher = on_shell_command(\"cmd\", parser=parser)\n\n# 解析失败\n@matcher.handle()\nasync def _(foo: ParserExit = ShellCommandArgs()):\n    if foo.status == 0:\n        foo.message  # help message\n    else:\n        foo.message  # error message\n\n# 解析成功\n@matcher.handle()\nasync def _(foo: Namespace = ShellCommandArgs()):\n    arg_dict = vars(foo)\n```\n\n  </TabItem>\n</Tabs>\n\n### RegexMatched\n\n获取正则匹配结果的对象。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {5}\nfrom re import Match\nfrom typing import Annotated\nfrom nonebot.params import RegexMatched\n\nasync def _(foo: Annotated[Match[str], RegexMatched()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {4}\nfrom re import Match\nfrom nonebot.params import RegexMatched\n\nasync def _(foo: Match[str] = RegexMatched()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### RegexStr\n\n获取正则匹配结果的文本。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot.params import RegexStr\n\nasync def _(foo: Annotated[str, RegexStr()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {3}\nfrom nonebot.params import RegexStr\n\nasync def _(foo: str = RegexStr()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### RegexGroup\n\n获取正则匹配结果的 group 元组。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Any, Annotated\nfrom nonebot.params import RegexGroup\n\nasync def _(foo: Annotated[tuple[Any, ...], RegexGroup()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {4}\nfrom typing import Any\nfrom nonebot.params import RegexGroup\n\nasync def _(foo: tuple[Any, ...] = RegexGroup()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### RegexDict\n\n获取正则匹配结果的 group 字典。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Any, Annotated\nfrom nonebot.params import RegexDict\n\nasync def _(foo: Annotated[dict[str, Any], RegexDict()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {4}\nfrom typing import Any\nfrom nonebot.params import RegexDict\n\nasync def _(foo: dict[str, Any] = RegexDict()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### Startswith\n\n获取触发响应器的消息前缀字符串。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot.params import Startswith\n\nasync def _(foo: Annotated[str, Startswith()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {3}\nfrom nonebot.params import Startswith\n\nasync def _(foo: str = Startswith()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### Endswith\n\n获取触发响应器的消息后缀字符串。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot.params import Endswith\n\nasync def _(foo: Annotated[str, Endswith()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {3}\nfrom nonebot.params import Endswith\n\nasync def _(foo: str = Endswith()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### Fullmatch\n\n获取触发响应器的消息字符串。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot.params import Fullmatch\n\nasync def _(foo: Annotated[str, Fullmatch()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {3}\nfrom nonebot.params import Fullmatch\n\nasync def _(foo: str = Fullmatch()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### Keyword\n\n获取触发响应器的关键字字符串。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot.params import Keyword\n\nasync def _(foo: Annotated[str, Keyword()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {3}\nfrom nonebot.params import Keyword\n\nasync def _(foo: str = Keyword()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### Received\n\n获取某次 `receive` 接收的事件。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {7}\nfrom typing import Annotated\n\nfrom nonebot.adapters import Event\nfrom nonebot.params import Received\n\n@matcher.receive(\"id\")\nasync def _(foo: Annotated[Event, Received(\"id\")]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {5}\nfrom nonebot.adapters import Event\nfrom nonebot.params import Received\n\n@matcher.receive(\"id\")\nasync def _(foo: Event = Received(\"id\")): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### LastReceived\n\n获取最近一次 `receive` 接收的事件。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {7}\nfrom typing import Annotated\n\nfrom nonebot.adapters import Event\nfrom nonebot.params import LastReceived\n\n@matcher.receive(\"any\")\nasync def _(foo: Annotated[Event, LastReceived()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {5}\nfrom nonebot.adapters import Event\nfrom nonebot.params import LastReceived\n\n@matcher.receive(\"any\")\nasync def _(foo: Event = LastReceived()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### ReceivePromptResult\n\n获取某次 `receive` 发送提示消息的结果。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {6}\nfrom typing import Any, Annotated\n\nfrom nonebot.params import ReceivePromptResult\n\n@matcher.receive(\"id\", prompt=\"prompt\")\nasync def _(result: Annotated[Any, ReceivePromptResult(\"id\")]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {6}\nfrom typing import Any\n\nfrom nonebot.params import ReceivePromptResult\n\n@matcher.receive(\"id\", prompt=\"prompt\")\nasync def _(result: Any = ReceivePromptResult(\"id\")): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### Arg\n\n获取某次 `got` 接收的参数。如果 `Arg` 参数留空，则使用函数的参数名作为要获取的参数。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {7,8}\nfrom typing import Annotated\n\nfrom nonebot.params import Arg\nfrom nonebot.adapters import Message\n\n@matcher.got(\"key\")\nasync def _(key: Annotated[Message, Arg()]): ...\nasync def _(foo: Annotated[Message, Arg(\"key\")]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {5,6}\nfrom nonebot.params import Arg\nfrom nonebot.adapters import Message\n\n@matcher.got(\"key\")\nasync def _(key: Message = Arg()): ...\nasync def _(foo: Message = Arg(\"key\")): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### ArgStr\n\n获取某次 `got` 接收的参数，并转换为字符串。如果 `Arg` 参数留空，则使用函数的参数名作为要获取的参数。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {6,7}\nfrom typing import Annotated\n\nfrom nonebot.params import ArgStr\n\n@matcher.got(\"key\")\nasync def _(key: Annotated[str, ArgStr()]): ...\nasync def _(foo: Annotated[str, ArgStr(\"key\")]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {4,5}\nfrom nonebot.params import ArgStr\n\n@matcher.got(\"key\")\nasync def _(key: str = ArgStr()): ...\nasync def _(foo: str = ArgStr(\"key\")): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### ArgPlainText\n\n获取某次 `got` 接收的参数的纯文本部分。如果 `Arg` 参数留空，则使用函数的参数名作为要获取的参数。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {6,7}\nfrom typing import Annotated\n\nfrom nonebot.params import ArgPlainText\n\n@matcher.got(\"key\")\nasync def _(key: Annotated[str, ArgPlainText()]): ...\nasync def _(foo: Annotated[str, ArgPlainText(\"key\")]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {4,5}\nfrom nonebot.params import ArgPlainText\n\n@matcher.got(\"key\")\nasync def _(key: str = ArgPlainText()): ...\nasync def _(foo: str = ArgPlainText(\"key\")): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### ArgPromptResult\n\n获取某次 `got` 发送提示消息的结果。如果 `Arg` 参数留空，则使用函数的参数名作为要获取的参数。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {6,7}\nfrom typing import Any, Annotated\n\nfrom nonebot.params import ArgPromptResult\n\n@matcher.got(\"key\", prompt=\"prompt\")\nasync def _(result: Annotated[Any, ArgPromptResult()]): ...\nasync def _(result: Annotated[Any, ArgPromptResult(\"key\")]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {6,7}\nfrom typing import Any\n\nfrom nonebot.params import ArgPromptResult\n\n@matcher.got(\"key\", prompt=\"prompt\")\nasync def _(result: Any = ArgPromptResult()): ...\nasync def _(result: Any = ArgPromptResult(\"key\")): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### PausePromptResult\n\n获取最近一次 `pause` 发送提示消息的结果。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {6}\nfrom typing import Any, Annotated\n\nfrom nonebot.params import PausePromptResult\n\n@matcher.handle()\nasync def _():\n    await matcher.pause(prompt=\"prompt\")\n\n@matcher.handle()\nasync def _(result: Annotated[Any, PausePromptResult()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {6}\nfrom typing import Any\n\nfrom nonebot.params import PausePromptResult\n\n@matcher.handle()\nasync def _():\n    await matcher.pause(prompt=\"prompt\")\n\n@matcher.handle()\nasync def _(result: Any = PausePromptResult()): ...\n```\n\n  </TabItem>\n</Tabs>\n"
  },
  {
    "path": "website/docs/advanced/driver.md",
    "content": "---\nsidebar_position: 0\ndescription: 选择合适的驱动器运行机器人\n\noptions:\n  menu:\n    - category: advanced\n      weight: 10\n---\n\n# 选择驱动器\n\n驱动器 (Driver) 是机器人运行的基石，它是机器人初始化的第一步，主要负责数据收发。\n\n:::important 提示\n驱动器的选择通常与机器人所使用的协议适配器相关，如果不知道该选择哪个驱动器，可以先阅读相关协议适配器文档说明。\n:::\n\n:::tip 提示\n如何**安装**驱动器请参考[安装驱动器](../tutorial/store.mdx#安装驱动器)。\n:::\n\n## 驱动器类型\n\n驱动器类型大体上可以分为两种：\n\n- `Forward`：即客户端型驱动器，多用于使用 HTTP 轮询，连接 WebSocket 服务器等情形。\n- `Reverse`：即服务端型驱动器，多用于使用 WebHook，接收 WebSocket 客户端连接等情形。\n\n客户端型驱动器可以分为以下两种：\n\n1. 异步发送 HTTP 请求，自定义 `HTTP Method`、`URL`、`Header`、`Body`、`Cookie`、`Proxy`、`Timeout` 等。\n2. 异步建立 WebSocket 连接上下文，自定义 `WebSocket URL`、`Header`、`Cookie`、`Proxy`、`Timeout` 等。\n\n服务端型驱动器目前有：\n\n1. ASGI 应用框架，具有以下功能：\n   - 协议适配器自定义 HTTP 上报地址以及对上报数据处理的回调函数。\n   - 协议适配器自定义 WebSocket 连接请求地址以及对 WebSocket 请求处理的回调函数。\n   - 用户可以向 ASGI 应用添加任何服务端相关功能，如：[添加自定义路由](./routing.md)。\n\n## 配置驱动器\n\n驱动器的配置方法已经在[配置](../appendices/config.mdx)章节中简单进行了介绍，这里将详细介绍驱动器配置的格式。\n\nNoneBot 中的客户端和服务端型驱动器可以相互配合使用，但服务端型驱动器**仅能选择一个**。所有驱动器模块都会包含一个 `Driver` 子类，即驱动器类，他可以作为驱动器单独运行。同时，客户端驱动器模块中还会提供一个 `Mixin` 子类，用于在与其他驱动器配合使用时加载。因此，驱动器配置格式采用特殊语法：`<module>[:<Driver>][+<module>[:<Mixin>]]*`。\n\n其中，`<module>` 代表**驱动器模块路径**；`<Driver>` 代表**驱动器类名**，默认为 `Driver`；`<Mixin>` 代表**驱动器混入类名**，默认为 `Mixin`。即，我们需要选择一个主要驱动器，然后在其基础上配合使用其他驱动器的功能。主要驱动器可以为客户端或服务端类型，但混入类驱动器只能为客户端类型。\n\n特别的，为了简化内置驱动器模块路径，我们可以使用 `~` 符号作为内置驱动器模块路径的前缀，如 `~fastapi` 代表使用内置驱动器 `fastapi`。NoneBot 内置了多个驱动器适配，但需要安装额外依赖才能使用，具体请参考[安装驱动器](../tutorial/store.mdx#安装驱动器)。常见的驱动器配置如下：\n\n```dotenv\nDRIVER=~fastapi\nDRIVER=~aiohttp\nDRIVER=~httpx+~websockets\nDRIVER=~fastapi+~httpx+~websockets\n```\n\n## 获取驱动器\n\n在 NoneBot 框架初始化完成后，我们就可以通过 `get_driver()` 方法获取全局驱动器实例：\n\n```python\nfrom nonebot import get_driver\n\ndriver = get_driver()\n```\n\n## 内置驱动器\n\n### None\n\n**类型：**服务端驱动器\n\nNoneBot 内置的空驱动器，不提供任何收发数据功能，可以在不需要外部网络连接时使用。\n\n```env\nDRIVER=~none\n```\n\n### FastAPI（默认）\n\n**类型：**ASGI 服务端驱动器\n\n> FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints.\n\n[FastAPI](https://fastapi.tiangolo.com/) 是一个易上手、高性能的异步 Web 框架，具有极佳的编写体验。 FastAPI 可以通过类型注解、依赖注入等方式实现输入参数校验、自动生成 API 文档等功能，也可以挂载其他 ASGI、WSGI 应用。\n\n```env\nDRIVER=~fastapi\n```\n\n#### FastAPI 配置项\n\n##### `fastapi_openapi_url`\n\n类型：`str | None`  \n默认值：`None`  \n说明：`FastAPI` 提供的 `OpenAPI` JSON 定义地址，如果为 `None`，则不提供 `OpenAPI` JSON 定义。\n\n##### `fastapi_docs_url`\n\n类型：`str | None`  \n默认值：`None`  \n说明：`FastAPI` 提供的 `Swagger` 文档地址，如果为 `None`，则不提供 `Swagger` 文档。\n\n##### `fastapi_redoc_url`\n\n类型：`str | None`  \n默认值：`None`  \n说明：`FastAPI` 提供的 `ReDoc` 文档地址，如果为 `None`，则不提供 `ReDoc` 文档。\n\n##### `fastapi_include_adapter_schema`\n\n类型：`bool`  \n默认值：`True`  \n说明：`FastAPI` 提供的 `OpenAPI` JSON 定义中是否包含适配器路由的 `Schema`。\n\n##### `fastapi_reload`\n\n:::caution 警告\n不推荐开启该配置项，在 Windows 平台上开启该功能有可能会造成预料之外的影响！替代方案：使用 `nb-cli` 命令行工具以及参数 `--reload` 启动 NoneBot。\n\n```bash\nnb run --reload\n```\n\n开启该功能后，在 uvicorn 运行时（FastAPI 提供的 ASGI 底层，即 reload 功能的实际来源），asyncio 使用的事件循环会被 uvicorn 从默认的 `ProactorEventLoop` 强制切换到 `SelectorEventLoop`。\n\n> 相关信息参考 [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)\n\n后者（`SelectorEventLoop`）在 Windows 平台的可使用性不如前者（`ProactorEventLoop`），包括但不限于\n\n1. 不支持创建子进程\n2. 最多只支持 512 个套接字\n3. ...\n\n> 具体信息参考 [Python 文档](https://docs.python.org/zh-cn/3/library/asyncio-platforms.html#windows)\n\n所以，一些使用了 asyncio 的库因此可能无法正常工作，如：\n\n1. [playwright](https://playwright.dev/python/docs/library#incompatible-with-selectoreventloop-of-asyncio-on-windows)\n\n如果在开启该功能后，原本**正常运行**的代码报错，且打印的异常堆栈信息和 asyncio 有关（异常一般为 `NotImplementedError`），\n你可能就需要考虑相关库对事件循环的支持，以及是否启用该功能。\n:::\n\n类型：`bool`  \n默认值：`False`  \n说明：是否开启 `uvicorn` 的 `reload` 功能，需要在机器人入口文件提供 ASGI 应用路径。\n\n```python title=bot.py\napp = nonebot.get_asgi()\nnonebot.run(app=\"bot:app\")\n```\n\n##### `fastapi_reload_dirs`\n\n类型：`List[str] | None`  \n默认值：`None`  \n说明：重载监控文件夹列表，默认为 uvicorn 默认值\n\n##### `fastapi_reload_delay`\n\n类型：`float | None`  \n默认值：`None`  \n说明：重载延迟，默认为 uvicorn 默认值\n\n##### `fastapi_reload_includes`\n\n类型：`List[str] | None`  \n默认值：`None`  \n说明：要监听的文件列表，支持 glob pattern，默认为 uvicorn 默认值\n\n##### `fastapi_reload_excludes`\n\n类型：`List[str] | None`  \n默认值：`None`  \n说明：不要监听的文件列表，支持 glob pattern，默认为 uvicorn 默认值\n\n##### `fastapi_extra`\n\n类型：`Dist[str, Any]`  \n默认值：`{}`  \n说明：传递给 `FastAPI` 的其他参数\n\n### Quart\n\n**类型：**ASGI 服务端驱动器\n\n> Quart is an asyncio reimplementation of the popular Flask microframework API.\n\n[Quart](https://quart.palletsprojects.com/) 是一个类 Flask 的异步版本，拥有与 Flask 非常相似的接口和使用方法。\n\n```env\nDRIVER=~quart\n```\n\n#### Quart 配置项\n\n##### `quart_reload`\n\n:::caution 警告\n不推荐开启该配置项，在 Windows 平台上开启该功能有可能会造成预料之外的影响！替代方案：使用 `nb-cli` 命令行工具以及参数 `--reload` 启动 NoneBot。\n\n```bash\nnb run --reload\n```\n\n:::\n\n类型：`bool`  \n默认值：`False`  \n说明：是否开启 `uvicorn` 的 `reload` 功能，需要在机器人入口文件提供 ASGI 应用路径。\n\n```python title=bot.py\napp = nonebot.get_asgi()\nnonebot.run(app=\"bot:app\")\n```\n\n##### `quart_reload_dirs`\n\n类型：`List[str] | None`  \n默认值：`None`  \n说明：重载监控文件夹列表，默认为 uvicorn 默认值\n\n##### `quart_reload_delay`\n\n类型：`float | None`  \n默认值：`None`  \n说明：重载延迟，默认为 uvicorn 默认值\n\n##### `quart_reload_includes`\n\n类型：`List[str] | None`  \n默认值：`None`  \n说明：要监听的文件列表，支持 glob pattern，默认为 uvicorn 默认值\n\n##### `quart_reload_excludes`\n\n类型：`List[str] | None`  \n默认值：`None`  \n说明：不要监听的文件列表，支持 glob pattern，默认为 uvicorn 默认值\n\n##### `quart_extra`\n\n类型：`Dist[str, Any]`  \n默认值：`{}`  \n说明：传递给 `Quart` 的其他参数\n\n### HTTPX\n\n**类型：**HTTP 客户端驱动器\n\n:::caution 注意\n本驱动器仅支持 HTTP 请求，不支持 WebSocket 连接请求。\n:::\n\n> [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.\n\n```env\nDRIVER=~httpx\n```\n\n### websockets\n\n**类型：**WebSocket 客户端驱动器\n\n:::caution 注意\n本驱动器仅支持 WebSocket 连接请求，不支持 HTTP 请求。\n:::\n\n> [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.\n\n```env\nDRIVER=~websockets\n```\n\n### AIOHTTP\n\n**类型：**HTTP/WebSocket 客户端驱动器\n\n> [AIOHTTP](https://docs.aiohttp.org/): Asynchronous HTTP Client/Server for asyncio and Python.\n\n```env\nDRIVER=~aiohttp\n```\n"
  },
  {
    "path": "website/docs/advanced/matcher-provider.md",
    "content": "---\nsidebar_position: 10\ndescription: 自定义事件响应器存储\n\noptions:\n  menu:\n    - category: advanced\n      weight: 110\n---\n\n# 事件响应器存储\n\n事件响应器是 NoneBot 处理事件的核心，它们默认存储在一个字典中。在进入会话状态后，事件响应器将会转为临时响应器，作为最高优先级同样存储于该字典中。因此，事件响应器的存储类似于会话存储，它决定了整个 NoneBot 对事件的处理行为。\n\nNoneBot 默认使用 Python 的字典将事件响应器存储于内存中，但是我们也可以自定义事件响应器存储，将事件响应器存储于其他地方，例如 Redis 等。这样我们就可以实现持久化、在多实例间共享会话状态等功能。\n\n## 编写存储提供者\n\n事件响应器的存储提供者 `MatcherProvider` 抽象类继承自 `MutableMapping[int, list[type[Matcher]]]`，即以优先级为键，以事件响应器列表为值的映射。我们可以方便地进行逐优先级事件传播。\n\n编写一个自定义的存储提供者，只需要继承并实现 `MatcherProvider` 抽象类：\n\n```python\nfrom nonebot.matcher import MatcherProvider\n\nclass CustomProvider(MatcherProvider):\n    ...\n```\n\n## 设置存储提供者\n\n我们可以通过 `matchers.set_provider` 方法设置存储提供者：\n\n```python {3}\nfrom nonebot.matcher import matchers\n\nmatchers.set_provider(CustomProvider)\n\nassert isinstance(matchers.provider, CustomProvider)\n```\n"
  },
  {
    "path": "website/docs/advanced/matcher.md",
    "content": "---\nsidebar_position: 5\ndescription: 事件响应器组成与内置响应规则\n\noptions:\n  menu:\n    - category: advanced\n      weight: 60\n---\n\n# 事件响应器进阶\n\n在[指南](../tutorial/matcher.md)与[深入](../appendices/rule.md)中，我们已经介绍了事件响应器的基本用法以及响应规则、权限控制等功能。在这一节中，我们将介绍事件响应器的组成，内置的响应规则，与第三方响应规则拓展。\n\n:::tip 提示\n事件响应器允许继承，你可以通过直接继承 `Matcher` 类来创建一个新的事件响应器。\n:::\n\n## 事件响应器组成\n\n### 事件响应器类型\n\n事件响应器类型 `type` 即是该响应器所要响应的事件类型，只有在接收到的事件类型与该响应器的类型相同时，才会触发该响应器。如果类型为空字符串 `\"\"`，则响应器将会响应所有类型的事件。事件响应器类型的检查在所有其他检查（权限控制、响应规则）之前进行。\n\nNoneBot 内置了四种常用事件类型：`meta_event`、`message`、`notice`、`request`，分别对应元事件、消息、通知、请求。通常情况下，协议适配器会将事件合理地分类至这四种类型中。如果有其他类型的事件需要响应，可以自行定义新的类型。\n\n### 事件触发权限\n\n事件触发权限 `permission` 是一个 `Permission` 对象，这在[权限控制](../appendices/permission.mdx)一节中已经介绍过。事件触发权限会在事件响应器的类型检查通过后进行检查，如果权限检查通过，则执行响应规则检查。\n\n### 事件响应规则\n\n事件响应规则 `rule` 是一个 `Rule` 对象，这在[响应规则](../appendices/rule.md)一节中已经介绍过。事件响应器的响应规则会在事件响应器的权限检查通过后进行匹配，如果响应规则检查通过，则触发该响应器。\n\n### 响应优先级\n\n响应优先级 `priority` 是一个正整数，用于指定响应器的优先级。响应器的优先级越小，越先被触发。如果响应器的优先级相同，则按照响应器的注册顺序进行触发。\n\n### 阻断\n\n阻断 `block` 是一个布尔值，用于指定响应器是否阻断事件的传播。如果阻断为 `True`，则在该响应器被触发后，事件将不会再传播给其他下一优先级的响应器。\n\nNoneBot 内置的事件响应器中，所有非 `command` 规则的 `message` 类型的事件响应器都会阻断事件传递，其他则不会。\n\n在部分情况中，可以使用 [`stop_propagation`](../appendices/session-control.mdx#stop_propagation) 方法动态阻止事件传播，该方法需要 handler 在参数中获取 matcher 实例后调用方法。\n\n### 有效期\n\n事件响应器的有效期分为 `temp` 和 `expire_time` 。`temp` 是一个布尔值，用于指定响应器是否为临时响应器。如果为 `True`，则该响应器在被触发后会被自动销毁。`expire_time` 是一个 `datetime` 对象，用于指定响应器的过期时间。如果 `expire_time` 不为 `None`，则在该时间点后，该响应器会被自动销毁。\n\n### 默认状态\n\n事件响应器的默认状态 `default_state` 是一个 `dict` 对象，用于指定响应器的默认状态。在响应器被触发时，响应器将会初始化默认状态然后开始执行事件处理流程。\n\n## 基本辅助函数\n\nNoneBot 为四种类型的事件响应器提供了五个基本的辅助函数：\n\n- `on`：创建任何类型的事件响应器。\n- `on_metaevent`：创建元事件响应器。\n- `on_message`：创建消息事件响应器。\n- `on_request`：创建请求事件响应器。\n- `on_notice`：创建通知事件响应器。\n\n除了 `on` 函数具有一个 `type` 参数外，其余参数均相同：\n\n- `rule`：响应规则，可以是 `Rule` 对象或者 `RuleChecker` 函数。\n- `permission`：事件触发权限，可以是 `Permission` 对象或者 `PermissionChecker` 函数。\n- `handlers`：事件处理函数列表。\n- `temp`：是否为临时响应器。\n- `expire_time`：响应器的过期时间。\n- `priority`：响应器的优先级。\n- `block`：是否阻断事件传播。\n- `state`：响应器的默认状态。\n\n在消息类型的事件响应器的基础上，NoneBot 还内置了一些常用的响应规则，并结合为辅助函数来方便我们快速创建指定功能的响应器。下面我们逐个介绍。\n\n## 内置响应规则\n\n:::tip\n响应规则的使用方法可以参考 [深入 - 响应规则](../appendices/rule.md)。\n:::\n\n### `startswith`\n\n`startswith` 响应规则用于匹配消息纯文本部分的开头是否与指定字符串（或一系列字符串）相同。可选参数 `ignorecase` 用于指定是否忽略大小写，默认为 `False`。\n\n例如，我们可以创建一个匹配消息开头为 `!` 或者 `/` 的规则：\n\n```python\nfrom nonebot.rule import startswith\n\nrule = startswith((\"!\", \"/\"), ignorecase=False)\n```\n\n也可以直接使用辅助函数新建一个响应器：\n\n```python\nfrom nonebot import on_startswith\n\nmatcher = on_startswith((\"!\", \"/\"), ignorecase=False)\n```\n\n### `endswith`\n\n`endswith` 响应规则用于匹配消息纯文本部分的结尾是否与指定字符串（或一系列字符串）相同。可选参数 `ignorecase` 用于指定是否忽略大小写，默认为 `False`。\n\n例如，我们可以创建一个匹配消息结尾为 `.` 或者 `。` 的规则：\n\n```python\nfrom nonebot.rule import endswith\n\nrule = endswith((\".\", \"。\"), ignorecase=False)\n```\n\n也可以直接使用辅助函数新建一个响应器：\n\n```python\nfrom nonebot import on_endswith\n\nmatcher = on_endswith((\".\", \"。\"), ignorecase=False)\n```\n\n### `fullmatch`\n\n`fullmatch` 响应规则用于匹配消息纯文本部分是否与指定字符串（或一系列字符串）完全相同。可选参数 `ignorecase` 用于指定是否忽略大小写，默认为 `False`。\n\n例如，我们可以创建一个匹配消息为 `ping` 或者 `pong` 的规则：\n\n```python\nfrom nonebot.rule import fullmatch\n\nrule = fullmatch((\"ping\", \"pong\"), ignorecase=False)\n```\n\n也可以直接使用辅助函数新建一个响应器：\n\n```python\nfrom nonebot import on_fullmatch\n\nmatcher = on_fullmatch((\"ping\", \"pong\"), ignorecase=False)\n```\n\n### `keyword`\n\n`keyword` 响应规则用于匹配消息纯文本部分是否包含指定字符串（或一系列字符串）。\n\n例如，我们可以创建一个匹配消息中包含 `hello` 或者 `hi` 的规则：\n\n```python\nfrom nonebot.rule import keyword\n\nrule = keyword(\"hello\", \"hi\")\n```\n\n也可以直接使用辅助函数新建一个响应器：\n\n```python\nfrom nonebot import on_keyword\n\nmatcher = on_keyword({\"hello\", \"hi\"})\n```\n\n### `command`\n\n`command` 是最常用的响应规则，它用于匹配消息是否为命令。它会根据配置中的 [Command Start 和 Command Separator](../appendices/config.mdx#command-start-和-command-separator) 来判断消息是否为命令。\n\n例如，当我们配置了 `Command Start` 为 `/`，`Command Separator` 为 `.` 时：\n\n```python\nfrom nonebot.rule import command\n\n# 匹配 \"/help\" 或者 \"/帮助\" 开头的消息\nrule = command(\"help\", \"帮助\")\n# 匹配 \"/help.cmd\" 开头的消息\nrule = command((\"help\", \"cmd\"))\n```\n\n也可以直接使用辅助函数新建一个响应器：\n\n```python\nfrom nonebot import on_command\n\nmatcher = on_command(\"help\", aliases={\"帮助\"})\n```\n\n此外，`command` 响应规则默认允许消息命令与参数间不加空格，如果需要严格匹配命令与参数间的空白符，可以使用 `command` 函数的 `force_whitespace` 参数。`force_whitespace` 参数可以是 bool 类型或者具体的字符串，默认为 `False`。如果为 `True`，则命令与参数间必须有任意个数的空白符；如果为字符串，则命令与参数间必须有且与给定字符串一致的空白符。\n\n```python\nrule = command(\"help\", force_whitespace=True)\nrule = command(\"help\", force_whitespace=\" \")\n```\n\n命令解析后的结果可以通过 [`Command`](./dependency.mdx#command)、[`RawCommand`](./dependency.mdx#rawcommand)、[`CommandArg`](./dependency.mdx#commandarg)、[`CommandStart`](./dependency.mdx#commandstart)、[`CommandWhitespace`](./dependency.mdx#commandwhitespace) 依赖注入获取。\n\n### `shell_command`\n\n`shell_command` 响应规则用于匹配类 shell 命令形式的消息。它首先与 [`command`](#command) 响应规则一样进行命令匹配，如果匹配成功，则会进行进一步的参数解析。参数解析采用 `argparse` 标准库进行，在此基础上添加了消息序列 `Message` 支持。\n\n例如，我们可以创建一个匹配 `/cmd` 命令并且带有 `-v` 选项与默认 `-h` 帮助选项的规则：\n\n```python\nfrom nonebot.rule import shell_command, ArgumentParser\n\nparser = ArgumentParser()\nparser.add_argument(\"-v\", \"--verbose\", action=\"store_true\")\n\nrule = shell_command(\"cmd\", parser=parser)\n```\n\n更多关于 `argparse` 的使用方法请参考 [argparse 文档](https://docs.python.org/zh-cn/3/library/argparse.html)。我们也可以选择不提供 `parser` 参数，这样 `shell_command` 将不会解析参数，但会提供参数列表 `argv`。\n\n直接使用辅助函数新建一个响应器：\n\n```python\nfrom nonebot import on_shell_command\nfrom nonebot.rule import ArgumentParser\n\nparser = ArgumentParser()\nparser.add_argument(\"-v\", \"--verbose\", action=\"store_true\")\n\nmatcher = on_shell_command(\"cmd\", parser=parser)\n```\n\n参数解析后的结果可以通过 [`ShellCommandArgv`](./dependency.mdx#shellcommandargv)、[`ShellCommandArgs`](./dependency.mdx#shellcommandargs) 依赖注入获取。\n\n### `regex`\n\n`regex` 响应规则用于匹配消息是否与指定正则表达式匹配。\n\n:::tip 提示\n正则表达式匹配使用 search 而非 match，如需从头匹配请使用 `r\"^xxx\"` 模式来确保匹配开头。\n:::\n\n例如，我们可以创建一个匹配消息中包含字母并且忽略大小写的规则：\n\n```python\nfrom nonebot.rule import regex\n\nrule = regex(r\"[a-z]+\", flags=re.IGNORECASE)\n```\n\n也可以直接使用辅助函数新建一个响应器：\n\n```python\nfrom nonebot import on_regex\n\nmatcher = on_regex(r\"[a-z]+\", flags=re.IGNORECASE)\n```\n\n正则匹配后的结果可以通过 [`RegexStr`](./dependency.mdx#regexstr)、[`RegexGroup`](./dependency.mdx#regexgroup)、[`RegexDict`](./dependency.mdx#regexdict) 依赖注入获取。\n\n### `to_me`\n\n`to_me` 响应规则用于匹配事件是否与机器人相关。\n\n例如：\n\n```python\nfrom nonebot.rule import to_me\n\nrule = to_me()\n```\n\n### `is_type`\n\n`is_type` 响应规则用于匹配事件类型是否为指定类型（或者一系列类型）。\n\n例如，我们可以创建一个匹配 OneBot v11 私聊和群聊消息事件的规则：\n\n```python\nfrom nonebot.rule import is_type\nfrom nonebot.adapters.onebot.v11 import PrivateMessageEvent, GroupMessageEvent\n\nrule = is_type(PrivateMessageEvent, GroupMessageEvent)\n```\n\n## 响应器组\n\n为了更方便的管理一系列功能相近的响应器，NoneBot 提供了两种响应器组，它们可以帮助我们进行响应器的统一管理。\n\n### `CommandGroup`\n\n`CommandGroup` 可以用于管理一系列具有相同前置命令的子命令响应器。\n\n例如，我们创建 `/cmd`、`/cmd.sub`、`/cmd.help` 三个命令，他们具有相同的优先级：\n\n```python\nfrom nonebot import CommandGroup\n\ngroup = CommandGroup(\"cmd\", priority=10)\n\ncmd = group.command(tuple())\nsub_cmd = group.command(\"sub\")\nhelp_cmd = group.command(\"help\")\n```\n\n命令别名 aliases 默认不会添加 `CommandGroup` 设定的前缀，如果需要为 aliases 添加前缀，可以添加 `prefix_aliases=True` 参数:\n\n```python\nfrom nonebot import CommandGroup\n\ngroup = CommandGroup(\"cmd\", prefix_aliases=True)\n\ncmd = group.command(tuple())\nhelp_cmd = group.command(\"help\", aliases={\"帮助\"})\n```\n\n这样就能成功匹配 `/cmd`、`/cmd.help`、`/cmd.帮助` 命令。如果未设置，将默认匹配 `/cmd`、`/cmd.help`、`/帮助` 命令。\n\n### `MatcherGroup`\n\n`MatcherGroup` 可以用于管理一系列具有相同属性的响应器。\n\n例如，我们创建一个具有相同响应规则的响应器组：\n\n```python\nfrom nonebot.rule import to_me\nfrom nonebot import MatcherGroup\n\ngroup = MatcherGroup(rule=to_me())\n\nmatcher1 = group.on_message()\nmatcher2 = group.on_message()\n```\n\n## 第三方响应规则\n\n### Alconna\n\n[`nonebot-plugin-alconna`](https://github.com/nonebot/plugin-alconna) 是一类提供了拓展响应规则的插件。\n该插件使用 [Alconna](https://github.com/ArcletProject/Alconna) 作为命令解析器，\n是一个简单、灵活、高效的命令参数解析器, 并且不局限于解析命令式字符串。\n\n该插件提供了一类新的事件响应器辅助函数 `on_alconna`，以及 `AlconnaResult` 等依赖注入函数。\n\n基于 `Alconna` 的特性，该插件同时提供了一系列便捷的消息段标注。\n标注可用于在 `Alconna` 中匹配消息中除 text 外的其他消息段，也可用于快速创建各适配器下的消息段。所有标注位于 `nonebot_plugin_alconna.adapters` 中。\n\n该插件同时通过提供 `UniMessage` (通用消息模型) 实现了**跨平台接收和发送消息**的功能。\n\n详情请阅读最佳实践中的 [命令解析拓展](../best-practice/alconna/README.mdx) 章节。\n"
  },
  {
    "path": "website/docs/advanced/plugin-info.md",
    "content": "---\nsidebar_position: 2\ndescription: 填写与获取插件相关的信息\n\noptions:\n  menu:\n    - category: advanced\n      weight: 30\n---\n\n# 插件信息\n\nNoneBot 是一个插件化的框架，可以通过加载插件来扩展功能。同时，我们也可以通过 NoneBot 的插件系统来获取相关信息，例如插件的名称、使用方法，用于收集帮助信息等。下面我们将介绍如何为插件添加元数据，以及如何获取插件信息。\n\n## 插件元数据\n\n在 NoneBot 中，插件 [`Plugin`](../api/plugin/model.md#Plugin) 对象中存储了插件系统所需要的一系列信息。包括插件的索引名称、插件模块、插件中的事件响应器、插件父子关系等。通常，只有插件开发者才需要关心这些信息，而插件使用者或者机器人用户想要看到的是插件使用方法等帮助信息。因此，我们可以为插件添加插件元数据 `PluginMetadata`，它允许插件开发者为插件添加一些额外的信息。这些信息编写于插件模块的顶层，可以直接通过源码查看，或者通过 NoneBot 插件系统获取收集到的信息，通过其他方式发送给机器人用户等。\n\n现在，假设我们有一个插件 `example`, 它的模块结构如下：\n\n```tree {4-6} title=Project\n📦 awesome-bot\n├── 📂 awesome_bot\n│   └── 📂 plugins\n|       └── 📂 example\n|           ├── 📜 __init__.py\n|           └── 📜 config.py\n├── 📜 pyproject.toml\n└── 📜 README.md\n```\n\n我们需要在插件顶层模块 `example/__init__.py` 中添加插件元数据，如下所示：\n\n```python {1,5-12} title=example/__init__.py\nfrom nonebot.plugin import PluginMetadata\n\nfrom .config import Config\n\n__plugin_meta__ = PluginMetadata(\n    name=\"示例插件\",\n    description=\"这是一个示例插件\",\n    usage=\"没什么用\",\n    type=\"application\",\n    config=Config,\n    extra={},\n)\n```\n\n我们可以看到，插件元数据 `PluginMetadata` 有三个基本属性：插件名称、插件描述、插件使用方法。除此之外，还有几个可选的属性（具体填写见[发布插件](../developer/plugin-publishing.mdx#插件元数据)章节）：\n\n- `type`：插件类别，发布插件必填。当前有效类别有：`library`（为其他插件编写提供功能），`application`（向机器人用户提供功能）；\n- `homepage`：插件项目主页，发布插件必填；\n- `config`：插件的[配置类](../appendices/config.mdx#插件配置)，发布插件时如有配置类则必须填写；\n- `supported_adapters`：支持的适配器模块名集合，若插件只使用了 NoneBot 基本抽象，应显式填写 `None`；\n- `extra`：一个字典，可以用于存储任意信息。其他插件可以通过约定 `extra` 字典的键名来达成收集某些特殊信息的目的。\n\n请注意，这里的**插件名称**是供使用者或机器人用户查看的人类可读名称，与插件索引名称无关。**插件索引名称（插件模块名称）**仅用于 NoneBot 插件系统**内部索引**。\n\n## 获取插件信息\n\nNoneBot 提供了多种获取插件对象的方法，例如获取当前所有已导入的插件：\n\n```python\nimport nonebot\n\nplugins: set[Plugin] = nonebot.get_loaded_plugins()\n```\n\n也可以通过插件索引名称获取插件对象：\n\n```python\nimport nonebot\n\nplugin: Plugin | None = nonebot.get_plugin(\"example\")\n```\n\n或者通过模块路径获取插件对象：\n\n```python\nimport nonebot\n\nplugin: Plugin | None = nonebot.get_plugin_by_module_name(\"awesome_bot.plugins.example\")\n```\n\n如果需要获取所有当前声明的插件名称（可能还未加载），可以使用 `get_available_plugin_names` 函数：\n\n```python\nimport nonebot\n\nplugin_names: set[str] = nonebot.get_available_plugin_names()\n```\n\n插件对象 `Plugin` 中包含了多个属性：\n\n- `name`：插件索引名称\n- `module`：插件模块\n- `module_name`：插件模块路径\n- `manager`：插件管理器\n- `matcher`：插件中定义的事件响应器\n- `parent_plugin`：插件的父插件\n- `sub_plugins`：插件的子插件集合\n- `metadata`：插件元数据\n\n通过这些属性以及插件元数据，我们就可以收集所需要的插件信息了。\n"
  },
  {
    "path": "website/docs/advanced/plugin-nesting.md",
    "content": "---\nsidebar_position: 3\ndescription: 编写与加载嵌套插件\n\noptions:\n  menu:\n    - category: advanced\n      weight: 40\n---\n\n# 嵌套插件\n\nNoneBot 支持嵌套插件，即一个插件可以包含其他插件。通过这种方式，我们可以将一个大型插件拆分成多个功能子插件，使得插件更加清晰、易于维护。我们可以直接在插件中使用 NoneBot 加载插件的方法来加载子插件。\n\n## 创建嵌套插件\n\n我们可以在使用 `nb-cli` 命令[创建插件](../tutorial/create-plugin.md#创建插件)时，选择直接通过模板创建一个嵌套插件：\n\n```bash\n$ nb plugin create\n[?] 插件名称: parent\n[?] 使用嵌套插件? (y/N) Y\n[?] 输出目录: awesome_bot/plugins\n```\n\n或者使用 `nb plugin create --sub-plugin` 选项直接创建一个嵌套插件。\n\n## 已有插件\n\n如果你已经有一个插件，想要在其中嵌套加载子插件，可以在插件的 `__init__.py` 中添加如下代码：\n\n```python title=parent/__init__.py\nimport nonebot\nfrom pathlib import Path\n\nsub_plugins = nonebot.load_plugins(\n    str(Path(__file__).parent.joinpath(\"plugins\").resolve())\n)\n```\n\n这样，`parent` 插件就会加载 `parent/plugins` 目录下的所有插件。NoneBot 会正确识别这些插件的父子关系，你可以在 `parent` 的插件信息中看到这些子插件的信息，也可以在子插件信息中看到它们的父插件信息。\n"
  },
  {
    "path": "website/docs/advanced/requiring.md",
    "content": "---\nsidebar_position: 4\ndescription: 使用其他插件提供的功能\n\noptions:\n  menu:\n    - category: advanced\n      weight: 50\n---\n\n# 跨插件访问\n\nNoneBot 插件化系统的设计使得插件之间可以功能独立、各司其职，我们可以更好地维护和扩展插件。但是，有时候我们可能需要在不同插件之间调用功能。NoneBot 生态中就有一类插件，它们专为其他插件提供功能支持，如：[定时任务插件](../best-practice/scheduler.md)、[数据存储插件](../best-practice/data-storing.md)等。这时候我们就需要在插件之间进行跨插件访问。\n\n## 插件跟踪\n\n由于 NoneBot 插件系统通过 [Import Hooks](https://docs.python.org/3/reference/import.html#import-hooks) 的方式实现插件加载与跟踪管理，因此我们**不能**在 NoneBot 跟踪插件前进行模块 import，这会导致插件加载失败。即，我们不能在使用 NoneBot 提供的加载插件方法前，直接使用 `import` 语句导入插件。\n\n对于在项目目录下的插件，我们通常直接使用 `load_from_toml` 等方法一次性加载所有插件。由于这些插件已经被声明，即便插件导入顺序不同，NoneBot 也能正确跟踪插件。此时，我们不需要对跨插件访问进行特殊处理。但当我们使用了外部插件，如果没有事先声明或加载插件，NoneBot 并不会将其当作插件进行跟踪，可能会出现意料之外的错误出现。\n\n简单来说，我们必须在 `import` 外部插件之前，确保依赖的外部插件已经被声明或加载。\n\n## 插件依赖声明\n\nNoneBot 提供了一种方法来确保我们依赖的插件已经被正确加载，即使用 `require` 函数。通过 `require` 函数，我们可以在当前插件中声明依赖的插件，NoneBot 会在加载当前插件时，检查依赖的插件是否已经被加载，如果没有，会尝试优先加载依赖的插件。\n\n假设我们有一个插件 `a` 依赖于插件 `b`，我们可以在插件 `a` 中使用 `require` 函数声明其依赖于插件 `b`：\n\n```python {3} title=a/__init__.py\nfrom nonebot import require\n\nrequire(\"b\")\n\nfrom b import some_function\n```\n\n其中，`require` 函数的参数为插件索引名称或者外部插件的模块名称。在完成依赖声明后，我们可以在插件 `a` 中直接导入插件 `b` 所提供的功能。\n"
  },
  {
    "path": "website/docs/advanced/routing.md",
    "content": "---\nsidebar_position: 9\ndescription: 添加服务端路由规则\n\noptions:\n  menu:\n    - category: advanced\n      weight: 100\n---\n\n# 添加路由\n\n在[驱动器](./driver.md)一节中，我们了解了驱动器的两种类型。既然驱动器可以作为服务端运行，那么我们就可以向驱动器添加路由规则，从而实现自定义的 API 接口等功能。在添加路由规则时，我们需要注意驱动器的类型，详情可以参考[选择驱动器](./driver.md#配置驱动器)。\n\nNoneBot 中，我们可以通过两种途径向 ASGI 驱动器添加路由规则：\n\n1. 通过 NoneBot 的兼容层建立路由规则。\n2. 直接向 ASGI 应用添加路由规则。\n\n这两种途径各有优劣，前者可以在各种服务端型驱动器下运行，但并不能直接使用 ASGI 应用框架提供的特性与功能；后者直接使用 ASGI 应用，更自由、功能完整，但只能在特定类型驱动器下运行。\n\n在向驱动器添加路由规则时，我们需要注意驱动器是否为服务端类型，我们可以通过以下方式判断：\n\n```python\nfrom nonebot import get_driver\nfrom nonebot.drivers import ASGIMixin\n\n# highlight-next-line\ncan_use = isinstance(get_driver(), ASGIMixin)\n```\n\n## 通过兼容层添加路由\n\nNoneBot 兼容层定义了两个数据类 `HTTPServerSetup` 和 `WebSocketServerSetup`，分别用于定义 HTTP 服务端和 WebSocket 服务端的路由规则。\n\n### HTTP 路由\n\n`HTTPServerSetup` 具有四个属性：\n\n- `path`：路由路径，不支持特殊占位表达式。类型为 `URL`。\n- `method`：请求方法。类型为 `str`。\n- `name`：路由名称，不可重复。类型为 `str`。\n- `handle_func`：路由处理函数。类型为 `Callable[[Request], Awaitable[Response]]`。\n\n例如，我们添加一个 `/hello` 的路由，当请求方法为 `GET` 时，返回 `200 OK` 以及返回体信息：\n\n```python\nfrom nonebot import get_driver\nfrom nonebot.drivers import URL, Request, Response, ASGIMixin, HTTPServerSetup\n\nasync def hello(request: Request) -> Response:\n    return Response(200, content=\"Hello, world!\")\n\nif isinstance((driver := get_driver()), ASGIMixin):\n    driver.setup_http_server(\n        HTTPServerSetup(\n            path=URL(\"/hello\"),\n            method=\"GET\",\n            name=\"hello\",\n            handle_func=hello,\n        )\n    )\n```\n\n对于 `Request` 和 `Response` 的详细信息，可以参考 [API 文档](../api/drivers/index.md)。\n\n### WebSocket 路由\n\n`WebSocketServerSetup` 具有三个属性：\n\n- `path`：路由路径，不支持特殊占位表达式。类型为 `URL`。\n- `name`：路由名称，不可重复。类型为 `str`。\n- `handle_func`：路由处理函数。类型为 `Callable[[WebSocket], Awaitable[Any]]`。\n\n例如，我们添加一个 `/ws` 的路由，发送所有接收到的数据：\n\n```python\nfrom nonebot import get_driver\nfrom nonebot.drivers import URL, ASGIMixin, WebSocket, WebSocketServerSetup\n\nasync def ws_handler(ws: WebSocket):\n    await ws.accept()\n    try:\n      while True:\n          data = await ws.receive()\n          await ws.send(data)\n    except WebSocketClosed as e:\n        # handle closed\n        ...\n    finally:\n        with contextlib.suppress(Exception):\n            await websocket.close()\n        # do some cleanup\n\nif isinstance((driver := get_driver()), ASGIMixin):\n    driver.setup_websocket_server(\n        WebSocketServerSetup(\n            path=URL(\"/ws\"),\n            name=\"ws\",\n            handle_func=ws_handler,\n        )\n    )\n```\n\n对于 `WebSocket` 的详细信息，可以参考 [API 文档](../api/drivers/index.md)。\n\n## 使用 ASGI 应用添加路由\n\n### 获取 ASGI 应用\n\nNoneBot 服务端类型的驱动器具有两个属性 `server_app` 和 `asgi`，分别对应驱动框架应用和 ASGI 应用。通常情况下，这两个应用是同一个对象。我们可以通过 `get_app()` 方法快速获取：\n\n```python\nimport nonebot\n\napp = nonebot.get_app()\nasgi = nonebot.get_asgi()\n```\n\n### 添加路由规则\n\n在获取到了 ASGI 应用后，我们就可以直接使用 ASGI 应用框架提供的功能来添加路由规则了。这里我们以 [FastAPI](./driver.md#fastapi默认) 为例，演示如何添加路由规则。\n\n在下面的代码中，我们添加了一个 `GET` 类型的 `/api` 路由，具体方法参考 [FastAPI 文档](https://fastapi.tiangolo.com/)。\n\n```python\nimport nonebot\nfrom fastapi import FastAPI\n\napp: FastAPI = nonebot.get_app()\n\n@app.get(\"/api\")\nasync def custom_api():\n    return {\"message\": \"Hello, world!\"}\n```\n"
  },
  {
    "path": "website/docs/advanced/runtime-hook.md",
    "content": "---\nsidebar_position: 8\ndescription: 在特定的生命周期中执行代码\n\noptions:\n  menu:\n    - category: advanced\n      weight: 90\n---\n\n# 钩子函数\n\n> [钩子编程](https://zh.wikipedia.org/wiki/%E9%92%A9%E5%AD%90%E7%BC%96%E7%A8%8B)（hooking），也称作“挂钩”，是计算机程序设计术语，指通过拦截软件模块间的函数调用、消息传递、事件传递来修改或扩展操作系统、应用程序或其他软件组件的行为的各种技术。处理被拦截的函数调用、事件、消息的代码，被称为钩子（hook）。\n\n在 NoneBot 中有一系列预定义的钩子函数，可以分为两类：**全局钩子函数**和**事件处理钩子函数**，这些钩子函数可以用装饰器的形式来使用。\n\n## 全局钩子函数\n\n全局钩子函数是指 NoneBot 针对其本身运行过程的钩子函数。\n\n这些钩子函数是由驱动器来运行的，故需要先[获得全局驱动器](./driver.md#获取驱动器)。\n\n### 启动准备\n\n这个钩子函数会在 NoneBot 启动时运行。很多时候，我们并不希望在模块被导入时就执行一些耗时操作，如：连接数据库，这时候我们可以在这个钩子函数中进行这些操作。\n\n```python\nfrom nonebot import get_driver\n\ndriver = get_driver()\n\n@driver.on_startup\nasync def do_something():\n    pass\n```\n\n### 终止处理\n\n这个钩子函数会在 NoneBot 终止时运行。我们可以在这个钩子函数中进行一些清理工作，如：关闭数据库连接。\n\n```python\nfrom nonebot import get_driver\n\ndriver = get_driver()\n\n@driver.on_shutdown\nasync def do_something():\n    pass\n```\n\n### Bot 连接处理\n\n这个钩子函数会在任何协议适配器连接 `Bot` 对象至 NoneBot 时运行。支持依赖注入，可以直接注入 `Bot` 对象。\n\n```python\nfrom nonebot import get_driver\n\ndriver = get_driver()\n\n@driver.on_bot_connect\nasync def do_something(bot: Bot):\n    pass\n```\n\n### Bot 断开处理\n\n这个钩子函数会在 `Bot` 断开与 NoneBot 的连接时运行。支持依赖注入，可以直接注入 `Bot` 对象。\n\n```python\nfrom nonebot import get_driver\n\ndriver = get_driver()\n\n@driver.on_bot_disconnect\nasync def do_something(bot: Bot):\n    pass\n```\n\n## 事件处理钩子函数\n\n这些钩子函数指的是影响 NoneBot 进行**事件处理**的函数, 这些函数可以跟普通的事件处理函数一样接受相应的参数。\n\n### 事件预处理\n\n这个钩子函数会在 NoneBot 接收到新的事件时运行。支持依赖注入，可以注入 `Bot` 对象、事件、会话状态。在这个钩子函数内抛出 `nonebot.exception.IgnoredException` 会使 NoneBot 忽略该事件。\n\n```python\nfrom nonebot.exception import IgnoredException\nfrom nonebot.message import event_preprocessor\n\n@event_preprocessor\nasync def do_something(event: Event):\n    if not event.is_tome():\n        raise IgnoredException(\"some reason\")\n```\n\n### 事件后处理\n\n这个钩子函数会在 NoneBot 处理事件完成后运行。支持依赖注入，可以注入 `Bot` 对象、事件、会话状态。\n\n```python\nfrom nonebot.message import event_postprocessor\n\n@event_postprocessor\nasync def do_something(event: Event):\n    pass\n```\n\n### 运行预处理\n\n这个钩子函数会在 NoneBot 运行事件响应器前运行。支持依赖注入，可以注入 `Bot` 对象、事件、事件响应器、会话状态。在这个钩子函数内抛出 `nonebot.exception.IgnoredException` 也会使 NoneBot 忽略本次运行。\n\n```python\nfrom nonebot.message import run_preprocessor\nfrom nonebot.exception import IgnoredException\n\n@run_preprocessor\nasync def do_something(event: Event, matcher: Matcher):\n    if not event.is_tome():\n        raise IgnoredException(\"some reason\")\n```\n\n### 运行后处理\n\n这个钩子函数会在 NoneBot 运行事件响应器后运行。支持依赖注入，可以注入 `Bot` 对象、事件、事件响应器、会话状态、运行中产生的异常。\n\n```python\nfrom nonebot.message import run_postprocessor\n\n@run_postprocessor\nasync def do_something(event: Event, matcher: Matcher, exception: Optional[Exception]):\n    pass\n```\n\n### 平台接口调用钩子\n\n这个钩子函数会在 `Bot` 对象调用平台接口时运行。在这个钩子函数中，我们可以通过引起 `MockApiException` 异常来阻止 `Bot` 对象调用平台接口并返回指定的结果。\n\n```python\nfrom nonebot.adapters import Bot\nfrom nonebot.exception import MockApiException\n\n@Bot.on_calling_api\nasync def handle_api_call(bot: Bot, api: str, data: Dict[str, Any]):\n    if api == \"send_msg\":\n        raise MockApiException(result={\"message_id\": 123})\n```\n\n### 平台接口调用后钩子\n\n这个钩子函数会在 `Bot` 对象调用平台接口后运行。在这个钩子函数中，我们可以通过引起 `MockApiException` 异常来忽略平台接口返回的结果并返回指定的结果。\n\n```python\nfrom nonebot.adapters import Bot\nfrom nonebot.exception import MockApiException\n\n@Bot.on_called_api\nasync def handle_api_result(\n    bot: Bot, exception: Optional[Exception], api: str, data: Dict[str, Any], result: Any\n):\n    if not exception and api == \"send_msg\":\n        raise MockApiException(result={**result, \"message_id\": 123})\n```\n"
  },
  {
    "path": "website/docs/advanced/session-updating.md",
    "content": "---\nsidebar_position: 7\ndescription: 控制会话响应对象\n\noptions:\n  menu:\n    - category: advanced\n      weight: 80\n---\n\n# 会话更新\n\n在 NoneBot 中，在某个事件响应器对事件响应后，即是进入了会话状态，会话状态会持续到整个事件响应流程结束。会话过程中，机器人可以与用户进行多次交互。每次需要等待用户事件时，NoneBot 将会复制一个新的临时事件响应器，并更新该事件响应器使其响应当前会话主体的消息，这个过程称为会话更新。\n\n会话更新分为两部分：**更新[事件响应器类型](./matcher.md#事件响应器类型)**和**更新[事件触发权限](./matcher.md#事件触发权限)**。\n\n## 更新事件响应器类型\n\n通常情况下，与机器人用户进行的会话都是通过消息事件进行的，因此会话更新后的默认响应事件类型为 `message`。如果希望接收一个特定类型的消息，比如 `notice` 等，我们需要自定义响应事件类型更新函数。响应事件类型更新函数是一个 `Dependent`，可以使用依赖注入。\n\n```python {3-5}\nfoo = on_message()\n\n@foo.type_updater\nasync def _() -> str:\n    return \"notice\"\n```\n\n在注册了上述响应事件类型更新函数后，当我们需要等待用户事件时，将只会响应 `notice` 类型的事件。如果希望在会话过程中的不同阶段响应不同类型的事件，我们就需要使用更复杂的逻辑来更新响应事件类型（如：根据会话状态），这里将不再展示。\n\n## 更新事件触发权限\n\n会话通常是由机器人与用户进行的一对一交互，因此会话更新后的默认触发权限为当前事件的会话 ID。这个会话 ID 由协议适配器生成，通常由用户 ID 和群 ID 等组成。如果希望实现更复杂的会话功能（如：多用户同时参与的会话），我们需要自定义触发权限更新函数。触发权限更新函数是一个 `Dependent`，可以使用依赖注入。\n\n```python {5-7}\nfrom nonebot.permission import User\n\nfoo = on_message()\n\n@foo.permission_updater\nasync def _(event: Event, matcher: Matcher) -> Permission:\n    return Permission(User.from_event(event, perm=matcher.permission))\n```\n\n上述权限更新函数是默认的权限更新函数，它将会话的触发权限更新为当前事件的会话 ID。如果我们希望响应多个用户的消息，我们可以如下修改：\n\n```python {5-7}\nfrom nonebot.permission import USER\n\nfoo = on_message()\n\n@foo.permission_updater\nasync def _(matcher: Matcher) -> Permission:\n    return USER(\"session1\", \"session2\", perm=matcher.permission)\n```\n\n请注意，此处为全大写字母的 `USER` 权限，它可以匹配多个会话 ID。通过这种方式，我们可以实现多用户同时参与的会话。\n\n我们已经了解了如何控制会话的更新，相信你已经能够实现更复杂的会话功能了，例如多人小游戏等等。欢迎将你的作品分享到[插件商店](/store/plugins)。\n"
  },
  {
    "path": "website/docs/api/.gitkeep",
    "content": ""
  },
  {
    "path": "website/docs/api/adapters/_category_.json",
    "content": "{\n  \"position\": 15\n}\n"
  },
  {
    "path": "website/docs/api/dependencies/_category_.json",
    "content": "{\n  \"position\": 13\n}\n"
  },
  {
    "path": "website/docs/api/drivers/_category_.json",
    "content": "{\n  \"position\": 14\n}\n"
  },
  {
    "path": "website/docs/api/plugin/_category_.json",
    "content": "{\n  \"position\": 12\n}\n"
  },
  {
    "path": "website/docs/appendices/api-calling.mdx",
    "content": "---\nsidebar_position: 4\ndescription: 使用平台接口，完成更多功能\n\noptions:\n  menu:\n    - category: appendices\n      weight: 50\n---\n\n# 使用平台接口\n\nimport Messenger from \"@/components/Messenger\";\n\n在 NoneBot 中，除了使用事件响应器操作发送文本消息外，我们还可以直接通过使用协议适配器提供的方法来使用平台特定的接口，完成发送特殊消息、获取信息等其他平台提供的功能。同时，在部分无法使用事件响应器的情况中，例如[定时任务](../best-practice/scheduler.md)，我们也可以使用平台接口来完成需要的功能。\n\n## 发送平台特殊消息\n\n在之前的章节中，我们介绍了如何向用户发送文本消息以及[如何处理平台消息](../tutorial/message.md)，现在我们来向用户发送平台特殊消息。\n\n:::caution 注意\n在以下的示例中，我们将使用 `Console` 协议适配器来演示如何发送平台消息。在实际使用中，你需要确保你使用的**消息序列类型**与你所要发送的**平台类型**一致。\n:::\n\n```python {4,7-17} title=weather/__init__.py\nimport inspect\nfrom nonebot.adapters.console import MessageSegment\n\n@weather.got(\"location\", prompt=MessageSegment.emoji(\"question\") + \"请输入地名\")\nasync def got_location(location: str = ArgPlainText()):\n    result = await weather.send(\n        MessageSegment.markdown(\n            inspect.cleandoc(\n                f\"\"\"\n                # {location}\n\n                - 今天\n\n                   ⛅ 多云 20℃~24℃\n                \"\"\"\n            )\n        )\n    )\n```\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/天气\" },\n    { position: \"left\", msg: \"❓请输入地名\" },\n    { position: \"right\", msg: \"北京\" },\n    {\n      position: \"left\",\n      monospace: true,\n      msg: \"┏━━━━━━━━━━━━━━━━┓\\n┃      北京       ┃\\n┗━━━━━━━━━━━━━━━━┛\\n• 今天\\n⛅ 多云 20℃~24℃\",\n    },\n  ]}\n/>\n\n在上面的示例中，我们使用了 `Console` 协议适配器提供的 `MessageSegment` 类来发送平台特定的消息 `emoji` 和 `markdown`。这两种消息可以显示在终端中，但是无法在其他平台上使用。在事件响应器操作中，我们可以使用 `str`、消息序列、消息段、消息模板四种类型来发送消息，但其中只有 `str` 和[纯文本形式的消息模板类型](../tutorial/message.md#使用消息模板)消息可以在所有平台上使用。\n\n`send` 事件响应器操作实际上是由协议适配器通过调用平台 API 来实现的，通常会将 API 调用的结果作为返回值返回。\n\n## 调用平台 API\n\n在 NoneBot 中，我们可以通过 `Bot` 对象来调用协议适配器支持的平台 API，来完成更多的功能。\n\n### 获取 Bot\n\n在调用平台 API 之前，我们首先要获得 Bot 对象。有两种方式可以获得 Bot 对象。\n\n在事件处理流程的上下文中，我们可以直接使用依赖注入 Bot 来获取：\n\n```python {1,4} title=weather/__init__.py\nfrom nonebot.adapters import Bot\n\n@weather.got(\"location\", prompt=\"请输入地名\")\nasync def got_location(bot: Bot, location: str = ArgPlainText()):\n    ...\n```\n\n依赖注入会确保你获得的 Bot 对象与类型注解的 Bot 类型一致。也就是说，如果你使用的是 Bot 基类，将会允许任何平台的 Bot 对象；如果你使用的是平台特定的 Bot 类型，将会只允许该平台的 Bot 对象，其他类型的 Bot 将会跳过这个事件处理函数。更多详情请参考[事件处理重载](./overload.md)。\n\n在其他情况下，我们可以通过 NoneBot 提供的方法来获取 Bot 对象，这些方法将会在[使用适配器](../advanced/adapter.md#获取-bot-对象)中详细介绍：\n\n```python {4,6}\nfrom nonebot import get_bot\n\n# 获取当前所有 Bot 中的第一个\nbot = get_bot()\n# 获取指定 ID 的 Bot\nbot = get_bot(\"bot_id\")\n```\n\n### 调用 API\n\n在获得 Bot 对象后，我们可以通过 Bot 的实例方法来调用平台 API：\n\n```python {2,5}\n# 通过 bot.api_name(**kwargs) 的方法调用 API\nresult = await bot.get_user_info(user_id=12345678)\n\n# 通过 bot.call_api(api_name, **kwargs) 的方法调用 API\nresult = await bot.call_api(\"get_user_info\", user_id=12345678)\n```\n\n:::caution 注意\n实际可以使用的 API 以及参数取决于平台提供的接口以及协议适配器的实现，请参考协议适配器以及平台文档。\n:::\n\n在了解了如何调用 API 后，我们可以来改进 `weather` 插件，使得消息发送后，调用 `Console` 接口响铃提醒机器人用户：\n\n```python {4,18} title=weather/__init__.py\nfrom nonebot.adapters.console import Bot, MessageSegment\n\n@weather.got(\"location\", prompt=MessageSegment.emoji(\"question\") + \"请输入地名\")\nasync def got_location(bot: Bot, location: str = ArgPlainText()):\n    await weather.send(\n        MessageSegment.markdown(\n            inspect.cleandoc(\n                f\"\"\"\n                # {location}\n\n                - 今天\n\n                   ⛅ 多云 20℃~24℃\n                \"\"\"\n            )\n        )\n    )\n    await bot.bell()\n```\n"
  },
  {
    "path": "website/docs/appendices/config.mdx",
    "content": "---\nsidebar_position: 0\ndescription: 读取用户配置来控制插件行为\n\noptions:\n  menu:\n    - category: appendices\n      weight: 10\n---\n\n# 配置\n\nimport Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\n\n配置是项目中非常重要的一部分，为了方便我们控制机器人的行为，NoneBot 提供了一套配置系统。下面我们将会补充[指南](../quick-start.mdx)中的天气插件，使其能够读取用户配置。在这之前，我们需要先了解一下配置系统，如果你已经了解了 NoneBot 中的配置方法，可以跳转到[编写插件配置](#插件配置)。\n\nNoneBot 使用 [`pydantic`](https://docs.pydantic.dev/) 以及 [`python-dotenv`](https://saurabh-kumar.com/python-dotenv/) 来读取 dotenv 配置文件以及环境变量，从而控制机器人行为。配置文件需要符合 dotenv 格式，复杂数据类型需使用 JSON 格式或 [pydantic 支持格式](https://docs.pydantic.dev/usage/types/)填写。\n\nNoneBot 内置的配置项列表及含义可以在[内置配置项](#内置配置项)中查看。\n\n:::caution 注意\n\nNoneBot 自 2.2.0 起兼容了 Pydantic v1 与 v2 版本，以下文档中 Pydantic 相关示例均采用 v2 版本用法。\n\n如果在使用商店或其他第三方插件的过程中遇到 Pydantic 相关警告或报错，例如：\n\n```python\npydantic_core._pydantic_core.ValidationError: 1 validation error for Config\n  Input should be a valid dictionary or instance of Config [type=model_type, input_value=Config(...), input_type=Config]\n```\n\n请考虑降级 Pydantic 至 v1 版本：\n\n```bash\npip install --force-reinstall 'pydantic~=1.10'\n```\n\n:::\n\n## 配置项的加载\n\n在 NoneBot 中，我们可以把配置途径分为 **直接传入**、**系统环境变量**、**dotenv 配置文件** 三种，其加载优先级依次由高到低。\n\n### 直接传入\n\n在 NoneBot 初始化的过程中，可以通过 `nonebot.init()` 传入任意合法的 Python 变量，也可以在初始化完成后直接赋值。\n\n通常，在初始化前的传参会在机器人的入口文件（如 `bot.py`）中进行，而初始化后的赋值可以在任何地方进行。\n\n```python {4,8,9} title=bot.py\nimport nonebot\n\n# 初始化时\nnonebot.init(custom_config1=\"config on init\")\n\n# 初始化后\nconfig = nonebot.get_driver().config\nconfig.custom_config1 = \"changed after init\"\nconfig.custom_config2 = \"new config after init\"\n```\n\n### 系统环境变量\n\n在 dotenv 配置文件中定义的配置项，也会在环境变量中进行寻找。如果在环境变量中发现同名配置项（大小写不敏感），将会覆盖 dotenv 中所填值。\n\n例如，在 dotenv 配置文件中存在配置项 `custom_config`：\n\n```dotenv\nCUSTOM_CONFIG=config in dotenv\n```\n\n同时，设置环境变量：\n\n```bash\n# windows cmd\nset CUSTOM_CONFIG 'config in environment variables'\n# windows powershell\n$Env:CUSTOM_CONFIG='config in environment variables'\n# linux/macOS\nexport CUSTOM_CONFIG='config in environment variables'\n```\n\n那最终 NoneBot 所读取的内容为环境变量中的内容，即 `config in environment variables`。\n\n:::caution 注意\n如果一个环境变量既不是 NoneBot 的[**内置配置项**](#内置配置项)，也不是任何插件所定义的[**插件配置**](#插件配置)，那么 NoneBot 不会自发读取该环境变量，需要在 dotenv 配置文件中先行声明。\n:::\n\n### dotenv 配置文件\n\ndotenv 是一种便捷的跨平台配置通用模式，也是我们推荐的配置方式。\n\nNoneBot 在启动时将会从系统环境变量或者 `.env` 文件中寻找配置项 `ENVIRONMENT` （大小写不敏感），默认值为 `prod`。这将决定 NoneBot 后续进一步加载环境配置的文件路径 `.env.{ENVIRONMENT}`。\n\n#### 配置项解析\n\ndotenv 文件中的配置值使用 JSON 进行解析。如果配置项值无法被解析，将作为**字符串**处理。例如：\n\n```dotenv\nSTRING_CONFIG=some string\nLIST_CONFIG=[1, 2, 3]\nDICT_CONFIG={\"key\": \"value\"}\nMULTILINE_CONFIG='\n[\n  {\n    \"item_key\": \"item_value\"\n  }\n]\n'\nEMPTY_CONFIG=\nNULL_CONFIG\n```\n\n将被解析为：\n\n```python\ndotenv_config = {\n    \"string_config\": \"some string\",\n    \"list_config\": [1, 2, 3],\n    \"dict_config\": {\"key\": \"value\"},\n    \"multiline_config\": [{\"item_key\": \"item_value\"}],\n    \"empty_config\": \"\",\n    \"null_config\": None\n}\n```\n\n特别的，NoneBot 支持使用 `env_nested_delimiter` 配置嵌套字典，在层与层之间使用 `__` 分隔即可：\n\n```dotenv\nDICT={\"k1\": \"v1\", \"k2\": null}\nDICT__K2=v2\nDICT__K3=v3\nDICT__INNER__K4=v4\n```\n\n将被解析为：\n\n```python\ndotenv_config = {\n    \"dict\": {\n        \"k1\": \"v1\",\n        \"k2\": \"v2\",\n        \"k3\": \"v3\",\n        \"inner\": {\n            \"k4\": \"v4\"\n        }\n    }\n}\n```\n\n#### .env 文件\n\n`.env` 文件是基础配置文件，该文件中的配置项在不同环境下都会被加载，但会被 `.env.{ENVIRONMENT}` 文件中的配置所**覆盖**。\n\n我们可以在 `.env` 文件中写入当前的环境信息：\n\n```dotenv\nENVIRONMENT=dev\nCOMMON_CONFIG=common config  # 这个配置项在任何环境中都会被加载\n```\n\n这样，我们在启动 NoneBot 时就会从 `.env.dev` 文件中加载剩余配置项。\n\n:::tip 提示\n在生产环境中，可以通过设置环境变量 `ENVIRONMENT=prod` 来确保 NoneBot 读取正确的环境配置。\n:::\n\n#### .env.\\{ENVIRONMENT\\} 文件\n\n`.env.{ENVIRONMENT}` 文件类似于预设，可以让我们在多套不同的配置方案中灵活切换，默认 NoneBot 会读取 `.env.prod` 配置。如果你使用了 `nb-cli` 创建 `simple` 项目，那么将含有两套预设配置：`.env.dev` 和 `.env.prod`。\n\n在 NoneBot 初始化时，可以指定加载某个环境配置文件：\n\n```python\nnonebot.init(_env_file=\".env.dev\")\n```\n\n这将忽略在 `.env` 文件或环境变量中指定的 `ENVIRONMENT` 配置项。\n\n## 读取全局配置项\n\nNoneBot 的全局配置对象可以通过 `driver` 获取，如：\n\n```python\nimport nonebot\n\nconfig = nonebot.get_driver().config\n```\n\n如果我们需要获取某个配置项，可以直接通过 `config` 对象的属性访问：\n\n```python\nsuperusers = config.superusers\n```\n\n如果配置项不存在，将会抛出异常。\n\n## 插件配置\n\n在一个涉及大量配置项的项目中，通过直接读取全局配置项的方式显然并不高效。同时，由于额外的全局配置项没有预先定义，开发时编辑器将无法提示字段与类型，并且运行时没有对配置项直接进行合法性检查。那么就需要一种方式来规范定义插件配置项。\n\n在 NoneBot 中，我们使用强大高效的 `pydantic` 来定义配置模型，这个模型可以被用于配置的读取和类型检查等。例如在 `weather` 插件目录中新建 `config.py` 来定义一个模型：\n\n```python title=weather/config.py\nfrom pydantic import BaseModel, field_validator\n\nclass Config(BaseModel):\n    weather_api_key: str\n    weather_command_priority: int = 10\n    weather_plugin_enabled: bool = True\n\n    @field_validator(\"weather_command_priority\")\n    @classmethod\n    def check_priority(cls, v: int) -> int:\n        if v >= 1:\n            return v\n        raise ValueError(\"weather command priority must greater than 1\")\n```\n\n在 `config.py` 中，我们定义了一个 `Config` 类，它继承自 `pydantic.BaseModel`，并定义了一些配置项。在 `Config` 类中，我们还定义了一个 `check_priority` 方法，它用于检查 `weather_command_priority` 配置项的合法性。更多关于 `pydantic` 的编写方式，可以参考 [pydantic 官方文档](https://docs.pydantic.dev/)。\n\n在定义好配置模型后，我们可以在插件加载时通过配置模型获取插件配置：\n\n```python {5,11} title=weather/__init__.py\nfrom nonebot import get_plugin_config\n\nfrom .config import Config\n\nplugin_config = get_plugin_config(Config)\n\nweather = on_command(\n    \"天气\",\n    rule=to_me(),\n    aliases={\"weather\", \"查天气\"},\n    priority=plugin_config.weather_command_priority,\n    block=True,\n)\n```\n\n然后，我们便可以从 `plugin_config` 中读取配置了，例如 `plugin_config.weather_api_key`。\n\n这种方式可以简洁、高效地读取配置项，同时也可以设置默认值或者在运行时对配置项进行合法性检查，防止由于配置项导致的插件出错等情况出现。\n\n:::tip 可配置的事件响应优先级\n发布插件应该为自身的事件响应器提供可配置的优先级，以便插件使用者可以自定义多个插件间的响应顺序。\n:::\n\n:::tip 插件配置获取逻辑\n无论是否在 dotenv 文件中声明了插件配置项，使用 `get_plugin_config` 获取插件配置模型中定义的配置项时都遵循[**配置项的加载**](#配置项的加载)一节中的优先级顺序进行读取。\n:::\n\n### 避免插件配置名称冲突\n\n由于插件配置项是从全局配置和环境变量中读取的，通常我们需要在配置项名称前面添加前缀名，以防止配置项冲突。例如在上方的示例中，我们就添加了配置项前缀 `weather_`。但是这样会导致使用配置项时变量名过长，此时我们可以使用 `pydantic` 的 `alias` 或者通过配置 scope 来简化配置项名称。这里我们以 scope 配置为例：\n\n```python title=weather/config.py\nfrom pydantic import BaseModel\n\nclass ScopedConfig(BaseModel):\n    api_key: str\n    command_priority: int = 10\n    plugin_enabled: bool = True\n\nclass Config(BaseModel):\n    weather: ScopedConfig\n```\n\n```python title=weather/__init__.py\nfrom nonebot import get_plugin_config\n\nfrom .config import Config\n\nplugin_config = get_plugin_config(Config).weather\n```\n\n这样我们就可以省略插件配置项名称中的前缀 `weather_` 了。但需要注意的是，如果我们使用了 scope 配置，那么在配置文件中也需要使用 [`env_nested_delimiter` 格式](#配置项解析)，例如：\n\n```dotenv\nWEATHER__API_KEY=123456\nWEATHER__COMMAND_PRIORITY=10\n```\n\n## 内置配置项\n\n配置项 API 文档可以前往 [Config 类](../api/config.md#Config)查看。\n\n### Driver\n\n- **类型**: `str`\n- **默认值**: `\"~fastapi\"`\n\nNoneBot 运行所使用的驱动器。具体配置方法可以参考[安装驱动器](../tutorial/store.mdx#安装驱动器)和[选择驱动器](../advanced/driver.md)。\n\n<Tabs groupId=\"configMethod\">\n  <TabItem value=\"dotenv\" label=\"dotenv\" default>\n\n```dotenv\nDRIVER=~fastapi+~httpx+~websockets\n```\n\n  </TabItem>\n  <TabItem value=\"env\" label=\"系统环境变量\">\n\n```bash\n# windows cmd\nset DRIVER '~fastapi+~httpx+~websockets'\n# windows powershell\n$Env:DRIVER='~fastapi+~httpx+~websockets'\n# linux/macOS\nexport DRIVER='~fastapi+~httpx+~websockets'\n```\n\n  </TabItem>\n  <TabItem value=\"init\" label=\"直接传入\">\n\n```python title=bot.py\nimport nonebot\n\nnonebot.init(driver=\"~fastapi+~httpx+~websockets\")\n```\n\n  </TabItem>\n</Tabs>\n\n### Host\n\n- **类型**: `IPvAnyAddress`\n- **默认值**: `127.0.0.1`\n\n当 NoneBot 作为服务端时，监听的 IP / 主机名。\n\n<Tabs groupId=\"configMethod\">\n  <TabItem value=\"dotenv\" label=\"dotenv\" default>\n\n```dotenv\nHOST=127.0.0.1\n```\n\n  </TabItem>\n  <TabItem value=\"env\" label=\"系统环境变量\">\n\n```bash\n# windows cmd\nset HOST '127.0.0.1'\n# windows powershell\n$Env:HOST='127.0.0.1'\n# linux/macOS\nexport HOST='127.0.0.1'\n```\n\n  </TabItem>\n  <TabItem value=\"init\" label=\"直接传入\">\n\n```python title=bot.py\nimport nonebot\n\nnonebot.init(host=\"127.0.0.1\")\n```\n\n  </TabItem>\n</Tabs>\n\n### Port\n\n- **类型**: `int` (1 ~ 65535)\n- **默认值**: `8080`\n\n当 NoneBot 作为服务端时，监听的端口。\n\n<Tabs groupId=\"configMethod\">\n  <TabItem value=\"dotenv\" label=\"dotenv\" default>\n\n```dotenv\nPORT=8080\n```\n\n  </TabItem>\n  <TabItem value=\"env\" label=\"系统环境变量\">\n\n```bash\n# windows cmd\nset PORT '8080'\n# windows powershell\n$Env:PORT='8080'\n# linux/macOS\nexport PORT='8080'\n```\n\n  </TabItem>\n  <TabItem value=\"init\" label=\"直接传入\">\n\n```python title=bot.py\nimport nonebot\n\nnonebot.init(port=8080)\n```\n\n  </TabItem>\n</Tabs>\n\n### Log Level\n\n- **类型**: `int | str`\n- **默认值**: `INFO`\n\nNoneBot 日志输出等级，可以为 `int` 类型等级或等级名称。具体等级对照表参考 [loguru 日志等级](https://loguru.readthedocs.io/en/stable/api/logger.html#levels)。\n\n:::tip 提示\n日志等级名称应为大写，如 `INFO`。\n:::\n\n<Tabs groupId=\"configMethod\">\n  <TabItem value=\"dotenv\" label=\"dotenv\" default>\n\n```dotenv\nLOG_LEVEL=DEBUG\n```\n\n  </TabItem>\n  <TabItem value=\"env\" label=\"系统环境变量\">\n\n```bash\n# windows cmd\nset LOG_LEVEL 'DEBUG'\n# windows powershell\n$Env:LOG_LEVEL='DEBUG'\n# linux/macOS\nexport LOG_LEVEL='DEBUG'\n```\n\n  </TabItem>\n  <TabItem value=\"init\" label=\"直接传入\">\n\n```python title=bot.py\nimport nonebot\n\nnonebot.init(log_level=\"DEBUG\")\n```\n\n  </TabItem>\n</Tabs>\n\n### API Timeout\n\n- **类型**: `float | None`\n- **默认值**: `30.0`\n\n调用平台接口的超时时间，单位为秒。`None` 表示不设置超时时间。\n\n<Tabs groupId=\"configMethod\">\n  <TabItem value=\"dotenv\" label=\"dotenv\" default>\n\n```dotenv\nAPI_TIMEOUT=10.0\n```\n\n  </TabItem>\n  <TabItem value=\"env\" label=\"系统环境变量\">\n\n```bash\n# windows cmd\nset API_TIMEOUT '10.0'\n# windows powershell\n$Env:API_TIMEOUT='10.0'\n# linux/macOS\nexport API_TIMEOUT='10.0'\n```\n\n  </TabItem>\n  <TabItem value=\"init\" label=\"直接传入\">\n\n```python title=bot.py\nimport nonebot\n\nnonebot.init(api_timeout=10.0)\n```\n\n  </TabItem>\n</Tabs>\n\n### SuperUsers\n\n- **类型**: `set[str]`\n- **默认值**: `set()`\n\n机器人超级用户，可以使用权限 [`SUPERUSER`](../api/permission.md#SUPERUSER)。\n\n<Tabs groupId=\"configMethod\">\n  <TabItem value=\"dotenv\" label=\"dotenv\" default>\n\n```dotenv\nSUPERUSERS=[\"123123123\"]\n```\n\n  </TabItem>\n  <TabItem value=\"env\" label=\"系统环境变量\">\n\n```bash\n# windows cmd\nset SUPERUSERS '[\"123123123\"]'\n# windows powershell\n$Env:SUPERUSERS='[\"123123123\"]'\n# linux/macOS\nexport SUPERUSERS='[\"123123123\"]'\n```\n\n  </TabItem>\n  <TabItem value=\"init\" label=\"直接传入\">\n\n```python title=bot.py\nimport nonebot\n\nnonebot.init(superusers={\"123123123\"})\n```\n\n  </TabItem>\n</Tabs>\n\n### Nickname\n\n- **类型**: `set[str]`\n- **默认值**: `set()`\n\n机器人昵称，通常协议适配器会根据用户是否 @bot 或者是否以机器人昵称开头来判断是否是向机器人发送的消息。\n\n<Tabs groupId=\"configMethod\">\n  <TabItem value=\"dotenv\" label=\"dotenv\" default>\n\n```dotenv\nNICKNAME=[\"bot\"]\n```\n\n  </TabItem>\n  <TabItem value=\"env\" label=\"系统环境变量\">\n\n```bash\n# windows cmd\nset NICKNAME '[\"bot\"]'\n# windows powershell\n$Env:NICKNAME='[\"bot\"]'\n# linux/macOS\nexport NICKNAME='[\"bot\"]'\n```\n\n  </TabItem>\n  <TabItem value=\"init\" label=\"直接传入\">\n\n```python title=bot.py\nimport nonebot\n\nnonebot.init(nickname={\"bot\"})\n```\n\n  </TabItem>\n</Tabs>\n\n### Command Start 和 Command Separator\n\n- **类型**: `set[str]`\n- **默认值**:\n  - Command Start: `{\"/\"}`\n  - Command Separator: `{\".\"}`\n\n命令消息的起始符和分隔符。用于 [`command`](../advanced/matcher.md#command) 规则。\n\n<Tabs groupId=\"configMethod\">\n  <TabItem value=\"dotenv\" label=\"dotenv\" default>\n\n```dotenv\nCOMMAND_START=[\"/\", \"\"]\nCOMMAND_SEP=[\".\", \" \"]\n```\n\n  </TabItem>\n  <TabItem value=\"env\" label=\"系统环境变量\">\n\n```bash\n# windows cmd\nset COMMAND_START '[\"/\", \"\"]'\nset COMMAND_SEP '[\".\", \" \"]'\n# windows powershell\n$Env:COMMAND_START='[\"/\", \"\"]'\n$Env:COMMAND_SEP='[\".\", \" \"]'\n# linux/macOS\nexport COMMAND_START='[\"/\", \"\"]'\nexport COMMAND_SEP='[\".\", \" \"]'\n```\n\n  </TabItem>\n  <TabItem value=\"init\" label=\"直接传入\">\n\n```python title=bot.py\nimport nonebot\n\nnonebot.init(command_start={\"/\", \"\"}, command_sep={\".\", \" \"})\n```\n\n  </TabItem>\n</Tabs>\n\n### Session Expire Timeout\n\n- **类型**: `timedelta`\n- **默认值**: `timedelta(minutes=2)`\n\n用户会话超时时间，配置格式参考 [Datetime Types](https://docs.pydantic.dev/latest/api/standard_library_types/#datetimetimedelta)。\n\n<Tabs groupId=\"configMethod\">\n  <TabItem value=\"dotenv\" label=\"dotenv\" default>\n\n```dotenv\nSESSION_EXPIRE_TIMEOUT=00:02:00\n```\n\n  </TabItem>\n  <TabItem value=\"env\" label=\"系统环境变量\">\n\n```bash\n# windows cmd\nset SESSION_EXPIRE_TIMEOUT '00:02:00'\n# windows powershell\n$Env:SESSION_EXPIRE_TIMEOUT='00:02:00'\n# linux/macOS\nexport SESSION_EXPIRE_TIMEOUT='00:02:00'\n```\n\n  </TabItem>\n  <TabItem value=\"init\" label=\"直接传入\">\n\n```python title=bot.py\nimport nonebot\n\nnonebot.init(session_expire_timeout=120)\n```\n\n  </TabItem>\n</Tabs>\n"
  },
  {
    "path": "website/docs/appendices/log.md",
    "content": "---\nsidebar_position: 6\ndescription: 记录与控制日志\n\noptions:\n  menu:\n    - category: appendices\n      weight: 70\n---\n\n# 日志\n\n无论是在开发还是在生产环境中，日志都是一个重要的功能，可以帮助我们了解运行状况、排查问题等。虽然我们可以使用 `print` 来将需要的信息输出到控制台，但是这种方式难以控制，而且不利于日志的归档、分析等。NoneBot 使用优秀的 [Loguru](https://loguru.readthedocs.io/) 库来进行日志记录。\n\n## 记录日志\n\n我们可以从 NoneBot 中导入 `logger` 对象，然后使用 `logger` 对象的方法来记录日志。\n\n```python\nfrom nonebot import logger\n\nlogger.trace(\"This is a trace message\")\nlogger.debug(\"This is a debug message\")\nlogger.info(\"This is an info message\")\nlogger.success(\"This is a success message\")\nlogger.warning(\"This is a warning message\")\nlogger.error(\"This is an error message\")\nlogger.critical(\"This is a critical message\")\n```\n\n我们仅需一行代码即可记录对应级别的日志。日志可以通过配置 [`LOG_LEVEL` 配置项](./config.mdx#log-level)来过滤输出等级，控制台中仅会输出大于等于 `LOG_LEVEL` 的日志。默认的 `LOG_LEVEL` 为 `INFO`，即只会输出 `INFO`、`SUCCESS`、`WARNING`、`ERROR`、`CRITICAL` 级别的日志。\n\n如果需要记录 `Exception traceback` 日志，可以向 `logger` 添加 `exception` 选项：\n\n```python {4}\ntry:\n    1 / 0\nexcept ZeroDivisionError:\n    logger.opt(exception=True).error(\"ZeroDivisionError\")\n```\n\n如果需要输出彩色日志，可以向 `logger` 添加 `colors` 选项：\n\n```python\nlogger.opt(colors=True).warning(\"We got a <red>BIG</red> problem\")\n```\n\n更多日志记录方法请参考 [Loguru 文档](https://loguru.readthedocs.io/)。\n\n## 自定义日志输出\n\nNoneBot 在启动时会添加一个默认的日志处理器，该处理器会将日志输出到**stdout**，并且根据 `LOG_LEVEL` 配置项过滤日志等级。\n\n默认的日志格式为：\n\n```text\n<g>{time:MM-DD HH:mm:ss}</g> [<lvl>{level}</lvl>] <c><u>{name}</u></c> | {message}\n```\n\n我们可以从 `nonebot.log` 模块导入以使用 NoneBot 的默认格式和过滤器：\n\n```python\nfrom nonebot.log import default_format, default_filter\n```\n\n如果需要自定义日志格式，我们需要移除 NoneBot 默认的日志处理器并添加新的日志处理器。例如，在机器人入口文件中 `nonebot.init` 之前添加以下内容：\n\n```python title=bot.py\nfrom nonebot.log import logger_id\n\n# 移除 NoneBot 默认的日志处理器\nlogger.remove(logger_id)\n# 添加新的日志处理器\nlogger.add(\n    sys.stdout,\n    level=0,\n    diagnose=True,\n    format=\"<g>{time:MM-DD HH:mm:ss}</g> [<lvl>{level}</lvl>] <c><u>{name}</u></c> | {message}\",\n    filter=default_filter\n)\n```\n\n如果想要输出日志到文件，我们可以使用 `logger.add` 方法添加文件处理器：\n\n```python title=bot.py\nlogger.add(\"error.log\", level=\"ERROR\", format=default_format, rotation=\"1 week\")\n```\n\n更多日志处理器的使用方法请参考 [Loguru 文档](https://loguru.readthedocs.io/)。\n\n## 重定向 logging 日志\n\n`logging` 是 Python 标准库中的日志模块，NoneBot 提供了一个 logging handler 用于将 `logging` 日志重定向到 `loguru` 处理。\n\n```python\nfrom nonebot.log import LoguruHandler\n\n# root logger 添加 LoguruHandler\nlogging.basicConfig(handlers=[LoguruHandler()])\n# 或者为其他 logging.Logger 添加 LoguruHandler\nlogger.addHandler(LoguruHandler())\n```\n"
  },
  {
    "path": "website/docs/appendices/overload.md",
    "content": "---\nsidebar_position: 7\ndescription: 根据事件类型进行不同的处理\n\noptions:\n  menu:\n    - category: appendices\n      weight: 80\n---\n\n# 事件类型与重载\n\n在之前的示例中，我们已经了解了如何[获取事件信息](../tutorial/event-data.mdx)以及[使用平台接口](./api-calling.mdx)。但是，事件信息通常不仅仅包含消息这一个内容，还有其他平台提供的信息，例如消息发送时间、消息发送者等等。同时，在使用平台接口时，我们需要确保使用的**平台接口**与所要发送的**平台类型**一致，对不同类型的事件需要做出不同的处理。在本章节中，我们将介绍如何获取事件更多的信息以及根据事件类型进行不同的处理。\n\n## 事件类型\n\n在 NoneBot 中，事件均是 `nonebot.adapters.Event` 基类的子类型，基类对一些必要的属性进行了抽象，子类型则根据不同的平台进行了实现。在[自定义权限](./permission.mdx#自定义权限)一节中，我们就使用了 `Event` 的抽象方法 `get_user_id` 来获取事件发送者 ID，这个方法由协议适配器进行了实现，返回机器人用户对应的平台 ID。更多的基类抽象方法可以在[使用适配器](../advanced/adapter.md#获取事件通用信息)中查看。\n\n既然事件是基类的子类型，我们实际可以获得的信息通常多于基类抽象方法所提供的。如果我们不满足于基类能获得的信息，我们可以小小的修改一下事件处理函数的事件参数类型注解，使其变为子类型，这样我们就可以通过协议适配器定义的子类型来获取更多的信息。我们以 `Console` 协议适配器为例：\n\n```python {4} title=weather/__init__.py\nfrom nonebot.adapters.console import MessageEvent\n\n@weather.got(\"location\", prompt=\"请输入地名\")\nasync def got_location(event: MessageEvent, location: str = ArgPlainText()):\n    await weather.finish(f\"{event.time.strftime('%Y-%m-%d')} {location} 的天气是...\")\n```\n\n在上面的代码中，我们获取了 `Console` 协议适配器的消息事件提供的发送时间 `time` 属性。\n\n:::caution 注意\n如果**基类**就能满足你的需求，那么就**不要修改**事件参数类型注解，这样可以使你的代码更加**通用**，可以在更多平台上运行。如何根据不同平台事件类型进行不同的处理，我们将在[重载](#重载)一节中介绍。\n:::\n\n## 重载\n\n我们在编写机器人时，常常会遇到这样一个问题：如何对私聊和群聊消息进行不同的处理？如何对不同平台的事件进行不同的处理？针对这些问题，NoneBot 提供了一个便捷而高效的解决方案 ── 重载。简单来说，依赖函数会根据其参数的类型注解来决定是否执行，忽略不符合其参数类型注解的情况。这样，我们就可以通过修改事件参数类型注解来实现对不同事件的处理，或者修改 `Bot` 参数类型注解来实现使用不同平台的接口。我们以 `OneBot` 协议适配器为例：\n\n```python {4,8}\nfrom nonebot.adapters.onebot.v11 import PrivateMessageEvent, GroupMessageEvent\n\n@matcher.handle()\nasync def handle_private(event: PrivateMessageEvent):\n    await matcher.finish(\"私聊消息\")\n\n@matcher.handle()\nasync def handle_group(event: GroupMessageEvent):\n    await matcher.finish(\"群聊消息\")\n```\n\n这样，机器人用户就会在私聊和群聊中分别收到不同的回复。同样的，我们也可以通过修改 `Bot` 参数类型注解来实现使用不同平台的接口：\n\n```python\nfrom nonebot.adapters.console import Bot as ConsoleBot\nfrom nonebot.adapters.onebot.v11 import Bot as OneBot\n\n@matcher.handle()\nasync def handle_console(bot: ConsoleBot):\n    await bot.bell()\n\n@matcher.handle()\nasync def handle_onebot(bot: OneBot):\n    await bot.send_group_message(group_id=123123, message=\"OneBot\")\n```\n\n:::caution 注意\n重载机制对所有的参数类型注解都有效，因此，依赖注入也可以使用这个特性来对不同的返回值进行处理。\n\n但 Bot、Event 和 Matcher 三者的参数类型注解具有最高检查优先级，如果三者任一类型注解不匹配，那么其他依赖注入将不会执行（如：`Depends`）。\n:::\n\n:::tip 提示\n如何更好地编写一个跨平台的插件，我们将在[最佳实践](../best-practice/multi-adapter.mdx)中介绍。\n:::\n"
  },
  {
    "path": "website/docs/appendices/permission.mdx",
    "content": "---\nsidebar_position: 5\ndescription: 控制事件响应器的权限\n\noptions:\n  menu:\n    - category: appendices\n      weight: 60\n---\n\n# 权限控制\n\nimport Messenger from \"@site/src/components/Messenger\";\n\n**权限控制**是机器人在实际应用中需要解决的重点问题之一，NoneBot 提供了灵活的权限控制机制 —— `Permission`。\n\n类似于响应规则 `Rule`，`Permission` 是由非负整数个 `PermissionChecker` 所共同组成的**用于筛选事件**的对象。但需要特别说明的是，权限和响应规则有如下区别：\n\n1. 权限检查**先于**响应规则检查\n2. `Permission` 只需**其中一个** `PermissionChecker` 返回 `True` 时就会检查通过\n3. 权限检查进行时，上下文中并不存在会话状态 `state`\n4. `Rule` 仅在**初次触发**事件响应器时进行检查，在余下的会话中并不会限制事件；而 `Permission` 会**持续生效**，在连续对话中一直对事件主体加以限制。\n\n## 基础使用\n\n通常情况下，`Permission` 更侧重于对于**触发事件的机器人用户**的筛选，例如由 NoneBot 自身提供的 `SUPERUSER` 权限，便是筛选出会话发起者是否为超级用户。它可以对输入的用户进行鉴别，如果符合要求则会被认为通过并返回 `True`，反之则返回 `False`。\n\n简单来说，`Permission` 是一个用于筛选出符合要求的用户的机制，可以通过 `Permission` 精确的控制响应对象的覆盖范围，从而拒绝掉我们所不希望的事件。\n\n例如，我们可以在 `weather` 插件中添加一个超级用户可用的指令：\n\n```python {3,9} title=weather/__init__.py\nfrom typing import Tuple\nfrom nonebot.params import Command\nfrom nonebot.permission import SUPERUSER\n\nmanage = on_command(\n    (\"天气\", \"启用\"),\n    rule=to_me(),\n    aliases={(\"天气\", \"禁用\")},\n    permission=SUPERUSER,\n)\n\n@manage.handle()\nasync def control(cmd: Tuple[str, str] = Command()):\n    _, action = cmd\n    if action == \"启用\":\n        plugin_config.weather_plugin_enabled = True\n    elif action == \"禁用\":\n        plugin_config.weather_plugin_enabled = False\n    await manage.finish(f\"天气插件已{action}\")\n```\n\n如上方示例所示，在注册事件响应器时，我们设置了 `permission` 参数，那么这个事件处理器在触发事件前的检查阶段会对用户身份进行验证，如果不符合我们设置的条件（此处即为**超级用户**）则不会响应。此时，我们向机器人发送 `/天气.禁用` 指令，机器人不会有任何响应，因为我们还不是机器人的超级管理员。我们在 dotenv 文件中设置了 `SUPERUSERS` 配置项之后，机器人就会响应我们的指令了。\n\n```dotenv title=.env\nSUPERUSERS=[\"console_user\"]\n```\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/天气.禁用\" },\n    { position: \"left\", msg: \"天气插件已禁用\" },\n    { position: \"right\", msg: \"/天气.启用\" },\n    { position: \"left\", msg: \"天气插件已启用\" },\n  ]}\n/>\n\n## 自定义权限\n\n与事件响应规则类似，`PermissionChecker` 也是一个返回值为 `bool` 类型的依赖函数，即 `PermissionChecker` 支持依赖注入。例如，我们可以限制用户的指令调用次数：\n\n```python title=weather/__init__.py\nfrom nonebot.adapters import Event\n\nfake_db: Dict[str, int] = {}\n\nasync def limit_permission(event: Event):\n    count = fake_db.setdefault(event.get_user_id(), 100)\n    if count > 0:\n        fake_db[event.get_user_id()] -= 1\n        return True\n    return False\n\nweather = on_command(\"天气\", permission=limit_permission)\n```\n\n## 权限组合\n\n权限之间可以通过 `|` 运算符进行组合，使得任意一个权限检查返回 `True` 时通过。例如：\n\n```python {4-6}\nperm1 = Permission(foo_checker)\nperm2 = Permission(bar_checker)\n\nperm = perm1 | perm2\nperm = perm1 | bar_checker\nperm = foo_checker | perm2\n```\n\n同样的，我们也无需担心组合了一个 `None` 值，`Permission` 会自动忽略 `None` 值。\n\n```python\nassert (perm | None) is perm\n```\n\n## 主动使用权限\n\n除了在事件响应器中使用权限外，我们也可以主动使用权限来判断事件是否符合条件。例如：\n\n```python {3}\nperm = Permission(some_checker)\n\nresult: bool = await perm(bot, event)\n```\n\n我们只需要传入 `Bot` 实例、事件，`Permission` 会并发调用所有 `PermissionChecker` 进行检查，并返回结果。\n"
  },
  {
    "path": "website/docs/appendices/rule.md",
    "content": "---\nsidebar_position: 1\ndescription: 自定义响应规则\n\noptions:\n  menu:\n    - category: appendices\n      weight: 20\n---\n\n# 响应规则\n\n机器人在实际应用中，往往会接收到多种多样的事件类型，NoneBot 通过响应规则来控制事件的处理。\n\n在[指南](../tutorial/matcher.md#为事件响应器添加参数)中，我们为 `weather` 命令添加了一个 `rule=to_me()` 参数，这个参数就是一个响应规则，确保只有在私聊或者 `@bot` 时才会响应。\n\n响应规则是一个 `Rule` 对象，它由一系列的 `RuleChecker` 函数组成，每个 `RuleChecker` 函数都会检查事件是否符合条件，如果所有的检查都通过，则事件会被处理。\n\n## RuleChecker\n\n`RuleChecker` 是一个返回值为 `bool` 类型的依赖函数，即 `RuleChecker` 支持依赖注入。我们可以根据上一节中添加的[配置项](./config.mdx#插件配置)，在 `weather` 插件目录中编写一个响应规则：\n\n```python {7,8} title=weather/__init__.py\nfrom nonebot import get_plugin_config\n\nfrom .config import Config\n\nplugin_config = get_plugin_config(Config)\n\nasync def is_enable() -> bool:\n    return plugin_config.weather_plugin_enabled\n\nweather = on_command(\"天气\", rule=is_enable)\n```\n\n在上面的代码中，我们定义了一个函数 `is_enable`，它会检查配置项 `weather_plugin_enabled` 是否为 `True`。这个函数 `is_enable` 即为一个 `RuleChecker`。\n\n## Rule\n\n`Rule` 是若干个 `RuleChecker` 的集合，它会并发调用每个 `RuleChecker`，只有当所有 `RuleChecker` 检查通过时匹配成功。例如：我们可以组合两个 `RuleChecker`，一个用于检查插件是否启用，一个用于检查用户是否在黑名单中：\n\n```python {10}\nfrom nonebot.rule import Rule\nfrom nonebot.adapters import Event\n\nasync def is_enable() -> bool:\n    return plugin_config.weather_plugin_enabled\n\nasync def is_blacklisted(event: Event) -> bool:\n    return event.get_user_id() not in BLACKLIST\n\nrule = Rule(is_enable, is_blacklisted)\n\nweather = on_command(\"天气\", rule=rule)\n```\n\n## 合并响应规则\n\n在定义响应规则时，我们可以将规则进行细分，来更好地复用规则。而在使用时，我们需要合并多个规则。除了使用 `Rule` 对象来组合多个 `RuleChecker` 外，我们还可以对 `Rule` 对象进行合并。在原 `weather` 插件中，我们可以将 `rule=to_me()` 与 `rule=is_enable` 使用 `&` 运算符合并：\n\n```python {13} title=weather/__init__.py\nfrom nonebot.rule import to_me\nfrom nonebot import get_plugin_config\n\nfrom .config import Config\n\nplugin_config = get_plugin_config(Config)\n\nasync def is_enable() -> bool:\n    return plugin_config.weather_plugin_enabled\n\nweather = on_command(\n    \"天气\",\n    rule=to_me() & is_enable,\n    aliases={\"weather\", \"查天气\"},\n    priority=plugin_config.weather_command_priority,\n    block=True,\n)\n```\n\n这样，`weather` 命令就只会在插件启用且在私聊或者 `@bot` 时才会响应。\n\n合并响应规则可以有多种形式，例如：\n\n```python {4-6}\nrule1 = Rule(foo_checker)\nrule2 = Rule(bar_checker)\n\nrule = rule1 & rule2\nrule = rule1 & bar_checker\nrule = foo_checker & rule2\n```\n\n同时，我们也无需担心合并了一个 `None` 值，`Rule` 会忽略 `None` 值。\n\n```python\nassert (rule & None) is rule\n```\n\n## 主动使用响应规则\n\n除了在事件响应器中使用响应规则外，我们也可以主动使用响应规则来判断事件是否符合条件。例如：\n\n```python {3}\nrule = Rule(some_checker)\n\nresult: bool = await rule(bot, event, state)\n```\n\n我们只需要传入 `Bot` 对象、事件和会话状态，`Rule` 会并发调用所有 `RuleChecker` 进行检查，并返回结果。\n\n## 内置响应规则\n\nNoneBot 内置了一些常用的响应规则，可以直接通过事件响应器辅助函数或者自行合并其他规则使用。内置响应规则列表可以参考[事件响应器进阶](../advanced/matcher.md)\n"
  },
  {
    "path": "website/docs/appendices/session-control.mdx",
    "content": "---\nsidebar_position: 2\ndescription: 更灵活的会话控制\n\noptions:\n  menu:\n    - category: appendices\n      weight: 30\n---\n\n# 会话控制\n\nimport Messenger from \"@site/src/components/Messenger\";\n\n在[指南](../tutorial/event-data.mdx#使用依赖注入)的 `weather` 插件中，我们使用依赖注入获取了机器人用户发送的地名参数，并根据地名参数进行相应的回复。但是，一问一答的对话模式仅仅适用于简单的对话场景，如果我们想要实现更复杂的对话模式，就需要使用会话控制。\n\n## 询问并获取用户输入\n\n在 `weather` 插件中，我们对于用户未输入地名参数的情况直接回复了 `请输入地名` 并结束了事件流程。但是，这样用户体验并不好，需要重新输入指令和地名参数才能获取天气回复。我们现在来实现询问并获取用户地名参数的功能。\n\n### 询问用户\n\n我们可以使用事件响应器操作中的 `got` 装饰器来表示当前事件处理流程需要询问并获取用户输入的消息：\n\n```python {6} title=weather/__init__.py\n@weather.handle()\nasync def handle_function(args: Message = CommandArg()):\n    if location := args.extract_plain_text():\n        await weather.finish(f\"今天{location}的天气是...\")\n\n@weather.got(\"location\", prompt=\"请输入地名\")\nasync def got_location():\n    ...\n```\n\n在上面的代码中，我们使用 `got` 事件响应器操作来向用户发送 `prompt` 消息，并等待用户的回复。用户的回复消息将会被作为 `location` 参数存储于事件响应器状态中。\n\n:::tip 提示\n事件处理函数根据定义的顺序依次执行。\n:::\n\n### 获取用户输入\n\n在询问以及用户回复之后，我们就可以获取到我们需要的 `location` 参数了。我们使用 `ArgPlainText` 依赖注入来获取参数纯文本信息：\n\n```python {9} title=weather/__init__.py\nfrom nonebot.params import ArgPlainText\n\n@weather.handle()\nasync def handle_function(args: Message = CommandArg()):\n    if location := args.extract_plain_text():\n        await weather.finish(f\"今天{location}的天气是...\")\n\n@weather.got(\"location\", prompt=\"请输入地名\")\nasync def got_location(location: str = ArgPlainText()):\n    await weather.finish(f\"今天{location}的天气是...\")\n```\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/天气\" },\n    { position: \"left\", msg: \"请输入地名\" },\n    { position: \"right\", msg: \"北京\" },\n    { position: \"left\", msg: \"今天北京的天气是...\" },\n  ]}\n/>\n\n在上面的代码中，我们在 `got_location` 函数中定义了一个依赖注入参数 `location`，他的值将会是用户回复的消息纯文本信息。获取到用户输入的地名参数后，我们就可以进行天气查询并回复了。\n\n:::tip 提示\n如果想要获取用户回复的消息对象 `Message` ，可以使用 `Arg` 依赖注入。\n:::\n\n### 跳过询问\n\n在上面的代码中，如果用户在输入天气指令时，同时提供了地名参数，我们直接回复了天气信息，这部分的逻辑是和询问用户地名参数之后的逻辑一致的。如果在复杂的业务场景下，我们希望这部分代码应该复用以减少代码冗余。我们可以使用事件响应器操作中的 `set_arg` 来主动设置一个参数：\n\n```python {4,6} title=weather/__init__.py\nfrom nonebot.matcher import Matcher\n\n@weather.handle()\nasync def handle_function(matcher: Matcher, args: Message = CommandArg()):\n    if args.extract_plain_text():\n        matcher.set_arg(\"location\", args)\n\n@weather.got(\"location\", prompt=\"请输入地名\")\nasync def got_location(location: str = ArgPlainText()):\n    await weather.finish(f\"今天{location}的天气是...\")\n```\n\n请注意，设置参数需要使用依赖注入来获取 `Matcher` 实例以确保上下文正确，且参数值应为 `Message` 对象。\n\n在 `location` 参数被设置之后，`got` 事件响应器操作将不再会询问并等待用户的回复，而是直接进入 `got_location` 函数。\n\n## 请求重新输入\n\n在实际的业务场景中，用户的输入很有可能并非是我们所期望的，而结束事件处理流程让用户重新发送指令也不是一个好的体验。这时我们可以使用 `reject` 事件响应器操作来请求用户重新输入：\n\n```python {8,9} title=weather/__init__.py\n@weather.handle()\nasync def handle_function(matcher: Matcher, args: Message = CommandArg()):\n    if args.extract_plain_text():\n        matcher.set_arg(\"location\", args)\n\n@weather.got(\"location\", prompt=\"请输入地名\")\nasync def got_location(location: str = ArgPlainText()):\n    if location not in [\"北京\", \"上海\", \"广州\", \"深圳\"]:\n        await weather.reject(f\"你想查询的城市 {location} 暂不支持，请重新输入！\")\n    await weather.finish(f\"今天{location}的天气是...\")\n```\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/天气\" },\n    { position: \"left\", msg: \"请输入地名\" },\n    { position: \"right\", msg: \"南京\" },\n    { position: \"left\", msg: \"你想查询的城市 南京 暂不支持，请重新输入！\" },\n    { position: \"right\", msg: \"北京\" },\n    { position: \"left\", msg: \"今天北京的天气是...\" },\n  ]}\n/>\n\n在上面的代码中，我们在 `got_location` 函数中判断用户输入的地名是否在支持的城市列表中，如果不在，则使用 `reject` 事件响应器操作。操作将会向用户发送 `reject` 参数中的消息，并等待用户回复后，重新执行 `got_location` 函数。通过 `got` 和 `reject` 事件响应器操作，我们实现了类似于**循环**的执行方式。\n\n`reject` 事件响应器操作与 `finish` 类似，NoneBot 会在向机器人用户发送消息内容后抛出 `RejectedException` 异常来暂停事件响应流程以等待用户输入。也就是说，在 `reject` 被执行后，后续的程序同样是不会被执行的。\n\n## 更多事件响应器操作\n\n在之前的章节中，我们已经大致了解了五个事件响应器操作：`handle`、`got`、`finish`、`send` 和 `reject`。现在我们来完整地介绍一下这些操作。\n\n事件响应器操作可以分为两大类：**交互操作**和**流程控制操作**。我们可以通过交互操作来与用户进行交互，而流程控制操作则可以用来控制事件处理流程的执行。\n\n:::tip 提示\n事件处理流程按照事件处理函数添加顺序执行，已经结束的事件处理函数不可能被恢复执行。\n:::\n\n### handle\n\n`handle` 事件响应器操作是一个装饰器，用于向事件处理流程添加一个事件处理函数。\n\n```python\n@matcher.handle()\nasync def handle_func():\n    ...\n```\n\n`handle` 装饰器支持嵌套操作，即一个事件处理函数可以被添加多次：\n\n```python\n@matcher.handle()\n@matcher.handle()\nasync def handle_func():\n    # 这个函数会被执行两次\n    ...\n```\n\n### got\n\n`got` 事件响应器操作也是一个装饰器，它会在当前装饰的事件处理函数运行之前，中断当前事件处理流程，等待接收一个新的事件。它可以通过 `prompt` 参数来向用户发送询问消息，然后等待用户的回复消息，贴近对话形式会话。\n\n`got` 装饰器接受一个参数 `key` 和一个可选参数 `prompt`。当会话状态中不存在 `key` 对应的消息时，会向用户发送 `prompt` 参数的消息，并等待用户回复。`prompt` 参数的类型和 [`send`](#send) 事件响应器操作的参数类型一致。\n\n在事件处理函数中，可以通过依赖注入的方式来获取接收到的消息，参考：[`Arg`](../advanced/dependency.mdx#arg)、[`ArgStr`](../advanced/dependency.mdx#argstr)、[`ArgPlainText`](../advanced/dependency.mdx#argplaintext)。\n\n```python\n@matcher.got(\"key\", prompt=\"请输入...\")\nasync def got_func(key: Message = Arg()):\n    ...\n```\n\n`got` 装饰器支持与 `got` 和 `receive` 装饰器嵌套操作，即一个事件处理函数可以在接收多个事件或消息后执行：\n\n```python\n@matcher.got(\"key1\", prompt=\"请输入key1...\")\n@matcher.got(\"key2\", prompt=\"请输入key2...\")\n@matcher.receive(\"key3\")\nasync def got_func(key1: Message = Arg(), key2: Message = Arg(), key3: Event = Received(\"key3\")):\n    ...\n```\n\n### receive\n\n`receive` 事件响应器操作也是一个装饰器，它会在当前装饰的事件处理函数运行之前，中断当前事件处理流程，等待接收一个新的事件。与 `got` 不同的是，`receive` 不会向用户发送询问消息，并且等待一个用户事件。可以接收的事件类型取决于[会话更新](../advanced/session-updating.md)。\n\n`receive` 装饰器接受一个可选参数 id，用于标识当前需要接收的事件，如果不指定，则默认为空 `\"\"`。\n\n在事件处理函数中，可以通过依赖注入的方式来获取接收到的事件，参考：[`Received`](../advanced/dependency.mdx#received)、[`LastReceived`](../advanced/dependency.mdx#lastreceived)。\n\n```python\n@matcher.receive(\"id\")\nasync def receive_func(event: Event = Received(\"id\")):\n    ...\n```\n\n`receive` 装饰器支持与 `got` 和 `receive` 装饰器嵌套操作，即一个事件处理函数可以在接收多个事件或消息后执行：\n\n```python\n@matcher.receive(\"key1\")\n@matcher.got(\"key2\", prompt=\"请输入key2...\")\n@matcher.got(\"key3\", prompt=\"请输入key3...\")\nasync def receive_func(key1: Event = Received(\"key1\"), key2: Message = Arg(), key3: Message = Arg()):\n    ...\n```\n\n### send\n\n`send` 事件响应器操作用于向用户回复一条消息。协议适配器会根据当前 event 选择回复的途径。\n\n`send` 操作接受一个参数 message 和其他任何协议适配器接受的参数。message 参数类型可以是字符串、消息序列、消息段或者消息模板。消息模板将会使用会话状态字典进行渲染后发送。\n\n这个操作等同于使用 `bot.send(event, message, **kwargs)`，但不需要自行传入 `event`。\n\n```python\n@matcher.handle()\nasync def _():\n    await matcher.send(\"Hello world!\")\n```\n\n### finish\n\n向用户回复一条消息（可选），并立即结束**整个处理流程**。\n\n参数与 [`send`](#send) 相同。\n\n```python\n@matcher.handle()\nasync def _():\n    await matcher.finish(\"Hello world!\")\n    # 下面的代码不会被执行\n```\n\n### pause\n\n向用户回复一条消息（可选），立即结束**当前**事件处理函数，等待接收一个新的事件后进入**下一个**事件处理函数。\n\n参数与 [`send`](#send) 相同。\n\n```python\n@matcher.handle()\nasync def _():\n    if need_confirm:\n        await matcher.pause(\"请在两分钟内确认执行\")\n\n@matcher.handle()\nasync def _():\n    ...\n```\n\n### reject\n\n向用户回复一条消息（可选），立即结束**当前**事件处理函数，等待接收一个新的事件后再次执行**当前**事件处理函数。\n\n`reject` 可以用于拒绝当前 `receive` 接收的事件或 `got` 接收的参数。通常在用户回复不符合格式或标准需要重新输入，或者用于循环进行用户交互。\n\n参数与 [`send`](#send) 相同。\n\n```python\n@matcher.got(\"arg\")\nasync def _(arg: str = ArgPlainText()):\n    if not is_valid(arg):\n        await matcher.reject(\"Invalid arg!\")\n```\n\n### reject_arg\n\n向用户回复一条消息（可选），立即结束**当前**事件处理函数，等待接收一个新的消息后再次执行**当前**事件处理函数。\n\n`reject_arg` 用于拒绝指定 `got` 接收的参数，通常在嵌套装饰器时使用。\n\n`reject_arg` 操作接受一个 key 参数以及可选的 prompt 参数。prompt 参数与 [`send`](#send) 相同。\n\n```python\n@matcher.got(\"a\")\n@matcher.got(\"b\")\nasync def _(a: str = ArgPlainText(), b: str = ArgPlainText()):\n    if a not in b:\n        await matcher.reject_arg(\"a\", \"Invalid a!\")\n```\n\n### reject_receive\n\n向用户回复一条消息（可选），立即结束**当前**事件处理函数，等待接收一个新的事件后再次执行**当前**事件处理函数。\n\n`reject_receive` 用于拒绝指定 `receive` 接收的事件，通常在嵌套装饰器时使用。\n\n`reject_receive` 操作接受一个可选的 id 参数以及可选的 prompt 参数。id 参数默认为空 `\"\"`，prompt 参数与 [`send`](#send) 相同。\n\n```python\n@matcher.receive(\"a\")\n@matcher.receive(\"b\")\nasync def _(a: Event = Received(\"a\"), b: Event = Received(\"b\")):\n    if a.get_user_id() != b.get_user_id():\n        await matcher.reject_receive(\"a\")\n```\n\n### skip\n\n立即结束当前事件处理函数，进入下一个事件处理函数。\n\n通常在依赖注入中使用，用于跳过当前事件处理函数的执行。\n\n```python\nfrom nonebot.params import Depends\n\nasync def dependency():\n    matcher.skip()\n\n@matcher.handle()\nasync def _(check=Depends(dependency)):\n    # 这个函数不会被执行\n```\n\n### stop_propagation\n\n阻止事件向更低优先级的事件响应器传播。\n\n```python\nfrom nonebot.matcher import Matcher\n\n@foo.handle()\nasync def _(matcher: Matcher):\n    matcher.stop_propagation()\n```\n\n:::caution 注意\n`stop_propagation` 操作是实例方法，需要先通过依赖注入获取事件响应器实例再进行调用。\n:::\n\n### get_arg\n\n获取一个 `got` 接收的参数。\n\n`get_arg` 操作接受一个 key 参数和一个可选的 default 参数。当参数不存在时，将返回 default 或 `None`。\n\n```python\nfrom nonebot.matcher import Matcher\n\n@matcher.handle()\nasync def _(matcher: Matcher):\n    key = matcher.get_arg(\"key\", default=None)\n```\n\n### set_arg\n\n设置 / 覆盖一个 `got` 接收的参数。\n\n`set_arg` 操作接受一个 key 参数和一个 value 参数。请注意，value 参数必须是消息序列对象，如需存储其他数据请使用[会话状态](./session-state.md)。\n\n```python\nfrom nonebot.matcher import Matcher\n\n@matcher.handle()\nasync def _(matcher: Matcher):\n    matcher.set_arg(\"key\", Message(\"value\"))\n```\n\n### get_receive\n\n获取一个 `receive` 接收的事件。\n\n`get_receive` 操作接受一个 id 参数和一个可选的 default 参数。当事件不存在时，将返回 default 或 `None`。\n\n```python\nfrom nonebot.matcher import Matcher\n\n@matcher.handle()\nasync def _(matcher: Matcher):\n    event = matcher.get_receive(\"id\", default=None)\n```\n\n### get_last_receive\n\n获取最近的一个 `receive` 接收的事件。\n\n`get_last_receive` 操作接受一个可选的 default 参数。当事件不存在时，将返回 default 或 `None`。\n\n```python\nfrom nonebot.matcher import Matcher\n\n@matcher.handle()\nasync def _(matcher: Matcher):\n    event = matcher.get_last_receive(default=None)\n```\n\n### set_receive\n\n设置 / 覆盖一个 `receive` 接收的事件。\n\n`set_receive` 操作接受一个 id 参数和一个 event 参数。请注意，event 参数必须是事件对象，如需存储其他数据请使用[会话状态](./session-state.md)。\n\n```python\nfrom nonebot.matcher import Matcher\n\n@matcher.handle()\nasync def _(matcher: Matcher):\n    matcher.set_receive(\"key\", Event())\n```\n"
  },
  {
    "path": "website/docs/appendices/session-state.md",
    "content": "---\nsidebar_position: 3\ndescription: 会话状态信息\n\noptions:\n  menu:\n    - category: appendices\n      weight: 40\n---\n\n# 会话状态\n\n在事件处理流程中，和用户交互的过程即是会话。在会话中，我们可能需要记录一些信息，例如用户的重试次数等等，以便在会话中的不同阶段进行判断和处理。这些信息都可以存储于会话状态中。\n\nNoneBot 中的会话状态是一个字典，可以通过类型 `T_State` 来获取。字典内可以存储任意类型的数据，但是要注意的是，NoneBot 本身会在会话状态中存储一些信息，因此不要使用 [NoneBot 使用的键名](../api/consts.md)。\n\n```python\nfrom nonebot.typing import T_State\n\n@matcher.got(\"key\", prompt=\"请输入密码\")\nasync def _(state: T_State, key: str = ArgPlainText()):\n    if key != \"some password\":\n        try_count = state.get(\"try_count\", 1)\n        if try_count >= 3:\n            await matcher.finish(\"密码错误次数过多\")\n        else:\n            state[\"try_count\"] = try_count + 1\n            await matcher.reject(\"密码错误，请重新输入\")\n    await matcher.finish(\"密码正确\")\n```\n\n会话状态的生命周期与事件处理流程相同，在期间的任何一个事件处理函数都可以进行读写。\n\n```python\nfrom nonebot.typing import T_State\n\n@matcher.handle()\nasync def _(state: T_State):\n    state[\"key\"] = \"value\"\n\n@matcher.handle()\nasync def _(state: T_State):\n    await matcher.finish(state[\"key\"])\n```\n\n会话状态还可以用于发送动态消息，消息模板在发送时会使用会话状态字典进行渲染。消息模板的使用方法已经在[消息处理](../tutorial/message.md#使用消息模板)中介绍过，这里不再赘述。\n\n```python\nfrom nonebot.typing import T_State\nfrom nonebot.adapters import MessageTemplate\n\n@matcher.handle()\nasync def _(state: T_State):\n    state[\"username\"] = \"user\"\n\n@matcher.got(\"password\", prompt=MessageTemplate(\"请输入 {username} 的密码\"))\nasync def _():\n    await matcher.finish(MessageTemplate(\"密码为 {password}\"))\n```\n"
  },
  {
    "path": "website/docs/appendices/whats-next.md",
    "content": "---\nsidebar_position: 99\ndescription: 下一步──进阶！\n---\n\n# 下一步\n\n至此，我们已经了解了 NoneBot 的大多数功能用法，相信你已经可以独自写出一个插件了。现在你可以选择：\n\n- 即刻开始插件编写！\n- 更深入地了解 NoneBot 的[更多功能和原理](../advanced/plugin-info.md)！\n"
  },
  {
    "path": "website/docs/best-practice/alconna/README.mdx",
    "content": "import Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\n\n# Alconna 插件\n\n[`nonebot-plugin-alconna`](https://github.com/nonebot/plugin-alconna) 是一类极大地提升了 NoneBot 开发体验的插件。\n\n该插件可分为三个部分：\n\n- 增强的命令解析: 基于 [Alconna](https://github.com/ArcletProject/Alconna), 提供一类新的事件响应器辅助函数 `on_alconna`. 相比 `on_command`, `on_shell`, `on_regex` 等函数，`on_alconna` 提供了更强大的命令解析能力与诸多特性。\n- 通用消息组件: 实现了跨平台接收、发送、撤回、编辑、表态消息的功能。\n  - `UniMessage` 通用消息模型，支持各适配器下的消息转换和导出，发送。\n  - `Text`, `Image`, `At` 等通用消息段模型，既与 `UniMessage` 配合使用，又能用于 `Alconna` 的命令解析。\n  - `message_recall`, `message_edit`, `message_reaction` 等功能函数。\n  - `Target` 通用消息目标模型，并通过该模型进行主动消息发送。\n  - `UniMsg`, `MsgId`, `MsgTarget`, `at_in`, `at_me` 等提供给 nonebot 使用的依赖注入和 `Rule`。\n- 内置功能插件：基于上述部分实现的内置功能插件。\n  - `echo`: 通过 `on_alconna` 实现的 echo 插件，支持回显回复消息。\n  - `help`: 列出所有 `on_alconna` 事件响应器的帮助信息或其对应的插件信息。\n  - `lang`: 切换 `Alconna` 使用的语言\n  - `switch`: 禁用/启用某个指令\n  - `with`: 针对具有多个子命令的指令，通过 `with` 在当前会话中载入命令头以节省输入。\n\n以最新版本为例 (v0.59), 本插件已支持 NoneBot 生态中几乎所有的适配器, 包括:\n\n| 协议名称                                                            | 路径                                 |\n| ------------------------------------------------------------------- | ------------------------------------ |\n| [OneBot 协议](https://onebot.dev/)                                  | adapters.onebot11, adapters.onebot12 |\n| [Telegram](https://core.telegram.org/bots/api)                      | adapters.telegram                    |\n| [飞书](https://open.feishu.cn/document/home/index)                  | adapters.feishu                      |\n| [GitHub](https://docs.github.com/en/developers/apps)                | adapters.github                      |\n| [QQ bot](https://github.com/nonebot/adapter-qq)                     | adapters.qq                          |\n| [钉钉](https://open.dingtalk.com/document/)                         | adapters.ding                        |\n| [Console](https://github.com/nonebot/adapter-console)               | adapters.console                     |\n| [开黑啦](https://developer.kookapp.cn/)                             | adapters.kook                        |\n| [Mirai](https://docs.mirai.mamoe.net/mirai-api-http/)               | adapters.mirai                       |\n| [Ntchat](https://github.com/JustUndertaker/adapter-ntchat)          | adapters.ntchat                      |\n| [MineCraft](https://github.com/17TheWord/nonebot-adapter-minecraft) | adapters.minecraft                   |\n| [Walle-Q](https://github.com/onebot-walle/nonebot_adapter_walleq)   | adapters.onebot12                    |\n| [Discord](https://github.com/nonebot/adapter-discord)               | adapters.discord                     |\n| [Red 协议](https://github.com/nonebot/adapter-red)                  | adapters.red                         |\n| [Satori](https://github.com/nonebot/adapter-satori)                 | adapters.satori                      |\n| [Dodo IM](https://github.com/nonebot/adapter-dodo)                  | adapters.dodo                        |\n| [Kritor](https://github.com/nonebot/adapter-kritor)                 | adapters.kritor                      |\n| [Tailchat](https://github.com/eya46/nonebot-adapter-tailchat)       | adapters.tailchat                    |\n| [Mail](https://github.com/mobyw/nonebot-adapter-mail)               | adapters.mail                        |\n| [微信公众号](https://github.com/YangRucheng/nonebot-adapter-wxmp)   | adapters.wxmp                        |\n| [黑盒语音](https://github.com/lclbm/adapter-heybox)                 | adapters.heybox                      |\n| [Milky](https://github.com/nonebot/adapter-milky)                   | adapters.milky                       |\n| [EFChat](https://github.com/molanp/nonebot_adapter_efchat)          | adapters.efchat                      |\n\n## 安装插件\n\n在使用前请先安装 `nonebot-plugin-alconna` 插件至项目环境中，可参考[获取商店插件](../../tutorial/store.mdx#安装插件)来了解并选择安装插件的方式。如：\n\n在**项目目录**下执行以下命令：\n\n<Tabs groupId=\"install\">\n<TabItem value=\"cli\" label=\"使用 nb-cli\">\n\n```shell\nnb plugin install nonebot-plugin-alconna\n```\n\n</TabItem>\n<TabItem value=\"pip\" label=\"使用 pip\">\n\n```shell\npip install nonebot-plugin-alconna\n```\n\n</TabItem>\n\n<TabItem value=\"pdm\" label=\"使用 pdm\">\n\n```shell\npdm add nonebot-plugin-alconna\n```\n\n</TabItem>\n</Tabs>\n\n## 导入插件\n\n由于 `nonebot-plugin-alconna` 作为插件，因此需要在使用前对其进行**加载**。使用 `require` 方法可轻松完成这一过程，可参考 [跨插件访问](../../advanced/requiring.md) 一节进行了解。\n\n```python\nfrom nonebot import require\n\nrequire(\"nonebot_plugin_alconna\")\n\nfrom nonebot_plugin_alconna import ...\n```\n\n## 使用插件\n\n在前面的[深入指南](../../appendices/session-control.mdx)中，我们已经得到了一个天气插件。\n现在我们将使用 `Alconna` 来改写这个插件。\n\n<details>\n  <summary>插件示例</summary>\n\n```python title=weather/__init__.py\nfrom nonebot import on_command\nfrom nonebot.rule import to_me\nfrom nonebot.matcher import Matcher\nfrom nonebot.adapters import Message\nfrom nonebot.params import CommandArg, ArgPlainText\n\nweather = on_command(\"天气\", rule=to_me(), aliases={\"weather\", \"天气预报\"})\n\n@weather.handle()\nasync def handle_function(matcher: Matcher, args: Message = CommandArg()):\n    if args.extract_plain_text():\n        matcher.set_arg(\"location\", args)\n\n@weather.got(\"location\", prompt=\"请输入地名\")\nasync def got_location(location: str = ArgPlainText()):\n    if location not in [\"北京\", \"上海\", \"广州\", \"深圳\"]:\n        await weather.reject(f\"你想查询的城市 {location} 暂不支持，请重新输入！\")\n    await weather.finish(f\"今天{location}的天气是...\")\n```\n\n</details>\n\n```python {5-9,13-15,17-18}\nfrom nonebot.rule import to_me\nfrom arclet.alconna import Alconna, Args\nfrom nonebot_plugin_alconna import Match, on_alconna\n\nweather = on_alconna(\n    Alconna(\"天气\", Args[\"location?\", str]),\n    aliases={\"weather\", \"天气预报\"},\n    rule=to_me(),\n)\n\n\n@weather.handle()\nasync def handle_function(location: Match[str]):\n    if location.available:\n        weather.set_path_arg(\"location\", location.result)\n\n@weather.got_path(\"location\", prompt=\"请输入地名\")\nasync def got_location(location: str):\n    if location not in [\"北京\", \"上海\", \"广州\", \"深圳\"]:\n        await weather.reject(f\"你想查询的城市 {location} 暂不支持，请重新输入！\")\n    await weather.finish(f\"今天{location}的天气是...\")\n```\n\n在上面的代码中，我们使用 `Alconna` 来解析命令，`on_alconna` 用来创建响应器，使用 `Match` 来获取解析结果。\n\n关于更多 `Alconna` 的使用方法，可参考 [Alconna 文档](https://arclet.top/tutorial/alconna)，\n或阅读 [Alconna 基本介绍](./command.md) 一节。\n\n关于更多 `on_alconna` 的使用方法，可参考 [插件文档](https://github.com/nonebot/plugin-alconna/blob/master/docs.md)，\n或阅读 [响应规则的使用](./matcher.mdx) 一节。\n\n## 交流与反馈\n\nQQ 交流群: [🔗 链接](https://jq.qq.com/?_wv=1027&k=PUPOnCSH)\n\n友链: [📚 文档](https://graiax.cn/guide/message_parser/alconna.html)\n"
  },
  {
    "path": "website/docs/best-practice/alconna/_category_.json",
    "content": "{\n  \"label\": \"命令解析拓展\",\n  \"position\": 6\n}\n"
  },
  {
    "path": "website/docs/best-practice/alconna/builtins.mdx",
    "content": "---\nsidebar_position: 7\ndescription: 内置组件\n---\n\nimport Messenger from \"@site/src/components/Messenger\";\n\n# 内置组件\n\n`nonebot_plugin_alconna` 插件提供了一系列内置组件以提升开发者和用户体验。\n\n## 内置插件\n\n类似于 Nonebot 本身提供的内置插件，`nonebot_plugin_alconna` 提供了多个内置插件。\n\n### 加载\n\n你可以用本插件的 `load_builtin_plugin(s)` 来加载它们：\n\n```python\nfrom nonebot_plugin_alconna import load_builtin_plugin, load_builtin_plugins\n\nload_builtin_plugins(\"echo\")\nload_builtin_plugins(\"help\", \"with\")\n```\n\n### 使用\n\n#### echo\n\n`echo` 插件能将用户发送的消息原样返回。\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/echo hello world!\" },\n    { position: \"left\", msg: \"hello world!\" },\n    { position: \"right\", msg: \"/echo [图片]\" },\n    { position: \"left\", msg: \"[图片]\" },\n  ]}\n/>\n\n#### help\n\n`help` 插件能列出所有 Alconna 指令。同时还能查询某个指令对应的插件信息。\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/帮助\" },\n    {\n      position: \"left\",\n      msg: \"# 当前可用的命令有:\\n 【0】/echo : echo 指令\\n 【1】/help : 显示所有命令帮助\\n# 输入'命令名 -h|--help' 查看特定命令的语法\",\n    },\n    { position: \"right\", msg: \"/help --plugin-info echo\" },\n    {\n      position: \"left\",\n      msg: \"插件名称: echo\\n插件标识: nonebot_plugin_alconna:echo\\n插件模块: nonebot-plugin-alconna\\n插件版本: 0.57.2\\n插件路径: nonebot_plugin_alconna.builtins.plugins.echo\",\n    },\n  ]}\n/>\n\nhelp 插件的帮助信息如下：\n\n```\n/help <query: str = -1>\n## 注释\n  query: 选择某条命令的id或者名称查看具体帮助\n显示所有命令帮助\n用法:\n可以使用 --hide 参数来显示隐藏命令，使用 -P 参数来显示命令所属插件名称\n\n可用的子命令有:\n* 是否列出命令所属命名空间\n  -N│--namespace│命名空间 [target: str]\n## 注释\n  target: 指定的命名空间\n  该子命令内可用的选项有:\n  * 列出所有命名空间\n    --list\n可用的选项有:\n* 查看指定页数的命令帮助\n  --page <index: int>\n* 查看命令所属插件的信息\n  -P│插件信息│--plugin-info\n* 是否列出隐藏命令\n  隐藏│-H│--hide\n```\n\n#### lang\n\n`lang` 插件能切换 i18n 的语言设置。\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/lang list\" },\n    {\n      position: \"left\",\n      msg: \"支持的语言列表:\\n * en-US\\n * zh-CN\",\n    },\n    { position: \"right\", msg: \"/lang switch en-US\" },\n    { position: \"left\", msg: \"Switch to 'en-US' successfully.\" },\n  ]}\n/>\n\nlang 插件的帮助信息如下：\n\n```\n/lang\ni18n配置相关功能\n\n可用的选项有:\n* 查看支持的语言列表\n  list [name: str]\n* 切换语言\n  switch [locale: str]\n```\n\n其中 `list` 选项可以查找某一插件下的语言支持情况 (例如 `/lang list nonebot_plugin_alconna`)。\n\n#### switch\n\n`switch` 插件能用来启用/禁用某个命令，其使用方法与 `help` 类似。\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/disable\" },\n    {\n      position: \"left\",\n      msg: \"【0】/echo : echo 指令\\n【1】/help : 显示所有命令帮助\\n【2】/lang : i18n配置相关功能\",\n    },\n    { position: \"right\", msg: \"/disable 0\" },\n    { position: \"left\", msg: \"已禁用 /echo\" },\n    { position: \"right\", msg: \"/echo 1234\" },\n    { position: \"right\", msg: \"/enable echo\" },\n    { position: \"left\", msg: \"已启用 /echo\" },\n    { position: \"right\", msg: \"/echo 1234\" },\n    { position: \"left\", msg: \"1234\" },\n  ]}\n/>\n\n#### with\n\n`with` 插件能在当前会话中设置一个局部命令前缀，以便于有多个子命令的指令使用。\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/with\" },\n    {\n      position: \"left\",\n      msg: \"当前群组未设置前缀\",\n    },\n    { position: \"right\", msg: \"/with lang\" },\n    { position: \"left\", msg: \"设置前缀成功\" },\n    { position: \"right\", msg: \"list\" },\n    {\n      position: \"left\",\n      msg: \"支持的语言列表:\\n * en-US\\n * zh-CN\",\n    },\n  ]}\n/>\n\nwith 插件的帮助信息如下：\n\n```\n.with [name: str]\nwith 指令\n用法:\n设置局部命令前缀\n\n可用的选项有:\n* 设置可能的生效时间\n  --expire│expire <time: datetime>\n* 取消当前前缀\n  unset│--unset\n\n快捷命令:\n'[.]局部前缀' => [.]with\n```\n\n### 配置\n\n内置插件也有其配置项，并且均以 `NBP_ALC` 开头。\n\n- `nbp_alc_echo_tome`: 是否让 `echo` 插件的消息经过 `to_me` 处理\n- `nbp_alc_page_size`: `help` 与 `switch` 插件的共同配置项，表示每页显示的命令数量\n- `nbp_alc_help_text`: `help` 指令的指令名，默认为 \"help\"\n- `nbp_alc_help_alias`: `help` 指令的别名，默认为 \"帮助\", \"命令帮助\"\n- `nbp_alc_help_all_alias`: `help` 指令显示隐藏指令时的别名，默认为 \"所有帮助\", \"所有命令帮助\"\n- `nbp_alc_switch_enable`: `switch` 插件的 `enable` 指令的指令名，默认为 \"enable\"\n- `nbp_alc_switch_enable_alias`: `switch` 插件的 `enable` 指令的别名，默认为 \"启用\", \"启用指令\"\n- `nbp_alc_switch_disable`: `switch` 插件的 `disable` 指令的指令名，默认为 \"disable\"\n- `nbp_alc_switch_disable_alias`: `switch` 插件的 `disable` 指令的别名，默认为 \"disable\", \"禁用\", \"禁用指令\"\n- `nbp_alc_with_text`: `with` 插件的指令名，默认为 \"with\"\n- `nbp_alc_with_alias`: `with` 插件的别名，默认为 \"局部前缀\"\n\n## 内置匹配拓展\n\n目前插件提供了 5 个内置的 `Extension`，它们在 `nonebot_plugin_alconna.builtins.extensions` 下：\n\n### ReplyRecordExtension\n\n`ReplyRecordExtension` 可将消息事件中的回复暂存在 extension 中，使得解析用的消息不带回复信息，同时可以在后续的处理中获取回复信息：\n\n```python\nfrom nonebot_plugin_alconna import MsgId, on_alconna\nfrom nonebot_plugin_alconna.builtins.extensions import ReplyRecordExtension\n\nmatcher = on_alconna(\"...\", extensions=[ReplyRecordExtension()])\n\n@matcher.handle()\nasync def handle(msg_id: MsgId, ext: ReplyRecordExtension):\n    if reply := ext.get_reply(msg_id):\n        ...\n    else:\n        ...\n```\n\n### ReplyMergeExtension\n\n`ReplyMergeExtension` 可将消息事件中的回复指向的原消息合并到当前消息中作为一部分参数：\n\n```python\nfrom nonebot_plugin_alconna import Match, on_alconna\nfrom nonebot_plugin_alconna.builtins.extensions.reply import ReplyMergeExtension\n\nmatcher = on_alconna(\"...\", extensions=[ReplyMergeExtension()])\n\n@matcher.handle()\nasync def handle(content: Match[str]):\n    ...\n```\n\n其构造时可传入两个参数:\n\n- `add_left`: 否在当前消息的左侧合并回复消息，默认为 False\n- `sep`: 合并时的分隔符，默认为空格\n\n### DiscordSlashExtension\n\n`DiscordSlashExtension` 可自动将 Alconna 对象翻译成 Discord 的 slash 指令并注册，且将收到的指令交互事件转为指令供命令解析：\n\n```python\nfrom nonebot_plugin_alconna import Match, on_alconna\nfrom nonebot_plugin_alconna.builtins.extensions.discord import DiscordSlashExtension\n\n\nalc = Alconna(\n    [\"/\"],\n    \"permission\",\n    Subcommand(\"add\", Args[\"plugin\", str][\"priority?\", int]),\n    Option(\"remove\", Args[\"plugin\", str][\"time?\", int]),\n    meta=CommandMeta(description=\"权限管理\"),\n)\n\nmatcher = on_alconna(alc, extensions=[DiscordSlashExtension()])\n\n@matcher.assign(\"add\")\nasync def add(plugin: Match[str], priority: Match[int], ext: DiscordSlashExtension):\n    await ext.send_followup_msg(f\"added {plugin.result} with {priority.result if priority.available else 0}\")\n\n@matcher.assign(\"remove\")\nasync def remove(plugin: Match[str], time: Match[int]):\n    await matcher.finish(f\"removed {plugin.result} with {time.result if time.available else -1}\")\n```\n\n### MarkdownOutputExtension\n\n`MarkdownOutputExtension` 可将 Alconna 的自动输出转换为 Markdown 格式\n\n其构造时可传入两个参数:\n\n- `escape_dot`: 是否转义句中的点号（用来避免被识别为 url）\n- `text_to_image` 将文本转换为图片的函数，可不传入。一般用来设置渲染 markdown 为图片的函数\n\n### TelegramSlashExtension\n\n`TelegramSlashExtension` 可将 Alconna 的命令注册在 Telegram 上以获得提示，类似于 `DiscordSlashExtension`。\n\n```python\nfrom nonebot_plugin_alconna import on_alconna\nfrom nonebot.adapters.telegram.model import BotCommandScopeChat\nfrom nonebot_plugin_alconna.builtins.extensions.telegram import TelegramSlashExtension\n\nTelegramSlashExtension.set_scope(BotCommandScopeChat())\n\nmatcher = on_alconna(\"...\", extensions=[TelegramSlashExtension()])\n```\n\n## 内置自定义消息段\n\n目前插件提供了 3 个内置的 `Segment`，它们在 `nonebot_plugin_alconna.builtins.segments` 下：\n\n- `Markdown`: 可以传入 **markdown模板** 的元素\n- `MarketFace`: 特指 QQ 的商城表情\n- `MusicShare`: 特指 QQ 的音乐分享卡片\n"
  },
  {
    "path": "website/docs/best-practice/alconna/command.md",
    "content": "---\nsidebar_position: 2\ndescription: Alconna 基本介绍\n---\n\n# Alconna 本体\n\n[`Alconna`](https://github.com/ArcletProject/Alconna) 隶属于 `ArcletProject`，是一个简单、灵活、高效的命令参数解析器, 并且不局限于解析命令式字符串。\n\n我们先通过一个例子来讲解 **Alconna** 的核心 —— `Args`, `Subcommand`, `Option`：\n\n```python\nfrom arclet.alconna import Alconna, Args, Subcommand, Option\n\n\nalc = Alconna(\n    \"pip\",\n    Subcommand(\n        \"install\",\n        Args[\"package\", str],\n        Option(\"-r|--requirement\", Args[\"file\", str]),\n        Option(\"-i|--index-url\", Args[\"url\", str]),\n    )\n)\n\nres = alc.parse(\"pip install nonebot2 -i URL\")\n\nprint(res)\n# 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'}\n\nprint(res.all_matched_args)\n# {'package': 'nonebot2', 'url': 'URL'}\n```\n\n这段代码通过`Alconna`创捷了一个接受主命令名为`pip`, 子命令为`install`且子命令接受一个 **Args** 参数`package`和二个 **Option** 参数`-r`和`-i`的命令参数解析器, 通过`parse`方法返回解析结果 **Arparma** 的实例。\n\n## 命令头\n\n命令头是指命令的前缀 (Prefix) 与命令名 (Command) 的组合，例如 !help 中的 ! 与 help。\n\n命令构造时, `Alconna([prefix], command)` 与 `Alconna(command, [prefix])` 是等价的。\n\n|             前缀             |   命令名   |                          匹配内容                           |       说明       |\n| :--------------------------: | :--------: | :---------------------------------------------------------: | :--------------: |\n|            不传入            |   \"foo\"    |                           `\"foo\"`                           | 无前缀的纯文字头 |\n|            不传入            |    123     |                            `123`                            |  无前缀的元素头  |\n|            不传入            | \"re:\\d{2}\" |                           `\"32\"`                            |  无前缀的正则头  |\n|            不传入            |    int     |                      `123` 或 `\"456\"`                       |  无前缀的类型头  |\n|         [int, bool]          |   不传入   |                       `True` 或 `123`                       |  无名的元素类头  |\n|        [\"foo\", \"bar\"]        |   不传入   |                     `\"foo\"` 或 `\"bar\"`                      |  无名的纯文字头  |\n|        [\"foo\", \"bar\"]        |   \"baz\"    |                  `\"foobaz\"` 或 `\"barbaz\"`                   |     纯文字头     |\n|         [int, bool]          |   \"foo\"    |             `[123, \"foo\"]` 或 `[False, \"foo\"]`              |      类型头      |\n|         [123, 4567]          |   \"foo\"    |              `[123, \"foo\"]` 或 `[4567, \"foo\"]`              |      元素头      |\n|      [nepattern.NUMBER]      |   \"bar\"    |            `[123, \"bar\"]` 或 `[123.456, \"bar\"]`             |     表达式头     |\n|         [123, \"foo\"]         |   \"bar\"    |      `[123, \"bar\"]` 或 `\"foobar\"` 或 `[\"foo\", \"bar\"]`       |      混合头      |\n| [(int, \"foo\"), (456, \"bar\")] |   \"baz\"    | `[123, \"foobaz\"]` 或 `[456, \"foobaz\"]` 或 `[456, \"barbaz\"]` |       对头       |\n\n对于无前缀的类型头，此时会将传入的值尝试转为 BasePattern，例如 `int` 会转为 `nepattern.INTEGER`。如此该命令头会匹配对应的类型， 例如 `int` 会匹配 `123` 或 `\"456\"`，但不会匹配 `\"foo\"`。解析后，Alconna 会将命令头匹配到的值转为对应的类型，例如 `int` 会将 `\"123\"` 转为 `123`。\n\n:::tip\n\n**正则内容只在命令名上生效，前缀中的正则会被转义**\n\n:::\n\n除了通过传入 `re:xxx` 来使用正则表达式外，Alconna 还提供了一种更加简洁的方式来使用正则表达式，称为 Bracket Header：\n\n```python\nalc = Alconna(\".rd{roll:int}\")\nassert alc.parse(\".rd123\").header[\"roll\"] == 123\n```\n\nBracket Header 类似 python 里的 f-string 写法，通过 `\"{}\"` 声明匹配类型\n\n`\"{}\"` 中的内容为 \"name:type or pat\"：\n\n- `\"{}\"`, `\"{:}\"` ⇔ `\"(.+)\"`, 占位符\n- `\"{foo}\"` ⇔ `\"(?P&lt;foo&gt;.+)\"`\n- `\"{:\\d+}\"` ⇔ `\"(\\d+)\"`\n- `\"{foo:int}\"` ⇔ `\"(?P&lt;foo&gt;\\d+)\"`，其中 `\"int\"` 部分若能转为 `BasePattern` 则读取里面的表达式\n\n## 参数声明(Args)\n\n`Args` 是用于声明命令参数的组件， 可以通过以下几种方式构造 **Args** ：\n\n- `Args[key, var, default][key1, var1, default1][...]`\n- `Args[(key, var, default)]`\n- `Args.key[var, default]`\n\n其中，key **一定**是字符串，而 var 一般为参数的类型，default 为具体的值或者 **arclet.alconna.args.Field**\n\n其与函数签名类似，但是允许含有默认值的参数在前；同时支持 keyword-only 参数不依照构造顺序传入 （但是仍需要在非 keyword-only 参数之后）。\n\n### key\n\n`key` 的作用是用以标记解析出来的参数并存放于 **Arparma** 中，以方便用户调用。\n\n其有三种为 Args 注解的标识符: `?`、`/`、 `!`, 标识符与 key 之间建议以 `;` 分隔：\n\n- `!` 标识符表示该处传入的参数应**不是**规定的类型，或**不在**指定的值中。\n- `?` 标识符表示该参数为**可选**参数，会在无参数匹配时跳过。\n- `/` 标识符表示该参数的类型注解需要隐藏。\n\n另外，对于参数的注释也可以标记在 `key` 中，其与 key 或者标识符 以 `#` 分割：  \n`foo#这是注释;?` 或 `foo?#这是注释`\n\n:::tip\n\n`Args` 中的 `key` 在实际命令中并不需要传入（keyword 参数除外）：\n\n```python\nfrom arclet.alconna import Alconna, Args\n\n\nalc = Alconna(\"test\", Args[\"foo\", str])\nalc.parse(\"test --foo abc\") # 错误\nalc.parse(\"test abc\") # 正确\n```\n\n若需要 `test --foo abc`，你应该使用 `Option`：\n\n```python\nfrom arclet.alconna import Alconna, Args, Option\n\n\nalc = Alconna(\"test\", Option(\"--foo\", Args[\"foo\", str]))\n```\n\n:::\n\n### var\n\nvar 负责命令参数的**类型检查**与**类型转化**\n\n`Args` 的`var`表面上看需要传入一个 `type`，但实际上它需要的是一个 `nepattern.BasePattern` 的实例：\n\n```python\nfrom arclet.alconna import Args\nfrom nepattern import BasePattern\n\n\n# 表示 foo 参数需要匹配一个 @number 样式的字符串\nargs = Args[\"foo\", BasePattern(\"@\\d+\")]\n```\n\n`pip` 示例中可以传入 `str` 是因为 `str` 已经注册在了 `nepattern.global_patterns` 中，因此会替换为 `nepattern.global_patterns[str]`\n\n`nepattern.global_patterns`默认支持的类型有：\n\n- `str`: 匹配任意字符串\n- `int`: 匹配整数\n- `float`: 匹配浮点数\n- `bool`: 匹配 `True` 与 `False` 以及他们小写形式\n- `hex`: 匹配 `0x` 开头的十六进制字符串\n- `url`: 匹配网址\n- `email`: 匹配 `xxxx@xxx` 的字符串\n- `ipv4`: 匹配 `xxx.xxx.xxx.xxx` 的字符串\n- `list`: 匹配类似 `[\"foo\",\"bar\",\"baz\"]` 的字符串\n- `dict`: 匹配类似 `{\"foo\":\"bar\",\"baz\":\"qux\"}` 的字符串\n- `datetime`: 传入一个 `datetime` 支持的格式字符串，或时间戳\n- `Any`: 匹配任意类型\n- `AnyString`: 匹配任意类型，转为 `str`\n- `Number`: 匹配 `int` 与 `float`，转为 `int`\n\n同时可以使用 typing 中的类型：\n\n- `Literal[X]`: 匹配其中的任意一个值\n- `Union[X, Y]`: 匹配其中的任意一个类型\n- `Optional[xxx]`: 会自动将默认值设为 `None`，并在解析失败时使用默认值\n- `List[X]`: 匹配一个列表，其中的元素为 `X` 类型\n- `Dict[X, Y]`: 匹配一个字典，其中的 key 为 `X` 类型，value 为 `Y` 类型\n- ...\n\n:::tip\n\n几类特殊的传入标记：\n\n- `\"foo\"`: 匹配字符串 \"foo\" (若没有某个 `BasePattern` 与之关联)\n- `RawStr(\"foo\")`: 匹配字符串 \"foo\" (即使有 `BasePattern` 与之关联也不会被替换)\n- `\"foo|bar|baz\"`: 匹配 \"foo\" 或 \"bar\" 或 \"baz\"\n- `[foo, bar, Baz, ...]`: 匹配其中的任意一个值或类型\n- `Callable[[X], Y]`: 匹配一个参数为 `X` 类型的值，并返回通过该函数调用得到的 `Y` 类型的值\n- `\"re:xxx\"`: 匹配一个正则表达式 `xxx`，会返回 Match[0]\n- `\"rep:xxx\"`: 匹配一个正则表达式 `xxx`，会返回 `re.Match` 对象\n- `{foo: bar, baz: qux}`: 匹配字典中的任意一个键, 并返回对应的值 (特殊的键 ... 会匹配任意的值)\n- ...\n\n**特别的**，你可以不传入 `var`，此时会使用 `key` 作为 `var`, 匹配 `key` 字符串。\n\n:::\n\n#### MultiVar 与 KeyWordVar\n\n`MultiVar` 是一个特殊的标注，用于告知解析器该参数可以接受多个值，类似于函数中的 `*args`，其构造方法形如 `MultiVar(str)`。\n\n同样的还有 `KeyWordVar`，类似于函数中的 `*, name: type`，其构造方法形如 `KeyWordVar(str)`，用于告知解析器该参数为一个 keyword-only 参数。\n\n:::tip\n\n`MultiVar` 与 `KeyWordVar` 组合时，代表该参数为一个可接受多个 key-value 的参数，类似于函数中的 `**kwargs`，其构造方法形如 `MultiVar(KeyWordVar(str))`\n\n`MultiVar` 与 `KeyWordVar` 也可以传入 `default` 参数，用于指定默认值\n\n`MultiVar` 不能在 `KeyWordVar` 之后传入\n\n:::\n\n#### AllParam\n\n`AllParam` 是一个特殊的标注，用于告知解析器该参数接收命令中在此位置之后的所有参数并**结束解析**，可以认为是**泛匹配参数**。\n\n`AllParam` 可直接使用 (`Args[\"xxx\", AllParam]`), 也可以传入指定的接收类型 (`Args[\"xxx\", AllParam(str)]`)。\n\n:::tip\n\n在 `nonebot_plugin_alconna` 下，`AllParam` 的返回值为 [`UniMessage`](./uniseg/message.mdx)\n\n:::\n\n### default\n\n`default` 传入的是该参数的默认值或者 `Field`，以携带对于该参数的更多信息。\n\n默认情况下 (即不声明) `default` 的值为特殊值 `Empty`。这也意味着你可以将默认值设置为 `None` 表示默认值为空值。\n\n`Field` 构造需要的参数说明如下：\n\n- default: 参数单元的默认值\n- alias: 参数单元默认值的别名\n- completion: 参数单元的补全说明生成函数\n- unmatch_tips: 参数单元的错误提示生成函数，其接收一个表示匹配失败的元素的参数\n- missing_tips: 参数单元的缺失提示生成函数\n\n## 选项与子命令(Option & Subcommand)\n\n`Option` 和 `Subcommand` 可以传入一组 `alias`，如 `Option(\"--foo|-F|--FOO|-f\")`，`Subcommand(\"foo\", alias=[\"F\"])`\n\n传入别名后，选项与子命令会选择其中长度最长的作为其名称。若传入为 \"--foo|-f\"，则命令名称为 \"--foo\"\n\n:::tip 特别提醒!!!\n\nOption 的名字或别名**没有要求**必须在前面写上 `-`\n\nOption 与 Subcommand 的唯一区别在于 Subcommand 可以传入自己的 **Option** 与 **Subcommand**\n\n:::\n\n他们拥有如下共同参数：\n\n- `help_text`: 传入该组件的帮助信息\n- `dest`: 被指定为解析完成时标注匹配结果的标识符，不传入时默认为选项或子命令的名称 (name)\n- `requires`: 一段指定顺序的字符串列表，作为唯一的前置序列与命令嵌套替换\n  对于命令 `test foo bar baz qux <a:int>` 来讲，因为`foo bar baz` 仅需要判断是否相等, 所以可以这么编写：\n\n```python\nAlconna(\"test\", Option(\"qux\", Args.a[int], requires=[\"foo\", \"bar\", \"baz\"]))\n```\n\n- `default`: 默认值，在该组件未被解析时使用使用该值替换。\n  特别的，使用 `OptionResult` 或 `SubcomanndResult` 可以设置包括参数字典在内的默认值：\n\n```python\nfrom arclet.alconna import Option, OptionResult\n\nopt1 = Option(\"--foo\", default=False)\nopt2 = Option(\"--foo\", default=OptionResult(value=False, args={\"bar\": 1}))\n```\n\n### Action\n\n`Option` 可以特别设置传入一类 `Action`，作为解析操作\n\n`Action` 分为三类：\n\n- `store`: 无 Args 时， 仅存储一个值， 默认为 Ellipsis； 有 Args 时， 后续的解析结果会覆盖之前的值\n- `append`: 无 Args 时， 将多个值存为列表， 默认为 Ellipsis； 有 Args 时， 每个解析结果会追加到列表中, 当存在默认值并且不为列表时， 会自动将默认值变成列表， 以保证追加的正确性\n- `count`: 无 Args 时， 计数器加一； 有 Args 时， 表现与 STORE 相同, 当存在默认值并且不为数字时， 会自动将默认值变成 1， 以保证计数器的正确性。\n\n`Alconna` 提供了预制的几类 `Action`：\n\n- `store`(默认)，`store_value`，`store_true`，`store_false`\n- `append`，`append_value`\n- `count`\n\n## 解析结果\n\n`Alconna.parse` 会返回由 **Arparma** 承载的解析结果\n\n`Arparma` 有如下属性：\n\n- 调试类\n  - matched: 是否匹配成功\n  - error_data: 解析失败时剩余的数据\n  - error_info: 解析失败时的异常内容\n  - origin: 原始命令，可以类型标注\n\n- 分析类\n  - header_match: 命令头部的解析结果，包括原始头部、解析后头部、解析结果与可能的正则匹配组\n  - main_args: 命令的主参数的解析结果\n  - options: 命令所有选项的解析结果\n  - subcommands: 命令所有子命令的解析结果\n  - other_args: 除主参数外的其他解析结果\n  - all_matched_args: 所有 Args 的解析结果\n\n### 路径查询\n\n`Arparma` 同时提供了便捷的查询方法 `query[type]()`，会根据传入的 `path` 查找参数并返回\n\n`path` 支持如下:\n\n- `main_args`, `options`, ...: 返回对应的属性\n- `args`: 返回 all_matched_args\n- `args.<key>`: 返回 all_matched_args 中 `key` 键对应的值\n- `main_args.<key>`: 返回主命令的解析参数字典中 `key` 键对应的值\n- `<node>`: 返回选项/子命令 `node` 的解析结果 (OptionResult | SubcommandResult)\n- `<node>.value`: 返回选项/子命令 `node` 的解析值\n- `<node>.args`: 返回选项/子命令 `node` 的解析参数字典\n- `<node>.<key>`, `<node>.args.<key>`: 返回选项/子命令 `node` 的参数字典中 `key` 键对应的值\n\n以及:\n\n- `options.<opt>`: 返回选项 `opt` 的解析结果 (OptionResult)\n- `options.<opt>.value`: 返回选项 `opt` 的解析值\n- `options.<opt>.args`: 返回选项 `opt` 的解析参数字典\n- `options.<opt>.<key>`, `options.<node>.args.<key>`: 返回选项 `opt` 的参数字典中 `key` 键对应的值\n- `subcommands.<subcmd>`: 返回子命令 `subcmd` 的解析结果 (SubcommandResult)\n- `subcommands.<subcmd>.value`: 返回子命令 `subcmd` 的解析值\n- `subcommands.<subcmd>.args`: 返回子命令 `subcmd` 的解析参数字典\n- `subcommands.<subcmd>.<key>`, `subcommands.<node>.args.<key>`: 返回子命令 `subcmd` 的参数字典中 `key` 键对应的值\n\n## 元数据(CommandMeta)\n\n`Alconna` 的元数据相当于其配置，拥有以下条目：\n\n- `description`: 命令的描述\n- `usage`: 命令的用法\n- `example`: 命令的使用样例\n- `author`: 命令的作者\n- `fuzzy_match`: 命令是否开启模糊匹配\n- `fuzzy_threshold`: 模糊匹配阈值\n- `raise_exception`: 命令是否抛出异常\n- `hide`: 命令是否对 manager 隐藏\n- `hide_shortcut`: 命令的快捷指令是否在 help 信息中隐藏\n- `keep_crlf`: 命令解析时是否保留换行字符\n- `compact`: 命令是否允许第一个参数紧随头部\n- `strict`: 命令是否严格匹配，若为 False 则未知参数将作为名为 $extra 的参数\n- `context_style`: 命令上下文插值的风格，None 为关闭，bracket 为 `{...}`，parentheses 为 `$(...)`\n- `extra`: 命令的自定义额外信息\n\n元数据一定使用 `meta=...` 形式传入：\n\n```python\nfrom arclet.alconna import Alconna, CommandMeta\n\nalc = Alconna(..., meta=CommandMeta(\"foo\", example=\"bar\"))\n```\n\n## 命名空间配置\n\n命名空间配置 （以下简称命名空间） 相当于 `Alconna` 的默认配置，其优先度低于 `CommandMeta`。\n\n`Alconna` 默认使用 \"Alconna\" 命名空间。\n\n命名空间有以下几个属性：\n\n- name: 命名空间名称\n- prefixes: 默认前缀配置\n- separators: 默认分隔符配置\n- formatter_type: 默认格式化器类型\n- fuzzy_match: 默认是否开启模糊匹配\n- raise_exception: 默认是否抛出异常\n- builtin_option_name: 默认的内置选项名称(--help, --shortcut, --comp)\n- disable_builtin_options: 默认禁用的内置选项(--help, --shortcut, --comp)\n- enable_message_cache: 默认是否启用消息缓存\n- compact: 默认是否开启紧凑模式\n- strict: 命令是否严格匹配\n- context_style: 命令上下文插值的风格\n- ...\n\n### 新建命名空间并替换\n\n```python\nfrom arclet.alconna import Alconna, namespace, Namespace, Subcommand, Args, config\n\n\nns = Namespace(\"foo\", prefixes=[\"/\"])  # 创建 \"foo\"命名空间配置, 它要求创建的Alconna的主命令前缀必须是/\n\nalc = Alconna(\"pip\", Subcommand(\"install\", Args[\"package\", str]), namespace=ns) # 在创建Alconna时候传入命名空间以替换默认命名空间\n\n# 可以通过with方式创建命名空间\nwith namespace(\"bar\") as np1:\n    np1.prefixes = [\"!\"]    # 以上下文管理器方式配置命名空间，此时配置会自动注入上下文内创建的命令\n    np1.formatter_type = ShellTextFormatter  # 设置此命名空间下的命令的 formatter 默认为 ShellTextFormatter\n    np1.builtin_option_name[\"help\"] = {\"帮助\", \"-h\"}  # 设置此命名空间下的命令的帮助选项名称\n\n# 你还可以使用config来管理所有命名空间并切换至任意命名空间\nconfig.namespaces[\"foo\"] = ns  # 将命名空间挂载到 config 上\n\nalc = Alconna(\"pip\", Subcommand(\"install\", Args[\"package\", str]), namespace=config.namespaces[\"foo\"]) # 也是同样可以切换到\"foo\"命名空间\n```\n\n### 修改默认的命名空间\n\n```python\nfrom arclet.alconna import config, namespace, Namespace\n\n\nconfig.default_namespace.prefixes = [...]  # 直接修改默认配置\n\nnp = Namespace(\"xxx\", prefixes=[...])\nconfig.default_namespace = np  # 更换默认的命名空间\n\nwith namespace(config.default_namespace.name) as np:\n    np.prefixes = [...]\n```\n\n## 快捷指令\n\n快捷命令可以做到标识一段命令, 并且传递参数给原命令\n\n一般情况下你可以通过 `Alconna.shortcut` 进行快捷指令操作 (创建，删除)\n\n`shortcut` 的第一个参数为快捷指令名称，第二个参数为 `ShortcutArgs`，作为快捷指令的配置：\n\n```python\nclass ShortcutArgs(TypedDict):\n    \"\"\"快捷指令参数\"\"\"\n\n    command: NotRequired[str]\n    \"\"\"快捷指令的命令\"\"\"\n    args: NotRequired[list[Any]]\n    \"\"\"快捷指令的附带参数\"\"\"\n    fuzzy: NotRequired[bool]\n    \"\"\"是否允许命令后随参数\"\"\"\n    prefix: NotRequired[bool]\n    \"\"\"是否调用时保留指令前缀\"\"\"\n    wrapper: NotRequired[ShortcutRegWrapper]\n    \"\"\"快捷指令的正则匹配结果的额外处理函数\"\"\"\n    humanized: NotRequired[str]\n    \"\"\"快捷指令的人类可读描述\"\"\"\n```\n\n### args的使用\n\n```python\nfrom arclet.alconna import Alconna, Args\n\n\nalc = Alconna(\"setu\", Args[\"count\", int])\n\nalc.shortcut(\"涩图(\\d+)张\", {\"args\": [\"{0}\"]})\n# 'Alconna::setu 的快捷指令: \"涩图(\\\\d+)张\" 添加成功'\n\nalc.parse(\"涩图3张\").query(\"count\")\n# 3\n```\n\n### command的使用\n\n```python\nfrom arclet.alconna import Alconna, Args\n\n\nalc = Alconna(\"eval\", Args[\"content\", str])\n\nalc.shortcut(\"echo\", {\"command\": \"eval print(\\\\'{*}\\\\')\"})\n# 'Alconna::eval 的快捷指令: \"echo\" 添加成功'\n\nalc.shortcut(\"echo\", delete=True) # 删除快捷指令\n# 'Alconna::eval 的快捷指令: \"echo\" 删除成功'\n\n@alc.bind() # 绑定一个命令执行器, 若匹配成功则会传入参数, 自动执行命令执行器\ndef cb(content: str):\n    eval(content, {}, {})\n\nalc.parse('eval print(\\\\\"hello world\\\\\")')\n# hello world\n\nalc.parse(\"echo hello world!\")\n# hello world!\n```\n\n当 `fuzzy` 为 False 时，第一个例子中传入 `\"涩图1张 abc\"` 之类的快捷指令将视为解析失败\n\n快捷指令允许三类特殊的 placeholder：\n\n- `{%X}`: 如 `setu {%0}`，表示此处填入快捷指令后随的第 X 个参数。\n\n例如，若快捷指令为 `涩图`, 配置为 `{\"command\": \"setu {%0}\"}`, 则指令 `涩图 1` 相当于 `setu 1`\n\n- `{*}`: 表示此处填入所有后随参数，并且可以通过 `{*X}` 的方式指定组合参数之间的分隔符。\n\n- `{X}`: 表示此处填入可能的正则匹配的组：\n\n- 若 `command` 中存在匹配组 `(xxx)`，则 `{X}` 表示第 X 个匹配组的内容\n- 若 `command` 中存储匹配组 `(?P<xxx>...)`, 则 `{X}` 表示 **名字** 为 X 的匹配结果\n\n除此之外, 通过 **Alconna** 内置选项 `--shortcut` 可以动态操作快捷指令\n\n例如：\n\n- `cmd --shortcut <key> <cmd>` 来增加一个快捷指令\n- `cmd --shortcut list` 来列出当前指令的所有快捷指令\n- `cmd --shortcut delete key` 来删除一个快捷指令\n\n```python\nfrom arclet.alconna import Alconna, Args\n\n\nalc = Alconna(\"eval\", Args[\"content\", str])\n\nalc.shortcut(\"echo\", {\"command\": \"eval print(\\\\'{*}\\\\')\"})\n\nalc.parse(\"eval --shortcut list\")\n# 'echo'\n```\n\n## 紧凑命令\n\n`Alconna`, `Option` 与 `Subcommand` 可以设置 `compact=True` 使得解析命令时允许名称与后随参数之间没有分隔：\n\n```python\nfrom arclet.alconna import Alconna, Option, CommandMeta, Args\n\n\nalc = Alconna(\"test\", Args[\"foo\", int], Option(\"BAR\", Args[\"baz\", str], compact=True), meta=CommandMeta(compact=True))\n\nassert alc.parse(\"test123 BARabc\").matched\n```\n\n这使得我们可以实现如下命令：\n\n```python\nfrom arclet.alconna import Alconna, Option, Args, append\n\n\nalc = Alconna(\"gcc\", Option(\"--flag|-F\", Args[\"content\", str], action=append, compact=True))\nprint(alc.parse(\"gcc -Fabc -Fdef -Fxyz\").query[list](\"flag.content\"))\n# ['abc', 'def', 'xyz']\n```\n\n当 `Option` 的 `action` 为 `count` 时，其自动支持 `compact` 特性：\n\n```python\nfrom arclet.alconna import Alconna, Option, count\n\n\nalc = Alconna(\"pp\", Option(\"--verbose|-v\", action=count, default=0))\nprint(alc.parse(\"pp -vvv\").query[int](\"verbose.value\"))\n# 3\n```\n\n## 模糊匹配\n\n模糊匹配会应用在任意需要进行名称判断的地方，如 **命令名称**，**选项名称** 和 **参数名称** (如指定需要传入参数名称)。\n\n```python\nfrom arclet.alconna import Alconna, CommandMeta\n\n\nalc = Alconna(\"test_fuzzy\", meta=CommandMeta(fuzzy_match=True))\n\nalc.parse(\"test_fuzy\")\n# test_fuzy is not matched. Do you mean \"test_fuzzy\"?\n```\n\n## 半自动补全\n\n半自动补全为用户提供了推荐后续输入的功能\n\n补全默认通过 `--comp` 或 `-cp` 或 `?` 触发：（命名空间配置可修改名称）\n\n```python\nfrom arclet.alconna import Alconna, Args, Option\n\n\nalc = Alconna(\"test\", Args[\"abc\", int]) + Option(\"foo\") + Option(\"bar\")\nalc.parse(\"test --comp\")\n\n'''\noutput\n\n以下是建议的输入：\n* <abc: int>\n* --help\n* -h\n* -sct\n* --shortcut\n* foo\n* bar\n'''\n```\n\n## Duplication\n\n**Duplication** 用来提供更好的自动补全，类似于 **ArgParse** 的 **Namespace**\n\n普通情况下使用，需要利用到 **ArgsStub**、**OptionStub** 和 **SubcommandStub** 三个部分\n\n以pip为例，其对应的 Duplication 应如下构造:\n\n```python\nfrom arclet.alconna import Alconna, Args, Option, OptionResult, Duplication, SubcommandStub, Subcommand, count\n\n\nclass MyDup(Duplication):\n    verbose: OptionResult\n    install: SubcommandStub\n\n\nalc = Alconna(\n    \"pip\",\n    Subcommand(\n        \"install\",\n        Args[\"package\", str],\n        Option(\"-r|--requirement\", Args[\"file\", str]),\n        Option(\"-i|--index-url\", Args[\"url\", str]),\n    ),\n    Option(\"-v|--version\"),\n    Option(\"-v|--verbose\", action=count),\n)\n\nres = alc.parse(\"pip -v install ...\") # 不使用duplication获得的提示较少\nprint(res.query(\"install\"))\n# (value=Ellipsis args={'package': '...'} options={} subcommands={})\n\nresult = alc.parse(\"pip -v install ...\", duplication=MyDup)\nprint(result.install)\n# 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')\n```\n\n**Duplication** 也可以如 **Namespace** 一样直接标明参数名称和类型：\n\n```python\nfrom typing import Optional\nfrom arclet.alconna import Duplication\n\n\nclass MyDup(Duplication):\n    package: str\n    file: Optional[str] = None\n    url: Optional[str] = None\n```\n\n## 上下文插值\n\n当 `context_style` 条目被设置后，传入的命令中符合上下文插值的字段会被自动替换成当前上下文中的信息。\n\n上下文可以在 `parse` 中传入：\n\n```python\nfrom arclet.alconna import Alconna, Args, CommandMeta\n\nalc = Alconna(\"test\", Args[\"foo\", int], meta=CommandMeta(context_style=\"parentheses\"))\n\nalc.parse(\"test $(bar)\", {\"bar\": 123})\n# {\"foo\": 123}\n```\n\ncontext_style 的值分两种：\n\n- `\"bracket\"`: 插值格式为 `{...}`，例如 `{foo}`\n- `\"parentheses\"`: 插值格式为 `$(...)`，例如 `$(bar)`\n"
  },
  {
    "path": "website/docs/best-practice/alconna/config.md",
    "content": "---\nsidebar_position: 4\ndescription: 配置项\n---\n\n# 配置项\n\n## alconna_auto_send_output\n\n- **类型**: `bool | None`\n- **默认值**: `None`\n\n是否全局启用输出信息自动发送，不启用则会在触发特殊内置选项后仍然将解析结果传递至响应器。\n\n## alconna_use_command_start\n\n- **类型**: `bool`\n- **默认值**: `False`\n\n是否读取 Nonebot 的配置项 `COMMAND_START` 来作为全局的 Alconna 命令前缀\n\n## alconna_global_completion\n\n- **类型**: [`CompConfig | None`](./matcher.mdx#补全会话)\n- **默认值**: `None`\n\n全局的补全会话配置 (不代表全局启用补全会话)。\n\n## alconna_use_origin\n\n- **类型**: `bool`\n- **默认值**: `False`\n\n是否全局使用原始消息 (即未经过 to_me 等处理的)，该选项会影响到 Alconna 的匹配行为。\n\n## alconna_use_command_sep\n\n- **类型**: `bool`\n- **默认值**: `False`\n\n是否读取 Nonebot 的配置项 `COMMAND_SEP` 来作为全局的 Alconna 命令分隔符。\n\n## alconna_global_extensions\n\n- **类型**: `list[str]`\n- **默认值**: `[]`\n\n全局加载的扩展，其读取路径以 . 分隔，如 `foo.bar.baz:DemoExtension`。\n\n对于内置扩展，路径为 `nonebot_plugin_alconna.builtins.extensions` 下的模块名，如 `ReplyMergeExtension`，可以使用 `@` 来缩写路径，\n如 `@reply:ReplyMergeExtension`。\n\n## alconna_context_style\n\n- **类型**: `Optional[Literal[\"bracket\", \"parentheses\"]]`\n- **默认值**: `None`\n\n全局命令上下文插值的风格，None 为关闭，bracket 为 `{...}`，parentheses 为 `$(...)`。\n\n## alconna_enable_saa_patch\n\n- **类型**: `bool`\n- **默认值**: `False`\n\n是否启用 SAA 补丁。\n\n## alconna_apply_filehost\n\n- **类型**: `bool`\n- **默认值**: `False`\n\n是否启用文件托管。\n\n## alconna_apply_fetch_targets\n\n- **类型**: `bool`\n- **默认值**: `False`\n\n是否启动时拉取一次[发送对象](./uniseg/utils.mdx#发送对象)列表。\n\n## alconna_builtin_plugins\n\n- **类型**: `set[str]`\n- **默认值**: `set()`\n\n需要加载的内置插件列表。\n\n## alconna_conflict_resolver\n\n- **类型**: `Literal[\"raise\", \"default\", \"ignore\", \"replace\"]`\n- **默认值**: `\"default\"`\n\n命令冲突解决策略，决定当不同插件之间或者同一插件之间存在两个以上相同的命令时的处理方式：\n\n- `default`: 默认处理方式，保留两个命令\n- `raise`: 抛出异常\n- `ignore`: 忽略较新的命令\n- `replace`: 替换较旧的命令\n\n## alconna_response_self\n\n- **类型**: `bool`\n- **默认值**: `False`\n\n是否让响应器处理由 bot 自身发送的消息。\n"
  },
  {
    "path": "website/docs/best-practice/alconna/matcher.mdx",
    "content": "---\nsidebar_position: 3\ndescription: 响应规则的使用\n---\n\nimport Messenger from \"@site/src/components/Messenger\";\nimport Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\n\n# `on_alconna` 响应器\n\n`nonebot_plugin_alconna` 插件本体的大部分功能都围绕着 `on_alconna` 响应器展开。\n\n该响应器类似于 `on_command`，基于 `Alconna` 解析器来解析命令。\n\n以下是一个简单的 `on_alconna` 响应器的例子：\n\n```python\nfrom nonebot_plugin_alconna import At, Image, Match, on_alconna\nfrom arclet.alconna import Args, Option, Alconna, MultiVar, Subcommand\n\n\nalc = Alconna(\n    \"role-group\",\n    Subcommand(\n        \"add|添加\",\n        Args[\"name\", str],\n        Option(\"member\", Args[\"target\", MultiVar(At)]),\n        dest=\"add\",\n        compact=True,\n    ),\n    Option(\"list\"),\n    Option(\"icon\", Args[\"icon\", Image])\n)\nrg = on_alconna(alc, use_command_start=True, aliases={\"角色组\"})\n\n\n@rg.assign(\"list\")\nasync def list_role_group():\n    img: bytes = await gen_role_group_list_image()\n    await rg.finish(Image(raw=img))\n\n@rg.assign(\"add\")\nasync def _(name: str, target: Match[tuple[At, ...]]):\n    group = await create_role_group(name)\n    if target.available:\n        ats: tuple[At, ...] = target.result\n        group.extend(member.target for member in ats)\n    await rg.finish(\"添加成功\")\n```\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/role-group list\" },\n    {\n      position: \"left\",\n      msg: \"[图片]\",\n    },\n    { position: \"right\", msg: \"/角色组 添加foo @bar @baz\" },\n    { position: \"left\", msg: \"添加成功\" },\n  ]}\n/>\n\n## 声明\n\n`on_alconna` 的参数如下:\n\n```python\ndef on_alconna(\n    command: Alconna | str,\n    rule: Rule | T_RuleChecker | None = None,\n    skip_for_unmatch: bool = True,\n    auto_send_output: bool | None = None,\n    aliases: set[str] | tuple[str, ...] | None = None,\n    comp_config: CompConfig | None = None,\n    extensions: list[type[Extension] | Extension] | None = None,\n    exclude_ext: list[type[Extension] | str] | None = None,\n    use_origin: bool | None = None,\n    use_cmd_start: bool | None = None,\n    use_cmd_sep: bool | None = None,\n    response_self: bool | None = None,\n    **kwargs: Any,\n) -> type[AlconnaMatcher]:\n    ...\n```\n\n- `command`: Alconna 命令或字符串，字符串将通过 `AlconnaFormat` 转换为 Alconna 命令\n- `rule`: 事件响应规则， 详见 [响应器规则](../../advanced/matcher.md#事件响应规则)\n- `skip_for_unmatch`: 是否在命令不匹配时跳过该响应, 默认为 `True`\n- `auto_send_output`: 是否自动发送输出信息并跳过该响应。\n  - `True`：自动发送输出信息并跳过该响应\n  - `False`：不自动发送输出信息，而是传递进行处理\n  - `None`：跟随全局配置项 `alconna_auto_send_output`，默认值为 `True`\n- `aliases`: 命令别名， 作用类似于 `on_command` 中的 aliases\n- `comp_config`: 补全会话配置， 不传入则不启用补全会话\n- `extensions`: 需要加载的匹配扩展, 可以是扩展类或扩展实例\n- `exclude_ext`: 需要排除的匹配扩展, 可以是扩展类或扩展的id\n- `use_origin`: 是否使用未经 to_me 等处理过的消息。`None` 时跟随全局配置项 `alconna_use_origin`，默认值为 `False`\n- `use_cmd_start`: 是否使用 COMMAND_START 作为命令前缀。`None` 时跟随全局配置项 `alconna_use_command_start`，默认值为 `False`\n- `use_cmd_sep`: 是否使用 COMMAND_SEP 作为命令分隔符。`None` 时跟随全局配置项 `alconna_use_command_sep`，默认值为 `False`\n- `response_self`: 是否响应自身消息。`None` 时跟随全局配置项 `alconna_response_self`，默认值为 `False`\n\n`on_alconna` 返回的是 `Matcher` 的子类 `AlconnaMatcher` ，其拓展了如下方法：\n\n- `.assign(path, value, or_not)`: 用于对包含多个选项/子命令的命令的分派处理\n- `.dispatch`: 同样的分派处理，但是是类似 `CommandGroup` 一样返回新的 `AlconnaMatcher`\n- `.got_path(path, prompt, middleware)`: 在 `got` 方法的基础上，会以 path 对应的参数为准，读取传入 message 的最后一个消息段并验证转换\n- `.got`, `send`, `reject`, ... : 拓展了 prompt 类型，即支持使用 `UniMessage` 作为 prompt\n- ...\n\n除了标准的创建方式，本插件也提供了 `funcommand` 和 `Command` 两种快捷方式来创建 `AlconnaMatcher`， 详见 [快捷方式](./shortcut.md)。\n\n## 依赖注入\n\n`AlconnaMatcher` 的特性之一是拓展了依赖注入的功能。\n\n### 注入模型\n\n插件提供了几种用来处理解析结果的模型：\n\n- `CommandResult`: 用于快捷访问命令解析结果\n  - `result (Arparma)`: 解析结果\n  - `source (Alconna)`: 源命令\n  - `matched (bool)`: 是否匹配\n  - `context (dict)`: 命令的上下文\n  - `output (str | None)`: 命令的输出\n- `Match`: 匹配项，表示参数是否存在于 `Arparma.all_matched_args` 内，可用 `Match.available` 判断是否匹配，`Match.result` 获取匹配的值\n  - `Match` 只能查找到 `Arparma.all_matched_args` 中的参数。对于特定选项/子命令的参数，需要使用 `Query` 来查询\n- `Query`: 查询项，表示参数是否可由 `Arparma.query` 查询并获得结果，可用 `Query.available` 判断是否查询成功，`Query.result` 获取查询结果\n  - `Query` 除了查询参数，也可以查询某个选项/子命令是否存在\n\n### 编写\n\n```python\nasync def handle(\n    result: CommandResult,\n    arp: Arparma,\n    dup: Duplication,\n    source: Alconna,\n    ext: Extension,\n    exts: SelectedExtensions,\n    abc: str,\n    foo: Match[str],\n    bar: Query[int] = Query(\"ttt.bar\", 0)\n):\n    ...\n```\n\n`AlconnaMatcher` 的依赖注入拓展支持以下情况：\n\n- `xxx: CommandResult`\n- `xxx: Arparma`：命令的[解析结果](./command.md#解析结果)\n- `xxx: Duplication`：命令的解析结果的 [`Duplication`](./command.md#duplication)\n- `xxx: Alconna`：命令的源命令\n- `<key>: Match[<type>]`：上述的匹配项，使用 `key` 作为查询路径\n- `xxx: Query[<type>] = Query(<path>, default)`：上述的查询项，必需声明默认值以设置查询路径 `path`\n  - 当用来查询选项/子命令是否存在时，可不写 `Query[<type>]`\n- `xxx: Extension`：当前 `AlconnaMatcher` 使用的指定类型的匹配扩展\n- `xxx: SelectedExtensions`：当前 `AlconnaMatcher` 使用的所有可用的匹配扩展\n- `<key>: <type>`: 其他情况\n  - 当 `key` 的名称是 \"ctx\" 或 \"context\" 并且类型为 `dict` 时，会注入命令的上下文\n  - 当 `key` 存在于命令的上下文中时，会注入对应的值\n  - 当 `key` 存在于 `Arparma` 的 `all_matched_args` 中时，会注入对应的值, 类似于 `Match` 的用法，但当该值不存在时将跳过响应器。\n  - 当 `key` 属于 `got_path` 的参数时，会注入对应的值\n  - 当 `key` 被某个 `Extension.before_catch` 确认为需要注入的参数时，会调用 `Extension.catch` 来注入对应的值\n\n:::note\n\n如果你更喜欢 Depends 式的依赖注入，`nonebot_plugin_alconna` 同时提供了一系列的依赖注入函数，他们包括：\n\n- `AlconnaResult`: `CommandResult` 类型的依赖注入函数\n- `AlconnaMatches`: `Arparma` 类型的依赖注入函数\n- `AlconnaDuplication`: `Duplication` 类型的依赖注入函数\n- `AlconnaMatch`: `Match` 类型的依赖注入函数，其能够额外传入一个 middleware 函数来处理得到的参数\n- `AlconnaQuery`: `Query` 类型的依赖注入函数，其能够额外传入一个 middleware 函数来处理得到的参数\n- `AlconnaExecResult`: 提供挂载在命令上的 callback 的返回结果 (`Dict[str, Any]`) 的依赖注入函数\n- `AlconnaExtension`: 提供指定类型的 `Extension` 的依赖注入函数\n\n:::\n\n示例：\n\n```python\nfrom nonebot import require\nrequire(\"nonebot_plugin_alconna\")\n\nfrom nonebot_plugin_alconna import AlconnaQuery, AlcResult, Match, Query, on_alconna\nfrom arclet.alconna import Alconna, Args, Option, Arparma\n\n\ntest = on_alconna(\n    Alconna(\n        \"test\",\n        Option(\"foo\", Args[\"bar\", int]),\n        Option(\"baz\", Args[\"qux\", bool, False])\n    )\n)\n\n@test.handle()\nasync def handle_test1(result: AlcResult):\n    await test.send(f\"matched: {result.matched}\")\n    await test.send(f\"maybe output: {result.output}\")\n\n@test.handle()\nasync def handle_test2(result: Arparma):\n    await test.send(f\"head result: {result.header_result}\")\n    await test.send(f\"args: {result.all_matched_args}\")\n\n@test.handle()\nasync def handle_test3(bar: Match[int]):\n    if bar.available:\n        await test.send(f\"foo={bar.result}\")\n\n@test.handle()\nasync def handle_test4(qux: Query[bool] = AlconnaQuery(\"baz.qux\", False)):\n    if qux.available:\n        await test.send(f\"baz.qux={qux.result}\")\n```\n\n## 条件控制\n\n### `assign` 方法\n\n`AlconnaMatcher` 的 `assign` 方法与 `handle` 类似，但是可以控制响应函数是否在不满足条件时跳过响应。\n\n`assign` 方法的参数如下：\n\n```python\ndef assign(\n    cls,\n    path: str,\n    value: Any = _seminal,\n    or_not: bool = False,\n    additional: CHECK | None = None,\n    parameterless: Iterable[Any] | None = None,\n):\n    ...\n```\n\n- `path`: 指定的[查询路径](./command.md#路径查询)\n  - \"$main\" 表示没有任何选项/子命令匹配的时候\n  - \"\\~XX\" 时会把 \"\\~\" 替换为父级路径\n- `value`: 可能的指定查询值\n- `or_not`: 是否同时处理没有查询成功的情况\n- `additional`: 额外的条件检查函数\n\n例如：\n\n```python\n# 处理没有任何选项/子命令匹配的情况\n@rg.assign(\"$main\")\nasync def handle_main(): ...\n\n# 处理 list 选项\n@rg.assign(\"list\")\nasync def handle_list(): ...\n\n# 处理 add 选项，且 name 为 admin\n@rg.assign(\"add.name\", \"admin\")\nasync def handle_add_admin(): ...\n```\n\n### `dispatch` 方法\n\n此外，使用 `.dispatch` 还能像 `CommandGroup` 一样为每个分发设置独立的 matcher：\n\n```python\nrg_list_cmd = rg.dispatch(\"list\")\n\n@rg_list_cmd.handle()\nasync def handle_list(): ...\n```\n\n`dispatch` 的参数与 `assign` 相同。\n\n当使用 `dispatch` 时，父级路径表示为传入 `dispatch` 的 `path`:\n\n```python\nrg_add_cmd = rg.dispatch(\"add\")\n\n# 此时 ~name 表示 add.name\n@rg_add_cmd.assign(\"~name\", \"admin\")\nasync def handle_add_admin(): ...\n```\n\n:::tip\n\n在 `dispatch` 下, `Query` 的 `path` 也同样支持 `~` 前缀来表示父级路径\n\n```python\n@rg_add_cmd.assign(\"~name\", \"admin\")\nasync def handle_add_admin(target: Query[tuple[At, ...]] = Query(\"~target\")):\n    if target.available:\n        await rg.send(f\"添加成功: {target.result}\")\n```\n\n:::\n\n### `got_path` 方法\n\n另外，`AlconnaMatcher` 有类似于 [`got`](../../appendices/session-control.mdx#got) 的 `got_path` 与配套的 `get_path_arg`, `set_path_arg`：\n\n```python\nfrom nonebot_plugin_alconna import At, Match, UniMessage, on_alconna\n\n\ntest_cmd = on_alconna(Alconna(\"test\", Args[\"target?\", Union[str, At]]))\n\n@test_cmd.handle()\nasync def tt_h(target: Match[Union[str, At]]):\n    if target.available:\n        test_cmd.set_path_arg(\"target\", target.result)\n\n@test_cmd.got_path(\"target\", prompt=\"请输入目标\")\nasync def tt(target: Union[str, At]):\n    await test_cmd.send(UniMessage([\"ok\\n\", target]))\n```\n\n`got_path` 与 `assign`，`Match`，`Query` 等地方一样，都需要指明 `path` 参数 (即对应 Arg 验证的路径)\n\n`got_path` 会获取消息的最后一个消息段并转为 path 对应的类型，例如示例中 `target` 对应的 Arg 里要求 str 或 At，则 got 后用户输入的消息只有为 text 或 at 才能进入处理函数。\n\n`got_path` 中可以使用依赖注入函数 `AlconnaArg`, 类似于 [`Arg`](../../advanced/dependency.mdx#arg).\n\n### `prompt` 方法\n\n基于 [`Waiter`](https://github.com/RF-Tar-Railt/nonebot-plugin-waiter) 插件，`AlconnaMatcher` 提供了 `prompt` 方法来实现更灵活的交互式提示。\n\n```python\nfrom nonebot_plugin_alconna import At, Match, UniMessage, on_alconna\n\n\ntest_cmd = on_alconna(Alconna(\"test\", Args[\"target?\", Union[str, At]]))\n\n@test_cmd.handle()\nasync def tt_h(target: Match[Union[str, At]]):\n    if target.available:\n        await test_cmd.finish(UniMessage([\"ok\\n\", target]))\n    resp = await test_cmd.prompt(\"请输入目标\", timeout=30)  # 等待 30 秒\n    if resp is None:\n        await test_cmd.finish(\"超时\")\n    await test_cmd.finish(UniMessage([\"ok\\n\", resp[-1]]))\n```\n\n## 返回值中间件\n\n在 `AlconnaMatch`，`AlconnaQuery` 或 `got_path` 中，你可以使用 `middleware` 参数来传入一个对返回值进行处理的函数：\n\n```python\nfrom nonebot_plugin_alconna import image_fetch\n\n\nmask_cmd = on_alconna(Alconna(\"search\", Args[\"img?\", Image]))\n\n\n@mask_cmd.handle()\nasync def mask_h(matcher: AlconnaMatcher, img: Match[bytes] = AlconnaMatch(\"img\", image_fetch)):\n    result = await search_img(img.result)\n    await matcher.send(result.content)\n```\n\n其中，`image_fetch` 是一个中间件，其接受一个 `Image` 对象，并提取图片的二进制数据返回。\n\n## i18n\n\n本插件基于 `tarina.lang` 模块提供了 i18n 的支持，参见 [Lang 用法](https://github.com/nonebot/plugin-alconna/discussions/50)。\n\n当你编写完语言文件后，你便可以通过 `AlconnaMatcher.i18n` 来快速地将语言文件中的内容转为 UniMessage.\n\n<Tabs groupId=\"i18n\">\n<TabItem value=\"zh\" label=\"中文\">\n\n```yaml title=\"zh-CN.yml\"\n# 中文语言文件\ndemo:\n  command:\n    role-group:\n      add: 添加 {name} 成功!\n```\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/角色组 添加 foo\" },\n    { position: \"left\", msg: \"添加 foo 成功!\" },\n  ]}\n/>\n\n</TabItem>\n<TabItem value=\"en\" label=\"英文\">\n\n```yaml title=\"en-US.yml\"\n# 英文语言文件\ndemo:\n  command:\n    role-group:\n      add: Add {name} successfully!\n```\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/role-group add foo\" },\n    { position: \"left\", msg: \"Add foo successfully!\" },\n  ]}\n/>\n</TabItem>\n</Tabs>\n\n```python title=\"使用 i18n\"\n@rg.assign(\"add\")\nasync def handle_add(name: str):\n    await rg.i18n(\"demo\", \"command.role-group.add\", name=name).finish()\n```\n\n## 匹配测试\n\n`AlconnaMatcher.test` 方法允许你在 NoneBot 启动时对命令进行测试。\n\n```python\ndef test(\n    cls,\n    message: str | UniMessage,\n    expected: dict[str, Any] | None = None,\n    prefix: bool = True\n): ...\n```\n\n- `message`: 测试的消息\n- `expected`: 预期的解析结果，若为 None 则表示只测试是否匹配\n- `prefix`: 是否使用命令前缀，默认为 True\n\n## 匹配拓展\n\n本插件提供了一个 `Extension` 类，其用于自定义 AlconnaMatcher 的部分行为\n\n目前 `Extension` 的功能有：\n\n- `validate`: 对于事件的来源适配器或 bot 选择是否接受响应\n- `output_converter`: 输出信息的自定义转换方法\n- `message_provider`: 从传入事件中自定义提取消息的方法\n- `receive_provider`: 对传入的消息 (UniMessage) 的额外处理\n- `context_provider`: 对命令上下文的额外处理\n- `permission_check`: 命令对消息解析并确认头部匹配（即确认选择响应）时对发送者的权限判断\n- `parse_wrapper`: 对命令解析结果的额外处理\n- `send_wrapper`: 对发送的消息 (Message 或 UniMessage) 的额外处理\n- `before_catch`: 自定义依赖注入的绑定确认函数\n- `catch`: 自定义依赖注入处理函数\n- `post_init`: 响应器创建后对命令对象的额外处理\n\n:::tip\n\nExtension 可以通过 `add_global_extension` 方法来全局添加。\n\n```python\nfrom nonebot_plugin_alconna import add_global_extension\nfrom nonebot_plugin_alconna.builtins.extensions.telegram import TelegramSlashExtension\n\nadd_global_extension(TelegramSlashExtension)\n```\n\n全局的 Extension 可延迟加载 (即若有全局拓展加载于部分 AlconnaMatcher 之后，这部分响应器会被追加拓展)\n\n:::\n\n例如一个 `LLMExtension` 可以如下实现 (仅举例)：\n\n```python\nfrom nonebot_plugin_alconna import Extension, Alconna, on_alconna, Interface\n\n\nclass LLMExtension(Extension):\n    @property\n    def priority(self) -> int:\n        return 10\n\n    @property\n    def id(self) -> str:\n        return \"LLMExtension\"\n\n    def __init__(self, llm):\n      self.llm = llm\n\n    def post_init(self, alc: Alconna) -> None:\n        self.llm.add_context(alc.command, alc.meta.description)\n\n    async def receive_wrapper(self, bot, event, receive):\n        resp = await self.llm.input(str(receive))\n        return receive.__class__(resp.content)\n\n    def before_catch(self, name, annotation, default):\n        return name == \"llm\"\n\n    def catch(self, interface: Interface):\n        if interface.name == \"llm\":\n            return self.llm\n\nmatcher = on_alconna(\n    Alconna(...),\n    extensions=[LLMExtension(LLM)]\n)\n...\n```\n\n那么添加了 `LLMExtension` 的响应器便能接受任何能通过 llm 翻译为具体命令的自然语言消息，同时可以在响应器中为所有 `llm` 参数注入模型变量。\n\n### validate\n\n```python\ndef validate(self, bot: Bot, event: Event) -> bool: ...\n```\n\n默认情况下，`validate` 方法会筛选 `event.get_type()` 为 `message` 的情况，表示接受消息事件。\n\n### output_converter\n\n```python\nasync def output_converter(self, output_type: OutputType, content: str) -> UniMessage: ...\n```\n\n依据输出信息的类型，将字符串转换为消息对象以便发送。\n\n其中 `OutputType` 为 \"help\", \"shortcut\", \"completion\", \"error\" 其中之一。\n\n该方法只会调用一次，即对于多个 Extension，选择优先级靠前且实现了该方法的 Extension。\n\n### message_provider\n\n```python\nasync def message_provider(\n    self, event: Event, state: T_State, bot: Bot, use_origin: bool = False\n) -> UniMessage | None:...\n```\n\n该方法用于从事件中提取消息，默认情况下会使用 `event.get_message()` 来获取消息。\n\n该方法可能会调用多次，即对于多个 Extension，选择优先级靠前且实现了该方法的 Extension，若调用的返回值不为 `None` 则作为结果。\n\n:::caution\n\n该方法的默认实现对结果 (UniMessage) 会进行缓存。`Extension` 的实现也应尽量实现缓存机制。\n\n:::\n\n### receive_provider\n\n```python\nasync def receive_provider(self, bot: Bot, event: Event, command: Alconna, receive: UniMessage) -> UniMessage: ...\n```\n\n该方法用于对传入的消息 (UniMessage) 进行额外处理，默认情况下会返回原始消息。\n\n该方法会调用多次，即对于多个 Extension，前一个 Extension 的返回值会作为下一个 Extension 的输入。\n\n### context_provider\n\n```python\nasync def context_provider(self, ctx: dict[str, Any], bot: Bot, event: Event, state: T_State) -> dict[str, Any]:\n```\n\n该方法用于提取命令上下文，默认情况下会返回 `ctx` 本身。\n\n该方法会调用多次，即对于多个 Extension，前一个 Extension 的返回值会作为下一个 Extension 的输入。\n\n### permission_check\n\n```python\nasync def permission_check(self, bot: Bot, event: Event, command: Alconna) -> bool: ...\n```\n\n该方法用于对发送者的权限进行检查，默认情况下会返回 `True`。\n\n该方法可能会调用多次，即对于多个 Extension，若调用的返回值不为 `True` 则结束判断。\n\n### parse_wrapper\n\n```python\nasync def parse_wrapper(self, bot: Bot, state: T_State, event: Event, res: Arparma) -> None: ...\n```\n\n该方法用于对命令解析结果进行额外处理。\n\n该方法会调用多次，即对于多个 Extension，会并发地调用该方法。\n\n### send_wrapper\n\n```python\nasync def send_wrapper(self, bot: Bot, event: Event, send: TMessage) -> TMessage: ...\n```\n\n该方法用于对 `AlconnaMatcher.send` 或 `UniMessage.send` 发送的消息 (str 或 Message 或 UniMessage) 进行额外处理，默认情况下会返回原始消息。\n\n该方法会调用多次，即对于多个 Extension，前一个 Extension 的返回值会作为下一个 Extension 的输入。\n\n由于需要保证输入与输出的类型一致，该方法内需要自行判断类型。\n\n### before_catch\n\n```python\ndef before_catch(self, name: str, annotation: type, default: Any) -> bool: ...\n```\n\n该方法用于响应函数中某个参数是否需要绑定到该 Extension 上。\n\n### catch\n\n```python\nasync def catch(self, interface: Interface) -> Any: ...\n```\n\n该方法用于注入经过 `before_catch` 确认的参数。其中 `Interface` 的定义为\n\n```python\nclass Interface(Generic[TE]):\n    event: TE\n    state: T_State\n    name: str\n    annotation: Any\n    default: Any\n```\n\n## 补全会话\n\n补全会话基于 [`半自动补全`](./command.md#半自动补全)，用于指令参数缺失或参数错误时给予交互式提示，类似于 `got-reject`：\n\n```python\nfrom nonebot_plugin_alconna import Alconna, Args, Field, At, on_alconna\n\nalc = Alconna(\n    \"添加教师\",\n    Args[\"name\", str, Field(completion=lambda: \"请输入姓名\")],\n    Args[\"phone\", int, Field(completion=lambda: \"请输入手机号\")],\n    Args[\"at\", [str, At], Field(completion=lambda: \"请输入教师号\")],\n)\n\ncmd = on_alconna(alc, comp_config={\"lite\": True}, skip_for_unmatch=False)\n\n@cmd.handle()\nasync def handle(result: Arparma):\n    cmd.finish(\"添加成功\")\n```\n\n此时，当用户输入 `添加教师` 时，会自动提示用户输入姓名，手机号和教师号，用户输入后会自动进入下一个提示：\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"添加教师\" },\n    { position: \"left\", msg: \"以下是建议的输入： \\n- name: 请输入姓名\" },\n    { position: \"right\", msg: \"foo\" },\n    { position: \"left\", msg: \"以下是建议的输入： \\n- phone: 请输入手机号\" },\n    { position: \"right\", msg: \"12345\" },\n    { position: \"left\", msg: \"以下是建议的输入： \\n- at: 请输入教师号\" },\n    { position: \"right\", msg: \"@me\" },\n    { position: \"left\", msg: \"添加成功\" },\n  ]}\n/>\n\n补全会话配置如下：\n\n```python\nclass CompConfig(TypedDict):\n    tab: NotRequired[str]\n    \"\"\"用于切换提示的指令的名称\"\"\"\n    enter: NotRequired[str]\n    \"\"\"用于输入提示的指令的名称\"\"\"\n    exit: NotRequired[str]\n    \"\"\"用于退出会话的指令的名称\"\"\"\n    timeout: NotRequired[int]\n    \"\"\"超时时间\"\"\"\n    hide_tabs: NotRequired[bool]\n    \"\"\"是否隐藏所有提示\"\"\"\n    hides: NotRequired[Set[Literal[\"tab\", \"enter\", \"exit\"]]]\n    \"\"\"隐藏的指令\"\"\"\n    disables: NotRequired[Set[Literal[\"tab\", \"enter\", \"exit\"]]]\n    \"\"\"禁用的指令\"\"\"\n    lite: NotRequired[bool]\n    \"\"\"是否使用简洁版本的补全会话（相当于同时配置 disables、hides、hide_tabs）\"\"\"\n    block: NotRequired[bool]\n    \"\"\"进行补全会话时是否阻塞响应器\"\"\"\n```\n"
  },
  {
    "path": "website/docs/best-practice/alconna/shortcut.md",
    "content": "---\nsidebar_position: 6\ndescription: 快捷方式\n---\n\n# 快捷方式声明\n\n针对 `Alconna` 编写对于入门开发者来说较为复杂的问题，本插件提供了一些快捷方式来简化开发者的工作。\n\n## 装饰器构造器\n\n本插件提供了一个 `funcommand` 装饰器, 其用于将一个接受任意参数， 返回 `str` 或 `Message` 或 `MessageSegment` 的函数转换为命令响应器：\n\n```python\nfrom nonebot_plugin_alconna import funcommand\n\n\n@funcommand()\nasync def echo(msg: str):\n    return msg\n```\n\n其等同于：\n\n```python\nfrom arclet.alconna import Alconna, Args\nfrom nonebot_plugin_alconna import on_alconna, AlconnaMatch, Match\n\n\necho = on_alconna(Alconna(\"echo\", Args[\"msg\", str]))\n\n@echo.handle()\nasync def echo_exit(msg: Match[str] = AlconnaMatch(\"msg\")):\n    await echo.finish(msg.result)\n\n```\n\n相比于 `on_alconna`， `funcommand` 增加了三个参数 `name`, `prefixes` 和 `description`。\n\n## 类 Koishi 构造器\n\n本插件提供了一个 `Command` 构造器，其基于 `arclet.alconna.tools` 中的 `AlconnaString`， 以类似 `Koishi` 中[注册命令](https://koishi.chat/zh-CN/guide/basic/command.html)的方式来构建一个 **AlconnaMatcher** ：\n\n```python\nfrom nonebot_plugin_alconna import Command, Arparma\n\n\nbook = (\n    Command(\"book\", \"测试\")\n    .option(\"writer\", \"-w <id:int>\")\n    .option(\"writer\", \"--anonymous\", {\"id\": 0})\n    .usage(\"book [-w <id:int> | --anonymous]\")\n    .shortcut(\"测试\", {\"args\": [\"--anonymous\"]})\n    .build()\n)\n\n@book.handle()\nasync def _(arp: Arparma):\n    await book.send(str(arp.options))\n```\n\n甚至，你可以设置 `action` 来设定响应行为：\n\n```python\nbook = (\n    Command(\"book\", \"测试\")\n    .option(\"writer\", \"-w <id:int>\")\n    .option(\"writer\", \"--anonymous\", {\"id\": 0})\n    .usage(\"book [-w <id:int> | --anonymous]\")\n    .shortcut(\"测试\", {\"args\": [\"--anonymous\"]})\n    .action(lambda options: str(options))  # 会自动通过 bot.send 发送\n    .build()\n)\n```\n\n### 参数类型\n\n`Command` 的参数类型也如 `koishi` 一样，**必选参数** 用尖括号包裹，**可选参数** 用方括号包裹:\n\n- `foo` 表示参数 `foo`, 类型为 Any\n- `foo:int` 表示参数 `foo`, 类型为 int\n- `foo:int=1` 表示参数 `foo`, 类型为 int, 默认值为 1\n- `...foo` 表示[泛匹配参数](command.md#allparam)\n- `foo:str+`, `foo:str*` 表示[变长参数](command.md#multivar-与-keywordvar) `foo`, 类型为 str\n- `foo:+str`, `foo:text` 表示参数 `foo`, 类型为 str, 并且将包含空格 (即将变长参数的结果用空格合并)\n\n特别的，针对类型部分，本插件拓展了如下内容:\n\n- `foo:At`, `foo:Image`, ... 表示类型为[通用消息段](./uniseg/segment.md)\n- `foo:select(Image).first` 表示获取子元素类型\n- `foo:Dot(Image, 'url')` 表示类型为 `Image`，并且只获取 `url` 属性\n\n### 从文件加载\n\n`Command` 支持读取 `json` 或 `yaml` 文件来加载命令：\n\n```yml title=\"book.yml\"\ncommand: book\nhelp: 测试\noptions:\n  - name: writer\n    opt: \"-w <id:int>\"\n  - name: writer\n    opt: \"--anonymous\"\n    default:\n      id: 1\nusage: book [-w <id:int> | --anonymous]\nshortcuts:\n  - key: 测试\n    args: [\"--anonymous\"]\nactions:\n  - params: [\"options\"]\n    code: |\n      return str(options)\n```\n\n```python title=\"加载\"\nfrom nonebot_plugin_alconna import command_from_yaml\n\nbook = command_from_yaml(\"book.yml\")\n```\n"
  },
  {
    "path": "website/docs/best-practice/alconna/uniseg/README.md",
    "content": "# 通用消息组件\n\n`uniseg` 模块属于 `nonebot-plugin-alconna` 的子插件。\n\n通用消息组件内容较多，故分为了一个示例以及数个专题。\n\n## 示例\n\n### 导入\n\n一般情况下，你只需要从 `nonebot_plugin_alconna.uniseg` 中导入 `UniMessage` 即可:\n\n```python\nfrom nonebot_plugin_alconna.uniseg import UniMessage\n```\n\n### 构建\n\n你可以通过 `UniMessage` 上的快捷方法来链式构造消息:\n\n```python\nmessage = (\n    UniMessage.text(\"hello world\")\n    .at(\"1234567890\")\n    .image(url=\"https://example.com/image.png\")\n)\n```\n\n也可以通过导入通用消息段来构建消息:\n\n```python\nfrom nonebot_plugin_alconna import Text, At, Image, UniMessage\n\nmessage = UniMessage(\n    [\n        Text(\"hello world\"),\n        At(\"user\", \"1234567890\"),\n        Image(url=\"https://example.com/image.png\"),\n    ]\n)\n```\n\n更深入一点，比如你想要发送一条包含多个按钮的消息，你可以这样做:\n\n```python\nfrom nonebot_plugin_alconna import Button, UniMessage\n\nmessage = (\n    UniMessage.text(\"hello world\")\n    .keyboard(\n        Button(\"link1\", url=\"https://example.com/1\"),\n        Button(\"link2\", url=\"https://example.com/2\"),\n        Button(\"link3\", url=\"https://example.com/3\"),\n        row=3,\n    )\n)\n```\n\n### 发送\n\n你可以通过 `.send` 方法来发送消息:\n\n```python\n@matcher.handle()\nasync def _():\n    message = UniMessage.text(\"hello world\").image(url=\"https://example.com/image.png\")\n    await message.send()\n    # 类似于 `matcher.finish`\n    await message.finish()\n```\n\n你可以通过参数来让消息 @ 发送者:\n\n```python\n@matcher.handle()\nasync def _():\n    message = UniMessage.text(\"hello world\").image(url=\"https://example.com/image.png\")\n    await message.send(at_sender=True)\n```\n\n或者回复消息:\n\n```python\n@matcher.handle()\nasync def _():\n    message = UniMessage.text(\"hello world\").image(url=\"https://example.com/image.png\")\n    await message.send(reply_to=True)\n```\n\n### 撤回，编辑，表态\n\n你可以通过 `message_recall`, `message_edit` 和 `message_reaction` 方法来撤回，编辑和表态消息事件。\n\n```python\nfrom nonebot_plugin_alconna import message_recall, message_edit, message_reaction\n\n@matcher.handle()\nasync def _():\n    await message_edit(UniMessage.text(\"hello world\"))\n    await message_reaction(\"👍\")\n    await message_recall()\n```\n\n你也可以对你自己发送的消息进行撤回，编辑和表态:\n\n```python\n@matcher.handle()\nasync def _():\n    message = UniMessage.text(\"hello world\").image(url=\"https://example.com/image.png\")\n    receipt = await message.send()\n    await receipt.edit(UniMessage.text(\"hello world!\"))\n    await receipt.reaction(\"👍\")\n    await receipt.recall(delay=5)  # 5秒后撤回\n```\n\n### 处理消息\n\n通过依赖注入，你可以在事件处理器中获取通用消息:\n\n```python\nfrom nonebot_plugin_alconna import UniMsg\n\n@matcher.handle()\nasync def _(msg: UniMsg):\n    ...\n```\n\n然后你可以通过 `UniMessage` 的方法来处理消息.\n\n例如，你想知道消息中是否包含图片，你可以这样做:\n\n```python\nans1 = Image in message\nans2 = message.has(Image)\nans3 = message.only(Image)\n```\n\n或者，提取所有的图片：\n\n```python\nimgs_1 = message[Image]\nimgs_2 = message.get(Image)\nimgs_3 = message.include(Image)\nimgs_4 = message.select(Image)\nimgs_5 = message.filter(lambda x: x.type == \"image\")\nimgs_6 = message.tranform({\"image\": True})\n```\n\n而后，如果你想提取出所有的图片链接，你可以这样做:\n\n```python\nurls = imgs.map(lambda x: x.url)\n```\n\n如果你想知道消息是否符合某个前缀，你可以这样做:\n\n```python\n@matcher.handle()\nasync def _(msg: UniMsg):\n    if msg.startswith(\"hello\"):\n        await matcher.finish(\"hello world\")\n    else:\n        await matcher.finish(\"not hello world\")\n```\n\n或者你想接着去除掉前缀:\n\n```python\n@matcher.handle()\nasync def _(msg: UniMsg):\n    if msg.startswith(\"hello\"):\n        msg = msg.removeprefix(\"hello\")\n        await matcher.finish(msg)\n    else:\n        await matcher.finish(\"not hello world\")\n```\n\n### 持久化\n\n假设你在编写一个词库查询插件，你可以通过 `UniMessage.dump` 方法来将消息序列化为 JSON 格式:\n\n```python\nfrom nonebot_plugin_alconna import UniMsg\n\n@matcher.handle()\nasync def _(msg: UniMsg):\n    data: list[dict] = msg.dump()\n    # 你可以将 data 存储到数据库或者 JSON 文件中\n```\n\n而后你可以通过 `UniMessage.load` 方法来将 JSON 格式的消息反序列化为 `UniMessage` 对象:\n\n```python\nfrom nonebot_plugin_alconna import UniMessage\n\n@matcher.handle()\nasync def _():\n    data = [\n        {\"type\": \"text\", \"text\": \"hello world\"},\n        {\"type\": \"image\", \"url\": \"https://example.com/image.png\"},\n    ]\n    message = UniMessage.load(data)\n```\n"
  },
  {
    "path": "website/docs/best-practice/alconna/uniseg/_category_.json",
    "content": "{\n  \"label\": \"通用消息组件\",\n  \"position\": 5\n}\n"
  },
  {
    "path": "website/docs/best-practice/alconna/uniseg/message.mdx",
    "content": "---\nsidebar_position: 3\ndescription: 消息序列\n---\n\nimport Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\n\n# 通用消息序列\n\n`uniseg` 提供了一个类似于 `Message` 的 `UniMessage` 类型，其元素为[通用消息段](./segment.md)。\n\n你可以用如下方式获取 `UniMessage`：\n\n<Tabs groupId=\"get_unimsg\">\n<TabItem value=\"depend\" label=\"使用依赖注入\">\n\n通过提供的 `UniversalMessage` 或基于 [`Annotated` 支持](https://github.com/nonebot/nonebot2/pull/1832)的 `UniMsg` 依赖注入器来获取 `UniMessage`。\n\n```python\nfrom nonebot_plugin_alconna.uniseg import UniMsg, At, Text\n\n\nmatcher = on_xxx(...)\n\n@matcher.handle()\nasync def _(msg: UniMsg):\n    text = msg[Text, 0]\n    print(text.text)\n    if msg.has(At):\n        ats = msg.get(At)\n        print(ats)\n    ...\n```\n\n</TabItem>\n<TabItem value=\"method\" label=\"使用 UniMessage.generate\">\n\n注意，`generate` 方法在响应器以外的地方如果不传入 `event` 与 `bot` 则无法处理 reply。\n\n```python\nfrom nonebot import Message, EventMessage\nfrom nonebot_plugin_alconna.uniseg import UniMessage\n\n\nmatcher = on_xxx(...)\n\n@matcher.handle()\nasync def _(message: Message = EventMessage()):\n    msg = await UniMessage.generate(message=message)\n    msg1 = UniMessage.generate_without_reply(message=message)\n```\n\n</TabItem>\n</Tabs>\n\n## 发送消息\n\n你还可以通过 `UniMessage` 的 `export` 与 `send` 方法来**跨平台发送消息**。\n\n`UniMessage.export` 会通过传入的 `bot: Bot` 参数，或上下文中的 `Bot` 对象读取适配器信息，并使用对应的生成方法把通用消息转为适配器对应的消息序列：\n\n```python\nfrom nonebot import Bot, on_command\nfrom nonebot_plugin_alconna.uniseg import Image, UniMessage\n\n\ntest = on_command(\"test\")\n\n@test.handle()\nasync def handle_test():\n    await test.send(await UniMessage(Image(path=\"path/to/img\")).export())\n```\n\n除此之外 `UniMessage.send`, `.finish` 方法基于 `UniMessage.export` 并调用各适配器下的发送消息方法，返回一个 `Receipt` 对象，用于修改/撤回/表态消息：\n\n```python\nfrom nonebot import Bot, on_command\nfrom nonebot_plugin_alconna.uniseg import UniMessage\n\n\ntest = on_command(\"test\")\n\n@test.handle()\nasync def handle():\n    receipt = await UniMessage.text(\"hello!\").send(at_sender=True, reply_to=True)\n    await receipt.recall(delay=1)\n```\n\n`UniMessage.send` 的定义如下：\n\n```python\nasync def send(\n    self,\n    target: Event | Target | None = None,\n    bot: Bot | None = None,\n    fallback: bool | FallbackStrategy = FallbackStrategy.rollback,\n    at_sender: str | bool = False,\n    reply_to: str | bool | Reply | None = False,\n    **kwargs: Any,\n) -> Receipt:\n    ...\n```\n\n- `target`: 发送目标，支持事件和[发送对象](./utils.mdx#发送对象)，不传入时会尝试从响应器上下文中获取。\n- `bot`: 发送消息使用的 Bot 对象，若不传入则会尝试从响应器上下文中获取。\n- `fallback`: [回退策略](#回退策略)。\n- `at_sender`: 是否提醒发送者，默认为 `False`。当类型为 `str` 时，表示指定用户的 id。\n- `reply_to`: 是否回复消息，默认为 `False`。\n  - `str` 表示消息 id。\n  - `bool` 表示是否回复当前消息。此时 `target` 不能是[发送对象](./utils.mdx#发送对象)。\n  - `Reply` 表示直接使用回复元素。\n- `**kwargs`: 各 `Bot.send` 的特定参数。\n\n而在 `AlconnaMatcher` 下，`got`, `send`, `reject` 等可以发送消息的方法皆支持使用 `UniMessage`，不需要手动调用 export 方法：\n\n```python\nfrom arclet.alconna import Alconna, Args\nfrom nonebot_plugin_alconna import Match, AlconnaMatcher, on_alconna\nfrom nonebot_plugin_alconna.uniseg import At,  UniMessage\n\n\ntest_cmd = on_alconna(Alconna(\"test\", Args[\"target?\", At]))\n\n@test_cmd.handle()\nasync def tt_h(matcher: AlconnaMatcher, target: Match[At]):\n    if target.available:\n        matcher.set_path_arg(\"target\", target.result)\n\n@test_cmd.got_path(\"target\", prompt=\"请输入目标\")\nasync def tt(target: At):\n    await test_cmd.send(UniMessage([target, \"\\ndone.\"]))\n```\n\n### 回退策略\n\n`send` 方法的 `fallback` 参数用于指定回退策略（即当前适配器不支持的消息段如何处理）：\n\n- `FallbackStrategy.ignore`: 忽略未转换的消息段\n- `FallbackStrategy.to_text`: 将未转换的消息段转为文本元素\n- `FallbackStrategy.rollback`: 从未转换消息段的子元素中提取可能的可发送消息段\n- `FallbackStrategy.forbid`: 抛出异常\n- `FallbackStrategy.auto`: 插件自动选择策略\n\n另外 `fallback` 传入 `bool` 时，`True` 等价于 `FallbackStrategy.auto`，`False` 等价于 `FallbackStrategy.forbid`。\n\n### 主动发送消息\n\n`UniMessage.send` 也可以用于主动发送消息：\n\n```python\nfrom nonebot_plugin_alconna.uniseg import UniMessage, Target, SupportScope\nfrom nonebot import get_driver\n\n\ndriver = get_driver()\n\n@driver.on_startup\nasync def on_startup():\n    target = Target(\"xxxx\", scope=SupportScope.qq_client)\n    await UniMessage(\"Hello!\").send(target=target)\n```\n\n:::caution\n\n在响应器以外的地方，除非启用了 `alconna_apply_fetch_targets` 配置项，否则 `bot` 参数必须手动传入。\n\n:::\n\n### Receipt 对象\n\n`send` 方法返回的 `Receipt` 对象可以用于修改/撤回/表态消息：\n\n```python\nasync def handle():\n    receipt = await UniMessage.text(\"hello!\").send(at_sender=True, reply_to=True)\n    await receipt.recall(delay=1)\n    recept1 = await UniMessage.text(\"hello!\").send(at_sender=True, reply_to=True)\n    await recept1.edit(\"world!\")\n```\n\n`Receipt` 对象拥有以下方法：\n\n- `recallable`: 表明是否可以撤回\n- `recall`: 撤回消息\n- `editable`: 表明是否可以修改\n- `edit`: 修改消息\n- `reactionable`: 表明是否可以表态\n- `reaction`: 表态消息\n- `get_reply`: 生成对已经发送的消息的回复元素\n- `send`, `finish`: 发送消息\n- `reply`: 回复已经发送的消息\n\n## 构造\n\n如同 `Message`, `UniMessage` 可以传入单个字符串/消息段，或可迭代的字符串/消息段：\n\n```python\nfrom nonebot_plugin_alconna.uniseg import UniMessage, At\n\n\nmsg = UniMessage(\"Hello\")\nmsg1 = UniMessage(At(\"user\", \"124\"))\nmsg2 = UniMessage([\"Hello\", At(\"user\", \"124\")])\n```\n\n`UniMessage` 上同时存在便捷方法，令其可以链式地添加消息段：\n\n```python\nfrom nonebot_plugin_alconna.uniseg import UniMessage, At, Image\n\n\nmsg = UniMessage.text(\"Hello\").at(\"124\").image(path=\"/path/to/img\")\nassert msg == UniMessage(\n    [\"Hello\", At(\"user\", \"124\"), Image(path=\"/path/to/img\")]\n)\n```\n\n### 使用消息模板\n\n`UniMessage.template` 同样类似于 `Message.template`，可以用于格式化消息，大体用法参考 [消息模板](../../../tutorial/message#使用消息模板)。\n\n这里额外说明 `UniMessage.template` 的拓展控制符\n\n相比 `Message`，UniMessage 对于 `{:XXX}` 做了另一类拓展。其能够识别例如 At(xxx, yyy) 或 Emoji(aaa, bbb)的字符串并执行\n\n以 At(...) 为例：\n\n```python title=使用通用消息段的拓展控制符\n>>> from nonebot_plugin_alconna.uniseg import UniMessage\n>>>  UniMessage.template(\"{:At(user, target)}\").format(target=\"123\")\nUniMessage(At(\"user\", \"123\"))\n>>> UniMessage.template(\"{:At(type=user, target=id)}\").format(id=\"123\")\nUniMessage(At(\"user\", \"123\"))\n>>> UniMessage.template(\"{:At(type=user, target=123)}\").format()\nUniMessage(At(\"user\", \"123\"))\n```\n\n而在 `AlconnaMatcher` 中，`{:XXX}` 更进一步地提供了获取 `event` 和 `bot` 中的属性的功能：\n\n```python title=在AlconnaMatcher中使用通用消息段的拓展控制符\nfrom arclet.alconna import Alconna, Args\nfrom nonebot_plugin_alconna import At, Match, UniMessage, AlconnaMatcher, on_alconna\n\n\ntest_cmd = on_alconna(Alconna(\"test\", Args[\"target?\", At]))\n\n@test_cmd.handle()\nasync def tt_h(matcher: AlconnaMatcher, target: Match[At]):\n    if target.available:\n        matcher.set_path_arg(\"target\", target.result)\n\n@test_cmd.got_path(\n    \"target\",\n    prompt=UniMessage.template(\"{:At(user, $event.get_user_id())} 请确认目标\")\n)\nasync def tt():\n    await test_cmd.send(\n      UniMessage.template(\"{:At(user, $event.get_user_id())} 已确认目标为 {target}\")\n    )\n```\n\n另外也有 `$message_id` 与 `$target` 两个特殊值。\n\n:::tip\n\n注意到上述代码中的 `{target}` 了吗？\n\n在 `AlconnaMatcher` 中，`UniMessage.template` 的格式化方法会自动将 `Arparma.all_matched_args`、 `state` 中的变量传入到 `format` 方法中，因此你可以直接使用上述变量。\n\n:::\n\n### 拼接消息\n\n`str`、`UniMessage`、`Segment` 对象之间可以直接相加，相加均会返回一个新的 `UniMessage` 对象：\n\n```python\n# 消息序列与消息段相加\nUniMessage(\"text\") + Text(\"text\")\n# 消息序列与字符串相加\nUniMessage([Text(\"text\")]) + \"text\"\n# 消息序列与消息序列相加\nUniMessage(\"text\") + UniMessage([Text(\"text\")])\n# 字符串与消息序列相加\n\"text\" + UniMessage([Text(\"text\")])\n# 消息段与消息段相加\nText(\"text\") + Text(\"text\")\n# 消息段与字符串相加\nText(\"text\") + \"text\"\n# 消息段与消息序列相加\nText(\"text\") + UniMessage([Text(\"text\")])\n# 字符串与消息段相加\n\"text\" + Text(\"text\")\n```\n\n如果需要在当前消息序列后直接拼接新的消息段，可以使用 `Message.append`、`Message.extend` 方法，或者使用自加：\n\n```python\nmsg = UniMessage([Text(\"text\")])\n# 自加\nmsg += \"text\"\nmsg += Text(\"text\")\nmsg += UniMessage([Text(\"text\")])\n# 附加\nmsg.append(Text(\"text\"))\n# 扩展\nmsg.extend([Text(\"text\")])\n```\n\n## 操作\n\n### 检查消息段\n\n我们可以通过 `in` 运算符或消息序列的 `has` 方法来：\n\n```python\n# 是否存在消息段\nAt(\"user\", \"1234\") in message\n# 是否存在指定类型的消息段\nAt in message\n```\n\n我们还可以使用 `only` 方法来检查消息中是否仅包含指定的消息段：\n\n```python\n# 是否都为 \"test\"\nmessage.only(\"test\")\n# 是否仅包含指定类型的消息段\nmessage.only(Text)\n```\n\n### 获取消息纯文本\n\n类似于 `Message.extract_plain_text()`，用于获取通用消息的纯文本：\n\n```python\n# 提取消息纯文本字符串\nassert UniMessage(\n    [At(\"user\", \"1234\"), \"text\"]\n).extract_plain_text() == \"text\"\n```\n\n### 遍历\n\n通用消息序列继承自 `List[Segment]` ，因此可以使用 `for` 循环遍历消息段：\n\n```python\nfor segment in message:  # type: Segment\n    ...\n```\n\n### 过滤、索引与切片\n\n消息序列对列表的索引与切片进行了增强，在原有列表 `int` 索引与 `slice` 切片的基础上，支持 `type` 过滤索引与切片：\n\n```python\nmessage = UniMessage(\n    [\n        Reply(...),\n        \"text1\",\n        At(\"user\", \"1234\"),\n        \"text2\"\n    ]\n)\n# 索引\nmessage[0] == Reply(...)\n# 切片\nmessage[0:2] == UniMessage([Reply(...), Text(\"text1\")])\n# 类型过滤\nmessage[At] == Message([At(\"user\", \"1234\")])\n# 类型索引\nmessage[At, 0] == At(\"user\", \"1234\")\n# 类型切片\nmessage[Text, 0:2] == UniMessage([Text(\"text1\"), Text(\"text2\")])\n```\n\n我们也可以通过消息序列的 `include`、`exclude` 方法进行类型过滤：\n\n```python\nmessage.include(Text, At)\nmessage.exclude(Reply)\n```\n\n或者使用 `filter` 方法：\n\n```python\nmessage.filter(lambda x: isinstance(x, At) and x.flag == \"user\")  # 仅保留 At(\"user\", xxx) 的消息段\n```\n\n同样的，消息序列对列表的 `index`、`count` 方法也进行了增强，可以用于索引指定类型的消息段：\n\n```python\n# 指定类型首个消息段索引\nmessage.index(Text) == 1\n# 指定类型消息段数量\nmessage.count(Text) == 2\n```\n\n此外，消息序列添加了一个 `get` 方法，可以用于获取指定类型指定个数的消息段：\n\n```python\n# 获取指定类型指定个数的消息段\nmessage.get(Text, 1) == UniMessage([Text(\"test1\")])\n```\n\n### 嵌套提取\n\n消息序列的 `select` 方法可以递归地从消息中选择指定类型的消息段：\n\n```python\nmessage = UniMessage(\n    [\n        Text(\"text1\"),\n        Image(url=\"url1\")(\n            Text(\"text2\"),\n        )\n    ]\n)\nassert message.select(Text) == UniMessage(\n    [\n        Text(\"text1\"),\n        Text(\"text2\")\n    ]\n)\n```\n\n### 转换\n\n消息序列的 `map` 方法可以简单地将消息段转换为指定类型的数据：\n\n```python\n# 转换消息段为另一类型的消息段，此时返回结果仍是 UniMessage\nmessage.map(lambda x: Text(x.target))  # 转换为 Text 消息段\n# 转换消息段为另一类型的数据，此时返回结果为 list[T]\nmessage.map(lambda x: x.target)  # 转换为 list[str]\n```\n\n在此之上，消息序列还提供了 `transform` 和 `transform_async` 方法，允许你传入转换规则，将消息段转换为另一类型的消息段，并返回一个新的消息序列：\n\n```python\nrule = {\n    \"text\": True,\n    \"at\": lambda attrs, children: Text(attrs[\"target\"])\n}\nmessage.transform(rule)\n```\n\n转换规则的类型一般为 `dict[str, Transformer]`，以消息元素类型的名称为键，定义方式如下：\n\n```typescript\ntype Fragment = Segment | Segment[];\ntype Render<T> = (attrs: dict, children: Segment[]) => T;\ntype Transformer = boolean | Fragment | Render<boolean | Fragment>;\n```\n\n### 字符串操作\n\n类似于 `str`，消息序列可以通过如下方法来操作消息内的文本部分：\n\n- `split`,\n- `replace`,\n- `startwith`, `endswith`,\n- `removeprefix`, `removesuffix`,\n- `strip`, `lstrip`, `rstrip`,\n\n```python\nmsg = UniMessage.text(\"foo bar\").at(\"1234\").text(\"baz qux\")\n# 分割，返回分割结果，类型为 list[UniMessage]\nparts = msg.split(\" \")\n# 替换，返回替换结果，类型为 UniMessage。新文本可以用 str 或 Text 来替换\nnew_msg = msg.replace(\"ba\", \"baaa\")\n# 前缀/后缀检查\nmsg.startswith(\"foo\")  # True\nmsg.endswith(\"qux\")  # True\n# 去除前缀/后缀\nmsg1 = msg.removeprefix(\"foo\")  # UniMessage([Text(\" bar\"), At(\"user\", \"1234\"), Text(\"baz qux\")])\nmsg2 = msg.removesuffix(\"qux\")  # UniMessage([Text(\"foo bar\"), At(\"user\", \"1234\"), Text(\"baz \")])\n# 去除空格\nmsg1 = msg1.lstrip()  # UniMessage([Text(\"bar\"), At(\"user\", \"1234\"), Text(\"baz qux\")])\nmsg2 = msg2.rstrip()  # UniMessage([Text(\"foo bar\"), At(\"user\", \"1234\"), Text(\"baz\")])\n```\n\n## 持久化\n\n特别的，`UniMessage` 还支持消息持久化，具体来说为 `dump` 与 `load` 方法：\n\n```python\nmsg = UniMessage.text(\"Hello\").image(url=\"url\")\ndata = msg.dump()  # [{\"type\": \"text\", \"text\": \"Hello\"}, {\"type\": \"image\", \"url\": \"url\"}]\n\nassert UniMessage.load(data) == msg\n```\n\n### dump\n\n`dump` 方法的定义如下：\n\n```python\ndef dump(self, media_save_dir: str | Path | bool | None = None, json: bool = False) -> str | list[dict[str, Any]]: ...\n```\n\n其中，`media_save_dir` 用于指定持久化的媒体文件存储目录:\n\n- 若 `media_save_dir` 为 str 或 Path，则会将媒体文件保存到指定目录下。\n- 若 `media_save_dir` 为 False，则不会保存媒体文件。\n- 若 `media_save_dir` 为 True，则会将文件数据转为 base64 编码。\n- 若不指定 `media_save_dir`，则会尝试导入 [`nonebot_plugin_localstore`](../../data-storing.md) 并使用其提供的路径。否则 (即 `localstore` 未安装)，将会尝试使用当前工作目录。\n\n### load\n\n`load` 方法的定义如下：\n\n```python\n@classmethod\ndef load(cls, data: str | list[dict[str, Any]]) -> UniMessage: ...\n```\n\n其中 `data` 应符合 JSON 格式。\n"
  },
  {
    "path": "website/docs/best-practice/alconna/uniseg/segment.md",
    "content": "---\nsidebar_position: 2\ndescription: 消息段\n---\n\n# 通用消息段\n\n通用消息段是对各适配器中的消息段的抽象总结。其可用于 Alconna 命令的参数定义，也可用于消息的构建和解析。\n\n```python\nfrom nonebot_plugin_alconna import Alconna, Args, Image, on_alconna\n\nmeme = on_alconna(Alconna(\"make_meme\", Args[\"name\", str][\"img\", Image]))\n\n@meme.handle()\nasync def _(img: Image):\n    ...\n```\n\n## 模型定义\n\n> **注意**: 本节的内容经过简化。实际情况以源码为准。\n\n```python\nclass Segment:\n    \"\"\"基类标注\"\"\"\n    @property\n    def type(self) -> str: ...\n    @property\n    def data(self) -> [str, Any]: ...\n    @property\n    def children(self) -> list[\"Segment\"]: ...\n\nclass Text(Segment):\n    \"\"\"Text对象, 表示一类文本元素\"\"\"\n    text: str\n    styles: dict[tuple[int, int], list[str]]\n\n    def cover(self, text: str): ...\n    def mark(self, start: Optional[int] = None, end: Optional[int] = None, *styles: str): ...\n\nclass At(Segment):\n    \"\"\"At对象, 表示一类提醒某用户的元素\"\"\"\n    flag: Literal[\"user\", \"role\", \"channel\"]\n    target: str\n    display: Optional[str]\n\nclass AtAll(Segment):\n    \"\"\"AtAll对象, 表示一类提醒所有人的元素\"\"\"\n    here: bool\n\nclass Emoji(Segment):\n    \"\"\"Emoji对象, 表示一类表情元素\"\"\"\n    id: str\n    name: Optional[str]\n\nclass Media(Segment):\n    id: Optional[str]\n    url: Optional[str]\n    path: Optional[Union[str, Path]]\n    raw: Optional[Union[bytes, BytesIO]]\n    mimetype: Optional[str]\n    name: str\n\n    to_url: ClassVar[Optional[MediaToUrl]]\n\nclass Image(Media):\n    \"\"\"Image对象, 表示一类图片元素\"\"\"\n    width: Optional[int]\n    height: Optional[int]\n\nclass Audio(Media):\n    \"\"\"Audio对象, 表示一类音频元素\"\"\"\n    duration: Optional[float]\n\nclass Voice(Media):\n    \"\"\"Voice对象, 表示一类语音元素\"\"\"\n    duration: Optional[float]\n\nclass Video(Media):\n    \"\"\"Video对象, 表示一类视频元素\"\"\"\n    thumbnail: Optional[Image]\n    duration: Optional[float]\n\nclass File(Media):\n    \"\"\"File对象, 表示一类文件元素\"\"\"\n\nclass Reply(Segment):\n    \"\"\"Reply对象，表示一类回复消息\"\"\"\n    id: str\n    \"\"\"此处不一定是消息ID，可能是其他ID，如消息序号等\"\"\"\n    msg: Optional[Union[Message, str]]\n    origin: Optional[Any]\n\nclass Reference(Segment):\n    \"\"\"Reference对象，表示一类引用消息。转发消息 (Forward) 也属于此类\"\"\"\n    id: Optional[str]\n    \"\"\"此处不一定是消息ID，可能是其他ID，如消息序号等\"\"\"\n    children: List[Union[RefNode, CustomNode]]\n\nclass Hyper(Segment):\n    \"\"\"Hyper对象，表示一类超级消息。如卡片消息、ark消息、小程序等\"\"\"\n    format: Literal[\"xml\", \"json\"]\n    raw: Optional[str]\n    content: Optional[Union[dict, list]]\n\nclass Reference(Segment):\n    \"\"\"Reference对象，表示一类引用消息。转发消息 (Forward) 也属于此类\"\"\"\n    id: Optional[str]\n    nodes: Sequence[Union[RefNode, CustomNode]]\n\nclass Button(Segment):\n    \"\"\"Button对象，表示一类按钮消息\"\"\"\n    flag: Literal[\"action\", \"link\", \"input\", \"enter\"]\n    \"\"\"\n    - 点击 action 类型的按钮时会触发一个关于 按钮回调 事件，该事件的 button 资源会包含上述 id\n    - 点击 link 类型的按钮时会打开一个链接或者小程序，该链接的地址为 `url`\n    - 点击 input 类型的按钮时会在用户的输入框中填充 `text`\n    - 点击 enter 类型的按钮时会直接发送 `text`\n    \"\"\"\n    label: Union[str, Text]\n    \"\"\"按钮上的文字\"\"\"\n    clicked_label: Optional[str]\n    \"\"\"点击后按钮上的文字\"\"\"\n    id: Optional[str]\n    url: Optional[str]\n    text: Optional[str]\n    style: Optional[str]\n    \"\"\"\n    仅建议使用下列值：primary, secondary, success, warning, danger, info, link, grey, blue\n\n    此处规定 `grey` 与 `secondary` 等同, `blue` 与 `primary` 等同\n    \"\"\"\n    permission: Union[Literal[\"admin\", \"all\"], list[At]] = \"all\"\n    \"\"\"\n    - admin: 仅管理者可操作\n    - all: 所有人可操作\n    - list[At]: 指定用户/身份组可操作\n    \"\"\"\n\nclass Keyboard(Segment):\n    \"\"\"Keyboard对象，表示一行按钮元素\"\"\"\n    id: Optional[str]\n    \"\"\"此处一般用来表示模板id，特殊情况下可能表示例如 bot_appid 等\"\"\"\n    buttons: Optional[list[Button]]\n    row: Optional[int]\n    \"\"\"当消息中只写有一个 Keyboard 时可根据此参数约定按钮组的列数\"\"\"\n\nclass Other(Segment):\n    \"\"\"其他 Segment\"\"\"\n    origin: MessageSegment\n\nclass I18n(Segment):\n    \"\"\"特殊的 Segment，用于 i18n 消息\"\"\"\n    item_or_scope: Union[LangItem, str]\n    type_: Optional[str] = None\n\n    def tp(self) -> UniMessageTemplate: ...\n```\n\n:::tip\n\n或许你注意到了 `Segment` 上有一个 `children` 属性。\n\n这是因为在 [`Satori`](https://satori.js.org/zh-CN/) 协议的规定下，一类元素可以用其子元素来代表一类兼容性消息\n（例如，qq 的商场表情在某些平台上可以用图片代替）。\n\n为此，本插件提供了 `select` 方法来表达 \"命令中获取子元素\" 的方法：\n\n```python\nfrom nonebot_plugin_alconna import Args, Image, Alconna, select\nfrom nonebot_plugin_alconna.builtins.uniseg.market_face import MarketFace\n\n# 表示这个指令需要的图片会在目标元素下进行搜索，将所有符合 Image 的元素选出来并将第一个作为结果\nalc1 = Alconna(\"make_meme\", Args[\"name\", str][\"img\", select(Image).first])  # 也可以使用 select(Image).nth(0)\n\n# 表示这个指令需要的图片要么直接是 Image 要么是在 MarketFace 元素内的 Image\nalc2 = Alconna(\"make_meme\", Args[\"name\", str][\"img\", [Image, select(Image).from_(MarketFace)]])\n```\n\n也可以参考通用消息的 [`嵌套提取`](./message.mdx#嵌套提取)\n\n:::\n\n## 自定义消息段\n\n`uniseg` 提供了部分方法来允许用户自定义 Segment 的序列化和反序列化：\n\n```python\nfrom dataclasses import dataclass\n\nfrom nonebot.adapters import Bot\nfrom nonebot.adapters import MessageSegment as BaseMessageSegment\nfrom nonebot.adapters.satori import Custom, Message, MessageSegment\n\nfrom nonebot_plugin_alconna.uniseg.builder import MessageBuilder\nfrom nonebot_plugin_alconna.uniseg.exporter import MessageExporter\nfrom nonebot_plugin_alconna.uniseg import Segment, custom_handler, custom_register\n\n\n@dataclass\nclass MarketFace(Segment):\n    tabId: str\n    faceId: str\n    key: str\n\n\n@custom_register(MarketFace, \"chronocat:marketface\")\ndef mfbuild(builder: MessageBuilder, seg: BaseMessageSegment):\n    if not isinstance(seg, Custom):\n        raise ValueError(\"MarketFace can only be built from Satori Message\")\n    return MarketFace(**seg.data)(*builder.generate(seg.children))\n\n\n@custom_handler(MarketFace)\nasync def mfexport(exporter: MessageExporter, seg: MarketFace, bot: Bot, fallback: bool):\n    if exporter.get_message_type() is Message:\n        return MessageSegment(\"chronocat:marketface\", seg.data)(await exporter.export(seg.children, bot, fallback))\n\n```\n\n具体而言，你可以使用 `custom_register` 来增加一个从 MessageSegment 到 Segment 的处理方法；使用 `custom_handler` 来增加一个从 Segment 到 MessageSegment 的处理方法。\n"
  },
  {
    "path": "website/docs/best-practice/alconna/uniseg/utils.mdx",
    "content": "---\nsidebar_position: 4\ndescription: 辅助模型\n---\n\nimport Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\n\n# 辅助功能\n\n`uniseg` 模块同时提供了多种方法以通用消息操作。\n\n:::note\n\n这些方法中与 `event`, `bot` 相关的参数都会尝试从上下文中获取对象。\n\n:::\n\n## 消息事件 ID\n\n消息事件 ID 是用来标识当前消息事件的唯一 ID，通常用于回复/撤回/编辑/表态当前消息。\n\n<Tabs groupId=\"get_msgid\">\n<TabItem value=\"depend\" label=\"使用依赖注入\">\n\n通过提供的 `MessageId` 或 `MsgId` 依赖注入器来获取消息事件 id。\n\n```python\nfrom nonebot_plugin_alconna.uniseg import MsgId\n\n\nmatcher = on_xxx(...)\n\n@matcher.handle()\nasycn def _(msg_id: MsgId):\n    ...\n```\n\n</TabItem>\n<TabItem value=\"method\" label=\"使用获取函数\">\n\n```python\nfrom nonebot import Event, Bot\nfrom nonebot_plugin_alconna.uniseg import get_message_id\n\n\nmatcher = on_xxx(...)\n\n@matcher.handle()\nasycn def _(bot: Bot, event: Event):\n    msg_id: str = get_message_id(event, bot)\n\n```\n\n</TabItem>\n</Tabs>\n\n:::caution\n\n该方法获取的消息事件 ID 不推荐直接用于各适配器的 API 调用中，可能会操作失败。\n\n:::\n\n## 发送对象\n\n消息发送对象是用来描述当前消息事件的可发送对象或者主动发送消息时的目标对象，它包含了以下属性：\n\n```python\nclass Target:\n    id: str\n    \"\"\"目标id；若为群聊则为 group_id 或者 channel_id，若为私聊则为 user_id\"\"\"\n    parent_id: str\n    \"\"\"父级id；若为频道则为 guild_id，其他情况下可能为空字符串（例如 Feishu 下可作为部门 id）\"\"\"\n    channel: bool\n    \"\"\"是否为频道，仅当目标平台符合频道概念时\"\"\"\n    private: bool\n    \"\"\"是否为私聊\"\"\"\n    source: str\n    \"\"\"可能的事件id\"\"\"\n    self_id: str | None\n    \"\"\"机器人id，若为 None 则 Bot 对象会随机选择\"\"\"\n    selector: Callable[[Bot], Awaitable[bool]] | None\n    \"\"\"选择器，用于在多个 Bot 对象中选择特定 Bot\"\"\"\n    extra: dict[str, Any]\n    \"\"\"额外信息，用于适配器扩展\"\"\"\n```\n\n<Tabs groupId=\"get_target\">\n<TabItem value=\"depend\" label=\"使用依赖注入\">\n\n通过提供的 `MessageTarget` 或 `MsgTarget` 依赖注入器来获取消息发送对象。\n\n```python\nfrom nonebot_plugin_alconna.uniseg import MsgTarget\n\n\nmatcher = on_xxx(...)\n\n@matcher.handle()\nasycn def _(target: MsgTarget):\n    ...\n```\n\n</TabItem>\n<TabItem value=\"method\" label=\"使用获取函数\">\n\n```python\nfrom nonebot import Event, Bot\nfrom nonebot_plugin_alconna.uniseg import Target, get_target\n\n\nmatcher = on_xxx(...)\n\n@matcher.handle()\nasycn def _(bot: Bot, event: Event):\n    target: Target = get_target(event, bot)\n\n```\n\n</TabItem>\n</Tabs>\n\n主动构造一个发送对象时，则需要如下参数：\n\n- `id`: 目标ID；若为群聊则为 `group_id` 或者 `channel_id`，若为私聊则为 `user_id`\n- `parent_id`: 父级ID；若为频道则为 `guild_id`，其他情况下可能为空字符串（例如 Feishu 下可作为部门 id）\n- `channel`: 是否为频道，仅当目标平台符合频道概念时\n- `private`: 是否为私聊\n- `source`: 可能的事件ID\n- `self_id`: 机器人id，若为 None 则 Bot 对象会随机选择\n- `selector`: 选择器，用于在多个 Bot 对象中选择特定 Bot\n- `scope`: 平台范围，表示当前发送对象的平台类别\n- `adapter`: 适配器名称，若为 None 则需要明确指定 Bot 对象\n- `platform`: 平台名称，仅当目标适配器存在多个平台时使用\n- `extra`: 额外信息，用于适配器扩展\n\n通过 `Target` 对象，我们可以在 `UniMessage.send` 中指定发送对象：\n\n```python\nfrom nonebot_plugin_alconna.uniseg import UniMessage, MsgTarget, Target, SupportScope\n\n\nmatcher = on_xxx(...)\n\n@matcher.handle()\nasync def _(target: MsgTarget):\n    # 将消息发送给当前事件的发送者\n    await UniMessage(\"Hello!\").send(target=target)\n    # 主动发送消息给群号为 12345 的 QQ 群聊\n    target1 = Target(\"12345\", scope=SupportScope.qq_client)\n    await UniMessage(\"Hello!\").send(target=target1)\n```\n\n### 选择器\n\n一般来说，主动发送消息时，`UniMessage.send` 或 `Target.self_id` 应指定一个 `Bot` 对象。但是这样会加重开发者的负担。\n\n因此，我们提供了选择器来帮助开发者选择一个 `Bot` 对象。当然，这并非说明一定需要传入 `selector` 参数。\n\n事实上，构造 `Target` 对象时，`self_id`, `scope`, `adapter` 和 `platform` 都会参与到 `selector` 的构造中。\n\n:::tip\n\n你其实可以使用 `Target` 来帮你筛选 `Bot` 对象：\n\n```python\nasync def _():\n    target = Target(\"12345\", scope=SupportScope.qq_client)\n    bot = await target.select()\n```\n\n:::\n\n若配置了 [`alconna_apply_fetch_targets`](../config.md#alconna_apply_fetch_targets) 选项，则在启动时会主动拉取一次发送对象列表。即对于\n某一主动构造的 `Target` 对象，插件将其与拉取下来的众多发送对象进行匹配，并选择第一个符合条件的发送对象，以选择对应的 Bot 对象。\n\n## 撤回消息\n\n通过 `message_recall` 方法来撤回消息事件。\n\n```python\nfrom nonebot_plugin_alconna.uniseg import message_recall\n\nmatcher = on_xxx(...)\n\n@matcher.handle()\nasync def _(msg_id: MsgId):\n    await message_recall(msg_id)\n```\n\n`message_recall` 方法的参数如下：\n\n```python\nasync def message_recall(\n    message_id: str | None = None,\n    event: Event | None = None,\n    bot: Bot | None = None,\n    adapter: str | None = None\n): ...\n```\n\n当 `message_id` 为 `None` 时，插件会尝试从 `event` 中获取消息事件 ID。\n\n## 编辑消息\n\n通过 `message_edit` 方法来编辑消息事件。\n\n```python\nfrom nonebot_plugin_alconna.uniseg import UniMessage, message_edit\n\nmatcher = on_xxx(...)\n\n@matcher.handle()\nasync def _():\n    await message_edit(UniMessage.text(\"1234\"))\n```\n\n`message_edit` 方法的参数如下：\n\n```python\nasync def message_edit(\n    msg: UniMessage,\n    message_id: str | None = None,\n    event: Event | None = None,\n    bot: Bot | None = None,\n    adapter: str | None = None,\n): ...\n```\n\n当 `message_id` 为 `None` 时，插件会尝试从 `event` 中获取消息事件 ID。\n\n## 表态消息\n\n:::caution\n\n该方法属于实验性功能。其接口可能会在未来的版本中发生变化。\n\n:::\n\n通过 `message_reaction` 方法来表态消息事件。\n\n```python\nfrom nonebot_plugin_alconna.uniseg import message_reaction\n\nmatcher = on_xxx(...)\n\n@matcher.handle()\nasync def _():\n    await message_reaction(\"👍\")\n```\n\n`message_reaction` 方法的参数如下：\n\n```python\nasync def message_reaction(\n    reaction: str | Emoji,\n    message_id: str | None = None,\n    event: Event | None = None,\n    bot: Bot | None = None,\n    adapter: str | None = None,\n    delete: bool = False,\n): ...\n```\n\n当 `message_id` 为 `None` 时，插件会尝试从 `event` 中获取消息事件 ID。\n\n`delete` 参数表示是否删除**自己的**表态消息，默认为 `False`。\n\n## 响应规则\n\n`uniseg` 模块提供了两个响应规则：\n\n- `at_in`: 是否在消息中 @ 了指定的用户\n- `at_me`: 是否在消息中 @ 了机器人\n\n相较于 NoneBot 内置的 `to_me` 规则，`at_me` 规则只会在消息中 @ 机器人时触发。\n\n```python\nfrom nonebot_plugin_alconna.uniseg import at_me\n\nmatcher = on_xxx(..., rule=at_me())\n```\n"
  },
  {
    "path": "website/docs/best-practice/data-storing.md",
    "content": "---\nsidebar_position: 1\ndescription: 存储数据文件到本地\n---\n\n# 数据存储\n\n在使用插件的过程中，难免会需要存储一些持久化数据，例如用户的个人信息、群组的信息等。除了使用数据库等第三方存储之外，还可以使用本地文件来自行管理数据。NoneBot 提供了 `nonebot-plugin-localstore` 插件，可用于获取正确的数据存储路径并写入数据。\n\n## 安装插件\n\n在使用前请先安装 `nonebot-plugin-localstore` 插件至项目环境中，可参考[获取商店插件](../tutorial/store.mdx#安装插件)来了解并选择安装插件的方式。如：\n\n在**项目目录**下执行以下命令：\n\n```bash\nnb plugin install nonebot-plugin-localstore\n```\n\n## 使用插件\n\n`nonebot-plugin-localstore` 插件兼容 Windows、Linux 和 macOS 等操作系统，使用时无需关心操作系统的差异。同时插件提供 `nb-cli` 脚本，可以使用 `nb localstore` 命令来检查数据存储路径。\n\n在使用本插件前同样需要使用 `require` 方法进行**加载**并**导入**需要使用的方法，可参考 [跨插件访问](../advanced/requiring.md) 一节进行了解，如：\n\n```python\nfrom nonebot import require\n\nrequire(\"nonebot_plugin_localstore\")\n\nimport nonebot_plugin_localstore as store\n\n# 获取插件缓存目录\ncache_dir = store.get_plugin_cache_dir()\n# 获取插件缓存文件\ncache_file = store.get_plugin_cache_file(\"file_name\")\n# 获取插件数据目录\ndata_dir = store.get_plugin_data_dir()\n# 获取插件数据文件\ndata_file = store.get_plugin_data_file(\"file_name\")\n# 获取插件配置目录\nconfig_dir = store.get_plugin_config_dir()\n# 获取插件配置文件\nconfig_file = store.get_plugin_config_file(\"file_name\")\n```\n\n:::danger 警告\n在 Windows 和 macOS 系统下，插件的数据目录和配置目录是同一个目录，因此在使用时需要注意避免文件名冲突。\n:::\n\n插件提供的方法均返回一个 `pathlib.Path` 路径，可以参考 [pathlib 文档](https://docs.python.org/zh-cn/3/library/pathlib.html)来了解如何使用。常用的方法有：\n\n```python\nfrom pathlib import Path\n\ndata_file = store.get_plugin_data_file(\"file_name\")\n# 写入文件内容\ndata_file.write_text(\"Hello World!\")\n# 读取文件内容\ndata = data_file.read_text()\n```\n\n:::note 提示\n\n对于嵌套插件，子插件的存储目录将位于父插件存储目录下。\n\n:::\n\n## 配置项\n\n### localstore_use_cwd\n\n使用当前工作目录作为数据存储目录，以下数据目录配置项默认值将会对应变更\n\n默认值：`False`\n\n```dotenv\nLOCALSTORE_USE_CWD=true\n```\n\n### localstore_cache_dir\n\n自定义缓存目录\n\n默认值：\n\n当 `localstore_use_cwd` 为 `True` 时，缓存目录为 `<current_working_directory>/cache`，否则：\n\n- macOS: `~/Library/Caches/nonebot2`\n- Unix: `~/.cache/nonebot2` (XDG default)\n- Windows: `C:\\Users\\<username>\\AppData\\Local\\nonebot2\\Cache`\n\n```dotenv\nLOCALSTORE_CACHE_DIR=/tmp/cache\n```\n\n### localstore_data_dir\n\n自定义数据目录\n\n默认值：\n\n当 `localstore_use_cwd` 为 `True` 时，数据目录为 `<current_working_directory>/data`，否则：\n\n- macOS: `~/Library/Application Support/nonebot2`\n- Unix: `~/.local/share/nonebot2` or in $XDG_DATA_HOME, if defined\n- Win XP (not roaming): `C:\\Documents and Settings\\<username>\\Application Data\\nonebot2`\n- Win 7 (not roaming): `C:\\Users\\<username>\\AppData\\Local\\nonebot2`\n\n```dotenv\nLOCALSTORE_DATA_DIR=/tmp/data\n```\n\n### localstore_config_dir\n\n自定义配置目录\n\n默认值：\n\n当 `localstore_use_cwd` 为 `True` 时，配置目录为 `<current_working_directory>/config`，否则：\n\n- macOS: same as user_data_dir\n- Unix: `~/.config/nonebot2`\n- Win XP (roaming): `C:\\Documents and Settings\\<username>\\Local Settings\\Application Data\\nonebot2`\n- Win 7 (roaming): `C:\\Users\\<username>\\AppData\\Roaming\\nonebot2`\n\n```dotenv\nLOCALSTORE_CONFIG_DIR=/tmp/config\n```\n\n### localstore_plugin_cache_dir\n\n自定义插件缓存目录\n\n默认值：`{}`\n\n```dotenv\nLOCALSTORE_PLUGIN_CACHE_DIR='\n{\n  \"plugin_id\": \"/tmp/plugin_cache\"\n}\n'\n```\n\n### localstore_plugin_data_dir\n\n自定义插件数据目录\n\n默认值：`{}`\n\n```dotenv\nLOCALSTORE_PLUGIN_DATA_DIR='\n{\n  \"plugin_id\": \"/tmp/plugin_data\"\n}\n'\n```\n\n### localstore_plugin_config_dir\n\n自定义插件配置目录\n\n默认值：`{}`\n\n```dotenv\nLOCALSTORE_PLUGIN_CONFIG_DIR='\n{\n  \"plugin_id\": \"/tmp/plugin_config\"\n}\n'\n```\n"
  },
  {
    "path": "website/docs/best-practice/database/README.mdx",
    "content": "import TabItem from \"@theme/TabItem\";\nimport Tabs from \"@theme/Tabs\";\n\n# 数据库\n\n[`nonebot-plugin-orm`](https://github.com/nonebot/plugin-orm) 是 NoneBot 的数据库支持插件。\n本插件基于 [SQLAlchemy](https://www.sqlalchemy.org/) 和 [Alembic](https://alembic.sqlalchemy.org/)，提供了许多与 NoneBot 紧密集成的功能：\n\n- 多 Engine / Connection 支持\n- Session 管理\n- 关系模型管理、依赖注入支持\n- 数据库迁移\n\n## 安装\n\n<Tabs groupId=\"install\">\n<TabItem value=\"cli\" label=\"使用 nb-cli\">\n\n```shell\nnb plugin install nonebot-plugin-orm\n```\n\n</TabItem>\n<TabItem value=\"pip\" label=\"使用 pip\">\n\n```shell\npip install nonebot-plugin-orm\n```\n\n</TabItem>\n\n<TabItem value=\"pdm\" label=\"使用 pdm\">\n\n```shell\npdm add nonebot-plugin-orm\n```\n\n</TabItem>\n</Tabs>\n\n## 数据库驱动和后端\n\n本插件只提供了 ORM 功能，没有数据库后端，也没有直接连接数据库后端的能力。\n所以你需要另行安装数据库驱动和数据库后端，并且配置数据库连接信息。\n\n### SQLite\n\n[SQLite](https://www.sqlite.org/) 是一个轻量级的嵌入式数据库，它的数据以单文件的形式存储在本地，不需要单独的数据库后端。\nSQLite 非常适合用于开发环境和小型应用，但是不适合用于大型应用的生产环境。\n\n虽然不需要另行安装数据库后端，但你仍然需要安装数据库驱动：\n\n<Tabs groupId=\"install\">\n<TabItem value=\"pip\" label=\"使用 pip\">\n\n```shell\npip install \"nonebot-plugin-orm[sqlite]\"\n```\n\n</TabItem>\n\n<TabItem value=\"pdm\" label=\"使用 pdm\">\n\n```shell\npdm add \"nonebot-plugin-orm[sqlite]\"\n```\n\n</TabItem>\n</Tabs>\n\n默认情况下，数据库文件为 `<data path>/nonebot-plugin-orm/db.sqlite3`（数据目录由 [nonebot-plugin-localstore](../data-storing) 提供）。\n或者，你可以通过配置 `SQLALCHEMY_DATABASE_URL` 来指定数据库文件路径：\n\n```shell\nSQLALCHEMY_DATABASE_URL=sqlite+aiosqlite:///file_path\n```\n\n### PostgreSQL\n\n[PostgreSQL](https://www.postgresql.org/) 是世界上最先进的开源关系数据库之一，对各种高级且广泛应用的功能有最好的支持，是中小型应用的首选数据库。\n\n<Tabs groupId=\"install\">\n<TabItem value=\"pip\" label=\"使用 pip\">\n\n```shell\npip install nonebot-plugin-orm[postgresql]\n```\n\n</TabItem>\n\n<TabItem value=\"pdm\" label=\"使用 pdm\">\n\n```shell\npdm add nonebot-plugin-orm[postgresql]\n```\n\n</TabItem>\n</Tabs>\n\n```shell\nSQLALCHEMY_DATABASE_URL=postgresql+psycopg://user:password@host:port/dbname[?key=value&key=value...]\n```\n\n### MySQL / MariaDB\n\n[MySQL](https://www.mysql.com/) 和 [MariaDB](https://mariadb.com/) 是经典的开源关系数据库，适合用于中小型应用。\n\n<Tabs groupId=\"install\">\n<TabItem value=\"pip\" label=\"使用 pip\">\n\n```shell\npip install nonebot-plugin-orm[mysql]\n```\n\n</TabItem>\n\n<TabItem value=\"pdm\" label=\"使用 pdm\">\n\n```shell\npdm add nonebot-plugin-orm[mysql]\n```\n\n</TabItem>\n</Tabs>\n\n```shell\nSQLALCHEMY_DATABASE_URL=mysql+aiomysql://user:password@host:port/dbname[?key=value&key=value...]\n```\n\n## 使用\n\n本插件提供了数据库迁移功能（此功能依赖于 [nb-cli 脚手架](../../quick-start#安装脚手架)）。\n在安装了新的插件或机器人之后，你需要执行一次数据库迁移操作，将数据库同步至与机器人一致的状态：\n\n```shell\nnb orm upgrade\n```\n\n运行完毕后，可以检查一下：\n\n```shell\nnb orm check\n```\n\n如果输出是 `没有检测到新的升级操作`，那么恭喜你，数据库已经迁移完成了，你可以启动机器人了。\n"
  },
  {
    "path": "website/docs/best-practice/database/_category_.json",
    "content": "{\n  \"label\": \"数据库\",\n  \"position\": 7\n}\n"
  },
  {
    "path": "website/docs/best-practice/database/developer/README.md",
    "content": "# 开发者指南\n\n开发者指南内容较多，故分为了一个示例以及数个专题。\n阅读（并且最好跟随实践）示例后，你将会对使用 `nonebot-plugin-orm` 开发插件有一个基本的认识。\n如果想要更深入地学习关于 [SQLAlchemy](https://www.sqlalchemy.org/) 和 [Alembic](https://alembic.sqlalchemy.org/) 的知识，或者在使用过程中遇到了问题，可以查阅专题以及其官方文档。\n\n## 示例\n\n### 模型定义\n\n首先，我们需要设计存储的数据的结构。\n例如天气插件，需要存储**什么地方 (`location`)** 的**天气是什么 (`weather`)**。\n其中，一个地方只会有一种天气，而不同地方可能有相同的天气。\n所以，我们可以设计出如下的模型：\n\n```python title=weather/__init__.py showLineNumbers\nfrom nonebot_plugin_orm import Model\nfrom sqlalchemy.orm import Mapped, mapped_column\n\n\nclass Weather(Model):\n    location: Mapped[str] = mapped_column(primary_key=True)\n    weather: Mapped[str]\n```\n\n其中，`primary_key=True` 意味着此列 (`location`) 是主键，即内容是唯一的且非空的。\n每一个模型必须有至少一个主键。\n\n我们可以用以下代码检查模型生成的数据库模式是否正确：\n\n```python\nfrom sqlalchemy.schema import CreateTable\n\nprint(CreateTable(Weather.__table__))\n```\n\n```sql\nCREATE TABLE weather_weather (\n        location VARCHAR NOT NULL,\n        weather VARCHAR NOT NULL,\n        CONSTRAINT pk_weather_weather PRIMARY KEY (location)\n)\n```\n\n可以注意到表名是 `weather_weather` 而不是 `Weather` 或者 `weather`。\n这是因为 `nonebot-plugin-orm` 会自动为模型生成一个表名，规则是：`<插件模块名>_<类名小写>`。\n\n你也可以通过指定 `__tablename__` 属性来自定义表名：\n\n```python {2}\nclass Weather(Model):\n    __tablename__ = \"weather\"\n    ...\n```\n\n```sql {1}\nCREATE TABLE weather (\n    ...\n)\n```\n\n但是，并不推荐你这么做，因为这可能会导致不同插件间的表名重复，引发冲突。\n特别是当你会发布插件时，你并不知道其他插件会不会使用相同的表名。\n\n### 首次迁移\n\n我们成功定义了模型，现在启动机器人试试吧：\n\n```shell\n$ nb run\n01-02 15:04:05 [SUCCESS] nonebot | NoneBot is initializing...\n01-02 15:04:05 [ERROR] nonebot_plugin_orm | 启动检查失败\n01-02 15:04:05 [ERROR] nonebot | Application startup failed. Exiting.\nTraceback (most recent call last):\n  ...\nclick.exceptions.UsageError: 检测到新的升级操作:\n[('add_table',\n  Table('weather', MetaData(), Column('location', String(), table=<weather>, primary_key=True, nullable=False), Column('weather', String(), table=<weather>, nullable=False), schema=None))]\n```\n\n咦，发生了什么？\n`nonebot-plugin-orm` 试图阻止我们启动机器人。\n原来是我们定义了模型，但是数据库中并没有对应的表，这会导致插件不能正常运行。\n所以，我们需要迁移数据库。\n\n首先，我们需要创建一个迁移脚本：\n\n```shell\nnb orm revision -m \"first revision\" --branch-label weather\n```\n\n其中，`-m` 参数是迁移脚本的描述，`--branch-label` 参数是迁移脚本的分支，一般为插件模块名。\n执行命令过后，出现了一个 `weather/migrations` 目录，其中有一个 `xxxxxxxxxxxx_first_revision.py` 文件：\n\n```shell {4,5}\nweather\n├── __init__.py\n├── config.py\n└── migrations\n    └── xxxxxxxxxxxx_first_revision.py\n```\n\n这就是我们创建的迁移脚本，它记录了数据库模式的变化。\n我们可以查看一下它的内容：\n\n```python title=weather/migrations/xxxxxxxxxxxx_first_revision.py {25-33,39-41} showLineNumbers\n\"\"\"first revision\n\n迁移 ID: xxxxxxxxxxxx\n父迁移:\n创建时间: 2006-01-02 15:04:05.999999\n\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom collections.abc import Sequence\n\nimport sqlalchemy as sa\nfrom alembic import op\n\nrevision: str = \"xxxxxxxxxxxx\"\ndown_revision: str | Sequence[str] | None = None\nbranch_labels: str | Sequence[str] | None = (\"weather\",)\ndepends_on: str | Sequence[str] | None = None\n\n\ndef upgrade(name: str = \"\") -> None:\n    if name:\n        return\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.create_table(\n        \"weather_weather\",\n        sa.Column(\"location\", sa.String(), nullable=False),\n        sa.Column(\"weather\", sa.String(), nullable=False),\n        sa.PrimaryKeyConstraint(\"location\", name=op.f(\"pk_weather_weather\")),\n        info={\"bind_key\": \"weather\"},\n    )\n    # ### end Alembic commands ###\n\n\ndef downgrade(name: str = \"\") -> None:\n    if name:\n        return\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_table(\"weather_weather\")\n    # ### end Alembic commands ###\n```\n\n可以注意到脚本的主体部分（其余是模版代码，请勿修改）是：\n\n```python\n# ### commands auto generated by Alembic - please adjust! ###\nop.create_table(  # CREATE TABLE\n    \"weather_weather\",  # weather_weather\n    sa.Column(\"location\", sa.String(), nullable=False),  # location VARCHAR NOT NULL,\n    sa.Column(\"weather\", sa.String(), nullable=False),  # weather VARCHAR NOT NULL,\n    sa.PrimaryKeyConstraint(\"location\", name=op.f(\"pk_weather_weather\")),  # CONSTRAINT pk_weather_weather PRIMARY KEY (location)\n    info={\"bind_key\": \"weather\"},\n)\n# ### end Alembic commands ###\n```\n\n```python\n# ### commands auto generated by Alembic - please adjust! ###\nop.drop_table(\"weather_weather\")  # DROP TABLE weather_weather;\n# ### end Alembic commands ###\n```\n\n虽然我们不是很懂这些代码的意思，但是可以注意到它们几乎与 SQL 语句 (DDL) 一一对应。\n显然，它们是用来创建和删除表的。\n\n我们还可以注意到，`upgrade()` 和 `downgrade()` 函数中的代码是**互逆**的。\n也就是说，执行一次 `upgrade()` 函数，再执行一次 `downgrade()` 函数后，数据库的模式就会回到原来的状态。\n\n这就是迁移脚本的作用：记录数据库模式的变化，以便我们在不同的环境中（例如开发环境和生产环境）**可复现地**、**可逆地**同步数据库模式，正如 git 对我们的代码做的事情那样。\n\n对了，不要忘记还有一段注释：`commands auto generated by Alembic - please adjust!`。\n它在提醒我们，这些代码是由 Alembic 自动生成的，我们应该检查它们，并且根据需要进行调整。\n\n:::caution 注意\n迁移脚本冗长且繁琐，我们一般不会手写它们，而是由 Alembic 自动生成。\n一般情况下，Alembic 足够智能，可以正确地生成迁移脚本。\n但是，在复杂或有歧义的情况下，我们可能需要手动调整迁移脚本。\n所以，**永远要检查迁移脚本，并且在开发环境中测试！**\n\n**迁移脚本中任何一处错误都足以使数据付之东流！**\n:::\n\n确定迁移脚本正确后，我们就可以执行迁移脚本，将数据库模式同步到数据库中：\n\n```shell\nnb orm upgrade\n```\n\n现在，我们可以正常启动机器人了。\n\n开发过程中，我们可能会频繁地修改模型，这意味着我们需要频繁地创建并执行迁移脚本，非常繁琐。\n实际上，此时我们不在乎数据安全，只需要数据库模式与模型定义一致即可。\n所以，我们可以关闭 `nonebot-plugin-orm` 的启动检查：\n\n```shell title=.env.dev\nALEMBIC_STARTUP_CHECK=false\n```\n\n现在，每次启动机器人时，数据库模式会自动与模型定义同步，无需手动迁移。\n\n### 会话管理\n\n我们已经成功定义了模型，并且迁移了数据库，现在可以开始使用数据库了……吗？\n并不能，因为模型只是数据结构的定义，并不能通过它操作数据（如果你曾经使用过 [Tortoise ORM](https://tortoise.github.io/)，可能会知道 `await Weather.get(location=\"上海\")` 这样的面向对象编程。\n但是 SQLAlchemy 不同，选择了命令式编程）。\n我们需要使用**会话**操作数据：\n\n```python title=weather/__init__.py {10,13} showLineNumbers\nfrom nonebot import on_command\nfrom nonebot.adapters import Message\nfrom nonebot.params import CommandArg\nfrom nonebot_plugin_orm import async_scoped_session\n\nweather = on_command(\"天气\")\n\n\n@weather.handle()\nasync def _(session: async_scoped_session, args: Message = CommandArg()):\n    location = args.extract_plain_text()\n\n    if wea := await session.get(Weather, location):\n        await weather.finish(f\"今天{location}的天气是{wea.weather}\")\n\n    await weather.finish(f\"未查询到{location}的天气\")\n```\n\n我们通过 `session: async_scoped_session` 依赖注入获得了一个会话，然后使用 `await session.get(Weather, location)` 查询数据库。\n`async_scoped_session` 是一个有作用域限制的会话，作用域为当前事件、当前事件响应器。\n会话产生的模型实例（例如此处的 `wea := await session.get(Weather, location)`）作用域与会话相同。\n\n:::caution 注意\n此处提到的“会话”指的是 ORM 会话，而非 [NoneBot 会话](../../../appendices/session-control)，两者的生命周期也是不同的（NoneBot 会话的生命周期中可能包含多个事件，不同的事件也会有不同的事件响应器）。\n具体而言，就是不要将 ORM 会话和模型实例存储在 NoneBot 会话状态中：\n\n```python {12}\nfrom nonebot.params import ArgPlainText\nfrom nonebot.typing import T_State\n\n\n@weather.got(\"location\", prompt=\"请输入地名\")\nasync def _(state: T_State, session: async_scoped_session, location: str = ArgPlainText()):\n    wea = await session.get(Weather, location)\n\n    if not wea:\n        await weather.finish(f\"未查询到{location}的天气\")\n\n    state[\"weather\"] = wea  # 不要这么做，除非你知道自己在做什么\n```\n\n当然非要这么做也不是不可以：\n\n```python {6}\n@weather.handle()\nasync def _(state: T_State, session: async_scoped_session):\n    # 通过 await session.merge(state[\"weather\"]) 获得了此 ORM 会话中的相应模型实例，\n    # 而非直接使用会话状态中的模型实例，\n    # 因为先前的 ORM 会话已经关闭了。\n    wea = await session.merge(state[\"weather\"])\n    await weather.finish(f\"今天{state['location']}的天气是{wea.weather}\")\n```\n\n:::\n\n当有数据更改时，我们需要提交事务，也要注意会话作用域问题：\n\n```python title=weather/__init__.py {12,20} showLineNumbers\nfrom nonebot.params import Depends\n\n\nasync def get_weather(\n    session: async_scoped_session, args: Message = CommandArg()\n) -> Weather:\n    location = args.extract_plain_text()\n\n    if not (wea := await session.get(Weather, location)):\n        wea = Weather(location=location, weather=\"未知\")\n        session.add(wea)\n        # await session.commit()  # 不应该在其他地方提交事务\n\n    return wea\n\n\n@weather.handle()\nasync def _(session: async_scoped_session, wea: Weather = Depends(get_weather)):\n    await weather.send(f\"今天的天气是{wea.weather}\")\n    await session.commit()  # 而应该在事件响应器结束前提交事务\n```\n\n当然我们也可以获得一个新的会话，不过此时就要手动管理会话了：\n\n```python title=weather/__init__.py {5-6} showLineNumbers\nfrom nonebot_plugin_orm import get_session\n\n\nasync def get_weather(location: str) -> str:\n    session = get_session()\n    async with session.begin():\n        wea = await session.get(Weather, location)\n\n        if not wea:\n            wea = Weather(location=location, weather=\"未知\")\n            session.add(wea)\n\n        return wea.weather\n\n\n@weather.handle()\nasync def _(args: Message = CommandArg()):\n    wea = await get_weather(args.extract_plain_text())\n    await weather.send(f\"今天的天气是{wea}\")\n```\n\n### 依赖注入\n\n在上面的示例中，我们都是通过会话获得数据的。\n不过，我们也可以通过依赖注入获得数据：\n\n```python title=weather/__init__.py {12-14} showLineNumbers\nfrom sqlalchemy import select\nfrom nonebot.params import Depends\nfrom nonebot_plugin_orm import SQLDepends\n\n\ndef extract_arg_plain_text(args: Message = CommandArg()) -> str:\n    return args.extract_plain_text()\n\n\n@weather.handle()\nasync def _(\n    wea: Weather = SQLDepends(\n        select(Weather).where(Weather.location == Depends(extract_arg_plain_text))\n    ),\n):\n    await weather.send(f\"今天的天气是{wea.weather}\")\n```\n\n其中，`SQLDepends` 是一个特殊的依赖注入，它会根据类型标注和 SQL 语句提供数据，SQL 语句中也可以有子依赖。\n\n不同的类型标注也会获得不同形式的数据：\n\n```python title=weather/__init__.py {5} showLineNumbers\nfrom collections.abc import Sequence\n\n@weather.handle()\nasync def _(\n    weas: Sequence[Weather] = SQLDepends(\n        select(Weather).where(Weather.weather == Depends(extract_arg_plain_text))\n    ),\n):\n    await weather.send(f\"今天的天气是{weas[0].weather}的城市有{'，'.join(wea.location for wea in weas)}\")\n```\n\n支持的类型标注请参见 [依赖注入](dependency)。\n\n我们也可以像 [类作为依赖](../../../advanced/dependency#类作为依赖) 那样，在类属性中声明子依赖：\n\n```python title=weather/__init__.py {5-6,10} showLineNumbers\nfrom collections.abc import Sequence\n\nclass Weather(Model):\n    location: Mapped[str] = mapped_column(primary_key=True)\n    weather: Mapped[str] = Depends(extract_arg_plain_text)\n    # weather: Annotated[Mapped[str], Depends(extract_arg_plain_text)]  # Annotated 支持\n\n\n@weather.handle()\nasync def _(weas: Sequence[Weather]):\n    await weather.send(\n        f\"今天的天气是{weas[0].weather}的城市有{'，'.join(wea.location for wea in weas)}\"\n    )\n```\n"
  },
  {
    "path": "website/docs/best-practice/database/developer/_category_.json",
    "content": "{\n  \"label\": \"开发者指南\",\n  \"position\": 3\n}\n"
  },
  {
    "path": "website/docs/best-practice/database/developer/dependency.md",
    "content": "---\nsidebar_position: 3\ndescription: 依赖注入\n---\n\n# 依赖注入\n\n`nonebot-plugin-orm` 提供了强大且灵活的依赖注入，可以方便地帮助你获取数据库会话和查询数据。\n\n## 数据库会话\n\n### AsyncSession\n\n新数据库会话，常用于有独立的数据库操作逻辑的插件。\n\n```python {13,26}\nfrom nonebot import on_message\nfrom nonebot.params import Depends\nfrom nonebot_plugin_orm import AsyncSession, Model, async_scoped_session\nfrom sqlalchemy.orm import Mapped, mapped_column\n\nmessage = on_message()\n\n\nclass Message(Model):\n    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)\n\n\nasync def get_message(session: AsyncSession) -> Message:\n    # 等价于 session = get_session()\n    async with session:\n        msg = Message()\n\n        session.add(msg)\n        await session.commit()\n        await session.refresh(msg)\n\n        return msg\n\n\n@message.handle()\nasync def _(session: async_scoped_session, msg: Message = Depends(get_message)):\n    await session.rollback()  # 无法回退 get_message() 中的更改\n    await message.send(str(msg.id))  # msg 被存储，msg.id 递增\n```\n\n### async_scoped_session\n\n数据库作用域会话，常用于事件响应器和有与响应逻辑相关的数据库操作逻辑的插件。\n\n```python {13，26}\nfrom nonebot import on_message\nfrom nonebot.params import Depends\nfrom nonebot_plugin_orm import Model, async_scoped_session\nfrom sqlalchemy.orm import Mapped, mapped_column\n\nmessage = on_message()\n\n\nclass Message(Model):\n    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)\n\n\nasync def get_message(session: async_scoped_session) -> Message:\n    # 等价于 session = get_scoped_session()\n    msg = Message()\n\n    session.add(msg)\n    await session.flush()\n    await session.refresh(msg)\n\n    return msg\n\n\n@message.handle()\nasync def _(session: async_scoped_session, msg: Message = Depends(get_message)):\n    await session.rollback()  # 可以回退 get_message() 中的更改\n    await message.send(str(msg.id))  # msg 没有被存储，msg.id 不变\n```\n\n## 查询数据\n\n### Model\n\n支持类作为依赖。\n\n```python\nfrom typing import Annotated\n\nfrom nonebot.params import Depends\nfrom nonebot_plugin_orm import Model\nfrom sqlalchemy.orm import Mapped, mapped_column\n\n\ndef get_id() -> int: ...\n\n\nclass Message(Model):\n    id: Annotated[Mapped[int], Depends(get_id)] = mapped_column(\n        primary_key=True, autoincrement=True\n    )\n\n\nasync def _(msg: Message):\n    # 等价于 msg = (\n    #     await (await session.stream(select(Message).where(Message.id == get_id())))\n    #     .scalars()\n    #     .one_or_none()\n    # )\n    ...\n```\n\n### SQLDepends\n\n参数为一个 SQL 语句，决定依赖注入的内容，SQL 语句中可以使用子依赖。\n\n```python {11-13}\nfrom nonebot.params import Depends\nfrom nonebot_plugin_orm import Model, SQLDepends\nfrom sqlalchemy import select\n\n\ndef get_id() -> int: ...\n\n\nasync def _(\n    model: Model = SQLDepends(select(Model).where(Model.id == Depends(get_id))),\n): ...\n```\n\n参数可以是任意 SQL 语句，但不建议使用 `select` 以外的语句，因为语句可能没有返回值（`returning` 除外），而且代码不清晰。\n\n### 类型标注\n\n类型标注决定依赖注入的数据结构，主要影响以下几个层面：\n\n- 迭代器（`session.execute()`）或异步迭代器（`session.stream()`）\n- 标量（`session.execute().scalars()`）或元组（`session.execute()`）\n- 一个（`session.execute().one_or_none()`，注意 `None` 时可能触发 [重载](../../../appendices/overload#重载)）或全部（`session.execute()` / `session.execute().all()`）\n- 连续（`session().execute()`）或分块（`session.execute().partitions()`）\n\n具体如下（可以使用父类型作为类型标注）：\n\n- ```python\n  async def _(rows_partitions: AsyncIterator[Sequence[Tuple[Model, ...]]]):\n      # 等价于 rows_partitions = await (await session.stream(sql).partitions())\n\n      async for partition in rows_partitions:\n          for row in partition:\n              print(row[0], row[1], ...)\n  ```\n\n- ```python\n  async def _(model_partitions: AsyncIterator[Sequence[Model]]):\n      # 等价于 model_partitions = await (await session.stream(sql).scalars().partitions())\n\n      async for partition in model_partitions:\n          for model in partition:\n              print(model)\n  ```\n\n- ```python\n  async def _(row_partitions: Iterator[Sequence[Tuple[Model, ...]]]):\n      # 等价于 row_partitions = await session.execute(sql).partitions()\n\n      for partition in rows_partitions:\n          for row in partition:\n              print(row[0], row[1], ...)\n  ```\n\n- ```python\n  async def _(model_partitions: Iterator[Sequence[Model]]):\n      # 等价于 model_partitions = await (await session.execute(sql).scalars().partitions())\n\n      for partition in model_partitions:\n          for model in partition:\n              print(model)\n  ```\n\n- ```python\n  async def _(rows: sa_async.AsyncResult[Tuple[Model, ...]]):\n      # 等价于 rows = await session.stream(sql)\n\n      async for row in rows:\n          print(row[0], row[1], ...)\n  ```\n\n- ```python\n  async def _(models: sa_async.AsyncScalarResult[Model]):\n      # 等价于 models = await session.stream(sql).scalars()\n\n      async for model in models:\n          print(model)\n  ```\n\n- ```python\n  async def _(rows: sa.Result[Tuple[Model, ...]]):\n      # 等价于 rows = await session.execute(sql)\n\n      for row in rows:\n          print(row[0], row[1], ...)\n  ```\n\n- ```python\n  async def _(models: sa.ScalarResult[Model]):\n      # 等价于 models = await session.execute(sql).scalars()\n\n      for model in models:\n          print(model)\n  ```\n\n- ```python\n  async def _(rows: Sequence[Tuple[Model, ...]]):\n      # 等价于 rows = await (await session.stream(sql).all())\n\n      for row in rows:\n            print(row[0], row[1], ...)\n  ```\n\n- ```python\n  async def _(models: Sequence[Model]):\n      # 等价于 models = await (await session.stream(sql).scalars().all())\n\n      for model in models:\n          print(model)\n  ```\n\n- ```python\n  async def _(row: Tuple[Model, ...]):\n      # 等价于 row = await (await session.stream(sql).one_or_none())\n\n      print(row[0], row[1], ...)\n  ```\n\n- ```python\n  async def _(model: Model):\n      # 等价于 model = await (await session.stream(sql).scalars().one_or_none())\n\n      print(model)\n  ```\n"
  },
  {
    "path": "website/docs/best-practice/database/developer/test.md",
    "content": "---\nsidebar_position: 2\ndescription: 测试\n---\n\n# 测试\n\n百思不如一试，测试是发现问题的最佳方式。\n\n不同的用户会有不同的配置，为了提高项目的兼容性，我们需要在不同数据库后端上测试。\n手动进行大量的、重复的测试不可靠，也不现实，因此我们推荐使用 [GitHub Actions](https://github.com/features/actions) 进行自动化测试：\n\n```yaml title=.github/workflows/test.yml {12-42,52-53} showLineNumbers\nname: Test\n\non:\n  push:\n    branches:\n      - main\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n\n    strategy:\n      matrix:\n        db:\n          - sqlite+aiosqlite:///db.sqlite3\n          - postgresql+psycopg://postgres:postgres@localhost:5432/postgres\n          - mysql+aiomysql://mysql:mysql@localhost:3306/mymysql\n\n      fail-fast: false\n\n    env:\n      SQLALCHEMY_DATABASE_URL: ${{ matrix.db }}\n\n    services:\n      postgresql:\n        image: ${{ startsWith(matrix.db, 'postgresql') && 'postgres' || '' }}\n        env:\n          POSTGRES_USER: postgres\n          POSTGRES_PASSWORD: postgres\n          POSTGRES_DB: postgres\n        ports:\n          - 5432:5432\n\n      mysql:\n        image: ${{ startsWith(matrix.db, 'mysql') && 'mysql' || '' }}\n        env:\n          MYSQL_ROOT_PASSWORD: mysql\n          MYSQL_USER: mysql\n          MYSQL_PASSWORD: mysql\n          MYSQL_DATABASE: mymysql\n        ports:\n          - 3306:3306\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - uses: actions/setup-python@v5\n\n      - name: Install dependencies\n        run: pip install -r requirements.txt\n\n      - name: Run migrations\n        run: pipx run nb-cli orm upgrade\n\n      - name: Run tests\n        run: pytest\n```\n\n如果项目还需要考虑跨平台和跨 Python 版本兼容，测试矩阵中还需要增加这两个维度。\n但是，我们没必要在所有平台和 Python 版本上运行所有数据库的测试，因为很显然，PostgreSQL 和 MySQL 这类独立的数据库后端不会受平台和 Python 影响，而且 Github Actions 的非 Linux 平台不支持运行独立服务：\n\n|             | Python 3.9 | Python 3.10 | Python 3.11 | Python 3.12                 |\n| ----------- | ---------- | ----------- | ----------- | --------------------------- |\n| **Linux**   | SQLite     | SQLite      | SQLite      | SQLite / PostgreSQL / MySQL |\n| **Windows** | SQLite     | SQLite      | SQLite      | SQLite                      |\n| **macOS**   | SQLite     | SQLite      | SQLite      | SQLite                      |\n\n```yaml title=.github/workflows/test.yml {12-24} showLineNumbers\nname: Test\n\non:\n  push:\n    branches:\n      - main\n\njobs:\n  test:\n    runs-on: ${{ matrix.os }}\n\n    strategy:\n      matrix:\n        os: [ubuntu-latest, windows-latest, macos-latest]\n        python-version: [\"3.9\", \"3.10\", \"3.11\", \"3.12\"]\n        db: [\"sqlite+aiosqlite:///db.sqlite3\"]\n\n        include:\n          - os: ubuntu-latest\n            python-version: \"3.12\"\n            db: postgresql+psycopg://postgres:postgres@localhost:5432/postgres\n          - os: ubuntu-latest\n            python-version: \"3.12\"\n            db: mysql+aiomysql://mysql:mysql@localhost:3306/mymysql\n\n      fail-fast: false\n\n    env:\n      SQLALCHEMY_DATABASE_URL: ${{ matrix.db }}\n\n    services:\n      postgresql:\n        image: ${{ startsWith(matrix.db, 'postgresql') && 'postgres' || '' }}\n        env:\n          POSTGRES_USER: postgres\n          POSTGRES_PASSWORD: postgres\n          POSTGRES_DB: postgres\n        ports:\n          - 5432:5432\n\n      mysql:\n        image: ${{ startsWith(matrix.db, 'mysql') && 'mysql' || '' }}\n        env:\n          MYSQL_ROOT_PASSWORD: mysql\n          MYSQL_USER: mysql\n          MYSQL_PASSWORD: mysql\n          MYSQL_DATABASE: mymysql\n        ports:\n          - 3306:3306\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - uses: actions/setup-python@v5\n        with:\n          python-version: ${{ matrix.python-version }}\n\n      - name: Install dependencies\n        run: pip install -r requirements.txt\n\n      - name: Run migrations\n        run: pipx run nb-cli orm upgrade\n\n      - name: Run tests\n        run: pytest\n```\n"
  },
  {
    "path": "website/docs/best-practice/database/user.md",
    "content": "---\nsidebar_position: 2\ndescription: 用户指南\n---\n\n# 用户指南\n\n`nonebot-plugin-orm` 功能强大且复杂，使用上有一定难度。\n不过，对于用户而言，只需要掌握部分功能即可。\n\n:::caution 注意\n请注意区分插件的项目名（如：`nonebot-plugin-wordcloud`）和模块名（如：`nonebot_plugin_wordcloud`）。`nonebot-plugin-orm` 中统一使用插件模块名。参见 [插件命名规范](../../developer/plugin-publishing#插件命名规范)。\n:::\n\n## 示例\n\n### 创建新机器人\n\n我们想要创建一个机器人，并安装 `nonebot-plugin-wordcloud` 插件，只需要执行以下命令：\n\n```shell\nnb init  # 初始化项目文件夹\n\npip install nonebot-plugin-orm[sqlite]  # 安装 nonebot-plugin-orm，并附带 SQLite 支持\n\nnb plugin install nonebot-plugin-wordcloud  # 安装插件\n\n# nb orm heads  # 查看有什么插件使用到了数据库（可选）\n\nnb orm upgrade  # 升级数据库\n\n# nb orm check  # 检查一下数据库模式是否与模型定义一致（可选）\n\nnb run  # 启动机器人\n```\n\n### 卸载插件\n\n我们已经安装了 `nonebot-plugin-wordcloud` 插件，但是现在想要卸载它，并且**删除它的数据**，只需要执行以下命令：\n\n```shell\nnb plugin uninstall nonebot-plugin-wordcloud  # 卸载插件\n\n# nb orm heads  # 查看有什么插件使用到了数据库。（可选）\n\nnb orm downgrade nonebot_plugin_wordcloud@base  # 降级数据库，删除数据\n\n# nb orm check  # 检查一下数据库模式是否与模型定义一致（可选）\n```\n\n## CLI\n\n接下来，让我们了解下示例中出现的 CLI 命令的含义：\n\n### heads\n\n显示所有的分支头。一般一个分支对应一个插件。\n\n```shell\nnb orm heads\n```\n\n输出格式为 `<迁移 ID> (<插件模块名>) (<头部类型>)`：\n\n```\n46327b837dd8 (nonebot_plugin_chatrecorder) (head)\n9492159f98f7 (nonebot_plugin_user) (head)\n71a72119935f (nonebot_plugin_session_orm) (effective head)\nade8cdca5470 (nonebot_plugin_wordcloud) (head)\n```\n\n### upgrade\n\n升级数据库。每次安装新的插件或更新插件版本后，都需要执行此命令。\n\n```shell\nnb orm upgrade <插件模块名>@<迁移 ID>\n```\n\n其中，`<插件模块名>@<迁移 ID>` 是可选参数。如果不指定，则会将所有分支升级到最新版本，这也是最常见的用法：\n\n```shell\nnb orm upgrade\n```\n\n### downgrade\n\n降级数据库。当需要回滚插件版本或删除插件时，可以执行此命令。\n\n```shell\nnb orm downgrade <插件模块名>@<迁移 ID>\n```\n\n其中，`<迁移 ID>` 也可以是 `base`，即回滚到初始状态。常用于卸载插件后删除其数据：\n\n```shell\nnb orm downgrade <插件模块名>@base\n```\n\n### check\n\n检查数据库模式是否与模型定义一致。机器人启动前会自动运行此命令（`ALEMBIC_STARTUP_CHECK=true` 时），并在检查失败时阻止启动。\n\n```shell\nnb orm check\n```\n\n## 配置\n\n### sqlalchemy_database_url\n\n默认数据库连接 URL。参见 [数据库驱动和后端](.#数据库驱动和后端) 和 [引擎配置 — SQLAlchemy 2.0 文档](https://docs.sqlalchemy.org/en/20/core/engines.html#database-urls)。\n\n```shell\nSQLALCHEMY_DATABASE_URL=dialect+driver://username:password@host:port/database\n```\n\n### sqlalchemy_bind\n\nbind 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) 实例的字典。\n例如，我们想要让 `nonebot-plugin-wordcloud` 插件使用一个 SQLite 数据库，并开启 [Echo 选项](https://docs.sqlalchemy.org/en/20/core/engines.html#sqlalchemy.create_engine.params.echo) 便于 debug，而其他插件使用默认的 PostgreSQL 数据库，可以这样配置：\n\n```shell\nSQLALCHEMY_BINDS='{\n    \"\": \"postgresql+psycopg://scott:tiger@localhost/mydatabase\",\n    \"nonebot_plugin_wordcloud\": {\n        \"url\": \"sqlite+aiosqlite://\",\n        \"echo\": true\n    }\n}'\n```\n\n### sqlalchemy_engine_options\n\n[`create_async_engine()`](https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html#sqlalchemy.ext.asyncio.create_async_engine) 默认参数字典。\n\n```shell\nSQLALCHEMY_ENGINE_OPTIONS='{\n    \"pool_size\": 5,\n    \"max_overflow\": 10,\n    \"pool_timeout\": 30,\n    \"pool_recycle\": 3600,\n    \"echo\": true\n}'\n```\n\n### sqlalchemy_echo\n\n开启 [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。\n\n```shell\nSQLALCHEMY_ECHO=true\n```\n\n:::caution 注意\n以上配置之间有覆盖关系，遵循特殊优先于一般的原则，具体为 [`sqlalchemy_database_url`](#sqlalchemy_database_url) > [`sqlalchemy_bind`](#sqlalchemy_bind) > [`sqlalchemy_echo`](#sqlalchemy_echo) > [`sqlalchemy_engine_options`](#sqlalchemy_engine_options)。\n但覆盖顺序并非显而易见，出于清晰考虑，请只配置必要的选项。\n:::\n"
  },
  {
    "path": "website/docs/best-practice/deployment.mdx",
    "content": "---\nsidebar_position: 3\ndescription: 部署你的机器人\n---\n\n# 部署\n\nimport Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\n\n在编写完成各类插件后，我们需要长期运行机器人来使得用户能够正常使用。通常，我们会使用云服务器来部署机器人。\n\n我们在开发插件时，机器人运行的环境称为开发环境；而在部署后，机器人运行的环境称为生产环境。与开发环境不同的是，在生产环境中，开发者通常不能随意地修改/添加/删除代码，开启或停止服务。\n\n## 部署前准备\n\n### 项目依赖管理\n\n由于部署后的机器人运行在生产环境中，因此，为确保机器人能够正常运行，我们需要保证机器人的运行环境与开发环境一致。我们可以通过以下几种方式来进行依赖管理：\n\n<Tabs groupId=\"tool\">\n  <TabItem value=\"poetry\" label=\"Poetry\" default>\n\n[Poetry](https://python-poetry.org/) 是一个 Python 项目的依赖管理工具。它可以通过声明项目所依赖的库，为你管理（安装/更新）它们。Poetry 提供了一个 `poetry.lock` 文件，以确保可重复安装，并可以构建用于分发的项目。\n\nPoetry 会在安装依赖时自动生成 `poetry.lock` 文件，在**项目目录**下执行以下命令：\n\n```bash\n# 初始化 poetry 配置\npoetry init\n# 添加项目依赖，这里以 nonebot2[fastapi] 为例\npoetry add nonebot2[fastapi]\n```\n\n  </TabItem>\n  <TabItem value=\"pdm\" label=\"PDM\">\n\n[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` 文件，以确保可重复安装，并可以构建用于分发的项目。\n\nPDM 会在安装依赖时自动生成 `pdm.lock` 文件，在**项目目录**下执行以下命令：\n\n```bash\n# 初始化 pdm 配置\npdm init\n# 添加项目依赖，这里以 nonebot2[fastapi] 为例\npdm add nonebot2[fastapi]\n```\n\n  </TabItem>\n  <TabItem value=\"pip\" label=\"pip\">\n\n[pip](https://pip.pypa.io/) 是 Python 包管理工具。他并不是一个依赖管理工具，为了尽可能保证环境的一致性，我们可以使用 `requirements.txt` 文件来声明依赖。\n\n```bash\npip freeze > requirements.txt\n```\n\n  </TabItem>\n</Tabs>\n\n### 安装 Docker\n\n[Docker](https://www.docker.com/) 是一个应用容器引擎，可以让开发者打包应用以及依赖包到一个可移植的镜像中，然后发布到服务器上。\n\n我们可以参考 [Docker 官方文档](https://docs.docker.com/get-docker/) 来安装 Docker 。\n\n在 Linux 上，我们可以使用以下一键脚本来安装 Docker 以及 Docker Compose Plugin：\n\n```bash\ncurl -fsSL https://get.docker.com | sh -s -- --mirror Aliyun\n```\n\n在 Windows/macOS 上，我们可以使用 [Docker Desktop](https://docs.docker.com/desktop/) 来安装 Docker 以及 Docker Compose Plugin。\n\n### 安装脚手架 Docker 插件\n\n我们可以使用 [nb-cli-plugin-docker](https://github.com/nonebot/cli-plugin-docker) 来快速部署机器人。\n\n插件可以帮助我们生成配置文件并构建 Docker 镜像，以及启动/停止/重启机器人。使用以下命令安装脚手架 Docker 插件：\n\n```bash\nnb self install nb-cli-plugin-docker\n```\n\n## Docker 部署\n\n### 快速部署\n\n使用脚手架命令即可一键生成配置并部署：\n\n```bash\nnb docker up\n```\n\n当看到 `Running` 字样时，说明机器人已经启动成功。我们可以通过以下命令来查看机器人的运行日志：\n\n<Tabs groupId=\"deploy-tool\">\n  <TabItem value=\"nb-cli\" label=\"NB CLI\" default>\n\n```bash\nnb docker logs\n```\n\n  </TabItem>\n  <TabItem value=\"docker-compose\" label=\"Docker Compose\">\n\n```bash\ndocker compose logs\n```\n\n  </TabItem>\n</Tabs>\n\n如果需要停止机器人，我们可以使用以下命令：\n\n<Tabs groupId=\"deploy-tool\">\n  <TabItem value=\"nb-cli\" label=\"NB CLI\" default>\n\n```bash\nnb docker down\n```\n\n  </TabItem>\n  <TabItem value=\"docker-compose\" label=\"Docker Compose\">\n\n```bash\ndocker compose down\n```\n\n  </TabItem>\n</Tabs>\n\n### 自定义部署\n\n在部分情况下，我们需要事先生成 Docker 配置文件，再到生产环境进行部署；或者自动生成的配置文件并不能满足复杂场景，需要根据实际需求手动修改配置文件。我们可以使用以下命令来生成基础配置文件：\n\n```bash\nnb docker generate\n```\n\nnb-cli 将会在项目目录下生成 `docker-compose.yml` 和 `Dockerfile` 等配置文件。在 nb-cli 完成配置文件的生成后，我们可以根据部署环境的实际情况使用 nb-cli 或者 Docker Compose 来启动机器人。\n\n我们可以参考 [Dockerfile 文件规范](https://docs.docker.com/engine/reference/builder/)和 [Compose 文件规范](https://docs.docker.com/compose/compose-file/)修改这两个文件。\n\n修改完成后我们可以直接启动或者手动构建镜像：\n\n<Tabs groupId=\"deploy-tool\">\n  <TabItem value=\"nb-cli\" label=\"NB CLI\" default>\n\n```bash\n# 启动机器人\nnb docker up\n# 手动构建镜像\nnb docker build\n```\n\n  </TabItem>\n  <TabItem value=\"docker-compose\" label=\"Docker Compose\">\n\n```bash\n# 启动机器人\ndocker compose up -d\n# 手动构建镜像\ndocker compose build\n```\n\n  </TabItem>\n</Tabs>\n\n### 持续集成\n\n我们可以使用 GitHub Actions 来实现持续集成（CI），我们只需要在 GitHub 上发布 Release 即可自动构建镜像并推送至镜像仓库。\n\n首先，我们需要在 [Docker Hub](https://hub.docker.com/) （或者其他平台，如：[GitHub Packages](https://github.com/features/packages)、[阿里云容器镜像服务](https://www.alibabacloud.com/zh/product/container-registry)等）上创建镜像仓库，用于存放镜像。\n\n前往项目仓库的 `Settings` > `Secrets` > `actions` 栏目 `New Repository Secret` 添加构建所需的密钥：\n\n- `DOCKERHUB_USERNAME`: 你的 Docker Hub 用户名\n- `DOCKERHUB_TOKEN`: 你的 Docker Hub PAT（[创建方法](https://docs.docker.com/docker-hub/access-tokens/)）\n\n将以下文件添加至**项目目录**下的 `.github/workflows/` 目录下，并将文件中高亮行中的仓库名称替换为你的仓库名称：\n\n```yaml title=.github/workflows/build.yml\nname: Docker Hub Release\n\non:\n  push:\n    tags:\n      - \"v*\"\n\njobs:\n  docker:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v3\n\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v2\n\n      - name: Setup Docker\n        uses: docker/setup-buildx-action@v2\n\n      - name: Login to DockerHub\n        uses: docker/login-action@v2\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n      - name: Generate Tags\n        uses: docker/metadata-action@v4\n        id: metadata\n        with:\n          images: |\n            # highlight-next-line\n            {organization}/{repository}\n          tags: |\n            type=semver,pattern={{version}}\n            type=semver,pattern={{major}}.{{minor}}\n            type=sha\n            type=raw,value=latest\n\n      - name: Build and Publish\n        uses: docker/build-push-action@v4\n        with:\n          context: .\n          push: true\n          tags: ${{ steps.metadata.outputs.tags }}\n          labels: ${{ steps.metadata.outputs.labels }}\n          cache-from: type=gha\n          cache-to: type=gha,mode=max\n```\n\n### 持续部署\n\n在完成发布并构建镜像后，我们可以自动将镜像部署到服务器上。\n\n前往项目仓库的 `Settings` > `Secrets` > `actions` 栏目 `New Repository Secret` 添加部署所需的密钥：\n\n- `DEPLOY_HOST`: 部署服务器的 SSH 地址\n- `DEPLOY_USER`: 部署服务器用户名\n- `DEPLOY_KEY`: 部署服务器私钥（[创建方法](https://github.com/appleboy/ssh-action#setting-up-a-ssh-key)）\n- `DEPLOY_PATH`: 部署服务器上的项目路径\n\n将以下文件添加至**项目目录**下的 `.github/workflows/` 目录下，在构建成功后触发部署：\n\n```yaml title=.github/workflows/deploy.yml\nname: Deploy\n\non:\n  workflow_run:\n    workflows:\n      - Docker Hub Release\n    types:\n      - completed\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    if: ${{ github.event.workflow_run.conclusion == 'success' }}\n    steps:\n      - name: Start Deployment\n        uses: bobheadxi/deployments@v1\n        id: deployment\n        with:\n          step: start\n          token: ${{ secrets.GITHUB_TOKEN }}\n          env: bot\n\n      - name: Run Remote SSH Command\n        uses: appleboy/ssh-action@master\n        env:\n          DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}\n        with:\n          host: ${{ secrets.DEPLOY_HOST }}\n          username: ${{ secrets.DEPLOY_USER }}\n          key: ${{ secrets.DEPLOY_KEY }}\n          envs: DEPLOY_PATH\n          script: |\n            cd $DEPLOY_PATH\n            docker compose up -d --pull always\n\n      - name: update deployment status\n        uses: bobheadxi/deployments@v0.6.2\n        if: always()\n        with:\n          step: finish\n          token: ${{ secrets.GITHUB_TOKEN }}\n          status: ${{ job.status }}\n          env: ${{ steps.deployment.outputs.env }}\n          deployment_id: ${{ steps.deployment.outputs.deployment_id }}\n```\n\n将上一部分的 `docker-compose.yml` 文件以及 `.env.prod` 配置文件添加至 `DEPLOY_PATH` 目录下，并修改 `docker-compose.yml` 文件中的镜像配置，替换为 Docker Hub 的仓库名称：\n\n```diff\n- build: .\n+ image: {organization}/{repository}:latest\n```\n"
  },
  {
    "path": "website/docs/best-practice/error-tracking.md",
    "content": "---\nsidebar_position: 2\ndescription: 使用 sentry 进行错误跟踪\n---\n\n# 错误跟踪\n\n在应用实际运行过程中，可能会出现各种各样的错误。可能是由于代码逻辑错误，也可能是由于用户输入错误，甚至是由于第三方服务的错误。这些错误都会导致应用的运行出现问题，这时候就需要对错误进行跟踪，以便及时发现问题并进行修复。NoneBot 提供了 `nonebot-plugin-sentry` 插件，支持 [sentry](https://sentry.io/) 平台，可以方便地进行错误跟踪。\n\n## 安装插件\n\n在使用前请先安装 `nonebot-plugin-sentry` 插件至项目环境中，可参考[获取商店插件](../tutorial/store.mdx#安装插件)来了解并选择安装插件的方式。如：\n\n在**项目目录**下执行以下命令：\n\n```bash\nnb plugin install nonebot-plugin-sentry\n```\n\n## 使用插件\n\n在安装完成之后，仅需要对插件进行简单的配置即可使用。\n\n### 获取 sentry DSN\n\n前往 [sentry](https://sentry.io/) 平台，注册并创建一个新的项目，然后在项目设置中找到 `Client Keys (DSN)`，复制其中的 `DSN` 值。\n\n### 配置插件\n\n:::caution 注意\n错误跟踪通常在生产环境中使用，因此开发环境中 `sentry_dsn` 留空即会停用插件。\n:::\n\n在项目 dotenv 配置文件中添加以下配置即可使用：\n\n```dotenv\nSENTRY_DSN=<your_sentry_dsn>\n```\n\n## 配置项\n\n配置项具体含义参考 [Sentry Docs](https://docs.sentry.io/platforms/python/configuration/options/)。\n\n- `sentry_dsn: str`\n- `sentry_debug: bool = False`\n- `sentry_release: str | None = None`\n- `sentry_release: str | None = None`\n- `sentry_environment: str | None = nonebot env`\n- `sentry_server_name: str | None = None`\n- `sentry_sample_rate: float = 1.`\n- `sentry_max_breadcrumbs: int = 100`\n- `sentry_attach_stacktrace: bool = False`\n- `sentry_send_default_pii: bool = False`\n- `sentry_in_app_include: List[str] = Field(default_factory=list)`\n- `sentry_in_app_exclude: List[str] = Field(default_factory=list)`\n- `sentry_request_bodies: str = \"medium\"`\n- `sentry_with_locals: bool = True`\n- `sentry_ca_certs: str | None = None`\n- `sentry_before_send: Callable[[Any, Any], Any | None] | None = None`\n- `sentry_before_breadcrumb: Callable[[Any, Any], Any | None] | None = None`\n- `sentry_transport: Any | None = None`\n- `sentry_http_proxy: str | None = None`\n- `sentry_https_proxy: str | None = None`\n- `sentry_shutdown_timeout: int = 2`\n"
  },
  {
    "path": "website/docs/best-practice/htmlkit-render.md",
    "content": "---\nsidebar_position: 8\ndescription: 轻量化 HTML 绘图\n---\n\n# 轻量化 HTML 绘图\n\n图片是机器人交互中不可或缺的一部分，对于信息展示的直观性、美观性有很大的作用。\n基于 PIL 直接绘制图片具有良好的性能和存储开销，但是难以调试、维护过程式的绘图代码。\n使用浏览器渲染类插件可以方便地绘制网页，且能够直接通过 JS 对网页效果进行编程，但是它占用的存储和内存空间相对可观。\n\nNoneBot 提供的 `nonebot-plugin-htmlkit` 提供了另一种基于 HTML 和 CSS 语法的轻量化绘图选择：它基于 `litehtml` 解析库，无须安装额外的依赖即可使用，没有进程间通信带来的额外开销，且在支持 `webp` `avif` 等丰富图片格式的前提下，安装用的 wheel 文件大小仅有约 10 MB。\n\n作为粗略的性能参考，在一台 Ryzen 7 9700X 的 Windows 电脑上，渲染 [PEP 7](https://peps.python.org/pep-0007/) 的 HTML 页面（分辨率为 800x5788，大小约 1.4MB，从本地文件系统读取 CSS）大约需要 100ms，每个渲染任务内存最高占用约为 40MB.\n\n## 安装插件\n\n在使用前请先安装 `nonebot-plugin-htmlkit` 插件至项目环境中，可参考[获取商店插件](../tutorial/store.mdx#安装插件)来了解并选择安装插件的方式。如：\n\n在**项目目录**下执行以下命令：\n\n```bash\nnb plugin install nonebot-plugin-htmlkit\n```\n\n`nonebot-plugin-htmlkit` 插件目前兼容以下系统架构：\n\n- Windows x64\n- macOS arm64（M-系列芯片）\n- Linux x64 （非 Alpine 等 musl 系发行版）\n- Linux arm64 （非 Alpine 等 musl 系发行版）\n\n:::caution 访问网络内容\n\n如果需要访问网络资源（如 http(s) 网页内容），NoneBot 需要客户端型驱动器（Forward）。内置的驱动器有 `~httpx` 与 `~aiohttp`。\n\n详见[选择驱动器](../advanced/driver.md)。\n\n:::\n\n## 使用插件\n\n### 加载插件\n\n在使用本插件前同样需要使用 `require` 方法进行**加载**并**导入**需要使用的方法，可参考 [跨插件访问](../advanced/requiring.md) 一节进行了解，如：\n\n```python\nfrom nonebot import require\n\nrequire(\"nonebot_plugin_htmlkit\")\n\nfrom nonebot_plugin_htmlkit import html_to_pic, md_to_pic, template_to_pic, text_to_pic\n```\n\n插件会自动使用[配置中的参数](#配置-fontconfig)初始化 `fontconfig` 以提供字体查找功能。\n\n### 渲染 API\n\n`nonebot-plugin-htmlkit` 主要提供以下**异步**渲染函数：\n\n#### html_to_pic\n\n```python\nasync def html_to_pic(\n    html: str,\n    *,\n    base_url: str = \"\",\n    dpi: float = 144.0,\n    max_width: float = 800.0,\n    device_height: float = 600.0,\n    default_font_size: float = 12.0,\n    font_name: str = \"sans-serif\",\n    allow_refit: bool = True,\n    image_format: Literal[\"png\", \"jpeg\"] = \"png\",\n    jpeg_quality: int = 100,\n    lang: str = \"zh\",\n    culture: str = \"CN\",\n    img_fetch_fn: ImgFetchFn = combined_img_fetcher,\n    css_fetch_fn: CSSFetchFn = combined_css_fetcher,\n    urljoin_fn: Callable[[str, str], str] = urllib3.parse.urljoin,\n) -> bytes:\n    ...\n```\n\n最核心的渲染函数。\n\n`base_url` 和 `urljoin_fn` 控制着传入 `image_fetch_fn` 和 `css_fetch_fn` 回调的 url 内容。\n\n`allow_refit` 如果为真，渲染时会自动缩小产出图片的宽度到最适合的宽度，否则必定产出 `max_width` 宽度的图片。\n\n`max_width` 与 `device_height` 会在 `@media` 判断中被使用。\n\n`img_fetch_fn` 预期为一个异步可调用对象（函数），接收图片 url 并返回对应 url 的 jpeg 或 png 二进制数据（`bytes`），可在拒绝加载时返回 `None`.\n\n`css_fetch_fn` 预期为一个异步可调用对象（函数），接收目标 CSS url 并返回对应 url 的 CSS 文本（`str`），可在拒绝加载时返回 `None`.\n\n以下为辅助的封装函数，关键字参数若未特殊说明均与 `html_to_pic` 含义相同。\n\n#### text_to_pic\n\n```python\nasync def text_to_pic(\n    text: str,\n    css_path: str = \"\",\n    *,\n    max_width: int = 500,\n    allow_refit: bool = True,\n    image_format: Literal[\"png\", \"jpeg\"] = \"png\",\n    jpeg_quality: int = 100,\n) -> bytes:\n    ...\n```\n\n可用于渲染多行文本。\n\n`text` 会被放置于 `<div id=\"main\" class=\"main-box\"> <div class=\"text\">` 中，可据此编写 CSS 来改变文本表现。\n\n#### md_to_pic\n\n```python\nasync def md_to_pic(\n    md: str = \"\",\n    md_path: str = \"\",\n    css_path: str = \"\",\n    *,\n    max_width: int = 500,\n    img_fetch_fn: ImgFetchFn = combined_img_fetcher,\n    allow_refit: bool = True,\n    image_format: Literal[\"png\", \"jpeg\"] = \"png\",\n    jpeg_quality: int = 100,\n) -> bytes:\n    ...\n```\n\n可用于渲染 Markdown 文本。默认为 GitHub Markdown Light 风格，支持基于 `pygments` 的代码高亮。\n\n`md` 和 `md_path` 二选一，前者设置时应为 Markdown 的文本，后者设置时应为指向 Markdown 文本文件的路径。\n\n#### template_to_pic\n\n```python\nasync def template_to_pic(\n    template_path: str | PathLike[str] | Sequence[str | PathLike[str]],\n    template_name: str,\n    templates: Mapping[Any, Any],\n    filters: None | Mapping[str, Any] = None,\n    *,\n    max_width: int = 500,\n    device_height: int = 600,\n    base_url: str | None = None,\n    img_fetch_fn: ImgFetchFn = combined_img_fetcher,\n    css_fetch_fn: CSSFetchFn = combined_css_fetcher,\n    allow_refit: bool = True,\n    image_format: Literal[\"png\", \"jpeg\"] = \"png\",\n    jpeg_quality: int = 100,\n) -> bytes:\n    ...\n```\n\n渲染 jinja2 模板。\n\n`template_path` 为 jinja2 环境的路径，`template_name` 是环境中要加载模板的名字，`templates` 为传入模板的参数，`filters` 为过滤器名 -> 自定义过滤器的映射。\n\n### 控制外部资源获取\n\n通过传入 `img_fetch_fn` 与 `css_fetch_fn`，我们可以在实际访问资源前进行审查，修改资源的来源，或是对 IO 操作进行缓存。\n\n`img_fetch_fn` 预期为一个异步可调用对象（函数），接收图片 url 并返回对应 url 的 jpeg 或 png 二进制数据（`bytes`），可在拒绝加载时返回 `None`.\n\n`css_fetch_fn` 预期为一个异步可调用对象（函数），接收目标 CSS url 并返回对应 url 的 CSS 文本（`str`），可在拒绝加载时返回 `None`.\n\n如果你想要禁用外部资源加载/只从文件系统加载/只从网络加载，可以使用 `none_fetcher` `filesystem_***_fetcher` `network_***_fetcher`。\n\n默认的 fetcher 行为（对于 `file://` 从文件系统加载，其余从网络加载）位于 `combined_***_fetcher`，可以通过对其封装实现缓存等操作。\n\n## 配置项\n\n### 配置 fontconfig\n\n`htmlkit` 使用 `fontconfig` 查找字体，请参阅 [`fontconfig 用户手册`](https://fontconfig.pages.freedesktop.org/fontconfig/fontconfig-user) 了解环境变量的具体含义、如何通过编写配置文件修改字体配置等。\n\n#### fontconfig_file\n\n- **类型**: `str | None`\n- **默认值**: `None`\n\n覆盖默认的配置文件路径。\n\n#### fontconfig_path\n\n- **类型**: `str | None`\n- **默认值**: `None`\n\n覆盖默认的配置目录。\n\n#### fontconfig_sysroot\n\n- **类型**: `str | None`\n- **默认值**: `None`\n\n覆盖默认的 sysroot。\n\n#### fc_debug\n\n- **类型**: `str | None`\n- **默认值**: `None`\n\n设置 Fontconfig 的 debug 级别。\n\n#### fc_dbg_match_filter\n\n- **类型**: `str | None`\n- **默认值**: `None`\n\n当 `FC_DEBUG` 设置为 `MATCH2` 时，过滤 debug 输出。\n\n#### fc_lang\n\n- **类型**: `str | None`\n- **默认值**: `None`\n\n设置默认语言，否则从 `LOCALE` 环境变量获取。\n\n#### fontconfig_use_mmap\n\n- **类型**: `str | None`\n- **默认值**: `None`\n\n是否使用 `mmap(2)` 读取字体缓存。\n"
  },
  {
    "path": "website/docs/best-practice/multi-adapter.mdx",
    "content": "---\nsidebar_position: 4\ndescription: 插件跨平台支持\n---\n\n# 插件跨平台支持\n\nimport Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\n\n## 使用 NoneBot 本身\n\n由于不同平台的事件与接口之间，存在着极大的差异性，NoneBot 通过[重载](../appendices/overload.md)的方式，使得插件可以在不同平台上正确响应。但为了减少跨平台的兼容性问题，我们应该尽可能的使用基类方法实现原生跨平台，而不是使用特定平台的方法。当基类方法无法满足需求时，我们可以使用依赖注入的方式，将特定平台的事件或机器人注入到事件处理函数中，实现针对特定平台的处理。\n\n:::tip 提示\n如果需要在多平台上**使用**跨平台插件，首先应该根据[注册适配器](../advanced/adapter.md#注册适配器)一节，为机器人注册各平台对应的适配器。\n:::\n\n### 基于基类的跨平台\n\n在[事件通用信息](../advanced/adapter.md#获取事件通用信息)中，我们了解了事件基类能够提供的通用信息。同时，[事件响应器操作](../appendices/session-control.mdx#更多事件响应器操作)也为我们提供了基本的用户交互方式。使用这些方法，可以让我们的插件运行在任何平台上。例如，一个简单的命令处理插件：\n\n```python {5,11}\nfrom nonebot import on_command\nfrom nonebot.adapters import Event\n\nasync def is_blacklisted(event: Event) -> bool:\n    return event.get_user_id() not in BLACKLIST\n\nweather = on_command(\"天气\", rule=is_blacklisted, priority=10, block=True)\n\n@weather.handle()\nasync def handle_function():\n    await weather.finish(\"今天的天气是...\")\n```\n\n由于此插件仅使用了事件通用信息和事件响应器操作的纯文本交互方式，这些方法不使用特定平台的信息或接口，因此是原生跨平台的，并不需要额外处理。但在一些较为复杂的需求下，例如发送图片消息时，并非所有平台都具有统一的接口，因此基类便无能为力，我们需要引入特定平台的适配器了。\n\n### 基于重载的跨平台\n\n重载是 NoneBot 跨平台操作的核心，在[事件类型与重载](../appendices/overload.md#重载)一节中，我们初步了解了如何通过类型注解来实现针对不同平台事件的处理方式。在[依赖注入](../advanced/dependency.mdx)一节中，我们又对依赖注入的使用方法进行了详细的介绍。结合这两节内容，我们可以实现更复杂的跨平台操作。\n\n#### 处理近似事件\n\n对于一系列**差异不大**的事件，我们往往具有相同的处理逻辑。这时，我们不希望将相同的逻辑编写两遍，而应该复用代码，以实现在同一个事件处理函数中处理多个近似事件。我们可以使用[事件重载](../advanced/dependency.mdx#event)的特性来实现这一功能。例如：\n\n<Tabs groupId=\"python\">\n  <TabItem value=\"3.10\" label=\"Python 3.10+\" default>\n\n```python\nfrom nonebot import on_command\nfrom nonebot.adapters import Message\nfrom nonebot.params import CommandArg\nfrom nonebot.adapters.onebot.v11 import MessageEvent as OnebotV11MessageEvent\nfrom nonebot.adapters.onebot.v12 import MessageEvent as OnebotV12MessageEvent\n\necho = on_command(\"echo\", priority=10, block=True)\n\n@echo.handle()\nasync def handle_function(event: OnebotV11MessageEvent | OnebotV12MessageEvent, args: Message = CommandArg()):\n    await echo.finish(args)\n```\n\n  </TabItem>\n  <TabItem value=\"3.9\" label=\"Python 3.9\">\n\n```python\nfrom typing import Union\n\nfrom nonebot import on_command\nfrom nonebot.adapters import Message\nfrom nonebot.params import CommandArg\nfrom nonebot.adapters.onebot.v11 import MessageEvent as OnebotV11MessageEvent\nfrom nonebot.adapters.onebot.v12 import MessageEvent as OnebotV12MessageEvent\n\necho = on_command(\"echo\", priority=10, block=True)\n\n@echo.handle()\nasync def handle_function(event: Union[OnebotV11MessageEvent, OnebotV12MessageEvent], args: Message = CommandArg()):\n    await echo.finish(args)\n```\n\n  </TabItem>\n</Tabs>\n\n#### 在依赖注入中使用重载\n\nNoneBot 依赖注入系统提供了自定义子依赖的方法，子依赖的类型同样会影响到事件处理函数的重载行为。例如：\n\n```python\nfrom datetime import datetime\n\nfrom nonebot import on_command\nfrom nonebot.adapters.console import MessageEvent\n\necho = on_command(\"echo\", priority=10, block=True)\n\ndef get_event_time(event: MessageEvent):\n    return event.time\n\n# 处理控制台消息事件\n@echo.handle()\nasync def handle_function(time: datetime = Depends(get_event_time)):\n    await echo.finish(time.strftime(\"%Y-%m-%d %H:%M:%S\"))\n```\n\n示例中 ，我们为 `handle_function` 事件处理函数注入了自定义的 `get_event_time` 子依赖，而此子依赖注入参数为 Console 适配器的 `MessageEvent`。因此 `handle_function` 仅会响应 Console 适配器的 `MessageEvent` ，而不能响应其他事件。\n\n#### 处理多平台事件\n\n不同平台的事件之间，往往存在着极大的差异性。为了满足我们插件的跨平台运行，通常我们需要抽离业务逻辑，以保证代码的复用性。一个合理的做法是，在事件响应器的处理流程中，首先先针对不同平台的事件分别进行处理，提取出核心业务逻辑所需要的信息；然后再将这些信息传递给业务逻辑处理函数；最后将业务逻辑的输出以各平台合适的方式返回给用户。也就是说，与平台绑定的处理部分应该与平台无关部分尽量分离。例如：\n\n```python\nimport inspect\n\nfrom nonebot import on_command\nfrom nonebot.typing import T_State\nfrom nonebot.matcher import Matcher\nfrom nonebot.adapters import Message\nfrom nonebot.params import CommandArg, ArgPlainText\nfrom nonebot.adapters.console import Bot as ConsoleBot\nfrom nonebot.adapters.onebot.v11 import Bot as OnebotBot\nfrom nonebot.adapters.console import MessageSegment as ConsoleMessageSegment\n\nweather = on_command(\"天气\", priority=10, block=True)\n\n@weather.handle()\nasync def handle_function(matcher: Matcher, args: Message = CommandArg()):\n    if args.extract_plain_text():\n        matcher.set_arg(\"location\", args)\n\n\nasync def get_weather(state: T_State, location: str = ArgPlainText()):\n    if location not in [\"北京\", \"上海\", \"广州\", \"深圳\"]:\n        await weather.reject(f\"你想查询的城市 {location} 暂不支持，请重新输入！\")\n\n    state[\"weather\"] = \"⛅ 多云 20℃~24℃\"\n\n\n# 处理控制台询问\n@weather.got(\n    \"location\",\n    prompt=ConsoleMessageSegment.emoji(\"question\") + \"请输入地名\",\n    parameterless=[Depends(get_weather)],\n)\nasync def handle_console(bot: ConsoleBot):\n    pass\n\n# 处理 OneBot 询问\n@weather.got(\n    \"location\",\n    prompt=\"请输入地名\",\n    parameterless=[Depends(get_weather)],\n)\nasync def handle_onebot(bot: OnebotBot):\n    pass\n\n# 通过依赖注入或事件处理函数来进行业务逻辑处理\n\n# 处理控制台回复\n@weather.handle()\nasync def handle_console_reply(bot: ConsoleBot, state: T_State, location: str = ArgPlainText()):\n    await weather.send(\n        ConsoleMessageSegment.markdown(\n            inspect.cleandoc(\n                f\"\"\"\n                # {location}\n\n                - 今天\n\n                   {state['weather']}\n                \"\"\"\n            )\n        )\n    )\n\n# 处理 OneBot 回复\n@weather.handle()\nasync def handle_onebot_reply(bot: OnebotBot, state: T_State, location: str = ArgPlainText()):\n    await weather.send(f\"今天{location}的天气是{state['weather']}\")\n```\n\n## 使用插件\n\n得益于众多开发者为 NoneBot 社区做出的贡献，我们可以通过一系列插件来完成跨平台插件的开发。\n\n这些插件可以分为三类：\n\n### 事件处理\n\n- [all4one](https://github.com/nonepkg/nonebot-plugin-all4one): 将不同平台的事件转为符合 OneBot V12 协议的插件\n  - 支持的适配器: OneBot V11/V12, Discord, QQ, Telegram\n\n### 消息处理\n\n- [alconna](https://github.com/nonebot/plugin-alconna): 对几乎所有适配器中消息的收发、撤回、编辑、表态的统一插件\n  - 支持的适配器: OneBot V11/V12, Telegram, Feishu, Github, QQ, Ding, Console, Kaiheila, Mirai, NtChat, Minecraft, Discord, Satori, Red, Dodo, Kritor, Tailchat, Mail, WXMP, Heybox, Gewechat\n- [send-anything-anywhere](https://github.com/felinae98/nonebot-plugin-send-anything-anywhere): 帮助处理不同适配器消息的适配和发送的插件\n  - 支持的适配器: OneBot V11/V12, Kaiheila, Telegram, Feishu, Red, DoDo, Satori, QQ, Discord\n\n### 会话信息提取\n\n- [uninfo](https://github.com/RF-Tar-Railt/nonebot-plugin-uninfo): 多平台的会话信息(用户、群组、频道)获取插件\n  - 支持的适配器: OneBot V11/V12, Telegram, Feishu, QQ, Console, Kaiheila, Mirai, Minecraft, Discord, Satori, Dodo, Kritor, Mail, WXMP, Gewechat\n- [session](https://github.com/noneplugin/nonebot-plugin-session): 会话信息提取与会话 id 定义插件\n  - 支持的适配器: OneBot V11/V12, Console, Kaiheila, Telegram, Feishu, Red, DoDo, Satori, QQ, Discord\n- [userinfo](https://github.com/noneplugin/nonebot-plugin-userinfo: 用户信息获取插件\n  - 支持的适配器: OneBot V11/V12, Console, Kaiheila, Telegram, Feishu, Red, DoDo, Satori, QQ, Discord\n"
  },
  {
    "path": "website/docs/best-practice/scheduler.md",
    "content": "---\nsidebar_position: 0\ndescription: 定时执行任务\n---\n\n# 定时任务\n\n[APScheduler](https://apscheduler.readthedocs.io/en/3.x/) (Advanced Python Scheduler) 是一个 Python 第三方库，其强大的定时任务功能被广泛应用于各个场景。在 NoneBot 中，定时任务作为一个额外功能，依赖于基于 APScheduler 开发的 [`nonebot-plugin-apscheduler`](https://github.com/nonebot/plugin-apscheduler) 插件进行支持。\n\n## 安装插件\n\n在使用前请先安装 `nonebot-plugin-apscheduler` 插件至项目环境中，可参考[获取商店插件](../tutorial/store.mdx#安装插件)来了解并选择安装插件的方式。如：\n\n在**项目目录**下执行以下命令：\n\n```bash\nnb plugin install nonebot-plugin-apscheduler\n```\n\n## 使用插件\n\n`nonebot-plugin-apscheduler` 本质上是对 [APScheduler](https://apscheduler.readthedocs.io/en/3.x/) 进行了封装以适用于 NoneBot 开发，因此其使用方式与 APScheduler 本身并无显著区别。在此我们会简要介绍其调用方法，更多的使用方面的功能请参考[APScheduler 官方文档](https://apscheduler.readthedocs.io/en/3.x/userguide.html)。\n\n### 导入调度器\n\n由于 `nonebot_plugin_apscheduler` 作为插件，因此需要在使用前对其进行**加载**并**导入**其中的 `scheduler` 调度器来创建定时任务。使用 `require` 方法可轻松完成这一过程，可参考 [跨插件访问](../advanced/requiring.md) 一节进行了解。\n\n```python\nfrom nonebot import require\n\nrequire(\"nonebot_plugin_apscheduler\")\n\nfrom nonebot_plugin_apscheduler import scheduler\n```\n\n### 添加定时任务\n\n在 [APScheduler 官方文档](https://apscheduler.readthedocs.io/en/3.x/userguide.html#adding-jobs) 中提供了以下两种直接添加任务的方式：\n\n```python\nfrom nonebot import require\n\nrequire(\"nonebot_plugin_apscheduler\")\n\nfrom nonebot_plugin_apscheduler import scheduler\n\n# 基于装饰器的方式\n@scheduler.scheduled_job(\"cron\", hour=\"*/2\", id=\"job_0\", args=[1], kwargs={arg2: 2})\nasync def run_every_2_hour(arg1: int, arg2: int):\n    pass\n\n# 基于 add_job 方法的方式\ndef run_every_day(arg1: int, arg2: int):\n    pass\n\nscheduler.add_job(\n    run_every_day, \"interval\", days=1, id=\"job_1\", args=[1], kwargs={arg2: 2}\n)\n```\n\n:::caution 注意\n由于 APScheduler 的定时任务并不是**由事件响应器所触发的事件**，因此其任务函数无法同[事件处理函数](../tutorial/handler.mdx#事件处理函数)一样通过[依赖注入](../tutorial/event-data.mdx#认识依赖注入)获取上下文信息，也无法通过事件响应器对象的方法进行任何操作，因此我们需要使用[调用平台 API](../appendices/api-calling.mdx#调用平台-api)的方式来获取信息或收发消息。\n\n相对于事件处理依赖而言，编写定时任务更像是编写普通的函数，需要我们自行获取信息以及发送信息，请**不要**将事件处理依赖的特殊语法用于定时任务！\n:::\n\n关于 APScheduler 的更多使用方法，可以参考 [APScheduler 官方文档](https://apscheduler.readthedocs.io/en/3.x/index.html) 进行了解。\n\n### 配置项\n\n#### apscheduler_autostart\n\n- **类型**: `bool`\n- **默认值**: `True`\n\n是否自动启动 `scheduler` ，若不启动需要自行调用 `scheduler.start()`。\n\n#### apscheduler_log_level\n\n- **类型**: `int`\n- **默认值**: `30`\n\napscheduler 输出的日志等级\n\n- `WARNING` = `30` (默认)\n- `INFO` = `20`\n- `DEBUG` = `10` (只有在开启 nonebot 的 debug 模式才会显示 debug 日志)\n\n#### apscheduler_config\n\n- **类型**: `dict`\n- **默认值**: `{ \"apscheduler.timezone\": \"Asia/Shanghai\" }`\n\n`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)\n\n配置需要包含 `apscheduler.` 作为前缀，例如 `apscheduler.timezone`。\n"
  },
  {
    "path": "website/docs/best-practice/testing/README.mdx",
    "content": "---\nsidebar_position: 1\ndescription: 使用 NoneBug 进行单元测试\n\nslug: /best-practice/testing/\n---\n\n# 配置与测试事件响应器\n\nimport Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\n\n> 在计算机编程中，单元测试（Unit Testing）又称为模块测试，是针对程序模块（软件设计的最小单位）来进行正确性检验的测试工作。\n\n为了保证代码的正确运行，我们不仅需要对错误进行跟踪，还需要对代码进行正确性检验，也就是测试。NoneBot 提供了一个测试工具——NoneBug，它是一个 [pytest](https://docs.pytest.org/en/stable/) 插件，可以帮助我们便捷地进行单元测试。\n\n:::tip 提示\n建议在阅读本文档前先阅读 [pytest 官方文档](https://docs.pytest.org/en/stable/)来了解 pytest 的相关术语和基本用法。\n:::\n\n## 安装 NoneBug\n\n在**项目目录**下激活虚拟环境后运行以下命令安装 NoneBug：\n\n<Tabs groupId=\"tool\">\n  <TabItem value=\"poetry\" label=\"Poetry\" default>\n\n```bash\npoetry add nonebug -G test\n```\n\n  </TabItem>\n  <TabItem value=\"pdm\" label=\"PDM\">\n\n```bash\npdm add nonebug -dG test\n```\n\n  </TabItem>\n  <TabItem value=\"pip\" label=\"pip\">\n\n```bash\npip install nonebug\n```\n\n  </TabItem>\n</Tabs>\n\n要运行 NoneBug 测试，还需要额外安装 pytest 异步插件 `pytest-asyncio` 或 `anyio` 以支持异步测试。文档中，我们以 `pytest-asyncio` 为例：\n\n<Tabs groupId=\"tool\">\n  <TabItem value=\"poetry\" label=\"Poetry\" default>\n\n```bash\npoetry add pytest-asyncio -G test\n```\n\n  </TabItem>\n  <TabItem value=\"pdm\" label=\"PDM\">\n\n```bash\npdm add pytest-asyncio -dG test\n```\n\n  </TabItem>\n  <TabItem value=\"pip\" label=\"pip\">\n\n```bash\npip install pytest-asyncio\n```\n\n  </TabItem>\n</Tabs>\n\n## 配置测试\n\n在开始测试之前，我们需要对测试进行一些配置，以正确启动我们的机器人。\n\n首先我们需要配置 pytest-asyncio，在 `pyproject.toml` 的 pytest 配置部分添加：\n\n```toml\n[tool.pytest.ini_options]\nasyncio_mode = \"auto\"\nasyncio_default_fixture_loop_scope = \"session\"\n```\n\n然后，我们在 `tests` 目录下新建 `conftest.py` 文件，添加以下内容：\n\n```python title=tests/conftest.py\nimport pytest\nimport nonebot\nfrom pytest_asyncio import is_async_test\n# 导入适配器\nfrom nonebot.adapters.console import Adapter as ConsoleAdapter\n\ndef pytest_collection_modifyitems(items: list[pytest.Item]):\n    pytest_asyncio_tests = (item for item in items if is_async_test(item))\n    session_scope_marker = pytest.mark.asyncio(loop_scope=\"session\")\n    for async_test in pytest_asyncio_tests:\n        async_test.add_marker(session_scope_marker, append=False)\n\n@pytest.fixture(scope=\"session\", autouse=True)\nasync def after_nonebot_init(after_nonebot_init: None):\n    # 加载适配器\n    driver = nonebot.get_driver()\n    driver.register_adapter(ConsoleAdapter)\n\n    # 加载插件\n    nonebot.load_from_toml(\"pyproject.toml\")\n```\n\n这样，我们就可以在测试中使用机器人的插件了。通常，我们不需要自行初始化 NoneBot，NoneBug 已经为我们运行了 `nonebot.init()`。如果需要自定义 NoneBot 初始化的参数，我们可以在 `conftest.py` 中添加 `pytest_configure` 钩子函数。例如，我们可以修改 NoneBot 配置环境为 `test` 并从环境变量中输入配置：\n\n```python {4,6,8-10} title=tests/conftest.py\nimport os\n\nimport pytest\nfrom nonebug import NONEBOT_INIT_KWARGS\n\nos.environ[\"ENVIRONMENT\"] = \"test\"\n\ndef pytest_configure(config: pytest.Config):\n    config.stash[NONEBOT_INIT_KWARGS] = {\"secret\": os.getenv(\"INPUT_SECRET\")}\n```\n\nNoneBug 默认也会为我们管理 lifespan 的 startup 与 shutdown。如果不希望 NoneBug 管理 lifespan，你可以在 `pytest_configure` 里添加以下配置：\n\n```python\nimport pytest\nfrom nonebug import NONEBOT_START_LIFESPAN\n\ndef pytest_configure(config: pytest.Config):\n    config.stash[NONEBOT_START_LIFESPAN] = False\n```\n\n## 编写插件测试\n\n在配置完成插件加载后，我们就可以在测试中使用插件了。NoneBug 通过 pytest fixture `app` 提供各种测试方法，我们可以在测试中使用它来测试插件。现在，我们创建一个测试脚本来测试[深入指南](../../appendices/session-control.mdx)中编写的天气插件。首先，我们先要导入我们需要的模块：\n\n<details>\n  <summary>插件示例</summary>\n\n```python title=weather/__init__.py\nfrom nonebot import on_command\nfrom nonebot.rule import to_me\nfrom nonebot.matcher import Matcher\nfrom nonebot.adapters import Message\nfrom nonebot.params import CommandArg, ArgPlainText\n\nweather = on_command(\"天气\", rule=to_me(), aliases={\"weather\", \"天气预报\"})\n\n@weather.handle()\nasync def handle_function(matcher: Matcher, args: Message = CommandArg()):\n    if args.extract_plain_text():\n        matcher.set_arg(\"location\", args)\n\n@weather.got(\"location\", prompt=\"请输入地名\")\nasync def got_location(location: str = ArgPlainText()):\n    if location not in [\"北京\", \"上海\", \"广州\", \"深圳\"]:\n        await weather.reject(f\"你想查询的城市 {location} 暂不支持，请重新输入！\")\n    await weather.finish(f\"今天{location}的天气是...\")\n```\n\n</details>\n\n```python {4,5,9,11-16} title=tests/test_weather.py\nfrom datetime import datetime\n\nimport pytest\nfrom nonebug import App\nfrom nonebot.adapters.console import User, Message, MessageEvent\n\n@pytest.mark.asyncio\nasync def test_weather(app: App):\n    from awesome_bot.plugins.weather import weather\n\n    event = MessageEvent(\n        time=datetime.now(),\n        self_id=\"test\",\n        message=Message(\"/天气 北京\"),\n        user=User(id=\"user\"),\n    )\n```\n\n在上面的代码中，我们引入了 NoneBug 的测试 `App` 对象，以及必要的适配器消息与事件定义等。在测试函数 `test_weather` 中，我们导入了要进行测试的事件响应器 `weather`。请注意，由于需要等待 NoneBot 初始化并加载插件完毕，插件内容必须在**测试函数内部**进行导入。然后，我们创建了一个 `MessageEvent` 事件对象，它模拟了一个用户发送了 `/天气 北京` 的消息。接下来，我们使用 `app.test_matcher` 方法来测试 `weather` 事件响应器：\n\n```python {11-15} title=tests/test_weather.py\n@pytest.mark.asyncio\nasync def test_weather(app: App):\n    from awesome_bot.plugins.weather import weather\n\n    event = MessageEvent(\n        time=datetime.now(),\n        self_id=\"test\",\n        message=Message(\"/天气 北京\"),\n        user=User(id=\"user\"),\n    )\n    async with app.test_matcher(weather) as ctx:\n        bot = ctx.create_bot()\n        ctx.receive_event(bot, event)\n        ctx.should_call_send(event, \"今天北京的天气是...\", result=None)\n        ctx.should_finished(weather)\n```\n\n这里我们使用 `async with` 语句并通过参数指定要测试的事件响应器 `weather` 来进入测试上下文。在测试上下文中，我们可以使用 `ctx.create_bot` 方法创建一个虚拟的机器人实例，并使用 `ctx.receive_event` 方法来模拟机器人接收到消息事件。然后，我们就可以定义预期行为来测试机器人是否正确运行。在上面的代码中，我们使用 `ctx.should_call_send` 方法来断言机器人应该发送 `今天北京的天气是...` 这条消息，并且将发送函数的调用结果作为第三个参数返回给事件处理函数。如果断言失败，测试将会不通过。我们也可以使用 `ctx.should_finished` 方法来断言机器人应该结束会话。\n\n为了测试更复杂的情况，我们可以为添加更多的测试用例。例如，我们可以测试用户输入了一个不支持的地名时机器人的反应：\n\n```python {17-21,23-26} title=tests/test_weather.py\ndef make_event(message: str = \"\") -> MessageEvent:\n    return MessageEvent(\n        time=datetime.now(),\n        self_id=\"test\",\n        message=Message(message),\n        user=User(id=\"user\"),\n    )\n\n@pytest.mark.asyncio\nasync def test_weather(app: App):\n    from awesome_bot.plugins.weather import weather\n\n    async with app.test_matcher(weather) as ctx:\n        ...  # 省略前面的测试用例\n\n    async with app.test_matcher(weather) as ctx:\n        bot = ctx.create_bot()\n        event = make_event(\"/天气 南京\")\n        ctx.receive_event(bot, event)\n        ctx.should_call_send(event, \"你想查询的城市 南京 暂不支持，请重新输入！\", result=None)\n        ctx.should_rejected(weather)\n\n        event = make_event(\"北京\")\n        ctx.receive_event(bot, event)\n        ctx.should_call_send(event, \"今天北京的天气是...\", result=None)\n        ctx.should_finished(weather)\n```\n\n在上面的代码中，我们使用 `ctx.should_rejected` 来断言机器人应该请求用户重新输入。然后，我们再次使用 `ctx.receive_event` 方法来模拟用户回复了 `北京`，并使用 `ctx.should_finished` 来断言机器人应该结束会话。\n\n更多的 NoneBug 用法将在后续章节中介绍。\n"
  },
  {
    "path": "website/docs/best-practice/testing/_category_.json",
    "content": "{\n  \"label\": \"单元测试\",\n  \"position\": 5\n}\n"
  },
  {
    "path": "website/docs/best-practice/testing/behavior.mdx",
    "content": "---\nsidebar_position: 2\ndescription: 测试事件响应、平台接口调用和会话控制\n---\n\n# 测试事件响应与会话操作\n\nimport Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\n\n在 NoneBot 接收到事件时，事件响应器根据优先级依次通过权限、响应规则来判断当前事件是否应该触发。事件响应流程中，机器人可能会通过 `send` 发送消息或者调用平台接口来执行预期的操作。因此，我们需要对这两种操作进行单元测试。\n\n在上一节中，我们对单个事件响应器进行了简单测试。但是在实际场景中，机器人可能定义了多个事件响应器，由于优先级和响应规则的存在，预期的事件响应器可能并不会被触发。NoneBug 支持同时测试多个事件响应器，以此来测试机器人的整体行为。\n\n## 测试事件响应\n\nNoneBug 提供了六种定义 `Rule` 和 `Permission` 预期行为的方法：\n\n- `should_pass_rule`\n- `should_not_pass_rule`\n- `should_ignore_rule`\n- `should_pass_permission`\n- `should_not_pass_permission`\n- `should_ignore_permission`\n\n:::tip 提示\n事件响应器类型的检查属于 `Permission` 的一部分，因此可以通过 `should_pass_permission` 和 `should_not_pass_permission` 方法来断言事件响应器类型的检查。\n:::\n\n下面我们根据插件示例来测试事件响应行为，我们首先定义两个事件响应器作为测试的对象：\n\n```python title=example.py\nfrom nonebot import on_command\n\ndef never_pass():\n    return False\n\nfoo = on_command(\"foo\")\nbar = on_command(\"bar\", permission=never_pass)\n```\n\n在这两个事件响应器中，`foo` 当收到 `/foo` 消息时会执行，而 `bar` 则不会执行。我们使用 NoneBug 来测试它们：\n\n<Tabs groupId=\"testScope\">\n  <TabItem value=\"separate\" label=\"独立测试\" default>\n\n```python {21,22,28,29} title=tests/test_example.py\nfrom datetime import datetime\n\nimport pytest\nfrom nonebug import App\nfrom nonebot.adapters.console import User, Message, MessageEvent\n\ndef make_event(message: str = \"\") -> MessageEvent:\n    return MessageEvent(\n        time=datetime.now(),\n        self_id=\"test\",\n        message=Message(message),\n        user=User(id=\"user\"),\n    )\n\n@pytest.mark.asyncio\nasync def test_example(app: App):\n    from awesome_bot.plugins.example import foo, bar\n\n    async with app.test_matcher(foo) as ctx:\n        bot = ctx.create_bot()\n        event = make_event(\"/foo\")\n        ctx.receive_event(bot, event)\n        ctx.should_pass_rule()\n        ctx.should_pass_permission()\n\n    async with app.test_matcher(bar) as ctx:\n        bot = ctx.create_bot()\n        event = make_event(\"/foo\")\n        ctx.receive_event(bot, event)\n        ctx.should_not_pass_rule()\n        ctx.should_not_pass_permission()\n```\n\n在上面的代码中，我们分别对 `foo` 和 `bar` 事件响应器进行响应测试。我们使用 `ctx.should_pass_rule` 和 `ctx.should_pass_permission` 断言 `foo` 事件响应器应该被触发，使用 `ctx.should_not_pass_rule` 和 `ctx.should_not_pass_permission` 断言 `bar` 事件响应器应该被忽略。\n\n  </TabItem>\n  <TabItem value=\"global\" label=\"集成测试\">\n\n```python title=tests/test_example.py\nfrom datetime import datetime\n\nimport pytest\nfrom nonebug import App\nfrom nonebot.adapters.console import User, Message, MessageEvent\n\ndef make_event(message: str = \"\") -> MessageEvent:\n    return MessageEvent(\n        time=datetime.now(),\n        self_id=\"test\",\n        message=Message(message),\n        user=User(id=\"user\"),\n    )\n\n@pytest.mark.asyncio\nasync def test_example(app: App):\n    from awesome_bot.plugins.example import foo, bar\n\n    async with app.test_matcher() as ctx:\n        bot = ctx.create_bot()\n        event = make_event(\"/foo\")\n        ctx.receive_event(bot, event)\n        ctx.should_pass_rule(foo)\n        ctx.should_pass_permission(foo)\n        ctx.should_not_pass_rule(bar)\n        ctx.should_not_pass_permission(bar)\n```\n\n在上面的代码中，我们对 `foo` 和 `bar` 事件响应器一起进行响应测试。我们使用 `ctx.should_pass_rule` 和 `ctx.should_pass_permission` 断言 `foo` 事件响应器应该被触发，使用 `ctx.should_not_pass_rule` 和 `ctx.should_not_pass_permission` 断言 `bar` 事件响应器应该被忽略。通过参数，我们可以指定断言的事件响应器。\n\n  </TabItem>\n</Tabs>\n\n当然，如果需要忽略某个事件响应器的响应规则和权限检查，强行进入响应流程，我们可以使用 `should_ignore_rule` 和 `should_ignore_permission` 方法：\n\n```python {21,22} title=tests/test_example.py\nfrom datetime import datetime\n\nimport pytest\nfrom nonebug import App\nfrom nonebot.adapters.console import User, Message, MessageEvent\n\ndef make_event(message: str = \"\") -> MessageEvent:\n    return MessageEvent(\n        time=datetime.now(),\n        self_id=\"test\",\n        message=Message(message),\n        user=User(id=\"user\"),\n    )\n\n@pytest.mark.asyncio\nasync def test_example(app: App):\n    from awesome_bot.plugins.example import foo, bar\n\n    async with app.test_matcher(bar) as ctx:\n        bot = ctx.create_bot()\n        event = make_event(\"/foo\")\n        ctx.receive_event(bot, event)\n        ctx.should_ignore_rule(bar)\n        ctx.should_ignore_permission(bar)\n```\n\n在忽略了响应规则和权限检查之后，就会进入 `bar` 事件响应器的响应流程。\n\n## 测试平台接口使用\n\n上一节的示例插件测试中，我们已经尝试了测试插件对事件的消息回复。通常情况下，事件处理流程中对平台接口的使用会通过事件响应器操作或者调用平台 API 两种途径进行。针对这两种途径，NoneBug 分别提供了 `ctx.should_call_send` 和 `ctx.should_call_api` 方法来测试平台接口的使用情况。\n\n1. `should_call_send`\n\n   定义事件响应器预期发送的消息，即通过[事件响应器操作 send](../../appendices/session-control.mdx#send)进行的操作。`should_call_send` 有四个参数：\n   - `event`：回复的目标事件。\n   - `message`：预期的消息对象，可以是 `str`、`Message` 或 `MessageSegment`。\n   - `result`：send 的返回值，将会返回给插件。\n   - `bot`（可选）：发送消息的 bot 对象。\n   - `**kwargs`：send 方法的额外参数。\n\n2. `should_call_api`\n   定义事件响应器预期调用的平台 API 接口，即通过[调用平台 API](../../appendices/api-calling.mdx#调用平台-api)进行的操作。`should_call_api` 有四个参数：\n   - `api`：API 名称。\n   - `data`：预期的请求数据。\n   - `result`：call_api 的返回值，将会返回给插件。\n   - `adapter`（可选）：调用 API 的平台适配器对象。\n   - `**kwargs`：call_api 方法的额外参数。\n\n下面是一个使用 `should_call_send` 和 `should_call_api` 方法的示例：\n\n我们先定义一个测试插件，在响应流程中向用户发送一条消息并调用 `Console` 适配器的 `bell` API。\n\n```python {8,9} title=example.py\nfrom nonebot import on_command\nfrom nonebot.adapters.console import Bot\n\nfoo = on_command(\"foo\")\n\n@foo.handle()\nasync def _(bot: Bot):\n    await foo.send(\"message\")\n    await bot.bell()\n```\n\n然后我们对该插件进行测试：\n\n```python title=tests/test_example.py\nfrom datetime import datetime\n\nimport pytest\nimport nonebot\nfrom nonebug import App\nfrom nonebot.adapters.console import Bot, User, Adapter, Message, MessageEvent\n\ndef make_event(message: str = \"\") -> MessageEvent:\n    return MessageEvent(\n        time=datetime.now(),\n        self_id=\"test\",\n        message=Message(message),\n        user=User(id=\"user\"),\n    )\n\n@pytest.mark.asyncio\nasync def test_example(app: App):\n    from awesome_bot.plugins.example import foo\n\n    async with app.test_matcher(foo) as ctx:\n        # highlight-start\n        adapter = nonebot.get_adapter(Adapter)\n        bot = ctx.create_bot(base=Bot, adapter=adapter)\n        # highlight-end\n        event = make_event(\"/foo\")\n        ctx.receive_event(bot, event)\n        # highlight-start\n        ctx.should_call_send(event, \"message\", result=None, bot=bot)\n        ctx.should_call_api(\"bell\", {}, result=None, adapter=adapter)\n        # highlight-end\n```\n\n请注意，对于在依赖注入中使用了非基类对象的情况，我们需要在 `create_bot` 方法中指定 `base` 和 `adapter` 参数，确保不会因为重载功能而出现非预期情况。\n\n## 测试会话控制\n\n在[会话控制](../../appendices/session-control.mdx)一节中，我们介绍了如何使用事件响应器操作来实现对用户的交互式会话。在上一节的示例插件测试中，我们其实已经使用了 `ctx.should_finished` 来断言会话结束。NoneBug 针对各种流程控制操作分别提供了相应的方法来定义预期的会话处理行为。它们分别是：\n\n- `should_finished`：断言会话结束，对应 `matcher.finish` 操作。\n- `should_rejected`：断言会话等待用户输入并重新执行当前事件处理函数，对应 `matcher.reject` 系列操作。\n- `should_paused`: 断言会话等待用户输入并执行下一个事件处理函数，对应 `matcher.pause` 操作。\n\n我们仅需在测试用例中的正确位置调用这些方法，就可以断言会话的预期行为。例如：\n\n```python title=example.py\nfrom nonebot import on_command\nfrom nonebot.typing import T_State\n\nfoo = on_command(\"foo\")\n\n@foo.got(\"key\", prompt=\"请输入密码\")\nasync def _(state: T_State, key: str = ArgPlainText()):\n    if key != \"some password\":\n        try_count = state.get(\"try_count\", 1)\n        if try_count >= 3:\n            await foo.finish(\"密码错误次数过多\")\n        else:\n            state[\"try_count\"] = try_count + 1\n            await foo.reject(\"密码错误，请重新输入\")\n    await foo.finish(\"密码正确\")\n```\n\n```python title=tests/test_example.py\nfrom datetime import datetime\n\nimport pytest\nfrom nonebug import App\nfrom nonebot.adapters.console import User, Message, MessageEvent\n\ndef make_event(message: str = \"\") -> MessageEvent:\n    return MessageEvent(\n        time=datetime.now(),\n        self_id=\"test\",\n        message=Message(message),\n        user=User(id=\"user\"),\n    )\n\n@pytest.mark.asyncio\nasync def test_example(app: App):\n    from awesome_bot.plugins.example import foo\n\n    async with app.test_matcher(foo) as ctx:\n        bot = ctx.create_bot()\n        event = make_event(\"/foo\")\n        ctx.receive_event(bot, event)\n        ctx.should_call_send(event, \"请输入密码\", result=None)\n        ctx.should_rejected(foo)\n        event = make_event(\"wrong password\")\n        ctx.receive_event(bot, event)\n        ctx.should_call_send(event, \"密码错误，请重新输入\", result=None)\n        ctx.should_rejected(foo)\n        event = make_event(\"wrong password\")\n        ctx.receive_event(bot, event)\n        ctx.should_call_send(event, \"密码错误，请重新输入\", result=None)\n        ctx.should_rejected(foo)\n        event = make_event(\"wrong password\")\n        ctx.receive_event(bot, event)\n        ctx.should_call_send(event, \"密码错误次数过多\", result=None)\n        ctx.should_finished(foo)\n```\n"
  },
  {
    "path": "website/docs/best-practice/testing/mock-network.md",
    "content": "---\nsidebar_position: 3\ndescription: 模拟网络通信以进行测试\n---\n\n# 模拟网络通信\n\nNoneBot 驱动器提供了多种方法来帮助适配器进行网络通信，主要包括客户端和服务端两种类型。模拟网络通信可以帮助我们更加接近实际机器人应用场景，进行更加真实的集成测试。同时，通过这种途径，我们还可以完成对适配器的测试。\n\nNoneBot 中的网络通信主要包括以下几种：\n\n- HTTP 服务端（WebHook）\n- WebSocket 服务端\n- HTTP 客户端\n- WebSocket 客户端\n\n下面我们将分别介绍如何使用 NoneBug 来模拟这几种通信方式。\n\n## 测试 HTTP 服务端\n\n当 NoneBot 作为 ASGI 服务端应用时，我们可以定义一系列的路由来处理 HTTP 请求，适配器同样也可以通过定义路由来响应机器人相关的网络通信。下面假设我们使用了一个适配器 `fake` ，它定义了一个路由 `/fake/http` ，用于接收平台 WebHook 并处理。实际应用测试时，应将该路由地址替换为**真实适配器注册的路由地址**。\n\n我们首先需要获取测试用模拟客户端：\n\n```python {5,6} title=tests/test_http_server.py\nfrom nonebug import App\n\n@pytest.mark.asyncio\nasync def test_http_server(app: App):\n    async with app.test_server() as ctx:\n        client = ctx.get_client()\n```\n\n默认情况下，`app.test_server()` 会通过 `nonebot.get_asgi` 获取测试对象，我们也可以通过参数指定 ASGI 应用：\n\n```python\nasync with app.test_server(asgi=asgi_app) as ctx:\n    ...\n```\n\n获取到模拟客户端后，即可像 `requests`、`httpx` 等库类似的方法进行使用：\n\n```python {3,11-14,16} title=tests/test_http_server.py\nimport nonebot\nfrom nonebug import App\nfrom nonebot.adapters.fake import Adapter\n\n@pytest.mark.asyncio\nasync def test_http_server(app: App):\n    adapter = nonebot.get_adapter(Adapter)\n\n    async with app.test_server() as ctx:\n        client = ctx.get_client()\n        response = await client.post(\"/fake/http\", json={\"bot_id\": \"fake\"})\n        assert response.status_code == 200\n        assert response.json() == {\"status\": \"success\"}\n        assert \"fake\" in nonebot.get_bots()\n\n    adapter.bot_disconnect(nonebot.get_bot(\"fake\"))\n```\n\n在上面的测试中，我们向 `/fake/http` 发送了一个模拟 POST 请求，适配器将会对该请求进行处理，我们可以通过检查请求返回是否正确、Bot 对象是否创建等途径来验证机器人是否正确运行。在完成测试后，我们通常需要对 Bot 对象进行清理，以避免对其他测试产生影响。\n\n## 测试 WebSocket 服务端\n\n当 NoneBot 作为 ASGI 服务端应用时，我们还可以定义一系列的路由来处理 WebSocket 通信。下面假设我们使用了一个适配器 `fake` ，它定义了一个路由 `/fake/ws` ，用于处理平台 WebSocket 连接信息。实际应用测试时，应将该路由地址替换为**真实适配器注册的路由地址**。\n\n我们同样需要通过 `app.test_server()` 获取测试用模拟客户端，这里就不再赘述。在获取到模拟客户端后，我们可以通过 `client.websocket_connect` 方法来模拟 WebSocket 连接：\n\n```python {3,11-15} title=tests/test_ws_server.py\nimport nonebot\nfrom nonebug import App\nfrom nonebot.adapters.fake import Adapter\n\n@pytest.mark.asyncio\nasync def test_ws_server(app: App):\n    adapter = nonebot.get_adapter(Adapter)\n\n    async with app.test_server() as ctx:\n        client = ctx.get_client()\n        async with client.websocket_connect(\"/fake/ws\") as ws:\n            await ws.send_json({\"bot_id\": \"fake\"})\n            response = await ws.receive_json()\n            assert response == {\"status\": \"success\"}\n            assert \"fake\" in nonebot.get_bots()\n```\n\n在上面的测试中，我们向 `/fake/ws` 进行了 WebSocket 模拟通信，通过发送消息与机器人进行交互，然后检查机器人发送的信息是否正确。\n\n## 测试 HTTP 客户端\n\n~~暂不支持~~\n\n## 测试 WebSocket 客户端\n\n~~暂不支持~~\n"
  },
  {
    "path": "website/docs/community/contact.md",
    "content": "---\nsidebar-position: 0\ndescription: 遇到问题如何获取帮助\n---\n\n# 参与讨论\n\n如果在安装或者开发 NoneBot 过程中遇到了任何问题，或者有新奇的点子，欢迎参与我们的社区讨论：\n\n1. 点击下方链接前往 GitHub，前往 Issues 页面，在 `New Issue` Template 中选择 `Question`\n\n   NoneBot：[![NoneBot project link](https://img.shields.io/github/stars/nonebot/nonebot2?style=social)](https://github.com/nonebot/nonebot2)\n\n2. 通过 QQ 群（点击下方链接直达）\n\n   [![QQ Chat Group](https://img.shields.io/badge/QQ%E7%BE%A4-768887710-orange?style=social)](https://jq.qq.com/?_wv=1027&k=5OFifDh)\n\n3. 通过 QQ 频道\n\n   [![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)\n\n4. 通过 Discord 服务器（点击下方链接直达）\n\n   [![Discord Server](https://discordapp.com/api/guilds/847819937858584596/widget.png?style=shield)](https://discord.gg/VKtE6Gdc4h)\n"
  },
  {
    "path": "website/docs/community/contributing.md",
    "content": "---\nsidebar-position: 1\ndescription: 如何为 NoneBot 贡献代码\n---\n\n# 贡献指南\n\n## Code of Conduct\n\n请参阅 [Code of Conduct](https://github.com/nonebot/nonebot2/blob/master/CODE_OF_CONDUCT.md)。\n\n## 参与开发\n\n请参阅 [Contributing](https://github.com/nonebot/nonebot2/blob/master/CONTRIBUTING.md)。\n\n## 鸣谢\n\n感谢以下开发者对 NoneBot2 作出的贡献：\n\n<a href=\"https://github.com/nonebot/nonebot2/graphs/contributors\">\n  <img src=\"https://contrib.rocks/image?repo=nonebot/nonebot2&max=1000\" />\n</a>\n"
  },
  {
    "path": "website/docs/developer/adapter-writing.md",
    "content": "---\nsidebar_position: 1\ndescription: 编写适配器对接新的平台\n---\n\n# 编写适配器\n\n在编写适配器之前，我们需要先了解[适配器的功能与组成](../advanced/adapter#适配器功能与组成)，适配器通常由 `Adapter`、`Bot`、`Event` 和 `Message` 四个部分组成，在编写适配器时，我们需要继承 NoneBot 中的基类，并根据实际平台来编写每个部分功能。\n\n## 组织结构\n\nNoneBot 适配器项目通常以 `nonebot-adapter-{adapter-name}` 作为项目名，并以**命名空间包**的形式编写，即在 `nonebot/adapters/{adapter-name}` 目录中编写实际代码，例如：\n\n```tree\n📦 nonebot-adapter-{adapter-name}\n├── 📂 nonebot\n│   ├── 📂 adapters\n│   │   ├── 📂 {adapter-name}\n│   │   │   ├── 📜 __init__.py\n│   │   │   ├── 📜 adapter.py\n│   │   │   ├── 📜 bot.py\n│   │   │   ├── 📜 config.py\n│   │   │   ├── 📜 event.py\n│   │   │   └── 📜 message.py\n├── 📜 pyproject.toml\n└── 📜 README.md\n```\n\n:::tip 提示\n\n上述的项目结构仅作推荐，不做强制要求，保证实际可用性即可。\n\n:::\n\n### 使用 NB-CLI 创建项目\n\n我们可以使用脚手架快速创建项目：\n\n```shell\nnb adapter create\n```\n\n按照指引，输入适配器名称以及存储位置，即可创建一个带有基本结构的适配器项目。\n\n## 组成部分\n\n:::tip 提示\n\n本章节的代码中提到的 `Adapter`、`Bot`、`Event` 和 `Message` 等，均为下文中适配器所编写的类，而非 NoneBot 中的基类。\n\n:::\n\n### Log\n\n适配器在处理时通常需要打印日志，但直接使用 NoneBot 的默认 `logger` 不方便区分适配器输出和其它日志。因此我们可以使用 NoneBot 提供的 `logger_wrapper` 方法，自定义一个 `log` 函数用于快捷打印适配器日志：\n\n```python {3} title=log.py\nfrom nonebot.utils import logger_wrapper\n\nlog = logger_wrapper(\"your_adapter_name\")\n```\n\n这个 `log` 函数会在默认 `logger` 中添加适配器名称前缀，它接收三个参数：日志等级、日志内容以及可选的异常，具体用法如下：\n\n```python\nfrom .log import log\n\nlog(\"DEBUG\", \"A DEBUG log.\")\nlog(\"INFO\", \"A INFO log.\")\n\ntry:\n    ...\nexcept Exception as e:\n    log(\"ERROR\", \"something error.\", e)\n```\n\n### Config\n\n通常适配器需要一些配置项，例如平台连接密钥等。适配器的配置方法与[插件配置](../appendices/config#%E6%8F%92%E4%BB%B6%E9%85%8D%E7%BD%AE)类似，例如：\n\n```python title=config.py\nfrom pydantic import BaseModel\n\nclass Config(BaseModel):\n    xxx_id: str\n    xxx_token: str\n```\n\n配置项的读取将在下方 [Adapter](#adapter) 中介绍。\n\n### Adapter\n\nAdapter 负责转换事件、调用接口，以及正确创建 Bot 对象并注册到 NoneBot 中。在编写平台相关内容之前，我们需要继承基类，并实现适配器的基本信息：\n\n```python {9,11,14,18} title=adapter.py\nfrom typing import Any\nfrom typing_extensions import override\n\nfrom nonebot.drivers import Driver\nfrom nonebot import get_plugin_config\nfrom nonebot.adapters import Adapter as BaseAdapter\n\nfrom .config import Config\n\nclass Adapter(BaseAdapter):\n    @override\n    def __init__(self, driver: Driver, **kwargs: Any):\n        super().__init__(driver, **kwargs)\n        # 读取适配器所需的配置项\n        self.adapter_config: Config = get_plugin_config(Config)\n\n    @classmethod\n    @override\n    def get_name(cls) -> str:\n        \"\"\"适配器名称\"\"\"\n        return \"your_adapter_name\"\n```\n\n#### 与平台交互\n\nNoneBot 提供了多种 [Driver](../advanced/driver) 来帮助适配器进行网络通信，主要分为客户端和服务端两种类型。我们需要**根据平台文档和特性**选择合适的通信方式，并编写相关方法用于初始化适配器，与平台建立连接和进行交互：\n\n##### 客户端通信方式\n\n```python {12,23,24} title=adapter.py\nimport asyncio\nfrom typing_extensions import override\n\nfrom nonebot import get_plugin_config\nfrom nonebot.exception import WebSocketClosed\nfrom nonebot.drivers import Request, WebSocketClientMixin\n\nclass Adapter(BaseAdapter):\n    @override\n    def __init__(self, driver: Driver, **kwargs: Any):\n        super().__init__(driver, **kwargs)\n        self.adapter_config: Config = get_plugin_config(Config)\n        self.task: Optional[asyncio.Task] = None  # 存储 ws 任务\n        self.setup()\n\n    def setup(self) -> None:\n        if not isinstance(self.driver, WebSocketClientMixin):\n            # 判断用户配置的Driver类型是否符合适配器要求，不符合时应抛出异常\n            raise RuntimeError(\n                f\"Current driver {self.config.driver} doesn't support websocket client connections!\"\n                f\"{self.get_name()} Adapter need a WebSocket Client Driver to work.\"\n            )\n        # 在 NoneBot 启动和关闭时进行相关操作\n        self.driver.on_startup(self.startup)\n        self.driver.on_shutdown(self.shutdown)\n\n    async def startup(self) -> None:\n        \"\"\"定义启动时的操作，例如和平台建立连接\"\"\"\n        self.task = asyncio.create_task(self._forward_ws())  # 建立 ws 连接\n\n    async def _forward_ws(self):\n        request = Request(\n            method=\"GET\",\n            url=\"your_platform_websocket_url\",\n            headers={\"token\": \"...\"},  # 鉴权请求头\n        )\n        while True:\n            try:\n                async with self.websocket(request) as ws:\n                    try:\n                        # 处理 websocket\n                        ...\n                    except WebSocketClosed as e:\n                        log(\n                            \"ERROR\",\n                            \"<r><bg #f8bbd0>WebSocket Closed</bg #f8bbd0></r>\",\n                            e,\n                        )\n                    except Exception as e:\n                        log(\n                            \"ERROR\",\n                            \"<r><bg #f8bbd0>Error while process data from \"\n                            \"websocket platform_websocket_url. \"\n                            \"Trying to reconnect...</bg #f8bbd0></r>\",\n                            e,\n                        )\n                    finally:\n                        # 这里要断开 Bot 连接\n            except Exception as e:\n                # 尝试重连\n                log(\n                    \"ERROR\",\n                    \"<r><bg #f8bbd0>Error while setup websocket to \"\n                    \"platform_websocket_url. Trying to reconnect...</bg #f8bbd0></r>\",\n                    e,\n                )\n                await asyncio.sleep(3)  # 重连间隔\n\n    async def shutdown(self) -> None:\n        \"\"\"定义关闭时的操作，例如停止任务、断开连接\"\"\"\n\n        # 断开 ws 连接\n        if self.task is not None and not self.task.done():\n            self.task.cancel()\n```\n\n##### 服务端通信方式\n\n```python {30,38} title=adapter.py\nfrom nonebot import get_plugin_config\nfrom nonebot.drivers import (\n    Request,\n    ASGIMixin,\n    WebSocket,\n    HTTPServerSetup,\n    WebSocketServerSetup\n)\n\nclass Adapter(BaseAdapter):\n    @override\n    def __init__(self, driver: Driver, **kwargs: Any):\n        super().__init__(driver, **kwargs)\n        self.adapter_config: Config = get_plugin_config(Config)\n        self.setup()\n\n    def setup(self) -> None:\n        if not isinstance(self.driver, ASGIMixin):\n            raise RuntimeError(\n                f\"Current driver {self.config.driver} doesn't support asgi server!\"\n                f\"{self.get_name()} Adapter need a asgi server driver to work.\"\n            )\n        # 建立服务端路由\n        # HTTP Webhook 路由\n        http_setup = HTTPServerSetup(\n            URL(\"your_webhook_url\"),  # 路由地址\n            \"POST\",  # 接收的方法\n            \"WEBHOOK name\",  # 路由名称\n            self._handle_http,  # 处理函数\n        )\n        self.setup_http_server(http_setup)\n\n        # 反向 Websocket 路由\n        ws_setup = WebSocketServerSetup(\n            URL(\"your_websocket_url\"),  # 路由地址\n            \"WebSocket name\",  # 路由名称\n            self._handle_ws,  # 处理函数\n        )\n        self.setup_websocket_server(ws_setup)\n\n\n    async def _handle_http(self, request: Request) -> Response:\n        \"\"\"HTTP 路由处理函数，只有一个类型为 Request 的参数，且返回值类型为 Response\"\"\"\n        ...\n        return Response(\n            status_code=200,  # 状态码\n            headers={\"something\": \"something\"},  # 响应头\n            content=\"xxx\",  # 响应内容\n        )\n\n    async def _handle_ws(self, websocket: WebSocket) -> Any:\n        \"\"\"WebSocket 路由处理函数，只有一个类型为 WebSocket 的参数\"\"\"\n        ...\n```\n\n更多通信交互方式可以参考以下适配器：\n\n- [OneBot](https://github.com/nonebot/adapter-onebot/blob/master/nonebot/adapters/onebot/v11/adapter.py) - `WebSocket 客户端`、`WebSocket 服务端`、`HTTP WEBHOOK`、`HTTP POST`\n- [QQ](https://github.com/nonebot/adapter-qq/blob/master/nonebot/adapters/qq/adapter.py) - `WebSocket 服务端`、`HTTP WEBHOOK`\n- [Telegram](https://github.com/nonebot/adapter-telegram/blob/beta/nonebot/adapters/telegram/adapter.py) - `HTTP WEBHOOK`\n\n#### 建立 Bot 连接\n\n在与平台建立连接后，我们需要将 [Bot](#bot) 实例化，并调用适配器提供的的 `bot_connect` 方法告知 NoneBot 建立了 Bot 连接。在与平台断开连接或出现某些异常进行重连时，我们需要调用 `bot_disconnect` 方法告知 NoneBot 断开了 Bot 连接。\n\n```python {7,8,11} title=adapter.py\nfrom .bot import Bot\n\nclass Adapter(BaseAdapter):\n\n    def _handle_connect(self):\n        bot_id = ...  # 通过配置或者平台 API 等方式，获取到 Bot 的 ID\n        bot = Bot(self, self_id=bot_id)  # 实例化 Bot\n        self.bot_connect(bot)  # 建立 Bot 连接\n\n    def _handle_disconnect(self):\n        self.bot_disconnect(bot)  # 断开 Bot 连接\n```\n\n#### 转换 Event 事件\n\n在接收到来自平台的事件数据后，我们需要将其转为适配器的 [Event](#event)，并调用 Bot 的 `handle_event` 方法来让 Bot 对事件进行处理：\n\n```python title=adapter.py\nimport asyncio\nfrom typing import Any, Dict\n\nfrom nonebot.compat import type_validate_python\n\nfrom .bot import Bot\nfrom .event import Event\nfrom .log import log\n\nclass Adapter(BaseAdapter):\n\n    @classmethod\n    def payload_to_event(cls, payload: Dict[str, Any]) -> Event:\n        \"\"\"根据平台事件的特性，转换平台 payload 为具体 Event\n\n        Event 模型继承自 pydantic.BaseModel，具体请参考 pydantic 文档\n        \"\"\"\n\n        # 做一层异常处理，以应对平台事件数据的变更\n        try:\n            return type_validate_python(your_event_class, payload)\n        except Exception as e:\n            # 无法正常解析为具体 Event 时，给出日志提示\n            log(\n                \"WARNING\",\n                f\"Parse event error: {str(payload)}\",\n            )\n            # 也可以尝试转为基础 Event 进行处理\n            return type_validate_python(Event, payload)\n\n\n    async def _forward(self, bot: Bot):\n\n        payload: Dict[str, Any]  # 接收到的事件数据\n        event = self.payload_to_event(payload)\n        # 让 bot 对事件进行处理\n        asyncio.create_task(bot.handle_event(event))\n```\n\n#### 调用平台 API\n\n我们需要实现 `Adapter` 的 `_call_api` 方法，使开发者能够调用平台提供的 API。如果通过 WebSocket 通信可以通过 `send` 方法来发送数据，如果采用 HTTP 请求，则需要通过 NoneBot 提供的 `Request` 对象，调用 `driver` 的 `request` 方法来发送请求。\n\n```python {11} title=adapter.py\nfrom typing import Any\nfrom typing_extensions import override\n\nfrom nonebot.drivers import Request, WebSocket\n\nfrom .bot import Bot\n\nclass Adapter(BaseAdapter):\n\n    @override\n    async def _call_api(self, bot: Bot, api: str, **data: Any) -> Any:\n        log(\"DEBUG\", f\"Calling API <y>{api}</y>\")  # 给予日志提示\n        platform_data = your_handle_data_method(data)  # 自行将数据转为平台所需要的格式\n\n        # 采用 HTTP 请求的方式，需要构造一个 Request 对象\n        request = Request(\n            method=\"GET\",  # 请求方法\n            url=api,  # 接口地址\n            headers=...,  # 请求头，通常需要包含鉴权信息\n            params=platform_data,  # 自行处理数据的传输形式\n            # json=platform_data,\n            # data=platform_data,\n        )\n        # 发送请求，返回结果\n        return await self.driver.request(request)\n\n\n        # 采用 WebSocket 通信的方式，可以直接调用 send 方法发送数据\n        # 通过某种方式获取到 bot 对应的 websocket 对象\n        ws: WebSocket = your_get_websocket_method(bot.self_id)\n\n        await ws.send_text(platform_data)  # 发送 str 类型的数据\n        await ws.send_bytes(platform_data)  # 发送 bytes 类型的数据\n        await ws.send(platform_data)  # 是以上两种方式的合体\n\n        # 接收并返回结果，同样的，也有 str 和 bytes 的区别\n        return await ws.receive_text()\n        return await ws.receive_bytes()\n        return await ws.receive()\n```\n\n`调用平台 API` 实现方式具体可以参考以下适配器：\n\nWebsocket:\n\n- [OneBot V11](https://github.com/nonebot/adapter-onebot/blob/54270edbbdb2a71332d744f90b1a3d7f4bf6463a/nonebot/adapters/onebot/v11/adapter.py#L167-L177)\n- [OneBot V12](https://github.com/nonebot/adapter-onebot/blob/54270edbbdb2a71332d744f90b1a3d7f4bf6463a/nonebot/adapters/onebot/v12/adapter.py#L204-L218)\n\nHTTP:\n\n- [OneBot V11](https://github.com/nonebot/adapter-onebot/blob/54270edbbdb2a71332d744f90b1a3d7f4bf6463a/nonebot/adapters/onebot/v11/adapter.py#L179-L215)\n- [OneBot V12](https://github.com/nonebot/adapter-onebot/blob/54270edbbdb2a71332d744f90b1a3d7f4bf6463a/nonebot/adapters/onebot/v12/adapter.py#L220-L266)\n- [QQ](https://github.com/nonebot/adapter-qq/blob/dc5d437e101f0e3db542de3300758a035ed7036e/nonebot/adapters/qq/adapter.py#L599-L605)\n- [Telegram](https://github.com/nonebot/adapter-telegram/blob/4a8633627e619245516767f5503dec2f58fe2193/nonebot/adapters/telegram/adapter.py#L148-L253)\n- [飞书](https://github.com/nonebot/adapter-feishu/blob/f8ab05e6d57a5e9013b944b0d019ca777725dfb0/nonebot/adapters/feishu/adapter.py#L201-L218)\n\n### Bot\n\nBot 是机器人开发者能够直接获取并使用的核心对象，负责存储平台机器人相关信息，并提供回复事件、调用 API 的上层方法。我们需要继承基类 `Bot`，并实现相关方法：\n\n```python {20,25,34} title=bot.py\nfrom typing import TYPE_CHECKING, Any, Union\nfrom typing_extensions import override\n\nfrom nonebot.message import handle_event\nfrom nonebot.adapters import Bot as BaseBot\n\nfrom .event import Event\nfrom .message import Message, MessageSegment\n\nif TYPE_CHECKING:\n    from .adapter import Adapter\n\n\nclass Bot(BaseBot):\n    \"\"\"\n    your_adapter_name 协议 Bot 适配。\n    \"\"\"\n\n    @override\n    def __init__(self, adapter: Adapter, self_id: str, **kwargs: Any):\n        super().__init__(adapter, self_id)\n        self.adapter: Adapter = adapter\n        # 一些有关 Bot 的信息也可以在此定义和存储\n\n    async def handle_event(self, event: Event):\n        # 根据需要，对事件进行某些预处理，例如：\n        # 检查事件是否和机器人有关操作，去除事件消息首尾的 @bot\n        # 检查事件是否有回复消息，调用平台 API 获取原始消息的消息内容\n        ...\n        # 调用 handle_event 让 NoneBot 对事件进行处理\n        await handle_event(self, event)\n\n    @override\n    async def send(\n        self,\n        event: Event,\n        message: Union[str, Message, MessageSegment],\n        **kwargs: Any,\n    ) -> Any:\n        # 根据平台实现 Bot 回复事件的方法\n\n        # 将消息处理为平台所需的格式后，调用发送消息接口进行发送，例如：\n        data = message_to_platform_data(message)\n        await self.send_message(\n            data=data,\n            ...\n        )\n```\n\n### Event\n\nEvent 是 NoneBot 中的事件主体对象，所有平台消息在进入处理流程前需要转换为 NoneBot 事件。我们需要继承基类 `Event`，并实现相关方法：\n\n```python {5,8,13,18,23,28,33} title=event.py\nfrom typing_extensions import override\n\nfrom nonebot.compat import model_dump\nfrom nonebot.adapters import Event as BaseEvent\n\nclass Event(BaseEvent):\n\n    @override\n    def get_event_name(self) -> str:\n        # 返回事件的名称，用于日志打印\n        return \"event name\"\n\n    @override\n    def get_event_description(self) -> str:\n        # 返回事件的描述，用于日志打印，请注意转义 loguru tag\n        return escape_tag(repr(model_dump(self)))\n\n    @override\n    def get_message(self):\n        # 获取事件消息的方法，根据事件具体实现，如果事件非消息类型事件，则抛出异常\n        raise ValueError(\"Event has no message!\")\n\n    @override\n    def get_user_id(self) -> str:\n        # 获取用户 ID 的方法，根据事件具体实现，如果事件没有用户 ID，则抛出异常\n        raise ValueError(\"Event has no context!\")\n\n    @override\n    def get_session_id(self) -> str:\n        # 获取事件会话 ID 的方法，根据事件具体实现，如果事件没有相关 ID，则抛出异常\n        raise ValueError(\"Event has no context!\")\n\n    @override\n    def is_tome(self) -> bool:\n        # 判断事件是否和机器人有关\n        return False\n```\n\n然后根据平台消息的类型，编写各种不同的事件，并且注意要根据事件类型实现 `get_type` 方法，具体请参考[事件类型](../advanced/adapter#事件类型)。消息类型事件还应重写 `get_message` 和 `get_user_id` 等方法，例如：\n\n```python {7,16,20,25,34,42} title=event.py\nfrom .message import Message\n\nclass HeartbeatEvent(Event):\n    \"\"\"心跳时间，通常为元事件\"\"\"\n\n    @override\n    def get_type(self) -> str:\n        return \"meta_event\"\n\nclass MessageEvent(Event):\n    \"\"\"消息事件\"\"\"\n    message_id: str\n    user_id: str\n\n    @override\n    def get_type(self) -> str:\n        return \"message\"\n\n    @override\n    def get_message(self) -> Message:\n        # 返回事件消息对应的 NoneBot Message 对象\n        return self.message\n\n    @override\n    def get_user_id(self) -> str:\n        return self.user_id\n\nclass JoinRoomEvent(Event):\n    \"\"\"加入房间事件，通常为通知事件\"\"\"\n    user_id: str\n    room_id: str\n\n    @override\n    def get_type(self) -> str:\n        return \"notice\"\n\nclass ApplyAddFriendEvent(Event):\n    \"\"\"申请添加好友事件，通常为请求事件\"\"\"\n    user_id: str\n\n    @override\n    def get_type(self) -> str:\n        return \"request\"\n```\n\n### Message\n\nMessage 负责正确序列化消息，以便机器人插件处理。我们需要继承 `MessageSegment` 和 `Message` 两个类，并实现相关方法：\n\n```python {9,12,17,22,27,30,36} title=message.py\nfrom typing import Type, Iterable\nfrom typing_extensions import override\n\nfrom nonebot.utils import escape_tag\n\nfrom nonebot.adapters import Message as BaseMessage\nfrom nonebot.adapters import MessageSegment as BaseMessageSegment\n\nclass MessageSegment(BaseMessageSegment[\"Message\"]):\n    @classmethod\n    @override\n    def get_message_class(cls) -> Type[\"Message\"]:\n        # 返回适配器的 Message 类型本身\n        return Message\n\n    @override\n    def __str__(self) -> str:\n        # 返回该消息段的纯文本表现形式，通常在日志中展示\n        return \"text of MessageSegment\"\n\n    @override\n    def is_text(self) -> bool:\n        # 判断该消息段是否为纯文本\n        return self.type == \"text\"\n\n\nclass Message(BaseMessage[MessageSegment]):\n    @classmethod\n    @override\n    def get_segment_class(cls) -> Type[MessageSegment]:\n        # 返回适配器的 MessageSegment 类型本身\n        return MessageSegment\n\n    @staticmethod\n    @override\n    def _construct(msg: str) -> Iterable[MessageSegment]:\n        # 实现从字符串中构造消息数组，如无字符串嵌入格式可直接返回文本类型 MessageSegment\n        ...\n```\n\n然后根据平台具体的消息类型，来实现各种 `MessageSegment` 消息段，具体可以参考以下适配器：\n\n- [OneBot V11](https://github.com/nonebot/adapter-onebot/blob/54270edbbdb2a71332d744f90b1a3d7f4bf6463a/nonebot/adapters/onebot/v11/message.py#L25-L259)\n- [QQ](https://github.com/nonebot/adapter-qq/blob/dc5d437e101f0e3db542de3300758a035ed7036e/nonebot/adapters/qq/message.py#L30-L520)\n- [Telegram](https://github.com/nonebot/adapter-telegram/blob/4a8633627e619245516767f5503dec2f58fe2193/nonebot/adapters/telegram/message.py#L13-L414)\n\n## 适配器测试\n\n关于适配器测试相关内容在这里不再展开，开发者可以根据需要进行合适的测试。这里为开发者提供几个常见问题的解决方法：\n\n1. 在测试中无法导入 editable 模式安装的适配器代码。在 pytest 的 `conftest.py` 内添加如下代码：\n\n   ```python title=tests/conftest.py\n   from pathlib import Path\n   import nonebot.adapters\n   nonebot.adapters.__path__.append(  # type: ignore\n       str((Path(__file__).parent.parent / \"nonebot\" / \"adapters\").resolve())\n   )\n   ```\n\n2. 需要计算适配器测试覆盖率，请在 `pyproject.toml` 中添加 pytest 配置：\n\n   ```toml title=pyproject.toml\n   [tool.pytest.ini_options]\n   addopts = \"--cov nonebot/adapters/{adapter-name} --cov-report term-missing\"\n   ```\n\n## 后续工作\n\n在完成适配器代码的编写后，如果想要将适配器发布到 NoneBot 商店，我们需要将适配器发布到 PyPI 中，然后前往[商店](/store/adapters)页面，切换到适配器页签，点击**发布适配器**按钮，填写适配器相关信息并提交。\n\n另外建议编写适配器文档或者一些插件开发示例，以便其他开发者使用我们的适配器。\n"
  },
  {
    "path": "website/docs/developer/plugin-publishing.mdx",
    "content": "---\nsidebar_position: 0\ndescription: 在商店发布自己的插件\n---\n\n# 发布插件\n\nimport Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\n\nNoneBot 为开发者提供了分享插件的官方商店。本指南囊括**从创建项目到发布到 PyPI，最终提交商店审核**的全过程。\n\n:::warning 警告\n如果你的插件只是满足自用需求，则完全可以选择**不发布插件**。发布插件**不是**使用插件的必要条件。\n\nNoneBot 社区对于插件有一定质量要求，对于不符合要求的插件，社区成员将会要求修改，直至符合要求后才能通过审核；如果长期未更新修改，社区将会关闭当前请求，之后如需发布请重新提交发布插件请求。相应的要求会在本章节以下部分介绍。\n:::\n\n:::tip 提示\n本章节仅包含插件发布流程指导，插件开发请查阅前述章节。\n:::\n\n## 准备工作\n\n### 插件命名规范\n\nNoneBot 插件使用下述命名规范：\n\n- 对于**项目名**，建议统一以 `nonebot-plugin-` 开头，之后为拟定的插件名字，词间用横杠 `-` 分隔；\n  - **项目名**用于代码仓库名称、PyPI 包的发布名称等；\n  - 本文使用 `nonebot-plugin-{your-plugin-name}` 为例。\n- 对于**模块名**，建议与**项目名**一致，但词间用下划线 `_` 分隔，即统一以 `nonebot_plugin_` 开头，之后为拟定的插件名字；\n  - **模块名**用于程序导入使用，应为插件文件（夹）的名称；\n  - 本文使用 `nonebot_plugin_{your_plugin_name}` 为例。\n\n### 项目结构\n\n:::tip 提示\n本段所述的项目结构仅作推荐，不做强制要求。\n:::\n\n插件程序本身结构可参考[插件结构](../tutorial/create-plugin.md#插件结构)一节，唯一区别在于，插件包可以直接处于项目顶层。\n\n插件项目的一种组织结构如下：\n\n```tree\n📦 nonebot-plugin-{your-plugin-name}\n├── 📂 nonebot_plugin_{your_plugin_name}\n│   ├── 📜 __init__.py\n│   └── 📜 config.py\n├── 📜 pyproject.toml\n└── 📜 README.md\n```\n\n功能开发可以在 `__init__.py` 中进行或在包内部创建其他模块并在 `__init__.py` 中导入。\n\n### 从项目模板开始\n\n为降低新手门槛，我们提供三条清晰、完整、可复制的发布路径。\n\n:::tip 提示\n你只需选择一条与你习惯一致的路径，**完整跟随即可成功发布**。无需在不同工具间切换或猜测配置。\n:::\n\nNoneBot 生态目前有如下插件项目模板：\n\n- [RF-Tar-Railt/nonebot-plugin-template](https://github.com/RF-Tar-Railt/nonebot-plugin-template)\n\n  此路径使用 **PDM** 项目管理器，符合 PEP 621 标准，自动化程度高。\n\n- [fllesser/nonebot-plugin-template](https://github.com/fllesser/nonebot-plugin-template)\n\n  此路径使用 **uv** 项目管理器和 **PoeThePoet** 任务运行器，构建速度快，适合追求效率的开发者。\n\n- [A-kirami/nonebot-plugin-template](https://github.com/A-kirami/nonebot-plugin-template)\n\n  此路径使用 **Poetry** 项目管理器，适合熟悉传统 Python 生态的开发者。\n\n#### 1. 创建项目\n\n1. 访问上述三个模板之一。\n2. 点击 **“Use this template”** → **“Create a new repository”**。\n3. 仓库名称填写：`nonebot-plugin-{your-plugin-name}`（此部分以 `nonebot-plugin-weather` 为例）。\n4. 点击 **“Create repository from template”**。\n\n#### 2. 配置发布权限\n\n1. 进入新仓库 → **Settings** → **Actions** → **General**。\n2. 在 **Workflow permissions** 下，勾选 **“Read and write permissions”** → 点击 **Save**。\n\n#### 3. 全局替换项目信息\n\n在仓库中点击 **“Add file”** → **“Create new file”**，创建一个空文件 `LICENSE`，选择开源协议并提交（此操作会触发工作流）。\n\n然后在本地克隆仓库，使用编辑器对以下内容进行**全局替换**：\n\n:::tip 提示\n此部分以“天气插件”为例，实际的替换内容应该根据你所创建的插件名称等相应调整。\n:::\n\n| 原内容                         | 替换为                             |\n| ------------------------------ | ---------------------------------- |\n| `nonebot-plugin-template`      | `nonebot-plugin-weather`           |\n| `nonebot_plugin_template`      | `nonebot_plugin_weather`           |\n| `<your_plugin_humanized_name>` | `天气查询`                         |\n| `<your_plugin_description>`    | `查询指定城市的实时天气与未来预报` |\n| `<your_github>`                | `你的GitHub用户名`                 |\n| `<your_email>`                 | `你的邮箱`                         |\n\n#### 4. 安装依赖与开发\n\n<Tabs groupId=\"publish-path\" defaultValue=\"pdm\" values={[\n  {label: 'PDM + RF-Tar-Railt 模板', value: 'pdm'},\n  {label: 'uv + fllesser 模板', value: 'uv'},\n  {label: 'Poetry + A-kirami 模板', value: 'poetry'},\n]}>\n  <TabItem value=\"pdm\" label=\"PDM + RF-Tar-Railt 模板\">\n\n```bash\n# 安装 PDM（若未安装）\ncurl -sSL https://pdm-project.org/install-pdm.py | python3 -\n\n# 安装项目依赖（自动创建虚拟环境）\npdm sync\n\n# 添加新依赖（如 httpx）\npdm add httpx\n```\n\n</TabItem>\n\n  <TabItem value=\"uv\" label=\"uv + fllesser 模板\">\n\n```bash\n# 安装 uv（Windows）\npowershell -c \"irm https://astral.sh/uv/install.ps1 | iex\"\n\n# 安装 uv（macOS/Linux）\ncurl -LsSf https://astral.sh/uv/install.sh | sh\n\n# 安装所有依赖（含 dev）\nuv sync --all-groups -p 3.12\n\n# 添加新依赖\nuv add httpx\n```\n\n</TabItem>\n\n  <TabItem value=\"poetry\" label=\"Poetry + A-kirami 模板\">\n\n```bash\n# 安装 Poetry（推荐方式）\ncurl -sSL https://install.python-poetry.org | python3 -\n\n# 安装项目依赖\npoetry install\n\n# 添加新依赖\npoetry add httpx\n```\n\n</TabItem>\n</Tabs>\n\n#### 5. 更新版本并发布\n\n<Tabs\n  groupId=\"publish-path-bump\"\n  defaultValue=\"bump-my-version\"\n  values={[\n    { label: \"使用 bump-my-version\", value: \"bump-my-version\" },\n    { label: \"使用项目管理器\", value: \"bump-manager\" },\n    { label: \"手动更新版本\", value: \"bump-manual\" },\n  ]}\n>\n  <TabItem value=\"bump-my-version\" label=\"使用 bump-my-version\">\n\n[bump-my-version](https://github.com/callowayproject/bump-my-version) 是一个功能强大、可配置的 Python 项目版本更新工具，支持自动提交到 Git 等 VCS。\n\n<Tabs groupId=\"publish-path\" defaultValue=\"pdm\" values={[\n  {label: 'PDM + RF-Tar-Railt 模板', value: 'pdm'},\n  {label: 'uv + fllesser 模板', value: 'uv'},\n  {label: 'Poetry + A-kirami 模板', value: 'poetry'},\n]}>\n  <TabItem value=\"pdm\" label=\"PDM + RF-Tar-Railt 模板\">\n\n```bash\n# 安装 bump-my-version\npdm add --dev bump-my-version\n\n# 更新 patch 版本\npdm run bump patch\n\n# 推送 tag 触发发布\ngit push origin --tags\n```\n\n</TabItem>\n\n  <TabItem value=\"uv\" label=\"uv + fllesser 模板\">\n\n```bash\n# 更新 patch 版本\nuv run poe bump patch\n\n# 推送 tag 触发发布\ngit push origin --tags\n```\n\n</TabItem>\n\n  <TabItem value=\"poetry\" label=\"Poetry + A-kirami 模板\">\n\n```bash\n# 安装 bump-my-version\npoetry add --dev bump-my-version\n\n# 更新 patch 版本\npoetry run bump patch\n\n# 推送 tag 触发发布\ngit push origin --tags\n```\n\n</TabItem>\n</Tabs>\n\n</TabItem>\n  <TabItem value=\"bump-manager\" label=\"使用项目管理器\">\n\n<Tabs groupId=\"publish-path\" defaultValue=\"pdm\" values={[\n  {label: 'PDM + RF-Tar-Railt 模板', value: 'pdm'},\n  {label: 'uv + fllesser 模板', value: 'uv'},\n  {label: 'Poetry + A-kirami 模板', value: 'poetry'},\n]}>\n  <TabItem value=\"pdm\" label=\"PDM + RF-Tar-Railt 模板\">\n\n需要安装 PDM 插件 [pdm-bump](https://github.com/carstencodes/pdm-bump)。\n\n```bash\n# 安装 pdm-bump\npdm self add pdm-bump\n\n# 更新 patch 版本\npdm bump patch\n\n# 推送 tag 触发发布\ngit push origin --tags\n```\n\n</TabItem>\n\n  <TabItem value=\"uv\" label=\"uv + fllesser 模板\">\n\n```bash\n# 更新 patch 版本\nuv version --bump patch\n\n# 创建相应提交与标签\ngit add pyproject.toml\ngit commit -m \"chore: release v0.1.1\"  # 替换为实际的版本号\ngit tag v0.1.1  # 替换为实际的版本号\n\n# 推送 tag 触发发布\ngit push origin --tags\n```\n\n</TabItem>\n\n  <TabItem value=\"poetry\" label=\"Poetry + A-kirami 模板\">\n\n```bash\n# 更新版本（自动提交并打标签）\npoetry version patch\n\n# 推送 tag 触发发布\ngit push origin --tags\n```\n\n</TabItem>\n</Tabs>\n\n</TabItem>\n  <TabItem value=\"bump-manual\" label=\"手动更新版本\">\n\n手动更新 `pyproject.toml` 中的 `version` 字段，然后推送 tag 触发发布工作流\n\n```bash\ngit add pyproject.toml\ngit commit -m \"chore: release v0.1.1\"  # 替换为实际的版本号\ngit tag v0.1.1  # 替换为实际的版本号\ngit push origin --tags\n```\n\n</TabItem>\n</Tabs>\n\n推送 `v*` 标签后，模板提供的 GitHub Actions 工作流将自动构建并发布到 PyPI。\n\n#### 6. 发布到 [PyPI](https://pypi.org)\n\n<Tabs groupId=\"publish-method\" defaultValue=\"template\" values={[\n  {label: '使用模板的自动发布工作流', value: 'template'},\n  {label: '手动发布', value: 'manual'},\n]}>\n  <TabItem value=\"template\" label=\"使用模板的自动发布工作流\">\n不同模板使用的发布方式可能不同，具体配置流程参考对应模板的详细使用指南。\n</TabItem>\n\n  <TabItem value=\"manual\" label=\"手动发布\">\n根据选用的构建系统，在项目的 `pyproject.toml` 中填入必要信息后进行构建与发布。\n\n:::tip 提示\n不同构建工具的使用可能存在差别。本文仅以 [`pdm`](https://pdm-project.org/zh/latest/),\n[`poetry`](https://python-poetry.org/docs/), [`setuptools`](https://setuptools.pypa.io/en/latest/)\n构建系统**本地构建与发布**为示例讲解，其余构建/管理工具等和自动化构建的用法请读者自行探索。\n:::\n\n<Tabs groupId=\"publishMethod\">\n  <TabItem value=\"poetry\" label=\"Poetry\" default>\n\n```bash\npoetry publish --build  # 构建并发布\n\n# 等效于以下两个命令\npoetry build            # 只构建\npoetry publish          # 只发布先前的构建\n```\n\n  </TabItem>\n\n  <TabItem value=\"pdm\" label=\"PDM\" default>\n\n```bash\npdm publish             # 构建并发布\n\n# 等效于以下两个命令\npdm build               # 只构建\npdm publish --no-build  # 只发布先前的构建\n```\n\n  </TabItem>\n\n  <TabItem value=\"setuptools\" label=\"Setuptools (PEP 517)\" default>\n\n```bash\npip install build twine             # 安装通用构建与发布工具\n\npython -m build --sdist --wheel .   # 只构建\ntwine upload dist/*                 # 只发布先前的构建\n```\n\n  </TabItem>\n</Tabs>\n\n</TabItem>\n</Tabs>\n\n:::tip 提示\n发布前建议自行测试构建包是否可用，避免遗漏代码文件或资源文件等问题。\n:::\n\n## 基本要求\n\n无论你选择哪条路径，以下内容**必须**完成，否则无法通过商店自动检查：\n\n### 能够正确加载\n\n插件包必须能够被 NoneBot 正确加载，在商店审核中会通过 **NoneFlow** 自动化加载测试进行。\n\n#### 依赖其他插件\n\n如果插件依赖其他插件提供的功能，则**必须**在代码中使用 `require()` 来引入该插件，然后才能 `import` 该插件提供的功能。具体细节参阅[跨插件访问](../advanced/requiring.md)。\n\n使用示例如下：\n\n```python title=nonebot_plugin_weather/__init__.py\nfrom nonebot import require\n\nrequire(\"nonebot_plugin_apscheduler\")\n\nfrom nonebot_plugin_apscheduler import scheduler\n```\n\n#### 不能零配置加载的插件\n\n如果插件需要必要配置项才能正常导入，则**必须**在商店提交表单中填写必要的配置项内容。\n\n但一种更好的做法是，**将插件设计为零配置即可加载**（允许缺少必要配置项时插件仍能正常导入，但不执行需要相应配置项的功能），尤其是对于一些必要配置含有敏感信息（如密钥、Token、API Key 等）的插件。这样可以避免在商店提交表单时填写敏感信息的风险。\n\n### 插件元数据\n\n插件包**必须**填写元数据才能通过 **NoneFlow** 自动化检查。\n\n下面是一个示例：\n\n```python title=nonebot_plugin_weather/__init__.py\nfrom nonebot.plugin import PluginMetadata\n\nfrom .config import Config\n\n__plugin_meta__ = PluginMetadata(\n    # 基本信息（必填）\n    name=\"天气查询\",  # 插件名称\n    description=\"查询指定城市的实时天气与未来预报\",  # 插件介绍\n    usage=\"发送【天气 城市名】获取天气信息\",  # 插件用法\n\n    # 发布额外信息\n    type=\"application\",  # 插件分类\n    # 发布必填，当前有效类型有：`library`（为其他插件编写提供功能），`application`（向机器人用户提供功能）。\n\n    homepage=\"https://github.com/你的用户名/nonebot-plugin-weather\",\n    # 发布必填。\n\n    config=Config,\n    # 插件配置项类，如果有配置类则必须填写。\n\n    supported_adapters={\"~onebot.v11\"},\n    # 支持的适配器集合，其中 `~` 在此处代表前缀 `nonebot.adapters.`，其余适配器亦按此格式填写。\n    # 若插件只使用了 NoneBot 基本抽象，应显式填写 None，否则应该列出插件支持的适配器。\n)\n```\n\n:::caution 注意\n`__plugin_meta__` 变量**必须**处于插件最外层（如 `__init__.py` 中），否则无法正常识别。\n\n一般做法是在 `__init__.py` 中定义 `__plugin_meta__`。\n:::\n\n#### 继承其他插件支持的适配器\n\n如果你的插件依赖于其他插件提供的支持功能，而其他插件可能支持更少的适配器，这时就应该使用\n[inherit_supported_adapters()](../api/plugin/load#inherit-supported-adapters) 函数来继承其他插件支持的适配器。\n\n示例用法如下：\n\n```python title=nonebot_plugin_weather/__init__.py\nfrom nonebot import require\nfrom nonebot.plugin import PluginMetadata, inherit_supported_adapters\n\nfrom .config import Config\n\nrequire(\"nonebot_plugin_alconna\")  # 必须先 require 才能被 inherit_supported_adapters 处理\n\n__plugin_meta__ = PluginMetadata(\n    name=\"天气查询\",\n    description=\"查询指定城市的实时天气与未来预报\",\n    usage=\"发送【天气 城市名】获取天气信息\",\n    type=\"application\",\n    homepage=\"https://github.com/你的用户名/nonebot-plugin-weather\",\n    config=Config,\n\n    supported_adapters=inherit_supported_adapters(\"nonebot_plugin_alconna\"),\n    # 继承 nonebot_plugin_alconna 插件的适配器支持列表\n)\n```\n\n### 准备项目主页\n\n通常可以使用 GitHub 项目页面作为项目主页，在 `README.md` 文件中编写插件介绍等内容。\n\n内容大致包括：\n\n- 插件功能介绍；\n- 安装方法\n  - **必须**有 NB-CLI 方式安装\n  - 可选依赖可以给出其他安装方式\n  - **不得**使用旧式的 `bot.py` 配置\n- 插件配置项（如 `Config` 类字段，若无可跳过）\n- 插件设置的触发规则（若无可跳过）\n- 插件的其它用法（按需编写）\n- 效果图、权限说明（按需编写）\n\n## 质量要求\n\n以下内容**强烈建议**完成，否则社区成员将会要求修改：\n\n### 依赖管理原则\n\n- **必须**包含 `nonebot2`。\n- **必须**将插件直接使用的适配器加入依赖列表，如：使用 OneBot 适配器的插件应添加 `nonebot-adapter-onebot` 依赖；\n- **禁止**使用 `==` 锁定单一版本，使用 `>=` 或 `~=`。\n- **禁止**添加 `nonebot`（V1）作为依赖。\n- 所有在代码中 `import` 的第三方库，必须在 `pyproject.toml` 的 `dependencies` 中列出。\n\n### 避免误用同步操作\n\nNoneBot 是一个异步框架，插件中**禁止**使用任何可能阻塞事件循环的同步操作，例如：\n\n- 同步 HTTP 请求（如 `requests` 库）；\n\n  **推荐**操作（以 `httpx` 为例）：\n\n  ```python\n  import httpx\n\n  async with httpx.AsyncClient() as client:\n      response = await client.get(\"https://api.example.com/data\")  # 异步操作，不阻塞机器人\n  ```\n\n  **禁止**操作：\n\n  ```python\n  import requests\n\n  requests.get(\"https://api.example.com/data\")  # 同步操作，会阻塞机器人\n  ```\n\n- 其他可能长时间运行阻塞事件循环的操作。\n\n### 本地文件存储\n\n如果插件需要在本地存储数据、配置或缓存文件，**必须**使用 [`nonebot-plugin-localstore`](https://github.com/nonebot/plugin-localstore) 管理，具体细节参阅[本地存储](../best-practice/data-storing.md)章节。\n\n参考示例：\n\n```python title=nonebot_plugin_weather/__init__.py\nfrom pathlib import Path\nfrom nonebot import require\nrequire(\"nonebot_plugin_localstore\")\n\nimport nonebot_plugin_localstore as store\n\n# 获取插件缓存文件（夹）路径\nweather_cache_dir: Path = store.get_plugin_cache_dir()\nweather_cache_file: Path = store.get_plugin_cache_file(\"cache.json\")\n\n# 获取插件配置文件（夹）路径\nweather_config_dir: Path = store.get_plugin_config_dir()\nweather_config_file: Path = store.get_plugin_config_file(\"config.toml\")\n\n# 获取插件数据文件（夹）路径\nweather_data_dir: Path = store.get_plugin_data_dir()\nweather_data_file: Path = store.get_plugin_data_file(\"resource-index.json\")\n```\n\n## 商店审核\n\n### 提交申请\n\n完成在 PyPI 的插件发布流程后，前往[商店](/store/plugins)页面，切换到插件页签，点击 **发布插件** 按钮。\n\n在弹出的插件信息提交表单内，填入您所要发布的相应插件信息。请注意，如果插件需要必要配置项才能正常导入，请在“插件配置项”中填写必要的内容（请勿填写密钥等敏感信息）。\n\n完成填写后，点击 **发布** 按钮，这将自动跳转 NoneBot 仓库内的“发布插件”页面。确认信息无误后点击页面下方的 `Submit new issue` 按钮进行最终提交即可。\n\n### 等待插件审核\n\n插件发布 Issue 创建后，将会经过 **NoneFlow Bot** 的自动前置检查，以确保插件信息正确无误、插件能被正确加载。\n\n:::tip 提示\n若插件检查未通过或信息有误，**不必**关闭当前 Issue。只需更新插件并上传到 PyPI/修改信息后勾选插件测试勾选框即可重新触发插件检查。\n:::\n\n之后，NoneBot 的维护者和一些志愿者会初步检查插件代码，帮助减少该插件的问题。\n\n完成这些步骤后，您的插件将会被自动合并到[商店](/store/plugins)，而您也将成为 [**NoneBot 贡献者**](https://github.com/nonebot/nonebot2/graphs/contributors)的一员。\n"
  },
  {
    "path": "website/docs/editor-support.md",
    "content": "---\nsidebar_position: 2\ndescription: 配置编辑器以获得最佳体验\n---\n\n# 编辑器支持\n\n框架基于 [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）工具进行类型检查，确保代码可以被编辑器正确解析。\n\n## CLI 脚手架提供的编辑器工具支持\n\n在使用 NB-CLI [创建项目](./quick-start.mdx#创建项目)时，如果选择了用于插件开发的 `simple` 模板，其会根据选择的开发工具，**自动配置项目根目录下的 `.vscode/extensions.json` 文件**，以推荐最匹配的 VS Code 插件，同时自动将相应的预设配置项写入 `pyproject.toml` 作为“开箱即用”配置，从而提升开发体验。\n\n```bash\n[?] 选择一个要使用的模板: simple (插件开发者)\n...\n[?] 要使用哪些开发工具?\n```\n\n### 支持的开发工具\n\n1. Pyright (Pylance)\n\n   [VS Code 插件](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance) | [项目](https://github.com/microsoft/pyright) | [文档](https://microsoft.github.io/pyright/)\n\n   由微软开发的 Python 静态类型检查器和语言服务器，提供智能感知、跳转定义、查找引用、实时错误检查等强大功能。\n\n   作为 VS Code 官方推荐的 Python 语言服务器，与 Pylance 扩展配合使用，能提供最流畅、最准确的代码补全和类型推断体验，是绝大多数开发者的首选。\n\n2. Ruff\n\n   [VS Code 插件](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff) | [项目](https://github.com/astral-sh/ruff) | [文档](https://docs.astral.sh/ruff/)\n\n   一个用 Rust 编写的超快 Python 代码格式化和 lint 工具，完全兼容 `black`、`isort`、`flake8` 等主流工具的规则。\n\n   速度极快（比 `black` 和 `flake8` 快 100 倍以上），配置简单，能自动格式化代码并检测潜在错误、代码风格问题（尤其是误用同步网络请求库），是提升代码质量和开发效率的必备利器。\n\n3. MyPy\n\n   [VS Code 插件](https://marketplace.visualstudio.com/items?itemName=matangover.mypy) | [项目](https://github.com/python/mypy) | [文档](https://mypy.readthedocs.io/en/stable/index.html)\n\n   一个官方实现的 Python 静态类型检查器，通过分析代码中的类型注解来发现类型错误。\n\n4. BasedPyright\n\n   [VS Code 插件](https://marketplace.visualstudio.com/items?itemName=detachhead.basedpyright) | [项目](https://github.com/DetachHead/basedpyright) | [文档](https://docs.basedpyright.com/)\n\n   一个基于 Pyright 的、由社区维护的替代性 Python 语言服务器，旨在提供更优的类型检查支持与接近 Pylance 的更好的使用体验。\n\n   相较于 Pylance，BasedPyright 允许配合 VS Code 之外的其他编辑器使用，同时也复刻了部分 Pylance 限定的功能。\n\n   如果您是高级用户，希望尝试 Pylance 的替代方案，或遇到 Pylance 在特定环境下的兼容性问题，可以考虑使用 BasedPyright。\n\n:::caution 提示\n为避免 `Pylance` 和 `BasedPyright` 相互冲突导致配置混乱甚至异常，脚手架默认不允许在创建项目时同时配置这两者。\n\n如果确实需要同时使用，请在创建项目时选择 Pylance/Pyright 并根据[相关文档](https://docs.basedpyright.com/latest/installation/ides/#vscode-vscodium)进行手动配置。\n:::\n\n### 配置效果\n\n选择上述工具后，NB-CLI 会在您的项目根目录下生成一个 `.vscode/extensions.json` 文件并在 `pyproject.toml` 文件中写入相应的配置项。当您在 VS Code 中打开此项目时，IDE\n会自动弹出提示，建议您安装这些推荐的扩展，一键即可完成开发环境的初始化，让您可以立即开始编写代码，无需手动搜索和安装插件。\n\n## 编辑器推荐配置\n\n### Visual Studio Code\n\n在 Visual Studio Code 中，可以使用 Pylance Language Server 并启用 `Type Checking` 配置以达到最佳开发体验。\n\n1. 在 VSCode 插件视图搜索并安装 `Python (ms-python.python)` 和 `Pylance (ms-python.vscode-pylance)` 插件。\n2. 修改 VSCode 配置\n   在 VSCode 设置视图搜索配置项 `Python: Language Server` 并将其值设置为 `Pylance`，搜索配置项 `Python > Analysis: Type Checking Mode` 并将其值设置为 `basic`。\n\n   或者向项目 `.vscode` 文件夹中配置文件添加以下内容：\n\n   ```json title=settings.json\n   {\n     \"python.languageServer\": \"Pylance\",\n     \"python.analysis.typeCheckingMode\": \"basic\"\n   }\n   ```\n\n### 其他\n\n欢迎提交 Pull Request 添加其他编辑器配置推荐。点击左下角 `Edit this page` 前往编辑。\n"
  },
  {
    "path": "website/docs/ospp/2021.md",
    "content": "---\nsidebar_position: 0\ndescription: 开源软件供应链点亮计划 - 暑期 2021\nmdx:\n  format: md\n---\n\n# 暑期 2021\n\n**开源软件供应链点亮计划 - 暑期 2021** 是**中国科学院软件研究所**与 **openEuler 社区**共同举办的一项面向高校学生的暑期活动，旨在鼓励在校学生积极参与开源软件的开发维护，促进优秀开源软件社区的蓬勃发展。关于具体的活动规划、报名方式，请查看该活动的 [官网](https://summer.iscas.ac.cn/) 和 [帮助文档](https://summer.iscas.ac.cn/help/)。\n\nNoneBot 社区有幸作为开源社区参与了本次活动，下面列出了目前我们已经发布的项目，欢迎感兴趣的同学在上面给出的活动官网报名，或通过 [contact@nonebot.dev](mailto:contact@nonebot.dev) 联系我们。\n\n## NoneBot v1\n\n### 更新 NoneBot v1 文档中的“指南”部分\n\n由于 NoneBot v1 和 aiocqhttp 最初基于的 QQ 机器人平台不再提供服务，CQHTTP 接口也转型且改名为 OneBot 标准，目前 NoneBot v1 文档的“指南”部分和 aiocqhttp 文档有部分过时内容需要更新。我们希望将其中与旧的机器人平台相关的内容改为基于 go-cqhttp 或通用的 OneBot 表述，同时对 NoneBot v1 的 awesome-bot 示例做一次全面检查，修改其中可能已经不可用的部分。\n\n**难度**：低\n\n**导师**：[@cleoold](https://github.com/cleoold)\n\n**产出要求**\n\n- 修改“指南”文档和 aiocqhttp 文档中与旧的 QQ 机器人平台相关的部分\n- 检查 awesome-bot 示例是否有已经过时/不可用的地方，并更新/修复\n- 修改“图灵机器人”案例，使用其它 AI 聊天 API 提供商（需先做简单调研）\n\n**技术要求**\n\n- 熟悉 Python 编程语言及 asyncio 机制\n- 了解 Git 基本用法\n- 了解聊天机器人基本开发过程\n- 了解 VuePress 更佳\n\n### NoneBot v1 API 文档自动生成\n\n目前 NoneBot v1 的文档中“API”部分是手动编写的，在更新代码接口的同时需要手动更新文档，可能造成文档与代码不匹配，形成额外的维护成本。我们希望将 API 文档改为直接编写在 Python docstring 中，通过工具自动生成 API 文档。\n\n**难度**：中\n\n**导师**：[@cleoold](https://github.com/cleoold)\n\n**产出要求**\n\n- 调研市面上常见的 Python API 文档生成工具\n- 在代码中补充 API 文档\n- 编写或应用开源工具自动生成 API 文档\n- 配置 GitHub Actions 或其它 CI 自动化构建和部署 API 文档\n\n**技术要求**\n\n- 熟悉 Python 编程语言及 asyncio 和 Type Hints\n- 了解 Git 基本用法\n- 了解 Sphinx 等文档生成工具更佳\n- 了解 GitHub Actions 等 CI 工具更佳\n\n## NoneBot v2\n\n### NoneBot v2 自动化测试框架“NoneBug”\n\n在聊天机器人的开发过程中，一套自动化的测试机制是非常重要的，特别是对于 NoneBot 2 这类为大型机器人开发而设计的项目来说，需要手动测试每一个边际条件是非常痛苦的。我们希望能够开发一款基于 NoneBot 2 插件机制的自动化测试框架，为 NoneBot 2 用户提供一套易用便捷、高度灵活的自动化测试框架。\n\n**难度**：高\n\n**导师**：[@yanyongyu](https://github.com/yanyongyu)\n\n**产出要求**\n\n- 调研现有的 Python 和其它语言集成测试框架\n- 设计 NoneBug 的用户 API 和实现方式\n- 实现 NoneBug 自动化测试框架\n- 编写详细的使用文档\n\n**技术要求**\n\n- 熟悉 Python 编程语言及 asyncio 和 Type Hints\n- 了解 Git 基本用法\n- 了解 NoneBot v2 的基本原理和使用方式\n- 了解主流的 Python 自动化测试框架\n\n### NoneBot v2 Telegram 适配器\n\n目前 NoneBot v2 已支持 OneBot、Mirai HTTP API、钉钉协议，社区反馈有更多的平台需求，希望能在 NoneBot v2 获得更多的跨平台支持，提高机器人的便携性。同时，我们也希望随着新平台加入，提升现有 NoneBot v2 核心代码的平台通用性。Telegram 是一款较为广泛使用的安全即时聊天软件，同时其官方提供了丰富的聊天机器人 API，因此我们希望为 NoneBot v2 编写一个 Telegram 适配器来支持 Telegram 机器人的开发。\n\n**难度**：中\n\n**导师**：[@yanyongyu](https://github.com/yanyongyu)\n\n**产出要求**\n\n- 调研 Telegram Bot API 以及 WebHook 等官方接口\n- 编写 Telegram 适配器并能够使用\n- 代码遵守项目 Contributing 规范\n\n**技术要求**\n\n- 熟悉 Python 编程语言及 asyncio 和 Type Hints\n- 了解 Git 基本用法\n- 了解 Web 开发相关知识\n- 了解 Sphinx 等文档生成工具更佳\n\n### NoneBot v2 飞书适配器\n\n目前 NoneBot v2 已支持 OneBot、Mirai HTTP API、钉钉协议，社区反馈有更多的平台需求，希望能在 NoneBot v2 获得更多的跨平台支持，提高机器人的便携性。同时，我们也希望随着新平台加入，提升现有 NoneBot v2 核心代码的平台通用性。飞书是目前企业用户广泛使用的即时聊天和协作软件，其官方提供了丰富的聊天机器人 API，因此我们希望为 NoneBot v2 编写一个飞书适配器来支持飞书机器人的开发。\n\n**难度**：中\n\n**导师**：[@yanyongyu](https://github.com/yanyongyu)\n\n**产出要求**\n\n- 调研飞书机器人 API 以及 WebHook 等官方接口\n- 编写飞书适配器并能够使用\n- 代码遵守项目 Contributing 规范\n\n**技术要求**\n\n- 熟悉 Python 编程语言及 asyncio 和 Type Hints\n- 了解 Git 基本用法\n- 了解 Web 开发相关知识\n- 了解 Sphinx 等文档生成工具更佳\n\n## OneBot\n\n### 设计 OneBot v12 接口标准\n\n目前的 OneBot 标准的 v11 版本仍然与 QQ 平台有较多耦合，我们希望在 v12 去掉与 QQ 耦合的历史包袱，形成一个通用的、可扩展的、易于使用的同时易于实现的聊天机器人接口标准。\n\n**难度**：中\n\n**导师**：[@richardchien](https://github.com/richardchien)\n\n**产出要求**\n\n- 调研各聊天机器人平台的官方/非官方接口特点\n- 通用化 OneBot 核心 API，分离 QQ 特定的 API，去掉无用 API\n- 优化现有的通信、消息表示机制\n- 补充 QQ 特定的缺失 API\n- 文档需符合风格指南\n\n**技术要求**\n\n- 熟悉至少两个聊天平台的聊天机器人开发\n- 了解 Git 基本用法\n- 了解使用不同语言编写聊天机器人时的常用实践\n- 对文档的优雅性与美观性有追求更佳\n\n### 实现 Rust 版 libonebot\n\n目前最常用的 OneBot 实现包括 go-cqhttp、onebot-kotlin、node-onebot 等，这些实现都各自重复实现了 Web 通信、消息解析、配置读写等功能，当社区中的开发者想针对一个新的聊天平台实现 OneBot 时，他们往往同样需要再次实现类似逻辑。我们希望使用 Rust 编写一个 libonebot 模块，该模块实现所有 OneBot 实现所共享的功能，从而方便其他开发者们使用 Rust 快速编写具体的 OneBot 实现。同时，我们希望借此项目在聊天机器人社区中推广 Rust 编程语言。\n\n> 注：这里的逻辑是 libonebot + 针对某聊天平台的对接代码 = 某聊天平台的 OneBot 实现，libonebot 要做的是让 OneBot 实现的开发者只需编写针对特定聊天平台的对接代码，而无需关心 OneBot 标准定义的通信方式、消息格式等。\n\n**难度**：高\n\n**导师**：[@richardchien](https://github.com/richardchien)\n\n**产出要求**\n\n- 实现所有 OneBot 实现所共享的功能，包括 Web 通信、消息解析、配置读写等\n- 充分考虑同时兼容 OneBot v11 和 v12 接口\n- 能够根据用户（OneBot 实现的开发者）所实现的接口自动实现类似 get_available_apis 等接口\n- 编写详细的使用文档\n- 如果可能，与 v12 设计项目联动，实现第一手 v12 支持\n\n**技术要求**\n\n- 熟悉聊天机器人开发\n- 熟悉 Rust Web 开发\n\n### 实现自选语言版 libonebot\n\n目前最常用的 OneBot 实现包括 go-cqhttp、onebot-kotlin、node-onebot 等，这些实现都各自重复实现了 Web 通信、消息解析、配置读写等功能，当社区中的开发者想针对一个新的聊天平台实现 OneBot 时，他们往往同样需要再次实现类似逻辑。我们希望使用 Python、Go、Kotlin、Node、PHP、C#.NET 等主流语言（任选一个）编写 libonebot 模块，该模块实现所有 OneBot 实现所共享的功能，从而方便其他开发者们使用对应语言快速编写具体的 OneBot 实现。\n\n> 注：这里的逻辑是 libonebot + 针对某聊天平台的对接代码 = 某聊天平台的 OneBot 实现，libonebot 要做的是让 OneBot 实现的开发者只需编写针对特定聊天平台的对接代码，而无需关心 OneBot 标准定义的通信方式、消息格式等。\n\n**难度**：中\n\n**导师**：[@richardchien](https://github.com/richardchien)\n\n**产出要求**\n\n- 实现所有 OneBot 实现所共享的功能，包括 Web 通信、消息解析、配置读写等\n- 充分考虑同时兼容 OneBot v11 和 v12 接口\n- 编写详细的使用文档\n- 如果可能，实现更多附加特性，如根据用户（OneBot 实现的开发者）所实现的接口自动实现类似 get_available_apis 等接口、实现第一手 v12 支持等\n\n**技术要求**\n\n- 熟悉聊天机器人开发\n- 熟悉所选语言的 Web 开发\n"
  },
  {
    "path": "website/docs/ospp/2022.md",
    "content": "---\nsidebar_position: 1\ndescription: 开源之夏 - 暑期 2022\nmdx:\n  format: md\n---\n\n# 暑期 2022\n\n**开源之夏 - 暑期 2022** 是由**开源软件供应链点亮计划**发起、由**中国科学院软件研究所**与 **openEuler 社区**主办的一项面向高校学生的暑期活动，类似 Google Summer of Code（GSoC），旨在鼓励在校学生积极参与开源软件的开发维护，促进优秀开源软件社区的蓬勃发展。关于具体的活动规划、报名方式，请查看该活动的 [官网](https://summer-ospp.ac.cn/) 和 [帮助文档](https://summer-ospp.ac.cn/help/)。\n\nNoneBot 社区有幸作为开源社区 [参与](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) 联系我们。\n\n## NoneBot2 命令行 CLI 交互体验升级\n\nNoneBot2 为用户提供了命令行脚手架 ──`nb-cli`，辅助用户更好地上手项目以及进行开发。nb-cli 主要包括：创建项目、运行项目、安装与卸载插件、部署项目等功能。随着 NoneBot2 Beta 版本的发布，脚手架功能存在一定的定位不明确、功能体验不佳。本项目旨在重新设计 nb-cli 功能框架，完善功能，优化用户体验。\n\n**难度**：进阶\n\n**导师**：[@yanyongyu](https://github.com/yanyongyu)\n\n**产出要求**\n\n- 设计 nb-cli 功能框架\n  - 明确各功能模块\n  - 设计用户交互模式\n- 完成 nb-cli 主要功能代码\n  - 项目管理\n  - 插件管理\n  - 其它\n- 同步更新使用文档\n\n**技术要求**\n\n- 熟悉 Python 命令行交互代码编写\n- 熟悉 NoneBot2 框架功能\n- 熟悉 NoneBot2 项目组织方式\n\n**成果仓库**\n\n- <https://github.com/nonebot/nb-cli>\n- <https://github.com/nonebot/nonebot2>\n\n## NoneBot2 命令行即时交互通信设计与实现\n\nNoneBot2 在早期提供了基于网页的 nonebot-plugin-test 插件，无需平台适配接入即可对机器人进行测试，方便了开发者直观的感受机器人文本交互功能。我们希望提供一款基于命令行的适配器/驱动器，用于无平台适配接入、可以运行机器人的场景进行功能体验或测试。\n\n**难度**：进阶\n\n**导师**：[@mnixry](https://github.com/mnixry)\n\n**产出要求**\n\n- 设计命令行与 NoneBot2 通信模式\n  - 直接调用/HTTP/WebSocket\n- 设计命令行交互界面\n- 实现相应适配器/驱动器\n- 同步更新使用说明文档\n\n**技术要求**\n\n- 熟悉 Python 命令行交互代码编写\n- 熟悉 NoneBot2 框架功能\n- 熟悉 NoneBot2 项目组织方式\n\n**成果仓库**\n\n- <https://github.com/nonebot/adapter-console>\n\n## NoneBot2 用户上手与深入教程设计\n\nNoneBot2 为用户提供了详细的文档介绍，辅助用户更好的上手项目以及进行开发。文档分为基础与进阶两个部分。基础部分帮助新用户快速上手开发，主要包括：安装 NoneBot2、使用脚手架、创建配置项目、使用适配器、加载插件、定义消息事件、处理消息事件、调用平台 API 等。进阶部分向已经熟悉开发流程的用户介绍更多高级技巧，主要包括：NoneBot2 工作原理、定时任务、权限控制、钩子函数、跨插件访问、单元测试、发布插件等。目前文档对于用户而言过于费解，导致用户难以理解 NoneBot2 开发。本项目旨在优化文档内容，使其更加通俗易懂，不让文档成为用户上手的阻碍，同时完善进阶内容，让有更复杂需求的用户，同样能从文档中受益。\n\n相关 issue：\n\n- <https://github.com/nonebot/nonebot2/issues/793>\n- <https://github.com/nonebot/nonebot2/issues/295>\n\n**难度**：进阶\n\n**导师**：[@SK-415](https://github.com/SK-415)\n\n**产出要求**\n\n- 文档通俗易懂\n  - 附有适当的图片指引（如 asciinema）\n- 内容完整，由浅入深\n- 适当的界面美化，合理分配布局\n\n**技术要求**\n\n- 熟悉文档结构组织与语言表达\n- 熟悉 NoneBot2 框架功能\n- 熟悉 NoneBot2 项目组织方式\n\n**成果仓库**\n\n- <https://github.com/nonebot/nonebot2>\n"
  },
  {
    "path": "website/docs/ospp/2023.md",
    "content": "---\nsidebar_position: 2\ndescription: 开源之夏 - 暑期 2023\nmdx:\n  format: md\n---\n\n# 暑期 2023\n\n**开源之夏 - 暑期 2023** 是由**开源软件供应链点亮计划**发起、由**中国科学院软件研究所**与 **openEuler 社区**主办的一项面向高校学生的暑期活动，类似 Google Summer of Code（GSoC），旨在鼓励在校学生积极参与开源软件的开发维护，促进优秀开源软件社区的蓬勃发展。关于具体的活动规划、报名方式，请查看该活动的 [官网](https://summer-ospp.ac.cn/) 和 [帮助文档](https://summer-ospp.ac.cn/help/)。\n\nNoneBot 社区有幸作为开源社区 [参与](https://summer-ospp.ac.cn/org/orgdetail/e1fb5b8d-125a-4138-b756-25bd32c0a31a?lang=zh) 了本次活动，下面列出了目前我们已经发布的项目，欢迎感兴趣的同学通过 [contact@nonebot.dev](mailto:contact@nonebot.dev) 联系我们。\n\n## NoneBot 项目管理图形化面板\n\nNoneBot 目前提供了开箱即用的命令行脚手架来帮助初次使用的用户更快的上手编写应用。但是，对于未有一定开发经验的用户，命令行的使用仍具有一定的困难。此外，其他项目如 koishi、vue 等，均可通过图形化界面的形式为用户提供更便捷的项目开发。因此，我们希望借助现有命令行脚手架的可扩展特性，提供一个项目管理面板服务，以网页的形式帮助用户开发 NoneBot 应用。\n\n**难度**：进阶\n\n**导师**：[@mnixry](https://github.com/mnixry)\n\n**产出要求**\n\n- 设计并实现项目管理面板相关功能\n  - 创建与管理项目\n  - 配置与运行项目\n  - NoneBot 插件管理\n- 实现相应 nb-cli 插件提供面板服务\n- 代码符合 NoneBot Contributing 规范\n\n**技术要求**\n\n- 熟悉 nb-cli 相关功能\n- 熟悉 NoneBot 框架功能\n- 熟悉前后端相关实现方式\n\n**成果仓库**\n\n- <https://github.com/nonebot/cli-plugin-webui>\n\n## NoneBot Discord 适配器\n\nNoneBot 作为一个跨平台聊天机器人框架，目前已有 OneBot、飞书、Telegram、QQ 频道等诸多平台的适配支持。作为众多用户期待的平台适配之一，我们希望借此机会接入 Discord 聊天机器人。\n\n**难度**：进阶\n\n**导师**：[@iyume](https://github.com/iyume)\n\n**产出要求**\n\n- 调研 Discord Bot 相关功能与接口\n- 设计与编写 NoneBot Discord 适配器\n- 代码符合 NoneBot Contributing 规范\n\n**技术要求**\n\n- 熟悉 NoneBot 框架功能\n- 熟悉 NoneBot 各模块职责与适配器编写\n\n**成果仓库**\n\n- <https://github.com/nonebot/adapter-discord>\n\n## NoneBot 数据库支持插件\n\nNoneBot 的插件系统为用户实现应用提供了极高的便捷性，但因此也增加了插件统一管理的难度。目前，我们发现许多用户发布的插件中存在文件存储结构化数据、数据存放散乱等现象，同时插件间也可能产生冲突。因此，我们希望提供一个统一的数据存储与管理方式，便于用户读写应用数据。\n\n**难度**：进阶\n\n**导师**：[@yanyongyu](https://github.com/yanyongyu)\n\n**产出要求**\n\n- 设计并实现 ORM 插件\n  - 提供关系模型定义功能\n  - 提供模型迁移与管理功能\n  - 能较好的支持 Python 类型检查与推导\n- 编写相应的用户使用文档\n- 代码符合 NoneBot Contributing 规范\n\n**技术要求**\n\n- 熟悉 NoneBot 框架功能与插件编写\n- 熟悉 SQLAlchemy 等 ORM 框架\n  - 熟悉 SQLAlchemy ORM\n  - 熟悉 alembic 等迁移工具\n- 熟悉 nb-cli 插件编写\n\n**成果仓库**\n\n- <https://github.com/nonebot/plugin-orm>\n"
  },
  {
    "path": "website/docs/ospp/2024.md",
    "content": "---\nsidebar_position: 3\ndescription: 开源之夏 - 暑期 2024\nmdx:\n  format: md\n---\n\n# 暑期 2024\n\n**开源之夏 - 暑期 2024** 是**中国科学院软件研究所**发起的**开源软件供应链点亮计划**系列暑期活动，旨在鼓励高校学生积极参与开源软件的开发维护，促进优秀开源软件社区的蓬勃发展。活动联合各大开源社区，针对重要开源软件的开发与维护提供项目开发任务，并向全球高校学生开放报名。关于具体的活动规划、报名方式，请查看该活动的 [官网](https://summer-ospp.ac.cn/) 和 [帮助文档](https://summer-ospp.ac.cn/help/)。\n\nNoneBot 社区有幸作为开源社区 [参与](https://summer-ospp.ac.cn/org/orgdetail/e1fb5b8d-125a-4138-b756-25bd32c0a31a?lang=zh) 了本次活动，下面列出了目前我们已经发布的项目，欢迎感兴趣的同学通过 [contact@nonebot.dev](mailto:contact@nonebot.dev) 联系我们。\n\n## NonePress 官网组件库更新与优化\n\nNoneBot 官网目前采用基于 TailwindCSS 自研的 NonePress 组件库及 Docusaurus 框架进行构建。由于相关依赖版本迭代迅速，目前官网组件库已产生了较大的版本落后。本项目希望在跟进框架新版本的基础上，对文档整体视觉体验进行重新设计，提升页面的无障碍访问性，基于 React Hydrate 特性实现完整的静态网站生成（SSG）以提升搜索引擎优化（SEO）水平。在解决以上问题的基础上，可对网页的开发以及生产构建性能做相应的优化提升，例如在生产构建使用自有的 webpack loader、替换现有的热重载逻辑以减少开发环境启动耗时等。\n\n**难度**：进阶\n\n**导师**：[@yanyongyu](https://github.com/yanyongyu)\n\n**产出要求**\n\n- 基于 Docusaurus v3 重构 NonePress 组件库及相关插件\n  - 升级相关依赖并重新打造 Docusaurus theme（布局与组件）\n  - 根据需求实现/修改 Docusaurus 插件使得官网内容构建正常\n  - 能够提升页面渲染性能与 MDX 相关能力\n- 升级官网采用新版组件库\n  - Algolia 索引与 SEO 正常\n  - 桌面端与移动端显示正常\n  - 优化官网开发与生产构建体验\n- （可选）优化官网部分页面\n  - 优化官网过长的 changelog\n  - 优化官网插件商店的展示细节\n\n**技术要求**\n\n- 熟练掌握 TS、PostCSS、TSX、MDX等相关技术\n- 掌握 React、Docusaurus、tailwind css 等框架\n- 熟悉静态网站生成 SSG、SEO 优化与 Algolia 索引原理等\n\n**成果仓库**\n\n- <https://github.com/nonebot/docusaurus-theme-nonepress>\n\n## NoneFlow 社区自动化工作流管理优化\n\nNoneFlow 在 NoneBot 社区中承担着重要的角色，它由 NoneBot 框架基于 GitHub APP 编写而成，能够自动化的完成许多复杂流程的处理，如：用户请求提交插件到商店时进行自动化检测，并在人工审核通过后自动存储至 registry；定时自动更新 registry 内插件信息，跟进插件新版本情况等。但是，在长期的使用中发现了一些问题和不足的地方，例如：项目本身结构复杂耦合，添加新自动化流程与维护现有流程困难；目前采用了 GitHub 用户名作为插件作者名，但已有不少插件作者改名；插件存储至 registry 并定时更新，缺少统计相关信息以帮助商店更好的展示当前插件状态；插件作者想要修改插件信息时无法便捷的找到操作方式等。本项目希望针对以上问题与不足的地方进行修复与优化，提升用户体验。\n\n**难度**：进阶\n\n**导师**：[@uy/sun](https://github.com/he0119)\n\n**产出要求**\n\n- 重构现有工作流处理结构\n  - 整合现有 Issue、Pull Request、Git 相关操作\n  - 提供用户修改信息的处理方式\n  - 正确处理 PR 的 Open、Close、Draft 状态\n- 修复流程中存在的问题\n  - 插件作者名正确展示\n  - registry 定时更新中需要插件测试环境隔离\n- 在 registry 定时更新的同时提供统计数据\n\n**技术要求**\n\n- 掌握 GitHub APP 开发\n  - 熟悉 GitHub REST API、GraphQL 等\n  - 熟悉 GitHub APP 权限限制\n- 熟悉 NoneBot 框架与 Python 相关技术\n- 熟悉 Git、GitHub Action、GitHub 工作流\n\n**成果仓库**\n\n- <https://github.com/nonebot/noneflow>\n\n## NoneBlockly 低代码框架开发\n\n经过深入分析社区反馈，我们发现部分新手因不熟悉编程概念或框架本身而遇到问题。为了解决初学者在使用面向开发者的聊天机器人框架 NoneBot 时遇到的挑战，我们计划引入 Blockly 提供低代码编程支持。通过减少常见的编码错误和降低入门门槛，使框架对初学者更加友好，从而提升用户体验并有助于 NoneBot 生态的成长。本项目将基于 Blockly 实现 NoneBot 插件的低代码编写，使得用户能够快速搭建聊天机器人。\n\n**难度**：进阶\n\n**导师**：[@mnixry](https://github.com/mnixry)\n\n**产出要求**\n\n- 实现 NoneBlockly 低代码开发框架\n  - 能够基于 Alconna 编写跨平台插件\n  - 确保插件对 Python 和 NoneBot 版本的兼容性\n  - 支持对多种类型 NoneBot 事件的响应\n  - 支持对 NoneBot 消息对象的便捷操作\n  - 集成 localstore 文件存储、apscheduler 定时任务、网络请求等常用功能\n- 对接 NB-CLI 脚手架，通过脚手架扩展使用低代码框架\n\n**技术要求**\n\n- 掌握 Python 与 NoneBot 框架的使用\n  - 熟悉 NoneBot 插件的开发，包括事件响应与消息处理等\n  - 熟悉 NoneBot 生态组件（Alconna、localstore、apscheduler等）的使用\n  - 了解 NB-CLI 脚手架的扩展开发\n- 熟悉 Blockly 低代码框架的使用和开发\n\n**成果仓库**\n\n- <https://github.com/nonebot/noneblockly>\n"
  },
  {
    "path": "website/docs/ospp/2025.md",
    "content": "---\nsidebar_position: 4\ndescription: 开源之夏 - 暑期 2025\nmdx:\n  format: md\n---\n\n# 暑期 2025\n\n**开源之夏 - 暑期 2025** 是**中国科学院软件研究所**发起的**开源软件供应链点亮计划**系列暑期活动，旨在鼓励高校学生积极参与开源软件的开发维护，培养和发掘更多优秀的开发者，促进优秀开源软件社区的蓬勃发展，助力开源软件供应链建设。活动联合各大开源社区，针对重要开源软件的开发与维护提供项目开发任务，并向全球高校学生开放报名。关于具体的活动规划、报名方式，请查看该活动的 [官网](https://summer-ospp.ac.cn/) 和 [帮助文档](https://summer-ospp.ac.cn/help/)。\n\nNoneBot 社区有幸作为开源社区 [参与](https://summer-ospp.ac.cn/org/orgdetail/e1fb5b8d-125a-4138-b756-25bd32c0a31a?lang=zh) 了本次活动，下面列出了目前我们已经发布的项目，欢迎感兴趣的同学通过 [contact@nonebot.dev](mailto:contact@nonebot.dev) 联系我们。\n\n## NoneBot HTML 图片渲染插件\n\n文字与图片一直是聊天机器人的两大主流交互方式，而图片的渲染一直是用户开发应用的一大痛点。常见的方式包括 PIL 图片编辑、浏览器渲染 HTML 截图等。PIL 图片编辑依赖人工构建图片布局，容易出现自适应问题，且提升图片特效、美观程度需要极大的开发成本。浏览器渲染方案通过 HTML 与 CSS 能够轻松完成美观自适应能力强的布局，但其部署门槛较高，难以支撑较大规模调用量。而其他轻量化渲染引擎通常不具有完整 HTML/CSS 现代化标准实现，且未提供 Python Binding 直接使用。\n\n本项目希望调研并实现一种高效、便捷的图片渲染方案。该方案需要在保障跨平台一致性、最大程度保证 HTML 与 CSS 现代化标准的前提下，低成本（资源消耗与吞吐量）将 HTML 渲染为对应图片。\n\n**难度**：进阶\n\n**导师**：[@MelodyKnit](https://github.com/MelodyKnit)\n\n**产出要求**\n\n- 调研 HTML/CSS 渲染引擎\n  - 调研 litehtml 等渲染引擎 标准支持能力与兼容性\n- 基于渲染引擎实现 HTML 图片渲染插件\n  - 将渲染引擎通过 binding 等方式集成为 Python 模块\n  - 基于集成模块实现 HTML 图片渲染能力\n  - 编写插件使用文档\n\n**技术要求**\n\n- 掌握 Python 及其异步编程\n- 熟悉 NoneBot 框架及其插件编写\n- 了解浏览器与 HTML 渲染原理\n\n**成果仓库**\n\n- <https://github.com/nonebot/plugin-htmlkit>\n\n## NB-CLI 命令行工具交互优化\n\nNB-CLI 作为 NoneBot 生态的核心入门与管理工具，主要负责新手引导项目创建、项目运行以及插件管理几大功能。目前该脚手架工具仍存在几点缺陷：\n\n- 作为插件管理工具，由于存储数据的局限性，无法很好地展示用户项目当前安装插件状态，并进行卸载等操作；\n- 当前插件管理高度依赖云端 registry 提供插件信息，在离线情况下完全无法使用；\n- 由于插件信息繁多，工具未能向用户展示充分的信息，交互复杂 体验较差。\n\n以上问题对用户使用 NB-CLI 管理项目插件造成了极大的阻碍。\n本项目希望重点针对插件管理部分，重构工具插件管理模块，完善框架缺陷，并通过缓存等方式确保可用性。其次，调研同类工具方案与 TUI 等相关技术，优化信息展示能力、用户交互方式，提升工具整体交互体验。\n\n**相关链接**\n\n- https://github.com/nonebot/nb-cli/issues/138\n- https://github.com/nonebot/nb-cli/issues/140\n\n**难度**：基础\n\n**导师**：[@yanyongyu](https://github.com/yanyongyu)\n\n**产出要求**\n\n- 重构 NB-CLI 插件管理模块\n  - 优化项目插件信息存储方式，支持列出、卸载插件等操作\n  - 通过缓存 registry 数据等方式确保离线场景的可用性\n- 提升 NB-CLI 交互体验\n  - 调研同类工具方案与 TUI 等相关技术\n  - 优化 registry 多字段信息展示能力\n  - 基于 TUI 等技术优化用户交互方式，提升整体交互体验\n\n**技术要求**\n\n- 熟练掌握 Python 及其异步编程\n- 熟悉 NoneBot 框架与 NB-CLI 使用方法\n- 了解 TUI 等终端交互技术\n\n**成果仓库**\n\n- <https://github.com/nonebot/nb-cli>\n"
  },
  {
    "path": "website/docs/quick-start.mdx",
    "content": "---\nsidebar_position: 1\ndescription: 尝试使用 NoneBot\n\noptions:\n  menu:\n    - category: tutorial\n      weight: 10\n---\n\nimport Asciinema from \"@site/src/components/Asciinema\";\nimport Messenger from \"@site/src/components/Messenger\";\n\n# 快速上手\n\n:::caution 前提条件\n\n- 请确保你的 Python 版本 >= 3.9\n- **我们强烈建议使用虚拟环境进行开发**，如果没有使用虚拟环境，请确保已经卸载可能存在的 NoneBot v1！！！\n  ```bash\n  pip uninstall nonebot\n  ```\n\n:::\n\n在本章节中，我们将介绍如何使用脚手架来创建一个 NoneBot 简易项目。项目将基于 nb-cli 脚手架运行，并允许我们从商店安装插件。\n\n<Asciinema\n  url=\"https://asciinema.org/a/569440.cast\"\n  options={{ theme: \"monokai\", poster: \"npt:21.5\" }}\n/>\n\n## 安装脚手架\n\n确保你已经安装了 Python 3.9 及以上版本，然后在命令行中执行以下命令：\n\n1. 安装 [pipx](https://pypa.github.io/pipx/)\n\n   ```bash\n   python -m pip install --user pipx\n   python -m pipx ensurepath\n   ```\n\n   如果在此步骤的输出中出现了“open a new terminal”或者“re-login”字样，那么请关闭当前终端并重新打开一个新的终端。\n\n2. 安装脚手架\n\n   ```bash\n   pipx install nb-cli\n   ```\n\n安装完成后，你可以在命令行使用 `nb` 命令来使用脚手架。如果出现无法找到命令的情况（例如出现“Command not found”字样），请参考 [pipx 文档](https://pypa.github.io/pipx/) 检查你的环境变量。\n\n## 创建项目\n\n使用脚手架来创建一个项目：\n\n```bash\nnb create\n```\n\n这一指令将会执行创建项目的流程，你将会看到一些询问：\n\n1. 项目模板\n\n   ```bash\n   [?] 选择一个要使用的模板: bootstrap (初学者或用户)\n   ```\n\n   这里我们选择 `bootstrap` 模板，它是一个简单的项目模板，能够安装商店插件。如果你需要**自行编写插件**，这里请选择 `simple` 模板。\n\n2. 项目名称\n\n   ```bash\n   [?] 项目名称: awesome-bot\n   ```\n\n   这里我们以 `awesome-bot` 为例，作为项目名称。你可以根据自己的需要来命名。\n\n3. 其他选项\n   请注意，多选项使用**空格**选中或取消，**回车**确认。\n\n   ```bash\n   [?] 要使用哪些适配器? Console (基于终端的交互式适配器)\n   [?] 要使用哪些驱动器? FastAPI (FastAPI 驱动器)\n   [?] 要使用什么本地存储策略? 用户全局 (默认，适用于单用户下单实例)\n   [?] 立即安装依赖? (Y/n) Yes\n   [?] 创建虚拟环境? (Y/n) Yes\n   ```\n\n   这里我们选择了创建虚拟环境，nb-cli 在之后的操作中将会自动使用这个虚拟环境。如果你不需要自动创建虚拟环境或者已经创建了其他虚拟环境，nb-cli 将会安装依赖至当前激活的 Python 虚拟环境。\n\n4. 选择内置插件\n\n   ```bash\n   [?] 要使用哪些内置插件? echo\n   ```\n\n   这里我们选择 `echo` 插件作为示例。这是一个简单的复读回显插件，可以用于测试你的机器人是否正常运行。\n\n## 运行项目\n\n在项目创建完成后，你可以在**项目目录**中使用以下命令来运行项目：\n\n```bash\nnb run\n```\n\n你现在应该已经运行起来了你的第一个 NoneBot 项目了！请注意，生成的项目中使用了 `FastAPI` 驱动器和 `Console` 适配器，你之后可以自行修改配置或安装其他适配器。\n\n## 尝试使用\n\n在项目运行起来后，`Console` 适配器会在你的终端启动交互模式，你可以直接在输入框中输入 `/echo hello world` 来测试你的机器人是否正常运行。\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/echo hello world\" },\n    { position: \"left\", msg: \"hello world\" },\n  ]}\n/>\n"
  },
  {
    "path": "website/docs/tutorial/application.mdx",
    "content": "---\nsidebar_position: 0\ndescription: 创建一个 NoneBot 项目\n\noptions:\n  menu:\n    - category: tutorial\n      weight: 20\n---\n\n# 手动创建项目\n\nimport Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\n\n在[快速上手](../quick-start.mdx)中，我们已经介绍了如何安装和使用 `nb-cli` 创建一个项目。在本章节中，我们将简要介绍如何在不使用 `nb-cli` 的方式创建一个机器人项目的**最小实例**并启动。如果你想要了解 NoneBot 的启动流程，也可以阅读本章节。\n\n:::caution 警告\n我们十分不推荐直接创建机器人项目，请优先考虑使用 nb-cli 进行项目创建。\n:::\n\n一个机器人项目的**最小实例**中**至少**需要包含以下内容:\n\n- 入口文件：初始化并运行机器人的 Python 文件\n- 配置文件：存储机器人启动所需的配置\n- 插件：为机器人提供具体的功能\n\n下面我们创建一个项目文件夹，来存放项目所需文件，以下步骤均在该文件夹中进行。\n\n## 安装依赖\n\n在创建项目前，我们首先需要将项目所需依赖安装至环境中。\n\n1. （可选）创建虚拟环境，以 venv 为例\n\n   <Tabs groupId=\"platform\">\n   <TabItem value=\"windows\" label=\"Windows\" default>\n\n   ```bash\n   # 创建虚拟环境\n   python -m venv .venv --prompt nonebot2\n   # 激活虚拟环境\n   .venv\\Scripts\\activate\n   ```\n\n   </TabItem>\n   <TabItem value=\"linux/macos\" label=\"Linux/macOS\">\n\n   ```bash\n   # 创建虚拟环境\n   python -m venv .venv --prompt nonebot2\n   # 激活虚拟环境\n   source .venv/bin/activate\n   ```\n\n   </TabItem>\n   </Tabs>\n\n2. 安装 nonebot2 以及驱动器，以 Fastapi 驱动器为例\n\n   <Tabs groupId=\"platform\">\n   <TabItem value=\"windows\" label=\"Windows\" default>\n\n   ```bash\n   pip install \"nonebot2[fastapi]\"\n   ```\n\n   </TabItem>\n   <TabItem value=\"linux/macos\" label=\"Linux/macOS\">\n\n   ```bash\n   pip install \"nonebot2[fastapi]\"\n   ```\n\n   </TabItem>\n   </Tabs>\n\n   驱动器包名可以在 [驱动器商店](/store/drivers) 中找到，请替换上文方括号中的内容。\n\n3. 安装适配器，以 Console 适配器为例\n\n   ```bash\n   pip install nonebot-adapter-console\n   ```\n\n   适配器包名可以在 [适配器商店](/store/adapters) 中找到。\n\n## 创建配置文件\n\n配置文件用于存放 NoneBot 运行所需要的配置项，使用 [`pydantic`](https://docs.pydantic.dev/) 以及 [`python-dotenv`](https://saurabh-kumar.com/python-dotenv/) 来读取配置。配置项需符合 dotenv 格式，复杂类型数据需使用 JSON 格式填写。具体可选配置方式以及配置项详情参考[配置](../appendices/config.mdx)。\n\n在**项目文件夹**中创建一个名为 `.env` 的文件，并写入以下内容:\n\n```bash title=.env\nHOST=0.0.0.0  # 配置 NoneBot 监听的 IP / 主机名\nPORT=8080  # 配置 NoneBot 监听的端口\nCOMMAND_START=[\"/\"]  # 配置命令起始字符\nCOMMAND_SEP=[\".\"]  # 配置命令分割字符\n```\n\n## 创建入口文件\n\n入口文件（ Entrypoint ）顾名思义，是用来初始化并运行机器人的 Python 文件。入口文件需要完成框架的初始化、注册适配器、加载插件等工作。\n\n:::tip 提示\n如果你使用 `nb-cli` 创建项目，入口文件不会被创建，该文件功能会被 `nb run` 命令代替。\n:::\n\n在**项目文件夹**中创建一个 `bot.py` 文件，并写入以下内容:\n\n```python title=bot.py\nimport nonebot\nfrom nonebot.adapters.console import Adapter as ConsoleAdapter  # 避免重复命名\n\n# 初始化 NoneBot\nnonebot.init()\n\n# 注册适配器\ndriver = nonebot.get_driver()\ndriver.register_adapter(ConsoleAdapter)\n\n# 在这里加载插件\nnonebot.load_builtin_plugins(\"echo\")  # 内置插件\n# nonebot.load_plugin(\"thirdparty_plugin\")  # 第三方插件\n# nonebot.load_plugins(\"awesome_bot/plugins\")  # 本地插件\n\nif __name__ == \"__main__\":\n    nonebot.run()\n```\n\n我们暂时不需要了解其中内容的含义，这些将会在稍后的章节中逐一介绍。在创建完成以上文件并确认已安装所需适配器和插件后，即可运行机器人。\n\n## 运行机器人\n\n在**项目文件夹**中，使用配置好环境的 Python 解释器运行入口文件:\n\n<Tabs groupId=\"platform\">\n  <TabItem value=\"windows\" label=\"Windows\" default>\n\n```bash\n# 激活虚拟环境（未使用虚拟环境时跳过此行）\n.venv\\Scripts\\activate\n# 运行机器人\npython bot.py\n```\n\n  </TabItem>\n  <TabItem value=\"linux/macos\" label=\"Linux/macOS\">\n\n```bash\n# 激活虚拟环境（未使用虚拟环境时跳过此行）\nsource .venv/bin/activate\n# 运行机器人\npython bot.py\n```\n\n  </TabItem>\n</Tabs>\n\n如果你后续使用了 `nb-cli` ，你仍可以使用 `nb run` 命令来运行机器人，`nb-cli` 会自动检测入口文件 `bot.py` 是否存在并运行。同时，你也可以使用 `nb run --reload` 来自动检测代码的更改并自动重新运行入口文件。\n"
  },
  {
    "path": "website/docs/tutorial/create-plugin.md",
    "content": "---\nsidebar_position: 3\ndescription: 创建并加载自定义插件\n\noptions:\n  menu:\n    - category: tutorial\n      weight: 50\n---\n\n# 插件编写准备\n\n在正式编写插件之前，我们需要先了解一下插件的概念。\n\n## 插件结构\n\n在 NoneBot 中，插件即是 Python 的一个[模块（module）](https://docs.python.org/zh-cn/3/glossary.html#term-module)。NoneBot 会在导入时对这些模块做一些特殊的处理使得他们成为一个插件。插件间应尽量减少耦合，可以进行有限制的相互调用，NoneBot 能够正确解析插件间的依赖关系。\n\n### 单文件插件\n\n一个普通的 `.py` 文件即可以作为一个插件，例如创建一个 `foo.py` 文件：\n\n```tree title=Project\n📂 plugins\n└── 📜 foo.py\n```\n\n这个时候模块 `foo` 已经可以被称为一个插件了，尽管它还什么都没做。\n\n### 包插件\n\n一个包含 `__init__.py` 的文件夹即是一个常规 Python [包 `package`](https://docs.python.org/zh-cn/3/glossary.html#term-regular-package)，例如创建一个 `foo` 文件夹：\n\n```tree title=Project\n📂 plugins\n└── 📂 foo\n    └── 📜 __init__.py\n```\n\n这个时候包 `foo` 同样是一个合法的插件，插件内容可以在 `__init__.py` 文件中编写。\n\n## 创建插件\n\n:::caution 注意\n如果在之前的[快速上手](../quick-start.mdx)章节中已经使用 `bootstrap` 模板创建了项目，那么你需要做出如下修改：\n\n1. 在项目目录中创建一个两层文件夹 `awesome_bot/plugins`\n\n   ```tree title=Project\n   📦 awesome-bot\n   ├── 📂 .venv\n   ├── 📂 awesome_bot\n   │   └── 📂 plugins\n   ├── 📜 .env.prod\n   ├── 📜 pyproject.toml\n   └── 📜 README.md\n   ```\n\n2. 修改 `pyproject.toml` 文件中的 `nonebot` 配置项，在 `plugin_dirs` 中添加 `awesome_bot/plugins`\n\n   ```toml title=pyproject.toml\n   [tool.nonebot]\n   plugin_dirs = [\"awesome_bot/plugins\"]\n   ```\n\n:::\n\n:::caution 注意\n如果在之前的[创建项目](./application.mdx)章节中手动创建了相关文件，那么你需要做出如下修改：\n\n1. 在项目目录中创建一个两层文件夹 `awesome_bot/plugins`\n\n   ```tree title=Project\n   📦 awesome-bot\n   ├── 📂 awesome_bot\n   │   └── 📂 plugins\n   └── 📜 bot.py\n   ```\n\n2. 修改 `bot.py` 文件中的加载插件部分，取消注释或者添加如下代码\n\n   ```python title=bot.py\n   # 在这里加载插件\n   nonebot.load_builtin_plugins(\"echo\")  # 内置插件\n   nonebot.load_plugins(\"awesome_bot/plugins\")  # 本地插件\n   ```\n\n:::\n\n创建插件可以通过 `nb-cli` 命令从完整模板创建，也可以手动新建空白文件。通过以下命令创建一个名为 `weather` 的插件：\n\n```bash\n$ nb plugin create\n[?] 插件名称: weather\n[?] 使用嵌套插件? (y/N) N\n[?] 请输入插件存储位置: awesome_bot/plugins\n```\n\n`nb-cli` 会在 `awesome_bot/plugins` 目录下创建一个名为 `weather` 的文件夹，其中包含的文件将在稍后章节中用到。\n\n```tree title=Project\n📦 awesome-bot\n├── 📂 .venv\n├── 📂 awesome_bot\n│   └── 📂 plugins\n|       └── 📂 weather\n|           ├── 📜 __init__.py\n|           └── 📜 config.py\n├── 📜 .env.prod\n├── 📜 pyproject.toml\n└── 📜 README.md\n```\n\n## 加载插件\n\n:::danger 警告\n请勿在插件被加载前 `import` 插件模块，这会导致 NoneBot 无法将其转换为插件而出现意料之外的情况。\n:::\n\n加载插件是在机器人入口文件中完成的，需要在框架初始化之后，运行之前进行。\n\n请注意，加载的插件模块名称（插件文件名或文件夹名）**不能相同**，且每一个插件**只能被加载一次**，重复加载将会导致异常。\n\n如果你使用 `nb-cli` 管理插件，那么你可以跳过这一节，`nb-cli` 将会自动处理加载。\n\n如果你**使用自定义的入口文件** `bot.py`，那么你需要在 `bot.py` 中加载插件。\n\n```python {5} title=bot.py\nimport nonebot\n\nnonebot.init()\n\n# 加载插件\n\nnonebot.run()\n```\n\n加载插件的方式有多种，但在底层的加载逻辑是一致的。以下是为加载插件提供的几种方式：\n\n### `load_plugin`\n\n通过点分割模块名称或使用 [`pathlib`](https://docs.python.org/zh-cn/3/library/pathlib.html) 的 `Path` 对象来加载插件。通常用于加载第三方插件或者项目插件。例如：\n\n```python\nfrom pathlib import Path\n\nnonebot.load_plugin(\"path.to.your.plugin\")  # 加载第三方插件\nnonebot.load_plugin(Path(\"./path/to/your/plugin.py\"))  # 加载项目插件\n```\n\n:::caution 注意\n请注意，本地插件的路径应该为相对机器人 **入口文件（通常为 bot.py）** 可导入的，例如在项目 `plugins` 目录下。\n:::\n\n### `load_plugins`\n\n加载传入插件目录中的所有插件，通常用于加载一系列本地编写的项目插件。例如：\n\n```python\nnonebot.load_plugins(\"src/plugins\", \"path/to/your/plugins\")\n```\n\n:::caution 注意\n请注意，插件目录应该为相对机器人 **入口文件（通常为 bot.py）** 可导入的，例如在项目 `plugins` 目录下。\n:::\n\n### `load_all_plugins`\n\n这种加载方式是以上两种方式的混合，加载所有传入的插件模块名称，以及所有给定目录下的插件。例如：\n\n```python\nnonebot.load_all_plugins([\"path.to.your.plugin\"], [\"path/to/your/plugins\"])\n```\n\n### `load_from_json`\n\n通过 JSON 文件加载插件，是 [`load_all_plugins`](#load_all_plugins) 的 JSON 变种。通过读取 JSON 文件中的 `plugins` 字段和 `plugin_dirs` 字段进行加载。例如：\n\n```json title=plugin_config.json\n{\n  \"plugins\": [\"path.to.your.plugin\"],\n  \"plugin_dirs\": [\"path/to/your/plugins\"]\n}\n```\n\n```python\nnonebot.load_from_json(\"plugin_config.json\", encoding=\"utf-8\")\n```\n\n:::tip 提示\n如果 JSON 配置文件中的字段无法满足你的需求，可以使用 [`load_all_plugins`](#load_all_plugins) 方法自行读取配置来加载插件。\n:::\n\n### `load_from_toml`\n\n通过 TOML 文件加载插件，是 [`load_all_plugins`](#load_all_plugins) 的 TOML 变种。通过读取 TOML 文件中的 `[tool.nonebot]` Table 中的 `plugin_dirs` Array 与\n`[tool.nonebot.plugins]` Table 中的多个 Array 进行加载。例如：\n\n```toml title=plugin_config.toml\n[tool.nonebot]\nplugin_dirs = [\"path/to/your/plugins\"]\n\n[tool.nonebot.plugins]\n\"@local\" = [\"path.to.your.plugin\"]  # 本地插件等非插件商店来源的插件\n\"nonebot-plugin-someplugin\" = [\"nonebot_plugin_someplugin\"]  # 插件商店来源的插件\n```\n\n```python\nnonebot.load_from_toml(\"plugin_config.toml\", encoding=\"utf-8\")\n```\n\n:::tip 提示\n如果 TOML 配置文件中的字段无法满足你的需求，可以使用 [`load_all_plugins`](#load_all_plugins) 方法自行读取配置来加载插件。\n:::\n\n### `load_builtin_plugin`\n\n加载一个内置插件，传入的插件名必须为 NoneBot 内置插件。该方法是 [`load_plugin`](#load_plugin) 的封装。例如：\n\n```python\nnonebot.load_builtin_plugin(\"echo\")\n```\n\n### `load_builtin_plugins`\n\n加载传入插件列表中的所有内置插件。例如：\n\n```python\nnonebot.load_builtin_plugins(\"echo\", \"single_session\")\n```\n\n### 其他加载方式\n\n有关其他插件加载的方式，可参考[跨插件访问](../advanced/requiring.md)和[嵌套插件](../advanced/plugin-nesting.md)。\n"
  },
  {
    "path": "website/docs/tutorial/event-data.mdx",
    "content": "---\nsidebar_position: 6\ndescription: 通过依赖注入获取所需事件信息\n\noptions:\n  menu:\n    - category: tutorial\n      weight: 80\n---\n\n# 获取事件信息\n\nimport Messenger from \"@site/src/components/Messenger\";\n\n在 NoneBot 事件处理流程中，获取事件信息并做出对应的操作是非常常见的场景。本章节中我们将介绍如何通过**依赖注入**获取事件信息。\n\n## 认识依赖注入\n\n在事件处理流程中，事件响应器具有自己独立的上下文，例如：当前响应的事件、收到事件的机器人或者其他处理流程中新增的信息等。这些数据可以根据我们的需求，通过依赖注入的方式，在执行事件处理流程中注入到事件处理函数中。\n\n相对于传统的信息获取方法，通过依赖注入获取信息的最大特色在于**按需获取**。如果该事件处理函数不需要任何额外信息即可运行，那么可以不进行依赖注入。如果事件处理函数需要额外的数据，可以通过依赖注入的方式灵活的标注出需要的依赖，在函数运行时便会被按需注入。\n\n## 使用依赖注入\n\n使用依赖注入获取上下文信息的方法十分简单，我们仅需要在函数的参数中声明所需的依赖，并正确的将函数添加为事件处理依赖即可。在 NoneBot 中，我们可以直接使用 `nonebot.params` 模块中定义的参数类型来声明依赖。\n\n例如，我们可以继续改进上一章节中的 `weather` 插件，使其可以获取到 `天气` 命令的地名参数，并根据地名返回天气信息。\n\n```python {9,11} title=weather/__init__.py\nfrom nonebot import on_command\nfrom nonebot.rule import to_me\nfrom nonebot.adapters import Message\nfrom nonebot.params import CommandArg\n\nweather = on_command(\"天气\", rule=to_me(), aliases={\"weather\", \"查天气\"}, priority=10, block=True)\n\n@weather.handle()\nasync def handle_function(args: Message = CommandArg()):\n    # 提取参数纯文本作为地名，并判断是否有效\n    if location := args.extract_plain_text():\n        await weather.finish(f\"今天{location}的天气是...\")\n    else:\n        await weather.finish(\"请输入地名\")\n```\n\n如上方示例所示，我们使用了 `args` 作为注入参数名，注入的内容为 `CommandArg()`，也就是**消息命令后跟随的内容**。在这个示例中，我们获得的参数会被检查是否有效，对无效参数则会结束事件。\n\n:::tip 提示\n命令与参数之间可以不需要空格，`CommandArg()` 获取的信息为命令后跟随的内容并去除了头部空白符。例如：`/天气 上海` 消息的参数为 `上海`。\n:::\n\n:::tip 提示\n`:=` 是 Python 3.8 引入的新语法 [Assignment Expressions](https://docs.python.org/zh-cn/3/reference/expressions.html#assignment-expressions)，也称为海象表达式，可以在表达式中直接赋值。\n:::\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/天气\" },\n    { position: \"left\", msg: \"请输入地名\" },\n    { position: \"right\", msg: \"/天气 上海\" },\n    { position: \"left\", msg: \"今天上海的天气是...\" },\n  ]}\n/>\n\nNoneBot 提供了多种依赖注入类型，可以获取不同的信息，具体内容可参考[依赖注入](../advanced/dependency.mdx)。\n"
  },
  {
    "path": "website/docs/tutorial/fundamentals.md",
    "content": "---\nsidebar_position: 1\ndescription: NoneBot 机器人构成及基本使用\n\noptions:\n  menu:\n    - category: tutorial\n      weight: 30\n---\n\n# 机器人的构成\n\n了解机器人的基本构成有助于你更好地使用 NoneBot，本章节将介绍 NoneBot 中的基本组成部分，稍后的文档中将会使用到这些概念。\n\n使用 NoneBot 框架搭建的机器人具有以下几个基本组成部分：\n\n1. NoneBot 机器人框架主体：负责连接各个组成部分，提供基本的机器人功能\n2. 驱动器 `Driver`：客户端/服务端的功能实现，负责接收和发送消息（通常为 HTTP 通信）\n3. 适配器 `Adapter`：驱动器的上层，负责将**平台消息**与 NoneBot 事件/操作系统的消息格式相互转换\n4. 插件 `Plugin`：机器人的功能实现，通常为负责处理事件并进行一系列的操作\n\n除 NoneBot 机器人框架主体外，其他部分均可按需选择、互相搭配，但由于平台的兼容性问题，部分插件可能仅在某些特定平台上可用（这由插件编写者决定）。\n\n在接下来的章节中，我们将重点介绍机器人功能实现，即插件 `Plugin` 部分。\n"
  },
  {
    "path": "website/docs/tutorial/handler.mdx",
    "content": "---\nsidebar_position: 5\ndescription: 处理接收到的特定事件\n\noptions:\n  menu:\n    - category: tutorial\n      weight: 70\n---\n\n# 事件处理\n\nimport Messenger from \"@site/src/components/Messenger\";\n\n在我们收到事件，并被某个事件响应器正确响应后，便正式开启了对于这个事件的**处理流程**。\n\n## 认识事件处理流程\n\n就像我们在解决问题时需要遵循流程一样，处理一个事件也需要一套流程。在事件响应器对一个事件进行响应之后，会依次执行一系列的**事件处理依赖**（通常是函数）。简单来说，事件处理流程并不是一个函数、一个对象或一个方法，而是一整套由开发者设计的流程。\n\n在这个流程中，我们**目前**只需要了解两个概念：函数形式的“事件处理依赖”（下称“事件处理函数”）和“事件响应器操作”。\n\n## 事件处理函数\n\n在事件响应器中，事件处理流程可以由一个或多个“事件处理函数”组成，这些事件处理函数将会按照顺序依次对事件进行处理，直到全部执行完成或被中断。我们可以采用事件响应器的“事件处理函数装饰器”来添加这些“事件处理函数”。\n\n顾名思义，“事件处理函数装饰器”是一个[装饰器（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)中所展示的包装用法相同。例如：\n\n```python {6-8} title=weather/__init__.py\nfrom nonebot import on_command\nfrom nonebot.rule import to_me\n\nweather = on_command(\"天气\", rule=to_me(), aliases={\"weather\", \"查天气\"}, priority=10, block=True)\n\n@weather.handle()\nasync def handle_function():\n    pass  # do something here\n```\n\n如上方示例所示，我们使用 `weather` 响应器的 `handle` 装饰器装饰了一个函数 `handle_function`。`handle_function` 函数会被添加到 `weather` 的事件处理流程中。在 `weather` 响应器被触发之后，将会依次调用 `weather` 响应器的事件处理函数，即 `handle_function` 来对事件进行处理。\n\n## 事件响应器操作\n\n在事件处理流程中，我们可以使用事件响应器操作来进行一些交互或改变事件处理流程，例如向机器人用户发送消息或提前结束事件处理流程等。\n\n事件响应器操作与事件处理函数装饰器类似，通常作为事件响应器 `Matcher` 的[类方法](https://docs.python.org/zh-cn/3/library/functions.html#classmethod)存在，因此事件响应器操作的调用方法也是 `Matcher.func()` 的形式。不过不同的是，事件响应器操作并不是装饰器，因此并不需要@进行标注。\n\n```python {8,9} title=weather/__init__.py\nfrom nonebot import on_command\nfrom nonebot.rule import to_me\n\nweather = on_command(\"天气\", rule=to_me(), aliases={\"weather\", \"查天气\"}, priority=10, block=True)\n\n@weather.handle()\nasync def handle_function():\n    # await weather.send(\"天气是...\")\n    await weather.finish(\"天气是...\")\n```\n\n如上方示例所示，我们使用 `weather` 响应器的 `finish` 操作方法向机器人用户回复了 `天气是...` 并结束了事件处理流程。效果如下：\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/天气\" },\n    { position: \"left\", msg: \"天气是...\" },\n  ]}\n/>\n\n值得注意的是，在执行 `finish` 方法时，NoneBot 会在向机器人用户发送消息内容后抛出 `FinishedException` 异常来结束事件响应流程。也就是说，在 `finish` 被执行后，后续的程序是不会被执行的。如果你需要回复机器人用户消息但不想事件处理流程结束，可以使用注释的部分中展示的 `send` 方法。\n\n:::danger 警告\n由于 `finish` 是通过抛出 `FinishedException` 异常来结束事件的，因此异常可能会被未加限制的 `try-except` 捕获，影响事件处理流程正确处理，导致无法正常结束此事件。请务必在异常捕获中指定错误类型或排除所有 [MatcherException](../api/exception.md#MatcherException) 类型的异常（如下所示），或将 `finish` 移出捕获范围进行使用。\n\n```python\nfrom nonebot.exception import MatcherException\n\ntry:\n    await weather.finish(\"天气是...\")\nexcept MatcherException:\n    raise\nexcept Exception as e:\n    pass # do something here\n```\n\n:::\n\n目前 NoneBot 提供了多种事件响应器操作，其中包括用于机器人用户交互与流程控制两大类，进阶使用方法可以查看[会话控制](../appendices/session-control.mdx)。\n"
  },
  {
    "path": "website/docs/tutorial/matcher.md",
    "content": "---\nsidebar_position: 4\ndescription: 响应接收到的特定事件\n\noptions:\n  menu:\n    - category: tutorial\n      weight: 60\n---\n\n# 事件响应器\n\n事件响应器（Matcher）是对接收到的事件进行响应的基本单元，所有的事件响应器都继承自 `Matcher` 基类。\n\n在 NoneBot 中，事件响应器可以通过一系列特定的规则**筛选**出**具有某种特征的事件**，并按照**特定的流程**交由**预定义的事件处理依赖**进行处理。例如，在[快速上手](../quick-start.mdx)中，我们使用了内置插件 `echo` ，它定义的事件响应器能响应机器人用户发送的“/echo hello world”消息，提取“hello world”信息并作为回复消息发送。\n\n## 事件响应器辅助函数\n\nNoneBot 中所有事件响应器均继承自 `Matcher` 基类，但直接使用 `Matcher.new()` 方法创建事件响应器过于繁琐且不能记录插件信息。因此，NoneBot 中提供了一系列“事件响应器辅助函数”（下称“辅助函数”）来辅助我们用**最简的方式**创建**带有不同规则预设**的事件响应器，提高代码可读性和书写效率。通常情况下，我们只需要使用辅助函数即可完成事件响应器的创建。\n\n在 NoneBot 中，辅助函数以 `on()` 或 `on_<type/rule>()` 形式出现（例如 `on_command()`），调用后根据不同的参数返回一个 `Type[Matcher]` 类型的新事件响应器。\n\n目前 NoneBot 提供了多种功能各异的辅助函数、具有共同命令名称前缀的命令组以及具有共同参数的响应器组，均可以从 `nonebot` 模块直接导入使用，具体内容参考[事件响应器进阶](../advanced/matcher.md)。\n\n## 创建事件响应器\n\n在上一节[创建插件](./create-plugin.md#创建插件)中，我们创建了一个 `weather` 插件，现在我们来实现他的功能。\n\n我们直接使用 `on_command()` 辅助函数来创建一个事件响应器：\n\n```python {3} title=weather/__init__.py\nfrom nonebot import on_command\n\nweather = on_command(\"天气\")\n```\n\n这样，我们就获得一个名为 `weather` 的事件响应器了，这个事件响应器会对 `/天气` 开头的消息进行响应。\n\n:::tip 提示\n如果一条消息中包含“@机器人”或以“机器人的昵称”开始，例如 `@bot /天气` 时，协议适配器会将 `event.is_tome()` 判断为 `True` ，同时也会自动去除 `@bot`，即事件响应器收到的信息内容为 `/天气`，方便进行命令匹配。\n:::\n\n### 为事件响应器添加参数\n\n在辅助函数中，我们可以添加一些参数来对事件响应器进行更加精细的调整，例如事件响应器的优先级、匹配规则等。例如：\n\n```python {4} title=weather/__init__.py\nfrom nonebot import on_command\nfrom nonebot.rule import to_me\n\nweather = on_command(\"天气\", rule=to_me(), aliases={\"weather\", \"查天气\"}, priority=10, block=True)\n```\n\n这样，我们就获得了一个可以响应 `天气`、`weather`、`查天气` 三个命令的响应规则，需要私聊或 `@bot` 时才会响应，优先级为 10（越小越优先），阻断事件向后续优先级传播的事件响应器了。这些内容的意义和使用方法将会在后续的章节中一一介绍。\n\n:::tip 提示\n需要注意的是，不同的辅助函数有不同的可选参数，在使用之前可以参考[事件响应器进阶 - 基本辅助函数](../advanced/matcher.md#基本辅助函数)或 [API 文档](../api/plugin/on.md#on)。\n:::\n"
  },
  {
    "path": "website/docs/tutorial/message.md",
    "content": "---\nsidebar_position: 7\ndescription: 处理消息序列与消息段\n\noptions:\n  menu:\n    - category: tutorial\n      weight: 90\n---\n\n# 处理消息\n\n在不同平台中，一条消息可能会有承载有各种不同的表现形式，它可能是一段纯文本、一张图片、一段语音、一篇富文本文章，也有可能是多种类型的组合等等。\n\n在 NoneBot 中，为确保消息的正常处理与跨平台兼容性，采用了扁平化的消息序列形式，即 `Message` 对象。消息序列是 NoneBot 中的消息载体，无论是接收还是发送的消息，都采用消息序列的形式进行处理。\n\n## 认识消息类型\n\n### 消息序列 `Message`\n\n在 NoneBot 中，消息序列 `Message` 的主要作用是用于表达“一串消息”。由于消息序列继承自 `List[MessageSegment]`，所以 `Message` 的本质是由若干消息段所组成的序列。因此，消息序列的使用方法与 `List` 有很多相似之处，例如切片、索引、拼接等。\n\n在上一节的[使用依赖注入](./event-data.mdx#使用依赖注入)中，我们已经通过依赖注入 `CommandArg()` 获取了命令的参数，它的类型即是消息序列。我们使用了消息序列的 `extract_plain_text()` 方法来获取消息序列中的纯文本内容。\n\n### 消息段 `MessageSegment`\n\n顾名思义，消息段 `MessageSegment` 是一段消息。由于消息序列的本质是由若干消息段所组成的序列，消息段可以被认为是构成消息序列的最小单位。简单来说，消息序列类似于一个自然段，而消息段则是组成自然段的一句话。同时，作为特殊消息载体的存在，绝大多数的平台都有着**独特的消息类型**，这些独特的内容均需要由对应的**协议适配器**所提供，以适应不同平台中的消息模式。**这也意味着，你需要导入对应的协议适配器中的消息序列和消息段后才能使用其特殊的工厂方法。**\n\n:::caution 注意\n消息段的类型是由协议适配器提供的，因此你需要参考协议适配器的文档并导入对应的消息段后才能使用其特殊的消息类型。\n\n在上一节的[使用依赖注入](./event-data.mdx#使用依赖注入)中，我们导入的为 `nonebot.adapters.Message` 抽象基类，因此我们无法使用平台特有的消息类型。仅能使用 `str` 作为纯文本消息回复。\n:::\n\n## 使用消息序列\n\n:::caution 注意\n在以下的示例中，为了更好的理解多种类型的消息组成方式，我们将使用 `Console` 协议适配器来演示消息序列的使用方法。在实际使用中，你需要确保你使用的**消息序列类型**与你所要发送的**平台类型**一致。\n:::\n\n通常情况下，适配器在接收到消息时，会将消息转换为消息序列，可以通过依赖注入 [`EventMessage`](../advanced/dependency.mdx#eventmessage)，或者使用 `event.get_message()` 获取。\n\n由于消息序列是 `List[MessageSegment]` 的子类，所以你总是可以用和操作 `List` 类似的方式来处理消息序列。例如：\n\n```python\n>>> from nonebot.adapters.console import Message, MessageSegment\n>>> message = Message([\n    MessageSegment(type=\"text\", data={\"text\":\"hello\"}),\n    MessageSegment(type=\"markdown\", data={\"markup\":\"**world**\"}),\n])\n>>> for segment in message:\n...     print(segment.type, segment.data)\n...\ntext {'text': 'hello'}\nmarkdown {'markup': '**world**'}\n>>> len(message)\n2\n```\n\n### 构造消息序列\n\n在使用事件响应器操作发送消息时，既可以使用 `str` 作为消息，也可以使用 `Message`、`MessageSegment` 或者 `MessageTemplate`。那么，我们就需要先构造一个消息序列。消息序列可以通过多种方式构造：\n\n#### 直接构造\n\n`Message` 类可以直接实例化，支持 `str`、`MessageSegment`、`Iterable[MessageSegment]` 或适配器自定义类型的参数。\n\n```python\nfrom nonebot.adapters.console import Message, MessageSegment\n\n# str\nMessage(\"Hello, world!\")\n# MessageSegment\nMessage(MessageSegment.text(\"Hello, world!\"))\n# List[MessageSegment]\nMessage([MessageSegment.text(\"Hello, world!\")])\n```\n\n#### 运算构造\n\n`Message` 对象可以通过 `str`、`MessageSegment` 相加构造，详情请参考[拼接消息](#拼接消息)。\n\n#### 从字典数组构造\n\n`Message` 对象支持 Pydantic 自定义类型构造，可以使用 Pydantic 的 `TypeAdapter` 方法进行构造。\n\n```python\nfrom pydantic import TypeAdapter\nfrom nonebot.adapters.console import Message, MessageSegment\n\n# 由字典构造消息段\nTypeAdapter(MessageSegment).validate_python(\n    {\"type\": \"text\", \"data\": {\"text\": \"text\"}}\n) == MessageSegment.text(\"text\")\n\n# 由字典数组构造消息序列\nTypeAdapter(Message).validate_python(\n    [MessageSegment.text(\"text\"), {\"type\": \"text\", \"data\": {\"text\": \"text\"}}],\n) == Message([MessageSegment.text(\"text\"), MessageSegment.text(\"text\")])\n```\n\n### 获取消息纯文本\n\n由于消息中存在各种类型的消息段，因此 `str(message)` 通常**不能得到消息的纯文本**，而是一个消息序列的字符串表示。\n\nNoneBot 为消息段定义了一个方法 `is_text()` ，可以用于判断消息段是否为纯文本；也可以使用 `message.extract_plain_text()` 方法获取消息纯文本。\n\n```python\nfrom nonebot.adapters.console import Message, MessageSegment\n\n# 判断消息段是否为纯文本\nMessageSegment.text(\"text\").is_text() == True\n\n# 提取消息纯文本字符串\nMessage(\n    [MessageSegment.text(\"text\"), MessageSegment.markdown(\"**markup**\")]\n).extract_plain_text() == \"text\"\n```\n\n### 遍历\n\n消息序列继承自 `List[MessageSegment]` ，因此可以使用 `for` 循环遍历消息段。\n\n```python\nfor segment in message:\n    ...\n```\n\n### 比较\n\n消息和消息段都可以使用 `==` 或 `!=` 运算符比较是否相同。\n\n```python\nMessageSegment.text(\"text\") != MessageSegment.text(\"foo\")\n\nsome_message == Message([MessageSegment.text(\"text\")])\n```\n\n### 检查消息段\n\n我们可以通过 `in` 运算符或消息序列的 `has` 方法来：\n\n```python\n# 是否存在消息段\nMessageSegment.text(\"text\") in message\n# 是否存在指定类型的消息段\n\"text\" in message\n```\n\n我们还可以使用消息序列的 `only` 方法来检查消息中是否仅包含指定的消息段。\n\n```python\n# 是否都为指定消息段\nmessage.only(MessageSegment.text(\"test\"))\n# 是否仅包含指定类型的消息段\nmessage.only(\"text\")\n```\n\n### 过滤、索引与切片\n\n消息序列对列表的索引与切片进行了增强，在原有列表 `int` 索引与 `slice` 切片的基础上，支持 `type` 过滤索引与切片。\n\n```python\nfrom nonebot.adapters.console import Message, MessageSegment\n\nmessage = Message(\n    [\n        MessageSegment.text(\"test\"),\n        MessageSegment.markdown(\"test2\"),\n        MessageSegment.markdown(\"test3\"),\n        MessageSegment.text(\"test4\"),\n    ]\n)\n# 索引\nmessage[0] == MessageSegment.text(\"test\")\n# 切片\nmessage[0:2] == Message(\n    [MessageSegment.text(\"test\"), MessageSegment.markdown(\"test2\")]\n)\n# 类型过滤\nmessage[\"markdown\"] == Message(\n    [MessageSegment.markdown(\"test2\"), MessageSegment.markdown(\"test3\")]\n)\n# 类型索引\nmessage[\"markdown\", 0] == MessageSegment.markdown(\"test2\")\n# 类型切片\nmessage[\"markdown\", 0:2] == Message(\n    [MessageSegment.markdown(\"test2\"), MessageSegment.markdown(\"test3\")]\n)\n```\n\n我们也可以通过消息序列的 `include`、`exclude` 方法进行类型过滤。\n\n```python\nmessage.include(\"text\", \"markdown\")\nmessage.exclude(\"text\")\n```\n\n同样的，消息序列对列表的 `index`、`count` 方法也进行了增强，可以用于索引指定类型的消息段。\n\n```python\n# 指定类型首个消息段索引\nmessage.index(\"markdown\") == 1\n# 指定类型消息段数量\nmessage.count(\"markdown\") == 2\n```\n\n此外，消息序列添加了一个 `get` 方法，可以用于获取指定类型指定个数的消息段。\n\n```python\n# 获取指定类型指定个数的消息段\nmessage.get(\"markdown\", 1) == Message([MessageSegment.markdown(\"test2\")])\n```\n\n### 拼接消息\n\n`str`、`Message`、`MessageSegment` 对象之间可以直接相加，相加均会返回一个新的 `Message` 对象。\n\n```python\n# 消息序列与消息段相加\nMessage([MessageSegment.text(\"text\")]) + MessageSegment.text(\"text\")\n# 消息序列与字符串相加\nMessage([MessageSegment.text(\"text\")]) + \"text\"\n# 消息序列与消息序列相加\nMessage([MessageSegment.text(\"text\")]) + Message([MessageSegment.text(\"text\")])\n# 字符串与消息序列相加\n\"text\" + Message([MessageSegment.text(\"text\")])\n# 消息段与消息段相加\nMessageSegment.text(\"text\") + MessageSegment.text(\"text\")\n# 消息段与字符串相加\nMessageSegment.text(\"text\") + \"text\"\n# 消息段与消息序列相加\nMessageSegment.text(\"text\") + Message([MessageSegment.text(\"text\")])\n# 字符串与消息段相加\n\"text\" + MessageSegment.text(\"text\")\n```\n\n如果需要在当前消息序列后直接拼接新的消息段，可以使用 `Message.append`、`Message.extend` 方法，或者使用自加。\n\n```python\nmsg = Message([MessageSegment.text(\"text\")])\n# 自加\nmsg += \"text\"\nmsg += MessageSegment.text(\"text\")\nmsg += Message([MessageSegment.text(\"text\")])\n# 附加\nmsg.append(\"text\")\nmsg.append(MessageSegment.text(\"text\"))\n# 扩展\nmsg.extend([MessageSegment.text(\"text\")])\n```\n\n我们也可以通过消息段或消息序列的 `join` 方法来拼接一串消息：\n\n```python\nseg = MessageSegment.text(\"text\")\nmsg = seg.join(\n    [\n        MessageSegment.text(\"first\"),\n        Message(\n            [\n                MessageSegment.text(\"second\"),\n                MessageSegment.text(\"third\"),\n            ]\n        )\n    ]\n)\nmsg == Message(\n    [\n        MessageSegment.text(\"first\"),\n        MessageSegment.text(\"text\"),\n        MessageSegment.text(\"second\"),\n        MessageSegment.text(\"third\"),\n    ]\n)\n```\n\n### 使用消息模板\n\n为了提供安全可靠的跨平台模板字符，我们提供了一个消息模板功能来构建消息序列\n\n它在以下常见场景中尤其有用：\n\n- 多行富文本编排（包含图片，文字以及表情等）\n- 客制化（由 Bot 最终用户提供消息模板时）\n\n在事实上，它的用法和 `str.format` 极为相近，所以你在使用的时候，总是可以参考[Python 文档](https://docs.python.org/zh-cn/3/library/stdtypes.html#str.format)来达到你想要的效果，这里给出几个简单的例子。\n\n默认情况下，消息模板采用 `str` 纯文本形式的格式化：\n\n```python title=基础格式化用法\n>>> from nonebot.adapters import MessageTemplate\n>>> MessageTemplate(\"{} {}\").format(\"hello\", \"world\")\n'hello world'\n```\n\n如果 `Message.template` 构建消息模板，那么消息模板将采用消息序列形式的格式化，此时的消息将会是平台特定的：\n\n:::caution 注意\n使用 `Message.template` 构建消息模板时，应注意消息序列为平台适配器提供的类型，不能使用 `nonebot.adapters.Message` 基类作为模板构建。使用基类构建模板与使用 `str` 构建模板的效果是一样的，因此请使用上述的 `MessageTemplate` 类直接构建模板。：\n:::\n\n```python title=平台格式化用法\n>>> from nonebot.adapters.console import Message, MessageSegment\n>>> Message.template(\"{} {}\").format(\"hello\", \"world\")\nMessage(\n    MessageSegment.text(\"hello\"),\n    MessageSegment.text(\" \"),\n    MessageSegment.text(\"world\")\n)\n```\n\n消息模板支持使用消息段进行格式化：\n\n```python title=对消息段进行安全的拼接\n>>> from nonebot.adapters.console import Message, MessageSegment\n>>> Message.template(\"{}{}\").format(MessageSegment.markdown(\"**markup**\"), \"world\")\nMessage(\n    MessageSegment(type='markdown', data={'markup': '**markup**'}),\n    MessageSegment(type='text', data={'text': 'world'})\n)\n```\n\n消息模板同样支持使用消息序列作为模板：\n\n```python title=以消息对象作为模板\n>>> from nonebot.adapters.console import Message, MessageSegment\n>>> Message.template(\n...     MessageSegment.text(\"{user_id}\") + MessageSegment.emoji(\"tada\") +\n...     MessageSegment.text(\"{message}\")\n... ).format_map({\"user_id\": 123456, \"message\": \"hello world\"})\nMessage(\n    MessageSegment(type='text', data={'text': '123456'}),\n    MessageSegment(type='emoji', data={'emoji': 'tada'}),\n    MessageSegment(type='text', data={'text': 'hello world'})\n)\n```\n\n:::caution 注意\n只有消息序列中的文本类型消息段才能被格式化，其他类型的消息段将会原样添加。\n:::\n\n消息模板支持使用拓展控制符来控制消息段类型：\n\n```python title=使用消息段的拓展控制符\n>>> from nonebot.adapters.console import Message, MessageSegment\n>>> Message.template(\"{name:emoji}\").format(name='tada')\nMessage(MessageSegment(type='emoji', data={'name': 'tada'}))\n```\n"
  },
  {
    "path": "website/docs/tutorial/store.mdx",
    "content": "---\nsidebar_position: 2\ndescription: 从商店安装适配器和插件\n\noptions:\n  menu:\n    - category: tutorial\n      weight: 40\n---\n\n# 获取商店内容\n\nimport Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Asciinema from \"@site/src/components/Asciinema\";\n\n:::tip 提示\n\n如果你暂时没有获取商店内容的需求，可以跳过本章节。\n\n:::\n\nNoneBot 提供了一个[商店](/store/plugins)，商店内容均由社区开发者贡献。你可以在商店中查找你需要的适配器和插件等，进行安装或者参考其文档等。\n\n商店中每个内容的卡片都包含了其名称和简介等信息，点击**卡片右上角**链接图标即可跳转到其主页。\n\n与此同时，NB-CLI 也提供了一个 TUI 版本的商店界面，可通过 `nb adapter store`、`nb plugin store`、`nb driver store` 命令或 CLI\n交互式界面进入。其提供了接近网页商店的体验，同时允许快捷安装到当前项目。\n\n## 安装插件\n\n<Asciinema\n  url=\"https://asciinema.org/a/569650.cast\"\n  options={{ theme: \"monokai\", poster: \"npt:16.8\" }}\n/>\n\n在商店插件页面中，点击你需要安装的插件下方的 `点击复制安装命令` 按钮，即可复制 `nb-cli` 命令。\n\n请在你的**项目目录**下执行该命令。`nb-cli` 会自动安装插件并将其添加到加载列表中。\n\n<Tabs groupId=\"cli-install\">\n  <TabItem value=\"cli\" label=\"使用命令\" default>\n\n```bash\nnb plugin install <插件名称>\n```\n\n  </TabItem>\n  <TabItem value=\"interactive\" label=\"交互式安装\">\n\n```bash\n$ nb plugin install\n[?] 想要安装的插件名称: <插件名称>\n```\n\n  </TabItem>\n  <TabItem value=\"pip\" label=\"使用 pip\">\n\n```bash\npip install <插件包名>\n```\n\n插件包名可以在商店插件卡片中找到，或者使用 `nb-cli` 搜索插件显示的详情中找到。安装完成后，需要参考[加载插件章节](./create-plugin.md#加载插件)自行加载。\n\n  </TabItem>\n</Tabs>\n\n如果想要查看插件列表，可以使用以下命令\n\n```bash\n# 列出商店所有插件\nnb plugin list\n# 搜索商店插件\nnb plugin search [可选关键词]\n```\n\n升级和卸载插件可以使用以下命令\n\n<Tabs groupId=\"cli-install\">\n  <TabItem value=\"cli\" label=\"使用命令\" default>\n\n```bash\nnb plugin update <插件名称>\nnb plugin uninstall <插件名称>\n```\n\n  </TabItem>\n  <TabItem value=\"interactive\" label=\"交互式安装\">\n\n```bash\n$ nb plugin update\n[?] 想要安装的插件名称: <插件名称>\n$ nb plugin uninstall\n[?] 想要卸载的插件名称: <插件名称>\n```\n\n  </TabItem>\n  <TabItem value=\"pip\" label=\"使用 pip\">\n\n```bash\npip install --upgrade <插件包名>\npip uninstall <插件包名>\n```\n\n插件包名可以在商店插件卡片中找到，或者使用 `nb-cli` 搜索插件显示的详情中找到。卸载完成后，需要自行移除插件加载。\n\n  </TabItem>\n</Tabs>\n\n## 安装适配器\n\n<Asciinema\n  url=\"https://asciinema.org/a/569664.cast\"\n  options={{ theme: \"monokai\", poster: \"npt:12.0\" }}\n/>\n\n安装适配器与安装插件类似，只是将命令换为 `nb adapter`，这里就不再赘述。\n\n请在你的**项目目录**下执行该命令。`nb-cli` 会自动安装适配器并将其添加到注册列表中。\n\n<Tabs groupId=\"cli-install\">\n  <TabItem value=\"cli\" label=\"使用命令\" default>\n\n```bash\nnb adapter install <适配器名称>\n```\n\n  </TabItem>\n  <TabItem value=\"interactive\" label=\"交互式安装\">\n\n```bash\n$ nb adapter install\n[?] 想要安装的适配器名称: <适配器名称>\n```\n\n  </TabItem>\n  <TabItem value=\"pip\" label=\"使用 pip\">\n\n```bash\npip install <适配器包名>\n```\n\n适配器包名可以在商店适配器卡片中找到，或者使用 `nb-cli` 搜索适配器显示的详情中找到。安装完成后，需要参考[注册适配器章节](../advanced/adapter.md#注册适配器)自行注册。\n\n  </TabItem>\n</Tabs>\n\n如果想要查看适配器列表，可以使用以下命令\n\n```bash\n# 列出商店所有适配器\nnb adapter list\n# 搜索商店适配器\nnb adapter search [可选关键词]\n```\n\n升级和卸载适配器可以使用以下命令\n\n<Tabs groupId=\"cli-install\">\n  <TabItem value=\"cli\" label=\"使用命令\" default>\n\n```bash\nnb adapter update <适配器名称>\nnb adapter uninstall <适配器名称>\n```\n\n  </TabItem>\n  <TabItem value=\"interactive\" label=\"交互式安装\">\n\n```bash\n$ nb adapter update\n[?] 想要安装的适配器名称: <适配器名称>\n$ nb adapter uninstall\n[?] 想要卸载的适配器名称: <适配器名称>\n```\n\n  </TabItem>\n  <TabItem value=\"pip\" label=\"使用 pip\">\n\n```bash\npip install --upgrade <适配器包名>\npip uninstall <适配器包名>\n```\n\n适配器包名可以在商店适配器卡片中找到，或者使用 `nb-cli` 搜索适配器显示的详情中找到。卸载完成后，需要自行移除适配器加载。\n\n  </TabItem>\n</Tabs>\n\n## 安装驱动器\n\n<Asciinema\n  url=\"https://asciinema.org/a/569665.cast\"\n  options={{ theme: \"monokai\", poster: \"npt:14.0\" }}\n/>\n\n安装驱动器与安装插件同样类似，只是将命令换为 `nb driver`，这里就不再赘述。\n\n如果你使用了虚拟环境，请在你的**项目目录**下执行该命令，`nb-cli` 会自动安装驱动器到虚拟环境中。\n\n请注意 `nb-cli` 并不会在安装驱动器后修改项目所使用的驱动器，请自行参考[配置方法](../appendices/config.mdx)章节以及 [`DRIVER` 配置项](../appendices/config.mdx#driver)修改驱动器。\n\n<Tabs groupId=\"cli-install\">\n  <TabItem value=\"cli\" label=\"使用命令\" default>\n\n```bash\nnb driver install <驱动器名称>\n```\n\n  </TabItem>\n  <TabItem value=\"interactive\" label=\"交互式安装\">\n\n```bash\n$ nb driver install\n[?] 想要安装的驱动器名称: <驱动器名称>\n```\n\n  </TabItem>\n  <TabItem value=\"pip\" label=\"使用 pip\">\n\n```bash\npip install <驱动器包名>\n```\n\n驱动器包名可以在商店驱动器卡片中找到，或者使用 `nb-cli` 搜索驱动器显示的详情中找到。\n\n  </TabItem>\n</Tabs>\n\n如果想要查看驱动器列表，可以使用以下命令\n\n```bash\n# 列出商店所有驱动器\nnb driver list\n# 搜索商店驱动器\nnb driver search [可选关键词]\n```\n\n升级和卸载驱动器可以使用以下命令\n\n<Tabs groupId=\"cli-install\">\n  <TabItem value=\"cli\" label=\"使用命令\" default>\n\n```bash\nnb driver update <驱动器名称>\nnb driver uninstall <驱动器名称>\n```\n\n  </TabItem>\n  <TabItem value=\"interactive\" label=\"交互式安装\">\n\n```bash\n$ nb driver update\n[?] 想要安装的驱动器名称: <驱动器名称>\n$ nb driver uninstall\n[?] 想要卸载的驱动器名称: <驱动器名称>\n```\n\n  </TabItem>\n  <TabItem value=\"pip\" label=\"使用 pip\">\n\n```bash\npip install --upgrade <驱动器包名>\npip uninstall <驱动器包名>\n```\n\n驱动器包名可以在商店驱动器卡片中找到，或者使用 `nb-cli` 搜索驱动器显示的详情中找到。卸载完成后，需要自行移除适配器加载。\n\n  </TabItem>\n</Tabs>\n"
  },
  {
    "path": "website/docusaurus.config.ts",
    "content": "import { themes } from \"prism-react-renderer\";\n\nimport type { Config } from \"@docusaurus/types\";\nimport type { Options as ChangelogOptions } from \"@nullbot/docusaurus-plugin-changelog\";\nimport type * as Preset from \"@nullbot/docusaurus-preset-nonepress\";\n\n// color mode config\nconst colorMode: Preset.ThemeConfig[\"colorMode\"] = {\n  defaultMode: \"light\",\n  respectPrefersColorScheme: true,\n};\n\n// navbar config\nconst navbar: Preset.ThemeConfig[\"navbar\"] = {\n  title: \"NoneBot\",\n  logo: {\n    alt: \"NoneBot\",\n    src: \"logo.png\",\n    href: \"/\",\n    target: \"_self\",\n    height: 32,\n    width: 32,\n  },\n  hideOnScroll: false,\n  items: [\n    {\n      label: \"指南\",\n      type: \"docsMenu\",\n      category: \"tutorial\",\n    },\n    {\n      label: \"深入\",\n      type: \"docsMenu\",\n      category: \"appendices\",\n    },\n    {\n      label: \"进阶\",\n      type: \"docsMenu\",\n      category: \"advanced\",\n    },\n    {\n      label: \"API\",\n      type: \"doc\",\n      docId: \"api/index\",\n    },\n    {\n      label: \"更多\",\n      type: \"dropdown\",\n      to: \"/store/plugins\",\n      items: [\n        {\n          label: \"最佳实践\",\n          type: \"doc\",\n          docId: \"best-practice/scheduler\",\n        },\n        {\n          label: \"开发者\",\n          type: \"doc\",\n          docId: \"developer/plugin-publishing\",\n        },\n        { label: \"社区\", type: \"doc\", docId: \"community/contact\" },\n        { label: \"开源之夏\", type: \"doc\", docId: \"ospp/2025\" },\n        { label: \"商店\", to: \"/store/plugins\" },\n        { label: \"更新日志\", to: \"/changelog/\" },\n        { label: \"论坛\", href: \"https://discussions.nonebot.dev\" },\n      ],\n    },\n  ],\n};\n\n// footer config\nconst footer: Preset.ThemeConfig[\"footer\"] = {\n  style: \"light\",\n  logo: {\n    alt: \"NoneBot\",\n    src: \"logo.png\",\n    href: \"/\",\n    target: \"_self\",\n    height: 32,\n    width: 32,\n  },\n  copyright: `Copyright © ${new Date().getFullYear()} NoneBot. All rights reserved.`,\n  links: [\n    {\n      title: \"Learn\",\n      items: [\n        { label: \"Introduction\", to: \"/docs/\" },\n        { label: \"QuickStart\", to: \"/docs/quick-start\" },\n        { label: \"Changelog\", to: \"/changelog/\" },\n      ],\n    },\n    {\n      title: \"NoneBot Team\",\n      items: [\n        {\n          label: \"Homepage\",\n          href: \"https://nonebot.dev\",\n        },\n        {\n          label: \"NoneBot V1\",\n          href: \"https://v1.nonebot.dev\",\n        },\n        { label: \"NoneBot CLI\", href: \"https://cli.nonebot.dev\" },\n      ],\n    },\n    {\n      title: \"Related\",\n      items: [\n        { label: \"OneBot\", href: \"https://onebot.dev/\" },\n        { label: \"go-cqhttp\", href: \"https://docs.go-cqhttp.org/\" },\n        { label: \"Mirai\", href: \"https://mirai.mamoe.net/\" },\n      ],\n    },\n  ],\n};\n\n// prism config\nconst lightCodeTheme = themes.github;\nconst darkCodeTheme = themes.dracula;\n\nconst prism: Preset.ThemeConfig[\"prism\"] = {\n  theme: lightCodeTheme,\n  darkTheme: darkCodeTheme,\n  additionalLanguages: [\"docker\", \"ini\"],\n};\n\n// algolia config\nconst algolia: Preset.ThemeConfig[\"algolia\"] = {\n  appId: \"X0X5UACHZQ\",\n  apiKey: \"ac03e1ac2bd0812e2ea38c0cc1ea38c5\",\n  indexName: \"nonebot\",\n  contextualSearch: true,\n};\n\n// nonepress config\nconst nonepress: Preset.ThemeConfig[\"nonepress\"] = {\n  tailwindConfig: require(\"./tailwind.config\"),\n  navbar: {\n    docsVersionDropdown: {\n      dropdownItemsAfter: [\n        {\n          label: \"1.x\",\n          href: \"https://v1.nonebot.dev/\",\n        },\n      ],\n    },\n    socialLinks: [\n      {\n        icon: [\"fab\", \"github\"],\n        href: \"https://github.com/nonebot/nonebot2\",\n      },\n    ],\n  },\n  footer: {\n    socialLinks: [\n      {\n        icon: [\"fab\", \"github\"],\n        href: \"https://github.com/nonebot/nonebot2\",\n      },\n      {\n        icon: [\"fab\", \"qq\"],\n        href: \"https://jq.qq.com/?_wv=1027&k=5OFifDh\",\n      },\n      {\n        icon: [\"fab\", \"telegram\"],\n        href: \"https://t.me/botuniverse\",\n      },\n      {\n        icon: [\"fab\", \"discord\"],\n        href: \"https://discord.gg/VKtE6Gdc4h\",\n      },\n    ],\n  },\n};\n\n// theme config\nconst themeConfig: Preset.ThemeConfig = {\n  colorMode,\n  navbar,\n  footer,\n  prism,\n  algolia,\n  nonepress,\n};\n\nexport default async function createConfigAsync() {\n  return {\n    title: \"NoneBot\",\n    tagline: \"跨平台 Python 异步机器人框架\",\n    favicon: \"icons/favicon.ico\",\n\n    // Set the production url of your site here\n    url: \"https://nonebot.dev\",\n    // Set the /<baseUrl>/ pathname under which your site is served\n    // For GitHub pages deployment, it is often '/<projectName>/'\n    baseUrl: process.env.BASE_URL || \"/\",\n\n    // GitHub pages deployment config.\n    // If you aren't using GitHub pages, you don't need these.\n    organizationName: \"nonebot\", // Usually your GitHub org/user name.\n    projectName: \"nonebot2\", // Usually your repo name.\n\n    onBrokenLinks: \"throw\",\n    onBrokenMarkdownLinks: \"warn\",\n\n    // Even if you don't use internalization, you can use this field to set useful\n    // metadata like html lang. For example, if your site is Chinese, you may want\n    // to replace \"en\" with \"zh-Hans\".\n    i18n: {\n      defaultLocale: \"zh-Hans\",\n      locales: [\"zh-Hans\"],\n    },\n\n    headTags: [\n      // 百度搜索资源平台\n      // https://ziyuan.baidu.com/\n      {\n        tagName: \"meta\",\n        attributes: {\n          name: \"baidu-site-verification\",\n          content: \"codeva-0GTZpDnDrW\",\n        },\n      },\n    ],\n    scripts: [\n      // 百度统计\n      // https://tongji.baidu.com/\n      {\n        type: \"text/javascript\",\n        charset: \"UTF-8\",\n        src: \"https://hm.baidu.com/hm.js?875efa50097818701ee681edd63eaac6\",\n        async: true,\n      },\n      // 万维广告\n      // https://wwads.cn/\n      {\n        type: \"text/javascript\",\n        charset: \"UTF-8\",\n        src: \"https://cdn.wwads.cn/js/makemoney.js\",\n        async: true,\n      },\n      // uwu logo\n      {\n        type: \"text/javascript\",\n        charset: \"UTF-8\",\n        src: \"/uwu.js\",\n      },\n    ],\n\n    presets: [\n      [\n        \"@nullbot/docusaurus-preset-nonepress\",\n        /** @type {import('@nullbot/docusaurus-preset-nonepress').Options} */\n        {\n          docs: {\n            sidebarPath: require.resolve(\"./sidebars.js\"),\n            // Please change this to your repo.\n            editUrl: \"https://github.com/nonebot/nonebot2/edit/master/website/\",\n            showLastUpdateAuthor: true,\n            showLastUpdateTime: true,\n            // exclude: [\n            //   \"**/_*.{js,jsx,ts,tsx,md,mdx}\",\n            //   \"**/_*/**\",\n            //   \"**/*.test.{js,jsx,ts,tsx}\",\n            //   \"**/__tests__/**\",\n            // ],\n            // async sidebarItemsGenerator({\n            //   isCategoryIndex: defaultCategoryIndexMatcher,\n            //   defaultSidebarItemsGenerator,\n            //   ...args\n            // }) {\n            //   return defaultSidebarItemsGenerator({\n            //     ...args,\n            //     isCategoryIndex(doc) {\n            //       // disable category index convention for generated API docs\n            //       if (\n            //         doc.directories.length > 0 &&\n            //         doc.directories.at(-1) === \"api\"\n            //       ) {\n            //         return false;\n            //       }\n            //       return defaultCategoryIndexMatcher(doc);\n            //     },\n            //   });\n            // },\n          },\n          // theme: {\n          //   customCss: require.resolve(\"./src/css/custom.css\"),\n          // },\n          sitemap: {\n            changefreq: \"daily\",\n            priority: 0.5,\n          },\n          gtag: {\n            trackingID: \"G-MRS1GMZG0F\",\n          },\n        },\n      ],\n    ],\n\n    future: {\n      experimental_faster: true,\n      v4: true,\n    },\n\n    plugins: [\n      require(\"./src/plugins/webpack-plugin.ts\"),\n      [\n        \"@nullbot/docusaurus-plugin-changelog\",\n        {\n          changelogPath: \"src/changelog/changelog.md\",\n          changelogHeader: `description: Changelog\ntoc_max_heading_level: 2\nsidebar_custom_props:\n  sidebar_id: changelog\n  sidebar_version: current`,\n        } satisfies ChangelogOptions,\n      ],\n    ],\n\n    markdown: {\n      mdx1Compat: {\n        headingIds: true,\n      },\n    },\n\n    themeConfig,\n  } satisfies Config;\n}\n"
  },
  {
    "path": "website/package.json",
    "content": "{\n  \"name\": \"nonebot\",\n  \"version\": \"2.0.0\",\n  \"description\": \"跨平台 Python 异步机器人框架\",\n  \"private\": true,\n  \"homepage\": \"https://nonebot.dev/\",\n  \"repository\": \"https://github.com/nonebot/nonebot2/\",\n  \"bugs\": {\n    \"url\": \"https://github.com/nonebot/nonebot2/issues\"\n  },\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"docusaurus\": \"docusaurus\",\n    \"start\": \"docusaurus start --host 0.0.0.0 --port 3000\",\n    \"build\": \"docusaurus build\",\n    \"build:fast\": \"cross-env BUILD_FAST=true yarn build\",\n    \"build:fast:rsdoctor\": \"cross-env BUILD_FAST=true RSDOCTOR=true yarn build\",\n    \"build:fast:profile\": \"cross-env BUILD_FAST=true node --cpu-prof --cpu-prof-dir .cpu-prof ./node_modules/.bin/docusaurus build\",\n    \"swizzle\": \"docusaurus swizzle\",\n    \"deploy\": \"docusaurus deploy\",\n    \"clear\": \"docusaurus clear\",\n    \"serve\": \"docusaurus serve\",\n    \"write-translations\": \"docusaurus write-translations\",\n    \"write-heading-ids\": \"docusaurus write-heading-ids\",\n    \"typecheck\": \"tsc\",\n    \"prettier\": \"prettier --config ../.prettierrc --write .\"\n  },\n  \"dependencies\": {\n    \"@docusaurus/core\": \"^3.7.0\",\n    \"@docusaurus/eslint-plugin\": \"3.7.0\",\n    \"@mdx-js/react\": \"^3.0.0\",\n    \"@nullbot/docusaurus-plugin-changelog\": \"^3.0.0\",\n    \"@nullbot/docusaurus-preset-nonepress\": \"^3.0.0\",\n    \"clsx\": \"^2.0.0\",\n    \"copy-text-to-clipboard\": \"^3.2.0\",\n    \"prism-react-renderer\": \"^2.3.0\",\n    \"react\": \"^19.0.0\",\n    \"react-color\": \"^2.19.3\",\n    \"react-dom\": \"^19.0.0\",\n    \"react-use-pagination\": \"^2.0.1\"\n  },\n  \"devDependencies\": {\n    \"@docusaurus/faster\": \"^3.7.0\",\n    \"@docusaurus/module-type-aliases\": \"^3.7.0\",\n    \"@nullbot/docusaurus-tsconfig\": \"^3.0.0\",\n    \"@types/react-color\": \"^3.0.10\",\n    \"asciinema-player\": \"^3.5.0\",\n    \"typescript\": \"~5.7.2\"\n  },\n  \"browserslist\": {\n    \"production\": [\n      \">0.5%\",\n      \"not dead\",\n      \"not op_mini all\"\n    ],\n    \"development\": [\n      \"last 1 chrome version\",\n      \"last 1 firefox version\",\n      \"last 1 safari version\"\n    ]\n  },\n  \"engines\": {\n    \"node\": \">=18.0\"\n  }\n}\n"
  },
  {
    "path": "website/sidebars.ts",
    "content": "/**\n * Creating a sidebar enables you to:\n - create an ordered group of docs\n - render a sidebar for each doc of that group\n - provide next/previous navigation\n\n The sidebars can be generated from the filesystem, or explicitly defined here.\n\n Create as many sidebars as you want.\n */\nimport path from \"path\";\n\nimport { getChangelogItemsSync } from \"@nullbot/docusaurus-plugin-changelog\";\n\nimport type { SidebarsConfig } from \"@docusaurus/plugin-content-docs\";\n\nconst changelogPath = path.join(__dirname, \"src/changelog/changelog.md\");\nconst changelogItems = getChangelogItemsSync(changelogPath, 10);\n\nconst sidebars: SidebarsConfig = {\n  tutorial: [\n    {\n      type: \"category\",\n      label: \"开始\",\n      collapsible: false,\n      items: [\"index\", \"quick-start\", \"editor-support\"],\n    },\n    {\n      type: \"category\",\n      label: \"指南\",\n      items: [\n        {\n          type: \"autogenerated\",\n          dirName: \"tutorial\",\n        },\n      ],\n    },\n    {\n      type: \"category\",\n      label: \"深入\",\n      items: [\n        {\n          type: \"autogenerated\",\n          dirName: \"appendices\",\n        },\n      ],\n    },\n    {\n      type: \"category\",\n      label: \"进阶\",\n      items: [\n        {\n          type: \"autogenerated\",\n          dirName: \"advanced\",\n        },\n      ],\n    },\n    {\n      type: \"category\",\n      label: \"最佳实践\",\n      items: [\n        {\n          type: \"autogenerated\",\n          dirName: \"best-practice\",\n        },\n      ],\n    },\n    {\n      type: \"category\",\n      label: \"开发者\",\n      items: [\n        {\n          type: \"autogenerated\",\n          dirName: \"developer\",\n        },\n      ],\n    },\n  ],\n  api: [{ type: \"autogenerated\", dirName: \"api\" }],\n  ecosystem: [\n    {\n      type: \"category\",\n      label: \"关于我们\",\n      collapsible: false,\n      items: [\n        {\n          type: \"autogenerated\",\n          dirName: \"community\",\n        },\n      ],\n    },\n    {\n      type: \"category\",\n      label: \"开源之夏\",\n      collapsible: true,\n      items: [\n        {\n          type: \"autogenerated\",\n          dirName: \"ospp\",\n        },\n      ],\n    },\n    {\n      type: \"category\",\n      label: \"社区资源\",\n      collapsible: false,\n      items: [\n        {\n          type: \"link\",\n          label: \"插件商店\",\n          href: \"/store/plugins\",\n        },\n        {\n          type: \"link\",\n          label: \"适配器商店\",\n          href: \"/store/adapters\",\n        },\n        {\n          type: \"link\",\n          label: \"驱动器商店\",\n          href: \"/store/drivers\",\n        },\n        {\n          type: \"link\",\n          label: \"机器人商店\",\n          href: \"/store/bots\",\n        },\n        {\n          type: \"link\",\n          label: \"Awesome NoneBot\",\n          href: \"https://awesome.nonebot.dev\",\n        },\n        {\n          type: \"link\",\n          label: \"论坛\",\n          href: \"https://discussions.nonebot.dev\",\n        },\n      ],\n    },\n  ],\n  changelog: [\n    {\n      type: \"category\",\n      label: \"更新日志\",\n      collapsible: false,\n      items: changelogItems.map<{ type: \"link\"; label: string; href: string }>(\n        (chunk, index) => ({\n          type: \"link\",\n          label: chunk[0]!.title,\n          href: `/changelog/${index > 0 ? index.toString() : \"\"}`,\n        })\n      ),\n    },\n  ],\n};\n\nexport default sidebars;\n"
  },
  {
    "path": "website/src/changelog/changelog.md",
    "content": "---\ndescription: Changelog\ntoc_max_heading_level: 2\n---\n\n# 更新日志\n\n## 最近更新\n\n### 💥 破坏性变更\n\n- Remove: 移除 Python 3.9 支持 [@shoucandanghehe](https://github.com/shoucandanghehe) ([#3860](https://github.com/nonebot/nonebot2/pull/3860))\n\n### 🚀 新功能\n\n- Feature: 放宽 pydantic compat model dump 类型 [@shoucandanghehe](https://github.com/shoucandanghehe) ([#3898](https://github.com/nonebot/nonebot2/pull/3898))\n\n### 🐛 Bug 修复\n\n- Fix: aiohttp 驱动未处理 WSMsgType.CLOSED 类型 [@shoucandanghehe](https://github.com/shoucandanghehe) ([#3862](https://github.com/nonebot/nonebot2/pull/3862))\n\n### 📝 文档\n\n- Docs: 修复插件元数据链接错误 [@yanyongyu](https://github.com/yanyongyu) ([#3894](https://github.com/nonebot/nonebot2/pull/3894))\n- Docs: 完善对「发布插件」章节的文档描述 [@NCBM](https://github.com/NCBM) ([#3865](https://github.com/nonebot/nonebot2/pull/3865))\n- Docs: Docker 部署镜像添加 latest tag [@AhsokaTano26](https://github.com/AhsokaTano26) ([#3787](https://github.com/nonebot/nonebot2/pull/3787))\n- Docs: 调整文档 `on_command` import 路径 [@Xfjie314](https://github.com/Xfjie314) ([#3747](https://github.com/nonebot/nonebot2/pull/3747))\n- Docs: 修复插件编写准备文档中的文本错误 [@Xfjie314](https://github.com/Xfjie314) ([#3746](https://github.com/nonebot/nonebot2/pull/3746))\n- Docs: 修复格式化导致的语法错误 [@yanyongyu](https://github.com/yanyongyu) ([#3737](https://github.com/nonebot/nonebot2/pull/3737))\n\n### 💫 杂项\n\n- Dev: 修复 devcontainer corepack 安装 yarn 卡死 [@yanyongyu](https://github.com/yanyongyu) ([#3893](https://github.com/nonebot/nonebot2/pull/3893))\n- Plugin: skland 插件添加标签 [@FrostN0v0](https://github.com/FrostN0v0) ([#3853](https://github.com/nonebot/nonebot2/pull/3853))\n- CI: 修改 `test_depend` cpython 版本范围 [@yanyongyu](https://github.com/yanyongyu) ([#3828](https://github.com/nonebot/nonebot2/pull/3828))\n- Plugin: 删除插件 nonebot_plugin_acmd [@hlfzsi](https://github.com/hlfzsi) ([#3750](https://github.com/nonebot/nonebot2/pull/3750))\n\n### 🍻 插件发布\n\n- Plugin: Codex [@noneflow](https://github.com/noneflow) ([#3889](https://github.com/nonebot/nonebot2/pull/3889))\n- Plugin: nonebot-plugin-nbnhhsh [@noneflow](https://github.com/noneflow) ([#3887](https://github.com/nonebot/nonebot2/pull/3887))\n- Plugin: mc服务器白名单管理工具 [@noneflow](https://github.com/noneflow) ([#3813](https://github.com/nonebot/nonebot2/pull/3813))\n- Plugin: 特朗普社媒监控 [@noneflow](https://github.com/noneflow) ([#3882](https://github.com/nonebot/nonebot2/pull/3882))\n- Plugin: The Betterest Mute Cat [@noneflow](https://github.com/noneflow) ([#3869](https://github.com/nonebot/nonebot2/pull/3869))\n- Plugin: nonebot-plugin-cardimg [@noneflow](https://github.com/noneflow) ([#3857](https://github.com/nonebot/nonebot2/pull/3857))\n- Plugin: Nonebot-Plugin-Rikka [@noneflow](https://github.com/noneflow) ([#3875](https://github.com/nonebot/nonebot2/pull/3875))\n- Plugin: nonebot-plugin-peek [@noneflow](https://github.com/noneflow) ([#3859](https://github.com/nonebot/nonebot2/pull/3859))\n- Plugin: 自动合成emoji [@noneflow](https://github.com/noneflow) ([#3867](https://github.com/nonebot/nonebot2/pull/3867))\n- Plugin: 今日doro结局 [@noneflow](https://github.com/noneflow) ([#3852](https://github.com/nonebot/nonebot2/pull/3852))\n- Plugin: Phira Server Manager [@noneflow](https://github.com/noneflow) ([#3855](https://github.com/nonebot/nonebot2/pull/3855))\n- Plugin: Shiro Web Console [@noneflow](https://github.com/noneflow) ([#3832](https://github.com/nonebot/nonebot2/pull/3832))\n- Plugin: 群聊拟人 [@noneflow](https://github.com/noneflow) ([#3820](https://github.com/nonebot/nonebot2/pull/3820))\n- Plugin: BitTorrent磁力搜索 [@noneflow](https://github.com/noneflow) ([#3844](https://github.com/nonebot/nonebot2/pull/3844))\n- Plugin: MCP Client [@noneflow](https://github.com/noneflow) ([#3842](https://github.com/nonebot/nonebot2/pull/3842))\n- Plugin: Dice Helper [@noneflow](https://github.com/noneflow) ([#3840](https://github.com/nonebot/nonebot2/pull/3840))\n- Plugin: Uniconfig-配置文件管理器 [@noneflow](https://github.com/noneflow) ([#3849](https://github.com/nonebot/nonebot2/pull/3849))\n- Plugin: osugreek [@noneflow](https://github.com/noneflow) ([#3836](https://github.com/nonebot/nonebot2/pull/3836))\n- Plugin: 基于QQ音乐歌单的音乐推荐 [@noneflow](https://github.com/noneflow) ([#3838](https://github.com/nonebot/nonebot2/pull/3838))\n- Plugin: nonebot-plugin-bili2mp4 [@noneflow](https://github.com/noneflow) ([#3792](https://github.com/nonebot/nonebot2/pull/3792))\n- Plugin: 战地6战绩查询 [@noneflow](https://github.com/noneflow) ([#3815](https://github.com/nonebot/nonebot2/pull/3815))\n- Plugin: Tavily Search [@noneflow](https://github.com/noneflow) ([#3834](https://github.com/nonebot/nonebot2/pull/3834))\n- Plugin: 群消息中继 [@noneflow](https://github.com/noneflow) ([#3804](https://github.com/nonebot/nonebot2/pull/3804))\n- Plugin: 互联网异常事件监测 [@noneflow](https://github.com/noneflow) ([#3831](https://github.com/nonebot/nonebot2/pull/3831))\n- Plugin: 词汇黑名单审查 [@noneflow](https://github.com/noneflow) ([#3817](https://github.com/nonebot/nonebot2/pull/3817))\n- Plugin: 汉化进度记录 [@noneflow](https://github.com/noneflow) ([#3807](https://github.com/nonebot/nonebot2/pull/3807))\n- Plugin: nonebot-plugin-ai-groupmate [@noneflow](https://github.com/noneflow) ([#3766](https://github.com/nonebot/nonebot2/pull/3766))\n- Plugin: nonebot_plugin_boardgamehelper [@noneflow](https://github.com/noneflow) ([#3800](https://github.com/nonebot/nonebot2/pull/3800))\n- Plugin: 即梦绘画 [@noneflow](https://github.com/noneflow) ([#3797](https://github.com/nonebot/nonebot2/pull/3797))\n- Plugin: 快捷回复 [@noneflow](https://github.com/noneflow) ([#3795](https://github.com/nonebot/nonebot2/pull/3795))\n- Plugin: pErithacus [@noneflow](https://github.com/noneflow) ([#3767](https://github.com/nonebot/nonebot2/pull/3767))\n- Plugin: MC服务器状态查询 [@noneflow](https://github.com/noneflow) ([#3781](https://github.com/nonebot/nonebot2/pull/3781))\n- Plugin: Instagram RapidAPI 解析 [@noneflow](https://github.com/noneflow) ([#3784](https://github.com/nonebot/nonebot2/pull/3784))\n- Plugin: 今天是什么小猪 [@noneflow](https://github.com/noneflow) ([#3773](https://github.com/nonebot/nonebot2/pull/3773))\n- Plugin: 御神签 [@noneflow](https://github.com/noneflow) ([#3777](https://github.com/nonebot/nonebot2/pull/3777))\n- Plugin: 火车迷铁路工具箱 [@noneflow](https://github.com/noneflow) ([#3770](https://github.com/nonebot/nonebot2/pull/3770))\n- Plugin: TerraLink [@noneflow](https://github.com/noneflow) ([#3775](https://github.com/nonebot/nonebot2/pull/3775))\n- Plugin: 安安说 [@noneflow](https://github.com/noneflow) ([#3726](https://github.com/nonebot/nonebot2/pull/3726))\n- Plugin: 安安的素描本聊天框 [@noneflow](https://github.com/noneflow) ([#3762](https://github.com/nonebot/nonebot2/pull/3762))\n- Plugin: manosoba-reply-generator [@noneflow](https://github.com/noneflow) ([#3753](https://github.com/nonebot/nonebot2/pull/3753))\n- Plugin: 模板绘图 [@noneflow](https://github.com/noneflow) ([#3752](https://github.com/nonebot/nonebot2/pull/3752))\n- Plugin: iPinfo [@noneflow](https://github.com/noneflow) ([#3759](https://github.com/nonebot/nonebot2/pull/3759))\n- Plugin: 电子课程表 [@noneflow](https://github.com/noneflow) ([#3743](https://github.com/nonebot/nonebot2/pull/3743))\n- Plugin: 魔裁 Memes [@noneflow](https://github.com/noneflow) ([#3755](https://github.com/nonebot/nonebot2/pull/3755))\n- Plugin: 图像对称处理 [@noneflow](https://github.com/noneflow) ([#3748](https://github.com/nonebot/nonebot2/pull/3748))\n- Plugin: 每日人品 [@noneflow](https://github.com/noneflow) ([#3735](https://github.com/nonebot/nonebot2/pull/3735))\n- Plugin: nonebot_plugin_markdown2img [@noneflow](https://github.com/noneflow) ([#3730](https://github.com/nonebot/nonebot2/pull/3730))\n- Plugin: B站解析助手 [@noneflow](https://github.com/noneflow) ([#3728](https://github.com/nonebot/nonebot2/pull/3728))\n\n### 🍻 机器人发布\n\n- Bot: Rosmontis.io [@noneflow](https://github.com/noneflow) ([#3878](https://github.com/nonebot/nonebot2/pull/3878))\n\n### 🍻 适配器发布\n\n- Adapter: 云湖适配器 [@noneflow](https://github.com/noneflow) ([#3741](https://github.com/nonebot/nonebot2/pull/3741))\n\n## v2.4.4\n\n### 🚀 新功能\n\n- Feature: 允许插件从环境变量中读取配置项并支持 alias [@AzideCupric](https://github.com/AzideCupric) ([#3673](https://github.com/nonebot/nonebot2/pull/3673))\n- Feature: 更新 NB-CLI 新版插件加载格式与文档 [@NCBM](https://github.com/NCBM) ([#3614](https://github.com/nonebot/nonebot2/pull/3614))\n\n### 🐛 Bug 修复\n\n- Fix: log level 配置项无法使用 int 类型配置 [@yanyongyu](https://github.com/yanyongyu) ([#3732](https://github.com/nonebot/nonebot2/pull/3732))\n- Fix: 兼容 pydantic v2.12 `FieldInfo` 改动 [@yanyongyu](https://github.com/yanyongyu) ([#3722](https://github.com/nonebot/nonebot2/pull/3722))\n\n### 📝 文档\n\n- Docs: 更新适配器编写指南中的链接 [@xjh2009](https://github.com/xjh2009) ([#3731](https://github.com/nonebot/nonebot2/pull/3731))\n- Feature: 更新 NB-CLI 新版插件加载格式与文档 [@NCBM](https://github.com/NCBM) ([#3614](https://github.com/nonebot/nonebot2/pull/3614))\n- Docs: 添加 htmlkit 文档至最佳实践 [@BlueGlassBlock](https://github.com/BlueGlassBlock) ([#3682](https://github.com/nonebot/nonebot2/pull/3682))\n- Docs: 修复 userinfo 插件链接 [@XieXiLin2](https://github.com/XieXiLin2) ([#3660](https://github.com/nonebot/nonebot2/pull/3660))\n- Docs: 升级 docusaurus 3.8.1 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#3649](https://github.com/nonebot/nonebot2/pull/3649))\n- Docs: 更新文档《手动创建项目》 [@Chen-Luan](https://github.com/Chen-Luan) ([#3623](https://github.com/nonebot/nonebot2/pull/3623))\n- Docs: 增加 B站直播间 适配器说明 [@MingxuanGame](https://github.com/MingxuanGame) ([#3636](https://github.com/nonebot/nonebot2/pull/3636))\n- Docs: 增加 VoceChat 适配器说明 [@5656565566](https://github.com/5656565566) ([#3627](https://github.com/nonebot/nonebot2/pull/3627))\n\n### 💫 杂项\n\n- CI: 严格约束 `test_depend` CPython 版本范围 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#3713](https://github.com/nonebot/nonebot2/pull/3713))\n- CI: 升级文档构建 node 版本 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#3668](https://github.com/nonebot/nonebot2/pull/3668))\n- CI: 测试矩阵加入 Python 3.13 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#3605](https://github.com/nonebot/nonebot2/pull/3605))\n\n### 🍻 插件发布\n\n- Plugin: 海龟汤游戏 [@noneflow](https://github.com/noneflow) ([#3697](https://github.com/nonebot/nonebot2/pull/3697))\n- Plugin: 每日必应壁纸 [@noneflow](https://github.com/noneflow) ([#3721](https://github.com/nonebot/nonebot2/pull/3721))\n- Plugin: pxchat [@noneflow](https://github.com/noneflow) ([#3712](https://github.com/nonebot/nonebot2/pull/3712))\n- Plugin: nonebot-plugin-memory [@noneflow](https://github.com/noneflow) ([#3701](https://github.com/nonebot/nonebot2/pull/3701))\n- Plugin: 远程文件打开 [@noneflow](https://github.com/noneflow) ([#3717](https://github.com/nonebot/nonebot2/pull/3717))\n- Plugin: MC新闻更新检测 [@noneflow](https://github.com/noneflow) ([#3699](https://github.com/nonebot/nonebot2/pull/3699))\n- Plugin: kook卡片消息编写适配插件 [@noneflow](https://github.com/noneflow) ([#3708](https://github.com/nonebot/nonebot2/pull/3708))\n- Plugin: 链接分享自动解析 [@noneflow](https://github.com/noneflow) ([#3706](https://github.com/nonebot/nonebot2/pull/3706))\n- Plugin: 怪物猎人集会码插件 [@noneflow](https://github.com/noneflow) ([#3684](https://github.com/nonebot/nonebot2/pull/3684))\n- Plugin: nonebot-plugin-htmlkit [@noneflow](https://github.com/noneflow) ([#3695](https://github.com/nonebot/nonebot2/pull/3695))\n- Plugin: 言令 [@noneflow](https://github.com/noneflow) ([#3675](https://github.com/nonebot/nonebot2/pull/3675))\n- Plugin: 算法比赛助手 [@noneflow](https://github.com/noneflow) ([#3672](https://github.com/nonebot/nonebot2/pull/3672))\n- Plugin: 复盘打卡 [@noneflow](https://github.com/noneflow) ([#3681](https://github.com/nonebot/nonebot2/pull/3681))\n- Plugin: DMP 饥荒管理平台机器人 [@noneflow](https://github.com/noneflow) ([#3616](https://github.com/nonebot/nonebot2/pull/3616))\n- Plugin: 谁是卧底小游戏 [@noneflow](https://github.com/noneflow) ([#3629](https://github.com/nonebot/nonebot2/pull/3629))\n- Plugin: 夸克自动转存 [@noneflow](https://github.com/noneflow) ([#3671](https://github.com/nonebot/nonebot2/pull/3671))\n- Plugin: 禁止复读 [@noneflow](https://github.com/noneflow) ([#3644](https://github.com/nonebot/nonebot2/pull/3644))\n- Plugin: 蔚蓝档案今日运势 [@noneflow](https://github.com/noneflow) ([#3653](https://github.com/nonebot/nonebot2/pull/3653))\n- Plugin: 分布式黑名单插件 [@noneflow](https://github.com/noneflow) ([#3655](https://github.com/nonebot/nonebot2/pull/3655))\n- Plugin: 图片手办化 [@noneflow](https://github.com/noneflow) ([#3662](https://github.com/nonebot/nonebot2/pull/3662))\n- Plugin: Akash Image Generator [@noneflow](https://github.com/noneflow) ([#3651](https://github.com/nonebot/nonebot2/pull/3651))\n- Plugin: 让我看看！！ [@noneflow](https://github.com/noneflow) ([#3648](https://github.com/nonebot/nonebot2/pull/3648))\n- Plugin: ImageLibrary [@noneflow](https://github.com/noneflow) ([#3620](https://github.com/nonebot/nonebot2/pull/3620))\n- Plugin: 抽象 [@noneflow](https://github.com/noneflow) ([#3638](https://github.com/nonebot/nonebot2/pull/3638))\n- Plugin: 卖若插件 [@noneflow](https://github.com/noneflow) ([#3631](https://github.com/nonebot/nonebot2/pull/3631))\n- Plugin: HuaEr聊天bot [@noneflow](https://github.com/noneflow) ([#3564](https://github.com/nonebot/nonebot2/pull/3564))\n- Plugin: Remove nonebot_plugin_cnrail [@noneflow](https://github.com/noneflow) ([#3645](https://github.com/nonebot/nonebot2/pull/3645))\n- Plugin: Remove nonebot_plugin_pingti [@noneflow](https://github.com/noneflow) ([#3646](https://github.com/nonebot/nonebot2/pull/3646))\n- Plugin: Anipusher推送机 [@noneflow](https://github.com/noneflow) ([#3582](https://github.com/nonebot/nonebot2/pull/3582))\n- Plugin: nonebot-plugin-simple-setu [@noneflow](https://github.com/noneflow) ([#3594](https://github.com/nonebot/nonebot2/pull/3594))\n- Plugin: Alisten [@noneflow](https://github.com/noneflow) ([#3635](https://github.com/nonebot/nonebot2/pull/3635))\n- Plugin: MC玩家皮肤渲染 [@noneflow](https://github.com/noneflow) ([#3613](https://github.com/nonebot/nonebot2/pull/3613))\n- Plugin: EconomyValue [@noneflow](https://github.com/noneflow) ([#3566](https://github.com/nonebot/nonebot2/pull/3566))\n\n### 🍻 机器人发布\n\n- Bot: Amrita [@noneflow](https://github.com/noneflow) ([#3641](https://github.com/nonebot/nonebot2/pull/3641))\n\n### 🍻 适配器发布\n\n- Adapter: B站直播间 [@noneflow](https://github.com/noneflow) ([#3592](https://github.com/nonebot/nonebot2/pull/3592))\n- Adapter: nonebot-adapter-vocechat [@noneflow](https://github.com/noneflow) ([#3536](https://github.com/nonebot/nonebot2/pull/3536))\n\n## v2.4.3\n\n### 🚀 新功能\n\n- Feature: 支持 PEP 695 类型别名 [@yanyongyu](https://github.com/yanyongyu) ([#3621](https://github.com/nonebot/nonebot2/pull/3621))\n- Feature: 升级至新版本 websockets client API [@yanyongyu](https://github.com/yanyongyu) ([#3606](https://github.com/nonebot/nonebot2/pull/3606))\n- Feature: 细化内置驱动器请求参数中的超时控制颗粒度 [@Ailitonia](https://github.com/Ailitonia) ([#3571](https://github.com/nonebot/nonebot2/pull/3571))\n- Feature: 为 `HTTPClient` 等内置驱动器添加流式请求方法 [@Ailitonia](https://github.com/Ailitonia) ([#3560](https://github.com/nonebot/nonebot2/pull/3560))\n\n### 📝 文档\n\n- Docs: 更新 Alconna 主页面 [@RF-Tar-Railt](https://github.com/RF-Tar-Railt) ([#3598](https://github.com/nonebot/nonebot2/pull/3598))\n- Docs: 添加插件 metadata 缺失的 docstring [@yanyongyu](https://github.com/yanyongyu) ([#3583](https://github.com/nonebot/nonebot2/pull/3583))\n- Docs: 修复组织成员提交 issue 时不遵守表单 [@ProgramRipper](https://github.com/ProgramRipper) ([#3558](https://github.com/nonebot/nonebot2/pull/3558))\n- Docs: 增加 `EFChat` 适配器说明 [@molanp](https://github.com/molanp) ([#3544](https://github.com/nonebot/nonebot2/pull/3544))\n- Docs: 插件发布表单描述优化 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#3520](https://github.com/nonebot/nonebot2/pull/3520))\n- Docs: 增加 `Milky` 适配器说明 [@RF-Tar-Railt](https://github.com/RF-Tar-Railt) ([#3492](https://github.com/nonebot/nonebot2/pull/3492))\n- Docs: 添加 OSPP 2025 项目 [@yanyongyu](https://github.com/yanyongyu) ([#3466](https://github.com/nonebot/nonebot2/pull/3466))\n- Docs: 更新最佳实践 `Alconna` 章节 [@RF-Tar-Railt](https://github.com/RF-Tar-Railt) ([#3447](https://github.com/nonebot/nonebot2/pull/3447))\n- Docs: 修复移动端侧边栏折叠状态异常 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#3414](https://github.com/nonebot/nonebot2/pull/3414))\n- Docs: 添加 Gewechat 适配器描述 [@Shine-Light](https://github.com/Shine-Light) ([#3372](https://github.com/nonebot/nonebot2/pull/3372))\n\n### 💫 杂项\n\n- Dev: 迁移使用 uv 管理项目依赖 [@yanyongyu](https://github.com/yanyongyu) ([#3607](https://github.com/nonebot/nonebot2/pull/3607))\n- Fix: `RUF005` tuple 拼接 [@Ailitonia](https://github.com/Ailitonia) ([#3572](https://github.com/nonebot/nonebot2/pull/3572))\n- Plugin: 更新插件维护情况 [@Agnes4m](https://github.com/Agnes4m) ([#3555](https://github.com/nonebot/nonebot2/pull/3555))\n- Plugin: 修改 nailong 插件作者 [@superbot-ai445](https://github.com/superbot-ai445) ([#3554](https://github.com/nonebot/nonebot2/pull/3554))\n- CI: 适配 NoneFlow 4.4.0 [@he0119](https://github.com/he0119) ([#3539](https://github.com/nonebot/nonebot2/pull/3539))\n- Develop: 修复 devcontainer feature 配置 [@yanyongyu](https://github.com/yanyongyu) ([#3515](https://github.com/nonebot/nonebot2/pull/3515))\n- CI: 修复 Ruff 并发组名称 [@KomoriDev](https://github.com/KomoriDev) ([#3434](https://github.com/nonebot/nonebot2/pull/3434))\n\n### 🍻 插件发布\n\n- Plugin: FinalShell 离线激活码 [@noneflow](https://github.com/noneflow) ([#3574](https://github.com/nonebot/nonebot2/pull/3574))\n- Plugin: 天气查询 [@noneflow](https://github.com/noneflow) ([#3596](https://github.com/nonebot/nonebot2/pull/3596))\n- Plugin: LazyTea命令拓展 [@noneflow](https://github.com/noneflow) ([#3604](https://github.com/nonebot/nonebot2/pull/3604))\n- Plugin: AWS Manager [@noneflow](https://github.com/noneflow) ([#3550](https://github.com/nonebot/nonebot2/pull/3550))\n- Plugin: 三角洲助手 [@noneflow](https://github.com/noneflow) ([#3590](https://github.com/nonebot/nonebot2/pull/3590))\n- Plugin: 猜猜病 [@noneflow](https://github.com/noneflow) ([#3585](https://github.com/nonebot/nonebot2/pull/3585))\n- Plugin: LLM-Helper [@noneflow](https://github.com/noneflow) ([#3569](https://github.com/nonebot/nonebot2/pull/3569))\n- Plugin: 更好的电子钓鱼 [@noneflow](https://github.com/noneflow) ([#3576](https://github.com/nonebot/nonebot2/pull/3576))\n- Plugin: nonebot-plugin-flomic [@noneflow](https://github.com/noneflow) ([#3529](https://github.com/nonebot/nonebot2/pull/3529))\n- Plugin: LazyTea [@noneflow](https://github.com/noneflow) ([#3548](https://github.com/nonebot/nonebot2/pull/3548))\n- Plugin: LitePerm 权限管理插件 [@noneflow](https://github.com/noneflow) ([#3562](https://github.com/nonebot/nonebot2/pull/3562))\n- Plugin: 三角洲鼠鼠偷吃模拟器 [@noneflow](https://github.com/noneflow) ([#3541](https://github.com/nonebot/nonebot2/pull/3541))\n- Plugin: 维维表情包搜索器 [@noneflow](https://github.com/noneflow) ([#3546](https://github.com/nonebot/nonebot2/pull/3546))\n- Plugin: 喜（悲）报生成器 [@noneflow](https://github.com/noneflow) ([#3527](https://github.com/nonebot/nonebot2/pull/3527))\n- Plugin: 依赖注入扩展 [@noneflow](https://github.com/noneflow) ([#3552](https://github.com/nonebot/nonebot2/pull/3552))\n- Plugin: nonebot-plugin-orm [@noneflow](https://github.com/noneflow) ([#3557](https://github.com/nonebot/nonebot2/pull/3557))\n- Plugin: 币安小助手 [@noneflow](https://github.com/noneflow) ([#3525](https://github.com/nonebot/nonebot2/pull/3525))\n- Plugin: nonebot_plugin_sunset_reminder [@noneflow](https://github.com/noneflow) ([#3538](https://github.com/nonebot/nonebot2/pull/3538))\n- Plugin: nonebot-plugin-NobleDuel [@noneflow](https://github.com/noneflow) ([#3521](https://github.com/nonebot/nonebot2/pull/3521))\n- Plugin: 绝区零角色数据获取 [@noneflow](https://github.com/noneflow) ([#3512](https://github.com/nonebot/nonebot2/pull/3512))\n- Plugin: 命令冷却 [@noneflow](https://github.com/noneflow) ([#3499](https://github.com/nonebot/nonebot2/pull/3499))\n- Plugin: Hacker News [@noneflow](https://github.com/noneflow) ([#3502](https://github.com/nonebot/nonebot2/pull/3502))\n- Plugin: nonebot_plugin_df_armor_repair_simulator [@noneflow](https://github.com/noneflow) ([#3506](https://github.com/nonebot/nonebot2/pull/3506))\n- Plugin: nonebot-plugin-emojilike-automonkey [@noneflow](https://github.com/noneflow) ([#3386](https://github.com/nonebot/nonebot2/pull/3386))\n- Plugin: bfvplayerlist [@noneflow](https://github.com/noneflow) ([#3450](https://github.com/nonebot/nonebot2/pull/3450))\n- Plugin: nonebot-plugin-pokemonle [@noneflow](https://github.com/noneflow) ([#3504](https://github.com/nonebot/nonebot2/pull/3504))\n- Plugin: NoneBotCloversClient [@noneflow](https://github.com/noneflow) ([#3494](https://github.com/nonebot/nonebot2/pull/3494))\n- Plugin: NYATuringTest [@noneflow](https://github.com/noneflow) ([#3479](https://github.com/nonebot/nonebot2/pull/3479))\n- Plugin: PicMenu Next [@noneflow](https://github.com/noneflow) ([#3488](https://github.com/nonebot/nonebot2/pull/3488))\n- Plugin: nonebot-plugin-ehentai [@noneflow](https://github.com/noneflow) ([#3475](https://github.com/nonebot/nonebot2/pull/3475))\n- Plugin: 真寻农场 [@noneflow](https://github.com/noneflow) ([#3477](https://github.com/nonebot/nonebot2/pull/3477))\n- Plugin: 修复 QQ 图床 SSL 错误 [@noneflow](https://github.com/noneflow) ([#3482](https://github.com/nonebot/nonebot2/pull/3482))\n- Plugin: 多源日报 [@noneflow](https://github.com/noneflow) ([#3468](https://github.com/nonebot/nonebot2/pull/3468))\n- Plugin: QQ账号详细信息查询 [@noneflow](https://github.com/noneflow) ([#3472](https://github.com/nonebot/nonebot2/pull/3472))\n- Plugin: doro大冒险 [@noneflow](https://github.com/noneflow) ([#3465](https://github.com/nonebot/nonebot2/pull/3465))\n- Plugin: nonebot_plugin_paper [@noneflow](https://github.com/noneflow) ([#3431](https://github.com/nonebot/nonebot2/pull/3431))\n- Plugin: Web侧载 [@noneflow](https://github.com/noneflow) ([#3470](https://github.com/nonebot/nonebot2/pull/3470))\n- Plugin: 群聊广告哒咩 [@noneflow](https://github.com/noneflow) ([#3446](https://github.com/nonebot/nonebot2/pull/3446))\n- Plugin: 词库语言进阶版 [@noneflow](https://github.com/noneflow) ([#3459](https://github.com/nonebot/nonebot2/pull/3459))\n- Plugin: nonebot-plugin-mhguesser [@noneflow](https://github.com/noneflow) ([#3464](https://github.com/nonebot/nonebot2/pull/3464))\n- Plugin: 扑克对决 [@noneflow](https://github.com/noneflow) ([#3409](https://github.com/nonebot/nonebot2/pull/3409))\n- Plugin: 一言+ [@noneflow](https://github.com/noneflow) ([#3444](https://github.com/nonebot/nonebot2/pull/3444))\n- Plugin: nonebot-plugin-ban-sticker [@noneflow](https://github.com/noneflow) ([#3429](https://github.com/nonebot/nonebot2/pull/3429))\n- Plugin: 塔吉多助手 [@noneflow](https://github.com/noneflow) ([#3455](https://github.com/nonebot/nonebot2/pull/3455))\n- Plugin: nonebot-plugin-jmcomic [@noneflow](https://github.com/noneflow) ([#3391](https://github.com/nonebot/nonebot2/pull/3391))\n- Plugin: nonebot-plugin-custom-face [@noneflow](https://github.com/noneflow) ([#3449](https://github.com/nonebot/nonebot2/pull/3449))\n- Plugin: 简易左轮禁言 [@noneflow](https://github.com/noneflow) ([#3453](https://github.com/nonebot/nonebot2/pull/3453))\n- Plugin: mcmod百科插件 [@noneflow](https://github.com/noneflow) ([#3443](https://github.com/nonebot/nonebot2/pull/3443))\n- Plugin: LaTeX 在线渲染插件 [@noneflow](https://github.com/noneflow) ([#3314](https://github.com/nonebot/nonebot2/pull/3314))\n- Plugin: nonebot-plugin-anywhere-llm [@noneflow](https://github.com/noneflow) ([#3393](https://github.com/nonebot/nonebot2/pull/3393))\n- Plugin: 贴吧监控 [@noneflow](https://github.com/noneflow) ([#3375](https://github.com/nonebot/nonebot2/pull/3375))\n- Plugin: bfvservermap [@noneflow](https://github.com/noneflow) ([#3451](https://github.com/nonebot/nonebot2/pull/3451))\n- Plugin: github_release_notifier [@noneflow](https://github.com/noneflow) ([#3388](https://github.com/nonebot/nonebot2/pull/3388))\n- Plugin: 暗语消息 [@noneflow](https://github.com/noneflow) ([#3433](https://github.com/nonebot/nonebot2/pull/3433))\n- Plugin: 森空岛 [@noneflow](https://github.com/noneflow) ([#3420](https://github.com/nonebot/nonebot2/pull/3420))\n- Plugin: asmr100 [@noneflow](https://github.com/noneflow) ([#3418](https://github.com/nonebot/nonebot2/pull/3418))\n- Plugin: 报错处理器 [@noneflow](https://github.com/noneflow) ([#3411](https://github.com/nonebot/nonebot2/pull/3411))\n- Plugin: AI 群聊助手 [@noneflow](https://github.com/noneflow) ([#3424](https://github.com/nonebot/nonebot2/pull/3424))\n- Plugin: nonebot-plugin-furryyunhei [@noneflow](https://github.com/noneflow) ([#3383](https://github.com/nonebot/nonebot2/pull/3383))\n- Plugin: MC服务器/玩家查询 [@noneflow](https://github.com/noneflow) ([#3422](https://github.com/nonebot/nonebot2/pull/3422))\n- Plugin: none_plugin_oi_helper [@noneflow](https://github.com/noneflow) ([#3416](https://github.com/nonebot/nonebot2/pull/3416))\n- Plugin: Gemini Vision [@noneflow](https://github.com/noneflow) ([#3413](https://github.com/nonebot/nonebot2/pull/3413))\n- Plugin: nonebot-plugin-zssm [@noneflow](https://github.com/noneflow) ([#3403](https://github.com/nonebot/nonebot2/pull/3403))\n- Plugin: 禁漫下载 [@noneflow](https://github.com/noneflow) ([#3395](https://github.com/nonebot/nonebot2/pull/3395))\n- Plugin: JMComic插件 [@noneflow](https://github.com/noneflow) ([#3398](https://github.com/nonebot/nonebot2/pull/3398))\n- Plugin: MoEllm聊天 [@noneflow](https://github.com/noneflow) ([#3351](https://github.com/nonebot/nonebot2/pull/3351))\n- Plugin: whoasked [@noneflow](https://github.com/noneflow) ([#3367](https://github.com/nonebot/nonebot2/pull/3367))\n- Plugin: 金价查询 [@noneflow](https://github.com/noneflow) ([#3378](https://github.com/nonebot/nonebot2/pull/3378))\n- Plugin: 丁真语音生成器 [@noneflow](https://github.com/noneflow) ([#3316](https://github.com/nonebot/nonebot2/pull/3316))\n- Plugin: LLM调用nonebot插件 [@noneflow](https://github.com/noneflow) ([#3380](https://github.com/nonebot/nonebot2/pull/3380))\n- Plugin: SuggarChat CloudFlare协议扩展附属插件 [@noneflow](https://github.com/noneflow) ([#3371](https://github.com/nonebot/nonebot2/pull/3371))\n- Plugin: No Dirty Message [@noneflow](https://github.com/noneflow) ([#3360](https://github.com/nonebot/nonebot2/pull/3360))\n- Plugin: maimai猜歌小游戏 [@noneflow](https://github.com/noneflow) ([#3318](https://github.com/nonebot/nonebot2/pull/3318))\n- Plugin: nonebot-plugin-aiochatllm [@noneflow](https://github.com/noneflow) ([#3358](https://github.com/nonebot/nonebot2/pull/3358))\n- Plugin: 拟人回复bot [@noneflow](https://github.com/noneflow) ([#3353](https://github.com/nonebot/nonebot2/pull/3353))\n\n### 🍻 机器人发布\n\n- Bot: nsybot [@noneflow](https://github.com/noneflow) ([#3610](https://github.com/nonebot/nonebot2/pull/3610))\n- Bot: Muicebot [@noneflow](https://github.com/noneflow) ([#3523](https://github.com/nonebot/nonebot2/pull/3523))\n- Bot: LiteBot [@noneflow](https://github.com/noneflow) ([#3514](https://github.com/nonebot/nonebot2/pull/3514))\n\n### 🍻 适配器发布\n\n- Adapter: nonebot-adapter-efchat [@noneflow](https://github.com/noneflow) ([#3496](https://github.com/nonebot/nonebot2/pull/3496))\n- Adapter: 删除 Gewechat 适配器 [@Shine-Light](https://github.com/Shine-Light) ([#3516](https://github.com/nonebot/nonebot2/pull/3516))\n- Adapter: nonebot-adapter-milky [@noneflow](https://github.com/noneflow) ([#3491](https://github.com/nonebot/nonebot2/pull/3491))\n- Adapter: Gewechat [@noneflow](https://github.com/noneflow) ([#3306](https://github.com/nonebot/nonebot2/pull/3306))\n\n## v2.4.2\n\n### 🚀 新功能\n\n- Feature: 添加 pydantic validator 兼容函数 [@RF-Tar-Railt](https://github.com/RF-Tar-Railt) ([#3291](https://github.com/nonebot/nonebot2/pull/3291))\n\n### 🐛 Bug 修复\n\n- Fix: shell command 词法解析错误未捕获 [@yanyongyu](https://github.com/yanyongyu) ([#3290](https://github.com/nonebot/nonebot2/pull/3290))\n\n### 📝 文档\n\n- Docs: 添加第三方插件模版 [@fllesser](https://github.com/fllesser) ([#3361](https://github.com/nonebot/nonebot2/pull/3361))\n- Docs: 商店头像 skeleton [@StarHeartHunt](https://github.com/StarHeartHunt) ([#3362](https://github.com/nonebot/nonebot2/pull/3362))\n- Docs: 添加 localstore `use_cwd` 配置项文档 [@yanyongyu](https://github.com/yanyongyu) ([#3345](https://github.com/nonebot/nonebot2/pull/3345))\n- Docs: 商店插件可用性筛选 \\& 更新排序 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#3334](https://github.com/nonebot/nonebot2/pull/3334))\n- Docs: 添加 微信公众平台 适配器描述 [@YangRucheng](https://github.com/YangRucheng) ([#3264](https://github.com/nonebot/nonebot2/pull/3264))\n- Docs: 添加 黑盒语音 适配器描述 [@lclbm](https://github.com/lclbm) ([#3259](https://github.com/nonebot/nonebot2/pull/3259))\n\n### 💫 杂项\n\n- Lint: 修复 async 函数返回值 [@yanyongyu](https://github.com/yanyongyu) ([#3364](https://github.com/nonebot/nonebot2/pull/3364))\n- Fix: 修复 pyright 类型推导问题 [@yanyongyu](https://github.com/yanyongyu) ([#3347](https://github.com/nonebot/nonebot2/pull/3347))\n- Fix: 修复 ruff lint 错误 [@yanyongyu](https://github.com/yanyongyu) ([#3346](https://github.com/nonebot/nonebot2/pull/3346))\n- Plugin: 删除插件 `pjsk` [@lgc2333](https://github.com/lgc2333) ([#3332](https://github.com/nonebot/nonebot2/pull/3332))\n- CI: 使用官方版本 ruff action [@yanyongyu](https://github.com/yanyongyu) ([#3286](https://github.com/nonebot/nonebot2/pull/3286))\n- CI: pyright 版本与 pylance 保持一致 [@yanyongyu](https://github.com/yanyongyu) ([#3285](https://github.com/nonebot/nonebot2/pull/3285))\n- CI: 临时降级 release-drafter [@yanyongyu](https://github.com/yanyongyu) ([#3284](https://github.com/nonebot/nonebot2/pull/3284))\n- Plugin: 删除 bawiki [@lgc2333](https://github.com/lgc2333) ([#3265](https://github.com/nonebot/nonebot2/pull/3265))\n- Plugin: 删除插件 nonebot_plugin_clovers [@KarisAya](https://github.com/KarisAya) ([#3254](https://github.com/nonebot/nonebot2/pull/3254))\n\n### 🍻 插件发布\n\n- Plugin: ChatGPT (OpenAI API 接口版) [@noneflow](https://github.com/noneflow) ([#3340](https://github.com/nonebot/nonebot2/pull/3340))\n- Plugin: 卡bin查询 [@noneflow](https://github.com/noneflow) ([#3324](https://github.com/nonebot/nonebot2/pull/3324))\n- Plugin: 唐菲检测 [@noneflow](https://github.com/noneflow) ([#3338](https://github.com/nonebot/nonebot2/pull/3338))\n- Plugin: nonebot-plugin-whois [@noneflow](https://github.com/noneflow) ([#3330](https://github.com/nonebot/nonebot2/pull/3330))\n- Plugin: Meme Stickers [@noneflow](https://github.com/noneflow) ([#3333](https://github.com/nonebot/nonebot2/pull/3333))\n- Plugin: 群昵称更新 [@noneflow](https://github.com/noneflow) ([#3336](https://github.com/nonebot/nonebot2/pull/3336))\n- Plugin: 简易AI聊天 [@noneflow](https://github.com/noneflow) ([#3327](https://github.com/nonebot/nonebot2/pull/3327))\n- Plugin: Vocu 语音插件 [@noneflow](https://github.com/noneflow) ([#3322](https://github.com/nonebot/nonebot2/pull/3322))\n- Plugin: LuoguLuck|洛谷运势 [@noneflow](https://github.com/noneflow) ([#3320](https://github.com/nonebot/nonebot2/pull/3320))\n- Plugin: 群聊配置 [@noneflow](https://github.com/noneflow) ([#3311](https://github.com/nonebot/nonebot2/pull/3311))\n- Plugin: llmchat [@noneflow](https://github.com/noneflow) ([#3309](https://github.com/nonebot/nonebot2/pull/3309))\n- Plugin: [ 缩写翻译 ] 能不能好好说话 - nbnhhsh [@noneflow](https://github.com/noneflow) ([#3296](https://github.com/nonebot/nonebot2/pull/3296))\n- Plugin: SuggarChat OpenAI协议聊天插件 [@noneflow](https://github.com/noneflow) ([#3222](https://github.com/nonebot/nonebot2/pull/3222))\n- Plugin: nonebot-plugin-ACMD [@noneflow](https://github.com/noneflow) ([#3283](https://github.com/nonebot/nonebot2/pull/3283))\n- Plugin: AI群聊机器人 [@noneflow](https://github.com/noneflow) ([#3258](https://github.com/nonebot/nonebot2/pull/3258))\n- Plugin: 追番小工具 [@noneflow](https://github.com/noneflow) ([#3279](https://github.com/nonebot/nonebot2/pull/3279))\n- Plugin: BotTap [@noneflow](https://github.com/noneflow) ([#3288](https://github.com/nonebot/nonebot2/pull/3288))\n- Plugin: DeepSeek [@noneflow](https://github.com/noneflow) ([#3281](https://github.com/nonebot/nonebot2/pull/3281))\n- Plugin: 群文件管理 [@noneflow](https://github.com/noneflow) ([#3271](https://github.com/nonebot/nonebot2/pull/3271))\n- Plugin: 中英文笑话 [@noneflow](https://github.com/noneflow) ([#3277](https://github.com/nonebot/nonebot2/pull/3277))\n- Plugin: 定时提醒 [@noneflow](https://github.com/noneflow) ([#3275](https://github.com/nonebot/nonebot2/pull/3275))\n- Plugin: NeuroDraw [@noneflow](https://github.com/noneflow) ([#3269](https://github.com/nonebot/nonebot2/pull/3269))\n- Plugin: Remove nonebot_plugin_bili_push [@noneflow](https://github.com/noneflow) ([#3260](https://github.com/nonebot/nonebot2/pull/3260))\n- Plugin: 堡垒之夜游戏插件 [@noneflow](https://github.com/noneflow) ([#3251](https://github.com/nonebot/nonebot2/pull/3251))\n- Plugin: nonebot-plugin-pictranslator [@noneflow](https://github.com/noneflow) ([#3257](https://github.com/nonebot/nonebot2/pull/3257))\n- Plugin: 玉！ [@noneflow](https://github.com/noneflow) ([#3247](https://github.com/nonebot/nonebot2/pull/3247))\n- Plugin: nonebot_plugin_palworld [@noneflow](https://github.com/noneflow) ([#3244](https://github.com/nonebot/nonebot2/pull/3244))\n- Plugin: 群聊总结 [@noneflow](https://github.com/noneflow) ([#3242](https://github.com/nonebot/nonebot2/pull/3242))\n- Plugin: 恶魔轮盘轻量版 [@noneflow](https://github.com/noneflow) ([#3224](https://github.com/nonebot/nonebot2/pull/3224))\n- Plugin: 羡慕 koishi [@noneflow](https://github.com/noneflow) ([#3234](https://github.com/nonebot/nonebot2/pull/3234))\n- Plugin: 每日天文一图 [@noneflow](https://github.com/noneflow) ([#3229](https://github.com/nonebot/nonebot2/pull/3229))\n- Plugin: 音频文件BPM计算器 [@noneflow](https://github.com/noneflow) ([#3217](https://github.com/nonebot/nonebot2/pull/3217))\n- Plugin: nonebot_plugin_qbittorrent_manager [@noneflow](https://github.com/noneflow) ([#3208](https://github.com/nonebot/nonebot2/pull/3208))\n- Plugin: 群成员检测 dev版 [@noneflow](https://github.com/noneflow) ([#3214](https://github.com/nonebot/nonebot2/pull/3214))\n- Plugin: Liar's Bar [@noneflow](https://github.com/noneflow) ([#3212](https://github.com/nonebot/nonebot2/pull/3212))\n- Plugin: 他们在聊什么 [@noneflow](https://github.com/noneflow) ([#3210](https://github.com/nonebot/nonebot2/pull/3210))\n- Plugin: 小真寻的WebUi [@noneflow](https://github.com/noneflow) ([#3206](https://github.com/nonebot/nonebot2/pull/3206))\n- Plugin: 夸克搜 [@noneflow](https://github.com/noneflow) ([#3203](https://github.com/nonebot/nonebot2/pull/3203))\n\n### 🍻 机器人发布\n\n- Bot: PickStarsBot [@noneflow](https://github.com/noneflow) ([#3273](https://github.com/nonebot/nonebot2/pull/3273))\n- Bot: Nekro Agent Bot [@noneflow](https://github.com/noneflow) ([#3267](https://github.com/nonebot/nonebot2/pull/3267))\n- Bot: AntiFraudBot [@noneflow](https://github.com/noneflow) ([#3263](https://github.com/nonebot/nonebot2/pull/3263))\n\n### 🍻 适配器发布\n\n- Adapter: WXMP [@noneflow](https://github.com/noneflow) ([#3219](https://github.com/nonebot/nonebot2/pull/3219))\n- Adapter: 黑盒语音 [@noneflow](https://github.com/noneflow) ([#3249](https://github.com/nonebot/nonebot2/pull/3249))\n\n## v2.4.1\n\n### 🚀 新功能\n\n- Feature: 存储 matcher 发送 prompt 的结果 [@yanyongyu](https://github.com/yanyongyu) ([#3155](https://github.com/nonebot/nonebot2/pull/3155))\n- Feature: 提升已加载的适配器日志等级 [@yanyongyu](https://github.com/yanyongyu) ([#3110](https://github.com/nonebot/nonebot2/pull/3110))\n\n### 🐛 Bug 修复\n\n- Fix: httpx proxy 与 aiohttp timeout 参数新版本修改 [@yanyongyu](https://github.com/yanyongyu) ([#3152](https://github.com/nonebot/nonebot2/pull/3152))\n- Fix: 屏蔽 pydantic 2.10.0 [@yanyongyu](https://github.com/yanyongyu) ([#3137](https://github.com/nonebot/nonebot2/pull/3137))\n\n### 📝 文档\n\n- Docs: 添加 localstore 插件配置 [@yanyongyu](https://github.com/yanyongyu) ([#3197](https://github.com/nonebot/nonebot2/pull/3197))\n- Docs: 使用勾选框而不是评论来重新测试插件 [@he0119](https://github.com/he0119) ([#3158](https://github.com/nonebot/nonebot2/pull/3158))\n- Docs: 添加 pytest-asyncio 配置 [@yanyongyu](https://github.com/yanyongyu) ([#3136](https://github.com/nonebot/nonebot2/pull/3136))\n- Docs: 移除侧栏遮罩及启用构建加速 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#3135](https://github.com/nonebot/nonebot2/pull/3135))\n- Docs: 添加 Mail 适配器说明 [@mobyw](https://github.com/mobyw) ([#3134](https://github.com/nonebot/nonebot2/pull/3134))\n- Docs: 修复 wwads 造成的 client 水合不匹配 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#3106](https://github.com/nonebot/nonebot2/pull/3106))\n- Docs: 修复 wwads [@StarHeartHunt](https://github.com/StarHeartHunt) ([#3105](https://github.com/nonebot/nonebot2/pull/3105))\n- Docs: 修复侧边栏折叠状态问题 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#3101](https://github.com/nonebot/nonebot2/pull/3101))\n- Docs: Changelog 按页码挂载 route [@StarHeartHunt](https://github.com/StarHeartHunt) ([#3100](https://github.com/nonebot/nonebot2/pull/3100))\n- Docs: 修复 changelog 链接 [@yanyongyu](https://github.com/yanyongyu) ([#3098](https://github.com/nonebot/nonebot2/pull/3098))\n\n### 💫 杂项\n\n- Develop: 发布议题模板增加 Publish 标签 [@he0119](https://github.com/he0119) ([#3174](https://github.com/nonebot/nonebot2/pull/3174))\n- Plugin: 移除插件 riffusion [@lgc2333](https://github.com/lgc2333) ([#3171](https://github.com/nonebot/nonebot2/pull/3171))\n- CI: 删除 NoneFlow 中关于 pre-commit 的部分 [@he0119](https://github.com/he0119) ([#3166](https://github.com/nonebot/nonebot2/pull/3166))\n- Develop: 完全使用 ruff 替代 isort 与 black [@yanyongyu](https://github.com/yanyongyu) ([#3151](https://github.com/nonebot/nonebot2/pull/3151))\n- Plugin: 删除`function` 插件，添加 `batch-withdrawal` 插件标签 [@zhongwen-4](https://github.com/zhongwen-4) ([#3118](https://github.com/nonebot/nonebot2/pull/3118))\n- Plugin: 删除插件 `nonebot-plugin-llob-master` [@kanbereina](https://github.com/kanbereina) ([#3115](https://github.com/nonebot/nonebot2/pull/3115))\n\n### 🍻 插件发布\n\n- Plugin: nonebot_plugin_pjsekaihelper [@noneflow](https://github.com/noneflow) ([#3180](https://github.com/nonebot/nonebot2/pull/3180))\n- Plugin: Prometheus 监控 [@noneflow](https://github.com/noneflow) ([#3199](https://github.com/nonebot/nonebot2/pull/3199))\n- Plugin: 加群自动审批 [@noneflow](https://github.com/noneflow) ([#3201](https://github.com/nonebot/nonebot2/pull/3201))\n- Plugin: nonebot-plugin-flo-luck [@noneflow](https://github.com/noneflow) ([#3188](https://github.com/nonebot/nonebot2/pull/3188))\n- Plugin: 求生之路addons文件管理 [@noneflow](https://github.com/noneflow) ([#3194](https://github.com/nonebot/nonebot2/pull/3194))\n- Plugin: 好友与群邀请管理 [@noneflow](https://github.com/noneflow) ([#3190](https://github.com/nonebot/nonebot2/pull/3190))\n- Plugin: 简易群聊屏蔽 [@noneflow](https://github.com/noneflow) ([#3196](https://github.com/nonebot/nonebot2/pull/3196))\n- Plugin: Arcaea表情包生成器 [@noneflow](https://github.com/noneflow) ([#3160](https://github.com/nonebot/nonebot2/pull/3160))\n- Plugin: PicStatus Template ZhenXun [@noneflow](https://github.com/noneflow) ([#3192](https://github.com/nonebot/nonebot2/pull/3192))\n- Plugin: nonebot-plugin-ollama [@noneflow](https://github.com/noneflow) ([#3182](https://github.com/nonebot/nonebot2/pull/3182))\n- Plugin: 南无阿弥陀佛 [@noneflow](https://github.com/noneflow) ([#3178](https://github.com/nonebot/nonebot2/pull/3178))\n- Plugin: 奶龙魔法 [@noneflow](https://github.com/noneflow) ([#3175](https://github.com/nonebot/nonebot2/pull/3175))\n- Plugin: CSGO_MARKET [@noneflow](https://github.com/noneflow) ([#3148](https://github.com/nonebot/nonebot2/pull/3148))\n- Plugin: 谷歌 Gemini 多模态助手 [@noneflow](https://github.com/noneflow) ([#3165](https://github.com/nonebot/nonebot2/pull/3165))\n- Plugin: 名片赞，表情回应插件 [@noneflow](https://github.com/noneflow) ([#3170](https://github.com/nonebot/nonebot2/pull/3170))\n- Plugin: PCR签到重制版 [@noneflow](https://github.com/noneflow) ([#3157](https://github.com/nonebot/nonebot2/pull/3157))\n- Plugin: 发言统计 [@noneflow](https://github.com/noneflow) ([#3162](https://github.com/nonebot/nonebot2/pull/3162))\n- Plugin: 链接分享解析器重制版 [@noneflow](https://github.com/noneflow) ([#3154](https://github.com/nonebot/nonebot2/pull/3154))\n- Plugin: ZXWB词库问答 [@noneflow](https://github.com/noneflow) ([#3143](https://github.com/nonebot/nonebot2/pull/3143))\n- Plugin: nonebot-plugin-hyp [@noneflow](https://github.com/noneflow) ([#3140](https://github.com/nonebot/nonebot2/pull/3140))\n- Plugin: nonebot-plugin-zepplife [@noneflow](https://github.com/noneflow) ([#3132](https://github.com/nonebot/nonebot2/pull/3132))\n- Plugin: 权限控制 [@noneflow](https://github.com/noneflow) ([#3112](https://github.com/nonebot/nonebot2/pull/3112))\n- Plugin: nonebot-plugin-api-scheduler [@noneflow](https://github.com/noneflow) ([#3133](https://github.com/nonebot/nonebot2/pull/3133))\n- Plugin: nb商店插件安装器web版 [@noneflow](https://github.com/noneflow) ([#3126](https://github.com/nonebot/nonebot2/pull/3126))\n- Plugin: 恩情课文 [@noneflow](https://github.com/noneflow) ([#3123](https://github.com/nonebot/nonebot2/pull/3123))\n- Plugin: 自动点赞订阅赞 [@noneflow](https://github.com/noneflow) ([#3103](https://github.com/nonebot/nonebot2/pull/3103))\n- Plugin: 违禁词撤回 [@noneflow](https://github.com/noneflow) ([#3117](https://github.com/nonebot/nonebot2/pull/3117))\n- Plugin: b站弹幕监控 [@noneflow](https://github.com/noneflow) ([#3091](https://github.com/nonebot/nonebot2/pull/3091))\n- Plugin: nonebot_plugin_better_broadcast [@noneflow](https://github.com/noneflow) ([#3114](https://github.com/nonebot/nonebot2/pull/3114))\n- Plugin: PyPi下载统计 [@noneflow](https://github.com/noneflow) ([#3109](https://github.com/nonebot/nonebot2/pull/3109))\n- Plugin: nonebot-plugin-leetcodeapi-khasa [@noneflow](https://github.com/noneflow) ([#3077](https://github.com/nonebot/nonebot2/pull/3077))\n- Plugin: Ohh My Bot [@noneflow](https://github.com/noneflow) ([#3089](https://github.com/nonebot/nonebot2/pull/3089))\n\n### 🍻 机器人发布\n\n- Bot: Mio澪 [@noneflow](https://github.com/noneflow) ([#3121](https://github.com/nonebot/nonebot2/pull/3121))\n\n### 🍻 适配器发布\n\n- Adapter: Mail [@noneflow](https://github.com/noneflow) ([#3129](https://github.com/nonebot/nonebot2/pull/3129))\n\n## v2.4.0\n\n### 🚀 新功能\n\n- Feature: 跳过部分非必要的 task group 创建 [@yanyongyu](https://github.com/yanyongyu) ([#3095](https://github.com/nonebot/nonebot2/pull/3095))\n- Feature: 迁移至结构化并发框架 AnyIO [@yanyongyu](https://github.com/yanyongyu) ([#3053](https://github.com/nonebot/nonebot2/pull/3053))\n- Feature: 添加 websockets 驱动器 proxy 连接警告 [@shoucandanghehe](https://github.com/shoucandanghehe) ([#2916](https://github.com/nonebot/nonebot2/pull/2916))\n\n### 🐛 Bug 修复\n\n- Fix: 修复结构化并发子依赖取消缓存问题 [@yanyongyu](https://github.com/yanyongyu) ([#3084](https://github.com/nonebot/nonebot2/pull/3084))\n\n### 📝 文档\n\n- Docs: 新增 nonebug 新版启动需要的配置 [@yanyongyu](https://github.com/yanyongyu) ([#3087](https://github.com/nonebot/nonebot2/pull/3087))\n- Docs: 修复侧边栏滚动 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#3062](https://github.com/nonebot/nonebot2/pull/3062))\n- Docs: 升级到 Docusaurus V3 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#2956](https://github.com/nonebot/nonebot2/pull/2956))\n- Docs: 修改文档示例代码与部分表述 [@yixinNB](https://github.com/yixinNB) ([#2797](https://github.com/nonebot/nonebot2/pull/2797))\n- Docs: 添加钩子函数 IgnoredException 用法 [@refparo](https://github.com/refparo) ([#2912](https://github.com/nonebot/nonebot2/pull/2912))\n\n### 💫 杂项\n\n- Plugin: 移除不再维护的插件 [@ssttkkl](https://github.com/ssttkkl) ([#3040](https://github.com/nonebot/nonebot2/pull/3040))\n- Plugin: 删除不再维护的 simplemusic hikarisearch 插件 [@MeetWq](https://github.com/MeetWq) ([#2933](https://github.com/nonebot/nonebot2/pull/2933))\n- Plugin: 删除插件 `nonebot-plugin-ntqq-restart` [@kanbereina](https://github.com/kanbereina) ([#2926](https://github.com/nonebot/nonebot2/pull/2926))\n- Adapter: 移除社区版 mirai 适配器 [@RF-Tar-Railt](https://github.com/RF-Tar-Railt) ([#2909](https://github.com/nonebot/nonebot2/pull/2909))\n\n### 🍻 插件发布\n\n- Plugin: Comfyui绘图插件 [@noneflow](https://github.com/noneflow) ([#3081](https://github.com/nonebot/nonebot2/pull/3081))\n- Plugin: 每日wife [@noneflow](https://github.com/noneflow) ([#3094](https://github.com/nonebot/nonebot2/pull/3094))\n- Plugin: nonebot_plugin_impart [@noneflow](https://github.com/noneflow) ([#3079](https://github.com/nonebot/nonebot2/pull/3079))\n- Plugin: Pix图库 [@noneflow](https://github.com/noneflow) ([#3083](https://github.com/nonebot/nonebot2/pull/3083))\n- Plugin: nonebot_plugin_partner_join [@noneflow](https://github.com/noneflow) ([#3051](https://github.com/nonebot/nonebot2/pull/3051))\n- Plugin: pong [@noneflow](https://github.com/noneflow) ([#3066](https://github.com/nonebot/nonebot2/pull/3066))\n- Plugin: Bot的消息也是消息 [@noneflow](https://github.com/noneflow) ([#3064](https://github.com/nonebot/nonebot2/pull/3064))\n- Plugin: BiliMusic Downloader [@noneflow](https://github.com/noneflow) ([#3046](https://github.com/nonebot/nonebot2/pull/3046))\n- Plugin: 防撤回 [@noneflow](https://github.com/noneflow) ([#3055](https://github.com/nonebot/nonebot2/pull/3055))\n- Plugin: nonebot_plugin_mai_arcade [@noneflow](https://github.com/noneflow) ([#3047](https://github.com/nonebot/nonebot2/pull/3047))\n- Plugin: DDNet 成绩查询 [@noneflow](https://github.com/noneflow) ([#3031](https://github.com/nonebot/nonebot2/pull/3031))\n- Plugin: 省流 [@noneflow](https://github.com/noneflow) ([#3052](https://github.com/nonebot/nonebot2/pull/3052))\n- Plugin: FishSpeechTTS [@noneflow](https://github.com/noneflow) ([#3050](https://github.com/nonebot/nonebot2/pull/3050))\n- Plugin: 语音点歌 [@noneflow](https://github.com/noneflow) ([#3037](https://github.com/nonebot/nonebot2/pull/3037))\n- Plugin: Gotify [@noneflow](https://github.com/noneflow) ([#3043](https://github.com/nonebot/nonebot2/pull/3043))\n- Plugin: 涩图插件 [@noneflow](https://github.com/noneflow) ([#3039](https://github.com/nonebot/nonebot2/pull/3039))\n- Plugin: boom [@noneflow](https://github.com/noneflow) ([#3017](https://github.com/nonebot/nonebot2/pull/3017))\n- Plugin: 恶魔轮盘赌 [@noneflow](https://github.com/noneflow) ([#3033](https://github.com/nonebot/nonebot2/pull/3033))\n- Plugin: 机厅 [@noneflow](https://github.com/noneflow) ([#3029](https://github.com/nonebot/nonebot2/pull/3029))\n- Plugin: PM帮助 [@noneflow](https://github.com/noneflow) ([#3023](https://github.com/nonebot/nonebot2/pull/3023))\n- Plugin: NailongRemove [@noneflow](https://github.com/noneflow) ([#2972](https://github.com/nonebot/nonebot2/pull/2972))\n- Plugin: 团购 [@noneflow](https://github.com/noneflow) ([#3027](https://github.com/nonebot/nonebot2/pull/3027))\n- Plugin: 真寻日报 [@noneflow](https://github.com/noneflow) ([#3021](https://github.com/nonebot/nonebot2/pull/3021))\n- Plugin: 运行状态 [@noneflow](https://github.com/noneflow) ([#3019](https://github.com/nonebot/nonebot2/pull/3019))\n- Plugin: 西工大翱翔门户成绩监控 [@noneflow](https://github.com/noneflow) ([#3013](https://github.com/nonebot/nonebot2/pull/3013))\n- Plugin: nb插件更新器 [@noneflow](https://github.com/noneflow) ([#3015](https://github.com/nonebot/nonebot2/pull/3015))\n- Plugin: 涩涩保存器 [@noneflow](https://github.com/noneflow) ([#2988](https://github.com/nonebot/nonebot2/pull/2988))\n- Plugin: nonebot_plugin_BFVsearch [@noneflow](https://github.com/noneflow) ([#3008](https://github.com/nonebot/nonebot2/pull/3008))\n- Plugin: lingyi_chat [@noneflow](https://github.com/noneflow) ([#3006](https://github.com/nonebot/nonebot2/pull/3006))\n- Plugin: ZXPM插件管理 [@noneflow](https://github.com/noneflow) ([#3003](https://github.com/nonebot/nonebot2/pull/3003))\n- Plugin: MinecraftWatcher [@noneflow](https://github.com/noneflow) ([#3010](https://github.com/nonebot/nonebot2/pull/3010))\n- Plugin: BF5_grouptools [@noneflow](https://github.com/noneflow) ([#3004](https://github.com/nonebot/nonebot2/pull/3004))\n- Plugin: lolinfo [@noneflow](https://github.com/noneflow) ([#2997](https://github.com/nonebot/nonebot2/pull/2997))\n- Plugin: osu! Match Monitor [@noneflow](https://github.com/noneflow) ([#2985](https://github.com/nonebot/nonebot2/pull/2985))\n- Plugin: Marsho AI插件 [@noneflow](https://github.com/noneflow) ([#2993](https://github.com/nonebot/nonebot2/pull/2993))\n- Plugin: nonechat [@noneflow](https://github.com/noneflow) ([#2990](https://github.com/nonebot/nonebot2/pull/2990))\n- Plugin: nonebot_plugin_SimpleToWrite [@noneflow](https://github.com/noneflow) ([#2995](https://github.com/nonebot/nonebot2/pull/2995))\n- Plugin: Beat Saber查分器 [@noneflow](https://github.com/noneflow) ([#2974](https://github.com/nonebot/nonebot2/pull/2974))\n- Plugin: githubmodels [@noneflow](https://github.com/noneflow) ([#2945](https://github.com/nonebot/nonebot2/pull/2945))\n- Plugin: 给我点颜色瞧瞧 [@noneflow](https://github.com/noneflow) ([#2984](https://github.com/nonebot/nonebot2/pull/2984))\n- Plugin: pjsk-helper [@noneflow](https://github.com/noneflow) ([#2980](https://github.com/nonebot/nonebot2/pull/2980))\n- Plugin: 趣味内容插件 [@noneflow](https://github.com/noneflow) ([#2981](https://github.com/nonebot/nonebot2/pull/2981))\n- Plugin: 计算器：游戏 [@noneflow](https://github.com/noneflow) ([#2976](https://github.com/nonebot/nonebot2/pull/2976))\n- Plugin: nonebot-plugin-yareminder [@noneflow](https://github.com/noneflow) ([#2964](https://github.com/nonebot/nonebot2/pull/2964))\n- Plugin: 批量撤回 [@noneflow](https://github.com/noneflow) ([#2966](https://github.com/nonebot/nonebot2/pull/2966))\n- Plugin: inspect [@noneflow](https://github.com/noneflow) ([#2971](https://github.com/nonebot/nonebot2/pull/2971))\n- Plugin: 通用信息 [@noneflow](https://github.com/noneflow) ([#2969](https://github.com/nonebot/nonebot2/pull/2969))\n- Plugin: SSE日志输出流 [@noneflow](https://github.com/noneflow) ([#2960](https://github.com/nonebot/nonebot2/pull/2960))\n- Plugin: WITFF [@noneflow](https://github.com/noneflow) ([#2955](https://github.com/nonebot/nonebot2/pull/2955))\n- Plugin: weather-rank [@noneflow](https://github.com/noneflow) ([#2949](https://github.com/nonebot/nonebot2/pull/2949))\n- Plugin: 二维码生成器 [@noneflow](https://github.com/noneflow) ([#2942](https://github.com/nonebot/nonebot2/pull/2942))\n- Plugin: 次元星辰 [@noneflow](https://github.com/noneflow) ([#2935](https://github.com/nonebot/nonebot2/pull/2935))\n- Plugin: nonebot-plugin-tarina-lang-turbo [@noneflow](https://github.com/noneflow) ([#2938](https://github.com/nonebot/nonebot2/pull/2938))\n- Plugin: 狼人杀 [@noneflow](https://github.com/noneflow) ([#2932](https://github.com/nonebot/nonebot2/pull/2932))\n- Plugin: 阿瓦隆 [@noneflow](https://github.com/noneflow) ([#2915](https://github.com/nonebot/nonebot2/pull/2915))\n- Plugin: 消音器 [@noneflow](https://github.com/noneflow) ([#2919](https://github.com/nonebot/nonebot2/pull/2919))\n- Plugin: 悠悠 [@noneflow](https://github.com/noneflow) ([#2928](https://github.com/nonebot/nonebot2/pull/2928))\n- Plugin: LLOneBot-Master [@noneflow](https://github.com/noneflow) ([#2925](https://github.com/nonebot/nonebot2/pull/2925))\n- Plugin: 无情的发图姬 [@noneflow](https://github.com/noneflow) ([#2923](https://github.com/nonebot/nonebot2/pull/2923))\n- Plugin: maimai DX 查分 [@noneflow](https://github.com/noneflow) ([#2921](https://github.com/nonebot/nonebot2/pull/2921))\n- Plugin: Minecraft查服 [@noneflow](https://github.com/noneflow) ([#2882](https://github.com/nonebot/nonebot2/pull/2882))\n- Plugin: lagrange [@noneflow](https://github.com/noneflow) ([#2898](https://github.com/nonebot/nonebot2/pull/2898))\n- Plugin: nekro-agent [@noneflow](https://github.com/noneflow) ([#2896](https://github.com/nonebot/nonebot2/pull/2896))\n- Plugin: nonebot_plugin_mute [@noneflow](https://github.com/noneflow) ([#2893](https://github.com/nonebot/nonebot2/pull/2893))\n- Plugin: LiteyukiBot(plugin) [@noneflow](https://github.com/noneflow) ([#2905](https://github.com/nonebot/nonebot2/pull/2905))\n- Plugin: 复读6 [@noneflow](https://github.com/noneflow) ([#2900](https://github.com/nonebot/nonebot2/pull/2900))\n\n### 🍻 机器人发布\n\n- Bot: CanrotBot [@noneflow](https://github.com/noneflow) ([#3086](https://github.com/nonebot/nonebot2/pull/3086))\n- Bot: 小安提Bot [@noneflow](https://github.com/noneflow) ([#3061](https://github.com/nonebot/nonebot2/pull/3061))\n\n## v2.3.3\n\n### 🚀 新功能\n\n- Feature: 优化依赖注入在 pydantic v2 下的性能 [@yanyongyu](https://github.com/yanyongyu) ([#2870](https://github.com/nonebot/nonebot2/pull/2870))\n- Feature: 添加遗漏的类型标注 [@yanyongyu](https://github.com/yanyongyu) ([#2856](https://github.com/nonebot/nonebot2/pull/2856))\n\n### 🐛 Bug 修复\n\n- Fix: 错误的类型标注和 annotated 处理 [@yanyongyu](https://github.com/yanyongyu) ([#2828](https://github.com/nonebot/nonebot2/pull/2828))\n\n### 📝 文档\n\n- Docs: 添加 Windows Powershell 设置环境变量方法 [@LeoQuote](https://github.com/LeoQuote) ([#2874](https://github.com/nonebot/nonebot2/pull/2874))\n- Docs: 更新 localstore 插件文档 [@yanyongyu](https://github.com/yanyongyu) ([#2871](https://github.com/nonebot/nonebot2/pull/2871))\n\n### 💫 杂项\n\n- Plugin: 修改插件 system-command 信息 [@tkgs0](https://github.com/tkgs0) ([#2862](https://github.com/nonebot/nonebot2/pull/2862))\n- Plugin: 修改 nonebot-plugin-fishing 插件作者 [@ALittleBot](https://github.com/ALittleBot) ([#2854](https://github.com/nonebot/nonebot2/pull/2854))\n- Bot: 更新 Minecraft QQBot 信息 [@Lonely-Sails](https://github.com/Lonely-Sails) ([#2838](https://github.com/nonebot/nonebot2/pull/2838))\n- Plugin: 移除 kanonbot 插件 [@SuperGuGuGu](https://github.com/SuperGuGuGu) ([#2819](https://github.com/nonebot/nonebot2/pull/2819))\n- Plugin: 更新插件 sparkapi 信息 [@CCLMSY](https://github.com/CCLMSY) ([#2812](https://github.com/nonebot/nonebot2/pull/2812))\n- Plugin: 修改插件 miragetank \\& charpic 信息 [@1umine](https://github.com/1umine) ([#2807](https://github.com/nonebot/nonebot2/pull/2807))\n\n### 🍻 插件发布\n\n- Plugin: nonebot-plugin-wait-a-minute [@noneflow](https://github.com/noneflow) ([#2902](https://github.com/nonebot/nonebot2/pull/2902))\n- Plugin: 你看我像 [@noneflow](https://github.com/noneflow) ([#2895](https://github.com/nonebot/nonebot2/pull/2895))\n- Plugin: dify插件 [@noneflow](https://github.com/noneflow) ([#2889](https://github.com/nonebot/nonebot2/pull/2889))\n- Plugin: mai2_pcount [@noneflow](https://github.com/noneflow) ([#2891](https://github.com/nonebot/nonebot2/pull/2891))\n- Plugin: nonebot-plugin-ehentai-search [@noneflow](https://github.com/noneflow) ([#2885](https://github.com/nonebot/nonebot2/pull/2885))\n- Plugin: pokepoke_miss [@noneflow](https://github.com/noneflow) ([#2883](https://github.com/nonebot/nonebot2/pull/2883))\n- Plugin: 聊天截图伪造 [@noneflow](https://github.com/noneflow) ([#2880](https://github.com/nonebot/nonebot2/pull/2880))\n- Plugin: ba-tools [@noneflow](https://github.com/noneflow) ([#2867](https://github.com/nonebot/nonebot2/pull/2867))\n- Plugin: 精华消息管理 [@noneflow](https://github.com/noneflow) ([#2873](https://github.com/nonebot/nonebot2/pull/2873))\n- Plugin: B站收藏夹监视器 [@noneflow](https://github.com/noneflow) ([#2869](https://github.com/nonebot/nonebot2/pull/2869))\n- Plugin: Alist [@noneflow](https://github.com/noneflow) ([#2865](https://github.com/nonebot/nonebot2/pull/2865))\n- Plugin: 🦌管签到 [@noneflow](https://github.com/noneflow) ([#2859](https://github.com/nonebot/nonebot2/pull/2859))\n- Plugin: 漂流瓶 [@noneflow](https://github.com/noneflow) ([#2861](https://github.com/nonebot/nonebot2/pull/2861))\n- Plugin: 奇怪的小功能 [@noneflow](https://github.com/noneflow) ([#2851](https://github.com/nonebot/nonebot2/pull/2851))\n- Plugin: SunoAI音乐生成 [@noneflow](https://github.com/noneflow) ([#2853](https://github.com/nonebot/nonebot2/pull/2853))\n- Plugin: 谁是卷王 [@noneflow](https://github.com/noneflow) ([#2849](https://github.com/nonebot/nonebot2/pull/2849))\n- Plugin: GPT-SoVITS 语音合成 [@noneflow](https://github.com/noneflow) ([#2847](https://github.com/nonebot/nonebot2/pull/2847))\n- Plugin: 基于清影的AI视频生成 [@noneflow](https://github.com/noneflow) ([#2843](https://github.com/nonebot/nonebot2/pull/2843))\n- Plugin: 命令行 [@noneflow](https://github.com/noneflow) ([#2840](https://github.com/nonebot/nonebot2/pull/2840))\n- Plugin: exe_code [@noneflow](https://github.com/noneflow) ([#2835](https://github.com/nonebot/nonebot2/pull/2835))\n- Plugin: nonebot-plugin-autopush [@noneflow](https://github.com/noneflow) ([#2833](https://github.com/nonebot/nonebot2/pull/2833))\n- Plugin: vv_helper [@noneflow](https://github.com/noneflow) ([#2825](https://github.com/nonebot/nonebot2/pull/2825))\n- Plugin: nonebot_plugin_game_torrent [@noneflow](https://github.com/noneflow) ([#2827](https://github.com/nonebot/nonebot2/pull/2827))\n- Plugin: 每日油价 [@noneflow](https://github.com/noneflow) ([#2822](https://github.com/nonebot/nonebot2/pull/2822))\n- Plugin: wordle [@noneflow](https://github.com/noneflow) ([#2818](https://github.com/nonebot/nonebot2/pull/2818))\n- Plugin: 再润 [@noneflow](https://github.com/noneflow) ([#2816](https://github.com/nonebot/nonebot2/pull/2816))\n- Plugin: 漫展/展览查询 [@noneflow](https://github.com/noneflow) ([#2811](https://github.com/nonebot/nonebot2/pull/2811))\n- Plugin: 鸣潮wiki [@noneflow](https://github.com/noneflow) ([#2804](https://github.com/nonebot/nonebot2/pull/2804))\n- Plugin: cloudfare R2 客服端 [@noneflow](https://github.com/noneflow) ([#2806](https://github.com/nonebot/nonebot2/pull/2806))\n- Plugin: AnyMate小助手 [@noneflow](https://github.com/noneflow) ([#2761](https://github.com/nonebot/nonebot2/pull/2761))\n\n### 🍻 机器人发布\n\n- Bot: Minecraft_QQBot [@noneflow](https://github.com/noneflow) ([#2837](https://github.com/nonebot/nonebot2/pull/2837))\n- Bot: 星辰 Bot [@noneflow](https://github.com/noneflow) ([#2824](https://github.com/nonebot/nonebot2/pull/2824))\n\n## v2.3.2\n\n### 🐛 Bug 修复\n\n- Fix: 修复 ForwardRef eval 时参数 recursive_guard 缺失 [@he0119](https://github.com/he0119) ([#2778](https://github.com/nonebot/nonebot2/pull/2778))\n\n### 📝 文档\n\n- Docs: 修改导航栏开源之夏链接 [@KomoriDev](https://github.com/KomoriDev) ([#2798](https://github.com/nonebot/nonebot2/pull/2798))\n- Docs: `on_keyword` 参数类型错误 [@TaskManagerOL](https://github.com/TaskManagerOL) ([#2795](https://github.com/nonebot/nonebot2/pull/2795))\n- Docs: 修复单元测试示例代码 [@mobyw](https://github.com/mobyw) ([#2741](https://github.com/nonebot/nonebot2/pull/2741))\n- Docs: 修改依赖注入定义链接 [@Weltolk](https://github.com/Weltolk) ([#2733](https://github.com/nonebot/nonebot2/pull/2733))\n\n### 🍻 插件发布\n\n- Plugin: 指令更新NapCat [@noneflow](https://github.com/noneflow) ([#2791](https://github.com/nonebot/nonebot2/pull/2791))\n- Plugin: QQ群-Discord 互通 [@noneflow](https://github.com/noneflow) ([#2788](https://github.com/nonebot/nonebot2/pull/2788))\n- Plugin: nonebot_plugin_obastatus [@noneflow](https://github.com/noneflow) ([#2780](https://github.com/nonebot/nonebot2/pull/2780))\n- Plugin: b站消息转发 [@noneflow](https://github.com/noneflow) ([#2785](https://github.com/nonebot/nonebot2/pull/2785))\n- Plugin: Daily Task [@noneflow](https://github.com/noneflow) ([#2769](https://github.com/nonebot/nonebot2/pull/2769))\n- Plugin: EVE ONLINE 多功能机器人\n  版本 - v0.2.3\n  [@noneflow](https://github.com/noneflow) ([#2782](https://github.com/nonebot/nonebot2/pull/2782))\n- Plugin: NTQQ自动登录/断连重启 [@noneflow](https://github.com/noneflow) ([#2786](https://github.com/nonebot/nonebot2/pull/2786))\n- Plugin: asmr [@noneflow](https://github.com/noneflow) ([#2775](https://github.com/nonebot/nonebot2/pull/2775))\n- Plugin: 日麻猜手牌小游戏 [@noneflow](https://github.com/noneflow) ([#2777](https://github.com/nonebot/nonebot2/pull/2777))\n- Plugin: 绝地潜兵信息查询小助手 [@noneflow](https://github.com/noneflow) ([#2772](https://github.com/nonebot/nonebot2/pull/2772))\n- Plugin: MCSM小助手 [@noneflow](https://github.com/noneflow) ([#2773](https://github.com/nonebot/nonebot2/pull/2773))\n- Plugin: 多模态AI工具 [@noneflow](https://github.com/noneflow) ([#2758](https://github.com/nonebot/nonebot2/pull/2758))\n- Plugin: nonebot-plugin-easymarkdown [@noneflow](https://github.com/noneflow) ([#2767](https://github.com/nonebot/nonebot2/pull/2767))\n- Plugin: 峯驰外包 [@noneflow](https://github.com/noneflow) ([#2765](https://github.com/nonebot/nonebot2/pull/2765))\n- Plugin: 鸣潮抽卡记录分析 [@noneflow](https://github.com/noneflow) ([#2763](https://github.com/nonebot/nonebot2/pull/2763))\n- Plugin: nonebot-plugin-xjie-weather [@noneflow](https://github.com/noneflow) ([#2756](https://github.com/nonebot/nonebot2/pull/2756))\n- Plugin: 颜值评分 [@noneflow](https://github.com/noneflow) ([#2752](https://github.com/nonebot/nonebot2/pull/2752))\n- Plugin: 学园偶像大师算分插件 [@noneflow](https://github.com/noneflow) ([#2750](https://github.com/nonebot/nonebot2/pull/2750))\n- Plugin: nonebot-plugin-lynchpined [@noneflow](https://github.com/noneflow) ([#2748](https://github.com/nonebot/nonebot2/pull/2748))\n- Plugin: QQShell [@noneflow](https://github.com/noneflow) ([#2745](https://github.com/nonebot/nonebot2/pull/2745))\n- Plugin: ai唱歌 [@noneflow](https://github.com/noneflow) ([#2743](https://github.com/nonebot/nonebot2/pull/2743))\n- Plugin: 复读姬+1 PlusOne [@noneflow](https://github.com/noneflow) ([#2732](https://github.com/nonebot/nonebot2/pull/2732))\n- Plugin: 高优先级关闭信号钩子插件 [@noneflow](https://github.com/noneflow) ([#2737](https://github.com/nonebot/nonebot2/pull/2737))\n- Plugin: 插件响应鉴权 [@noneflow](https://github.com/noneflow) ([#2727](https://github.com/nonebot/nonebot2/pull/2727))\n- Plugin: DG-Lab-Play [@noneflow](https://github.com/noneflow) ([#2729](https://github.com/nonebot/nonebot2/pull/2729))\n\n## v2.3.1\n\n### 🐛 Bug 修复\n\n- Fix: State ForwardRef 检测错误 [@yanyongyu](https://github.com/yanyongyu) ([#2698](https://github.com/nonebot/nonebot2/pull/2698))\n\n### 📝 文档\n\n- Docs: 修正 匹配扩展 中的示例 [@KomoriDev](https://github.com/KomoriDev) ([#2722](https://github.com/nonebot/nonebot2/pull/2722))\n- Docs: 更新 Mirai 适配器说明 [@RF-Tar-Railt](https://github.com/RF-Tar-Railt) ([#2715](https://github.com/nonebot/nonebot2/pull/2715))\n- Docs: 添加 Tailchat 适配器说明 [@eya46](https://github.com/eya46) ([#2694](https://github.com/nonebot/nonebot2/pull/2694))\n- Docs: 添加 uwu logo [@StarHeartHunt](https://github.com/StarHeartHunt) ([#2689](https://github.com/nonebot/nonebot2/pull/2689))\n\n### 💫 杂项\n\n- Plugin: 移除已在 PyPI 上删除的 `covid` 插件和 `molar-mass` 插件 [@NCBM](https://github.com/NCBM) ([#2712](https://github.com/nonebot/nonebot2/pull/2712))\n\n### 🍻 插件发布\n\n- Plugin: 自定义人格和AI绘图的混合聊天BOT [@noneflow](https://github.com/noneflow) ([#2724](https://github.com/nonebot/nonebot2/pull/2724))\n- Plugin: nonebot-plugin-calc24 [@noneflow](https://github.com/noneflow) ([#2721](https://github.com/nonebot/nonebot2/pull/2721))\n- Plugin: nonebot-plugin-tsugu-bangdream-bot [@noneflow](https://github.com/noneflow) ([#2719](https://github.com/nonebot/nonebot2/pull/2719))\n- Plugin: 科大讯飞星火大语言模型官方API聊天机器人插件 [@noneflow](https://github.com/noneflow) ([#2717](https://github.com/nonebot/nonebot2/pull/2717))\n- Plugin: nonebot_plugin_valve_server_query [@noneflow](https://github.com/noneflow) ([#2711](https://github.com/nonebot/nonebot2/pull/2711))\n- Plugin: 库洛游戏信息 [@noneflow](https://github.com/noneflow) ([#2706](https://github.com/nonebot/nonebot2/pull/2706))\n- Plugin: BanG Dream! Tsugu Frontend [@noneflow](https://github.com/noneflow) ([#2708](https://github.com/nonebot/nonebot2/pull/2708))\n- Plugin: 神秘学助手 [@noneflow](https://github.com/noneflow) ([#2700](https://github.com/nonebot/nonebot2/pull/2700))\n- Plugin: nonebot-plugin-furryfusion [@noneflow](https://github.com/noneflow) ([#2705](https://github.com/nonebot/nonebot2/pull/2705))\n- Plugin: nonebot-plugin-RanFurryPic [@noneflow](https://github.com/noneflow) ([#2703](https://github.com/nonebot/nonebot2/pull/2703))\n- Plugin: with_ai_agents [@noneflow](https://github.com/noneflow) ([#2697](https://github.com/nonebot/nonebot2/pull/2697))\n- Plugin: 番剧下载 [@noneflow](https://github.com/noneflow) ([#2691](https://github.com/nonebot/nonebot2/pull/2691))\n\n### 🍻 适配器发布\n\n- Adapter: Mirai [@noneflow](https://github.com/noneflow) ([#2714](https://github.com/nonebot/nonebot2/pull/2714))\n- Adapter: Tailchat [@noneflow](https://github.com/noneflow) ([#2693](https://github.com/nonebot/nonebot2/pull/2693))\n\n## v2.3.0\n\n### 💥 破坏性变更\n\n- Feature: 嵌套插件名称作用域优化 [@yanyongyu](https://github.com/yanyongyu) ([#2665](https://github.com/nonebot/nonebot2/pull/2665))\n- Remove: 移除 Python 3.8 支持 [@yanyongyu](https://github.com/yanyongyu) ([#2641](https://github.com/nonebot/nonebot2/pull/2641))\n\n### 🚀 新功能\n\n- Feature: 嵌套插件名称作用域优化 [@yanyongyu](https://github.com/yanyongyu) ([#2665](https://github.com/nonebot/nonebot2/pull/2665))\n- Feature: 优化调用栈识别 [@yanyongyu](https://github.com/yanyongyu) ([#2644](https://github.com/nonebot/nonebot2/pull/2644))\n- Feature: 支持 HTTP 客户端会话 [@yanyongyu](https://github.com/yanyongyu) ([#2627](https://github.com/nonebot/nonebot2/pull/2627))\n- Develop: 添加 ruff RUF 规则 [@he0119](https://github.com/he0119) ([#2598](https://github.com/nonebot/nonebot2/pull/2598))\n\n### 🐛 Bug 修复\n\n- Fix: none 系列驱动器启动失败时未退出应用 [@yanyongyu](https://github.com/yanyongyu) ([#2687](https://github.com/nonebot/nonebot2/pull/2687))\n- Bug: inherit_supported_adapters 在展开缩写前取交集 [@AzideCupric](https://github.com/AzideCupric) ([#2654](https://github.com/nonebot/nonebot2/pull/2654))\n- Bug: 添加 HTTP 客户端会话上下文检查 [@yanyongyu](https://github.com/yanyongyu) ([#2632](https://github.com/nonebot/nonebot2/pull/2632))\n- Fix: 将 aiohttp 的 quote_fields 默认设为 False [@j1g5awi](https://github.com/j1g5awi) ([#2619](https://github.com/nonebot/nonebot2/pull/2619))\n\n### 📝 文档\n\n- Docs: 数据库最佳实践 [@ProgramRipper](https://github.com/ProgramRipper) ([#2545](https://github.com/nonebot/nonebot2/pull/2545))\n- Docs: 更新最佳实践的 Alconna 部分 [@RF-Tar-Railt](https://github.com/RF-Tar-Railt) ([#2686](https://github.com/nonebot/nonebot2/pull/2686))\n- Docs: 添加 OSPP 2024 项目说明 [@yanyongyu](https://github.com/yanyongyu) ([#2676](https://github.com/nonebot/nonebot2/pull/2676))\n- Docs: 更新 Villa 适配器说明 [@CMHopeSunshine](https://github.com/CMHopeSunshine) ([#2661](https://github.com/nonebot/nonebot2/pull/2661))\n- Docs: 添加 Kritor 适配器说明 [@RF-Tar-Railt](https://github.com/RF-Tar-Railt) ([#2660](https://github.com/nonebot/nonebot2/pull/2660))\n- Docs: 更新最佳实践的 Alconna 部分 [@RF-Tar-Railt](https://github.com/RF-Tar-Railt) ([#2656](https://github.com/nonebot/nonebot2/pull/2656))\n- Docs: 添加 RocketChat 适配器说明 [@yanyongyu](https://github.com/yanyongyu) ([#2640](https://github.com/nonebot/nonebot2/pull/2640))\n- Docs: 商店卡片样式调整 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#2633](https://github.com/nonebot/nonebot2/pull/2633))\n- Docs: 为商店插件卡片添加更多展示内容 [@AzideCupric](https://github.com/AzideCupric) ([#2626](https://github.com/nonebot/nonebot2/pull/2626))\n- Docs: 修复 `RegexMatched` 文档类型标注错误 [@A-kirami](https://github.com/A-kirami) ([#2629](https://github.com/nonebot/nonebot2/pull/2629))\n- Docs: 修复 `RegexMatched​` 文档高亮行错误 [@A-kirami](https://github.com/A-kirami) ([#2628](https://github.com/nonebot/nonebot2/pull/2628))\n- Docs: 为商店的详情卡片添加跳转链接 [@AzideCupric](https://github.com/AzideCupric) ([#2623](https://github.com/nonebot/nonebot2/pull/2623))\n- Docs: 添加 `RegexMatched` 依赖注入文档 [@A-kirami](https://github.com/A-kirami) ([#2618](https://github.com/nonebot/nonebot2/pull/2618))\n- Docs: 添加百度搜索资源验证 [@yanyongyu](https://github.com/yanyongyu) ([#2590](https://github.com/nonebot/nonebot2/pull/2590))\n\n### 💫 杂项\n\n- CI: 修复 NoneFlow reaction 范围 [@yanyongyu](https://github.com/yanyongyu) ([#2685](https://github.com/nonebot/nonebot2/pull/2685))\n- CI: 修复测试 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#2682](https://github.com/nonebot/nonebot2/pull/2682))\n- CI: NoneFlow 添加 reaction 响应提示 [@yanyongyu](https://github.com/yanyongyu) ([#2677](https://github.com/nonebot/nonebot2/pull/2677))\n- Plugin: 移除不维护的插件 `eitherchoice` [@lgc2333](https://github.com/lgc2333) ([#2599](https://github.com/nonebot/nonebot2/pull/2599))\n\n### 🍻 插件发布\n\n- Plugin: 表情包保存器 [@noneflow](https://github.com/noneflow) ([#2684](https://github.com/nonebot/nonebot2/pull/2684))\n- Plugin: HelpWithPic [@noneflow](https://github.com/noneflow) ([#2681](https://github.com/nonebot/nonebot2/pull/2681))\n- Plugin: cyberfurry [@noneflow](https://github.com/noneflow) ([#2679](https://github.com/nonebot/nonebot2/pull/2679))\n- Plugin: 三爻易数 [@noneflow](https://github.com/noneflow) ([#2675](https://github.com/nonebot/nonebot2/pull/2675))\n- Plugin: 战双表情 [@noneflow](https://github.com/noneflow) ([#2669](https://github.com/nonebot/nonebot2/pull/2669))\n- Plugin: QQ频道-Discord 互通 [@noneflow](https://github.com/noneflow) ([#2667](https://github.com/nonebot/nonebot2/pull/2667))\n- Plugin: Yinying-Chat [@noneflow](https://github.com/noneflow) ([#2662](https://github.com/nonebot/nonebot2/pull/2662))\n- Plugin: 淫语 [@noneflow](https://github.com/noneflow) ([#2650](https://github.com/nonebot/nonebot2/pull/2650))\n- Plugin: 飞花令 [@noneflow](https://github.com/noneflow) ([#2648](https://github.com/nonebot/nonebot2/pull/2648))\n- Plugin: Hx_YinYing [@noneflow](https://github.com/noneflow) ([#2646](https://github.com/nonebot/nonebot2/pull/2646))\n- Plugin: clovers插件框架 [@noneflow](https://github.com/noneflow) ([#2643](https://github.com/nonebot/nonebot2/pull/2643))\n- Plugin: nonebot-plugin-nai3 [@noneflow](https://github.com/noneflow) ([#2639](https://github.com/nonebot/nonebot2/pull/2639))\n- Plugin: nonebot-plugin-auto-bot-selector [@noneflow](https://github.com/noneflow) ([#2635](https://github.com/nonebot/nonebot2/pull/2635))\n- Plugin: Chikari_economy [@noneflow](https://github.com/noneflow) ([#2631](https://github.com/nonebot/nonebot2/pull/2631))\n- Plugin: diffsinger [@noneflow](https://github.com/noneflow) ([#2625](https://github.com/nonebot/nonebot2/pull/2625))\n- Plugin: ghtiles [@noneflow](https://github.com/noneflow) ([#2622](https://github.com/nonebot/nonebot2/pull/2622))\n- Plugin: 人类友好数据配置 [@noneflow](https://github.com/noneflow) ([#2616](https://github.com/nonebot/nonebot2/pull/2616))\n- Plugin: nonebot-plugin-pallas-repeater [@noneflow](https://github.com/noneflow) ([#2614](https://github.com/nonebot/nonebot2/pull/2614))\n- Plugin: nonebot-plugin-duel [@noneflow](https://github.com/noneflow) ([#2612](https://github.com/nonebot/nonebot2/pull/2612))\n- Plugin: Sekai Stickers [@noneflow](https://github.com/noneflow) ([#2610](https://github.com/nonebot/nonebot2/pull/2610))\n- Plugin: 100orangejuice [@noneflow](https://github.com/noneflow) ([#2601](https://github.com/nonebot/nonebot2/pull/2601))\n- Plugin: Steam Info [@noneflow](https://github.com/noneflow) ([#2608](https://github.com/nonebot/nonebot2/pull/2608))\n- Plugin: nonebot-plugin-dice-narrator [@noneflow](https://github.com/noneflow) ([#2606](https://github.com/nonebot/nonebot2/pull/2606))\n- Plugin: a2s查询 [@noneflow](https://github.com/noneflow) ([#2603](https://github.com/nonebot/nonebot2/pull/2603))\n- Plugin: 赛博钓鱼 [@noneflow](https://github.com/noneflow) ([#2596](https://github.com/nonebot/nonebot2/pull/2596))\n- Plugin: 人性化的ChatGLM [@noneflow](https://github.com/noneflow) ([#2592](https://github.com/nonebot/nonebot2/pull/2592))\n- Plugin: nonebot-plugin-vits-tts [@noneflow](https://github.com/noneflow) ([#2595](https://github.com/nonebot/nonebot2/pull/2595))\n\n### 🍻 适配器发布\n\n- Adapter: Kritor [@noneflow](https://github.com/noneflow) ([#2659](https://github.com/nonebot/nonebot2/pull/2659))\n- Adapter: RocketChat [@noneflow](https://github.com/noneflow) ([#2637](https://github.com/nonebot/nonebot2/pull/2637))\n\n## v2.2.1\n\n### 🚀 新功能\n\n- Feature: 优化 pydantic 兼容函数 `model_dump` 和 `type_validate_json` [@MingxuanGame](https://github.com/MingxuanGame) ([#2579](https://github.com/nonebot/nonebot2/pull/2579))\n\n### 🐛 Bug 修复\n\n- Fix: 修改遗漏的过时 Pydantic 方法 [@yanyongyu](https://github.com/yanyongyu) ([#2577](https://github.com/nonebot/nonebot2/pull/2577))\n- Fix: `Message.__contains__()` 未考虑 `bool(MessageSegment)` 存在 False 情况导致的异常结果 [@lgc2333](https://github.com/lgc2333) ([#2572](https://github.com/nonebot/nonebot2/pull/2572))\n\n### 📝 文档\n\n- Docs: 更新 Session Expire Timeout​ 文档 [@MingxuanGame](https://github.com/MingxuanGame) ([#2585](https://github.com/nonebot/nonebot2/pull/2585))\n- Docs: 添加适配器测试注意事项 [@yanyongyu](https://github.com/yanyongyu) ([#2570](https://github.com/nonebot/nonebot2/pull/2570))\n\n### 💫 杂项\n\n- Plugin: 修改 phigros 相关内容 [@XTxiaoting14332](https://github.com/XTxiaoting14332) ([#2578](https://github.com/nonebot/nonebot2/pull/2578))\n\n### 🍻 插件发布\n\n- Plugin: 运行状态 [@noneflow](https://github.com/noneflow) ([#2587](https://github.com/nonebot/nonebot2/pull/2587))\n- Plugin: nonebot-plugin-bf1marneserverlist [@noneflow](https://github.com/noneflow) ([#2584](https://github.com/nonebot/nonebot2/pull/2584))\n- Plugin: splatoon3游戏nso查询 [@noneflow](https://github.com/noneflow) ([#2576](https://github.com/nonebot/nonebot2/pull/2576))\n- Plugin: Chikari_yinpa [@noneflow](https://github.com/noneflow) ([#2573](https://github.com/nonebot/nonebot2/pull/2573))\n\n## v2.2.0\n\n### 🚀 新功能\n\n- Feature: 添加插件 Pydantic 相关使用方法 [@yanyongyu](https://github.com/yanyongyu) ([#2563](https://github.com/nonebot/nonebot2/pull/2563))\n- Feature: 兼容 Pydantic v2 [@yanyongyu](https://github.com/yanyongyu) ([#2544](https://github.com/nonebot/nonebot2/pull/2544))\n- Feature: 使用自定义配置加载替代 `pydantic-settings` [@yanyongyu](https://github.com/yanyongyu) ([#2521](https://github.com/nonebot/nonebot2/pull/2521))\n- Feature: 带参数的 `RegexStr()` [@ProgramRipper](https://github.com/ProgramRipper) ([#2499](https://github.com/nonebot/nonebot2/pull/2499))\n\n### 🐛 Bug 修复\n\n- Fix: websockets 驱动器连接关闭 code 获取错误 [@yanyongyu](https://github.com/yanyongyu) ([#2537](https://github.com/nonebot/nonebot2/pull/2537))\n- Fix: 修复 `echo` 发送空消息 [@yanyongyu](https://github.com/yanyongyu) ([#2525](https://github.com/nonebot/nonebot2/pull/2525))\n- Fix: `MessageTemplate` 禁止访问私有属性 [@mnixry](https://github.com/mnixry) ([#2509](https://github.com/nonebot/nonebot2/pull/2509))\n\n### 📝 文档\n\n- Docs: 更新 Alconna 文档 [@lengmianzz](https://github.com/lengmianzz) ([#2568](https://github.com/nonebot/nonebot2/pull/2568))\n- Docs: 添加产品赞助列表 [@yanyongyu](https://github.com/yanyongyu) ([#2566](https://github.com/nonebot/nonebot2/pull/2566))\n- Docs: 修复表单标签状态更新 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#2558](https://github.com/nonebot/nonebot2/pull/2558))\n- Docs: 添加 CITATION 文件 [@yanyongyu](https://github.com/yanyongyu) ([#2520](https://github.com/nonebot/nonebot2/pull/2520))\n\n### 💫 杂项\n\n- Plugin: 移除不再维护的几款插件 [@mnixry](https://github.com/mnixry) ([#2561](https://github.com/nonebot/nonebot2/pull/2561))\n- CI: 更新 prettier 配置 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#2546](https://github.com/nonebot/nonebot2/pull/2546))\n- Plugin: 恢复删除的插件 `nonebot-plugin-eitherchoice` [@lgc2333](https://github.com/lgc2333) ([#2502](https://github.com/nonebot/nonebot2/pull/2502))\n\n### 🍻 插件发布\n\n- Plugin: 定时提醒 [@noneflow](https://github.com/noneflow) ([#2559](https://github.com/nonebot/nonebot2/pull/2559))\n- Plugin: 黑名单插件 [@noneflow](https://github.com/noneflow) ([#2554](https://github.com/nonebot/nonebot2/pull/2554))\n- Plugin: ChatGPT 聊天 [@noneflow](https://github.com/noneflow) ([#2556](https://github.com/nonebot/nonebot2/pull/2556))\n- Plugin: BA模拟抽卡 [@noneflow](https://github.com/noneflow) ([#2550](https://github.com/nonebot/nonebot2/pull/2550))\n- Plugin: 随机发送图片 [@noneflow](https://github.com/noneflow) ([#2548](https://github.com/nonebot/nonebot2/pull/2548))\n- Plugin: 哪吒监控插件 [@noneflow](https://github.com/noneflow) ([#2552](https://github.com/nonebot/nonebot2/pull/2552))\n- Plugin: SakuraFrp [@noneflow](https://github.com/noneflow) ([#2543](https://github.com/nonebot/nonebot2/pull/2543))\n- Plugin: haruka_bot_red [@noneflow](https://github.com/noneflow) ([#2541](https://github.com/nonebot/nonebot2/pull/2541))\n- Plugin: nonebot-plugin-gemini [@noneflow](https://github.com/noneflow) ([#2527](https://github.com/nonebot/nonebot2/pull/2527))\n- Plugin: 最终台词 [@noneflow](https://github.com/noneflow) ([#2523](https://github.com/nonebot/nonebot2/pull/2523))\n- Plugin: nonebot-plugin-nekoimage [@noneflow](https://github.com/noneflow) ([#2534](https://github.com/nonebot/nonebot2/pull/2534))\n- Plugin: 谷歌Bard聊天 [@noneflow](https://github.com/noneflow) ([#2529](https://github.com/nonebot/nonebot2/pull/2529))\n- Plugin: nonebot-plugin-mypower [@noneflow](https://github.com/noneflow) ([#2533](https://github.com/nonebot/nonebot2/pull/2533))\n- Plugin: 文心一言4适配 [@noneflow](https://github.com/noneflow) ([#2516](https://github.com/nonebot/nonebot2/pull/2516))\n- Plugin: 最佳平替 [@noneflow](https://github.com/noneflow) ([#2519](https://github.com/nonebot/nonebot2/pull/2519))\n- Plugin: 随机MC图 [@noneflow](https://github.com/noneflow) ([#2512](https://github.com/nonebot/nonebot2/pull/2512))\n- Plugin: nonebot_plugin_nikke [@noneflow](https://github.com/noneflow) ([#2508](https://github.com/nonebot/nonebot2/pull/2508))\n- Plugin: nonebot-plugin-imagemaster [@noneflow](https://github.com/noneflow) ([#2504](https://github.com/nonebot/nonebot2/pull/2504))\n- Plugin: Waiter 插件 [@noneflow](https://github.com/noneflow) ([#2506](https://github.com/nonebot/nonebot2/pull/2506))\n- Plugin: AntiMonkey [@noneflow](https://github.com/noneflow) ([#2501](https://github.com/nonebot/nonebot2/pull/2501))\n\n## v2.1.3\n\n### 🐛 Bug 修复\n\n- Fix: 新增 `Lifespan.on_ready()` 供适配器使用 [@ProgramRipper](https://github.com/ProgramRipper) ([#2483](https://github.com/nonebot/nonebot2/pull/2483))\n- Fix: 忽略 Pyright 对动态类创建的检查错误 [@yanyongyu](https://github.com/yanyongyu) ([#2486](https://github.com/nonebot/nonebot2/pull/2486))\n\n### 📝 文档\n\n- Docs: 商店详情卡片添加宽度限制与文本省略 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#2473](https://github.com/nonebot/nonebot2/pull/2473))\n- Docs: 修复商店发布 上一步 按钮显示问题 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#2464](https://github.com/nonebot/nonebot2/pull/2464))\n- Docs: 添加商店表单支持 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#2460](https://github.com/nonebot/nonebot2/pull/2460))\n- Docs: 修复事件后处理函数类型 docstring 错误 [@lgc2333](https://github.com/lgc2333) ([#2459](https://github.com/nonebot/nonebot2/pull/2459))\n- Docs: 修改 QQ 频道为 QQ [@bingqiu456](https://github.com/bingqiu456) ([#2457](https://github.com/nonebot/nonebot2/pull/2457))\n- Docs: 更新最佳实践的 Alconna 部分 [@RF-Tar-Railt](https://github.com/RF-Tar-Railt) ([#2443](https://github.com/nonebot/nonebot2/pull/2443))\n\n### 💫 杂项\n\n- Plugin: 更新 splatoon3 插件地址 [@Cypas](https://github.com/Cypas) ([#2494](https://github.com/nonebot/nonebot2/pull/2494))\n- Plugin: 删除不维护的 `eitherchoice` 插件 [@lgc2333](https://github.com/lgc2333) ([#2491](https://github.com/nonebot/nonebot2/pull/2491))\n- Plugin: 移除不再维护的插件 [@j1g5awi](https://github.com/j1g5awi) ([#2474](https://github.com/nonebot/nonebot2/pull/2474))\n- Plugin: 移除不再维护的插件 [@NCBM](https://github.com/NCBM) ([#2472](https://github.com/nonebot/nonebot2/pull/2472))\n- Plugin: 移除不再维护的插件 [@MeetWq](https://github.com/MeetWq) ([#2471](https://github.com/nonebot/nonebot2/pull/2471))\n- CI: 测试矩阵添加 Python 3.12 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#2441](https://github.com/nonebot/nonebot2/pull/2441))\n\n### 🍻 插件发布\n\n- Plugin: Phigros查分器(Adapter-qq) [@noneflow](https://github.com/noneflow) ([#2497](https://github.com/nonebot/nonebot2/pull/2497))\n- Plugin: Riffusion [@noneflow](https://github.com/noneflow) ([#2493](https://github.com/nonebot/nonebot2/pull/2493))\n- Plugin: nonebot_plugin_longtu [@noneflow](https://github.com/noneflow) ([#2490](https://github.com/nonebot/nonebot2/pull/2490))\n- Plugin: CNRail [@noneflow](https://github.com/noneflow) ([#2488](https://github.com/nonebot/nonebot2/pull/2488))\n- Plugin: ba塔罗牌，运势与魔法占卜！ [@noneflow](https://github.com/noneflow) ([#2481](https://github.com/nonebot/nonebot2/pull/2481))\n- Plugin: 群聊 NSFW 图片检测 [@noneflow](https://github.com/noneflow) ([#2477](https://github.com/nonebot/nonebot2/pull/2477))\n- Plugin: sm.ms图床 [@noneflow](https://github.com/noneflow) ([#2470](https://github.com/nonebot/nonebot2/pull/2470))\n- Plugin: 文件托管支持 [@noneflow](https://github.com/noneflow) ([#2468](https://github.com/nonebot/nonebot2/pull/2468))\n- Plugin: 短链接服务支持 [@noneflow](https://github.com/noneflow) ([#2466](https://github.com/nonebot/nonebot2/pull/2466))\n- Plugin: 用户 [@noneflow](https://github.com/noneflow) ([#2463](https://github.com/nonebot/nonebot2/pull/2463))\n- Plugin: DALL-E 3绘图 [@noneflow](https://github.com/noneflow) ([#2452](https://github.com/nonebot/nonebot2/pull/2452))\n- Plugin: 局域网唤醒 [@noneflow](https://github.com/noneflow) ([#2449](https://github.com/nonebot/nonebot2/pull/2449))\n- Plugin: nonebot-plugin-bertvits2 [@noneflow](https://github.com/noneflow) ([#2446](https://github.com/nonebot/nonebot2/pull/2446))\n- Plugin: Nonebot2 Any 多平台服务 [@noneflow](https://github.com/noneflow) ([#2442](https://github.com/nonebot/nonebot2/pull/2442))\n\n### 🍻 机器人发布\n\n- Bot: Sakiko [@noneflow](https://github.com/noneflow) ([#2439](https://github.com/nonebot/nonebot2/pull/2439))\n\n### 🍻 适配器发布\n\n- Adapter: DoDo [@noneflow](https://github.com/noneflow) ([#2456](https://github.com/nonebot/nonebot2/pull/2456))\n\n## v2.1.2\n\n### 🚀 新功能\n\n- Feature: 添加多消息段命令解析支持 [@RainEggplant](https://github.com/RainEggplant) ([#2419](https://github.com/nonebot/nonebot2/pull/2419))\n\n### 🐛 Bug 修复\n\n- Fix: 修复依赖注入对 Literal 检查报错 [@yanyongyu](https://github.com/yanyongyu) ([#2433](https://github.com/nonebot/nonebot2/pull/2433))\n\n### 📝 文档\n\n- Docs: 修复 Alconna 文档 typo [@StarHeartHunt](https://github.com/StarHeartHunt) ([#2429](https://github.com/nonebot/nonebot2/pull/2429))\n- Docs: 文档启用百度统计 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#2424](https://github.com/nonebot/nonebot2/pull/2424))\n- Docs: 更新最佳实践 Alconna [@RF-Tar-Railt](https://github.com/RF-Tar-Railt) ([#2401](https://github.com/nonebot/nonebot2/pull/2401))\n- Docs: 修改商店发布的跳转链接 [@KomoriDev](https://github.com/KomoriDev) ([#2387](https://github.com/nonebot/nonebot2/pull/2387))\n- Docs: 修复文档主页 Features 不居中 [@MingxuanGame](https://github.com/MingxuanGame) ([#2390](https://github.com/nonebot/nonebot2/pull/2390))\n\n### 💫 杂项\n\n- Fix: 修复升级 pytest-asyncio 0.22 pytest collect 问题 [@yanyongyu](https://github.com/yanyongyu) ([#2436](https://github.com/nonebot/nonebot2/pull/2436))\n- Plugin: 移除 `nonebot-plugin-nya-music` 插件 [@nikissXI](https://github.com/nikissXI) ([#2398](https://github.com/nonebot/nonebot2/pull/2398))\n- CI: 调整商店数据存放位置与内容 [@he0119](https://github.com/he0119) ([#2385](https://github.com/nonebot/nonebot2/pull/2385))\n- Adapter: 修改频道适配器为 QQ 适配器 [@yanyongyu](https://github.com/yanyongyu) ([#2382](https://github.com/nonebot/nonebot2/pull/2382))\n\n### 🍻 插件发布\n\n- Plugin: 定时广播插件 [@noneflow](https://github.com/noneflow) ([#2432](https://github.com/nonebot/nonebot2/pull/2432))\n- Plugin: 选择困难症 [@noneflow](https://github.com/noneflow) ([#2428](https://github.com/nonebot/nonebot2/pull/2428))\n- Plugin: nonebot-plugin-getbapics [@noneflow](https://github.com/noneflow) ([#2423](https://github.com/nonebot/nonebot2/pull/2423))\n- Plugin: nonebot-plugin-maimaidx [@noneflow](https://github.com/noneflow) ([#2422](https://github.com/nonebot/nonebot2/pull/2422))\n- Plugin: BlueArchive Title Generator [@noneflow](https://github.com/noneflow) ([#2418](https://github.com/nonebot/nonebot2/pull/2418))\n- Plugin: VRChat查询 [@noneflow](https://github.com/noneflow) ([#2411](https://github.com/nonebot/nonebot2/pull/2411))\n- Plugin: FGO猜从者 [@noneflow](https://github.com/noneflow) ([#2416](https://github.com/nonebot/nonebot2/pull/2416))\n- Plugin: 肯定机 [@noneflow](https://github.com/noneflow) ([#2409](https://github.com/nonebot/nonebot2/pull/2409))\n- Plugin: morep-finder [@noneflow](https://github.com/noneflow) ([#2407](https://github.com/nonebot/nonebot2/pull/2407))\n- Plugin: op-finder [@noneflow](https://github.com/noneflow) ([#2403](https://github.com/nonebot/nonebot2/pull/2403))\n- Plugin: nonebot-plugin-playercheck [@noneflow](https://github.com/noneflow) ([#2400](https://github.com/nonebot/nonebot2/pull/2400))\n- Plugin: talk with eop ai [@noneflow](https://github.com/noneflow) ([#2397](https://github.com/nonebot/nonebot2/pull/2397))\n- Plugin: 算法比赛查询和今日比赛自动提醒 [@noneflow](https://github.com/noneflow) ([#2395](https://github.com/nonebot/nonebot2/pull/2395))\n- Plugin: 屏蔽词插件 [@noneflow](https://github.com/noneflow) ([#2392](https://github.com/nonebot/nonebot2/pull/2392))\n- Plugin: Nonebot Agent [@noneflow](https://github.com/noneflow) ([#2389](https://github.com/nonebot/nonebot2/pull/2389))\n- Plugin: 聚能环 [@noneflow](https://github.com/noneflow) ([#2384](https://github.com/nonebot/nonebot2/pull/2384))\n\n### 🍻 机器人发布\n\n- Bot: 芙芙 [@noneflow](https://github.com/noneflow) ([#2426](https://github.com/nonebot/nonebot2/pull/2426))\n- Bot: 妃爱 [@noneflow](https://github.com/noneflow) ([#2413](https://github.com/nonebot/nonebot2/pull/2413))\n\n### 🍻 适配器发布\n\n- Adapter: Satori [@noneflow](https://github.com/noneflow) ([#2405](https://github.com/nonebot/nonebot2/pull/2405))\n\n## v2.1.1\n\n### 🚀 新功能\n\n- Feature: 优先使用 `Annotated` 的最后一个子依赖 [@ProgramRipper](https://github.com/ProgramRipper) ([#2360](https://github.com/nonebot/nonebot2/pull/2360))\n- Feature: 优化检查事件响应器的日志 [@A-kirami](https://github.com/A-kirami) ([#2355](https://github.com/nonebot/nonebot2/pull/2355))\n\n### 🐛 Bug 修复\n\n- Fix: bot.call_api 在被 called api hook mock 后应该忽略 exception [@Ailitonia](https://github.com/Ailitonia) ([#2374](https://github.com/nonebot/nonebot2/pull/2374))\n\n### 📝 文档\n\n- Docs: 修复商店搜索信息的错字 [@KomoriDev](https://github.com/KomoriDev) ([#2377](https://github.com/nonebot/nonebot2/pull/2377))\n- Docs: 修复侧边栏 TOC 在 SSR 模式下的渲染问题 [@yanyongyu](https://github.com/yanyongyu) ([#2376](https://github.com/nonebot/nonebot2/pull/2376))\n- Docs: 升级新版 NonePress 主题 [@yanyongyu](https://github.com/yanyongyu) ([#2375](https://github.com/nonebot/nonebot2/pull/2375))\n- Docs: 增加赞助者显示 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#2371](https://github.com/nonebot/nonebot2/pull/2371))\n- Docs: 更新 `get_asgi` 函数的文档字符串 [@A-kirami](https://github.com/A-kirami) ([#2359](https://github.com/nonebot/nonebot2/pull/2359))\n\n### 💫 杂项\n\n- Develop: 禁用 Pyright Bytes Promotion 配置 [@yanyongyu](https://github.com/yanyongyu) ([#2379](https://github.com/nonebot/nonebot2/pull/2379))\n- Plugin: 修改 `Sekai Stickers` 插件信息 [@lgc2333](https://github.com/lgc2333) ([#2372](https://github.com/nonebot/nonebot2/pull/2372))\n- CI: 使用更现代的功能 [@he0119](https://github.com/he0119) ([#2362](https://github.com/nonebot/nonebot2/pull/2362))\n- Docs: 添加 wwads [@yanyongyu](https://github.com/yanyongyu) ([#2361](https://github.com/nonebot/nonebot2/pull/2361))\n\n### 🍻 插件发布\n\n- Plugin: 大电老师活字印刷 [@noneflow](https://github.com/noneflow) ([#2370](https://github.com/nonebot/nonebot2/pull/2370))\n- Plugin: nonebot-plugin-video-api [@noneflow](https://github.com/noneflow) ([#2367](https://github.com/nonebot/nonebot2/pull/2367))\n- Plugin: 青年大学习提交 [@noneflow](https://github.com/noneflow) ([#2357](https://github.com/nonebot/nonebot2/pull/2357))\n\n## v2.1.0\n\n### 🚀 新功能\n\n- Feature: 为 Matcher.HANDLER_PARAM_TYPES 补增类型 [@RF-Tar-Railt](https://github.com/RF-Tar-Railt) ([#2352](https://github.com/nonebot/nonebot2/pull/2352))\n- Feature: 为事件响应器添加更多源码信息 [@yanyongyu](https://github.com/yanyongyu) ([#2351](https://github.com/nonebot/nonebot2/pull/2351))\n- Feature: 补充依赖注入部分情况下类型错误时的日志提示 [@A-kirami](https://github.com/A-kirami) ([#2343](https://github.com/nonebot/nonebot2/pull/2343))\n- Feature: 支持子依赖定义 Pydantic 类型校验 [@yanyongyu](https://github.com/yanyongyu) ([#2310](https://github.com/nonebot/nonebot2/pull/2310))\n- Feature: 细化 driver 职责类型 [@yanyongyu](https://github.com/yanyongyu) ([#2296](https://github.com/nonebot/nonebot2/pull/2296))\n\n### 🐛 Bug 修复\n\n- Fix: 修复依赖注入解析类型标注错误 [@yanyongyu](https://github.com/yanyongyu) ([#2338](https://github.com/nonebot/nonebot2/pull/2338))\n- Fix: 设置 file request 默认 filename [@eya46](https://github.com/eya46) ([#2284](https://github.com/nonebot/nonebot2/pull/2284))\n\n### 📝 文档\n\n- Docs: 更新最佳实践部分的 Alconna 章节 [@RF-Tar-Railt](https://github.com/RF-Tar-Railt) ([#2349](https://github.com/nonebot/nonebot2/pull/2349))\n- Docs: 添加 Discord 适配器描述，补充 Villa 适配器协议链接 [@CMHopeSunshine](https://github.com/CMHopeSunshine) ([#2316](https://github.com/nonebot/nonebot2/pull/2316))\n- Docs: 添加 Red 适配器描述 [@RF-Tar-Railt](https://github.com/RF-Tar-Railt) ([#2313](https://github.com/nonebot/nonebot2/pull/2313))\n- Docs: 更新最佳实践部分的 Alconna 章节 [@RF-Tar-Railt](https://github.com/RF-Tar-Railt) ([#2303](https://github.com/nonebot/nonebot2/pull/2303))\n- Docs: 修复 Alconna 中 `CommandResult` 描述错误 [@KomoriDev](https://github.com/KomoriDev) ([#2282](https://github.com/nonebot/nonebot2/pull/2282))\n- Docs: 修复子依赖部分代码行号错误 [@A-kirami](https://github.com/A-kirami) ([#2279](https://github.com/nonebot/nonebot2/pull/2279))\n- Docs: 补充 `get_last_receive` 示例 [@A-kirami](https://github.com/A-kirami) ([#2278](https://github.com/nonebot/nonebot2/pull/2278))\n- Docs: 修复文档中错误的标点 [@A-kirami](https://github.com/A-kirami) ([#2275](https://github.com/nonebot/nonebot2/pull/2275))\n- Docs: 修复配置文档中 `Nickname` 属性的描述错误 [@A-kirami](https://github.com/A-kirami) ([#2271](https://github.com/nonebot/nonebot2/pull/2271))\n- Docs: 适配器编写教程 [@CMHopeSunshine](https://github.com/CMHopeSunshine) ([#2079](https://github.com/nonebot/nonebot2/pull/2079))\n- Docs: 更新贡献指南 [@A-kirami](https://github.com/A-kirami) ([#2255](https://github.com/nonebot/nonebot2/pull/2255))\n- Docs: 修复文档 Last updated author 错误 [@eya46](https://github.com/eya46) ([#2241](https://github.com/nonebot/nonebot2/pull/2241))\n- Docs: 更新最佳实践部分的 Alconna 章节 [@RF-Tar-Railt](https://github.com/RF-Tar-Railt) ([#2237](https://github.com/nonebot/nonebot2/pull/2237))\n\n### 💫 杂项\n\n- Plugin: 删除插件 nonebot-plugin-heisi [@yzyyz1387](https://github.com/yzyyz1387) ([#2353](https://github.com/nonebot/nonebot2/pull/2353))\n- CI: 更新到 node 18 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#2344](https://github.com/nonebot/nonebot2/pull/2344))\n- CI: 插件测试使用最新的稳定版 Python 版本 [@he0119](https://github.com/he0119) ([#2336](https://github.com/nonebot/nonebot2/pull/2336))\n- Plugin: 删除不再维护的插件 [@ZM25XC](https://github.com/ZM25XC) ([#2330](https://github.com/nonebot/nonebot2/pull/2330))\n- Plugin: 删除插件 poe ai [@nikissXI](https://github.com/nikissXI) ([#2308](https://github.com/nonebot/nonebot2/pull/2308))\n- Plugin: 移除不再维护的插件，修改插件信息 [@Well2333](https://github.com/Well2333) ([#2292](https://github.com/nonebot/nonebot2/pull/2292))\n- Fix: 修复 ruff 发现的问题 [@yanyongyu](https://github.com/yanyongyu) ([#2286](https://github.com/nonebot/nonebot2/pull/2286))\n- Develop: 添加 dependabot actions 更新检查 [@yanyongyu](https://github.com/yanyongyu) ([#2256](https://github.com/nonebot/nonebot2/pull/2256))\n- Develop: 添加 git attributes 定义 [@yanyongyu](https://github.com/yanyongyu) ([#2210](https://github.com/nonebot/nonebot2/pull/2210))\n\n### 🍻 插件发布\n\n- Plugin: 文心一言 [@noneflow](https://github.com/noneflow) ([#2342](https://github.com/nonebot/nonebot2/pull/2342))\n- Plugin: nonebot_plugin_group_whitelist [@noneflow](https://github.com/noneflow) ([#2320](https://github.com/nonebot/nonebot2/pull/2320))\n- Plugin: 森空岛明日方舟签到器 [@noneflow](https://github.com/noneflow) ([#2340](https://github.com/nonebot/nonebot2/pull/2340))\n- Plugin: 女装 ! [@noneflow](https://github.com/noneflow) ([#2337](https://github.com/nonebot/nonebot2/pull/2337))\n- Plugin: helper_plus [@noneflow](https://github.com/noneflow) ([#2324](https://github.com/nonebot/nonebot2/pull/2324))\n- Plugin: nonebot-plugin-souti [@noneflow](https://github.com/noneflow) ([#2334](https://github.com/nonebot/nonebot2/pull/2334))\n- Plugin: Alconna 帮助工具 [@noneflow](https://github.com/noneflow) ([#2326](https://github.com/nonebot/nonebot2/pull/2326))\n- Plugin: 消息伪造 [@noneflow](https://github.com/noneflow) ([#2312](https://github.com/nonebot/nonebot2/pull/2312))\n- Plugin: 二维码 [@noneflow](https://github.com/noneflow) ([#2302](https://github.com/nonebot/nonebot2/pull/2302))\n- Plugin: httpcat-状态猫 😺 [@noneflow](https://github.com/noneflow) ([#2306](https://github.com/nonebot/nonebot2/pull/2306))\n- Plugin: 雪豹闭嘴 [@noneflow](https://github.com/noneflow) ([#2300](https://github.com/nonebot/nonebot2/pull/2300))\n- Plugin: Nonebot Requests [@noneflow](https://github.com/noneflow) ([#2294](https://github.com/nonebot/nonebot2/pull/2294))\n- Plugin: 双向聊天插件 [@noneflow](https://github.com/noneflow) ([#2291](https://github.com/nonebot/nonebot2/pull/2291))\n- Plugin: 识别动漫 gal 角色 [@noneflow](https://github.com/noneflow) ([#2288](https://github.com/nonebot/nonebot2/pull/2288))\n- Plugin: arxiv 订阅 [@noneflow](https://github.com/noneflow) ([#2285](https://github.com/nonebot/nonebot2/pull/2285))\n- Plugin: SUDO [@noneflow](https://github.com/noneflow) ([#2277](https://github.com/nonebot/nonebot2/pull/2277))\n- Plugin: 消息推送插件 [@noneflow](https://github.com/noneflow) ([#2273](https://github.com/nonebot/nonebot2/pull/2273))\n- Plugin: 周易蓍草占卜 [@noneflow](https://github.com/noneflow) ([#2268](https://github.com/nonebot/nonebot2/pull/2268))\n- Plugin: 欧若可骰娘 [@noneflow](https://github.com/noneflow) ([#2266](https://github.com/nonebot/nonebot2/pull/2266))\n- Plugin: 科大讯飞星火大模型聊天 [@noneflow](https://github.com/noneflow) ([#2258](https://github.com/nonebot/nonebot2/pull/2258))\n- Plugin: 剑网三查询和推送 [@noneflow](https://github.com/noneflow) ([#2254](https://github.com/nonebot/nonebot2/pull/2254))\n- Plugin: Muteme(我禁我自己) [@noneflow](https://github.com/noneflow) ([#2252](https://github.com/nonebot/nonebot2/pull/2252))\n- Plugin: MC 版本更新检测 [@noneflow](https://github.com/noneflow) ([#2247](https://github.com/nonebot/nonebot2/pull/2247))\n- Plugin: KanonBot [@noneflow](https://github.com/noneflow) ([#2244](https://github.com/nonebot/nonebot2/pull/2244))\n- Plugin: CSGO 饰品查询机器人 [@noneflow](https://github.com/noneflow) ([#2225](https://github.com/nonebot/nonebot2/pull/2225))\n- Plugin: talk with poe ai [@noneflow](https://github.com/noneflow) ([#2230](https://github.com/nonebot/nonebot2/pull/2230))\n- Plugin: 命运方舟流浪商人卡牌刷新提示 [@noneflow](https://github.com/noneflow) ([#2234](https://github.com/nonebot/nonebot2/pull/2234))\n- Plugin: Savepic [@noneflow](https://github.com/noneflow) ([#2232](https://github.com/nonebot/nonebot2/pull/2232))\n- Plugin: 跨平台账户绑定 [@noneflow](https://github.com/noneflow) ([#2227](https://github.com/nonebot/nonebot2/pull/2227))\n- Plugin: Among US 中的 TOH 模组职业介绍 [@noneflow](https://github.com/noneflow) ([#2221](https://github.com/nonebot/nonebot2/pull/2221))\n- Plugin: NoneMeme [@noneflow](https://github.com/noneflow) ([#2219](https://github.com/nonebot/nonebot2/pull/2219))\n- Plugin: The World [@noneflow](https://github.com/noneflow) ([#2216](https://github.com/nonebot/nonebot2/pull/2216))\n- Plugin: Bot 上下线邮件通知 [@noneflow](https://github.com/noneflow) ([#2214](https://github.com/nonebot/nonebot2/pull/2214))\n- Plugin: bot 断连通知 [@noneflow](https://github.com/noneflow) ([#2212](https://github.com/nonebot/nonebot2/pull/2212))\n\n### 🍻 机器人发布\n\n- Bot: OCNbot [@noneflow](https://github.com/noneflow) ([#2261](https://github.com/nonebot/nonebot2/pull/2261))\n- Bot: 星见 Kirami [@noneflow](https://github.com/noneflow) ([#2263](https://github.com/nonebot/nonebot2/pull/2263))\n- Bot: 不正经的妹妹 [@noneflow](https://github.com/noneflow) ([#2249](https://github.com/nonebot/nonebot2/pull/2249))\n\n### 🍻 适配器发布\n\n- Adapter: Discord [@noneflow](https://github.com/noneflow) ([#2315](https://github.com/nonebot/nonebot2/pull/2315))\n- Adapter: RedProtocol [@noneflow](https://github.com/noneflow) ([#2239](https://github.com/nonebot/nonebot2/pull/2239))\n\n## v2.0.1\n\n### 🚀 新功能\n\n- Develop: 添加 Pyright 检查 [@yanyongyu](https://github.com/yanyongyu) ([#2194](https://github.com/nonebot/nonebot2/pull/2194))\n- Feature: 使用 `typing.override` 标记 [@yanyongyu](https://github.com/yanyongyu) ([#2193](https://github.com/nonebot/nonebot2/pull/2193))\n- Feature: 补充响应器组属性 [@eya46](https://github.com/eya46) ([#2154](https://github.com/nonebot/nonebot2/pull/2154))\n- Feature: CommandGroup 支持命令别名添加前缀选项 [@eya46](https://github.com/eya46) ([#2134](https://github.com/nonebot/nonebot2/pull/2134))\n- Feature: 添加用于动态继承支持适配器数据的方法 [@NCBM](https://github.com/NCBM) ([#2127](https://github.com/nonebot/nonebot2/pull/2127))\n- Feature: 添加内置插件的插件元数据 [@yanyongyu](https://github.com/yanyongyu) ([#2113](https://github.com/nonebot/nonebot2/pull/2113))\n- Feature: 插件商店适配最新的插件元数据 [@he0119](https://github.com/he0119) ([#2094](https://github.com/nonebot/nonebot2/pull/2094))\n- Feature: 依赖注入支持 Generic TypeVar 和 Matcher 重载 [@yanyongyu](https://github.com/yanyongyu) ([#2089](https://github.com/nonebot/nonebot2/pull/2089))\n\n### 🐛 Bug 修复\n\n- Fix: 修复 Quart WS task 上下文错误 [@yanyongyu](https://github.com/yanyongyu) ([#2192](https://github.com/nonebot/nonebot2/pull/2192))\n- Fix: 修复 dotenv 配置项为 None 将会跳过赋值 [@eya46](https://github.com/eya46) ([#2143](https://github.com/nonebot/nonebot2/pull/2143))\n- Fix: 修复 `ArgParam` 不支持 `Annotated` [@eya46](https://github.com/eya46) ([#2124](https://github.com/nonebot/nonebot2/pull/2124))\n- Fix: aiohttp 请求时 data 和 file 不能同时存在 [@j1g5awi](https://github.com/j1g5awi) ([#2088](https://github.com/nonebot/nonebot2/pull/2088))\n- Fix: 修复因 loguru 更新导致的启动和关闭日志 name 不正常 [@DiheChen](https://github.com/DiheChen) ([#2080](https://github.com/nonebot/nonebot2/pull/2080))\n\n### 📝 文档\n\n- Docs: 移动 Alconna 文档至最佳实践 [@RF-Tar-Railt](https://github.com/RF-Tar-Railt) ([#2208](https://github.com/nonebot/nonebot2/pull/2208))\n- Docs: 移除商店中不符合现规范的 tag [@j1g5awi](https://github.com/j1g5awi) ([#2205](https://github.com/nonebot/nonebot2/pull/2205))\n- Docs: 添加 scoped 插件配置指南 [@yanyongyu](https://github.com/yanyongyu) ([#2198](https://github.com/nonebot/nonebot2/pull/2198))\n- Docs: 钩子函数代码片段补充 [@A-kirami](https://github.com/A-kirami) ([#2173](https://github.com/nonebot/nonebot2/pull/2173))\n- Docs: 格式化钩子函数中的代码片段 [@A-kirami](https://github.com/A-kirami) ([#2172](https://github.com/nonebot/nonebot2/pull/2172))\n- Docs: 补充 Message.only 文档 [@eya46](https://github.com/eya46) ([#2155](https://github.com/nonebot/nonebot2/pull/2155))\n- Docs: 修复日志自定义文档 typo [@17TheWord](https://github.com/17TheWord) ([#2140](https://github.com/nonebot/nonebot2/pull/2140))\n- Docs: 修复依赖注入文档 `ArgStr` 3.9+ 和 3.8+ 版本代码写反 [@eya46](https://github.com/eya46) ([#2126](https://github.com/nonebot/nonebot2/pull/2126))\n- Docs: 删除商店插件发布多余模块 [@forchannot](https://github.com/forchannot) ([#2095](https://github.com/nonebot/nonebot2/pull/2095))\n- Docs: 微调插件元数据的部分描述 [@NCBM](https://github.com/NCBM) ([#2096](https://github.com/nonebot/nonebot2/pull/2096))\n- Docs: 完成发布插件教程 [@NCBM](https://github.com/NCBM) ([#2078](https://github.com/nonebot/nonebot2/pull/2078))\n- Docs: 更新插件元数据的相关描述 [@NCBM](https://github.com/NCBM) ([#2087](https://github.com/nonebot/nonebot2/pull/2087))\n- Docs: 添加 Villa 适配器到 README [@CMHopeSunshine](https://github.com/CMHopeSunshine) ([#2086](https://github.com/nonebot/nonebot2/pull/2086))\n\n### 💫 杂项\n\n- Plugin: 黑白名单添加标签 [@A-kirami](https://github.com/A-kirami) ([#2170](https://github.com/nonebot/nonebot2/pull/2170))\n- Plugin: 修改 nonebot-plugin-ocgbot-v2 插件名称 [@fireinsect](https://github.com/fireinsect) ([#2147](https://github.com/nonebot/nonebot2/pull/2147))\n- Plugin: 更新 SparkGPT 插件描述 [@canxin121](https://github.com/canxin121) ([#2144](https://github.com/nonebot/nonebot2/pull/2144))\n- Plugin: 修改 nonebot-plugin-ocgbot-v2 插件名称 [@fireinsect](https://github.com/fireinsect) ([#2141](https://github.com/nonebot/nonebot2/pull/2141))\n- Plugin: 删除 nonebot-plugin-phlogo [@kexue-z](https://github.com/kexue-z) ([#2128](https://github.com/nonebot/nonebot2/pull/2128))\n- Plugin: 修改 `nonebot-plugin-gw2` 模块名 [@Agnes4m](https://github.com/Agnes4m) ([#2123](https://github.com/nonebot/nonebot2/pull/2123))\n- Develop: 添加 ruff linter [@yanyongyu](https://github.com/yanyongyu) ([#2114](https://github.com/nonebot/nonebot2/pull/2114))\n- Plugin: 更新 `nonebot-plugin-msgbuf` 插件的名称等信息 [@NCBM](https://github.com/NCBM) ([#2119](https://github.com/nonebot/nonebot2/pull/2119))\n- Plugin: 修改插件信息和仓库地址 [@Agnes4m](https://github.com/Agnes4m) ([#2115](https://github.com/nonebot/nonebot2/pull/2115))\n- Test: 移除 httpbin 并整理测试 [@yanyongyu](https://github.com/yanyongyu) ([#2110](https://github.com/nonebot/nonebot2/pull/2110))\n- CI: 缓存 NoneFlow 所需的 pre-commit hooks [@he0119](https://github.com/he0119) ([#2104](https://github.com/nonebot/nonebot2/pull/2104))\n- Plugin: 移除过时未更新的插件\\&Bot [@FYWinds](https://github.com/FYWinds) ([#2072](https://github.com/nonebot/nonebot2/pull/2072))\n- Plugin: 删除插件 nonebot_plugin_r6s [@BalconyJH](https://github.com/BalconyJH) ([#2071](https://github.com/nonebot/nonebot2/pull/2071))\n\n### 🍻 插件发布\n\n- Plugin: 方寸狭间 [@noneflow](https://github.com/noneflow) ([#2207](https://github.com/nonebot/nonebot2/pull/2207))\n- Plugin: DALL-E 绘图 [@noneflow](https://github.com/noneflow) ([#2204](https://github.com/nonebot/nonebot2/pull/2204))\n- Plugin: 指定戳一戳 [@noneflow](https://github.com/noneflow) ([#2202](https://github.com/nonebot/nonebot2/pull/2202))\n- Plugin: templates_render [@noneflow](https://github.com/noneflow) ([#2197](https://github.com/nonebot/nonebot2/pull/2197))\n- Plugin: MongoDB [@noneflow](https://github.com/noneflow) ([#2189](https://github.com/nonebot/nonebot2/pull/2189))\n- Plugin: pjsk 表情 [@noneflow](https://github.com/noneflow) ([#2187](https://github.com/nonebot/nonebot2/pull/2187))\n- Plugin: nonebot-plugin-wenan [@noneflow](https://github.com/noneflow) ([#2184](https://github.com/nonebot/nonebot2/pull/2184))\n- Plugin: nonebot-plugin-picture-api [@noneflow](https://github.com/noneflow) ([#2180](https://github.com/nonebot/nonebot2/pull/2180))\n- Plugin: Blocker [@noneflow](https://github.com/noneflow) ([#2178](https://github.com/nonebot/nonebot2/pull/2178))\n- Plugin: nonebot-plugin-nobahpicture [@noneflow](https://github.com/noneflow) ([#2176](https://github.com/nonebot/nonebot2/pull/2176))\n- Plugin: 过期事件过滤器 [@noneflow](https://github.com/noneflow) ([#2169](https://github.com/nonebot/nonebot2/pull/2169))\n- Plugin: 猫猫虫咖波图片发送 [@noneflow](https://github.com/noneflow) ([#2167](https://github.com/nonebot/nonebot2/pull/2167))\n- Plugin: nonebot-plugin-splatoon3 [@noneflow](https://github.com/noneflow) ([#2165](https://github.com/nonebot/nonebot2/pull/2165))\n- Plugin: nonebot-plugin-cfassistant [@noneflow](https://github.com/noneflow) ([#2164](https://github.com/nonebot/nonebot2/pull/2164))\n- Plugin: 算法竞赛比赛查询 [@noneflow](https://github.com/noneflow) ([#2159](https://github.com/nonebot/nonebot2/pull/2159))\n- Plugin: nonebot-plugin-update [@noneflow](https://github.com/noneflow) ([#2153](https://github.com/nonebot/nonebot2/pull/2153))\n- Plugin: 远程同意好友 [@noneflow](https://github.com/noneflow) ([#2146](https://github.com/nonebot/nonebot2/pull/2146))\n- Plugin: 戳一戳事件 [@noneflow](https://github.com/noneflow) ([#2139](https://github.com/nonebot/nonebot2/pull/2139))\n- Plugin: EitherChoice [@noneflow](https://github.com/noneflow) ([#2137](https://github.com/nonebot/nonebot2/pull/2137))\n- Plugin: 用户信息 [@noneflow](https://github.com/noneflow) ([#2133](https://github.com/nonebot/nonebot2/pull/2133))\n- Plugin: Diablo4 地狱狂潮 boss 提醒小助手 [@noneflow](https://github.com/noneflow) ([#2122](https://github.com/nonebot/nonebot2/pull/2122))\n- Plugin: nonbot-plugin-ocgbot-v2 [@noneflow](https://github.com/noneflow) ([#2120](https://github.com/nonebot/nonebot2/pull/2120))\n- Plugin: 错误告警 [@noneflow](https://github.com/noneflow) ([#2117](https://github.com/nonebot/nonebot2/pull/2117))\n- Plugin: follow_withdraw [@noneflow](https://github.com/noneflow) ([#2112](https://github.com/nonebot/nonebot2/pull/2112))\n- Plugin: 战雷查水表 [@noneflow](https://github.com/noneflow) ([#2103](https://github.com/nonebot/nonebot2/pull/2103))\n- Plugin: bili_push [@noneflow](https://github.com/noneflow) ([#2101](https://github.com/nonebot/nonebot2/pull/2101))\n- Plugin: AI 作曲 [@noneflow](https://github.com/noneflow) ([#2093](https://github.com/nonebot/nonebot2/pull/2093))\n- Plugin: pcrjjc [@noneflow](https://github.com/noneflow) ([#2091](https://github.com/nonebot/nonebot2/pull/2091))\n- Plugin: twitter 订阅 [@noneflow](https://github.com/noneflow) ([#2082](https://github.com/nonebot/nonebot2/pull/2082))\n- Plugin: 链接防夹 [@noneflow](https://github.com/noneflow) ([#2074](https://github.com/nonebot/nonebot2/pull/2074))\n- Plugin: 碧蓝航线攻略 [@noneflow](https://github.com/noneflow) ([#2076](https://github.com/nonebot/nonebot2/pull/2076))\n\n### 🍻 机器人发布\n\n- Bot: 米缸 [@noneflow](https://github.com/noneflow) ([#2191](https://github.com/nonebot/nonebot2/pull/2191))\n- Bot: 林汐 [@noneflow](https://github.com/noneflow) ([#2182](https://github.com/nonebot/nonebot2/pull/2182))\n- Bot: web_bot [@noneflow](https://github.com/noneflow) ([#2131](https://github.com/nonebot/nonebot2/pull/2131))\n- Bot: ReimeiBot-黎明机器人 [@noneflow](https://github.com/noneflow) ([#2107](https://github.com/nonebot/nonebot2/pull/2107))\n\n### 🍻 适配器发布\n\n- Adapter: 大别野 [@noneflow](https://github.com/noneflow) ([#2085](https://github.com/nonebot/nonebot2/pull/2085))\n\n## v2.0.0\n\n### 💥 破坏性变更\n\n- Feature: 支持 `re.Match` 依赖注入 [@yanyongyu](https://github.com/yanyongyu) ([#1950](https://github.com/nonebot/nonebot2/pull/1950))\n\n### 🚀 新功能\n\n- Feature: 优化事件分发方法 [@yanyongyu](https://github.com/yanyongyu) ([#2067](https://github.com/nonebot/nonebot2/pull/2067))\n- Feature: 移除部分依赖注入参数默认值检查 [@yanyongyu](https://github.com/yanyongyu) ([#2034](https://github.com/nonebot/nonebot2/pull/2034))\n- Feature: 添加插件元数据字段 `type` `homepage` `supported_adapters` [@yanyongyu](https://github.com/yanyongyu) ([#2012](https://github.com/nonebot/nonebot2/pull/2012))\n- Feature: 支持 `re.Match` 依赖注入 [@yanyongyu](https://github.com/yanyongyu) ([#1950](https://github.com/nonebot/nonebot2/pull/1950))\n- Feature: 支持主动停止 `none` 系列驱动器 [@yanyongyu](https://github.com/yanyongyu) ([#1951](https://github.com/nonebot/nonebot2/pull/1951))\n- Feature: 为消息类添加 `has` `join` `include` `exclude` 方法 [@yanyongyu](https://github.com/yanyongyu) ([#1895](https://github.com/nonebot/nonebot2/pull/1895))\n\n### 🐛 Bug 修复\n\n- Fix: 修复插件 require 未声明插件会识别为子插件 [@yanyongyu](https://github.com/yanyongyu) ([#2040](https://github.com/nonebot/nonebot2/pull/2040))\n- Fix: 修复命令强制空白符影响无参数情况 [@yanyongyu](https://github.com/yanyongyu) ([#1975](https://github.com/nonebot/nonebot2/pull/1975))\n- Fix: `run_sync` 上下文 [@synodriver](https://github.com/synodriver) ([#1968](https://github.com/nonebot/nonebot2/pull/1968))\n- Fix: shell command 包含富文本时报错信息出错 [@yanyongyu](https://github.com/yanyongyu) ([#1923](https://github.com/nonebot/nonebot2/pull/1923))\n\n### 📝 文档\n\n- Docs: 添加 Alconna 响应器介绍 [@RF-Tar-Railt](https://github.com/RF-Tar-Railt) ([#2069](https://github.com/nonebot/nonebot2/pull/2069))\n- Docs: 更新 README 适配器链接 [@yanyongyu](https://github.com/yanyongyu) ([#2068](https://github.com/nonebot/nonebot2/pull/2068))\n- Docs: 使用 issue form 进行商店发布 [@yanyongyu](https://github.com/yanyongyu) ([#2010](https://github.com/nonebot/nonebot2/pull/2010))\n- Docs: 修复获取事件信息文档代码范例中的高亮行 [@Lptr-byte](https://github.com/Lptr-byte) ([#1983](https://github.com/nonebot/nonebot2/pull/1983))\n- Docs: 修复事件处理函数文档代码范例中缺失的 import [@Lptr-byte](https://github.com/Lptr-byte) ([#1982](https://github.com/nonebot/nonebot2/pull/1982))\n- Docs: 修复获取事件信息文档代码范例中缺失的 import [@Lptr-byte](https://github.com/Lptr-byte) ([#1980](https://github.com/nonebot/nonebot2/pull/1980))\n- Docs: 新增插件跨平台指南 [@Well2333](https://github.com/Well2333) ([#1938](https://github.com/nonebot/nonebot2/pull/1938))\n- Docs: 开启 blank issues [@yanyongyu](https://github.com/yanyongyu) ([#1945](https://github.com/nonebot/nonebot2/pull/1945))\n- Docs: 使用 issue 表单替换 issue 模板 [@A-kirami](https://github.com/A-kirami) ([#1928](https://github.com/nonebot/nonebot2/pull/1928))\n- Docs: 修正教程中部分 import 缺失的问题 [@Well2333](https://github.com/Well2333) ([#1927](https://github.com/nonebot/nonebot2/pull/1927))\n- Docs: 添加 Walle-Q 到 Readme [@yanyongyu](https://github.com/yanyongyu) ([#1891](https://github.com/nonebot/nonebot2/pull/1891))\n- Docs: 更新部署文档 [@yanyongyu](https://github.com/yanyongyu) ([#1890](https://github.com/nonebot/nonebot2/pull/1890))\n\n### 💫 杂项\n\n- Plugin: Hello World 添加 tag [@A-kirami](https://github.com/A-kirami) ([#2056](https://github.com/nonebot/nonebot2/pull/2056))\n- Plugin: 修改 nonebot-plugin-logpile 的名称和描述 [@A-kirami](https://github.com/A-kirami) ([#2057](https://github.com/nonebot/nonebot2/pull/2057))\n- Plugin: 移除 `nonebot_paddle_ocr` 和 `nonebot_poe_chat` [@canxin121](https://github.com/canxin121) ([#2039](https://github.com/nonebot/nonebot2/pull/2039))\n- Plugin: 移除 `nonebot-plugin-rtfm` 插件 [@MingxuanGame](https://github.com/MingxuanGame) ([#2037](https://github.com/nonebot/nonebot2/pull/2037))\n- Plugin: 移除 extrautils 工具拓展插件（暂停维护） [@NCBM](https://github.com/NCBM) ([#2033](https://github.com/nonebot/nonebot2/pull/2033))\n- Adapter: 更新 Minecraft 适配器 [@17TheWord](https://github.com/17TheWord) ([#1972](https://github.com/nonebot/nonebot2/pull/1972))\n- Docs: 更正 issue 表单部分内容 [@A-kirami](https://github.com/A-kirami) ([#1961](https://github.com/nonebot/nonebot2/pull/1961))\n- Plugin: 更新 AutoReply 插件描述 [@lgc2333](https://github.com/lgc2333) ([#1949](https://github.com/nonebot/nonebot2/pull/1949))\n- Plugin: 移除 `MC_QQ_MCRcon` [@17TheWord](https://github.com/17TheWord) ([#1948](https://github.com/nonebot/nonebot2/pull/1948))\n- Plugin: 更新 lgc2333 插件仓库地址 [@lgc2333](https://github.com/lgc2333) ([#1935](https://github.com/nonebot/nonebot2/pull/1935))\n- Plugin: 更新多功能哔哩哔哩解析工具 [@djkcyl](https://github.com/djkcyl) ([#1913](https://github.com/nonebot/nonebot2/pull/1913))\n- CI: 跳过 PR 仓库为 fork 的情况 [@he0119](https://github.com/he0119) ([#1905](https://github.com/nonebot/nonebot2/pull/1905))\n- Plugin: 移除旧版本的 GenshinUID [@KimigaiiWuyi](https://github.com/KimigaiiWuyi) ([#1904](https://github.com/nonebot/nonebot2/pull/1904))\n- CI: 使用最新的 NoneFlow [@he0119](https://github.com/he0119) ([#1899](https://github.com/nonebot/nonebot2/pull/1899))\n- CI: 使用 NoneFlow 管理工作流 [@yanyongyu](https://github.com/yanyongyu) ([#1892](https://github.com/nonebot/nonebot2/pull/1892))\n- CI: 移除 poetry 版本限制 [@yanyongyu](https://github.com/yanyongyu) ([#1872](https://github.com/nonebot/nonebot2/pull/1872))\n\n### 🍻 插件发布\n\n- Plugin: stablediffusion 绘画插件 [@noneflow](https://github.com/noneflow) ([#2066](https://github.com/nonebot/nonebot2/pull/2066))\n- Plugin: 随机抽取自定义内容 [@noneflow](https://github.com/noneflow) ([#2064](https://github.com/nonebot/nonebot2/pull/2064))\n- Plugin: NAGA 公交车 [@noneflow](https://github.com/noneflow) ([#2062](https://github.com/nonebot/nonebot2/pull/2062))\n- Plugin: 本子标题关键词提取 [@noneflow](https://github.com/noneflow) ([#2058](https://github.com/nonebot/nonebot2/pull/2058))\n- Plugin: puzzle [@noneflow](https://github.com/noneflow) ([#2054](https://github.com/nonebot/nonebot2/pull/2054))\n- Plugin: homo_mathematician [@noneflow](https://github.com/noneflow) ([#2052](https://github.com/nonebot/nonebot2/pull/2052))\n- Plugin: cuber [@noneflow](https://github.com/noneflow) ([#2048](https://github.com/nonebot/nonebot2/pull/2048))\n- Plugin: nonebot-plugin-lua [@noneflow](https://github.com/noneflow) ([#2049](https://github.com/nonebot/nonebot2/pull/2049))\n- Plugin: Github 仓库卡片 [@noneflow](https://github.com/noneflow) ([#2042](https://github.com/nonebot/nonebot2/pull/2042))\n- Plugin: 股票看盘助手 [@noneflow](https://github.com/noneflow) ([#2032](https://github.com/nonebot/nonebot2/pull/2032))\n- Plugin: 便携插件安装器 [@noneflow](https://github.com/noneflow) ([#2027](https://github.com/nonebot/nonebot2/pull/2027))\n- Plugin: 会话 id [@noneflow](https://github.com/noneflow) ([#2025](https://github.com/nonebot/nonebot2/pull/2025))\n- Plugin: SD 绘画插件 [@noneflow](https://github.com/noneflow) ([#2023](https://github.com/nonebot/nonebot2/pull/2023))\n- Plugin: 《女神异闻录 5》预告信生成器 [@noneflow](https://github.com/noneflow) ([#2021](https://github.com/nonebot/nonebot2/pull/2021))\n- Plugin: 小小的 WEBAPI 调用插件 [@noneflow](https://github.com/noneflow) ([#2020](https://github.com/nonebot/nonebot2/pull/2020))\n- Plugin: MultiNCM [@noneflow](https://github.com/noneflow) ([#2018](https://github.com/nonebot/nonebot2/pull/2018))\n- Plugin: 签到 [@noneflow](https://github.com/noneflow) ([#2014](https://github.com/nonebot/nonebot2/pull/2014))\n- Plugin: 链接解析 [@noneflow](https://github.com/noneflow) ([#2011](https://github.com/nonebot/nonebot2/pull/2011))\n- Plugin: 信鸽巴夫 [@noneflow](https://github.com/noneflow) ([#2008](https://github.com/nonebot/nonebot2/pull/2008))\n- Plugin: 明日方舟抽卡模拟 [@noneflow](https://github.com/noneflow) ([#2005](https://github.com/nonebot/nonebot2/pull/2005))\n- Plugin: 雷神工业 [@noneflow](https://github.com/noneflow) ([#2003](https://github.com/nonebot/nonebot2/pull/2003))\n- Plugin: nonebot-plugin-logpile [@noneflow](https://github.com/noneflow) ([#1999](https://github.com/nonebot/nonebot2/pull/1999))\n- Plugin: Spark-GPT [@noneflow](https://github.com/noneflow) ([#1997](https://github.com/nonebot/nonebot2/pull/1997))\n- Plugin: 企鹅物流统计数据查询 [@noneflow](https://github.com/noneflow) ([#1995](https://github.com/nonebot/nonebot2/pull/1995))\n- Plugin: CallAPI [@noneflow](https://github.com/noneflow) ([#1990](https://github.com/nonebot/nonebot2/pull/1990))\n- Plugin: 群聊人数锁定 [@noneflow](https://github.com/noneflow) ([#1988](https://github.com/nonebot/nonebot2/pull/1988))\n- Plugin: CSGO 开箱模拟器 [@noneflow](https://github.com/noneflow) ([#1986](https://github.com/nonebot/nonebot2/pull/1986))\n- Plugin: wordle_help [@noneflow](https://github.com/noneflow) ([#1974](https://github.com/nonebot/nonebot2/pull/1974))\n- Plugin: 星穹铁道活动日历 [@noneflow](https://github.com/noneflow) ([#1970](https://github.com/nonebot/nonebot2/pull/1970))\n- Plugin: 水印大师 [@noneflow](https://github.com/noneflow) ([#1965](https://github.com/nonebot/nonebot2/pull/1965))\n- Plugin: 图片/漫画翻译 [@noneflow](https://github.com/noneflow) ([#1955](https://github.com/nonebot/nonebot2/pull/1955))\n- Plugin: 为美好群聊献上爆炎 [@noneflow](https://github.com/noneflow) ([#1953](https://github.com/nonebot/nonebot2/pull/1953))\n- Plugin: 公共画板插件 [@noneflow](https://github.com/noneflow) ([#1957](https://github.com/nonebot/nonebot2/pull/1957))\n- Plugin: 运行代码 [@noneflow](https://github.com/noneflow) ([#1942](https://github.com/nonebot/nonebot2/pull/1942))\n- Plugin: brainfuck [@noneflow](https://github.com/noneflow) ([#1944](https://github.com/nonebot/nonebot2/pull/1944))\n- Plugin: Mixin [@noneflow](https://github.com/noneflow) ([#1947](https://github.com/nonebot/nonebot2/pull/1947))\n- Plugin: AppInsights 日志监控 [@noneflow](https://github.com/noneflow) ([#1940](https://github.com/nonebot/nonebot2/pull/1940))\n- Plugin: nonebot_poe_chat [@noneflow](https://github.com/noneflow) ([#1937](https://github.com/nonebot/nonebot2/pull/1937))\n- Plugin: 更改 BOT 群名片 [@noneflow](https://github.com/noneflow) ([#1934](https://github.com/nonebot/nonebot2/pull/1934))\n- Plugin: Akinator [@noneflow](https://github.com/noneflow) ([#1925](https://github.com/nonebot/nonebot2/pull/1925))\n- Plugin: Bilifan [@noneflow](https://github.com/noneflow) ([#1921](https://github.com/nonebot/nonebot2/pull/1921))\n- Plugin: osu!入群审批 [@noneflow](https://github.com/noneflow) ([#1919](https://github.com/nonebot/nonebot2/pull/1919))\n- Plugin: 与 ChatGpt 聊天 [@noneflow](https://github.com/noneflow) ([#1917](https://github.com/nonebot/nonebot2/pull/1917))\n- Plugin: TataruBot2 [@noneflow](https://github.com/noneflow) ([#1915](https://github.com/nonebot/nonebot2/pull/1915))\n- Plugin: 宝可梦融合 [@noneflow](https://github.com/noneflow) ([#1912](https://github.com/nonebot/nonebot2/pull/1912))\n- Plugin: FuckYou [@noneflow](https://github.com/noneflow) ([#1910](https://github.com/nonebot/nonebot2/pull/1910))\n- Plugin: SDGPT [@noneflow](https://github.com/noneflow) ([#1908](https://github.com/nonebot/nonebot2/pull/1908))\n- Plugin: nonebot clock 群闹钟 ⏰ [@noneflow](https://github.com/noneflow) ([#1906](https://github.com/nonebot/nonebot2/pull/1906))\n- Plugin: B 站直播间路灯 [@noneflow](https://github.com/noneflow) ([#1901](https://github.com/nonebot/nonebot2/pull/1901))\n- Plugin: GenshinUID [@noneflow](https://github.com/noneflow) ([#1903](https://github.com/nonebot/nonebot2/pull/1903))\n- Plugin: 多功能哔哩哔哩解析工具 [@noneflow](https://github.com/noneflow) ([#1898](https://github.com/nonebot/nonebot2/pull/1898))\n- Plugin: Steam 游戏状态播报 [@yanyongyu](https://github.com/yanyongyu) ([#1887](https://github.com/nonebot/nonebot2/pull/1887))\n- Plugin: AI 生成 PPT [@yanyongyu](https://github.com/yanyongyu) ([#1884](https://github.com/nonebot/nonebot2/pull/1884))\n- Plugin: nonebot_paddle_ocr [@yanyongyu](https://github.com/yanyongyu) ([#1882](https://github.com/nonebot/nonebot2/pull/1882))\n- Plugin: nonebot_api_paddle [@yanyongyu](https://github.com/yanyongyu) ([#1880](https://github.com/nonebot/nonebot2/pull/1880))\n- Plugin: 来份睡眠套餐 [@yanyongyu](https://github.com/yanyongyu) ([#1876](https://github.com/nonebot/nonebot2/pull/1876))\n- Plugin: 今日老婆 [@yanyongyu](https://github.com/yanyongyu) ([#1874](https://github.com/nonebot/nonebot2/pull/1874))\n- Plugin: 激战 2！！！ [@yanyongyu](https://github.com/yanyongyu) ([#1871](https://github.com/nonebot/nonebot2/pull/1871))\n- Plugin: ROLL [@yanyongyu](https://github.com/yanyongyu) ([#1868](https://github.com/nonebot/nonebot2/pull/1868))\n\n### 🍻 机器人发布\n\n- Bot: 狐尾 [@noneflow](https://github.com/noneflow) ([#2009](https://github.com/nonebot/nonebot2/pull/2009))\n- Bot: ay 机器人 [@noneflow](https://github.com/noneflow) ([#1993](https://github.com/nonebot/nonebot2/pull/1993))\n- Bot: March7th [@noneflow](https://github.com/noneflow) ([#1978](https://github.com/nonebot/nonebot2/pull/1978))\n- Bot: XDbot2 [@noneflow](https://github.com/noneflow) ([#1932](https://github.com/nonebot/nonebot2/pull/1932))\n- Bot: CoolQBot [@noneflow](https://github.com/noneflow) ([#1894](https://github.com/nonebot/nonebot2/pull/1894))\n\n### 🍻 适配器发布\n\n- Adapter: Walle-Q [@yanyongyu](https://github.com/yanyongyu) ([#1889](https://github.com/nonebot/nonebot2/pull/1889))\n\n## v2.0.0rc4\n\n### 🚀 新功能\n\n- Feature: 公开自定义 `on` 函数所需的函数 [@A-kirami](https://github.com/A-kirami) ([#1856](https://github.com/nonebot/nonebot2/pull/1856))\n- Feature: 重构驱动器 lifespan 方法 [@yanyongyu](https://github.com/yanyongyu) ([#1860](https://github.com/nonebot/nonebot2/pull/1860))\n- Test: 使用 conditional coverage 插件 [@yanyongyu](https://github.com/yanyongyu) ([#1858](https://github.com/nonebot/nonebot2/pull/1858))\n- Feature: 在 Windows 上处理 SIGBREAK 信号 [@he0119](https://github.com/he0119) ([#1836](https://github.com/nonebot/nonebot2/pull/1836))\n- Feature: 为子依赖添加 PEP593 `Annotated` 支持 [@mnixry](https://github.com/mnixry) ([#1832](https://github.com/nonebot/nonebot2/pull/1832))\n- Feature: 为 `User` 权限添加便捷创建方法 [@yanyongyu](https://github.com/yanyongyu) ([#1825](https://github.com/nonebot/nonebot2/pull/1825))\n- Feature: 移除内置响应规则事件类型限制 [@yanyongyu](https://github.com/yanyongyu) ([#1824](https://github.com/nonebot/nonebot2/pull/1824))\n- Feature: 允许继承和使用 Matcher 子类 [@yanyongyu](https://github.com/yanyongyu) ([#1815](https://github.com/nonebot/nonebot2/pull/1815))\n- Feature: 添加 `get_adapter` 类型 overload [@yanyongyu](https://github.com/yanyongyu) ([#1755](https://github.com/nonebot/nonebot2/pull/1755))\n- Feature: 命令匹配支持强制指定空白符 [@yanyongyu](https://github.com/yanyongyu) ([#1748](https://github.com/nonebot/nonebot2/pull/1748))\n- Feature: 添加获取已注册适配器的方法 [@yanyongyu](https://github.com/yanyongyu) ([#1747](https://github.com/nonebot/nonebot2/pull/1747))\n- Feature: 使用 `tomllib` 读取 toml 配置 [@yanyongyu](https://github.com/yanyongyu) ([#1720](https://github.com/nonebot/nonebot2/pull/1720))\n- Feature: 优化插件加载日志 [@yanyongyu](https://github.com/yanyongyu) ([#1716](https://github.com/nonebot/nonebot2/pull/1716))\n- Feature: 在加载 driver 引发 ImportError 时，使用 `raise from e` [@shoucandanghehe](https://github.com/shoucandanghehe) ([#1689](https://github.com/nonebot/nonebot2/pull/1689))\n- Feature: 添加端口配置项约束验证 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#1632](https://github.com/nonebot/nonebot2/pull/1632))\n\n### 🐛 Bug 修复\n\n- Test: coverage condition invert [@yanyongyu](https://github.com/yanyongyu) ([#1862](https://github.com/nonebot/nonebot2/pull/1862))\n- Fix: 检测运行时创建响应器的插件 [@yanyongyu](https://github.com/yanyongyu) ([#1857](https://github.com/nonebot/nonebot2/pull/1857))\n- Fix: 修复事件响应器辅助函数丢失 block [@yanyongyu](https://github.com/yanyongyu) ([#1859](https://github.com/nonebot/nonebot2/pull/1859))\n- Fix: 修复 bot hook 缺少依赖缓存和上下文管理 [@yanyongyu](https://github.com/yanyongyu) ([#1826](https://github.com/nonebot/nonebot2/pull/1826))\n- Fix: 会话更新依赖注入缺少缓存和上下文管理 [@yanyongyu](https://github.com/yanyongyu) ([#1807](https://github.com/nonebot/nonebot2/pull/1807))\n- Fix: 修复适配器能断开非自身所有的 Bot 对象 [@yanyongyu](https://github.com/yanyongyu) ([#1757](https://github.com/nonebot/nonebot2/pull/1757))\n\n### 📝 文档\n\n- Docs: 修改 NoneBug 独立测试模式流程控制参数 [@yanyongyu](https://github.com/yanyongyu) ([#1866](https://github.com/nonebot/nonebot2/pull/1866))\n- Docs: 添加 VSCode 配置项名称 [@yanyongyu](https://github.com/yanyongyu) ([#1863](https://github.com/nonebot/nonebot2/pull/1863))\n- Docs: 添加 Message 基类模板使用警告 [@yanyongyu](https://github.com/yanyongyu) ([#1853](https://github.com/nonebot/nonebot2/pull/1853))\n- Docs: 移除 Messenger 移动端预期外的蓝色遮罩 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#1842](https://github.com/nonebot/nonebot2/pull/1842))\n- Docs: 更新指向文档的链接 [@he0119](https://github.com/he0119) ([#1841](https://github.com/nonebot/nonebot2/pull/1841))\n- Docs: 更新 setup 动图 [@yanyongyu](https://github.com/yanyongyu) ([#1840](https://github.com/nonebot/nonebot2/pull/1840))\n- Docs: 重写教程与进阶指南 [@yanyongyu](https://github.com/yanyongyu) ([#1604](https://github.com/nonebot/nonebot2/pull/1604))\n- Docs: pip 安装指令添加引号 [@3yude](https://github.com/3yude) ([#1724](https://github.com/nonebot/nonebot2/pull/1724))\n- Docs: 修正交互模式命令 [@3yude](https://github.com/3yude) ([#1719](https://github.com/nonebot/nonebot2/pull/1719))\n\n### 💫 杂项\n\n- Plugin: 删除 bnhhsh [@lgc2333](https://github.com/lgc2333) ([#1792](https://github.com/nonebot/nonebot2/pull/1792))\n- CI: 暂时修复 poetry 依赖安装 [@yanyongyu](https://github.com/yanyongyu) ([#1776](https://github.com/nonebot/nonebot2/pull/1776))\n- Plugin: 修改链接分享解析器插件名称 [@zhiyu1998](https://github.com/zhiyu1998) ([#1715](https://github.com/nonebot/nonebot2/pull/1715))\n- Bot: 移除 ShigureBot [@lgc2333](https://github.com/lgc2333) ([#1699](https://github.com/nonebot/nonebot2/pull/1699))\n- CI: 发布机器人使用 latest 标签 [@he0119](https://github.com/he0119) ([#1690](https://github.com/nonebot/nonebot2/pull/1690))\n- Fix: 修改 bilibili live 的模块路径 [@yanyongyu](https://github.com/yanyongyu) ([#1679](https://github.com/nonebot/nonebot2/pull/1679))\n- Docs: 移除商店中的过期插件 2023 [@j1g5awi](https://github.com/j1g5awi) ([#1610](https://github.com/nonebot/nonebot2/pull/1610))\n\n### 🍻 插件发布\n\n- Plugin: ChatGPT 网页端 API [@yanyongyu](https://github.com/yanyongyu) ([#1865](https://github.com/nonebot/nonebot2/pull/1865))\n- Plugin: 原神 cos [@yanyongyu](https://github.com/yanyongyu) ([#1855](https://github.com/nonebot/nonebot2/pull/1855))\n- Plugin: 颠倒问号 [@yanyongyu](https://github.com/yanyongyu) ([#1849](https://github.com/nonebot/nonebot2/pull/1849))\n- Plugin: nonebot-plugin-miao [@yanyongyu](https://github.com/yanyongyu) ([#1851](https://github.com/nonebot/nonebot2/pull/1851))\n- Plugin: 通括膨胀 [@yanyongyu](https://github.com/yanyongyu) ([#1847](https://github.com/nonebot/nonebot2/pull/1847))\n- Plugin: Hello World [@yanyongyu](https://github.com/yanyongyu) ([#1845](https://github.com/nonebot/nonebot2/pull/1845))\n- Plugin: 喵喵点歌 [@yanyongyu](https://github.com/yanyongyu) ([#1838](https://github.com/nonebot/nonebot2/pull/1838))\n- Plugin: ChatGLM-6B API 版 [@yanyongyu](https://github.com/yanyongyu) ([#1834](https://github.com/nonebot/nonebot2/pull/1834))\n- Plugin: ChatGLM [@yanyongyu](https://github.com/yanyongyu) ([#1831](https://github.com/nonebot/nonebot2/pull/1831))\n- Plugin: 基于 OpenAI 的 AI 模拟面试官 [@yanyongyu](https://github.com/yanyongyu) ([#1829](https://github.com/nonebot/nonebot2/pull/1829))\n- Plugin: 多平台热搜获取插件 [@yanyongyu](https://github.com/yanyongyu) ([#1823](https://github.com/nonebot/nonebot2/pull/1823))\n- Plugin: 随机点名 [@yanyongyu](https://github.com/yanyongyu) ([#1819](https://github.com/nonebot/nonebot2/pull/1819))\n- Plugin: 表情包制作（调用 API 版） [@yanyongyu](https://github.com/yanyongyu) ([#1821](https://github.com/nonebot/nonebot2/pull/1821))\n- Plugin: 群聊语录库 [@yanyongyu](https://github.com/yanyongyu) ([#1817](https://github.com/nonebot/nonebot2/pull/1817))\n- Plugin: 随机狗妈 [@yanyongyu](https://github.com/yanyongyu) ([#1813](https://github.com/nonebot/nonebot2/pull/1813))\n- Plugin: apex 信息查询 [@yanyongyu](https://github.com/yanyongyu) ([#1811](https://github.com/nonebot/nonebot2/pull/1811))\n- Plugin: unoconv 文件转换 [@yanyongyu](https://github.com/yanyongyu) ([#1809](https://github.com/nonebot/nonebot2/pull/1809))\n- Plugin: 原神历史卡池 [@yanyongyu](https://github.com/yanyongyu) ([#1806](https://github.com/nonebot/nonebot2/pull/1806))\n- Plugin: 括号补全 [@yanyongyu](https://github.com/yanyongyu) ([#1804](https://github.com/nonebot/nonebot2/pull/1804))\n- Plugin: 修仙模拟器 [@yanyongyu](https://github.com/yanyongyu) ([#1802](https://github.com/nonebot/nonebot2/pull/1802))\n- Plugin: 发 6 [@yanyongyu](https://github.com/yanyongyu) ([#1798](https://github.com/nonebot/nonebot2/pull/1798))\n- Plugin: 群聊自定义表情包 [@yanyongyu](https://github.com/yanyongyu) ([#1795](https://github.com/nonebot/nonebot2/pull/1795))\n- Plugin: RimoFun [@yanyongyu](https://github.com/yanyongyu) ([#1791](https://github.com/nonebot/nonebot2/pull/1791))\n- Plugin: ChatPDF 文章分析 [@yanyongyu](https://github.com/yanyongyu) ([#1788](https://github.com/nonebot/nonebot2/pull/1788))\n- Plugin: 和团子聊天！ [@yanyongyu](https://github.com/yanyongyu) ([#1785](https://github.com/nonebot/nonebot2/pull/1785))\n- Plugin: 多功能的 ChatGPT 机器人 [@yanyongyu](https://github.com/yanyongyu) ([#1781](https://github.com/nonebot/nonebot2/pull/1781))\n- Plugin: ChatGPT 官方接口版 [@yanyongyu](https://github.com/yanyongyu) ([#1767](https://github.com/nonebot/nonebot2/pull/1767))\n- Plugin: 明日方舟抽卡记录分析 [@yanyongyu](https://github.com/yanyongyu) ([#1786](https://github.com/nonebot/nonebot2/pull/1786))\n- Plugin: Sanae [@yanyongyu](https://github.com/yanyongyu) ([#1775](https://github.com/nonebot/nonebot2/pull/1775))\n- Plugin: 小爱课程表 [@yanyongyu](https://github.com/yanyongyu) ([#1773](https://github.com/nonebot/nonebot2/pull/1773))\n- Plugin: AutoRepeater [@yanyongyu](https://github.com/yanyongyu) ([#1769](https://github.com/nonebot/nonebot2/pull/1769))\n- Plugin: 60s 日历 [@yanyongyu](https://github.com/yanyongyu) ([#1765](https://github.com/nonebot/nonebot2/pull/1765))\n- Plugin: 青年大学习提交(基础版) [@yanyongyu](https://github.com/yanyongyu) ([#1764](https://github.com/nonebot/nonebot2/pull/1764))\n- Plugin: 青年大学习提交(Web UI) [@yanyongyu](https://github.com/yanyongyu) ([#1762](https://github.com/nonebot/nonebot2/pull/1762))\n- Plugin: 网抑云 [@yanyongyu](https://github.com/yanyongyu) ([#1760](https://github.com/nonebot/nonebot2/pull/1760))\n- Plugin: nonebot_plugin_eventdone [@yanyongyu](https://github.com/yanyongyu) ([#1758](https://github.com/nonebot/nonebot2/pull/1758))\n- Plugin: 爱发电审核 [@yanyongyu](https://github.com/yanyongyu) ([#1750](https://github.com/nonebot/nonebot2/pull/1750))\n- Plugin: 战地一入群审批 [@yanyongyu](https://github.com/yanyongyu) ([#1745](https://github.com/nonebot/nonebot2/pull/1745))\n- Plugin: wf 的 wm 市场 [@yanyongyu](https://github.com/yanyongyu) ([#1742](https://github.com/nonebot/nonebot2/pull/1742))\n- Plugin: 呆呆兽都会用的 chatbot 接 api [@yanyongyu](https://github.com/yanyongyu) ([#1740](https://github.com/nonebot/nonebot2/pull/1740))\n- Plugin: 呆呆兽都会起来锻炼 H2E [@yanyongyu](https://github.com/yanyongyu) ([#1739](https://github.com/nonebot/nonebot2/pull/1739))\n- Plugin: 修仙\\_2.0 [@yanyongyu](https://github.com/yanyongyu) ([#1730](https://github.com/nonebot/nonebot2/pull/1730))\n- Plugin: 发病语录 [@yanyongyu](https://github.com/yanyongyu) ([#1728](https://github.com/nonebot/nonebot2/pull/1728))\n- Plugin: 峯驰物流 [@yanyongyu](https://github.com/yanyongyu) ([#1723](https://github.com/nonebot/nonebot2/pull/1723))\n- Plugin: Bing Chat [@yanyongyu](https://github.com/yanyongyu) ([#1714](https://github.com/nonebot/nonebot2/pull/1714))\n- Plugin: 视频、图片解析器 [@yanyongyu](https://github.com/yanyongyu) ([#1710](https://github.com/nonebot/nonebot2/pull/1710))\n- Plugin: 你画我猜组队 [@yanyongyu](https://github.com/yanyongyu) ([#1705](https://github.com/nonebot/nonebot2/pull/1705))\n- Plugin: 明日方舟工具箱 [@yanyongyu](https://github.com/yanyongyu) ([#1698](https://github.com/nonebot/nonebot2/pull/1698))\n- Plugin: 原神深境螺旋数据查询 [@yanyongyu](https://github.com/yanyongyu) ([#1696](https://github.com/nonebot/nonebot2/pull/1696))\n- Plugin: 工具拓展 [@yanyongyu](https://github.com/yanyongyu) ([#1694](https://github.com/nonebot/nonebot2/pull/1694))\n- Plugin: OneBot 实现 [@yanyongyu](https://github.com/yanyongyu) ([#1692](https://github.com/nonebot/nonebot2/pull/1692))\n- Plugin: 舞萌 maimai 插件版 [@yanyongyu](https://github.com/yanyongyu) ([#1687](https://github.com/nonebot/nonebot2/pull/1687))\n- Plugin: ACMReminder [@yanyongyu](https://github.com/yanyongyu) ([#1686](https://github.com/nonebot/nonebot2/pull/1686))\n- Plugin: 通用指令阻断 [@yanyongyu](https://github.com/yanyongyu) ([#1683](https://github.com/nonebot/nonebot2/pull/1683))\n- Plugin: 今天吃喝什么（图片版） [@yanyongyu](https://github.com/yanyongyu) ([#1678](https://github.com/nonebot/nonebot2/pull/1678))\n- Plugin: Q 群消息事件监控 [@yanyongyu](https://github.com/yanyongyu) ([#1672](https://github.com/nonebot/nonebot2/pull/1672))\n- Plugin: DickyPK [@yanyongyu](https://github.com/yanyongyu) ([#1670](https://github.com/nonebot/nonebot2/pull/1670))\n- Plugin: 每日人品 2 [@yanyongyu](https://github.com/yanyongyu) ([#1669](https://github.com/nonebot/nonebot2/pull/1669))\n- Plugin: 娶群友 [@yanyongyu](https://github.com/yanyongyu) ([#1665](https://github.com/nonebot/nonebot2/pull/1665))\n- Plugin: 我要一张 xx 涩图 [@yanyongyu](https://github.com/yanyongyu) ([#1663](https://github.com/nonebot/nonebot2/pull/1663))\n- Plugin: AutoReply [@yanyongyu](https://github.com/yanyongyu) ([#1660](https://github.com/nonebot/nonebot2/pull/1660))\n- Plugin: B 站热搜 [@yanyongyu](https://github.com/yanyongyu) ([#1658](https://github.com/nonebot/nonebot2/pull/1658))\n- Plugin: MC Ping [@yanyongyu](https://github.com/yanyongyu) ([#1656](https://github.com/nonebot/nonebot2/pull/1656))\n- Plugin: impact 淫趴 [@yanyongyu](https://github.com/yanyongyu) ([#1653](https://github.com/nonebot/nonebot2/pull/1653))\n- Plugin: 更人性化的 GPT-Ai 聊天插件 [@yanyongyu](https://github.com/yanyongyu) ([#1651](https://github.com/nonebot/nonebot2/pull/1651))\n- Plugin: uuid 生成器 [@yanyongyu](https://github.com/yanyongyu) ([#1649](https://github.com/nonebot/nonebot2/pull/1649))\n- Plugin: 舔狗日记 [@yanyongyu](https://github.com/yanyongyu) ([#1646](https://github.com/nonebot/nonebot2/pull/1646))\n- Plugin: 查找轻小说 [@yanyongyu](https://github.com/yanyongyu) ([#1644](https://github.com/nonebot/nonebot2/pull/1644))\n- Plugin: XDU 校园服务 [@yanyongyu](https://github.com/yanyongyu) ([#1642](https://github.com/nonebot/nonebot2/pull/1642))\n- Plugin: nonebot-plugin-mcport [@yanyongyu](https://github.com/yanyongyu) ([#1640](https://github.com/nonebot/nonebot2/pull/1640))\n- Plugin: Alconna 命令工具 [@yanyongyu](https://github.com/yanyongyu) ([#1639](https://github.com/nonebot/nonebot2/pull/1639))\n- Plugin: Group_Link_Guild [@yanyongyu](https://github.com/yanyongyu) ([#1637](https://github.com/nonebot/nonebot2/pull/1637))\n- Plugin: 简易群管女生自用 99 新 [@yanyongyu](https://github.com/yanyongyu) ([#1635](https://github.com/nonebot/nonebot2/pull/1635))\n- Plugin: 青岚 [@yanyongyu](https://github.com/yanyongyu) ([#1631](https://github.com/nonebot/nonebot2/pull/1631))\n- Plugin: 对话超管 [@yanyongyu](https://github.com/yanyongyu) ([#1627](https://github.com/nonebot/nonebot2/pull/1627))\n- Plugin: 摩尔质量计算器 [@yanyongyu](https://github.com/yanyongyu) ([#1625](https://github.com/nonebot/nonebot2/pull/1625))\n- Plugin: 植物大战僵尸小游戏 [@yanyongyu](https://github.com/yanyongyu) ([#1622](https://github.com/nonebot/nonebot2/pull/1622))\n\n### 🍻 机器人发布\n\n- Bot: 桃桃酱 [@yanyongyu](https://github.com/yanyongyu) ([#1801](https://github.com/nonebot/nonebot2/pull/1801))\n- Bot: fubot [@yanyongyu](https://github.com/yanyongyu) ([#1783](https://github.com/nonebot/nonebot2/pull/1783))\n- Bot: LOVE 酱 [@yanyongyu](https://github.com/yanyongyu) ([#1779](https://github.com/nonebot/nonebot2/pull/1779))\n- Bot: 脑积水 [@yanyongyu](https://github.com/yanyongyu) ([#1771](https://github.com/nonebot/nonebot2/pull/1771))\n- Bot: koishi [@yanyongyu](https://github.com/yanyongyu) ([#1681](https://github.com/nonebot/nonebot2/pull/1681))\n- Bot: ChensQBOTv2 [@yanyongyu](https://github.com/yanyongyu) ([#1676](https://github.com/nonebot/nonebot2/pull/1676))\n- Bot: 青岚 [@yanyongyu](https://github.com/yanyongyu) ([#1630](https://github.com/nonebot/nonebot2/pull/1630))\n\n## v2.0.0rc3\n\n### 🚀 新功能\n\n- Feature: 添加事件响应器检查完成日志 [@A-kirami](https://github.com/A-kirami) ([#1578](https://github.com/nonebot/nonebot2/pull/1578))\n- Remove: 移除默认安装 FastAPI [@yanyongyu](https://github.com/yanyongyu) ([#1557](https://github.com/nonebot/nonebot2/pull/1557))\n- Feature: 支持给 `FastAPI` 和 `Quart` 传递额外的参数 [@A-kirami](https://github.com/A-kirami) ([#1543](https://github.com/nonebot/nonebot2/pull/1543))\n- Feature: 添加 `logger` 重导出 [@A-kirami](https://github.com/A-kirami) ([#1526](https://github.com/nonebot/nonebot2/pull/1526))\n- Feature: 将 block driver 转正为 none 驱动器 [@he0119](https://github.com/he0119) ([#1522](https://github.com/nonebot/nonebot2/pull/1522))\n- Develop: 使用 pycln 自动移除未使用的 import [@yanyongyu](https://github.com/yanyongyu) ([#1481](https://github.com/nonebot/nonebot2/pull/1481))\n- Feature: 添加正则匹配文本注入 [@A-kirami](https://github.com/A-kirami) ([#1457](https://github.com/nonebot/nonebot2/pull/1457))\n- Feature: 支持主动销毁事件响应器 [@A-kirami](https://github.com/A-kirami) ([#1444](https://github.com/nonebot/nonebot2/pull/1444))\n\n### 🐛 Bug 修复\n\n- Fix: 屏蔽 fastapi 0.89.0 [@yanyongyu](https://github.com/yanyongyu) ([#1574](https://github.com/nonebot/nonebot2/pull/1574))\n- Fix: 修复子插件加载失败时没有从父插件中移除的问题 [@A-kirami](https://github.com/A-kirami) ([#1559](https://github.com/nonebot/nonebot2/pull/1559))\n- Fix: 修复客户端请求未处理 cookies [@yanyongyu](https://github.com/yanyongyu) ([#1491](https://github.com/nonebot/nonebot2/pull/1491))\n- Fix: `on_type` typing error [@yanyongyu](https://github.com/yanyongyu) ([#1482](https://github.com/nonebot/nonebot2/pull/1482))\n- Fix: 修复 ArgumentParser 错误信息叠加问题 [@yanyongyu](https://github.com/yanyongyu) ([#1426](https://github.com/nonebot/nonebot2/pull/1426))\n\n### 📝 文档\n\n- Docs: 修改更新部分文档 [@yanyongyu](https://github.com/yanyongyu) ([#1615](https://github.com/nonebot/nonebot2/pull/1615))\n- Docs: 商店搜索大小写不敏感 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#1609](https://github.com/nonebot/nonebot2/pull/1609))\n- Docs: 更新测试文档中的连接方式\\&细化插件发布描述 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#1504](https://github.com/nonebot/nonebot2/pull/1504))\n- Docs: 修复文档中部分超链接跳转到 `/store.html` 的问题 [@yzyyz1387](https://github.com/yzyyz1387) ([#1470](https://github.com/nonebot/nonebot2/pull/1470))\n- Fix: 补充 `params` 模块的类型注解 [@A-kirami](https://github.com/A-kirami) ([#1458](https://github.com/nonebot/nonebot2/pull/1458))\n- Docs: 移除文档 `自定义日志` 中多余的符号 [@A-kirami](https://github.com/A-kirami) ([#1448](https://github.com/nonebot/nonebot2/pull/1448))\n- Docs: 完善 `调用平台 API` 部分 [@A-kirami](https://github.com/A-kirami) ([#1447](https://github.com/nonebot/nonebot2/pull/1447))\n- Docs: 修正文档中部分配置文件示例的符号误用 [@MingxuanGame](https://github.com/MingxuanGame) ([#1432](https://github.com/nonebot/nonebot2/pull/1432))\n\n### 💫 杂项\n\n- Plugin: 移除 nonebot-plugin-puppet [@j1g5awi](https://github.com/j1g5awi) ([#1605](https://github.com/nonebot/nonebot2/pull/1605))\n- Plugin: 更新 MC 的插件信息 [@nikissXI](https://github.com/nikissXI) ([#1589](https://github.com/nonebot/nonebot2/pull/1589))\n- Plugin: 移除 `nonebot-plugin-aidraw` [@A-kirami](https://github.com/A-kirami) ([#1588](https://github.com/nonebot/nonebot2/pull/1588))\n- Plugins: 更新 ayaka_games 插件名和描述 [@bridgeL](https://github.com/bridgeL) ([#1586](https://github.com/nonebot/nonebot2/pull/1586))\n- Plugin: 更新 tts_gal 插件名和描述 [@dpm12345](https://github.com/dpm12345) ([#1581](https://github.com/nonebot/nonebot2/pull/1581))\n- Plugin: 移除 `nonebot_plugin_super_resolution` [@A-kirami](https://github.com/A-kirami) ([#1561](https://github.com/nonebot/nonebot2/pull/1561))\n- Plugin: 更新 OlivOS.nb2 import 包名 [@j1g5awi](https://github.com/j1g5awi) ([#1560](https://github.com/nonebot/nonebot2/pull/1560))\n- Develop: 添加 pyright 环境配置 [@yanyongyu](https://github.com/yanyongyu) ([#1554](https://github.com/nonebot/nonebot2/pull/1554))\n- CI: 优化触发条件减少无效运行 [@he0119](https://github.com/he0119) ([#1545](https://github.com/nonebot/nonebot2/pull/1545))\n- Plugin: 删除 ayaka_who_is_suspect 插件 [@bridgeL](https://github.com/bridgeL) ([#1525](https://github.com/nonebot/nonebot2/pull/1525))\n- Fix: 修复异常在 traceback 中无法正常显示信息 [@he0119](https://github.com/he0119) ([#1521](https://github.com/nonebot/nonebot2/pull/1521))\n- CI: 添加插件加载测试 [@he0119](https://github.com/he0119) ([#1519](https://github.com/nonebot/nonebot2/pull/1519))\n- Plugin: 移除 `nonebot-plugin-filehost` [@mnixry](https://github.com/mnixry) ([#1516](https://github.com/nonebot/nonebot2/pull/1516))\n- Plugin: 更新 `abstain_diary` 插件名和描述 [@Ikaros-521](https://github.com/Ikaros-521) ([#1509](https://github.com/nonebot/nonebot2/pull/1509))\n- Plugin: 更新 gpt3 插件模块名 [@chrisyy2003](https://github.com/chrisyy2003) ([#1501](https://github.com/nonebot/nonebot2/pull/1501))\n- Plugin: 更新 随机禁言 插件功能描述 [@Ikaros-521](https://github.com/Ikaros-521) ([#1495](https://github.com/nonebot/nonebot2/pull/1495))\n- Plugin: 更新 multi chatgpt 插件仓库地址 [@chrisyy2003](https://github.com/chrisyy2003) ([#1487](https://github.com/nonebot/nonebot2/pull/1487))\n- Plugin: 更新 ayaka_games 介绍 [@bridgeL](https://github.com/bridgeL) ([#1431](https://github.com/nonebot/nonebot2/pull/1431))\n- Plugin: 修改 novelai send magiadice 插件模块名 [@sena-nana](https://github.com/sena-nana) ([#1423](https://github.com/nonebot/nonebot2/pull/1423))\n\n### 🍻 插件发布\n\n- Plugin: 反向词典 [@yanyongyu](https://github.com/yanyongyu) ([#1619](https://github.com/nonebot/nonebot2/pull/1619))\n- Plugin: PicMCStat [@yanyongyu](https://github.com/yanyongyu) ([#1614](https://github.com/nonebot/nonebot2/pull/1614))\n- Plugin: 犯人在跳舞 [@yanyongyu](https://github.com/yanyongyu) ([#1608](https://github.com/nonebot/nonebot2/pull/1608))\n- Plugin: 喵喵自记菜谱 [@yanyongyu](https://github.com/yanyongyu) ([#1599](https://github.com/nonebot/nonebot2/pull/1599))\n- Plugin: 语音功能 [@yanyongyu](https://github.com/yanyongyu) ([#1597](https://github.com/nonebot/nonebot2/pull/1597))\n- Plugin: OrangeDice! [@yanyongyu](https://github.com/yanyongyu) ([#1595](https://github.com/nonebot/nonebot2/pull/1595))\n- Plugin: 简易谷歌翻译插件 [@yanyongyu](https://github.com/yanyongyu) ([#1593](https://github.com/nonebot/nonebot2/pull/1593))\n- Plugin: 哔哩哔哩 q 群登录 [@yanyongyu](https://github.com/yanyongyu) ([#1591](https://github.com/nonebot/nonebot2/pull/1591))\n- Plugin: 原神实时公告 [@yanyongyu](https://github.com/yanyongyu) ([#1585](https://github.com/nonebot/nonebot2/pull/1585))\n- Plugin: 心灵鸡汤 [@yanyongyu](https://github.com/yanyongyu) ([#1580](https://github.com/nonebot/nonebot2/pull/1580))\n- Plugin: Bing 每日图片获取 [@yanyongyu](https://github.com/yanyongyu) ([#1577](https://github.com/nonebot/nonebot2/pull/1577))\n- Plugin: 星座运势 [@yanyongyu](https://github.com/yanyongyu) ([#1572](https://github.com/nonebot/nonebot2/pull/1572))\n- Plugin: 回声洞 [@yanyongyu](https://github.com/yanyongyu) ([#1573](https://github.com/nonebot/nonebot2/pull/1573))\n- Plugin: 整点报时 [@yanyongyu](https://github.com/yanyongyu) ([#1569](https://github.com/nonebot/nonebot2/pull/1569))\n- Plugin: Hypixel 数据查询 [@yanyongyu](https://github.com/yanyongyu) ([#1556](https://github.com/nonebot/nonebot2/pull/1556))\n- Plugin: 查找图片出处 [@yanyongyu](https://github.com/yanyongyu) ([#1553](https://github.com/nonebot/nonebot2/pull/1553))\n- Plugin: 云签到 [@yanyongyu](https://github.com/yanyongyu) ([#1551](https://github.com/nonebot/nonebot2/pull/1551))\n- Plugin: 图像标注 [@yanyongyu](https://github.com/yanyongyu) ([#1550](https://github.com/nonebot/nonebot2/pull/1550))\n- Plugin: 对对联 [@yanyongyu](https://github.com/yanyongyu) ([#1542](https://github.com/nonebot/nonebot2/pull/1542))\n- Plugin: 群聊学习 [@yanyongyu](https://github.com/yanyongyu) ([#1540](https://github.com/nonebot/nonebot2/pull/1540))\n- Plugin: 求生之路 2——服务器操作 [@yanyongyu](https://github.com/yanyongyu) ([#1538](https://github.com/nonebot/nonebot2/pull/1538))\n- Plugin: setu_customization [@yanyongyu](https://github.com/yanyongyu) ([#1537](https://github.com/nonebot/nonebot2/pull/1537))\n- Plugin: 主动消息撤回 [@yanyongyu](https://github.com/yanyongyu) ([#1536](https://github.com/nonebot/nonebot2/pull/1536))\n- Plugin: HttpCat🐱 猫猫 http 状态码 [@yanyongyu](https://github.com/yanyongyu) ([#1529](https://github.com/nonebot/nonebot2/pull/1529))\n- Plugin: 命令探查 [@yanyongyu](https://github.com/yanyongyu) ([#1524](https://github.com/nonebot/nonebot2/pull/1524))\n- Plugin: AnimalVoice_Convert [@yanyongyu](https://github.com/yanyongyu) ([#1518](https://github.com/nonebot/nonebot2/pull/1518))\n- Plugin: 服务状态查询 [@yanyongyu](https://github.com/yanyongyu) ([#1513](https://github.com/nonebot/nonebot2/pull/1513))\n- Plugin: 腾讯云图像变换 [@yanyongyu](https://github.com/yanyongyu) ([#1515](https://github.com/nonebot/nonebot2/pull/1515))\n- Plugin: Ping [@yanyongyu](https://github.com/yanyongyu) ([#1508](https://github.com/nonebot/nonebot2/pull/1508))\n- Plugin: 群友召唤术 [@yanyongyu](https://github.com/yanyongyu) ([#1503](https://github.com/nonebot/nonebot2/pull/1503))\n- Plugin: 战地群聊天插件 [@yanyongyu](https://github.com/yanyongyu) ([#1506](https://github.com/nonebot/nonebot2/pull/1506))\n- Plugin: 不要复读 [@yanyongyu](https://github.com/yanyongyu) ([#1500](https://github.com/nonebot/nonebot2/pull/1500))\n- Plugin: JAVA MC 服务器信息查询 [@yanyongyu](https://github.com/yanyongyu) ([#1497](https://github.com/nonebot/nonebot2/pull/1497))\n- Plugin: 防撤回 [@yanyongyu](https://github.com/yanyongyu) ([#1489](https://github.com/nonebot/nonebot2/pull/1489))\n- Plugin: 随机禁言 [@yanyongyu](https://github.com/yanyongyu) ([#1486](https://github.com/nonebot/nonebot2/pull/1486))\n- Plugin: 只因进化录 [@yanyongyu](https://github.com/yanyongyu) ([#1484](https://github.com/nonebot/nonebot2/pull/1484))\n- Plugin: GPT3 [@yanyongyu](https://github.com/yanyongyu) ([#1480](https://github.com/nonebot/nonebot2/pull/1480))\n- Plugin: 熊老板 [@yanyongyu](https://github.com/yanyongyu) ([#1472](https://github.com/nonebot/nonebot2/pull/1472))\n- Plugin: QQ 群文件备份 [@yanyongyu](https://github.com/yanyongyu) ([#1478](https://github.com/nonebot/nonebot2/pull/1478))\n- Plugin: 戒色打卡日记 [@yanyongyu](https://github.com/yanyongyu) ([#1475](https://github.com/nonebot/nonebot2/pull/1475))\n- Plugin: nonebot_plugin_idiom [@yanyongyu](https://github.com/yanyongyu) ([#1469](https://github.com/nonebot/nonebot2/pull/1469))\n- Plugin: 随机配色方案 [@yanyongyu](https://github.com/yanyongyu) ([#1466](https://github.com/nonebot/nonebot2/pull/1466))\n- Plugin: multi-ChatGPT [@yanyongyu](https://github.com/yanyongyu) ([#1462](https://github.com/nonebot/nonebot2/pull/1462))\n- Plugin: 权限控制 [@yanyongyu](https://github.com/yanyongyu) ([#1464](https://github.com/nonebot/nonebot2/pull/1464))\n- Plugin: 汇率换算 [@yanyongyu](https://github.com/yanyongyu) ([#1452](https://github.com/nonebot/nonebot2/pull/1452))\n- Plugin: 全群广播 [@yanyongyu](https://github.com/yanyongyu) ([#1450](https://github.com/nonebot/nonebot2/pull/1450))\n- Plugin: 图片背景消除 [@yanyongyu](https://github.com/yanyongyu) ([#1446](https://github.com/nonebot/nonebot2/pull/1446))\n- Plugin: 雀魂信息查询 [@yanyongyu](https://github.com/yanyongyu) ([#1443](https://github.com/nonebot/nonebot2/pull/1443))\n- Plugin: ChatGPT [@yanyongyu](https://github.com/yanyongyu) ([#1439](https://github.com/nonebot/nonebot2/pull/1439))\n- Plugin: 免费快捷点歌插件 [@yanyongyu](https://github.com/yanyongyu) ([#1436](https://github.com/nonebot/nonebot2/pull/1436))\n- Plugin: 动画截图追溯来源 [@yanyongyu](https://github.com/yanyongyu) ([#1434](https://github.com/nonebot/nonebot2/pull/1434))\n- Plugin: b 站图片下载 [@yanyongyu](https://github.com/yanyongyu) ([#1430](https://github.com/nonebot/nonebot2/pull/1430))\n- Plugin: 记事本 [@yanyongyu](https://github.com/yanyongyu) ([#1420](https://github.com/nonebot/nonebot2/pull/1420))\n- Plugin: 原神前瞻直播兑换码查询 [@yanyongyu](https://github.com/yanyongyu) ([#1422](https://github.com/nonebot/nonebot2/pull/1422))\n\n### 🍻 机器人发布\n\n- Bot: SuzunoBot [@yanyongyu](https://github.com/yanyongyu) ([#1601](https://github.com/nonebot/nonebot2/pull/1601))\n- Bot: 辞辞(cici)Bot [@yanyongyu](https://github.com/yanyongyu) ([#1583](https://github.com/nonebot/nonebot2/pull/1583))\n- Bot: RanBot [@yanyongyu](https://github.com/yanyongyu) ([#1511](https://github.com/nonebot/nonebot2/pull/1511))\n\n### 🍻 适配器发布\n\n- Adapter: BilibiliLive [@yanyongyu](https://github.com/yanyongyu) ([#1617](https://github.com/nonebot/nonebot2/pull/1617))\n- Adapter: Spigot [@yanyongyu](https://github.com/yanyongyu) ([#1612](https://github.com/nonebot/nonebot2/pull/1612))\n\n## v2.0.0rc2\n\n### 💥 破坏性变更\n\n- Feature: 使用 `importlib.metadata` 替换 `pkg_resources` [@A-kirami](https://github.com/A-kirami) ([#1388](https://github.com/nonebot/nonebot2/pull/1388))\n\n### 🚀 新功能\n\n- Feature: 支持自定义 matchers 存储管理 [@yanyongyu](https://github.com/yanyongyu) ([#1395](https://github.com/nonebot/nonebot2/pull/1395))\n- Feature: 升级 devcontainer 配置 [@yanyongyu](https://github.com/yanyongyu) ([#1392](https://github.com/nonebot/nonebot2/pull/1392))\n- Feature: 使用 `importlib.metadata` 替换 `pkg_resources` [@A-kirami](https://github.com/A-kirami) ([#1388](https://github.com/nonebot/nonebot2/pull/1388))\n- CI: 测试环境添加 Python 3.11 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#1366](https://github.com/nonebot/nonebot2/pull/1366))\n- Feature: 新增 dotenv 嵌套配置项支持 [@yanyongyu](https://github.com/yanyongyu) ([#1324](https://github.com/nonebot/nonebot2/pull/1324))\n- Feature: 添加 State 响应器触发消息注入 [@A-kirami](https://github.com/A-kirami) ([#1315](https://github.com/nonebot/nonebot2/pull/1315))\n- Remove: 移除无用的 namespace 声明 [@yanyongyu](https://github.com/yanyongyu) ([#1306](https://github.com/nonebot/nonebot2/pull/1306))\n\n### 🐛 Bug 修复\n\n- Fix: Bot `__getattr__` 不再对 `__xxx__` 方法返回 [@synodriver](https://github.com/synodriver) ([#1398](https://github.com/nonebot/nonebot2/pull/1398))\n- Fix: 修复 run pre/post hook 没有在正确的上下文中运行 [@yanyongyu](https://github.com/yanyongyu) ([#1391](https://github.com/nonebot/nonebot2/pull/1391))\n\n### 📝 文档\n\n- Docs: 添加 ntchat 社区适配器 [@JustUndertaker](https://github.com/JustUndertaker) ([#1414](https://github.com/nonebot/nonebot2/pull/1414))\n\n### 💫 杂项\n\n- Plugin: b 站用户信息查询 [@Ikaros-521](https://github.com/Ikaros-521) ([#1410](https://github.com/nonebot/nonebot2/pull/1410))\n- Plugin: 由于 Sena-nana 项目拆分，之前的插件地址更改 [@sena-nana](https://github.com/sena-nana) ([#1378](https://github.com/nonebot/nonebot2/pull/1378))\n- Plugin: 更新 ayaka 插件的主页链接 [@bridgeL](https://github.com/bridgeL) ([#1346](https://github.com/nonebot/nonebot2/pull/1346))\n- Plugin: 补充 novelai 插件信息 [@sena-nana](https://github.com/sena-nana) ([#1333](https://github.com/nonebot/nonebot2/pull/1333))\n- Bot: 修改 Inkar Suki 描述 [@HornCopper](https://github.com/HornCopper) ([#1312](https://github.com/nonebot/nonebot2/pull/1312))\n- Plugin: 修改插件 MCQQ MCRcon 主页地址 [@17TheWord](https://github.com/17TheWord) ([#1303](https://github.com/nonebot/nonebot2/pull/1303))\n\n### 🍻 插件发布\n\n- Plugin: 谁在窥屏 [@yanyongyu](https://github.com/yanyongyu) ([#1416](https://github.com/nonebot/nonebot2/pull/1416))\n- Plugin: 免费版 NovelAI 生图插件 [@yanyongyu](https://github.com/yanyongyu) ([#1408](https://github.com/nonebot/nonebot2/pull/1408))\n- Plugin: sky 光遇 [@yanyongyu](https://github.com/yanyongyu) ([#1394](https://github.com/nonebot/nonebot2/pull/1394))\n- Plugin: Colab-NovelAI [@yanyongyu](https://github.com/yanyongyu) ([#1390](https://github.com/nonebot/nonebot2/pull/1390))\n- Plugin: b 站用户直播号、粉丝、舰团数查询 [@yanyongyu](https://github.com/yanyongyu) ([#1385](https://github.com/nonebot/nonebot2/pull/1385))\n- Plugin: 投胎模拟器 [@yanyongyu](https://github.com/yanyongyu) ([#1382](https://github.com/nonebot/nonebot2/pull/1382))\n- Plugin: Apex API Query [@yanyongyu](https://github.com/yanyongyu) ([#1375](https://github.com/nonebot/nonebot2/pull/1375))\n- Plugin: 随个人 [@yanyongyu](https://github.com/yanyongyu) ([#1373](https://github.com/nonebot/nonebot2/pull/1373))\n- Plugin: 动漫资源获取 [@yanyongyu](https://github.com/yanyongyu) ([#1371](https://github.com/nonebot/nonebot2/pull/1371))\n- Plugin: 日麻小工具 [@yanyongyu](https://github.com/yanyongyu) ([#1365](https://github.com/nonebot/nonebot2/pull/1365))\n- Plugin: 图像超分辨率增强 [@yanyongyu](https://github.com/yanyongyu) ([#1362](https://github.com/nonebot/nonebot2/pull/1362))\n- Plugin: 二次元化图像 [@yanyongyu](https://github.com/yanyongyu) ([#1360](https://github.com/nonebot/nonebot2/pull/1360))\n- Plugin: 日麻寄分器 [@yanyongyu](https://github.com/yanyongyu) ([#1357](https://github.com/nonebot/nonebot2/pull/1357))\n- Plugin: 文本生成器 [@yanyongyu](https://github.com/yanyongyu) ([#1355](https://github.com/nonebot/nonebot2/pull/1355))\n- Plugin: 反嘴臭插件 [@yanyongyu](https://github.com/yanyongyu) ([#1350](https://github.com/nonebot/nonebot2/pull/1350))\n- Plugin: 用户\\&群聊黑名单 [@yanyongyu](https://github.com/yanyongyu) ([#1348](https://github.com/nonebot/nonebot2/pull/1348))\n- Plugin: NoneBot SQLAlchemy 封装 [@yanyongyu](https://github.com/yanyongyu) ([#1345](https://github.com/nonebot/nonebot2/pull/1345))\n- Plugin: 通用抽图/语音 [@yanyongyu](https://github.com/yanyongyu) ([#1341](https://github.com/nonebot/nonebot2/pull/1341))\n- Plugin: kfcrazy [@yanyongyu](https://github.com/yanyongyu) ([#1339](https://github.com/nonebot/nonebot2/pull/1339))\n- Plugin: 二次元图像鉴赏 [@yanyongyu](https://github.com/yanyongyu) ([#1337](https://github.com/nonebot/nonebot2/pull/1337))\n- Plugin: ayaka 衍生插件 - 坏词撤回 [@yanyongyu](https://github.com/yanyongyu) ([#1335](https://github.com/nonebot/nonebot2/pull/1335))\n- Plugin: ayaka 衍生插件 - 时区助手 [@yanyongyu](https://github.com/yanyongyu) ([#1332](https://github.com/nonebot/nonebot2/pull/1332))\n- Plugin: ayaka 衍生插件 - 谁是卧底 [@yanyongyu](https://github.com/yanyongyu) ([#1330](https://github.com/nonebot/nonebot2/pull/1330))\n- Plugin: ayaka 衍生插件 - 小游戏合集 [@yanyongyu](https://github.com/yanyongyu) ([#1328](https://github.com/nonebot/nonebot2/pull/1328))\n- Plugin: bnhhsh -「不能好好说话！」 [@yanyongyu](https://github.com/yanyongyu) ([#1326](https://github.com/nonebot/nonebot2/pull/1326))\n- Plugin: AI 绘图 [@yanyongyu](https://github.com/yanyongyu) ([#1323](https://github.com/nonebot/nonebot2/pull/1323))\n- Plugin: novelai [@yanyongyu](https://github.com/yanyongyu) ([#1319](https://github.com/nonebot/nonebot2/pull/1319))\n- Plugin: 游戏王小程序查价 [@yanyongyu](https://github.com/yanyongyu) ([#1317](https://github.com/nonebot/nonebot2/pull/1317))\n- Plugin: 监测群事件 [@yanyongyu](https://github.com/yanyongyu) ([#1320](https://github.com/nonebot/nonebot2/pull/1320))\n- Plugin: 轮盘禁言小游戏 [@yanyongyu](https://github.com/yanyongyu) ([#1311](https://github.com/nonebot/nonebot2/pull/1311))\n- Plugin: 真白萌自动签到 [@yanyongyu](https://github.com/yanyongyu) ([#1308](https://github.com/nonebot/nonebot2/pull/1308))\n- Plugin: BiliRequestAll [@yanyongyu](https://github.com/yanyongyu) ([#1302](https://github.com/nonebot/nonebot2/pull/1302))\n- Plugin: 监听者 [@yanyongyu](https://github.com/yanyongyu) ([#1299](https://github.com/nonebot/nonebot2/pull/1299))\n\n### 🍻 机器人发布\n\n- Bot: Bread Dog Bot [@yanyongyu](https://github.com/yanyongyu) ([#1380](https://github.com/nonebot/nonebot2/pull/1380))\n- Bot: hsbot [@yanyongyu](https://github.com/yanyongyu) ([#1369](https://github.com/nonebot/nonebot2/pull/1369))\n\n### 🍻 适配器发布\n\n- Adapter: Ntchat [@yanyongyu](https://github.com/yanyongyu) ([#1314](https://github.com/nonebot/nonebot2/pull/1314))\n\n## v2.0.0-rc.1\n\n### 💥 破坏性变更\n\n- Feature: `SUPERUSER` 权限匹配任意超管事件 [@AkiraXie](https://github.com/AkiraXie) ([#1275](https://github.com/nonebot/nonebot2/pull/1275))\n- Remove: 移除过时的 State 注入参数 [@yanyongyu](https://github.com/yanyongyu) ([#1160](https://github.com/nonebot/nonebot2/pull/1160))\n- Remove: 移除过时的 `nonebot.plugins` toml 配置 [@yanyongyu](https://github.com/yanyongyu) ([#1151](https://github.com/nonebot/nonebot2/pull/1151))\n- Remove: 移除 Python 3.7 支持 [@yanyongyu](https://github.com/yanyongyu) ([#1148](https://github.com/nonebot/nonebot2/pull/1148))\n- Remove: 删除过时的 Export 功能 [@yanyongyu](https://github.com/yanyongyu) ([#1125](https://github.com/nonebot/nonebot2/pull/1125))\n\n### 🚀 新功能\n\n- Feature: `SUPERUSER` 权限匹配任意超管事件 [@AkiraXie](https://github.com/AkiraXie) ([#1275](https://github.com/nonebot/nonebot2/pull/1275))\n- Feature: 改进 `CommandGroup` 与 `MatcherGroup` 的结构 [@A-kirami](https://github.com/A-kirami) ([#1240](https://github.com/nonebot/nonebot2/pull/1240))\n- Feature: 调整日志输出格式与等级 [@yanyongyu](https://github.com/yanyongyu) ([#1233](https://github.com/nonebot/nonebot2/pull/1233))\n- Feature: 优化依赖注入结构 [@yanyongyu](https://github.com/yanyongyu) ([#1227](https://github.com/nonebot/nonebot2/pull/1227))\n- Featue: `load_plugin` 支持 `pathlib.Path` [@Lancercmd](https://github.com/Lancercmd) ([#1194](https://github.com/nonebot/nonebot2/pull/1194))\n- Feature: 新增事件类型过滤 rule [@yanyongyu](https://github.com/yanyongyu) ([#1183](https://github.com/nonebot/nonebot2/pull/1183))\n- Feature: shell command 添加富文本支持 [@yanyongyu](https://github.com/yanyongyu) ([#1171](https://github.com/nonebot/nonebot2/pull/1171))\n\n### 🐛 Bug 修复\n\n- Fix: 内置规则和权限没有捕获错误 [@yanyongyu](https://github.com/yanyongyu) ([#1291](https://github.com/nonebot/nonebot2/pull/1291))\n- Fix: 修复 User 会话权限更新嵌套问题 [@yanyongyu](https://github.com/yanyongyu) ([#1208](https://github.com/nonebot/nonebot2/pull/1208))\n- Fix: 修复当消息与不支持的类型相加时抛出的异常类型错误 [@mnixry](https://github.com/mnixry) ([#1166](https://github.com/nonebot/nonebot2/pull/1166))\n\n### 💫 杂项\n\n- Fix: 修正 GenshinUID 的发布类型 [@A-kirami](https://github.com/A-kirami) ([#1243](https://github.com/nonebot/nonebot2/pull/1243))\n- Remove: 移除未使用的导入 [@A-kirami](https://github.com/A-kirami) ([#1236](https://github.com/nonebot/nonebot2/pull/1236))\n- Plugin: 更新插件米游社辅助工具 tag [@Ljzd-PRO](https://github.com/Ljzd-PRO) ([#1221](https://github.com/nonebot/nonebot2/pull/1221))\n- Plugin: 修改插件多功能简易群管信息 [@HuYihe2008](https://github.com/HuYihe2008) ([#1180](https://github.com/nonebot/nonebot2/pull/1180))\n- Plugin: 修改插件多功能简易群管信息 [@HuYihe2008](https://github.com/HuYihe2008) ([#1159](https://github.com/nonebot/nonebot2/pull/1159))\n- Plugin: 修改 QQ 续火花插件信息 [@GC-ZF](https://github.com/GC-ZF) ([#1158](https://github.com/nonebot/nonebot2/pull/1158))\n- Plugin: 修改插件多功能简易群管信息 [@HuYihe2008](https://github.com/HuYihe2008) ([#1154](https://github.com/nonebot/nonebot2/pull/1154))\n\n### 🍻 插件发布\n\n- Plugin: 文字识别 [@yanyongyu](https://github.com/yanyongyu) ([#1295](https://github.com/nonebot/nonebot2/pull/1295))\n- Plugin: 在线编曲 [@yanyongyu](https://github.com/yanyongyu) ([#1293](https://github.com/nonebot/nonebot2/pull/1293))\n- Plugin: 图灵机器人 [@yanyongyu](https://github.com/yanyongyu) ([#1289](https://github.com/nonebot/nonebot2/pull/1289))\n- Plugin: PicStatus [@yanyongyu](https://github.com/yanyongyu) ([#1287](https://github.com/nonebot/nonebot2/pull/1287))\n- Plugin: 阿里云盘福利码自动兑换 [@yanyongyu](https://github.com/yanyongyu) ([#1283](https://github.com/nonebot/nonebot2/pull/1283))\n- Plugin: gal 角色语音生成 [@yanyongyu](https://github.com/yanyongyu) ([#1281](https://github.com/nonebot/nonebot2/pull/1281))\n- Plugin: 漂流瓶 [@yanyongyu](https://github.com/yanyongyu) ([#1279](https://github.com/nonebot/nonebot2/pull/1279))\n- Plugin: BWIKI 助手移植版 [@yanyongyu](https://github.com/yanyongyu) ([#1274](https://github.com/nonebot/nonebot2/pull/1274))\n- Plugin: nonebot 物联网插件 [@yanyongyu](https://github.com/yanyongyu) ([#1265](https://github.com/nonebot/nonebot2/pull/1265))\n- Plugin: 狼人杀插件 [@yanyongyu](https://github.com/yanyongyu) ([#1252](https://github.com/nonebot/nonebot2/pull/1252))\n- Plugin: ayaka - 文字游戏开发辅助插件 [@yanyongyu](https://github.com/yanyongyu) ([#1254](https://github.com/nonebot/nonebot2/pull/1254))\n- Plugin: 图像超分辨率重建 [@yanyongyu](https://github.com/yanyongyu) ([#1250](https://github.com/nonebot/nonebot2/pull/1250))\n- Plugin: Minecraft Server 聊天同步 [@yanyongyu](https://github.com/yanyongyu) ([#1245](https://github.com/nonebot/nonebot2/pull/1245))\n- Plugin: 查询 ETH 合并日期 [@yanyongyu](https://github.com/yanyongyu) ([#1232](https://github.com/nonebot/nonebot2/pull/1232))\n- Plugin: 星际战甲事件查询 [@yanyongyu](https://github.com/yanyongyu) ([#1220](https://github.com/nonebot/nonebot2/pull/1220))\n- Plugin: 米游社辅助工具 [@yanyongyu](https://github.com/yanyongyu) ([#1218](https://github.com/nonebot/nonebot2/pull/1218))\n- Plugin: 原神每日材料查询 [@yanyongyu](https://github.com/yanyongyu) ([#1216](https://github.com/nonebot/nonebot2/pull/1216))\n- Plugin: MC_QQ_MCRcon [@yanyongyu](https://github.com/yanyongyu) ([#1211](https://github.com/nonebot/nonebot2/pull/1211))\n- Plugin: 原神角色展柜查询 [@yanyongyu](https://github.com/yanyongyu) ([#1209](https://github.com/nonebot/nonebot2/pull/1209))\n- Plugin: 修仙模拟器 [@yanyongyu](https://github.com/yanyongyu) ([#1202](https://github.com/nonebot/nonebot2/pull/1202))\n- Plugin: 赛博浅草寺 [@yanyongyu](https://github.com/yanyongyu) ([#1206](https://github.com/nonebot/nonebot2/pull/1206))\n- Plugin: 不背单词 [@yanyongyu](https://github.com/yanyongyu) ([#1204](https://github.com/nonebot/nonebot2/pull/1204))\n- Plugin: 自识别 todo [@yanyongyu](https://github.com/yanyongyu) ([#1193](https://github.com/nonebot/nonebot2/pull/1193))\n- Plugin: 雨课堂自动签到 [@yanyongyu](https://github.com/yanyongyu) ([#1189](https://github.com/nonebot/nonebot2/pull/1189))\n- Plugin: 反馈及通知 [@yanyongyu](https://github.com/yanyongyu) ([#1187](https://github.com/nonebot/nonebot2/pull/1187))\n- Plugin: MagiaDice 骰娘及 TRPGLOG [@yanyongyu](https://github.com/yanyongyu) ([#1185](https://github.com/nonebot/nonebot2/pull/1185))\n- Plugin: 面麻小助手 [@yanyongyu](https://github.com/yanyongyu) ([#1191](https://github.com/nonebot/nonebot2/pull/1191))\n- Plugin: 话痨排行榜 [@yanyongyu](https://github.com/yanyongyu) ([#1182](https://github.com/nonebot/nonebot2/pull/1182))\n- Plugin: 保存群聊闪照 [@yanyongyu](https://github.com/yanyongyu) ([#1179](https://github.com/nonebot/nonebot2/pull/1179))\n- Plugin: 课表查询 [@yanyongyu](https://github.com/yanyongyu) ([#1168](https://github.com/nonebot/nonebot2/pull/1168))\n- Plugin: 业余无线电助手 [@yanyongyu](https://github.com/yanyongyu) ([#1173](https://github.com/nonebot/nonebot2/pull/1173))\n- Plugin: NoneBot 树形帮助插件 [@yanyongyu](https://github.com/yanyongyu) ([#1177](https://github.com/nonebot/nonebot2/pull/1177))\n- Plugin: 工作性价比 [@yanyongyu](https://github.com/yanyongyu) ([#1175](https://github.com/nonebot/nonebot2/pull/1175))\n- Plugin: 娶群友 [@yanyongyu](https://github.com/yanyongyu) ([#1170](https://github.com/nonebot/nonebot2/pull/1170))\n- Plugin: PixivBot [@yanyongyu](https://github.com/yanyongyu) ([#1165](https://github.com/nonebot/nonebot2/pull/1165))\n- Plugin: 日韩中 VITS 模型原神拟声 [@yanyongyu](https://github.com/yanyongyu) ([#1162](https://github.com/nonebot/nonebot2/pull/1162))\n- Plugin: 每日人品 [@yanyongyu](https://github.com/yanyongyu) ([#1156](https://github.com/nonebot/nonebot2/pull/1156))\n- Plugin: nonebot-plugin-drawer [@yanyongyu](https://github.com/yanyongyu) ([#1146](https://github.com/nonebot/nonebot2/pull/1146))\n- Plugin: 小游戏合集 [@yanyongyu](https://github.com/yanyongyu) ([#1150](https://github.com/nonebot/nonebot2/pull/1150))\n- Plugin: 简易群管（带入群欢迎） [@yanyongyu](https://github.com/yanyongyu) ([#1142](https://github.com/nonebot/nonebot2/pull/1142))\n- Plugin: wiki 条目搜索、获取简介 [@yanyongyu](https://github.com/yanyongyu) ([#1133](https://github.com/nonebot/nonebot2/pull/1133))\n- Plugin: bangumi 搜索 [@yanyongyu](https://github.com/yanyongyu) ([#1137](https://github.com/nonebot/nonebot2/pull/1137))\n- Plugin: 疫情小助手-频道版 [@yanyongyu](https://github.com/yanyongyu) ([#1131](https://github.com/nonebot/nonebot2/pull/1131))\n- Plugin: MC_QQ 通信 [@yanyongyu](https://github.com/yanyongyu) ([#1127](https://github.com/nonebot/nonebot2/pull/1127))\n- Plugin: BAWiki [@yanyongyu](https://github.com/yanyongyu) ([#1129](https://github.com/nonebot/nonebot2/pull/1129))\n\n### 🍻 机器人发布\n\n- Bot: IdhagnBot [@yanyongyu](https://github.com/yanyongyu) ([#1267](https://github.com/nonebot/nonebot2/pull/1267))\n- Bot: LittlePaimon [@yanyongyu](https://github.com/yanyongyu) ([#1256](https://github.com/nonebot/nonebot2/pull/1256))\n- Bot: GenshinUID [@yanyongyu](https://github.com/yanyongyu) ([#1226](https://github.com/nonebot/nonebot2/pull/1226))\n- Bot: 小白机器人 [@yanyongyu](https://github.com/yanyongyu) ([#1224](https://github.com/nonebot/nonebot2/pull/1224))\n\n### 🍻 适配器发布\n\n- Adapter: GitHub [@yanyongyu](https://github.com/yanyongyu) ([#1297](https://github.com/nonebot/nonebot2/pull/1297))\n- Adapter: Console [@yanyongyu](https://github.com/yanyongyu) ([#1213](https://github.com/nonebot/nonebot2/pull/1213))\n\n## v2.0.0-beta.5\n\n### 🚀 新功能\n\n- Feature: on_x 支持 expire_time 参数 [@Dobiichi-Origami](https://github.com/Dobiichi-Origami) ([#1106](https://github.com/nonebot/nonebot2/pull/1106))\n- Feature: 正向驱动器 startup/shutdown hook 支持同步函数 [@synodriver](https://github.com/synodriver) ([#1104](https://github.com/nonebot/nonebot2/pull/1104))\n\n### 🐛 Bug 修复\n\n- Fix: 修复插件父子关系识别错漏 [@yanyongyu](https://github.com/yanyongyu) ([#1121](https://github.com/nonebot/nonebot2/pull/1121))\n- Fix: run post hook 应该处理 matcher.state [@AkiraXie](https://github.com/AkiraXie) ([#1119](https://github.com/nonebot/nonebot2/pull/1119))\n- Fix: 修复 setuptools 未安装导致 ImportError [@yanyongyu](https://github.com/yanyongyu) ([#1116](https://github.com/nonebot/nonebot2/pull/1116))\n- Fix: 修复 typing 中 T_RunPostProcessor 类型的注释描述不正确 [@A-kirami](https://github.com/A-kirami) ([#1057](https://github.com/nonebot/nonebot2/pull/1057))\n\n### 📝 文档\n\n- Docs: 添加 nonemoji 并更新开发指南 [@yanyongyu](https://github.com/yanyongyu) ([#1088](https://github.com/nonebot/nonebot2/pull/1088))\n- Docs: 修复 event message 类型注释错误 [@yanyongyu](https://github.com/yanyongyu) ([#1079](https://github.com/nonebot/nonebot2/pull/1079))\n- Docs: 修复旧 Vuepress 文档缓存问题 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#1077](https://github.com/nonebot/nonebot2/pull/1077))\n- Docs: 更新 Readme 贡献图片 [@yanyongyu](https://github.com/yanyongyu) ([#1074](https://github.com/nonebot/nonebot2/pull/1074))\n- Docs: 注销旧 Vuepress 文档的 Service Worker [@StarHeartHunt](https://github.com/StarHeartHunt) ([#1073](https://github.com/nonebot/nonebot2/pull/1073))\n- Docs: 修改 `权限控制` 一节中主动调用的错误 [@MingxuanGame](https://github.com/MingxuanGame) ([#1072](https://github.com/nonebot/nonebot2/pull/1072))\n\n### 💫 杂项\n\n- Bot: 修改剑网三 bot 信息 [@JustUndertaker](https://github.com/JustUndertaker) ([#1107](https://github.com/nonebot/nonebot2/pull/1107))\n\n### 🍻 插件发布\n\n- Plugin: 「能不能好好说话？」缩写翻译 [@yanyongyu](https://github.com/yanyongyu) ([#1118](https://github.com/nonebot/nonebot2/pull/1118))\n- Plugin: 推送钩子 [@yanyongyu](https://github.com/yanyongyu) ([#1115](https://github.com/nonebot/nonebot2/pull/1115))\n- Plugin: 易命令 [@yanyongyu](https://github.com/yanyongyu) ([#1111](https://github.com/nonebot/nonebot2/pull/1111))\n- Plugin: 群昵称时间 [@yanyongyu](https://github.com/yanyongyu) ([#1109](https://github.com/nonebot/nonebot2/pull/1109))\n- Plugin: 处理好友添加和群邀请 [@yanyongyu](https://github.com/yanyongyu) ([#1099](https://github.com/nonebot/nonebot2/pull/1099))\n- Plugin: 明日方舟寻访记录分析 [@yanyongyu](https://github.com/yanyongyu) ([#1097](https://github.com/nonebot/nonebot2/pull/1097))\n- Plugin: b 站视频每日推送 [@yanyongyu](https://github.com/yanyongyu) ([#1095](https://github.com/nonebot/nonebot2/pull/1095))\n- Plugin: 自动回复（文 i）插件 [@yanyongyu](https://github.com/yanyongyu) ([#1090](https://github.com/nonebot/nonebot2/pull/1090))\n- Plugin: ACC 计算工具 [@yanyongyu](https://github.com/yanyongyu) ([#1093](https://github.com/nonebot/nonebot2/pull/1093))\n- Plugin: OSU 查分插件 [@yanyongyu](https://github.com/yanyongyu) ([#1082](https://github.com/nonebot/nonebot2/pull/1082))\n- Plugin: 战地 1、5 战绩查询工具 [@yanyongyu](https://github.com/yanyongyu) ([#1087](https://github.com/nonebot/nonebot2/pull/1087))\n- Plugin: 一起燚 xN 吧 [@yanyongyu](https://github.com/yanyongyu) ([#1085](https://github.com/nonebot/nonebot2/pull/1085))\n- Plugin: 米游币商品自动兑换 [@yanyongyu](https://github.com/yanyongyu) ([#1076](https://github.com/nonebot/nonebot2/pull/1076))\n- Plugin: 赛马 [@yanyongyu](https://github.com/yanyongyu) ([#1069](https://github.com/nonebot/nonebot2/pull/1069))\n- Plugin: PicMenu [@yanyongyu](https://github.com/yanyongyu) ([#1071](https://github.com/nonebot/nonebot2/pull/1071))\n- Plugin: nonebot-plugin-bread [@yanyongyu](https://github.com/yanyongyu) ([#1064](https://github.com/nonebot/nonebot2/pull/1064))\n- Plugin: 黑白名单 [@yanyongyu](https://github.com/yanyongyu) ([#1061](https://github.com/nonebot/nonebot2/pull/1061))\n- Plugin: BitTorrent [@yanyongyu](https://github.com/yanyongyu) ([#1059](https://github.com/nonebot/nonebot2/pull/1059))\n\n### 🍻 机器人发布\n\n- Bot: SkadiBot [@yanyongyu](https://github.com/yanyongyu) ([#1113](https://github.com/nonebot/nonebot2/pull/1113))\n- Bot: 真宵 Bot [@yanyongyu](https://github.com/yanyongyu) ([#1103](https://github.com/nonebot/nonebot2/pull/1103))\n\n## v2.0.0-beta.4\n\n### 🚀 新功能\n\n- Feature: 添加插件元信息定义 [@yanyongyu](https://github.com/yanyongyu) ([#1046](https://github.com/nonebot/nonebot2/pull/1046))\n- Feature: 日志记录自动检测终端是否支持彩色 [@BlueGlassBlock](https://github.com/BlueGlassBlock) ([#1034](https://github.com/nonebot/nonebot2/pull/1034))\n- Feature: 优化插件加载内部逻辑 [@yanyongyu](https://github.com/yanyongyu) ([#1011](https://github.com/nonebot/nonebot2/pull/1011))\n\n### 🐛 Bug 修复\n\n- Fix: 修复 MessageSegment 在有额外数据时报错 [@yanyongyu](https://github.com/yanyongyu) ([#1055](https://github.com/nonebot/nonebot2/pull/1055))\n- Fix: 修复环境变量无法覆盖 dotenv 内配置项值 [@yanyongyu](https://github.com/yanyongyu) ([#1052](https://github.com/nonebot/nonebot2/pull/1052))\n- Fix: 修复依赖注入 bot event 参数 union 校验失败 [@yanyongyu](https://github.com/yanyongyu) ([#1001](https://github.com/nonebot/nonebot2/pull/1001))\n\n### 📝 文档\n\n- Docs：添加文档排版规范 [@j1g5awi](https://github.com/j1g5awi) ([#1005](https://github.com/nonebot/nonebot2/pull/1005))\n- Docs: 更新 require 样例 [@yanyongyu](https://github.com/yanyongyu) ([#996](https://github.com/nonebot/nonebot2/pull/996))\n- Docs: 更新 README 中的 QQ 频道图标 [@mnixry](https://github.com/mnixry) ([#997](https://github.com/nonebot/nonebot2/pull/997))\n- Docs: 调整跨插件访问文档 [@AkiraXie](https://github.com/AkiraXie) ([#993](https://github.com/nonebot/nonebot2/pull/993))\n\n### 🍻 插件发布\n\n- Plugin: 历史上的今天 [@yanyongyu](https://github.com/yanyongyu) ([#1049](https://github.com/nonebot/nonebot2/pull/1049))\n- Plugin: smart_reply [@yanyongyu](https://github.com/yanyongyu) ([#1054](https://github.com/nonebot/nonebot2/pull/1054))\n- Plugin: nonebot_plugin_setu4 [@yanyongyu](https://github.com/yanyongyu) ([#1051](https://github.com/nonebot/nonebot2/pull/1051))\n- Plugin: 命令重启机器人 [@yanyongyu](https://github.com/yanyongyu) ([#1038](https://github.com/nonebot/nonebot2/pull/1038))\n- Plugin: 青年大学习自动提交 [@yanyongyu](https://github.com/yanyongyu) ([#1036](https://github.com/nonebot/nonebot2/pull/1036))\n- Plugin: 疫情小助手 [@yanyongyu](https://github.com/yanyongyu) ([#1033](https://github.com/nonebot/nonebot2/pull/1033))\n- Plugin: 谁艾特我了 [@yanyongyu](https://github.com/yanyongyu) ([#1031](https://github.com/nonebot/nonebot2/pull/1031))\n- Plugin: Hikari-战舰世界水表查询 [@yanyongyu](https://github.com/yanyongyu) ([#1025](https://github.com/nonebot/nonebot2/pull/1025))\n- Plugin: Warframe 时间查询 [@yanyongyu](https://github.com/yanyongyu) ([#1023](https://github.com/nonebot/nonebot2/pull/1023))\n- Plugin: imagetools [@yanyongyu](https://github.com/yanyongyu) ([#1021](https://github.com/nonebot/nonebot2/pull/1021))\n- Plugin: 明日方舟工具箱 [@yanyongyu](https://github.com/yanyongyu) ([#1019](https://github.com/nonebot/nonebot2/pull/1019))\n- Plugin: B 站视频伪分享卡片 [@yanyongyu](https://github.com/yanyongyu) ([#1014](https://github.com/nonebot/nonebot2/pull/1014))\n- Plugin: TETRIS Stats [@yanyongyu](https://github.com/yanyongyu) ([#1009](https://github.com/nonebot/nonebot2/pull/1009))\n- Plugin: 签到插件 [@yanyongyu](https://github.com/yanyongyu) ([#1007](https://github.com/nonebot/nonebot2/pull/1007))\n- Plugin: 数据库连接插件 [@yanyongyu](https://github.com/yanyongyu) ([#995](https://github.com/nonebot/nonebot2/pull/995))\n- Plugin: 百度翻译 [@yanyongyu](https://github.com/yanyongyu) ([#992](https://github.com/nonebot/nonebot2/pull/992))\n- Plugin: MockingBird 语音 [@yanyongyu](https://github.com/yanyongyu) ([#989](https://github.com/nonebot/nonebot2/pull/989))\n\n### 🍻 机器人发布\n\n- Bot: nya_bot [@yanyongyu](https://github.com/yanyongyu) ([#1045](https://github.com/nonebot/nonebot2/pull/1045))\n- Bot: LiteyukiBot-轻雪机器人 [@yanyongyu](https://github.com/yanyongyu) ([#1003](https://github.com/nonebot/nonebot2/pull/1003))\n\n### 🍻 适配器发布\n\n- Adapter: OneBot V12 [@yanyongyu](https://github.com/yanyongyu) ([#1027](https://github.com/nonebot/nonebot2/pull/1027))\n\n## v2.0.0-beta.3\n\n### 💥 破坏性变更\n\n- Fix: 添加 export 方法 Deprecation 警告 [@yanyongyu](https://github.com/yanyongyu) ([#983](https://github.com/nonebot/nonebot2/pull/983))\n- Feature: 支持 WebSocket 连接同时获取 str 或 bytes [@yanyongyu](https://github.com/yanyongyu) ([#962](https://github.com/nonebot/nonebot2/pull/962))\n\n### 🚀 新功能\n\n- Feature: 支持 WebSocket 连接同时获取 str 或 bytes [@yanyongyu](https://github.com/yanyongyu) ([#962](https://github.com/nonebot/nonebot2/pull/962))\n- Feature: 添加 `CommandStart` 依赖注入参数 [@MeetWq](https://github.com/MeetWq) ([#915](https://github.com/nonebot/nonebot2/pull/915))\n- Feature: 添加 Rule, Permission 反向位运算支持 [@yanyongyu](https://github.com/yanyongyu) ([#872](https://github.com/nonebot/nonebot2/pull/872))\n- Feature: 新增文本完整匹配规则 [@A-kirami](https://github.com/A-kirami) ([#797](https://github.com/nonebot/nonebot2/pull/797))\n\n### 🐛 Bug 修复\n\n- Fix: 修复依赖注入默认值参数在 `__eq__` 被重写时报错的问题 [@yanyongyu](https://github.com/yanyongyu) ([#971](https://github.com/nonebot/nonebot2/pull/971))\n- Fix: 修复`MessageTemplate`在没有格式化说明符时行为不正确的问题 [@mnixry](https://github.com/mnixry) ([#947](https://github.com/nonebot/nonebot2/pull/947))\n- Fix: Bot Hook 没有捕获跳过异常 [@yanyongyu](https://github.com/yanyongyu) ([#905](https://github.com/nonebot/nonebot2/pull/905))\n- Fix: 修复部分事件响应器参数类型中冗余的 Optional [@A-kirami](https://github.com/A-kirami) ([#904](https://github.com/nonebot/nonebot2/pull/904))\n- Fix: 修复 event 类型检查会对类型进行自动转换 [@yanyongyu](https://github.com/yanyongyu) ([#876](https://github.com/nonebot/nonebot2/pull/876))\n- Fix: 修复 `on_fullmatch` 返回类型错误 [@yanyongyu](https://github.com/yanyongyu) ([#815](https://github.com/nonebot/nonebot2/pull/815))\n- Fix: 修复 DataclassEncoder 嵌套 encode 的问题 [@AkiraXie](https://github.com/AkiraXie) ([#812](https://github.com/nonebot/nonebot2/pull/812))\n\n### 📝 文档\n\n- Docs: 修复定时任务一节中的部分拼写错误 [@Nova-Noir](https://github.com/Nova-Noir) ([#982](https://github.com/nonebot/nonebot2/pull/982))\n- Fix: 商店搜索失效 [@yanyongyu](https://github.com/yanyongyu) ([#978](https://github.com/nonebot/nonebot2/pull/978))\n- Docs: 添加 QQ 频道链接 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#961](https://github.com/nonebot/nonebot2/pull/961))\n- Docs: 添加 nonebug 单元测试文档 [@MingxuanGame](https://github.com/MingxuanGame) ([#929](https://github.com/nonebot/nonebot2/pull/929))\n- Docs: 添加 pm2 部署文档 [@evlic](https://github.com/evlic) ([#853](https://github.com/nonebot/nonebot2/pull/853))\n- Docs: 更新 GitHub Action 部署文档 [@kexue-z](https://github.com/kexue-z) ([#937](https://github.com/nonebot/nonebot2/pull/937))\n- Docs: 添加自定义匹配规则文档 [@yanyongyu](https://github.com/yanyongyu) ([#914](https://github.com/nonebot/nonebot2/pull/914))\n- Docs: 修复适配器文档内商店链接 [@yanyongyu](https://github.com/yanyongyu) ([#861](https://github.com/nonebot/nonebot2/pull/861))\n- Docs: tips for finding adapters' document link [@StarHeartHunt](https://github.com/StarHeartHunt) ([#860](https://github.com/nonebot/nonebot2/pull/860))\n- Docs: 添加对 `fastapi_reload` 在 Windows 平台额外影响的说明 [@CherryGS](https://github.com/CherryGS) ([#830](https://github.com/nonebot/nonebot2/pull/830))\n- Docs: 修复 ci/cd action 中错误的版本号 [@Bubbleioa](https://github.com/Bubbleioa) ([#819](https://github.com/nonebot/nonebot2/pull/819))\n- Docs: 减小更新日志 toc 最大显示等级 [@yanyongyu](https://github.com/yanyongyu) ([#813](https://github.com/nonebot/nonebot2/pull/813))\n- Docs: 修改议题模板中的错误链接 [@he0119](https://github.com/he0119) ([#807](https://github.com/nonebot/nonebot2/pull/807))\n- Docs: 修改消息模板文档中错误的样例 [@mnixry](https://github.com/mnixry) ([#806](https://github.com/nonebot/nonebot2/pull/806))\n- Docs: 更新贡献指南 [@yanyongyu](https://github.com/yanyongyu) ([#798](https://github.com/nonebot/nonebot2/pull/798))\n\n### 💫 杂项\n\n- Plugin: nonebot-plugin-chess 改名为 nonebot-plugin-boardgame [@MeetWq](https://github.com/MeetWq) ([#953](https://github.com/nonebot/nonebot2/pull/953))\n- Plugin: 网易云无损音乐下载更改 [@kitUIN](https://github.com/kitUIN) ([#924](https://github.com/nonebot/nonebot2/pull/924))\n- Docs: 移除商店中的过期插件 [@j1g5awi](https://github.com/j1g5awi) ([#902](https://github.com/nonebot/nonebot2/pull/902))\n- CI: 修复发布机器人的意外错误 [@he0119](https://github.com/he0119) ([#892](https://github.com/nonebot/nonebot2/pull/892))\n- Docs: 替换和移除部分已经失效的插件 [@MeetWq](https://github.com/MeetWq) ([#879](https://github.com/nonebot/nonebot2/pull/879))\n- Docs: 添加 netlify 标签 [@yanyongyu](https://github.com/yanyongyu) ([#816](https://github.com/nonebot/nonebot2/pull/816))\n- Fix: 修改错误的插件 PyPI 项目名称 [@Lancercmd](https://github.com/Lancercmd) ([#804](https://github.com/nonebot/nonebot2/pull/804))\n- CI: 添加更新日志自动更新 action [@yanyongyu](https://github.com/yanyongyu) ([#799](https://github.com/nonebot/nonebot2/pull/799))\n\n### 🍻 插件发布\n\n- Plugin: imageutils [@yanyongyu](https://github.com/yanyongyu) ([#985](https://github.com/nonebot/nonebot2/pull/985))\n- Plugin: 摸鱼日历 [@yanyongyu](https://github.com/yanyongyu) ([#980](https://github.com/nonebot/nonebot2/pull/980))\n- Plugin: 走迷宫 [@yanyongyu](https://github.com/yanyongyu) ([#977](https://github.com/nonebot/nonebot2/pull/977))\n- Plugin: 语录娱乐 [@yanyongyu](https://github.com/yanyongyu) ([#973](https://github.com/nonebot/nonebot2/pull/973))\n- Plugin: 国内新冠疫情数据查询 [@yanyongyu](https://github.com/yanyongyu) ([#975](https://github.com/nonebot/nonebot2/pull/975))\n- Plugin: nonebot_plugin_eventdone [@yanyongyu](https://github.com/yanyongyu) ([#966](https://github.com/nonebot/nonebot2/pull/966))\n- Plugin: 幻影坦克图片合成 [@yanyongyu](https://github.com/yanyongyu) ([#968](https://github.com/nonebot/nonebot2/pull/968))\n- Plugin: 合成字符画(GIF) [@yanyongyu](https://github.com/yanyongyu) ([#964](https://github.com/nonebot/nonebot2/pull/964))\n- Plugin: 国际象棋 [@yanyongyu](https://github.com/yanyongyu) ([#957](https://github.com/nonebot/nonebot2/pull/957))\n- Plugin: NoneBot2 文档搜索 [@yanyongyu](https://github.com/yanyongyu) ([#952](https://github.com/nonebot/nonebot2/pull/952))\n- Plugin: 中国象棋 [@yanyongyu](https://github.com/yanyongyu) ([#949](https://github.com/nonebot/nonebot2/pull/949))\n- Plugin: B 站视频封面提取 [@yanyongyu](https://github.com/yanyongyu) ([#946](https://github.com/nonebot/nonebot2/pull/946))\n- Plugin: 一言 [@yanyongyu](https://github.com/yanyongyu) ([#944](https://github.com/nonebot/nonebot2/pull/944))\n- Plugin: 答案之书 [@yanyongyu](https://github.com/yanyongyu) ([#942](https://github.com/nonebot/nonebot2/pull/942))\n- Plugin: 支付宝到账语音 [@yanyongyu](https://github.com/yanyongyu) ([#940](https://github.com/nonebot/nonebot2/pull/940))\n- Plugin: nonebot-plugin-dida [@yanyongyu](https://github.com/yanyongyu) ([#934](https://github.com/nonebot/nonebot2/pull/934))\n- Plugin: 随机唐可可 [@yanyongyu](https://github.com/yanyongyu) ([#931](https://github.com/nonebot/nonebot2/pull/931))\n- Plugin: splatoon2 新闻 [@yanyongyu](https://github.com/yanyongyu) ([#917](https://github.com/nonebot/nonebot2/pull/917))\n- Plugin: nonebot_plugin_draw [@yanyongyu](https://github.com/yanyongyu) ([#910](https://github.com/nonebot/nonebot2/pull/910))\n- Plugin: 扫雷游戏 [@yanyongyu](https://github.com/yanyongyu) ([#907](https://github.com/nonebot/nonebot2/pull/907))\n- Plugin: 汉兜 Handle [@yanyongyu](https://github.com/yanyongyu) ([#899](https://github.com/nonebot/nonebot2/pull/899))\n- Plugin: 多适配器帮助函数 [@yanyongyu](https://github.com/yanyongyu) ([#897](https://github.com/nonebot/nonebot2/pull/897))\n- Plugin: 语句抽象化 [@yanyongyu](https://github.com/yanyongyu) ([#894](https://github.com/nonebot/nonebot2/pull/894))\n- Plugin: 快速搜索 [@yanyongyu](https://github.com/yanyongyu) ([#889](https://github.com/nonebot/nonebot2/pull/889))\n- Plugin: wordle 猜单词 [@yanyongyu](https://github.com/yanyongyu) ([#891](https://github.com/nonebot/nonebot2/pull/891))\n- Plugin: MediaWiki 查询 [@yanyongyu](https://github.com/yanyongyu) ([#886](https://github.com/nonebot/nonebot2/pull/886))\n- Plugin: HikariSearch [@yanyongyu](https://github.com/yanyongyu) ([#884](https://github.com/nonebot/nonebot2/pull/884))\n- Plugin: 第二个 leetcode 查询插件 [@yanyongyu](https://github.com/yanyongyu) ([#882](https://github.com/nonebot/nonebot2/pull/882))\n- Plugin: 成分姬 [@yanyongyu](https://github.com/yanyongyu) ([#878](https://github.com/nonebot/nonebot2/pull/878))\n- Plugin: Arcaea 查分插件 [@yanyongyu](https://github.com/yanyongyu) ([#875](https://github.com/nonebot/nonebot2/pull/875))\n- Plugin: QQ 自动同意好友申请 [@yanyongyu](https://github.com/yanyongyu) ([#871](https://github.com/nonebot/nonebot2/pull/871))\n- Plugin: 21 点游戏插件 [@yanyongyu](https://github.com/yanyongyu) ([#865](https://github.com/nonebot/nonebot2/pull/865))\n- Plugin: 色图生成 [@yanyongyu](https://github.com/yanyongyu) ([#863](https://github.com/nonebot/nonebot2/pull/863))\n- Plugin: bilibili 通知插件 [@yanyongyu](https://github.com/yanyongyu) ([#859](https://github.com/nonebot/nonebot2/pull/859))\n- Plugin: 订阅推送管理 [@yanyongyu](https://github.com/yanyongyu) ([#855](https://github.com/nonebot/nonebot2/pull/855))\n- Plugin: 动漫新闻 [@yanyongyu](https://github.com/yanyongyu) ([#852](https://github.com/nonebot/nonebot2/pull/852))\n- Plugin: 游戏王卡查 [@yanyongyu](https://github.com/yanyongyu) ([#846](https://github.com/nonebot/nonebot2/pull/846))\n- Plugin: 二维码识别与发送 [@yanyongyu](https://github.com/yanyongyu) ([#843](https://github.com/nonebot/nonebot2/pull/843))\n- Plugin: mockingbird [@yanyongyu](https://github.com/yanyongyu) ([#841](https://github.com/nonebot/nonebot2/pull/841))\n- Plugin: QQ 自动续火花 [@yanyongyu](https://github.com/yanyongyu) ([#839](https://github.com/nonebot/nonebot2/pull/839))\n- Plugin: 每日一句 [@yanyongyu](https://github.com/yanyongyu) ([#832](https://github.com/nonebot/nonebot2/pull/832))\n- Plugin: 原神抽卡记录分析 [@yanyongyu](https://github.com/yanyongyu) ([#829](https://github.com/nonebot/nonebot2/pull/829))\n- Plugin: YetAnotherPicSearch [@yanyongyu](https://github.com/yanyongyu) ([#825](https://github.com/nonebot/nonebot2/pull/825))\n- Plugin: 60s 读世界小插件 [@yanyongyu](https://github.com/yanyongyu) ([#810](https://github.com/nonebot/nonebot2/pull/810))\n- Plugin: pixiv.net p 站查询图片 [@yanyongyu](https://github.com/yanyongyu) ([#803](https://github.com/nonebot/nonebot2/pull/803))\n\n### 🍻 机器人发布\n\n- Bot: 屑岛风 Bot [@yanyongyu](https://github.com/yanyongyu) ([#987](https://github.com/nonebot/nonebot2/pull/987))\n- Bot: ShigureBot [@yanyongyu](https://github.com/yanyongyu) ([#959](https://github.com/nonebot/nonebot2/pull/959))\n- Bot: Inkar Suki [@yanyongyu](https://github.com/yanyongyu) ([#955](https://github.com/nonebot/nonebot2/pull/955))\n\n## v2.0.0-beta.2\n\n- 修复 `receive`, `got` 在参数为空消息时依旧会反复询问\n- 修复文档商店分页显示错误\n- 修复插件导入失败时，依然存在于已导入插件列表中\n- 移除 `state` 依赖注入所需的默认值 `State()`\n- 增加 `fastapi` 配置项：是否将适配器路由包含在 schema 中\n- 修改 `load_builtin_plugins` 函数，使其能够支持加载多个内置插件\n- 新增 `load_builtin_plugin` 函数，用于加载单个内置插件\n- 修改 `Message` 和 `MessageSegment` 类，完善 typing，转移 Mapping 构建支持至 pydantic validate\n- 调整项目结构，分离内部定义与用户接口\n- 新增 Bot 连接事件钩子 (如 `driver.on_bot_connect` ) 的依赖注入\n\n## v2.0.0-beta.1\n\n- 新增 `MessageTemplate` 对于 `str` 普通模板的支持\n- 移除插件加载的 `NameSpace` 模式\n- 修改 toml 加载插件时的键名为 `tool.nonebot` 以符合规范\n- 新增 Handler 依赖注入支持，同步/异步支持\n- 统一 `Processor`, `Rule`, `Permission`, `Processor` 使用 `Handler`\n- 修改内置 `Rule`, `Permission` 如 `startswith`, `command` 等使用 class 实现\n- 更换文档框架 (docusaurus) 以及主题 (docusaurus-theme-nonepress)\n- 移除 Matcher `state_factory` 支持\n\n## v2.0.0a16\n\n- 新增 `MessageTemplate` 可用于 `Message` 的模板生成\n- 新增 `matcher.got` `matcher.send` `matcher.pause` `matcher.reject` `matcher.finish` 支持 `MessageTemplate`\n- 移除 `matcher.got` 原本的 `state format` 支持，由 `MessageTemplate` template 替代\n- `adapter` 基类拆分为单独文件\n- 修复 `fastapi` Driver Websocket 未能正确提供请求头部\n- 新增 `fastapi` Driver 更多的 uvicorn 相关配置项\n- 新增 `quart` Driver 更多的 uvicorn 相关配置项\n- 修复 `endswith` Rule 错误的正则匹配\n- 修复 `cqhttp` Adapter `image`, `record`, `video` 对 `BytesIO` 不正常的读取操作\n\n## v2.0.0a15\n\n- 修复 `fastapi` Driver 未能正确进行 reconnect\n- 修复 `MessageSegment` 错误的 Mapping 映射\n\n## v2.0.0a14\n\n- 修改日志等级，支持输出等级自定义\n- 修复日志输出模块名错误\n- 修改 `Matcher` 属性 `module` 类型\n- 新增 `Matcher` 属性 `plugin_name` `module_name` `module_prefix`\n- 移除 `bot.call_api` 参数 `self_id` 切换机器人支持\n- 修复 `type_updater` `permission_updater` 未传递的错误\n- 修复 `type_updater` `permission_updater` 参数 `state` 错误\n- 修复使用 `state_factory` 后导致无法在 session 内传递 `state`\n- 重构 `Driver` 及连接信息抽象\n- 新增正向 Driver(Client) 支持\n- 新增 `aiohttp` 正向 Driver\n- `fastapi` Driver 新增正向支持\n\n## v2.0.0a13.post1\n\n- 分离 `handler` 与 `matcher`\n- 修复 `cqhttp` secret 校验出错\n- 修复 `pydantic 1.8` 导致的 `alias` 问题\n- 修改 `cqhttp` `ding` `session id`，不再允许跨群\n- 修改 `shell_command` 存储 message\n- 修复 `cqhttp` 检查 reply 失败退出\n- 新增 `call_api` hook 接口\n- 优化 `import hook`\n\n## v2.0.0a11\n\n- 修改 `nonebot` 项目结构，分离所有 `adapter`\n- 修改插件加载逻辑，使用 `import hook` (PEP 302)\n- 新增插件加载方式: `json`, `toml`\n- 适配 `pydantic` ~1.8\n- 移除 4 种内置事件类型限制，允许自定义事件类型\n- 新增会话权限更新自定义，会话中断时更新权限以做到多人会话\n\n## v2.0.0a10\n\n- 新增 `Quart Driver` 支持\n- 修复 `mirai` 协议适配命令处理以及消息转义\n\n## v2.0.0a9\n\n- 修复 `Message` 消息为 `None` 时的处理错误\n- 修复 `Message.extract_plain_text` 返回为转义字符串的问题\n- 修复命令处理错误地删除了后续空格\n- 增加好友添加和加群请求事件 `approve`, `reject` 方法\n- 新增 `mirai-api-http` 协议适配\n- 修复 rule 运行时 state 覆盖问题，隔离 state\n- 新增 `shell like command` 支持\n\n## v2.0.0a8\n\n- 修改 typing 类型注释\n- 修改 event 基类接口\n- 修复部分非法 CQ 码被识别导致报错\n- 修复非 text 类型 CQ 码 data 未进行去转义\n- 修复内置插件未进行去转义，修改内置插件为 cqhttp 定制\n- 修复 `load_plugins` 加载不合法的包时出现 `spec` 为 `None` 的问题\n- 出于**CQ 码安全性考虑**，使用 cqhttp 的 `bot.send` 或者 `matcher.send` 时默认对字符串进行转义\n- 移动 cqhttp 相关 `Permission` 至 `nonebot.adapters.cqhttp` 包内\n\n## v2.0.0a7\n\n- 修复 cqhttp 检查 to me 时出现 IndexError\n- 修复已失效的事件响应器仍会运行一次的 bug\n- 修改 cqhttp 检查 reply 时未去除后续 at 以及空格\n- 添加 get_plugin 获取插件函数\n- 添加插件 export, require 方法\n- **移除**内置 apscheduler 定时任务支持\n- **移除**内置协议适配默认加载\n- 新增**钉钉**协议适配\n- 移除原有共享型 `MatcherGroup` 改为默认型 `MatcherGroup`\n\n## v2.0.0a6\n\n- 修复 block 失效问题 (hotfix)\n\n## v2.0.0a5\n\n- 更新插件指南文档\n- 修复临时事件响应器运行后删除造成的多次响应问题\n"
  },
  {
    "path": "website/src/components/Asciinema/container.tsx",
    "content": "import React, { useEffect, useRef } from \"react\";\n\nimport * as AsciinemaPlayer from \"asciinema-player\";\n\nexport type AsciinemaOptions = {\n  cols: number;\n  rows: number;\n  autoPlay: boolean;\n  preload: boolean;\n  loop: boolean;\n  startAt: number | string;\n  speed: number;\n  idleTimeLimit: number;\n  theme: string;\n  poster: string;\n  fit: string;\n  fontSize: string;\n};\n\nexport type Props = {\n  url: string;\n  options?: Partial<AsciinemaOptions>;\n};\n\nexport default function AsciinemaContainer({\n  url,\n  options = {},\n}: Props): React.ReactNode {\n  const ref = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    AsciinemaPlayer.create(url, ref.current, options);\n  }, [url, options]);\n\n  return <div ref={ref} className=\"not-prose ap-container\" />;\n}\n"
  },
  {
    "path": "website/src/components/Asciinema/index.tsx",
    "content": "import React from \"react\";\n\nimport BrowserOnly from \"@docusaurus/BrowserOnly\";\n\nimport \"asciinema-player/dist/bundle/asciinema-player.css\";\n\nimport type { Props } from \"./container\";\n\nimport \"./styles.css\";\n\nexport type { Props } from \"./container\";\n\nexport default function Asciinema(props: Props): React.ReactNode {\n  return (\n    <BrowserOnly\n      fallback={\n        <a href={props.url} title=\"Asciinema video player\">\n          Asciinema cast\n        </a>\n      }\n    >\n      {() => {\n        // eslint-disable-next-line @typescript-eslint/no-var-requires\n        const AsciinemaContainer = require(\"./container.tsx\").default;\n        return <AsciinemaContainer {...props} />;\n      }}\n    </BrowserOnly>\n  );\n}\n"
  },
  {
    "path": "website/src/components/Asciinema/styles.css",
    "content": ".ap-player svg {\n  @apply inline-block;\n}\n\n.ap-container {\n  @apply w-full my-4;\n}\n"
  },
  {
    "path": "website/src/components/Form/Adapter.tsx",
    "content": "import React from \"react\";\n\nimport { Form } from \".\";\n\nexport default function AdapterForm(): React.ReactNode {\n  const formItems = [\n    {\n      name: \"基本信息\",\n      items: [\n        {\n          type: \"text\",\n          name: \"name\",\n          labelText: \"适配器名称\",\n        },\n        { type: \"text\", name: \"description\", labelText: \"适配器描述\" },\n        {\n          type: \"text\",\n          name: \"homepage\",\n          labelText: \"适配器项目仓库/主页链接\",\n        },\n      ],\n    },\n    {\n      name: \"包信息\",\n      items: [\n        { type: \"text\", name: \"pypi\", labelText: \"PyPI 项目名\" },\n        { type: \"text\", name: \"module\", labelText: \"适配器 import 包名\" },\n      ],\n    },\n    {\n      name: \"其他信息\",\n      items: [{ type: \"tag\", name: \"tags\", labelText: \"标签\" }],\n    },\n  ];\n  const handleSubmit = (result: Record<string, string>) => {\n    window.open(\n      `https://github.com/nonebot/nonebot2/issues/new?${new URLSearchParams({\n        template: \"adapter_publish.yml\",\n        title: `Adapter: ${result.name}`,\n        ...result,\n      })}`\n    );\n  };\n\n  return (\n    <Form type=\"adapter\" formItems={formItems} handleSubmit={handleSubmit} />\n  );\n}\n"
  },
  {
    "path": "website/src/components/Form/Bot.tsx",
    "content": "import React from \"react\";\n\nimport { Form } from \".\";\n\nexport default function BotForm(): React.ReactNode {\n  const formItems = [\n    {\n      name: \"基本信息\",\n      items: [\n        {\n          type: \"text\",\n          name: \"name\",\n          labelText: \"机器人名称\",\n        },\n        { type: \"text\", name: \"description\", labelText: \"机器人描述\" },\n        {\n          type: \"text\",\n          name: \"homepage\",\n          labelText: \"机器人项目仓库/主页链接\",\n        },\n      ],\n    },\n    {\n      name: \"其他信息\",\n      items: [{ type: \"tag\", name: \"tags\", labelText: \"标签\" }],\n    },\n  ];\n\n  const handleSubmit = (result: Record<string, string>) => {\n    window.open(\n      `https://github.com/nonebot/nonebot2/issues/new?${new URLSearchParams({\n        template: \"bot_publish.yml\",\n        title: `Bot: ${result.name}`,\n        ...result,\n      })}`\n    );\n  };\n\n  return <Form type=\"bot\" formItems={formItems} handleSubmit={handleSubmit} />;\n}\n"
  },
  {
    "path": "website/src/components/Form/Items/Tag/index.tsx",
    "content": "import React, { useState } from \"react\";\n\nimport clsx from \"clsx\";\n\nimport { FontAwesomeIcon } from \"@fortawesome/react-fontawesome\";\nimport { ChromePicker, type ColorResult } from \"react-color\";\n\nimport \"./styles.css\";\n\nimport TagComponent from \"@/components/Tag\";\n\nimport type { Tag as TagType } from \"@/types/tag\";\n\nexport type Props = {\n  allowTags: TagType[];\n  onTagUpdate: (tags: TagType[]) => void;\n};\n\nexport default function TagFormItem({\n  allowTags,\n  onTagUpdate,\n}: Props): React.ReactNode {\n  const [tags, setTags] = useState<TagType[]>([]);\n  const [label, setLabel] = useState<TagType[\"label\"]>(\"\");\n  const [color, setColor] = useState<TagType[\"color\"]>(\"#ea5252\");\n  const slicedTags = Array.from(\n    new Set(\n      allowTags\n        .filter((tag) => tag.label.toLocaleLowerCase().includes(label))\n        .map((e) => e.label)\n    )\n  ).slice(0, 5);\n\n  const validateTag = () => {\n    return label.length >= 1 && label.length <= 10;\n  };\n  const newTag = () => {\n    if (tags.length >= 3) {\n      return;\n    }\n    if (validateTag()) {\n      const tag: TagType = { label, color };\n      const newTags = [...tags, tag];\n      setTags(newTags);\n      onTagUpdate(newTags);\n    }\n  };\n  const delTag = (index: number) => {\n    const newTags = tags.filter((_, i) => i !== index);\n    setTags(newTags);\n    onTagUpdate(newTags);\n  };\n  const onChangeColor = (color: ColorResult) => {\n    setColor(color.hex as TagType[\"color\"]);\n  };\n\n  return (\n    <>\n      <label className=\"flex flex-wrap gap-x-1 gap-y-1\">\n        {tags.map((tag, index) => (\n          <TagComponent\n            key={index}\n            {...tag}\n            className=\"cursor-pointer\"\n            onClick={() => delTag(index)}\n          />\n        ))}\n        {tags.length < 3 && (\n          <span\n            className={clsx(\"add-btn\", { \"add-btn-disabled\": !validateTag() })}\n            onClick={() => newTag()}\n          >\n            <FontAwesomeIcon className=\"pr-1\" icon={[\"fas\", \"plus\"]} />\n            新建标签\n          </span>\n        )}\n      </label>\n      <div className=\"form-item-container\">\n        <span className=\"form-item-title\">标签名称</span>\n        <div className=\"dropdown dropdown-bottom w-full\">\n          <input\n            type=\"text\"\n            value={label}\n            className=\"form-item form-item-input\"\n            placeholder=\"请输入\"\n            onChange={(e) => setLabel(e.target.value)}\n          />\n          {slicedTags.length > 0 && (\n            <ul\n              tabIndex={0}\n              className=\"dropdown-content z-10 menu p-2 shadow bg-base-100 rounded-box w-52\"\n            >\n              {slicedTags.map((tag) => (\n                <li key={tag}>\n                  <a onClick={() => setLabel(tag)}>{tag}</a>\n                </li>\n              ))}\n            </ul>\n          )}\n        </div>\n      </div>\n      <div className=\"form-item-container\">\n        <span className=\"form-item-title\">标签颜色</span>\n        <ChromePicker\n          className=\"my-4 fix-input-color\"\n          color={color}\n          disableAlpha\n          onChangeComplete={onChangeColor}\n        />\n      </div>\n    </>\n  );\n}\n"
  },
  {
    "path": "website/src/components/Form/Items/Tag/styles.css",
    "content": ".add-btn {\n  @apply px-2 select-none cursor-pointer min-w-[64px] rounded-full hover:bg-opacity-[.08];\n  @apply flex justify-center items-center border-dashed border-2;\n\n  &-disabled {\n    @apply pointer-events-none opacity-60;\n  }\n}\n\n.form-item {\n  @apply basis-3/4;\n\n  &-title {\n    @apply basis-1/4 label-text;\n  }\n\n  &-input {\n    @apply input input-sm input-bordered;\n  }\n\n  &-select {\n    @apply select select-sm select-bordered;\n  }\n\n  &-container {\n    @apply flex items-center mt-2;\n  }\n}\n\n.fix-input-color {\n  @apply !text-base-content !bg-base-100;\n  input {\n    @apply !text-base-content !bg-base-100;\n  }\n}\n"
  },
  {
    "path": "website/src/components/Form/Plugin.tsx",
    "content": "import React from \"react\";\n\nimport Link from \"@docusaurus/Link\";\n\nimport { Form } from \".\";\n\nexport default function PluginForm(): React.ReactNode {\n  const formItems = [\n    {\n      name: \"包信息\",\n      items: [\n        { type: \"text\", name: \"pypi\", labelText: \"PyPI 项目名\" },\n        { type: \"text\", name: \"module\", labelText: \"插件模块名\" },\n      ],\n    },\n    {\n      name: \"其他信息\",\n      items: [{ type: \"tag\", name: \"tags\", labelText: \"标签\" }],\n    },\n  ];\n  const handleSubmit = (result: Record<string, string>) => {\n    window.open(\n      `https://github.com/nonebot/nonebot2/issues/new?${new URLSearchParams({\n        template: \"plugin_publish.yml\",\n        title: `Plugin: ${result.pypi}`,\n        ...result,\n      })}`\n    );\n  };\n\n  const description = (\n    <p>\n      请在发布前阅读{\" \"}\n      <Link\n        className=\"text-primary\"\n        href=\"https://nonebot.dev/docs/developer/plugin-publishing\"\n      >\n        NoneBot 插件发布流程指导\n      </Link>\n      ，并确保满足其中所述条件。\n    </p>\n  );\n\n  return (\n    <Form\n      type=\"plugin\"\n      formItems={formItems}\n      handleSubmit={handleSubmit}\n      description={description}\n    />\n  );\n}\n"
  },
  {
    "path": "website/src/components/Form/index.tsx",
    "content": "import React, { useEffect, useState } from \"react\";\n\nimport clsx from \"clsx\";\n\nimport \"./styles.css\";\n\nimport type { Resource } from \"@/libs/store\";\nimport { fetchRegistryData } from \"@/libs/store\";\n\nimport TagFormItem from \"./Items/Tag\";\n\nimport type { Tag as TagType } from \"@/types/tag\";\n\nexport type FormItemData = {\n  type: string;\n  name: string;\n  labelText: string;\n};\n\nexport type FormItemGroup = {\n  name: string;\n  items: FormItemData[];\n};\n\nexport type Props = {\n  children?: React.ReactNode;\n  description?: React.ReactNode;\n  type: Resource[\"resourceType\"];\n  formItems: FormItemGroup[];\n  handleSubmit: (result: Record<string, string>) => void;\n};\n\nexport function Form({\n  type,\n  children,\n  description,\n  formItems,\n  handleSubmit,\n}: Props): React.ReactNode {\n  const [currentStep, setCurrentStep] = useState<number>(0);\n  const [result, setResult] = useState<Record<string, string>>({});\n  const [allowTags, setAllowTags] = useState<TagType[]>([]);\n\n  // load tags asynchronously\n  useEffect(() => {\n    fetchRegistryData(type)\n      .then((data) =>\n        setAllowTags(\n          data\n            .filter((item) => item.tags.length > 0)\n            .map((ele) => ele.tags)\n            .flat()\n        )\n      )\n      .catch((e) => {\n        console.error(e);\n      });\n  }, [type]);\n\n  const setFormValue = (key: string, value: string) => {\n    setResult({ ...result, [key]: value });\n  };\n\n  const handleNextStep = () => {\n    const currentStepNames = formItems[currentStep].items.map(\n      (item) => item.name\n    );\n    if (currentStepNames.every((name) => result[name])) {\n      setCurrentStep(currentStep + 1);\n    }\n  };\n  const onPrev = () => currentStep > 0 && setCurrentStep(currentStep - 1);\n  const onNext = () =>\n    currentStep < formItems.length - 1\n      ? handleNextStep()\n      : handleSubmit(result);\n\n  return (\n    <>\n      <ul className=\"steps\">\n        {formItems.map((item, index) => (\n          <li\n            key={index}\n            className={clsx(\"step\", currentStep === index && \"step-primary\")}\n          >\n            {item.name}\n          </li>\n        ))}\n      </ul>\n      {description && currentStep === 0 && (\n        <div className=\"form-description\">{description}</div>\n      )}\n      <div className=\"form-control w-full min-h-[300px]\">\n        {children ||\n          formItems[currentStep].items.map((item) => (\n            <FormItem\n              key={item.name}\n              type={item.type}\n              name={item.name}\n              labelText={item.labelText}\n              allowTags={allowTags}\n              result={result}\n              setResult={setFormValue}\n            />\n          ))}\n      </div>\n      <div className=\"flex justify-between\">\n        <button\n          className={clsx(\"form-btn form-btn-prev\", {\n            \"form-btn-hidden\": currentStep === 0,\n          })}\n          onClick={onPrev}\n        >\n          上一步\n        </button>\n        <button className=\"form-btn form-btn-next\" onClick={onNext}>\n          {currentStep === formItems.length - 1 ? \"提交\" : \"下一步\"}\n        </button>\n      </div>\n    </>\n  );\n}\n\nexport function FormItem({\n  type,\n  name,\n  labelText,\n  allowTags,\n  result,\n  setResult,\n}: FormItemData & {\n  allowTags: TagType[];\n  result: Record<string, string>;\n  setResult: (key: string, value: string) => void;\n}): React.ReactNode {\n  return (\n    <>\n      <label className=\"label\">\n        <span className=\"label-text\">{labelText}</span>\n      </label>\n      {type === \"text\" && (\n        <input\n          value={result[name] || \"\"}\n          type=\"text\"\n          name={name}\n          onChange={(e) => setResult(name, e.target.value)}\n          placeholder=\"请输入\"\n          className={clsx(\"form-input\", {\n            \"form-input-error\": !result[name],\n          })}\n        />\n      )}\n      {type === \"text\" && !result[name] && (\n        <label className=\"label\">\n          <span className=\"form-label form-label-error\">请输入{labelText}</span>\n        </label>\n      )}\n      {type === \"tag\" && (\n        <TagFormItem\n          allowTags={allowTags}\n          onTagUpdate={(tags) => setResult(name, JSON.stringify(tags))}\n        />\n      )}\n    </>\n  );\n}\n"
  },
  {
    "path": "website/src/components/Form/styles.css",
    "content": ".form-btn {\n  @apply btn btn-sm btn-primary no-animation;\n\n  &-prev {\n    @apply mr-auto;\n  }\n\n  &-next {\n    @apply ml-auto;\n  }\n\n  &-hidden {\n    @apply !hidden;\n  }\n}\n\n.form-input {\n  @apply input input-bordered w-full;\n\n  &-error {\n    @apply input-error;\n  }\n}\n\n.form-label {\n  @apply text-xs;\n\n  &-error {\n    @apply text-error;\n  }\n}\n"
  },
  {
    "path": "website/src/components/Home/Feature.tsx",
    "content": "import React from \"react\";\n\nimport CodeBlock from \"@theme/CodeBlock\";\n\nexport type Feature = {\n  title: string;\n  tagline?: string;\n  description?: string;\n  annotaion?: string;\n  children?: React.ReactNode;\n};\n\nexport function HomeFeature({\n  title,\n  tagline,\n  description,\n  annotaion,\n  children,\n}: Feature): React.ReactNode {\n  return (\n    <div className=\"flex flex-col items-center justify-center p-4\">\n      <p className=\"text-sm text-base-content/70 font-medium tracking-wide uppercase\">\n        {tagline}\n      </p>\n      <h1 className=\"mt-3 font-mono font-light text-4xl tracking-tight sm:text-5xl md:text-5xl text-primary\">\n        {title}\n      </h1>\n      <p className=\"mt-10 mb-6\">{description}</p>\n      {children}\n      <p className=\"text-sm italic text-base-content/70\">{annotaion}</p>\n    </div>\n  );\n}\n\nfunction HomeFeatureSingleColumn(props: Feature): React.ReactNode {\n  return (\n    <div className=\"grid grid-cols-1 px-4 py-8 md:px-16 mx-auto\">\n      <HomeFeature {...props} />\n    </div>\n  );\n}\n\nfunction HomeFeatureDoubleColumn({\n  features: [feature1, feature2],\n  children,\n}: {\n  features: [Feature, Feature];\n  children?: [React.ReactNode, React.ReactNode];\n}): React.ReactNode {\n  const [children1, children2] = children ?? [];\n\n  return (\n    <div className=\"grid gap-x-6 gap-y-8 grid-cols-1 lg:grid-cols-2 max-w-7xl px-4 py-8 md:px-16 mx-auto\">\n      <HomeFeature {...feature1}>{children1}</HomeFeature>\n      <HomeFeature {...feature2}>{children2}</HomeFeature>\n    </div>\n  );\n}\n\nfunction HomeFeatures(): React.ReactNode {\n  return (\n    <>\n      <HomeFeatureSingleColumn\n        title=\"开箱即用\"\n        tagline=\"out of box\"\n        description=\"使用 NB-CLI 快速构建属于你的机器人\"\n      >\n        <CodeBlock\n          title=\"Installation\"\n          language=\"bash\"\n          className=\"home-codeblock\"\n        >\n          {[\n            \"$ pipx install nb-cli\",\n            \"$ nb\",\n            // \"d8b   db  .d88b.  d8b   db d88888b d8888b.  .d88b.  d888888b\",\n            // \"888o  88 .8P  Y8. 888o  88 88'     88  `8D .8P  Y8. `~~88~~'\",\n            // \"88V8o 88 88    88 88V8o 88 88ooooo 88oooY' 88    88    88\",\n            // \"88 V8o88 88    88 88 V8o88 88~~~~~ 88~~~b. 88    88    88\",\n            // \"88  V888 `8b  d8' 88  V888 88.     88   8D `8b  d8'    88\",\n            // \"VP   V8P  `Y88P'  VP   V8P Y88888P Y8888P'  `Y88P'     YP\",\n            \"[?] What do you want to do?\",\n            \"❯ Create a NoneBot project.\",\n            \"  Run the bot in current folder.\",\n            \"  Manage bot driver.\",\n            \"  Manage bot adapters.\",\n            \"  Manage bot plugins.\",\n            \"  ...\",\n          ].join(\"\\n\")}\n        </CodeBlock>\n      </HomeFeatureSingleColumn>\n      <HomeFeatureDoubleColumn\n        features={[\n          {\n            title: \"插件系统\",\n            tagline: \"plugin system\",\n            description: \"插件化开发，模块化管理\",\n          },\n          {\n            title: \"跨平台支持\",\n            tagline: \"cross-platform support\",\n            description: \"支持多种平台，以及多样的事件响应方式\",\n          },\n        ]}\n      >\n        <CodeBlock title=\"\" language=\"python\" className=\"home-codeblock\">\n          {[\n            \"import nonebot\",\n            \"# 加载一个插件\",\n            'nonebot.load_plugin(\"path.to.your.plugin\")',\n            \"# 从文件夹加载插件\",\n            'nonebot.load_plugins(\"plugins\")',\n            \"# 从配置文件加载多个插件\",\n            'nonebot.load_from_json(\"plugins.json\")',\n            'nonebot.load_from_toml(\"pyproject.toml\")',\n          ].join(\"\\n\")}\n        </CodeBlock>\n        <CodeBlock title=\"\" language=\"python\" className=\"home-codeblock\">\n          {[\n            \"import nonebot\",\n            \"# OneBot\",\n            \"from nonebot.adapters.onebot.v11 import Adapter as OneBotAdapter\",\n            \"# QQ 机器人\",\n            \"from nonebot.adapters.qq import Adapter as QQAdapter\",\n            \"driver = nonebot.get_driver()\",\n            \"driver.register_adapter(OneBotAdapter)\",\n            \"driver.register_adapter(QQAdapter)\",\n          ].join(\"\\n\")}\n        </CodeBlock>\n      </HomeFeatureDoubleColumn>\n      <HomeFeatureDoubleColumn\n        features={[\n          {\n            title: \"异步开发\",\n            tagline: \"asynchronous first\",\n            description: \"异步优先式开发，提高运行效率\",\n          },\n          {\n            title: \"依赖注入\",\n            tagline: \"builtin dependency injection system\",\n            description: \"简单清晰的依赖注入系统，内置依赖函数减少用户代码\",\n          },\n        ]}\n      >\n        <CodeBlock title=\"\" language=\"python\" className=\"home-codeblock\">\n          {[\n            \"from nonebot import on_message\",\n            \"# 注册一个消息响应器\",\n            \"matcher = on_message()\",\n            \"# 注册一个消息处理器\",\n            \"# 并重复收到的消息\",\n            \"@matcher.handle()\",\n            \"async def handler(event: Event) -> None:\",\n            \"    await matcher.send(event.get_message())\",\n          ].join(\"\\n\")}\n        </CodeBlock>\n        <CodeBlock title=\"\" language=\"python\" className=\"home-codeblock\">\n          {[\n            \"from nonebot import on_command\",\n            \"# 注册一个命令响应器\",\n            'matcher = on_command(\"help\", alias={\"帮助\"})',\n            \"# 注册一个命令处理器\",\n            \"# 通过依赖注入获得命令名以及参数\",\n            \"@matcher.handle()\",\n            \"async def handler(cmd = Command(), arg = CommandArg()) -> None:\",\n            \"    await matcher.finish()\",\n          ].join(\"\\n\")}\n        </CodeBlock>\n      </HomeFeatureDoubleColumn>\n    </>\n  );\n}\n\nexport default React.memo(HomeFeatures);\n"
  },
  {
    "path": "website/src/components/Home/Hero.tsx",
    "content": "import React, { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport Link from \"@docusaurus/Link\";\nimport useDocusaurusContext from \"@docusaurus/useDocusaurusContext\";\nimport { FontAwesomeIcon } from \"@fortawesome/react-fontawesome\";\nimport { useNonepressThemeConfig } from \"@nullbot/docusaurus-theme-nonepress/client\";\n// @ts-expect-error: we need to make package have type: module\nimport copy from \"copy-text-to-clipboard\";\n\nimport IconCopy from \"@theme/Icon/Copy\";\nimport IconSuccess from \"@theme/Icon/Success\";\n\nfunction HomeHeroInstallButton(): React.ReactNode {\n  const code = \"pipx run nb-cli create\";\n\n  const [isCopied, setIsCopied] = useState(false);\n  const copyTimeout = useRef<number | undefined>(undefined);\n\n  const handleCopyCode = useCallback(() => {\n    copy(code);\n    setIsCopied(true);\n    copyTimeout.current = window.setTimeout(() => {\n      setIsCopied(false);\n    }, 1500);\n  }, [code]);\n\n  useEffect(() => () => window.clearTimeout(copyTimeout.current), []);\n\n  return (\n    <button\n      className=\"btn no-animation home-hero-copy\"\n      onClick={handleCopyCode}\n    >\n      <code>$ {code}</code>\n      {isCopied ? <IconSuccess className=\"text-success\" /> : <IconCopy />}\n    </button>\n  );\n}\n\nfunction HomeHero(): React.ReactNode {\n  const {\n    siteConfig: { tagline },\n  } = useDocusaurusContext();\n  const {\n    navbar: { logo },\n  } = useNonepressThemeConfig();\n\n  return (\n    <div className=\"home-hero\">\n      <img src=\"/img/uwu.svg\" alt=\"uwu\" className=\"home-hero-uwu\" />\n      <img src={logo!.src} alt={logo!.alt} className=\"home-hero-logo\" />\n      <h1 className=\"home-hero-title\">\n        <span className=\"text-primary\">None</span>\n        Bot\n      </h1>\n      <p className=\"home-hero-tagline\">{tagline}</p>\n      <div className=\"home-hero-actions\">\n        <Link to=\"/docs/\" className=\"btn btn-primary\">\n          开始使用 <FontAwesomeIcon icon={[\"fas\", \"chevron-right\"]} />\n        </Link>\n        <HomeHeroInstallButton />\n      </div>\n      <div className=\"home-hero-next\">\n        <FontAwesomeIcon icon={[\"fas\", \"angle-down\"]} />\n      </div>\n    </div>\n  );\n}\n\nexport default React.memo(HomeHero);\n"
  },
  {
    "path": "website/src/components/Home/index.tsx",
    "content": "import React from \"react\";\n\nimport HomeFeatures from \"./Feature\";\nimport HomeHero from \"./Hero\";\n\nimport \"./styles.css\";\n\nexport default function HomeContent(): React.ReactNode {\n  return (\n    <div className=\"home-container\">\n      <HomeHero />\n      <HomeFeatures />\n    </div>\n  );\n}\n"
  },
  {
    "path": "website/src/components/Home/styles.css",
    "content": ".home {\n  &-container {\n    @apply -mt-16;\n  }\n\n  &-hero {\n    @apply relative flex flex-col items-center justify-center gap-4 h-screen;\n\n    &-logo {\n      @apply h-48 w-auto;\n    }\n\n    &-title {\n      @apply text-5xl font-normal tracking-tight;\n    }\n\n    &-tagline {\n      @apply text-sm font-medium uppercase tracking-wide text-base-content/70;\n    }\n\n    &-actions {\n      @apply flex flex-col sm:flex-row gap-4;\n    }\n\n    &-copy {\n      @apply font-normal normal-case text-base-content/70;\n    }\n\n    &-next {\n      @apply absolute bottom-4;\n\n      & svg {\n        @apply animate-bounce text-primary text-4xl;\n      }\n    }\n  }\n\n  &-codeblock {\n    @apply inline-block !max-w-[600px];\n  }\n}\n\n.home-hero-uwu {\n  @apply hidden;\n}\n\n[data-uwu=\"true\"] .home-hero-uwu {\n  @apply block max-w-xs;\n}\n\n[data-uwu=\"true\"] .home-hero-logo,\n[data-uwu=\"true\"] .home-hero-title {\n  @apply hidden;\n}\n"
  },
  {
    "path": "website/src/components/Messenger/index.tsx",
    "content": "import React from \"react\";\n\nimport clsx from \"clsx\";\n\nimport useBaseUrl from \"@docusaurus/useBaseUrl\";\nimport { FontAwesomeIcon } from \"@fortawesome/react-fontawesome\";\nimport { useNonepressThemeConfig } from \"@nullbot/docusaurus-theme-nonepress/client\";\n\nimport ThemedImage from \"@theme/ThemedImage\";\n\nimport \"./styles.css\";\n\nexport type Message = {\n  msg: string;\n  position?: \"left\" | \"right\";\n  monospace?: boolean;\n};\n\nfunction MessageBox({\n  msg,\n  position = \"left\",\n  monospace = false,\n}: Message): React.ReactNode {\n  const {\n    navbar: { logo },\n  } = useNonepressThemeConfig();\n  const sources = {\n    light: useBaseUrl(logo!.src),\n    dark: useBaseUrl(logo!.srcDark || logo!.src),\n  };\n\n  const isRight = position === \"right\";\n\n  return (\n    <div className={clsx(\"chat\", isRight ? \"chat-end\" : \"chat-start\")}>\n      <div className=\"chat-image avatar\">\n        <div\n          className={clsx(\n            \"messenger-chat-avatar\",\n            isRight && \"messenger-chat-avatar-user\"\n          )}\n        >\n          {isRight ? (\n            <FontAwesomeIcon icon={[\"fas\", \"user\"]} />\n          ) : (\n            <ThemedImage sources={sources} />\n          )}\n        </div>\n      </div>\n      <div\n        className={clsx(\n          \"chat-bubble messenger-chat-bubble\",\n          monospace && \"font-mono\"\n        )}\n        dangerouslySetInnerHTML={{\n          __html: msg.replace(/\\n/g, \"<br/>\").replace(/ /g, \"&nbsp;\"),\n        }}\n      />\n    </div>\n  );\n}\n\nexport default function Messenger({\n  msgs = [],\n}: {\n  msgs?: Message[];\n}): React.ReactNode {\n  return (\n    <div className=\"messenger-container\">\n      <header className=\"messenger-title\">\n        <div className=\"messenger-title-back\">\n          <FontAwesomeIcon icon={[\"fas\", \"chevron-left\"]} />\n        </div>\n        <div className=\"messenger-title-name\">\n          <span>NoneBot</span>\n        </div>\n        <div className=\"messenger-title-more\">\n          <FontAwesomeIcon icon={[\"fas\", \"bars\"]} />\n        </div>\n      </header>\n      <div className=\"messenger-chat\">\n        {msgs.map((msg, i) => (\n          <MessageBox {...msg} key={i} />\n        ))}\n      </div>\n      <div className=\"messenger-footer\">\n        <div className=\"messenger-footer-action\">\n          <div className=\"messenger-footer-action-input\">\n            <input\n              className=\"input input-xs input-bordered input-info w-full\"\n              readOnly\n            />\n          </div>\n          <div className=\"messenger-footer-action-send\">\n            <button className=\"btn btn-xs btn-info no-animation text-white\">\n              发送\n            </button>\n          </div>\n        </div>\n        <div className=\"messenger-footer-tools\">\n          <div>\n            <FontAwesomeIcon icon={[\"fas\", \"microphone\"]} />\n          </div>\n          <div>\n            <FontAwesomeIcon icon={[\"fas\", \"image\"]} />\n          </div>\n          <div>\n            <FontAwesomeIcon icon={[\"fas\", \"camera\"]} />\n          </div>\n          <div>\n            <FontAwesomeIcon icon={[\"fas\", \"wallet\"]} />\n          </div>\n          <div>\n            <FontAwesomeIcon icon={[\"fas\", \"smile-wink\"]} />\n          </div>\n          <div>\n            <FontAwesomeIcon icon={[\"fas\", \"plus-circle\"]} />\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "website/src/components/Messenger/styles.css",
    "content": ".messenger {\n  &-container {\n    @apply block w-full my-4 overflow-hidden;\n    @apply rounded-lg outline-none bg-base-200;\n    @apply transition-[background-color] duration-500;\n  }\n\n  &-title {\n    @apply flex items-center h-12 px-4 bg-info text-white;\n\n    &-back {\n      @apply text-left text-base grow;\n    }\n\n    &-name {\n      @apply flex-initial grow-0 text-xl font-bold;\n    }\n\n    &-more {\n      @apply text-right text-base grow;\n    }\n  }\n\n  &-chat {\n    @apply p-4 min-h-[150px];\n\n    &-avatar {\n      @apply !flex items-center justify-center;\n      @apply w-10 rounded-full;\n\n      &-user {\n        @apply bg-info text-white;\n      }\n    }\n\n    &-bubble {\n      @apply bg-base-100 text-base-content [word-break:break-word];\n      @apply transition-[color,background-color] duration-500;\n    }\n  }\n\n  &-footer {\n    @apply px-4;\n\n    &-action {\n      @apply flex items-center gap-2;\n\n      &-input {\n        @apply flex-1;\n\n        & > input {\n          @apply transition-[color,background-color] duration-500;\n        }\n      }\n\n      &-send {\n        @apply flex-initial w-fit;\n      }\n    }\n\n    &-tools {\n      @apply grid grid-cols-6 items-center py-1;\n      @apply text-center text-base text-base-content/60;\n    }\n  }\n}\n"
  },
  {
    "path": "website/src/components/Modal/index.tsx",
    "content": "import React, { useEffect, useState } from \"react\";\n\nimport clsx from \"clsx\";\n\nimport IconClose from \"@theme/Icon/Close\";\n\nimport \"./styles.css\";\n\nexport type Props = {\n  children?: React.ReactNode;\n  className?: string;\n  title: string;\n  useCustomTitle?: boolean;\n  backdropExit?: boolean;\n  setOpenModal: (isOpen: boolean) => void;\n};\n\nexport default function Modal({\n  setOpenModal,\n  className,\n  children,\n  useCustomTitle,\n  backdropExit,\n  title,\n}: Props): React.ReactNode {\n  const [transitionClass, setTransitionClass] = useState<string>(\"\");\n\n  const onFadeIn = () => setTransitionClass(\"fade-in\");\n  const onFadeOut = () => setTransitionClass(\"fade-out\");\n  const onTransitionEnd = () =>\n    transitionClass === \"fade-out\" && setOpenModal(false);\n\n  useEffect(onFadeIn, []);\n\n  return (\n    <div className={clsx(\"nb-modal-root\", className)}>\n      <div\n        className={clsx(\"nb-modal-backdrop\", transitionClass)}\n        onTransitionEnd={onTransitionEnd}\n        onClick={() => backdropExit && onFadeOut()}\n      />\n      <div className={clsx(\"nb-modal-container\", transitionClass)}>\n        <div className=\"card bg-base-100 shadow-xl max-w-2xl\">\n          <div className=\"card-body\">\n            {!useCustomTitle && (\n              <div className=\"nb-modal-title\">\n                {title}\n                <div className=\"card-actions ml-auto\">\n                  <button className=\"btn btn-square btn-sm\" onClick={onFadeOut}>\n                    <IconClose />\n                  </button>\n                </div>\n              </div>\n            )}\n            {children}\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "website/src/components/Modal/styles.css",
    "content": ".nb-modal {\n  &-title {\n    @apply flex items-center font-bold;\n  }\n\n  &-root {\n    @apply fixed z-[1300] inset-0 flex items-center justify-center;\n  }\n\n  &-container {\n    @apply absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 min-w-[400px] lg:min-w-[600px];\n    @apply opacity-0;\n    @apply transition-opacity duration-[225ms] ease-in-out delay-0;\n\n    &.fade-in {\n      @apply opacity-100;\n    }\n\n    &.fade-out {\n      @apply opacity-0;\n    }\n  }\n\n  &-backdrop {\n    @apply fixed flex right-0 bottom-0 top-0 left-0 bg-transparent opacity-0;\n    @apply transition-all duration-[225ms] ease-in-out delay-0 -z-[1];\n\n    &.fade-in {\n      @apply opacity-100 bg-black/50;\n    }\n\n    &.fade-out {\n      @apply opacity-0 bg-transparent;\n    }\n  }\n}\n"
  },
  {
    "path": "website/src/components/Paginate/index.tsx",
    "content": "import React, { useCallback } from \"react\";\n\nimport clsx from \"clsx\";\n\nimport { FontAwesomeIcon } from \"@fortawesome/react-fontawesome\";\n\nimport type { usePagination } from \"react-use-pagination\";\n\nimport \"./styles.css\";\n\nconst MAX_LENGTH = 7;\n\nexport type Props = Pick<\n  ReturnType<typeof usePagination>,\n  | \"totalPages\"\n  | \"currentPage\"\n  | \"setNextPage\"\n  | \"setPreviousPage\"\n  | \"setPage\"\n  | \"previousEnabled\"\n  | \"nextEnabled\"\n> & {\n  className?: string;\n};\n\nexport default function Paginate({\n  className,\n  totalPages,\n  currentPage,\n  setPreviousPage,\n  setNextPage,\n  setPage,\n  previousEnabled,\n  nextEnabled,\n}: Props): React.ReactNode {\n  // const [containerElement, setContainerElement] = useState<HTMLElement | null>(\n  //   null\n  // );\n\n  // const ref = useCallback(\n  //   (element: HTMLElement | null) => {\n  //     setContainerElement(element);\n  //   },\n  //   [setContainerElement]\n  // );\n\n  // const maxWidth = useContentWidth(\n  //   containerElement?.parentElement ?? undefined\n  // );\n  // const maxLength = Math.min(\n  //   (maxWidth && Math.floor(maxWidth / 50) - 2) || totalPages,\n  //   totalPages\n  // );\n\n  const range = useCallback((start: number, end: number) => {\n    const result = [];\n    start = start > 0 ? start : 1;\n    for (let i = start; i <= end; i++) {\n      result.push(i);\n    }\n    return result;\n  }, []);\n\n  const pages: (React.ReactNode | number)[] = [];\n  const ellipsis = <FontAwesomeIcon icon=\"ellipsis-h\" />;\n\n  const even = MAX_LENGTH % 2 === 0 ? 1 : 0;\n  const left = Math.floor(MAX_LENGTH / 2);\n  const right = totalPages - left + even + 1;\n  currentPage += 1;\n\n  if (totalPages <= MAX_LENGTH) {\n    pages.push(...range(1, totalPages));\n  } else if (currentPage > left && currentPage < right) {\n    const firstItem = 1;\n    const lastItem = totalPages;\n    const start = currentPage - left + 2;\n    const end = currentPage + left - 2 - even;\n    const secondItem = start - 1 === firstItem + 1 ? 2 : ellipsis;\n    const beforeLastItem = end + 1 === lastItem - 1 ? end + 1 : ellipsis;\n\n    pages.push(1, secondItem, ...range(start, end), beforeLastItem, totalPages);\n  } else if (currentPage === left) {\n    const end = currentPage + left - 1 - even;\n    pages.push(...range(1, end), ellipsis, totalPages);\n  } else if (currentPage === right) {\n    const start = currentPage - left + 1;\n    pages.push(1, ellipsis, ...range(start, totalPages));\n  } else {\n    pages.push(...range(1, left), ellipsis, ...range(right, totalPages));\n  }\n\n  return (\n    <nav\n      className={clsx(\"paginate-container\", className)}\n      role=\"navigation\"\n      aria-label=\"Pagination Navigation\"\n    >\n      <button\n        className=\"paginate-button\"\n        onClick={setPreviousPage}\n        disabled={!previousEnabled}\n      >\n        <FontAwesomeIcon icon={[\"fas\", \"chevron-left\"]} />\n      </button>\n      <ul className=\"paginate-pager\">\n        {pages.map((page, index) => (\n          <li\n            key={index}\n            className={clsx(\n              \"paginate-button\",\n              typeof page !== \"number\" && \"ellipsis\",\n              currentPage === page && \"active\"\n            )}\n            onClick={() =>\n              typeof page === \"number\" &&\n              currentPage !== page &&\n              setPage(page - 1)\n            }\n          >\n            {page}\n          </li>\n        ))}\n      </ul>\n      <button\n        className=\"paginate-button\"\n        onClick={setNextPage}\n        disabled={!nextEnabled}\n      >\n        <FontAwesomeIcon icon={[\"fas\", \"chevron-right\"]} />\n      </button>\n    </nav>\n  );\n}\n"
  },
  {
    "path": "website/src/components/Paginate/styles.css",
    "content": ".paginate {\n  &-container {\n    @apply flex items-center justify-center gap-2;\n  }\n\n  &-button {\n    @apply flex items-center justify-center cursor-pointer select-none;\n    @apply w-8 h-8 text-sm leading-8 text-center bg-base-200 text-base-content;\n\n    &.ellipsis {\n      @apply !text-base-content cursor-default;\n    }\n\n    &.active {\n      @apply bg-primary !text-primary-content cursor-default;\n    }\n\n    &:hover {\n      @apply text-primary;\n    }\n\n    &:disabled {\n      @apply !text-base-content/80 cursor-not-allowed;\n    }\n  }\n\n  &-pager {\n    @apply flex items-center justify-center gap-2;\n  }\n}\n"
  },
  {
    "path": "website/src/components/Resource/Avatar/index.tsx",
    "content": "import { useState } from \"react\";\nimport Link from \"@docusaurus/Link\";\nimport clsx from \"clsx\";\n\ninterface Props {\n  className?: string;\n  authorLink: string;\n  authorAvatar: string;\n}\n\nexport default function Avatar({ authorLink, authorAvatar, className }: Props) {\n  const [loaded, setLoaded] = useState(false);\n  const onLoad = () => setLoaded(true);\n\n  return (\n    <div className=\"avatar\">\n      <div className={className}>\n        <Link href={authorLink}>\n          <div className=\"relative w-full h-full\">\n            {!loaded && (\n              <div\n                className={clsx(\n                  \"absolute inset-0 rounded-full bg-gray-200\",\n                  \"animate-pulse\"\n                )}\n              />\n            )}\n            <img\n              src={authorAvatar}\n              onLoad={onLoad}\n              className={clsx(\n                \"w-full h-full rounded-full object-cover\",\n                \"transition-opacity duration-300\",\n                loaded ? \"opacity-100\" : \"opacity-0\"\n              )}\n              alt=\"Avatar\"\n            />\n          </div>\n        </Link>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "website/src/components/Resource/Card/index.tsx",
    "content": "import React from \"react\";\n\nimport clsx from \"clsx\";\n\nimport Link from \"@docusaurus/Link\";\nimport { FontAwesomeIcon } from \"@fortawesome/react-fontawesome\";\n\nimport Avatar from \"@/components/Resource/Avatar\";\nimport Tag from \"@/components/Resource/Tag\";\nimport ValidStatus from \"@/components/Resource/ValidStatus\";\nimport type { Resource } from \"@/libs/store\";\n\nimport \"./styles.css\";\n\nexport type Props = {\n  resource: Resource;\n  onClick?: () => void;\n  onTagClick: (tag: string) => void;\n  onAuthorClick: () => void;\n  className?: string;\n};\n\nexport default function ResourceCard({\n  resource,\n  onClick,\n  onTagClick,\n  onAuthorClick,\n  className,\n}: Props): React.ReactNode {\n  const isGithub = /^https:\\/\\/github.com\\/[^/]+\\/[^/]+/.test(\n    resource.homepage\n  );\n\n  const isPlugin = resource.resourceType === \"plugin\";\n  const registryLink =\n    isPlugin &&\n    `https://registry.nonebot.dev/plugin/${resource.project_link}:${resource.module_name}`;\n\n  const authorLink = `https://github.com/${resource.author}`;\n  const authorAvatar = `${authorLink}.png?size=80`;\n\n  return (\n    <div className={clsx(\"resource-card-container\", className)}>\n      <div className=\"resource-card-header\">\n        <div className=\"resource-card-header-title\">\n          <div className=\"resource-card-header-text\" data-tip={resource.name}>\n            <div className=\"truncate min-w-0\">{resource.name}</div>\n          </div>\n          {resource.is_official && (\n            <FontAwesomeIcon\n              className=\"resource-card-header-check\"\n              icon={[\"fas\", \"circle-check\"]}\n            />\n          )}\n          <ValidStatus\n            className=\"mx-2\"\n            resource={resource}\n            validLink={registryLink as string}\n            simple\n          />\n        </div>\n        <div className=\"resource-card-header-expand\" onClick={onClick}>\n          <FontAwesomeIcon icon={[\"fas\", \"expand\"]} />\n        </div>\n      </div>\n      <div className=\"resource-card-desc\">{resource.desc}</div>\n      <div className=\"resource-card-footer\">\n        <div className=\"resource-card-footer-tags\">\n          {resource.tags.map((tag, index) => (\n            <Tag\n              className=\"resource-card-footer-tag\"\n              key={index}\n              {...tag}\n              onClick={() => onTagClick(tag.label)}\n            />\n          ))}\n        </div>\n        <div className=\"divider resource-card-footer-divider\" />\n        <div className=\"resource-card-footer-info\">\n          <div className=\"resource-card-footer-group\">\n            <Link href={resource.homepage}>\n              {isGithub ? (\n                <FontAwesomeIcon\n                  className=\"resource-card-footer-icon\"\n                  icon={[\"fab\", \"github\"]}\n                />\n              ) : (\n                <FontAwesomeIcon\n                  className=\"resource-card-footer-icon\"\n                  icon={[\"fas\", \"link\"]}\n                />\n              )}\n            </Link>\n            {isPlugin && (\n              <Link href={registryLink as string}>\n                <FontAwesomeIcon\n                  className=\"resource-card-footer-icon\"\n                  icon={[\"fas\", \"cube\"]}\n                />\n              </Link>\n            )}\n          </div>\n          <div className=\"resource-card-footer-group\">\n            <Avatar\n              className=\"resource-card-footer-avatar\"\n              key={resource.author}\n              authorAvatar={authorAvatar}\n              authorLink={authorLink}\n            />\n            <span\n              className=\"resource-card-footer-author\"\n              onClick={onAuthorClick}\n            >\n              {resource.author}\n            </span>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "website/src/components/Resource/Card/styles.css",
    "content": ".resource-card {\n  &-container {\n    @apply flex flex-col gap-y-2 w-full min-h-[12rem] p-4;\n    @apply transition-colors duration-500 bg-base-200;\n    @apply border-2 border-base-200 rounded-lg;\n    @apply hover:border-primary;\n  }\n\n  &-header {\n    @apply min-w-0 flex items-center w-full text-lg font-medium;\n\n    &-title {\n      @apply min-w-0 grow justify-start flex items-center flex-initial;\n    }\n\n    &-text {\n      @apply min-w-0 cursor-help tooltip;\n    }\n\n    &-check {\n      @apply ml-2 text-green-600 dark:text-green-400 fill-current;\n    }\n\n    &-expand {\n      @apply flex-none fill-current cursor-pointer;\n    }\n  }\n\n  &-desc {\n    @apply flex-1 w-full text-sm text-ellipsis break-words;\n  }\n\n  &-footer {\n    @apply flex flex-col w-full cursor-default;\n\n    &-tags {\n      @apply flex flex-wrap gap-1;\n    }\n\n    &-tag {\n      @apply cursor-pointer;\n    }\n\n    &-divider {\n      @apply m-0;\n    }\n\n    &-info {\n      @apply flex items-center justify-between w-full;\n    }\n\n    &-group {\n      @apply flex items-center justify-center gap-x-1 leading-none;\n    }\n\n    &-icon {\n      @apply w-5 h-5 fill-current opacity-80;\n\n      &:hover {\n        @apply opacity-100;\n      }\n    }\n\n    &-avatar {\n      @apply w-5 h-5 rounded-full transition-shadow;\n\n      &:hover {\n        @apply ring-1 ring-primary ring-offset-base-100 ring-offset-1;\n      }\n    }\n\n    &-author {\n      @apply text-sm cursor-pointer;\n    }\n  }\n}\n"
  },
  {
    "path": "website/src/components/Resource/DetailCard/index.tsx",
    "content": "import { useEffect, useState } from \"react\";\n\nimport { FontAwesomeIcon } from \"@fortawesome/react-fontawesome\";\nimport copy from \"copy-text-to-clipboard\";\n\nimport Tag from \"@/components/Resource/Tag\";\nimport ValidStatus from \"@/components/Resource/ValidStatus\";\nimport type { Resource } from \"@/libs/store\";\n\nimport type { PyPIData } from \"./types\";\n\nimport Avatar from \"../Avatar\";\nimport \"./styles.css\";\n\nexport type Props = {\n  resource: Resource;\n};\n\nexport default function ResourceDetailCard({ resource }: Props) {\n  const [pypiData, setPypiData] = useState<PyPIData | null>(null);\n  const [copied, setCopied] = useState<boolean>(false);\n\n  const authorLink = `https://github.com/${resource.author}`;\n  const authorAvatar = `${authorLink}.png?size=100`;\n\n  const isPlugin = resource.resourceType === \"plugin\";\n  const registryLink =\n    isPlugin &&\n    `https://registry.nonebot.dev/plugin/${resource.project_link}:${resource.module_name}`;\n\n  const getProjectLink = (resource: Resource) => {\n    switch (resource.resourceType) {\n      case \"plugin\":\n      case \"adapter\":\n      case \"driver\":\n        return resource.project_link;\n      default:\n        return null;\n    }\n  };\n\n  const getModuleName = (resource: Resource) => {\n    switch (resource.resourceType) {\n      case \"plugin\":\n      case \"adapter\":\n        return resource.module_name;\n      case \"driver\":\n        return resource.module_name.replace(/~/, \"nonebot.drivers.\");\n      default:\n        return null;\n    }\n  };\n\n  const getHomepageLink = (resource: Resource) => {\n    switch (resource.resourceType) {\n      case \"plugin\":\n      case \"adapter\":\n      case \"driver\":\n        return resource.homepage;\n      default:\n        return null;\n    }\n  };\n\n  const getPypiProjectLink = (resource: Resource) => {\n    switch (resource.resourceType) {\n      case \"plugin\":\n      case \"adapter\":\n        return `https://pypi.org/project/${resource.project_link}`;\n      default:\n        return null;\n    }\n  };\n\n  const getPluginStatusUpdatedTime = (resource: Resource) => {\n    switch (resource.resourceType) {\n      case \"plugin\":\n        return new Date(resource.time).toLocaleString();\n      default:\n        return null;\n    }\n  };\n\n  const fetchPypiProject = (projectName: string) =>\n    fetch(`https://pypi.org/pypi/${projectName}/json`)\n      .then((response) => response.json())\n      .then((data) => setPypiData(data));\n\n  const copyCommand = (resource: Resource) => {\n    const projectLink = getProjectLink(resource);\n    if (projectLink) {\n      copy(`nb ${resource.resourceType} install ${projectLink}`);\n      setCopied(true);\n      setTimeout(() => setCopied(false), 2000);\n    }\n  };\n\n  useEffect(() => {\n    const fetchingTasks: Promise<void>[] = [];\n    if (resource.resourceType === \"bot\" || resource.resourceType === \"driver\") {\n      return;\n    }\n\n    if (resource.project_link) {\n      fetchingTasks.push(fetchPypiProject(resource.project_link));\n    }\n\n    Promise.all(fetchingTasks);\n  }, [resource]);\n\n  const projectLink = getProjectLink(resource) || \"无\";\n  const moduleName = getModuleName(resource) || \"无\";\n  const homepageLink = getHomepageLink(resource);\n  const pypiProjectLink = getPypiProjectLink(resource);\n  const updatedTime = getPluginStatusUpdatedTime(resource);\n\n  return (\n    <>\n      <div className=\"detail-card-header\">\n        <Avatar\n          className=\"detail-card-avatar\"\n          key={resource.author}\n          authorLink={authorLink}\n          authorAvatar={authorAvatar}\n        />\n        <div className=\"detail-card-title\">\n          <span className=\"detail-card-title-main flex items-center gap-x-1\">\n            {resource.name}\n            {resource.is_official && (\n              <div className=\"rounded-md text-sm bg-green-400/10 text-green-600 px-1 py-0.5\">\n                官方\n              </div>\n            )}\n          </span>\n          <a\n            className=\"detail-card-title-sub hover:underline hover:text-primary\"\n            target=\"_blank\"\n            rel=\"noreferrer\"\n            href={authorLink}\n          >\n            {resource.author}\n          </a>\n        </div>\n        <div className=\"detail-card-actions\">\n          <ValidStatus\n            resource={resource}\n            validLink={registryLink as string}\n            className=\"detail-card-actions-desktop\"\n          />\n          <button\n            className=\"detail-card-actions-button detail-card-actions-desktop w-28\"\n            onClick={() => copyCommand(resource)}\n          >\n            {copied ? \"复制成功\" : \"复制安装命令\"}\n          </button>\n        </div>\n      </div>\n      <div className=\"divider detail-card-header-divider\" />\n      <div className=\"detail-card-body\">\n        <div className=\"detail-card-body-left\">\n          <span className=\"h-full\">{resource.desc}</span>\n          <div className=\"resource-card-footer-tags mb-4\">\n            {resource.tags.map((tag, index) => (\n              <Tag className=\"align-bottom\" key={index} {...tag} />\n            ))}\n          </div>\n        </div>\n        <div className=\"detail-card-body-divider\" />\n        <div className=\"detail-card-body-right\">\n          <div className=\"detail-card-meta-item\">\n            <span>\n              <FontAwesomeIcon fixedWidth icon={[\"fab\", \"python\"]} />{\" \"}\n              {(pypiData && pypiData.info.requires_python) || \"无\"}\n            </span>\n          </div>\n          <div className=\"detail-card-meta-item\">\n            <FontAwesomeIcon fixedWidth icon={[\"fas\", \"file-zipper\"]} />{\" \"}\n            {(pypiData &&\n              pypiData.releases[pypiData.info.version] &&\n              `${\n                pypiData.releases[pypiData.info.version].reduce(\n                  (acc, curr) => acc + curr.size,\n                  0\n                ) / 1000\n              }K`) ||\n              \"无\"}\n          </div>\n          <div className=\"detail-card-meta-item\">\n            <span>\n              <FontAwesomeIcon fixedWidth icon={[\"fas\", \"scale-balanced\"]} />{\" \"}\n              {(pypiData && pypiData.info.license) || \"无\"}\n            </span>\n          </div>\n          <div className=\"detail-card-meta-item\">\n            <FontAwesomeIcon fixedWidth icon={[\"fas\", \"tag\"]} />{\" \"}\n            {(pypiData && pypiData.info.version) || \"无\"}\n          </div>\n\n          {homepageLink && (\n            <div className=\"detail-card-meta-item\">\n              <FontAwesomeIcon fixedWidth icon={[\"fas\", \"fingerprint\"]} />{\" \"}\n              <a\n                href={homepageLink}\n                target=\"_blank\"\n                rel=\"noreferrer\"\n                className=\"detail-card-meta-item-link\"\n              >\n                {moduleName}\n              </a>\n            </div>\n          )}\n\n          {pypiProjectLink && (\n            <div className=\"detail-card-meta-item\">\n              <FontAwesomeIcon fixedWidth icon={[\"fas\", \"cubes\"]} />{\" \"}\n              <a\n                href={pypiProjectLink}\n                target=\"_blank\"\n                rel=\"noreferrer\"\n                className=\"detail-card-meta-item-link\"\n              >\n                {projectLink}\n              </a>\n            </div>\n          )}\n\n          <div className=\"detail-card-meta-item\">\n            <FontAwesomeIcon fixedWidth icon={[\"fas\", \"clock-rotate-left\"]} />{\" \"}\n            {updatedTime}\n          </div>\n\n          <div className=\"detail-card-actions\">\n            <ValidStatus\n              resource={resource}\n              validLink={registryLink as string}\n              className=\"detail-card-actions-mobile\"\n            />\n            <button\n              className=\"detail-card-actions detail-card-actions-button detail-card-actions-mobile w-28\"\n              onClick={() => copyCommand(resource)}\n            >\n              {copied ? \"复制成功\" : \"复制安装命令\"}\n            </button>\n          </div>\n        </div>\n      </div>\n    </>\n  );\n}\n"
  },
  {
    "path": "website/src/components/Resource/DetailCard/styles.css",
    "content": ".detail-card {\n  &-header {\n    @apply flex items-center align-middle;\n\n    &-divider {\n      @apply m-0;\n    }\n  }\n\n  &-avatar {\n    @apply mr-3 w-12 h-12 rounded-full;\n  }\n\n  &-title {\n    @apply inline-flex flex-col h-12 justify-start;\n\n    &-main {\n      @apply font-bold;\n    }\n\n    &-sub {\n      @apply text-sm;\n    }\n  }\n\n  &-actions {\n    @apply flex items-center gap-x-2 lg:ml-auto;\n\n    &-button {\n      @apply btn btn-sm;\n    }\n\n    &-mobile {\n      @apply lg:hidden;\n    }\n\n    &-desktop {\n      @apply max-lg:hidden;\n    }\n  }\n\n  &-body {\n    @apply flex flex-col w-full lg:flex-row;\n\n    &-left {\n      @apply flex flex-col min-h-[150px] lg:basis-3/4 max-w-[65%];\n    }\n\n    &-divider {\n      @apply divider lg:divider-horizontal;\n    }\n\n    &-right {\n      @apply flex flex-col justify-start gap-y-2 lg:basis-1/4 lg:max-w-[45%];\n    }\n  }\n\n  &-meta-item {\n    @apply text-sm truncate;\n\n    &-link {\n      @apply hover:text-primary hover:transition;\n    }\n  }\n}\n"
  },
  {
    "path": "website/src/components/Resource/DetailCard/types.ts",
    "content": "export type Downloads = {\n  last_day: number;\n  last_month: number;\n  last_week: number;\n};\n\nexport type Info = {\n  author: string;\n  author_email: string;\n  bugtrack_url: null;\n  classifiers: string[];\n  description: string;\n  description_content_type: string;\n  docs_url: null;\n  download_url: string;\n  downloads: Downloads;\n  home_page: string;\n  keywords: string;\n  license: string;\n  maintainer: string;\n  maintainer_email: string;\n  name: string;\n  package_url: string;\n  platform: null;\n  project_url: string;\n  release_url: string;\n  requires_dist: string[];\n  requires_python: string;\n  summary: string;\n  version: string;\n  yanked: boolean;\n  yanked_reason: null;\n};\n\nexport interface Digests {\n  blake2b_256: string;\n  md5: string;\n  sha256: string;\n}\n\nexport type Releases = {\n  comment_text: string;\n  digests: Digests;\n  downloads: number;\n  filename: string;\n  has_sig: boolean;\n  md5_digest: string;\n  packagetype: string;\n  python_version: string;\n  requires_python: string;\n  size: number;\n  upload_time: Date;\n  upload_time_iso_8601: Date;\n  url: string;\n  yanked: boolean;\n  yanked_reason: null;\n};\nexport type PyPIData = {\n  info: Info;\n  last_serial: number;\n  releases: { [key: string]: Releases[] };\n  urls: URL[];\n  vulnerabilities: unknown[];\n};\n"
  },
  {
    "path": "website/src/components/Resource/Tag/index.tsx",
    "content": "import React from \"react\";\n\nimport clsx from \"clsx\";\n\nimport { pickTextColor } from \"@/libs/color\";\n\nimport type { Tag } from \"@/types/tag\";\n\nimport \"./styles.css\";\n\nexport type Props = Tag & {\n  className?: string;\n  onClick?: React.MouseEventHandler<HTMLSpanElement>;\n};\n\nexport default function ResourceTag({\n  label,\n  color,\n  className,\n  onClick,\n}: Props): React.ReactNode {\n  return (\n    <span\n      className={clsx(\"resource-tag\", className)}\n      style={{\n        backgroundColor: color,\n        color: pickTextColor(color, \"#fff\", \"#000\"),\n      }}\n      onClick={onClick}\n    >\n      {label}\n    </span>\n  );\n}\n"
  },
  {
    "path": "website/src/components/Resource/Tag/styles.css",
    "content": ".resource-tag {\n  @apply inline-flex items-center justify-center;\n  @apply text-xs font-mono w-fit h-5 px-2 rounded;\n}\n"
  },
  {
    "path": "website/src/components/Resource/ValidStatus/index.tsx",
    "content": "import React from \"react\";\n\nimport clsx from \"clsx\";\n\nimport { FontAwesomeIcon } from \"@fortawesome/react-fontawesome\";\n\nimport type { Resource } from \"@/libs/store\";\nimport { ValidStatus } from \"@/libs/valid\";\n\nimport type { IconName } from \"@fortawesome/fontawesome-common-types\";\n\nexport const getValidStatus = (resource: Resource) => {\n  switch (resource.resourceType) {\n    case \"plugin\":\n      if (resource.skip_test) {\n        return ValidStatus.SKIP;\n      }\n      if (resource.valid) {\n        return ValidStatus.VALID;\n      }\n      return ValidStatus.INVALID;\n    default:\n      return ValidStatus.MISSING;\n  }\n};\n\nexport const validIcons: {\n  [key in ValidStatus]: IconName;\n} = {\n  [ValidStatus.VALID]: \"plug-circle-check\",\n  [ValidStatus.INVALID]: \"plug-circle-xmark\",\n  [ValidStatus.SKIP]: \"plug-circle-exclamation\",\n  [ValidStatus.MISSING]: \"plug-circle-exclamation\",\n};\n\nexport type Props = {\n  resource: Resource;\n  validLink: string;\n  className?: string;\n  simple?: boolean;\n};\n\nexport default function ValidDisplay({\n  resource,\n  validLink,\n  className,\n  simple,\n}: Props) {\n  const validStatus = getValidStatus(resource);\n\n  const isValid = validStatus === ValidStatus.VALID;\n  const isInvalid = validStatus === ValidStatus.INVALID;\n  const isSkip = validStatus === ValidStatus.SKIP;\n\n  return (\n    validStatus !== ValidStatus.MISSING && (\n      <a\n        target=\"_blank\"\n        rel=\"noreferrer\"\n        href={validLink}\n        className={className}\n      >\n        <div\n          className={clsx({\n            \"rounded-md text-sm flex items-center gap-x-1 px-2 py-1 whitespace-nowrap\":\n              !simple,\n            \"bg-green-400/10\": !simple && isValid,\n            \"text-green-600 dark:text-green-400\": isValid,\n            \"bg-red-400/10\": !simple && isInvalid,\n            \"text-red-600 dark:text-red-400\": isInvalid,\n            \"bg-blue-400/10\": !simple && isSkip,\n            \"text-blue-600 dark:text-blue-400\": isSkip,\n          })}\n        >\n          <FontAwesomeIcon icon={validIcons[validStatus]} />\n          {!simple && (\n            <>\n              {isValid && <p>插件已通过测试</p>}\n              {isInvalid && <p>插件未通过测试</p>}\n              {isSkip && <p>插件跳过测试</p>}\n            </>\n          )}\n        </div>\n      </a>\n    )\n  );\n}\n"
  },
  {
    "path": "website/src/components/Searcher/index.tsx",
    "content": "import React, { useRef } from \"react\";\n\nimport clsx from \"clsx\";\n\nimport { translate } from \"@docusaurus/Translate\";\nimport { FontAwesomeIcon } from \"@fortawesome/react-fontawesome\";\n\nimport \"./styles.css\";\n\nexport type Props = {\n  onChange: (value: string) => void;\n  onSubmit: (value: string) => void;\n  onBackspace: () => void;\n  onClear: () => void;\n  onTagClick: (index: number) => void;\n  tags?: string[];\n  className?: string;\n  placeholder?: string;\n  disabled?: boolean;\n};\n\nexport default function Searcher({\n  onChange,\n  onSubmit,\n  onBackspace,\n  onClear,\n  onTagClick,\n  tags = [],\n  className,\n  placeholder,\n  disabled = false,\n}: Props): React.ReactNode {\n  const ref = useRef<HTMLInputElement>(null);\n\n  const handleSubmit = (e: React.FormEvent<HTMLInputElement>) => {\n    onSubmit(e.currentTarget.value);\n    e.currentTarget.value = \"\";\n    e.preventDefault();\n  };\n\n  const handleEscape = (e: React.KeyboardEvent<HTMLInputElement>) => {\n    e.currentTarget.value = \"\";\n  };\n\n  const handleBackspace = (e: React.KeyboardEvent<HTMLInputElement>) => {\n    if (e.currentTarget.value === \"\") {\n      onBackspace();\n      e.preventDefault();\n    }\n  };\n\n  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {\n    switch (e.key) {\n      case \"Enter\": {\n        handleSubmit(e);\n        break;\n      }\n      case \"Escape\": {\n        handleEscape(e);\n        break;\n      }\n      case \"Backspace\": {\n        handleBackspace(e);\n        break;\n      }\n      default:\n        break;\n    }\n  };\n\n  const handleClear = () => {\n    if (ref.current) {\n      ref.current.value = \"\";\n      ref.current.focus();\n    }\n    onClear();\n  };\n\n  return (\n    <div className={clsx(\"searcher-box\", className)}>\n      <div className=\"searcher-container\">\n        {tags.map((tag, index) => (\n          <div\n            key={index}\n            className=\"badge badge-primary searcher-tag\"\n            onClick={() => onTagClick(index)}\n          >\n            {tag}\n            <FontAwesomeIcon\n              className=\"searcher-action-icon close ml-1\"\n              icon={[\"fas\", \"xmark\"]}\n            />\n          </div>\n        ))}\n        <input\n          ref={ref}\n          className=\"searcher-input\"\n          placeholder={\n            placeholder ??\n            translate({\n              id: \"theme.searcher.input.placeholder\",\n              description: \"Search input placeholder\",\n              message: \"搜索\",\n            })\n          }\n          onChange={(e) => onChange(e.currentTarget.value)}\n          onKeyDown={handleKeyDown}\n          disabled={disabled}\n        />\n      </div>\n      <div className=\"searcher-action\" onClick={handleClear}>\n        <FontAwesomeIcon\n          className=\"searcher-action-icon search\"\n          icon={[\"fas\", \"magnifying-glass\"]}\n        />\n        <FontAwesomeIcon\n          className=\"searcher-action-icon close\"\n          icon={[\"fas\", \"xmark\"]}\n        />\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "website/src/components/Searcher/styles.css",
    "content": ".searcher {\n  &-box {\n    @apply flex items-center w-full rounded-3xl bg-base-200;\n    @apply transition-[color,background-color] duration-500;\n  }\n\n  &-container {\n    @apply flex-1 flex items-center flex-wrap gap-x-1 gap-y-2;\n    @apply pl-5 py-3;\n  }\n\n  &-tag {\n    @apply flex-initial shrink-0;\n    @apply font-medium cursor-pointer select-none;\n  }\n\n  &-input {\n    @apply flex-1 text-sm min-w-[10rem];\n    @apply bg-transparent border-none outline-none;\n  }\n\n  &-action {\n    @apply flex-initial shrink-0 flex items-center justify-center cursor-pointer w-12 h-10;\n\n    &-icon {\n      @apply h-4 opacity-50;\n    }\n\n    &:hover &-icon.search {\n      @apply hidden;\n    }\n\n    &:not(:hover) &-icon.close {\n      @apply hidden;\n    }\n  }\n}\n"
  },
  {
    "path": "website/src/components/Store/Content/Adapter.tsx",
    "content": "import React, { useCallback, useEffect, useState } from \"react\";\n\nimport Translate from \"@docusaurus/Translate\";\nimport { usePagination } from \"react-use-pagination\";\n\nimport Admonition from \"@theme/Admonition\";\nimport AdapterForm from \"@/components/Form/Adapter\";\nimport Modal from \"@/components/Modal\";\nimport Paginate from \"@/components/Paginate\";\nimport ResourceCard from \"@/components/Resource/Card\";\nimport ResourceDetailCard from \"@/components/Resource/DetailCard\";\nimport Searcher from \"@/components/Searcher\";\nimport StoreToolbar, { type Action } from \"@/components/Store/Toolbar\";\nimport { authorFilter, tagFilter } from \"@/libs/filter\";\nimport { useSearchControl } from \"@/libs/search\";\nimport { fetchRegistryData, loadFailedTitle } from \"@/libs/store\";\nimport { useToolbar } from \"@/libs/toolbar\";\n\nimport type { Adapter } from \"@/types/adapter\";\n\nexport default function AdapterPage(): React.ReactNode {\n  const [adapters, setAdapters] = useState<Adapter[] | null>(null);\n  const adapterCount = adapters?.length ?? 0;\n  const loading = adapters === null;\n\n  const [error, setError] = useState<Error | null>(null);\n  const [isOpenModal, setIsOpenModal] = useState<boolean>(false);\n  const [isOpenCardModal, setIsOpenCardModal] = useState<boolean>(false);\n  const [clickedAdapter, setClickedAdapter] = useState<Adapter | null>(null);\n\n  const {\n    filteredResources: filteredAdapters,\n    searcherTags,\n    addFilter,\n    onSearchQueryChange,\n    onSearchQuerySubmit,\n    onSearchBackspace,\n    onSearchClear,\n    onSearchTagClick,\n  } = useSearchControl<Adapter>(adapters ?? []);\n  const filteredAdapterCount = filteredAdapters.length;\n\n  const {\n    startIndex,\n    endIndex,\n    totalPages,\n    currentPage,\n    setNextPage,\n    setPreviousPage,\n    setPage,\n    previousEnabled,\n    nextEnabled,\n  } = usePagination({\n    totalItems: filteredAdapters.length,\n    initialPageSize: 12,\n  });\n  const currentAdapters = filteredAdapters.slice(startIndex, endIndex + 1);\n\n  // load adapters asynchronously\n  useEffect(() => {\n    fetchRegistryData(\"adapter\")\n      .then(setAdapters)\n      .catch((e) => {\n        setError(e);\n        console.error(e);\n      });\n  }, []);\n\n  const { filters: filterTools } = useToolbar({\n    resources: adapters ?? [],\n    addFilter,\n  });\n\n  const actionTool: Action = {\n    label: \"发布适配器\",\n    icon: [\"fas\", \"plus\"],\n    onClick: () => {\n      setIsOpenModal(true);\n    },\n  };\n\n  const onCardClick = useCallback((adapter: Adapter) => {\n    setClickedAdapter(adapter);\n    setIsOpenCardModal(true);\n  }, []);\n\n  const onCardTagClick = useCallback(\n    (tag: string) => {\n      addFilter(tagFilter(tag));\n    },\n    [addFilter]\n  );\n\n  const onCardAuthorClick = useCallback(\n    (author: string) => {\n      addFilter(authorFilter(author));\n    },\n    [addFilter]\n  );\n\n  return (\n    <>\n      <p className=\"store-description\">\n        {adapterCount === filteredAdapterCount ? (\n          <Translate\n            id=\"pages.store.adapter.info\"\n            description=\"Adapters info of the adapter store page\"\n            values={{ adapterCount }}\n          >\n            {\"当前共有 {adapterCount} 个适配器\"}\n          </Translate>\n        ) : (\n          <Translate\n            id=\"pages.store.adapter.searchInfo\"\n            description=\"Adapters search info of the adapter store page\"\n            values={{\n              adapterCount,\n              filteredAdapterCount,\n            }}\n          >\n            {\"当前共有 {filteredAdapterCount} / {adapterCount} 个适配器\"}\n          </Translate>\n        )}\n      </p>\n\n      <Searcher\n        className=\"store-searcher not-prose\"\n        onChange={onSearchQueryChange}\n        onSubmit={onSearchQuerySubmit}\n        onBackspace={onSearchBackspace}\n        onClear={onSearchClear}\n        onTagClick={onSearchTagClick}\n        tags={searcherTags}\n        disabled={loading}\n      />\n\n      <StoreToolbar\n        className=\"not-prose\"\n        filters={filterTools}\n        action={actionTool}\n      />\n\n      {error ? (\n        <Admonition type=\"caution\" title={loadFailedTitle}>\n          {error.message}\n        </Admonition>\n      ) : loading ? (\n        <p className=\"store-loading-container\">\n          <span className=\"loading loading-dots loading-lg store-loading\" />\n        </p>\n      ) : (\n        <div className=\"store-container\">\n          {currentAdapters.map((adapter, index) => (\n            <ResourceCard\n              key={index}\n              className=\"not-prose\"\n              resource={adapter}\n              onClick={() => onCardClick(adapter)}\n              onTagClick={onCardTagClick}\n              onAuthorClick={() => onCardAuthorClick(adapter.author)}\n            />\n          ))}\n        </div>\n      )}\n\n      <Paginate\n        className=\"not-prose\"\n        totalPages={totalPages}\n        currentPage={currentPage}\n        setNextPage={setNextPage}\n        setPreviousPage={setPreviousPage}\n        setPage={setPage}\n        nextEnabled={nextEnabled}\n        previousEnabled={previousEnabled}\n      />\n      {isOpenModal && (\n        <Modal\n          className=\"not-prose\"\n          title=\"发布适配器\"\n          setOpenModal={setIsOpenModal}\n        >\n          <AdapterForm />\n        </Modal>\n      )}\n      {isOpenCardModal && (\n        <Modal\n          className=\"not-prose\"\n          title=\"适配器详情\"\n          backdropExit\n          setOpenModal={setIsOpenCardModal}\n        >\n          {clickedAdapter && <ResourceDetailCard resource={clickedAdapter} />}\n        </Modal>\n      )}\n    </>\n  );\n}\n"
  },
  {
    "path": "website/src/components/Store/Content/Bot.tsx",
    "content": "import React, { useCallback, useEffect, useState } from \"react\";\n\nimport Translate from \"@docusaurus/Translate\";\nimport { usePagination } from \"react-use-pagination\";\n\nimport Admonition from \"@theme/Admonition\";\nimport BotForm from \"@/components/Form/Bot\";\nimport Modal from \"@/components/Modal\";\nimport Paginate from \"@/components/Paginate\";\nimport ResourceCard from \"@/components/Resource/Card\";\nimport Searcher from \"@/components/Searcher\";\nimport StoreToolbar, { type Action } from \"@/components/Store/Toolbar\";\nimport { authorFilter, tagFilter } from \"@/libs/filter\";\nimport { useSearchControl } from \"@/libs/search\";\nimport { fetchRegistryData, loadFailedTitle } from \"@/libs/store\";\nimport { useToolbar } from \"@/libs/toolbar\";\n\nimport type { Bot } from \"@/types/bot\";\n\nexport default function PluginPage(): React.ReactNode {\n  const [bots, setBots] = useState<Bot[] | null>(null);\n  const botCount = bots?.length ?? 0;\n  const loading = bots === null;\n\n  const [error, setError] = useState<Error | null>(null);\n  const [isOpenModal, setIsOpenModal] = useState<boolean>(false);\n\n  const {\n    filteredResources: filteredBots,\n    searcherTags,\n    addFilter,\n    onSearchQueryChange,\n    onSearchQuerySubmit,\n    onSearchBackspace,\n    onSearchClear,\n    onSearchTagClick,\n  } = useSearchControl<Bot>(bots ?? []);\n  const filteredBotCount = filteredBots.length;\n\n  const {\n    startIndex,\n    endIndex,\n    totalPages,\n    currentPage,\n    setNextPage,\n    setPreviousPage,\n    setPage,\n    previousEnabled,\n    nextEnabled,\n  } = usePagination({\n    totalItems: filteredBots.length,\n    initialPageSize: 12,\n  });\n  const currentBots = filteredBots.slice(startIndex, endIndex + 1);\n\n  // load bots asynchronously\n  useEffect(() => {\n    fetchRegistryData(\"bot\")\n      .then(setBots)\n      .catch((e) => {\n        setError(e);\n        console.error(e);\n      });\n  }, []);\n\n  const { filters: filterTools } = useToolbar({\n    resources: bots ?? [],\n    addFilter,\n  });\n\n  const actionTool: Action = {\n    label: \"发布机器人\",\n    icon: [\"fas\", \"plus\"],\n    onClick: () => {\n      setIsOpenModal(true);\n    },\n  };\n\n  const onCardTagClick = useCallback(\n    (tag: string) => {\n      addFilter(tagFilter(tag));\n    },\n    [addFilter]\n  );\n\n  const onAuthorClick = useCallback(\n    (author: string) => {\n      addFilter(authorFilter(author));\n    },\n    [addFilter]\n  );\n\n  return (\n    <>\n      <p className=\"store-description\">\n        {botCount === filteredBotCount ? (\n          <Translate\n            id=\"pages.store.bot.info\"\n            description=\"Bots info of the bot store page\"\n            values={{ botCount }}\n          >\n            {\"当前共有 {botCount} 个机器人\"}\n          </Translate>\n        ) : (\n          <Translate\n            id=\"pages.store.bot.searchInfo\"\n            description=\"Bots search info of the bot store page\"\n            values={{\n              botCount,\n              filteredBotCount,\n            }}\n          >\n            {\"当前共有 {filteredBotCount} / {botCount} 个机器人\"}\n          </Translate>\n        )}\n      </p>\n\n      <Searcher\n        className=\"store-searcher not-prose\"\n        onChange={onSearchQueryChange}\n        onSubmit={onSearchQuerySubmit}\n        onBackspace={onSearchBackspace}\n        onClear={onSearchClear}\n        onTagClick={onSearchTagClick}\n        tags={searcherTags}\n        disabled={loading}\n      />\n\n      <StoreToolbar\n        className=\"not-prose\"\n        filters={filterTools}\n        action={actionTool}\n      />\n\n      {error ? (\n        <Admonition type=\"caution\" title={loadFailedTitle}>\n          {error.message}\n        </Admonition>\n      ) : loading ? (\n        <p className=\"store-loading-container\">\n          <span className=\"loading loading-dots loading-lg store-loading\" />\n        </p>\n      ) : (\n        <div className=\"store-container\">\n          {currentBots.map((bot, index) => (\n            <ResourceCard\n              key={index}\n              className=\"not-prose\"\n              resource={bot}\n              onTagClick={onCardTagClick}\n              onAuthorClick={() => onAuthorClick(bot.author)}\n            />\n          ))}\n        </div>\n      )}\n\n      <Paginate\n        className=\"not-prose\"\n        totalPages={totalPages}\n        currentPage={currentPage}\n        setNextPage={setNextPage}\n        setPreviousPage={setPreviousPage}\n        setPage={setPage}\n        nextEnabled={nextEnabled}\n        previousEnabled={previousEnabled}\n      />\n      {isOpenModal && (\n        <Modal\n          className=\"not-prose\"\n          title=\"发布机器人\"\n          setOpenModal={setIsOpenModal}\n        >\n          <BotForm />\n        </Modal>\n      )}\n    </>\n  );\n}\n"
  },
  {
    "path": "website/src/components/Store/Content/Driver.tsx",
    "content": "import React, { useCallback, useEffect, useState } from \"react\";\n\nimport Translate from \"@docusaurus/Translate\";\nimport { usePagination } from \"react-use-pagination\";\n\nimport Admonition from \"@theme/Admonition\";\nimport Modal from \"@/components/Modal\";\nimport Paginate from \"@/components/Paginate\";\nimport ResourceCard from \"@/components/Resource/Card\";\nimport ResourceDetailCard from \"@/components/Resource/DetailCard\";\nimport Searcher from \"@/components/Searcher\";\nimport { authorFilter, tagFilter } from \"@/libs/filter\";\nimport { useSearchControl } from \"@/libs/search\";\nimport { fetchRegistryData, loadFailedTitle } from \"@/libs/store\";\n\nimport type { Driver } from \"@/types/driver\";\n\nexport default function DriverPage(): React.ReactNode {\n  const [drivers, setDrivers] = useState<Driver[] | null>(null);\n  const driverCount = drivers?.length ?? 0;\n  const loading = drivers === null;\n\n  const [error, setError] = useState<Error | null>(null);\n  const [isOpenCardModal, setIsOpenCardModal] = useState<boolean>(false);\n  const [clickedDriver, setClickedDriver] = useState<Driver | null>(null);\n\n  const {\n    filteredResources: filteredDrivers,\n    searcherTags,\n    addFilter,\n    onSearchQueryChange,\n    onSearchQuerySubmit,\n    onSearchBackspace,\n    onSearchClear,\n    onSearchTagClick,\n  } = useSearchControl<Driver>(drivers ?? []);\n  const filteredDriverCount = filteredDrivers.length;\n\n  const {\n    startIndex,\n    endIndex,\n    totalPages,\n    currentPage,\n    setNextPage,\n    setPreviousPage,\n    setPage,\n    previousEnabled,\n    nextEnabled,\n  } = usePagination({\n    totalItems: filteredDrivers.length,\n    initialPageSize: 12,\n  });\n  const currentDrivers = filteredDrivers.slice(startIndex, endIndex + 1);\n\n  // load drivers asynchronously\n  useEffect(() => {\n    fetchRegistryData(\"driver\")\n      .then(setDrivers)\n      .catch((e) => {\n        setError(e);\n        console.error(e);\n      });\n  }, []);\n\n  const onCardClick = useCallback((driver: Driver) => {\n    setClickedDriver(driver);\n    setIsOpenCardModal(true);\n  }, []);\n\n  const onCardTagClick = useCallback(\n    (tag: string) => {\n      addFilter(tagFilter(tag));\n    },\n    [addFilter]\n  );\n\n  const onAuthorClick = useCallback(\n    (author: string) => {\n      addFilter(authorFilter(author));\n    },\n    [addFilter]\n  );\n\n  return (\n    <>\n      <p className=\"store-description\">\n        {driverCount === filteredDriverCount ? (\n          <Translate\n            id=\"pages.store.driver.info\"\n            description=\"Drivers info of the driver store page\"\n            values={{ driverCount }}\n          >\n            {\"当前共有 {driverCount} 个驱动器\"}\n          </Translate>\n        ) : (\n          <Translate\n            id=\"pages.store.driver.searchInfo\"\n            description=\"Drivers search info of the driver store page\"\n            values={{\n              driverCount,\n              filteredDriverCount,\n            }}\n          >\n            {\"当前共有 {filteredDriverCount} / {driverCount} 个驱动器\"}\n          </Translate>\n        )}\n      </p>\n\n      <Searcher\n        className=\"store-searcher not-prose\"\n        onChange={onSearchQueryChange}\n        onSubmit={onSearchQuerySubmit}\n        onBackspace={onSearchBackspace}\n        onClear={onSearchClear}\n        onTagClick={onSearchTagClick}\n        tags={searcherTags}\n        disabled={loading}\n      />\n\n      {error ? (\n        <Admonition type=\"caution\" title={loadFailedTitle}>\n          {error.message}\n        </Admonition>\n      ) : loading ? (\n        <p className=\"store-loading-container\">\n          <span className=\"loading loading-dots loading-lg store-loading\" />\n        </p>\n      ) : (\n        <div className=\"store-container\">\n          {currentDrivers.map((driver, index) => (\n            <ResourceCard\n              key={index}\n              className=\"not-prose\"\n              resource={driver}\n              onClick={() => onCardClick(driver)}\n              onTagClick={onCardTagClick}\n              onAuthorClick={() => onAuthorClick(driver.author)}\n            />\n          ))}\n        </div>\n      )}\n\n      <Paginate\n        className=\"not-prose\"\n        totalPages={totalPages}\n        currentPage={currentPage}\n        setNextPage={setNextPage}\n        setPreviousPage={setPreviousPage}\n        setPage={setPage}\n        nextEnabled={nextEnabled}\n        previousEnabled={previousEnabled}\n      />\n      {isOpenCardModal && (\n        <Modal\n          className=\"not-prose\"\n          useCustomTitle\n          backdropExit\n          title=\"驱动器详情\"\n          setOpenModal={setIsOpenCardModal}\n        >\n          {clickedDriver && <ResourceDetailCard resource={clickedDriver} />}\n        </Modal>\n      )}\n    </>\n  );\n}\n"
  },
  {
    "path": "website/src/components/Store/Content/Plugin.tsx",
    "content": "import React, { useCallback, useEffect, useState } from \"react\";\n\nimport Translate, { translate } from \"@docusaurus/Translate\";\nimport { usePagination } from \"react-use-pagination\";\n\nimport Admonition from \"@theme/Admonition\";\nimport PluginForm from \"@/components/Form/Plugin\";\nimport Modal from \"@/components/Modal\";\nimport Paginate from \"@/components/Paginate\";\nimport ResourceCard from \"@/components/Resource/Card\";\nimport ResourceDetailCard from \"@/components/Resource/DetailCard\";\nimport Searcher from \"@/components/Searcher\";\nimport StoreToolbar, {\n  type Action,\n  type Sorter,\n} from \"@/components/Store/Toolbar\";\nimport { authorFilter, tagFilter } from \"@/libs/filter\";\nimport { useSearchControl } from \"@/libs/search\";\nimport { SortMode } from \"@/libs/sorter\";\nimport { fetchRegistryData, loadFailedTitle } from \"@/libs/store\";\nimport { useToolbar } from \"@/libs/toolbar\";\n\nimport type { Plugin } from \"@/types/plugin\";\n\nexport default function PluginPage(): React.ReactNode {\n  const [plugins, setPlugins] = useState<Plugin[] | null>(null);\n  const pluginCount = plugins?.length ?? 0;\n  const loading = plugins === null;\n\n  const [error, setError] = useState<Error | null>(null);\n  const [isOpenModal, setIsOpenModal] = useState<boolean>(false);\n  const [isOpenCardModal, setIsOpenCardModal] = useState<boolean>(false);\n  const [clickedPlugin, setClickedPlugin] = useState<Plugin | null>(null);\n  const [sortMode, setSortMode] = useState<SortMode>(SortMode.Default);\n\n  const sorterTool: Sorter = {\n    label:\n      sortMode === SortMode.Default\n        ? translate({\n            id: \"pages.store.sorter.label.default\",\n            description: \"The label of default sorter\",\n            message: \"默认顺序\",\n          })\n        : translate({\n            id: \"pages.store.sorter.label.updateDesc\",\n            description: \"The label of updateDesc sorter\",\n            message: \"更新时间倒序\",\n          }),\n    icon: [\"fas\", \"sort-amount-down\"],\n    active: sortMode === SortMode.UpdateDesc,\n    onClick: () => {\n      setSortMode(\n        sortMode === SortMode.Default ? SortMode.UpdateDesc : SortMode.Default\n      );\n    },\n  };\n\n  const getSortedPlugins = (plugins: Plugin[]): Plugin[] => {\n    if (sortMode === SortMode.UpdateDesc) {\n      return [...plugins].sort(\n        (a, b) => new Date(b.time).getTime() - new Date(a.time).getTime()\n      );\n    }\n    return plugins;\n  };\n\n  const {\n    filteredResources: filteredPlugins,\n    searcherTags,\n    addFilter,\n    onSearchQueryChange,\n    onSearchQuerySubmit,\n    onSearchBackspace,\n    onSearchClear,\n    onSearchTagClick,\n  } = useSearchControl<Plugin>(getSortedPlugins(plugins ?? []));\n  const filteredPluginCount = filteredPlugins.length;\n\n  const {\n    startIndex,\n    endIndex,\n    totalPages,\n    currentPage,\n    setNextPage,\n    setPreviousPage,\n    setPage,\n    previousEnabled,\n    nextEnabled,\n  } = usePagination({\n    totalItems: filteredPlugins.length,\n    initialPageSize: 12,\n  });\n  const currentPlugins = filteredPlugins.slice(startIndex, endIndex + 1);\n\n  // load plugins asynchronously\n  useEffect(() => {\n    fetchRegistryData(\"plugin\")\n      .then(setPlugins)\n      .catch((e) => {\n        setError(e);\n        console.error(e);\n      });\n  }, []);\n\n  const { filters: filterTools } = useToolbar({\n    resources: plugins ?? [],\n    addFilter,\n  });\n\n  const actionTool: Action = {\n    label: \"发布插件\",\n    icon: [\"fas\", \"plus\"],\n    onClick: () => {\n      setIsOpenModal(true);\n    },\n  };\n\n  const onCardClick = useCallback((plugin: Plugin) => {\n    setClickedPlugin(plugin);\n    setIsOpenCardModal(true);\n  }, []);\n\n  const onCardTagClick = useCallback(\n    (tag: string) => {\n      addFilter(tagFilter(tag));\n    },\n    [addFilter]\n  );\n\n  const onCardAuthorClick = useCallback(\n    (author: string) => {\n      addFilter(authorFilter(author));\n    },\n    [addFilter]\n  );\n\n  return (\n    <>\n      <p className=\"store-description\">\n        {pluginCount === filteredPluginCount ? (\n          <Translate\n            id=\"pages.store.plugin.info\"\n            description=\"Plugins info of the plugin store page\"\n            values={{ pluginCount }}\n          >\n            {\"当前共有 {pluginCount} 个插件\"}\n          </Translate>\n        ) : (\n          <Translate\n            id=\"pages.store.plugin.searchInfo\"\n            description=\"Plugins search info of the plugin store page\"\n            values={{ pluginCount, filteredPluginCount }}\n          >\n            {\"当前共有 {filteredPluginCount} / {pluginCount} 个插件\"}\n          </Translate>\n        )}\n      </p>\n\n      <Searcher\n        className=\"store-searcher not-prose\"\n        onChange={onSearchQueryChange}\n        onSubmit={onSearchQuerySubmit}\n        onBackspace={onSearchBackspace}\n        onClear={onSearchClear}\n        onTagClick={onSearchTagClick}\n        tags={searcherTags}\n        disabled={loading}\n      />\n\n      <StoreToolbar\n        className=\"not-prose\"\n        filters={filterTools}\n        sorter={sorterTool}\n        action={actionTool}\n      />\n\n      {error ? (\n        <Admonition type=\"caution\" title={loadFailedTitle}>\n          {error.message}\n        </Admonition>\n      ) : loading ? (\n        <p className=\"store-loading-container\">\n          <span className=\"loading loading-dots loading-lg store-loading\" />\n        </p>\n      ) : (\n        <div className=\"store-container\">\n          {currentPlugins.map((plugin, index) => (\n            <ResourceCard\n              key={index}\n              className=\"not-prose\"\n              resource={plugin}\n              onClick={() => onCardClick(plugin)}\n              onTagClick={onCardTagClick}\n              onAuthorClick={() => onCardAuthorClick(plugin.author)}\n            />\n          ))}\n        </div>\n      )}\n\n      <Paginate\n        className=\"not-prose\"\n        totalPages={totalPages}\n        currentPage={currentPage}\n        setNextPage={setNextPage}\n        setPreviousPage={setPreviousPage}\n        setPage={setPage}\n        nextEnabled={nextEnabled}\n        previousEnabled={previousEnabled}\n      />\n      {isOpenModal && (\n        <Modal\n          className=\"not-prose\"\n          title=\"发布插件\"\n          setOpenModal={setIsOpenModal}\n        >\n          <PluginForm />\n        </Modal>\n      )}\n      {isOpenCardModal && (\n        <Modal\n          className=\"not-prose\"\n          useCustomTitle\n          backdropExit\n          title=\"插件详情\"\n          setOpenModal={setIsOpenCardModal}\n        >\n          {clickedPlugin && <ResourceDetailCard resource={clickedPlugin} />}\n        </Modal>\n      )}\n    </>\n  );\n}\n"
  },
  {
    "path": "website/src/components/Store/Layout.tsx",
    "content": "import React from \"react\";\n\nimport { useDocsVersionCandidates } from \"@docusaurus/plugin-content-docs/client\";\nimport { PageMetadata } from \"@docusaurus/theme-common\";\nimport { useVersionedSidebar } from \"@nullbot/docusaurus-plugin-getsidebar/client\";\nimport { SidebarContentFiller } from \"@nullbot/docusaurus-theme-nonepress/contexts\";\n\nimport BackToTopButton from \"@theme/BackToTopButton\";\nimport Heading from \"@theme/Heading\";\nimport Layout from \"@theme/Layout\";\nimport Page from \"@theme/Page\";\n\nimport \"./styles.css\";\n\nconst SIDEBAR_ID = \"ecosystem\";\n\ntype Props = {\n  title: string;\n  children: React.ReactNode;\n};\n\nfunction StorePage({ title, children }: Props): React.ReactNode {\n  const sidebarItems = useVersionedSidebar(\n    useDocsVersionCandidates()[0].name,\n    SIDEBAR_ID\n  )!;\n\n  return (\n    <Page hideTableOfContents reduceContentWidth={false} sidebarId={SIDEBAR_ID}>\n      <SidebarContentFiller items={sidebarItems} />\n      <article className=\"prose max-w-full\">\n        <Heading as=\"h1\" className=\"store-title\">\n          {title}\n        </Heading>\n        {children}\n      </article>\n    </Page>\n  );\n}\n\nexport default function StoreLayout({\n  title,\n  ...props\n}: Props): React.ReactNode {\n  return (\n    <>\n      <PageMetadata title={title} />\n\n      <Layout>\n        <BackToTopButton />\n\n        <StorePage title={title} {...props} />\n      </Layout>\n    </>\n  );\n}\n"
  },
  {
    "path": "website/src/components/Store/Toolbar.tsx",
    "content": "import React, { useState } from \"react\";\n\nimport clsx from \"clsx\";\n\nimport { FontAwesomeIcon } from \"@fortawesome/react-fontawesome\";\n\nimport type { IconProp } from \"@fortawesome/fontawesome-svg-core\";\n\nexport type Filter = {\n  label: string;\n  icon: IconProp;\n  choices?: string[];\n  onSubmit: (query: string) => void;\n};\n\nexport type Sorter = {\n  label: string;\n  icon: IconProp;\n  active: boolean;\n  onClick: () => void;\n};\n\nexport type Action = {\n  label: string;\n  icon: IconProp;\n  onClick: () => void;\n};\n\nexport type Props = {\n  filters?: Filter[];\n  sorter?: Sorter;\n  action?: Action;\n  className?: string;\n};\n\nfunction ToolbarFilter({\n  label,\n  icon,\n  choices,\n  onSubmit,\n}: Filter): React.ReactNode {\n  const [query, setQuery] = useState<string>(\"\");\n\n  const filteredChoices = choices\n    ?.filter((choice) => choice.toLowerCase().includes(query.toLowerCase()))\n    ?.slice(0, 5);\n\n  const handleQuerySubmit = () => {\n    if (filteredChoices && filteredChoices.length > 0) {\n      onSubmit(filteredChoices[0]);\n    } else if (choices === null) {\n      onSubmit(query);\n    }\n  };\n\n  const onQueryKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {\n    if (e.key === \"Enter\") {\n      handleQuerySubmit();\n      e.preventDefault();\n    }\n  };\n\n  const onChoiceKeyDown = (e: React.KeyboardEvent<HTMLLIElement>) => {\n    if (e.key === \"Enter\") {\n      onSubmit(e.currentTarget.innerText);\n      e.preventDefault();\n    }\n  };\n\n  return (\n    <div className=\"dropdown\">\n      <label\n        className=\"btn btn-sm btn-outline btn-primary no-animation\"\n        tabIndex={0}\n      >\n        <FontAwesomeIcon icon={icon} />\n        <span className=\"hidden sm:block\">{label}</span>\n      </label>\n      <div className=\"dropdown-content store-toolbar-dropdown\">\n        <input\n          type=\"text\"\n          placeholder=\"搜索\"\n          value={query}\n          onChange={(e) => setQuery(e.target.value)}\n          onKeyDown={onQueryKeyDown}\n          className=\"input input-sm input-bordered w-full\"\n        />\n        {filteredChoices && (\n          <ul className=\"menu menu-sm\">\n            {filteredChoices.map((choice, index) => (\n              <li\n                key={index}\n                onClick={() => onSubmit(choice)}\n                onKeyDown={onChoiceKeyDown}\n              >\n                <a tabIndex={0}>{choice}</a>\n              </li>\n            ))}\n          </ul>\n        )}\n      </div>\n    </div>\n  );\n}\n\nexport default function StoreToolbar({\n  filters,\n  sorter,\n  action,\n  className,\n}: Props): React.ReactNode | null {\n  if (!(filters && filters.length > 0) && !action) {\n    return null;\n  }\n\n  return (\n    <>\n      <div className={clsx(\"store-toolbar\", className)}>\n        <div className=\"store-toolbar-filters\">\n          {filters?.map((filter, index) => (\n            <ToolbarFilter key={index} {...filter} />\n          ))}\n          {sorter && (\n            <div className=\"store-toolbar-sorter store-toolbar-sorter-desktop\">\n              <button\n                className={clsx(\n                  \"btn btn-sm btn-primary no-animation mr-2\",\n                  !sorter.active && \"btn-outline\"\n                )}\n                onClick={sorter.onClick}\n              >\n                <FontAwesomeIcon icon={sorter.icon} />\n                {sorter.label}\n              </button>\n            </div>\n          )}\n        </div>\n\n        {action && (\n          <div className=\"store-toolbar-action\">\n            <button\n              className=\"btn btn-sm btn-primary no-animation\"\n              onClick={action.onClick}\n            >\n              <FontAwesomeIcon icon={action.icon} />\n              {action.label}\n            </button>\n          </div>\n        )}\n      </div>\n      <div className={clsx(\"store-toolbar store-toolbar-second\", className)}>\n        {sorter && (\n          <div className=\"store-toolbar-sorter\">\n            <button\n              className={clsx(\n                \"btn btn-sm btn-primary no-animation mr-2\",\n                !sorter.active && \"btn-outline\"\n              )}\n              onClick={sorter.onClick}\n            >\n              <FontAwesomeIcon icon={sorter.icon} />\n              {sorter.label}\n            </button>\n          </div>\n        )}\n      </div>\n    </>\n  );\n}\n"
  },
  {
    "path": "website/src/components/Store/styles.css",
    "content": ".store {\n  &-title {\n    @apply text-center;\n  }\n\n  &-description {\n    @apply text-center;\n  }\n\n  &-searcher {\n    @apply max-w-2xl mx-auto my-4;\n  }\n\n  &-toolbar {\n    @apply flex items-center justify-center my-4;\n\n    &-second {\n      @apply lg:hidden;\n    }\n\n    &-sorter {\n      @apply max-lg:flex-1;\n\n      &-desktop {\n        @apply max-lg:hidden;\n      }\n    }\n\n    &-filters {\n      @apply flex grow gap-2;\n    }\n\n    &-dropdown {\n      @apply w-36 z-10 m-0 p-2;\n      @apply rounded-md bg-base-100 shadow-lg border border-base-200;\n    }\n  }\n\n  &-loading {\n    @apply text-primary;\n\n    &-container {\n      @apply text-center;\n    }\n  }\n\n  &-container {\n    @apply grid grid-cols-[repeat(auto-fill,minmax(300px,1fr))] gap-6 mt-4 mb-8;\n  }\n}\n"
  },
  {
    "path": "website/src/components/Tag/index.tsx",
    "content": "import React from \"react\";\n\nimport clsx from \"clsx\";\n\nimport \"./styles.css\";\n\nimport { pickTextColor } from \"@/libs/color\";\n\nimport type { Tag as TagType } from \"@/types/tag\";\n\nexport default function Tag({\n  label,\n  color,\n  className,\n  onClick,\n}: TagType & {\n  className?: string;\n  onClick?: React.MouseEventHandler<HTMLSpanElement>;\n}): React.ReactNode {\n  return (\n    <span\n      className={clsx(\"tag\", className)}\n      style={{\n        backgroundColor: color,\n        color: pickTextColor(color, \"#fff\", \"#000\"),\n      }}\n      onClick={onClick}\n    >\n      {label}\n    </span>\n  );\n}\n"
  },
  {
    "path": "website/src/components/Tag/styles.css",
    "content": ".tag {\n  @apply font-mono inline-flex px-3 rounded-full items-center align-middle;\n}\n"
  },
  {
    "path": "website/src/libs/color.ts",
    "content": "/**\n * Choose fg color by bg color\n * @see https://www.npmjs.com/package/colord\n * @see https://www.w3.org/TR/AERT/#color-contrast\n */\nexport function pickTextColor(\n  bgColor: string,\n  lightColor: string,\n  darkColor: string\n) {\n  const color = bgColor.charAt(0) === \"#\" ? bgColor.substring(1, 7) : bgColor;\n  const r = parseInt(color.substring(0, 2), 16); // hexToR\n  const g = parseInt(color.substring(2, 4), 16); // hexToG\n  const b = parseInt(color.substring(4, 6), 16); // hexToB\n  return r * 0.299 + g * 0.587 + b * 0.114 > 186 ? darkColor : lightColor;\n}\n"
  },
  {
    "path": "website/src/libs/filter.ts",
    "content": "import { useCallback, useState } from \"react\";\n\nimport { translate } from \"@docusaurus/Translate\";\n\nimport { getValidStatus } from \"@/components/Resource/ValidStatus\";\n\nimport { ValidStatus } from \"./valid\";\n\nimport type { Resource } from \"./store\";\n\nexport type Filter<T extends Resource = Resource> = {\n  type: string;\n  id: string;\n  displayName?: string;\n  filter: (resource: T) => boolean;\n};\n\nconst validStatusDisplayName = {\n  [ValidStatus.VALID]: translate({\n    id: \"pages.store.filter.validateStatusDisplayName.valid\",\n    description: \"The display name of validateStatus filter\",\n    message: \"状态: 通过\",\n  }),\n  [ValidStatus.INVALID]: translate({\n    id: \"pages.store.filter.validateStatusDisplayName.invalid\",\n    description: \"The display name of validateStatus filter\",\n    message: \"状态: 未通过\",\n  }),\n  [ValidStatus.SKIP]: translate({\n    id: \"pages.store.filter.validateStatusDisplayName.skip\",\n    description: \"The display name of validateStatus filter\",\n    message: \"状态: 跳过\",\n  }),\n  [ValidStatus.MISSING]: translate({\n    id: \"pages.store.filter.validateStatusDisplayName.missing\",\n    description: \"The display name of validateStatus filter\",\n    message: \"状态: 缺失\",\n  }),\n};\n\nexport const validStatusFilter = <T extends Resource = Resource>(\n  validStatus: ValidStatus\n): Filter<T> => ({\n  type: \"validStatus\",\n  id: `validStatus-${validStatus}`,\n  displayName: validStatusDisplayName[validStatus],\n  filter: (resource: Resource): boolean =>\n    resource.resourceType === \"plugin\"\n      ? getValidStatus(resource) === validStatus\n      : true,\n});\n\nexport const tagFilter = <T extends Resource = Resource>(\n  tag: string\n): Filter<T> => ({\n  type: \"tag\",\n  id: `tag-${tag}`,\n  displayName: translate(\n    {\n      id: \"pages.store.filter.tagDisplayName\",\n      description: \"The display name of tag filter\",\n      message: \"标签: {tag}\",\n    },\n    { tag }\n  ),\n  filter: (resource: Resource): boolean =>\n    resource.tags.map((tag) => tag.label).includes(tag),\n});\n\nexport const officialFilter = <T extends Resource = Resource>(\n  official: boolean = true\n): Filter<T> => ({\n  type: \"official\",\n  id: `official-${official}`,\n  displayName: translate({\n    id: \"pages.store.filter.officialDisplayName\",\n    description: \"The display name of official filter\",\n    message: \"非官方|官方\",\n  }).split(\"|\")[Number(official)],\n  filter: (resource: Resource): boolean => resource.is_official === official,\n});\n\nexport const authorFilter = <T extends Resource = Resource>(\n  author: string\n): Filter<T> => ({\n  type: \"author\",\n  id: `author-${author}`,\n  displayName: translate(\n    {\n      id: \"pages.store.filter.authorDisplayName\",\n      description: \"The display name of author filter\",\n      message: \"作者: {author}\",\n    },\n    { author }\n  ),\n  filter: (resource: Resource): boolean => resource.author === author,\n});\n\nexport const queryFilter = <T extends Resource = Resource>(\n  query: string\n): Filter<T> => ({\n  type: \"query\",\n  id: `query-${query}`,\n  displayName: query,\n  filter: (resource: Resource): boolean => {\n    if (!query) {\n      return true;\n    }\n    const queryLower = query.toLowerCase();\n    const pluginMatch =\n      resource.resourceType === \"plugin\" &&\n      (resource.module_name?.toLowerCase().includes(queryLower) ||\n        resource.project_link?.toLowerCase().includes(queryLower));\n    const commonMatch =\n      resource.name.toLowerCase().includes(queryLower) ||\n      resource.desc.toLowerCase().includes(queryLower) ||\n      resource.author.toLowerCase().includes(queryLower) ||\n      resource.tags.filter((t) => t.label.toLowerCase().includes(queryLower))\n        .length > 0;\n    return pluginMatch || commonMatch;\n  },\n});\n\nexport function filterResources<T extends Resource>(\n  resources: T[],\n  filters: Filter<T>[]\n): T[] {\n  return resources.filter((resource) =>\n    filters.every((filter) => filter.filter(resource))\n  );\n}\n\ntype useFilteredResourcesReturn<T extends Resource> = {\n  filters: Filter<T>[];\n  addFilter: (filter: Filter<T>) => void;\n  removeFilter: (filter: Filter<T> | string) => void;\n  filteredResources: T[];\n};\n\nexport function useFilteredResources<T extends Resource>(\n  resources: T[]\n): useFilteredResourcesReturn<T> {\n  const [filters, setFilters] = useState<Filter<T>[]>([]);\n\n  const addFilter = useCallback(\n    (filter: Filter<T>) => {\n      if (filters.some((f) => f.id === filter.id)) {\n        return;\n      }\n      setFilters((filters) => [...filters, filter]);\n    },\n    [filters, setFilters]\n  );\n  const removeFilter = useCallback(\n    (filter: Filter<T> | string) => {\n      setFilters((filters) =>\n        filters.filter((f) =>\n          typeof filter === \"string\" ? f.id !== filter : f !== filter\n        )\n      );\n    },\n    [setFilters]\n  );\n\n  const filteredResources = useCallback(\n    () => filterResources(resources, filters),\n    [resources, filters]\n  );\n\n  return {\n    filters,\n    addFilter,\n    removeFilter,\n    filteredResources: filteredResources(),\n  };\n}\n"
  },
  {
    "path": "website/src/libs/search.ts",
    "content": "import { useCallback, useEffect, useState } from \"react\";\n\nimport { type Filter, useFilteredResources, queryFilter } from \"./filter\";\n\nimport type { Resource } from \"./store\";\n\ntype useSearchControlReturn<T extends Resource> = {\n  filteredResources: T[];\n  searcherTags: string[];\n  addFilter: (filter: Filter<T>) => void;\n  onSearchQueryChange: (query: string) => void;\n  onSearchQuerySubmit: () => void;\n  onSearchBackspace: () => void;\n  onSearchClear: () => void;\n  onSearchTagClick: (index: number) => void;\n};\n\nexport function useSearchControl<T extends Resource>(\n  resources: T[]\n): useSearchControlReturn<T> {\n  const [currentFilter, setCurrentFilter] = useState<Filter<T> | null>(null);\n\n  const { filters, addFilter, removeFilter, filteredResources } =\n    useFilteredResources(resources);\n\n  // display tags in searcher (except current filter)\n  const [searcherFilters, setSearcherFilters] = useState<Filter<T>[]>([]);\n\n  useEffect(() => {\n    setSearcherFilters(\n      filters.filter((f) => !(currentFilter && f === currentFilter))\n    );\n  }, [filters, currentFilter]);\n\n  const onSearchQueryChange = useCallback(\n    (newQuery: string) => {\n      // remove current filter if query is empty\n      if (newQuery === \"\") {\n        currentFilter && removeFilter(currentFilter);\n        setCurrentFilter(null);\n        return;\n      }\n\n      const newFilter = queryFilter<T>(newQuery);\n      // do nothing if filter is not changed\n      if (currentFilter?.id === newFilter.id) {\n        return;\n      }\n\n      // remove old currentFilter\n      currentFilter && removeFilter(currentFilter);\n      // add new filter\n      setCurrentFilter(newFilter);\n      addFilter(newFilter);\n    },\n    [currentFilter, setCurrentFilter, addFilter, removeFilter]\n  );\n\n  const onSearchQuerySubmit = useCallback(() => {\n    // set current filter to null to make filter permanent\n    setCurrentFilter(null);\n  }, [setCurrentFilter]);\n\n  const onSearchBackspace = useCallback(() => {\n    // remove last filter\n    removeFilter(searcherFilters[searcherFilters.length - 1]);\n  }, [removeFilter, searcherFilters]);\n\n  const onSearchClear = useCallback(() => {\n    // remove all filters\n    searcherFilters.forEach((filter) => removeFilter(filter));\n  }, [removeFilter, searcherFilters]);\n\n  const onSearchTagClick = useCallback(\n    (index: number) => {\n      removeFilter(searcherFilters[index]);\n    },\n    [removeFilter, searcherFilters]\n  );\n\n  return {\n    filteredResources,\n    searcherTags: searcherFilters.map((filter) => filter.displayName),\n    addFilter,\n    onSearchQueryChange,\n    onSearchQuerySubmit,\n    onSearchBackspace,\n    onSearchClear,\n    onSearchTagClick,\n  };\n}\n"
  },
  {
    "path": "website/src/libs/sorter.ts",
    "content": "export enum SortMode {\n  Default,\n  UpdateDesc,\n}\n"
  },
  {
    "path": "website/src/libs/store.ts",
    "content": "import { translate } from \"@docusaurus/Translate\";\n\nimport type { Adapter, AdaptersResponse } from \"@/types/adapter\";\nimport type { Bot, BotsResponse } from \"@/types/bot\";\nimport type { Driver, DriversResponse } from \"@/types/driver\";\nimport type { Plugin, PluginsResponse } from \"@/types/plugin\";\n\ntype RegistryDataResponseTypes = {\n  adapter: AdaptersResponse;\n  bot: BotsResponse;\n  driver: DriversResponse;\n  plugin: PluginsResponse;\n};\ntype RegistryDataType = keyof RegistryDataResponseTypes;\n\ntype ResourceTypes = {\n  adapter: Adapter;\n  bot: Bot;\n  driver: Driver;\n  plugin: Plugin;\n};\n\nexport type Resource = Adapter | Bot | Driver | Plugin;\n\nexport async function fetchRegistryData<T extends RegistryDataType>(\n  dataType: T\n): Promise<ResourceTypes[T][]> {\n  const resp = await fetch(\n    `https://registry.nonebot.dev/${dataType}s.json`\n  ).catch((e) => {\n    throw new Error(`Failed to fetch ${dataType}s: ${e}`);\n  });\n  if (!resp.ok) {\n    throw new Error(\n      `Failed to fetch ${dataType}s: ${resp.status} ${resp.statusText}`\n    );\n  }\n  const data = (await resp.json()) as RegistryDataResponseTypes[T];\n  return data.map(\n    (resource) => ({ ...resource, resourceType: dataType }) as ResourceTypes[T]\n  );\n}\n\nexport const loadFailedTitle = translate({\n  id: \"pages.store.loadFailed.title\",\n  message: \"加载失败\",\n  description: \"Title to display when loading content failed\",\n});\n"
  },
  {
    "path": "website/src/libs/toolbar.ts",
    "content": "import { translate } from \"@docusaurus/Translate\";\n\nimport type { Filter as FilterTool } from \"@/components/Store/Toolbar\";\n\nimport {\n  authorFilter,\n  tagFilter,\n  validStatusFilter,\n  type Filter,\n} from \"./filter\";\nimport { ValidStatus } from \"./valid\";\n\nimport type { Resource } from \"./store\";\n\ntype Props<T extends Resource = Resource> = {\n  resources: T[];\n  addFilter: (filter: Filter<T>) => void;\n};\n\ntype useToolbarReturns = {\n  filters: FilterTool[];\n};\n\nexport function useToolbar<T extends Resource = Resource>({\n  resources,\n  addFilter,\n}: Props<T>): useToolbarReturns {\n  const authorFilterTool: FilterTool = {\n    label: \"作者\",\n    icon: [\"fas\", \"user\"],\n    choices: Array.from(new Set(resources.map((resource) => resource.author))),\n    onSubmit: (author: string) => {\n      addFilter(authorFilter(author));\n    },\n  };\n\n  const tagFilterTool: FilterTool = {\n    label: \"标签\",\n    icon: [\"fas\", \"tag\"],\n    choices: Array.from(\n      new Set(\n        resources.flatMap((resource) => resource.tags.map((tag) => tag.label))\n      )\n    ),\n    onSubmit: (tag: string) => {\n      addFilter(tagFilter(tag));\n    },\n  };\n\n  const validateStatusFilterMapping: Record<string, ValidStatus> = {\n    [translate({\n      id: \"pages.store.filter.validateStatusDisplayName.valid\",\n      description: \"The display name of validateStatus filter\",\n      message: \"通过\",\n    })]: ValidStatus.VALID,\n    [translate({\n      id: \"pages.store.filter.validateStatusDisplayName.invalid\",\n      description: \"The display name of validateStatus filter\",\n      message: \"未通过\",\n    })]: ValidStatus.INVALID,\n    [translate({\n      id: \"pages.store.filter.validateStatusDisplayName.skip\",\n      description: \"The display name of validateStatus filter\",\n      message: \"跳过\",\n    })]: ValidStatus.SKIP,\n    [translate({\n      id: \"pages.store.filter.validateStatusDisplayName.missing\",\n      description: \"The display name of validateStatus filter\",\n      message: \"缺失\",\n    })]: ValidStatus.MISSING,\n  };\n\n  const validStatusFilterTool: FilterTool = {\n    label: \"状态\",\n    icon: [\"fas\", \"plug\"],\n    choices: Object.keys(validateStatusFilterMapping),\n    onSubmit: (type: string) => {\n      const validStatus = validateStatusFilterMapping[type];\n      if (!validStatus) {\n        return;\n      }\n      addFilter(validStatusFilter(validStatus));\n    },\n  };\n\n  return {\n    filters: [authorFilterTool, tagFilterTool, validStatusFilterTool],\n  };\n}\n"
  },
  {
    "path": "website/src/libs/valid.ts",
    "content": "export enum ValidStatus {\n  VALID = \"valid\",\n  INVALID = \"invalid\",\n  SKIP = \"skip\",\n  MISSING = \"missing\",\n}\n"
  },
  {
    "path": "website/src/pages/index.tsx",
    "content": "import React from \"react\";\n\nimport Layout from \"@theme/Layout\";\nimport HomeContent from \"@/components/Home\";\n\nexport default function Homepage(): React.ReactNode {\n  return (\n    <Layout>\n      <HomeContent />\n    </Layout>\n  );\n}\n"
  },
  {
    "path": "website/src/pages/store/adapters.tsx",
    "content": "import React from \"react\";\n\nimport { translate } from \"@docusaurus/Translate\";\n\nimport AdapterPageContent from \"@/components/Store/Content/Adapter\";\nimport StoreLayout from \"@/components/Store/Layout\";\n\nexport default function StoreAdapters(): React.ReactNode {\n  const title = translate({\n    id: \"pages.store.adapter.title\",\n    message: \"适配器商店\",\n    description: \"Title for the adapter store page\",\n  });\n\n  return (\n    <StoreLayout title={title}>\n      <AdapterPageContent />\n    </StoreLayout>\n  );\n}\n"
  },
  {
    "path": "website/src/pages/store/bots.tsx",
    "content": "import React from \"react\";\n\nimport { translate } from \"@docusaurus/Translate\";\n\nimport BotPageContent from \"@/components/Store/Content/Bot\";\nimport StoreLayout from \"@/components/Store/Layout\";\n\nexport default function StoreBots(): React.ReactNode {\n  const title = translate({\n    id: \"pages.store.bot.title\",\n    message: \"机器人商店\",\n    description: \"Title for the bot store page\",\n  });\n\n  return (\n    <StoreLayout title={title}>\n      <BotPageContent />\n    </StoreLayout>\n  );\n}\n"
  },
  {
    "path": "website/src/pages/store/drivers.tsx",
    "content": "import React from \"react\";\n\nimport { translate } from \"@docusaurus/Translate\";\n\nimport DriverPageContent from \"@/components/Store/Content/Driver\";\nimport StoreLayout from \"@/components/Store/Layout\";\n\nexport default function StoreDrivers(): React.ReactNode {\n  const title = translate({\n    id: \"pages.store.driver.title\",\n    message: \"驱动器商店\",\n    description: \"Title for the driver store page\",\n  });\n\n  return (\n    <StoreLayout title={title}>\n      <DriverPageContent />\n    </StoreLayout>\n  );\n}\n"
  },
  {
    "path": "website/src/pages/store/index.tsx",
    "content": "import React from \"react\";\n\nimport { Redirect } from \"@docusaurus/router\";\n\nexport default function Store(): React.ReactNode {\n  return <Redirect to=\"/store/plugins\" />;\n}\n"
  },
  {
    "path": "website/src/pages/store/plugins.tsx",
    "content": "import React from \"react\";\n\nimport { translate } from \"@docusaurus/Translate\";\n\nimport PluginPageContent from \"@/components/Store/Content/Plugin\";\nimport StoreLayout from \"@/components/Store/Layout\";\n\nexport default function StorePlugins(): React.ReactNode {\n  const title = translate({\n    id: \"pages.store.plugin.title\",\n    message: \"插件商店\",\n    description: \"Title for the plugin store page\",\n  });\n\n  return (\n    <StoreLayout title={title}>\n      <PluginPageContent />\n    </StoreLayout>\n  );\n}\n"
  },
  {
    "path": "website/src/plugins/webpack-plugin.ts",
    "content": "import path from \"path\";\n\nimport type { PluginConfig } from \"@docusaurus/types\";\n\nexport default (function webpackPlugin() {\n  return {\n    name: \"webpack-plugin\",\n    configureWebpack() {\n      return {\n        resolve: {\n          alias: {\n            \"@\": path.resolve(__dirname, \"../\"),\n          },\n        },\n      };\n    },\n  };\n} satisfies PluginConfig);\n"
  },
  {
    "path": "website/src/theme/Footer/Copyright/index.tsx",
    "content": "import React from \"react\";\n\nimport Link from \"@docusaurus/Link\";\nimport Translate, { translate } from \"@docusaurus/Translate\";\n\nimport type { Props } from \"@theme/Footer/Copyright\";\nimport IconCloudflare from \"@theme/Icon/Cloudflare\";\nimport IconNetlify from \"@theme/Icon/Netlify\";\nimport OriginCopyright from \"@theme-original/Footer/Copyright\";\n\nexport default function FooterCopyright(props: Props) {\n  return (\n    <>\n      <OriginCopyright {...props} />\n      <div className=\"footer-support\">\n        <Translate\n          id=\"theme.FooterCopyright.deployBy\"\n          description=\"The deploy by message.\"\n        >\n          Deployed by\n        </Translate>\n        <Link\n          to=\"https://www.netlify.com/\"\n          title={translate({\n            id: \"theme.FooterCopyright.netlifyLinkTitle\",\n            message: \"Go to the Netlify website\",\n            description: \"The title attribute for the Netlify logo link\",\n          })}\n        >\n          <IconNetlify className=\"footer-support-icon\" />\n        </Link>\n        <Link\n          to=\"https://www.cloudflare.com/\"\n          title={translate({\n            id: \"theme.FooterCopyright.cloudflareLinkTitle\",\n            message: \"Go to the Cloudflare website\",\n            description: \"The title attribute for the Cloudflare logo link\",\n          })}\n        >\n          <IconCloudflare className=\"footer-support-icon\" />\n        </Link>\n      </div>\n    </>\n  );\n}\n"
  },
  {
    "path": "website/src/theme/Icon/Cloudflare.tsx",
    "content": "import React, { type ComponentProps } from \"react\";\n\nexport interface Props extends Omit<ComponentProps<\"svg\">, \"viewBox\"> {}\n\nexport default function IconCloudflare(props: Props): React.ReactNode {\n  return (\n    <svg\n      viewBox=\"0 0 651.29 94.76\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      {...props}\n    >\n      <path\n        fill=\"#f78100\"\n        d=\"M143.05,93.42l1.07-3.71c1.27-4.41.8-8.48-1.34-11.48-2-2.76-5.26-4.38-9.25-4.57L58,72.7a1.47,1.47,0,0,1-1.35-2,2,2,0,0,1,1.75-1.34l76.26-1c9-.41,18.84-7.75,22.27-16.71l4.34-11.36a2.68,2.68,0,0,0,.18-1,3.31,3.31,0,0,0-.06-.54,49.67,49.67,0,0,0-95.49-5.14,22.35,22.35,0,0,0-35,23.42A31.73,31.73,0,0,0,.34,93.45a1.47,1.47,0,0,0,1.45,1.27l139.49,0h0A1.83,1.83,0,0,0,143.05,93.42Z\"\n      />\n      <path\n        fill=\"#fcad32\"\n        d=\"M168.22,41.15q-1,0-2.1.06a.88.88,0,0,0-.32.07,1.17,1.17,0,0,0-.76.8l-3,10.26c-1.28,4.41-.81,8.48,1.34,11.48a11.65,11.65,0,0,0,9.24,4.57l16.11,1a1.44,1.44,0,0,1,1.14.62,1.5,1.5,0,0,1,.17,1.37,2,2,0,0,1-1.75,1.34l-16.73,1c-9.09.42-18.88,7.75-22.31,16.7l-1.21,3.16a.9.9,0,0,0,.79,1.22h57.63A1.55,1.55,0,0,0,208,93.63a41.34,41.34,0,0,0-39.76-52.48Z\"\n      />\n      <polygon points=\"273.03 59.66 282.56 59.66 282.56 85.72 299.23 85.72 299.23 94.07 273.03 94.07 273.03 59.66\" />\n      <path d=\"M309.11,77v-.09c0-9.88,8-17.9,18.58-17.9s18.48,7.92,18.48,17.8v.1c0,9.88-8,17.89-18.58,17.89S309.11,86.85,309.11,77m27.33,0v-.09c0-5-3.59-9.29-8.85-9.29s-8.7,4.22-8.7,9.19v.1c0,5,3.59,9.29,8.8,9.29s8.75-4.23,8.75-9.2\" />\n      <path d=\"M357.84,79V59.66h9.69V78.78c0,5,2.5,7.33,6.34,7.33s6.34-2.26,6.34-7.08V59.66h9.68V78.73c0,11.11-6.34,16-16.12,16s-15.93-5-15.93-15.73\" />\n      <path d=\"M404.49,59.66h13.27c12.29,0,19.42,7.08,19.42,17v.1c0,9.93-7.23,17.3-19.61,17.3H404.49Zm13.42,26c5.7,0,9.49-3.15,9.49-8.71v-.09c0-5.51-3.79-8.71-9.49-8.71H414V85.62Z\" />\n      <polygon points=\"451.04 59.66 478.56 59.66 478.56 68.02 460.58 68.02 460.58 73.87 476.85 73.87 476.85 81.78 460.58 81.78 460.58 94.07 451.04 94.07 451.04 59.66\" />\n      <polygon points=\"491.84 59.66 501.37 59.66 501.37 85.72 518.04 85.72 518.04 94.07 491.84 94.07 491.84 59.66\" />\n      <path d=\"M543,59.42h9.19L566.8,94.07H556.58l-2.51-6.14H540.79l-2.45,6.14h-10Zm8.35,21.08-3.83-9.78L543.6,80.5Z\" />\n      <path d=\"M579.08,59.66h16.27c5.27,0,8.9,1.38,11.21,3.74a10.64,10.64,0,0,1,3.05,8v.1a10.88,10.88,0,0,1-7.08,10.57l8.21,12h-11L592.8,83.65h-4.18V94.07h-9.54Zm15.83,16.52c3.25,0,5.12-1.58,5.12-4.08V72c0-2.71-2-4.08-5.17-4.08h-6.24v8.26Z\" />\n      <polygon points=\"623.37 59.66 651.05 59.66 651.05 67.77 632.81 67.77 632.81 72.98 649.33 72.98 649.33 80.5 632.81 80.5 632.81 85.96 651.29 85.96 651.29 94.07 623.37 94.07 623.37 59.66\" />\n      <path d=\"M252.15,81a8.44,8.44,0,0,1-7.88,5.16c-5.22,0-8.8-4.33-8.8-9.29v-.1c0-5,3.49-9.2,8.7-9.2a8.64,8.64,0,0,1,8.18,5.71h10C260.79,65.09,253.6,59,244.27,59c-10.62,0-18.58,8-18.58,17.9V77c0,9.88,7.86,17.8,18.48,17.8,9.08,0,16.18-5.88,18.05-13.76Z\" />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "website/src/theme/Icon/Netlify.tsx",
    "content": "import React, { type ComponentProps } from \"react\";\n\nexport interface Props extends Omit<ComponentProps<\"svg\">, \"viewBox\"> {}\n\nexport default function IconNetlify(props: Props): React.ReactNode {\n  return (\n    <svg viewBox=\"0 0 256 105\" xmlns=\"http://www.w3.org/2000/svg\" {...props}>\n      <g clipPath=\"url(#clip0_236_25)\">\n        <path\n          d=\"M58.4704 103.765V77.4144L59.0165 76.8683H65.6043L66.1504 77.4144V103.765L65.6043 104.311H59.0165L58.4704 103.765Z\"\n          fill=\"#05BDBA\"\n        />\n        <path\n          d=\"M58.4704 26.8971V0.546133L59.0165 0H65.6043L66.1504 0.546133V26.8971L65.6043 27.4432H59.0165L58.4704 26.8971Z\"\n          fill=\"#05BDBA\"\n        />\n        <path\n          d=\"M35.7973 85.2395H34.8928L30.3616 80.7083V79.8037L38.8523 71.3045L43.648 71.3131L44.288 71.9445V76.7403L35.7973 85.2395Z\"\n          fill=\"#05BDBA\"\n        />\n        <path\n          d=\"M30.3616 24.7467V23.8336L34.8928 19.3109H35.7973L44.288 27.8016V32.5888L43.648 33.2373H38.8523L30.3616 24.7467Z\"\n          fill=\"#05BDBA\"\n        />\n        <path\n          d=\"M0.546133 48.3072H37.8795L38.4256 48.8533V55.4496L37.8795 55.9958H0.546133L0 55.4496V48.8533L0.546133 48.3072Z\"\n          fill=\"#05BDBA\"\n        />\n        <path\n          d=\"M255.445 48.3157L255.991 48.8619V55.4496L255.445 55.9957H217.566L217.02 55.4496L219.759 48.8619L220.305 48.3157H255.445Z\"\n          fill=\"#05BDBA\"\n        />\n        <path\n          d=\"M74.6667 65.8859H68.0789L67.5328 65.3397V49.92C67.5328 47.1723 66.4576 45.0475 63.1467 44.9792C61.44 44.9365 59.4944 44.9792 57.4123 45.0645L57.0965 45.3803V65.3312L56.5504 65.8773H49.9627L49.4165 65.3312V38.9803L49.9627 38.4341H64.7851C70.5451 38.4341 75.2128 43.1019 75.2128 48.8619V65.3312L74.6667 65.8773V65.8859Z\"\n          fill=\"#014847\"\n        />\n        <path\n          d=\"M106.573 54.3488L106.027 54.8949H88.9941L88.448 55.4411C88.448 56.5419 89.5488 59.8357 93.9435 59.8357C95.5904 59.8357 97.2373 59.2896 97.792 58.1888L98.3381 57.6427H104.926L105.472 58.1888C104.926 61.4827 102.178 66.432 93.9349 66.432C84.5995 66.432 80.2048 59.8443 80.2048 52.1472C80.2048 44.4501 84.5995 37.8624 93.3888 37.8624C102.178 37.8624 106.573 44.4501 106.573 52.1472V54.3488ZM98.3296 48.8533C98.3296 48.3072 97.7835 44.4587 93.3888 44.4587C88.9941 44.4587 88.448 48.3072 88.448 48.8533L88.9941 49.3995H97.7835L98.3296 48.8533Z\"\n          fill=\"#014847\"\n        />\n        <path\n          d=\"M121.95 57.6427C121.95 58.7435 122.496 59.2896 123.597 59.2896H128.538L129.084 59.8358V65.3312L128.538 65.8773H123.597C118.656 65.8773 114.261 63.6758 114.261 57.6342V45.5509L113.715 45.0048H109.867L109.321 44.4587V38.9632L109.867 38.4171H113.715L114.261 37.8709V32.9301L114.807 32.384H121.395L121.941 32.9301V37.8709L122.487 38.4171H128.529L129.075 38.9632V44.4587L128.529 45.0048H122.487L121.941 45.5509V57.6342L121.95 57.6427Z\"\n          fill=\"#014847\"\n        />\n        <path\n          d=\"M142.276 65.8859H135.689L135.142 65.3397V27.9808L135.689 27.4347H142.276L142.822 27.9808V65.3312L142.276 65.8773V65.8859Z\"\n          fill=\"#014847\"\n        />\n        <path\n          d=\"M157.107 34.0224H150.519L149.973 33.4763V27.9808L150.519 27.4347H157.107L157.653 27.9808V33.4763L157.107 34.0224ZM157.107 65.8859H150.519L149.973 65.3397V38.9717L150.519 38.4256H157.107L157.653 38.9717V65.3397L157.107 65.8859Z\"\n          fill=\"#014847\"\n        />\n        <path\n          d=\"M182.929 27.9808V33.4763L182.383 34.0224H177.442C176.341 34.0224 175.795 34.5685 175.795 35.6693V37.8709L176.341 38.4171H181.837L182.383 38.9632V44.4587L181.837 45.0048H176.341L175.795 45.5509V65.3227L175.249 65.8688H168.661L168.115 65.3227V45.5509L167.569 45.0048H163.721L163.174 44.4587V38.9632L163.721 38.4171H167.569L168.115 37.8709V35.6693C168.115 29.6277 172.51 27.4261 177.451 27.4261H182.391L182.938 27.9723L182.929 27.9808Z\"\n          fill=\"#014847\"\n        />\n        <path\n          d=\"M203.247 66.432C201.045 71.9275 198.852 75.2213 191.164 75.2213H188.416L187.87 74.6752V69.1797L188.416 68.6336H191.164C193.911 68.6336 194.458 68.0875 195.012 66.4405V65.8944L186.223 44.4672V38.9717L186.769 38.4256H191.71L192.256 38.9717L198.844 57.6512H199.39L205.978 38.9717L206.524 38.4256H211.465L212.011 38.9717V44.4672L203.221 66.4405L203.247 66.432Z\"\n          fill=\"#014847\"\n        />\n      </g>\n      <defs>\n        <clipPath id=\"clip0_236_25\">\n          <rect width=\"256\" height=\"104.311\" fill=\"white\" />\n        </clipPath>\n      </defs>\n    </svg>\n  );\n}\n"
  },
  {
    "path": "website/src/theme/Page/TOC/Container/index.tsx",
    "content": "import React from \"react\";\n\nimport { useWindowSize } from \"@nullbot/docusaurus-theme-nonepress/client\";\n\nimport type { Props } from \"@theme/Page/TOC/Container\";\nimport OriginTOCContainer from \"@theme-original/Page/TOC/Container\";\n\nimport \"./styles.css\";\n\nexport default function TOCContainer({\n  children,\n  ...props\n}: Props): React.ReactNode {\n  const windowSize = useWindowSize();\n  const isClient = windowSize !== \"ssr\";\n\n  return (\n    <OriginTOCContainer {...props}>\n      {children}\n      {isClient && (\n        <div className=\"toc-ads-container\">\n          <div className=\"wwads-cn wwads-vertical toc-ads\" data-id=\"281\" />\n        </div>\n      )}\n    </OriginTOCContainer>\n  );\n}\n"
  },
  {
    "path": "website/src/theme/Page/TOC/Container/styles.css",
    "content": ".toc-ads {\n  @apply max-w-full !m-0 !bg-transparent;\n\n  & .wwads-text {\n    @apply !text-base-content;\n    @apply transition-[color] duration-500;\n  }\n\n  &-container {\n    @apply shrink-0 w-full max-w-full px-4;\n  }\n}\n"
  },
  {
    "path": "website/src/types/adapter.ts",
    "content": "import type { Tag } from \"./tag\";\n\ntype BaseAdapter = {\n  module_name: string;\n  project_link: string;\n  name: string;\n  desc: string;\n  author: string;\n  homepage: string;\n  tags: Tag[];\n  is_official: boolean;\n};\n\nexport type Adapter = { resourceType: \"adapter\" } & BaseAdapter;\n\nexport type AdaptersResponse = BaseAdapter[];\n"
  },
  {
    "path": "website/src/types/bot.ts",
    "content": "import type { Tag } from \"./tag\";\n\ntype BaseBot = {\n  name: string;\n  desc: string;\n  author: string;\n  homepage: string;\n  tags: Tag[];\n  is_official: boolean;\n};\n\nexport type Bot = { resourceType: \"bot\" } & BaseBot;\n\nexport type BotsResponse = BaseBot[];\n"
  },
  {
    "path": "website/src/types/driver.ts",
    "content": "import type { Tag } from \"./tag\";\n\ntype BaseDriver = {\n  module_name: string;\n  project_link: string;\n  name: string;\n  desc: string;\n  author: string;\n  homepage: string;\n  tags: Tag[];\n  is_official: boolean;\n};\n\nexport type Driver = { resourceType: \"driver\" } & BaseDriver;\n\nexport type DriversResponse = BaseDriver[];\n"
  },
  {
    "path": "website/src/types/plugin.ts",
    "content": "import type { Tag } from \"./tag\";\n\ntype BasePlugin = {\n  author: string;\n  name: string;\n  desc: string;\n  homepage: string;\n  is_official: boolean;\n  module_name: string;\n  project_link: string;\n  skip_test: boolean;\n  supported_adapters: string[] | null;\n  tags: Array<Tag>;\n  time: string;\n  type: string;\n  valid: boolean;\n  version: string;\n};\n\nexport type Plugin = { resourceType: \"plugin\" } & BasePlugin;\n\nexport type PluginsResponse = BasePlugin[];\n"
  },
  {
    "path": "website/src/types/tag.ts",
    "content": "export type Tag = {\n  label: string;\n  color: `#${string}`;\n};\n"
  },
  {
    "path": "website/static/manifest.json",
    "content": "{\n  \"name\": \"NoneBot\",\n  \"short_name\": \"NoneBot\",\n  \"background-color\": \"#ffffff\",\n  \"theme-color\": \"#ea5252\",\n  \"description\": \"跨平台 Python 异步聊天机器人框架\",\n  \"display\": \"standalone\",\n  \"icons\": [\n    {\n      \"src\": \"/icons/android-chrome-192x192.png\",\n      \"sizes\": \"192x192\",\n      \"type\": \"image/png\"\n    },\n    {\n      \"src\": \"/icons/android-chrome-384x384.png\",\n      \"sizes\": \"384x384\",\n      \"type\": \"image/png\"\n    }\n  ]\n}\n"
  },
  {
    "path": "website/static/service-worker.js",
    "content": "self.addEventListener(\"install\", () => {\n  self.skipWaiting();\n});\n\nself.addEventListener(\"activate\", () => {\n  self.registration\n    .unregister()\n    .then(() => {\n      return self.clients.matchAll();\n    })\n    .then((clients) => {\n      clients.forEach((client) => client.navigate(client.url));\n    });\n});\n"
  },
  {
    "path": "website/static/uwu.js",
    "content": "if (location.search.includes(\"?uwu\")) {\n  document.documentElement.setAttribute(\"data-uwu\", \"true\");\n}\n"
  },
  {
    "path": "website/tailwind.config.ts",
    "content": "import typography from \"@tailwindcss/typography\";\nimport daisyui from \"daisyui\";\nimport themes from \"daisyui/src/theming/themes\";\n\nconst lightTheme = themes.light;\nconst darkTheme = themes.dark;\n\nfunction excludeThemeColor(\n  theme: { [key: string]: string },\n  exclude: string[]\n): { [key: string]: string } {\n  const newObj: { [key: string]: string } = {};\n  for (const key in theme) {\n    if (exclude.includes(key)) {\n      continue;\n    }\n    newObj[key] = theme[key]!;\n  }\n  return newObj;\n}\n\nexport default {\n  plugins: [typography, daisyui],\n  daisyui: {\n    base: false,\n    themes: [\n      {\n        light: {\n          ...excludeThemeColor(lightTheme, [\n            \"primary-content\",\n            \"secondary-content\",\n            \"accent-content\",\n          ]),\n          primary: \"#ea5252\",\n          \"primary-content\": \"#ffffff\",\n          secondary: \"#ef9fbc\",\n          accent: \"#65c3c8\",\n        },\n      },\n      {\n        dark: {\n          ...excludeThemeColor(darkTheme, [\n            \"primary-content\",\n            \"secondary-content\",\n            \"accent-content\",\n          ]),\n          primary: \"#ea5252\",\n          \"primary-content\": \"#ffffff\",\n          secondary: \"#ef9fbc\",\n          accent: \"#65c3c8\",\n        },\n      },\n    ],\n    darkTheme: false,\n  },\n  darkMode: [\"class\", '[data-theme=\"dark\"]'],\n};\n"
  },
  {
    "path": "website/tsconfig.json",
    "content": "{\n  // This file is not used in compilation. It is here just for a nice editor experience.\n  \"extends\": \"@nullbot/docusaurus-tsconfig\",\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"ESNext\"],\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"./src/*\"],\n      \"@theme/*\": [\"./src/theme/*\"]\n    },\n    \"resolveJsonModule\": true,\n    \"allowArbitraryExtensions\": true,\n\n    // Duplicated from the root config, because TS does not support extending\n    // multiple configs and we want to dogfood the @docusaurus/tsconfig one\n    \"allowUnreachableCode\": false,\n    \"exactOptionalPropertyTypes\": false,\n    \"noFallthroughCasesInSwitch\": true,\n    \"noImplicitOverride\": true,\n    \"noImplicitReturns\": true,\n    \"noPropertyAccessFromIndexSignature\": false,\n    \"noUncheckedIndexedAccess\": true,\n    \"strict\": true,\n    \"alwaysStrict\": true,\n    \"noImplicitAny\": true,\n    \"noImplicitThis\": true,\n    \"strictBindCallApply\": true,\n    \"strictFunctionTypes\": true,\n    \"strictNullChecks\": true,\n    \"strictPropertyInitialization\": true,\n    \"useUnknownInCatchVariables\": true,\n    \"noUnusedLocals\": false,\n    \"noUnusedParameters\": false,\n    \"importsNotUsedAsValues\": \"remove\",\n\n    // This is important. We run `yarn tsc` in website so we can catch issues\n    // with our declaration files (mostly names that are forgotten to be\n    // imported, invalid semantics...). Because we don't have end-to-end type\n    // tests, removing this would make things much harder to catch.\n    \"skipLibCheck\": false\n  }\n}\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/README.md",
    "content": "---\nsidebar_position: 0\nid: index\nslug: /\n---\n\n# 概览\n\nNoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架（下称 NoneBot），它基于 Python 的类型注解和异步优先特性（兼容同步），能够为你的需求实现提供便捷灵活的支持。同时，NoneBot 拥有大量的开发者为其开发插件，用户无需编写任何代码，仅需完成环境配置及插件安装，就可以正常使用 NoneBot。\n\n需要注意的是，NoneBot 仅支持 **Python 3.9 以上版本**\n\n## 特色\n\n### 异步优先\n\nNoneBot 基于 Python [asyncio](https://docs.python.org/zh-cn/3/library/asyncio.html) / [trio](https://trio.readthedocs.io/en/stable/) 编写，并在异步机制的基础上进行了一定程度的同步函数兼容。\n\n### 完整的类型注解\n\nNoneBot 参考 [PEP 484](https://www.python.org/dev/peps/pep-0484/) 等 PEP 完整实现了类型注解，通过 Pyright（Pylance） 检查。配合编辑器的类型推导功能，能将绝大多数的 Bug 杜绝在编辑器中（[编辑器支持](./editor-support)）。\n\n### 开箱即用\n\nNoneBot 提供了使用便捷、具有交互式功能的命令行工具--`nb-cli`，使得用户初次接触 NoneBot 时更容易上手。使用方法请阅读本文档[指南](./quick-start.mdx)以及 [CLI 文档](https://cli.nonebot.dev/)。\n\n### 插件系统\n\n插件系统是 NoneBot 的核心，通过它可以实现机器人的模块化以及功能扩展，便于维护和管理。\n\n### 依赖注入系统\n\nNoneBot 采用了一套自行定义的依赖注入系统，可以让事件的处理过程更加的简洁、清晰，增加代码的可读性，减少代码冗余。\n\n#### 什么是依赖注入\n\n[**『依赖注入』**](https://zh.wikipedia.org/wiki/%E6%8E%A7%E5%88%B6%E5%8F%8D%E8%BD%AC)意思是，在编程中，有一种方法可以让你的代码声明它工作和使用所需要的东西，即**『依赖』**。\n\n系统（在这里是指 NoneBot）将负责做任何需要的事情，为你的代码提供这些必要依赖（即**『注入』**依赖性）\n\n这在你有以下情形的需求时非常有用：\n\n- 这部分代码拥有共享的逻辑（同样的代码逻辑多次重复）\n- 共享数据库以及网络请求连接会话\n  - 比如 `httpx.AsyncClient`、`aiohttp.ClientSession` 和 `sqlalchemy.Session`\n- 机器人用户权限检查以及认证\n- 还有更多...\n\n它在完成上述工作的同时，还能尽量减少代码的耦合和重复\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/advanced/adapter.md",
    "content": "---\nsidebar_position: 1\ndescription: 注册适配器与指定平台交互\n\noptions:\n  menu:\n    - category: advanced\n      weight: 20\n---\n\n# 使用适配器\n\n适配器 (Adapter) 是机器人与平台交互的核心桥梁，它负责在驱动器和机器人插件之间转换与传递消息。\n\n## 适配器功能与组成\n\n适配器通常有两种功能，分别是**接收事件**和**调用平台接口**。其中，接收事件是指将驱动器收到的事件消息转换为 NoneBot 定义的事件模型，然后交由机器人插件处理；调用平台接口是指将机器人插件调用平台接口的数据转换为平台指定的格式，然后交由驱动器发送，并接收接口返回数据。\n\n为了实现这两种功能，适配器通常由四个部分组成：\n\n- **Adapter**：负责转换事件和调用接口，正确创建 Bot 对象并注册到 NoneBot 中。\n- **Bot**：负责存储平台机器人相关信息，并提供回复事件的方法。\n- **Event**：负责定义事件内容，以及事件主体对象。\n- **Message**：负责正确序列化消息，以便机器人插件处理。\n\n## 注册适配器\n\n在使用适配器之前，我们需要先将适配器注册到驱动器中，这样适配器就可以通过驱动器接收事件和调用接口了。我们以 Console 适配器为例，来看看如何注册适配器：\n\n```python {2,5} title=bot.py\nimport nonebot\nfrom nonebot.adapters.console import Adapter\n\ndriver = nonebot.get_driver()\ndriver.register_adapter(Adapter)\n```\n\n我们首先需要从适配器模块中导入所需要的适配器类，然后通过驱动器的 `register_adapter` 方法将适配器注册到驱动器中即可。如果我们需要多平台支持，可以多次调用 `register_adapter` 方法来注册多个适配器。\n\n## 获取已注册的适配器\n\nNoneBot 提供了 `get_adapter` 方法来获取已注册的适配器，我们可以通过适配器的名称或类型来获取指定的适配器实例：\n\n```python\nimport nonebot\nfrom nonebot.adapters.console import Adapter\n\nadapters = nonebot.get_adapters()\nconsole_adapter = nonebot.get_adapter(Adapter)\nconsole_adapter = nonebot.get_adapter(Adapter.get_name())\n```\n\n## 获取 Bot 对象\n\n当前所有适配器已连接的 Bot 对象可以通过 `get_bots` 方法获取，这是一个以机器人 ID 为键的字典：\n\n```python\nimport nonebot\n\nbots = nonebot.get_bots()\n```\n\n我们也可以通过 `get_bot` 方法获取指定 ID 的 Bot 对象。如果省略 ID 参数，将会返回所有 Bot 中的第一个：\n\n```python\nimport nonebot\n\nbot = nonebot.get_bot(\"bot_id\")\n```\n\n如果需要获取指定适配器连接的 Bot 对象，我们可以通过适配器的 `bots` 属性获取，这也是一个以机器人 ID 为键的字典：\n\n```python\nimport nonebot\nfrom nonebot.adapters.console import Adapter\n\nconsole_adapter = nonebot.get_adapter(Adapter)\nbots = console_adapter.bots\n```\n\nBot 对象都具有一个 `self_id` 属性，它是机器人的唯一 ID，由适配器填写，通常为机器人的帐号 ID 或者 APP ID。\n\n## 获取事件通用信息\n\n适配器的所有事件模型均继承自 `Event` 基类，在[事件类型与重载](../appendices/overload.md)一节中，我们也提到了如何使用基类抽象方法来获取事件通用信息。基类能提供如下信息：\n\n### 事件类型\n\n事件类型通常为 `meta_event`、`message`、`notice`、`request`。\n\n```python\ntype: str = event.get_type()\n```\n\n### 事件名称\n\n事件名称由适配器定义，通常用于日志记录。\n\n```python\nname: str = event.get_event_name()\n```\n\n### 事件描述\n\n事件描述由适配器定义，通常用于日志记录。\n\n```python\ndescription: str = event.get_event_description()\n```\n\n### 事件日志字符串\n\n事件日志字符串由事件名称和事件描述组成，用于日志记录。\n\n```python\nlog: str = event.get_log_string()\n```\n\n### 事件主体 ID\n\n事件主体 ID 通常为机器人用户 ID。\n\n```python\nuser_id: str = event.get_user_id()\n```\n\n### 事件会话 ID\n\n事件会话 ID 通常为机器人用户 ID 与群聊/频道 ID 组合而成。\n\n```python\nsession_id: str = event.get_session_id()\n```\n\n### 事件消息\n\n如果事件包含消息，则可以通过该方法获取，否则会产生异常。\n\n```python\nmessage: Message = event.get_message()\n```\n\n### 事件纯文本消息\n\n通常为事件消息的纯文本内容，如果事件不包含消息，则会产生异常。\n\n```python\ntext: str = event.get_plaintext()\n```\n\n### 事件是否与机器人有关\n\n由适配器实现的判断，通常将事件目标主体为机器人、消息中包含“@机器人”或以“机器人的昵称”开始视为与机器人有关。\n\n```python\nis_tome: bool = event.is_tome()\n```\n\n## 更多\n\n官方支持的适配器和社区贡献的适配器均可在[商店](/store/adapters)中查看。如果你想要开发自己的适配器，可以参考[开发文档](../developer/adapter-writing.md)。欢迎通过商店发布你的适配器。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/advanced/dependency.mdx",
    "content": "---\nsidebar_position: 6\ndescription: 通过依赖注入获取上下文信息\n\noptions:\n  menu:\n    - category: advanced\n      weight: 70\n---\n\n# 依赖注入\n\nimport Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\n\n在事件处理流程中，事件响应器具有自己独立的上下文，例如：当前的事件、机器人等信息。在 NoneBot 中，这些信息通过依赖注入的方式提供给事件处理函数，可以让代码更加整洁可读、提升复用能力。\n\n在了解如何使用依赖注入获取上下文信息之前，我们需要先了解两个概念：\n\n- `Dependent`：使用依赖注入的函数或其他任意可调用对象。如：事件处理函数、自定义的依赖函数等。\n- `Dependency`：依赖注入的对象。如：当前事件、机器人等。\n\n在之前的文档中，我们已经多次使用了依赖注入来获取事件信息。通过对函数参数依照一定规则填写类型注解，即可获得想要的上下文信息。任何一个事件处理函数在添加到事件处理流程时，都会根据一定规则提前将其解析成一个 `Dependent` 对象，方便运行时进行注入。如果遇到无法解析的参数，将会抛出 `ValueError(\"Unknown parameter\")` 的异常。整个依赖注入系统可以分为两部分：\n\n- 参数解析\n  - 依据一定规则解析函数参数，识别 `Dependency` 依赖。\n  - 生成 `Dependent` 对象。\n- 执行\n  - 根据已经解析的 `Dependency` 依赖，执行调用。\n  - 将所有 `Dependency` 的返回值根据参数名传入并调用 `Dependent` 。\n\n:::danger 警告\n在依赖注入中，类型注解是非常重要的，因为它不仅可以决定依赖注入的对象，还可以触发[重载机制](../appendices/overload.md#重载)。如果类型注解与实际获得数据类型不一致，将会跳过当前 `Dependent` 对象（即事件处理函数）。\n:::\n\n:::tip 提示\n如果对于依赖注入的解析流程有疑问，可以调整[日志等级配置项](../appendices/config.mdx#log-level)为 `TRACE`，查看依赖解析日志。\n:::\n\n## 同步支持\n\n对于依赖注入系统中的 `Dependent` 或者 `Dependency` 对象，均支持同步类型的函数或可调用对象。例如：\n\n```python {6,10}\nfrom nonebot import on_command\nfrom nonebot.params import Depends\n\nmatcher = on_command(\"foo\")\n\ndef dependency() -> str:\n    return \"something\"\n\n@matcher.handle()\ndef _(result: str = Depends(dependency)):\n    ...\n```\n\n## 非依赖参数\n\n在依赖注入解析中，任何无法解析的参数如果带有默认值，将会被视为非依赖参数。这些参数在依赖运行时将不会被注入而使用函数默认值。例如：\n\n```python\nasync def _(foo: str = \"bar\"): ...\n```\n\n## 类型依赖注入\n\n这一类的依赖注入仅需要在函数参数中添加对应的类型注解即可。\n\n### Bot\n\n获取当前事件的 Bot 对象。\n\n通过标注参数为 `Bot` 类型，或者一系列 `Bot` 类型，即可获取到当前事件的 Bot 对象。为兼容性考虑，如果参数名为 `bot` 且无类型注解，也会视为 Bot 依赖注入。\n\nBot 依赖注入支持重载（即：可以标注参数为子类型）且具有[重载优先检查权](../appendices/overload.md#重载)。\n\n<Tabs groupId=\"python\">\n  <TabItem value=\"3.10\" label=\"Python 3.10+\" default>\n\n```python\nfrom nonebot.adapters import Bot\nfrom nonebot.adapters.console import Bot as ConsoleBot\nfrom nonebot.adapters.onebot.v11 import Bot as OneBotV11Bot\n\nasync def _(foo: Bot): ...\nasync def _(foo: ConsoleBot | OneBotV11Bot): ...\nasync def _(bot): ...  # 兼容性处理\n```\n\n  </TabItem>\n  <TabItem value=\"3.9\" label=\"Python 3.9\">\n\n```python\nfrom typing import Union\n\nfrom nonebot.adapters import Bot\nfrom nonebot.adapters.console import Bot as ConsoleBot\nfrom nonebot.adapters.onebot.v11 import Bot as OneBotV11Bot\n\nasync def _(foo: Bot): ...\nasync def _(foo: Union[ConsoleBot, OneBotV11Bot]): ...\nasync def _(bot): ...  # 兼容性处理\n```\n\n  </TabItem>\n</Tabs>\n\n### Event\n\n获取当前事件。\n\n通过标注参数为 `Event` 类型，或者一系列 `Event` 类型，即可获取到当前事件。为兼容性考虑，如果参数名为 `event` 且无类型注解，也会视为 Event 依赖注入。\n\nEvent 依赖注入支持重载（即：可以标注参数为子类型）且具有[重载优先检查权](../appendices/overload.md#重载)。\n\n<Tabs groupId=\"python\">\n  <TabItem value=\"3.10\" label=\"Python 3.10+\" default>\n\n```python\nfrom nonebot.adapters import Event\nfrom nonebot.adapters.onebot.v11 import PrivateMessageEvent, GroupMessageEvent\n\nasync def _(foo: Event): ...\nasync def _(foo: PrivateMessageEvent | GroupMessageEvent): ...\nasync def _(event): ...  # 兼容性处理\n```\n\n  </TabItem>\n  <TabItem value=\"3.9\" label=\"Python 3.9\">\n\n```python\nfrom typing import Union\n\nfrom nonebot.adapters import Event\nfrom nonebot.adapters.onebot.v11 import PrivateMessageEvent, GroupMessageEvent\n\nasync def _(foo: Event): ...\nasync def _(foo: Union[PrivateMessageEvent, GroupMessageEvent]): ...\nasync def _(event): ...  # 兼容性处理\n```\n\n  </TabItem>\n</Tabs>\n\n### State\n\n获取当前[会话状态](../appendices/session-state.md)。\n\n通过标注参数为 `T_State` 类型，即可获取到当前会话状态。为兼容性考虑，如果参数名为 `state` 且无类型注解，也会视为 State 依赖注入。\n\n```python\nfrom nonebot.typing import T_State\n\nasync def _(foo: T_State): ...\n```\n\n### Matcher\n\n获取当前事件响应器实例。常用于使用[事件响应器操作](../appendices/session-control.mdx)。\n\n通过标注参数为 `Matcher` 类型，或者一系列 `Matcher` 类型，即可获取到当前事件。为兼容性考虑，如果参数名为 `matcher` 且无类型注解，也会视为 Matcher 依赖注入。\n\nMatcher 依赖注入支持重载（即：可以标注参数为子类型）且具有[重载优先检查权](../appendices/overload.md#重载)。\n\n```python\nfrom nonebot.matcher import Matcher\n\nasync def _(foo: Matcher): ...\nasync def _(matcher): ...  # 兼容性处理\n```\n\n### Exception\n\n获取事件响应器运行中抛出的异常。该依赖注入目前仅在事件响应器运行后处理 Hook 中可用。\n\n通过标注参数为异常类型，或者一系列异常类型，即可获取到事件响应器运行中抛出的异常。\n\n<Tabs groupId=\"python\">\n  <TabItem value=\"3.10\" label=\"Python 3.10+\" default>\n\n```python {5,8}\nfrom nonebot.message import run_postprocessor\nfrom nonebot.exception import ActionFailed, NetworkError\n\n@run_postprocessor\nasync def _(e: Exception): ...\n\n@run_postprocessor\nasync def _(e: ActionFailed | NetworkError): ...\n```\n\n  </TabItem>\n  <TabItem value=\"3.9\" label=\"Python 3.9\">\n\n```python {6,9}\nfrom typing import Union\nfrom nonebot.message import run_postprocessor\nfrom nonebot.exception import ActionFailed, NetworkError\n\n@run_postprocessor\nasync def _(e: Exception): ...\n\n@run_postprocessor\nasync def _(e: Union[ActionFailed, NetworkError]): ...\n```\n\n  </TabItem>\n</Tabs>\n\n## 子依赖\n\n在依赖注入系统中，我们可以定义一个子依赖，来执行自定义的操作，提高代码复用性以及处理性能。\n\n### 定义子依赖\n\n子依赖使用 `Depends` 标记进行定义，其参数即依赖的函数或可调用对象，同样会被解析为 `Dependent` 对象，将会在依赖注入期间执行。我们来看一个例子：\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {5,15}\nfrom typing import Annotated\n\nfrom nonebot import on_command\nfrom nonebot.adapters import Event\nfrom nonebot.params import Depends\n\ntest = on_command(\"test\")\n\nasync def check(event: Event) -> Event:\n    if event.get_user_id() in BLACKLIST:\n        await test.finish()\n    return event\n\n@test.handle()\nasync def _(event: Annotated[Event, Depends(check)]):\n    ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {3,13}\nfrom nonebot import on_command\nfrom nonebot.adapters import Event\nfrom nonebot.params import Depends\n\ntest = on_command(\"test\")\n\nasync def check(event: Event) -> Event:\n    if event.get_user_id() in BLACKLIST:\n        await test.finish()\n    return event\n\n@test.handle()\nasync def _(event: Event = Depends(check)):\n    ...\n```\n\n  </TabItem>\n</Tabs>\n\n在上面的代码中，我们使用 `Depends` 标记定义了一个子依赖 `check`。它判断事件主体用户是否在黑名单中，如果在，则直接结束事件处理流程。如果不在，则返回事件对象，以便事件处理函数可以继续执行。\n\n通过将 `Depends` 包裹的子依赖作为参数的默认值，我们就可以在执行事件处理函数之前执行子依赖，并将其返回值作为参数传入事件处理函数。子依赖和普通的事件处理函数并没有区别，同样可以使用依赖注入，并且可以返回任何类型的值。但需要注意的是，如果事件处理函数参数的类型注解与子依赖返回值的类型**不一致**，将会触发[重载](../appendices/overload.md)而跳过当前事件处理函数。\n\n特别的，我们可以为 `Dependent` 对象定义一系列前置子依赖，它们会在参数执行前被顺序执行，且返回值将会被忽略，例如：\n\n```python {11}\nfrom nonebot import on_command\nfrom nonebot.adapters import Event\nfrom nonebot.params import Depends\n\ntest = on_command(\"test\")\n\nasync def check(event: Event):\n    if event.get_user_id() in BLACKLIST:\n        await test.finish()\n\n@test.handle(parameterless=[Depends(check)])\nasync def _():\n    ...\n```\n\n### 依赖缓存\n\nNoneBot 在执行子依赖时，会将其返回值缓存起来。当我们在使用子依赖时，`Depends` 具有一个参数 `use_cache`，默认为 `True`。此时在事件处理流程中，多次使用同一个子依赖时，将会使用缓存中的结果而不会重复执行。这在很多情景中非常有用，例如：\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {7}\nimport random\nfrom typing import Annotated\n\nasync def random_result() -> int:\n    return random.randint(1, 100)\n\nasync def _(x: Annotated[int, Depends(random_result)]):\n    print(x)\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {6}\nimport random\n\nasync def random_result() -> int:\n    return random.randint(1, 100)\n\nasync def _(x: int = Depends(random_result)):\n    print(x)\n```\n\n  </TabItem>\n</Tabs>\n\n此时，在同一事件处理流程中，这个随机函数的返回值将会保持一致。如果我们希望每次都重新执行子依赖，可以将 `use_cache` 设置为 `False`。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {7}\nimport random\nfrom typing import Annotated\n\nasync def random_result() -> int:\n    return random.randint(1, 100)\n\nasync def _(x: Annotated[int, Depends(random_result, use_cache=False)]):\n    print(x)\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {6}\nimport random\n\nasync def random_result() -> int:\n    return random.randint(1, 100)\n\nasync def _(x: int = Depends(random_result, use_cache=False)):\n    print(x)\n```\n\n  </TabItem>\n</Tabs>\n\n:::tip 提示\n缓存的生命周期与当前接收到的事件相同。接收到事件后，子依赖在首次执行时缓存，在该事件处理完成后，缓存就会被清除。\n:::\n\n### 类型转换与校验\n\n在依赖注入系统中，我们可以对子依赖的返回值进行自动类型转换与校验。这个功能由 Pydantic 支持，因此我们通过参数类型注解自动使用 Pydantic 支持的类型转换。例如：\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {6,9}\nfrom typing import Annotated\n\nfrom nonebot.params import Depends\nfrom nonebot.adapters import Event\n\ndef get_user_id(event: Event) -> str:\n    return event.get_user_id()\n\nasync def _(user_id: Annotated[int, Depends(get_user_id, validate=True)]):\n    print(user_id)\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {4,7}\nfrom nonebot.params import Depends\nfrom nonebot.adapters import Event\n\ndef get_user_id(event: Event) -> str:\n    return event.get_user_id()\n\nasync def _(user_id: int = Depends(get_user_id, validate=True)):\n    print(user_id)\n```\n\n  </TabItem>\n</Tabs>\n\n在进行类型自动转换的同时，Pydantic 还支持对数据进行更多的限制，如：大于、小于、长度等。使用方法如下：\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {7,10}\nfrom typing import Annotated\n\nfrom pydantic import Field\nfrom nonebot.params import Depends\nfrom nonebot.adapters import Event\n\ndef get_user_id(event: Event) -> str:\n    return event.get_user_id()\n\nasync def _(user_id: Annotated[int, Depends(get_user_id, validate=Field(gt=100))]):\n    print(user_id)\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {5,8}\nfrom pydantic import Field\nfrom nonebot.params import Depends\nfrom nonebot.adapters import Event\n\ndef get_user_id(event: Event) -> str:\n    return event.get_user_id()\n\nasync def _(user_id: int = Depends(get_user_id, validate=Field(gt=100))):\n    print(user_id)\n```\n\n  </TabItem>\n</Tabs>\n\n### 类作为依赖\n\n在前面的事例中，我们使用了函数作为子依赖。实际上，我们还可以使用类作为依赖。当我们在实例化一个类的时候，其实我们就在调用它，类本身也是一个可调用对象。例如：\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {16}\nfrom typing import Annotated\nfrom dataclasses import dataclass\n\nfrom nonebot.params import Depends\nfrom nonebot.adapters import Event\nfrom nonebot.typing import T_State\n\ndef get_context(state: T_State) -> dict:\n    return state.setdefault(\"context\", {})\n\n@dataclass\nclass ClassDependency:\n    event: Event\n    context: dict = Depends(get_context)\n\nasync def _(data: Annotated[ClassDependency, Depends(ClassDependency)]):\n    print(data.event, data.context)\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {15}\nfrom dataclasses import dataclass\n\nfrom nonebot.params import Depends\nfrom nonebot.adapters import Event\nfrom nonebot.typing import T_State\n\ndef get_context(state: T_State) -> dict:\n    return state.setdefault(\"context\", {})\n\n@dataclass\nclass ClassDependency:\n    event: Event\n    context: dict = Depends(get_context)\n\nasync def _(data: ClassDependency = Depends(ClassDependency)):\n    print(data.event, data.context)\n```\n\n  </TabItem>\n</Tabs>\n\n可以看到，我们使用 `dataclass` 定义了一个类。由于这个类的 `__init__` 方法可以被依赖注入系统解析，因此，我们可以将其作为子依赖进行声明。特别地，对于类依赖，`Depends` 的参数可以为空，NoneBot 将会使用参数的类型注解进行解析与推断：\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python\nfrom typing import Annotated\n\nasync def _(data: Annotated[ClassDependency, Depends()]):\n    print(data.event, data.context)\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python\nasync def _(data: ClassDependency = Depends()):\n    print(data.event, data.context)\n```\n\n  </TabItem>\n</Tabs>\n\n### 生成器作为依赖\n\nNoneBot 的依赖注入支持依赖项在事件处理流程结束后进行一些额外的工作，比如数据库 session 或者网络 IO 的关闭，互斥锁的解锁等等。同时，由于[依赖缓存](#依赖缓存)的存在，我们可以通过这种方式来实现共享一个 session 等功能。\n\n要实现上述功能，我们可以用生成器函数作为依赖项，我们用 `yield` 关键字取代 `return` 关键字，并在 `yield` 之后进行额外的工作。\n\n我们可以看下述代码段, 使用 `httpx.AsyncClient` 异步网络 IO，并在事件处理流程中共用一个 client：\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {15}\nfrom typing import Annotated\nfrom collections.abc import AsyncGenerator\n\nimport httpx\nfrom nonebot.params import Depends\n\nasync def get_client() -> AsyncGenerator[httpx.AsyncClient, None]:\n    try:\n        async with httpx.AsyncClient() as client:\n            yield client\n    finally:\n        # 在这里进行额外的工作\n\n\n@test.handle()\nasync def _(x: Annotated[httpx.AsyncClient, Depends(get_client)]):\n    resp = await x.get(\"https://nonebot.dev\")\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {15}\nfrom collections.abc import AsyncGenerator\n\nimport httpx\nfrom nonebot.params import Depends\n\nasync def get_client() -> AsyncGenerator[httpx.AsyncClient, None]:\n    try:\n        async with httpx.AsyncClient() as client:\n            yield client\n    finally:\n        # 在这里进行额外的工作\n\n\n@test.handle()\nasync def _(x: httpx.AsyncClient = Depends(get_client)):\n    resp = await x.get(\"https://nonebot.dev\")\n```\n\n  </TabItem>\n</Tabs>\n\n:::caution 注意\n生成器作为依赖时，其中只能进行一次 `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 内部就使用了这两个装饰器。\n:::\n\n### 可调用对象作为依赖\n\n在 Python 里，为类定义 `__call__` 方法就可以使得这个类的实例成为一个可调用对象。因此，我们也可以将定义了 `__call__` 方法的类的实例作为依赖。事实上，NoneBot 的[内置响应规则](./matcher.md#内置响应规则)就广泛使用了这种方式，以 `is_type` 规则为例：\n\n```python\nfrom nonebot.adapters import Event\n\nclass IsTypeRule:\n    def __init__(self, *types: type[Event]):\n        self.types = types\n\n    async def __call__(self, event: Event) -> bool:\n        return isinstance(event, self.types)\n```\n\n我们在使用 `is_type` 时，即实例化了 `IsTypeRule` 类，然后将实例作为响应规则依赖项传入。\n\n## 其他依赖注入\n\n这一类的依赖注入通常基于子依赖编写，为我们开发者提供更方便的途径获取上下文信息。\n\n### EventType\n\n获取当前事件的类型。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot.params import EventType\n\nasync def _(foo: Annotated[str, EventType()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {3}\nfrom nonebot.params import EventType\n\nasync def _(foo: str = EventType()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### EventMessage\n\n获取当前事件的消息。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {5}\nfrom typing import Annotated\nfrom nonebot.adapters import Message\nfrom nonebot.params import EventMessage\n\nasync def _(foo: Annotated[Message, EventMessage()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {4}\nfrom nonebot.adapters import Message\nfrom nonebot.params import EventMessage\n\nasync def _(foo: Message = EventMessage()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### EventPlainText\n\n获取当前事件的消息纯文本部分。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot.params import EventPlainText\n\nasync def _(foo: Annotated[str, EventPlainText()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {3}\nfrom nonebot.params import EventPlainText\n\nasync def _(foo: str = EventPlainText()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### EventToMe\n\n获取当前事件是否与机器人相关。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot.params import EventToMe\n\nasync def _(foo: Annotated[bool, EventToMe()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {3}\nfrom nonebot.params import EventToMe\n\nasync def _(foo: bool = EventToMe()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### Command\n\n获取当前命令型消息的元组形式命令名。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot.params import Command\n\nasync def _(foo: Annotated[tuple[str, ...], Command()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {4}\nfrom nonebot.params import Command\n\nasync def _(foo: tuple[str, ...] = Command()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n:::tip 提示\n命令详情只能在**触发命令型事件响应器时**获取。如果在事件处理后续流程中获取，则会获取到不同的值。\n:::\n\n### RawCommand\n\n获取当前命令型消息的文本形式命令名。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot.params import RawCommand\n\nasync def _(foo: Annotated[str, RawCommand()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {3}\nfrom nonebot.params import RawCommand\n\nasync def _(foo: str = RawCommand()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n:::tip 提示\n命令详情只能在**触发命令型事件响应器时**获取。如果在事件处理后续流程中获取，则会获取到不同的值。\n:::\n\n### CommandArg\n\n获取命令型消息命令后跟随的参数。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {5}\nfrom typing import Annotated\nfrom nonebot.adapters import Message\nfrom nonebot.params import CommandArg\n\nasync def _(foo: Annotated[Message, CommandArg()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {4}\nfrom nonebot.adapters import Message\nfrom nonebot.params import CommandArg\n\nasync def _(foo: Message = CommandArg()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n:::tip 提示\n命令详情只能在**触发命令型事件响应器时**获取。如果在事件处理后续流程中获取，则会获取到不同的值。\n:::\n\n### CommandStart\n\n获取命令型消息命令前缀。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot.params import CommandStart\n\nasync def _(foo: Annotated[str, CommandStart()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {3}\nfrom nonebot.params import CommandStart\n\nasync def _(foo: str = CommandStart()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n:::tip 提示\n命令详情只能在**触发命令型事件响应器时**获取。如果在事件处理后续流程中获取，则会获取到不同的值。\n:::\n\n### CommandWhitespace\n\n获取命令型消息命令与参数间空白符。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot.params import CommandWhitespace\n\nasync def _(foo: Annotated[str, CommandWhitespace()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {3}\nfrom nonebot.params import CommandWhitespace\n\nasync def _(foo: str = CommandWhitespace()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n:::tip 提示\n命令详情只能在**触发命令型事件响应器时**获取。如果在事件处理后续流程中获取，则会获取到不同的值。\n:::\n\n### ShellCommandArgv\n\n获取 shell 命令解析前的参数列表，列表中可能包含文本字符串和富文本消息段（如：图片）。当词法解析出错的时候，返回值将为 `None`。通过重载机制即可处理两种不同的情况。\n\n<Tabs groupId=\"python\">\n  <TabItem value=\"3.10\" label=\"Python 3.10+\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot import on_shell_command\nfrom nonebot.params import ShellCommandArgv\n\nmatcher = on_shell_command(\"cmd\")\n\n# 解析失败\n@matcher.handle()\nasync def _(foo: Annotated[None, ShellCommandArgv()]): ...\n\n# 解析成功\n@matcher.handle()\nasync def _(foo: Annotated[list[str | MessageSegment], ShellCommandArgv()]): ...\n```\n\n```python {4}\nfrom nonebot import on_shell_command\nfrom nonebot.params import ShellCommandArgv\n\nmatcher = on_shell_command(\"cmd\")\n\n# 解析失败\n@matcher.handle()\nasync def _(foo: None = ShellCommandArgv()): ...\n\n# 解析成功\n@matcher.handle()\nasync def _(foo: list[str | MessageSegment] = ShellCommandArgv()): ...\n```\n\n  </TabItem>\n  <TabItem value=\"3.9\" label=\"Python 3.9\">\n\n```python {4}\nfrom typing import Union, Annotated\nfrom nonebot import on_shell_command\nfrom nonebot.params import ShellCommandArgv\n\nmatcher = on_shell_command(\"cmd\")\n\n# 解析失败\n@matcher.handle()\nasync def _(foo: Annotated[None, ShellCommandArgv()]): ...\n\n# 解析成功\n@matcher.handle()\nasync def _(foo: Annotated[list[Union[str, MessageSegment]], ShellCommandArgv()]): ...\n```\n\n```python {4}\nfrom typing import Union\nfrom nonebot import on_shell_command\nfrom nonebot.params import ShellCommandArgv\n\nmatcher = on_shell_command(\"cmd\")\n\n# 解析失败\n@matcher.handle()\nasync def _(foo: None = ShellCommandArgv()): ...\n\n# 解析成功\n@matcher.handle()\nasync def _(foo: list[Union[str, MessageSegment]] = ShellCommandArgv()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### ShellCommandArgs\n\n获取 shell 命令解析后的参数 Namespace，支持 MessageSegment 富文本（如：图片）。\n\n:::tip 提示\n如果参数解析成功，则为 parser 返回的 Namespace；如果参数解析失败，则为 [`ParserExit`](../api/exception.md#ParserExit) 异常，并携带错误码与错误信息。在前置词法解析失败时，返回值也为 [`ParserExit`](../api/exception.md#ParserExit) 异常。通过重载机制即可处理两种不同的情况。\n\n由于 `ArgumentParser` 在解析到 `--help` 参数时也会抛出异常，这种情况下错误码为 `0` 且错误信息即为帮助信息。\n:::\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {14,22}\nfrom typing import Annotated\n\nfrom nonebot import on_shell_command\nfrom nonebot.exception import ParserExit\nfrom nonebot.params import ShellCommandArgs\nfrom nonebot.rule import Namespace, ArgumentParser\n\nparser = ArgumentParser(\"demo\")\n# parser.add_argument ...\nmatcher = on_shell_command(\"cmd\", parser=parser)\n\n# 解析失败\n@matcher.handle()\nasync def _(foo: Annotated[ParserExit, ShellCommandArgs()]):\n    if foo.status == 0:\n        foo.message  # help message\n    else:\n        foo.message  # error message\n\n# 解析成功\n@matcher.handle()\nasync def _(foo: Annotated[Namespace, ShellCommandArgs()]):\n    arg_dict = vars(foo)\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {12,20}\nfrom nonebot import on_shell_command\nfrom nonebot.exception import ParserExit\nfrom nonebot.params import ShellCommandArgs\nfrom nonebot.rule import Namespace, ArgumentParser\n\nparser = ArgumentParser(\"demo\")\n# parser.add_argument ...\nmatcher = on_shell_command(\"cmd\", parser=parser)\n\n# 解析失败\n@matcher.handle()\nasync def _(foo: ParserExit = ShellCommandArgs()):\n    if foo.status == 0:\n        foo.message  # help message\n    else:\n        foo.message  # error message\n\n# 解析成功\n@matcher.handle()\nasync def _(foo: Namespace = ShellCommandArgs()):\n    arg_dict = vars(foo)\n```\n\n  </TabItem>\n</Tabs>\n\n### RegexMatched\n\n获取正则匹配结果的对象。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {5}\nfrom re import Match\nfrom typing import Annotated\nfrom nonebot.params import RegexMatched\n\nasync def _(foo: Annotated[Match[str], RegexMatched()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {4}\nfrom re import Match\nfrom nonebot.params import RegexMatched\n\nasync def _(foo: Match[str] = RegexMatched()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### RegexStr\n\n获取正则匹配结果的文本。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot.params import RegexStr\n\nasync def _(foo: Annotated[str, RegexStr()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {3}\nfrom nonebot.params import RegexStr\n\nasync def _(foo: str = RegexStr()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### RegexGroup\n\n获取正则匹配结果的 group 元组。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Any, Annotated\nfrom nonebot.params import RegexGroup\n\nasync def _(foo: Annotated[tuple[Any, ...], RegexGroup()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {4}\nfrom typing import Any\nfrom nonebot.params import RegexGroup\n\nasync def _(foo: tuple[Any, ...] = RegexGroup()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### RegexDict\n\n获取正则匹配结果的 group 字典。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Any, Annotated\nfrom nonebot.params import RegexDict\n\nasync def _(foo: Annotated[dict[str, Any], RegexDict()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {4}\nfrom typing import Any\nfrom nonebot.params import RegexDict\n\nasync def _(foo: dict[str, Any] = RegexDict()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### Startswith\n\n获取触发响应器的消息前缀字符串。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot.params import Startswith\n\nasync def _(foo: Annotated[str, Startswith()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {3}\nfrom nonebot.params import Startswith\n\nasync def _(foo: str = Startswith()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### Endswith\n\n获取触发响应器的消息后缀字符串。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot.params import Endswith\n\nasync def _(foo: Annotated[str, Endswith()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {3}\nfrom nonebot.params import Endswith\n\nasync def _(foo: str = Endswith()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### Fullmatch\n\n获取触发响应器的消息字符串。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot.params import Fullmatch\n\nasync def _(foo: Annotated[str, Fullmatch()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {3}\nfrom nonebot.params import Fullmatch\n\nasync def _(foo: str = Fullmatch()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### Keyword\n\n获取触发响应器的关键字字符串。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot.params import Keyword\n\nasync def _(foo: Annotated[str, Keyword()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {3}\nfrom nonebot.params import Keyword\n\nasync def _(foo: str = Keyword()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### Received\n\n获取某次 `receive` 接收的事件。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {7}\nfrom typing import Annotated\n\nfrom nonebot.adapters import Event\nfrom nonebot.params import Received\n\n@matcher.receive(\"id\")\nasync def _(foo: Annotated[Event, Received(\"id\")]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {5}\nfrom nonebot.adapters import Event\nfrom nonebot.params import Received\n\n@matcher.receive(\"id\")\nasync def _(foo: Event = Received(\"id\")): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### LastReceived\n\n获取最近一次 `receive` 接收的事件。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {7}\nfrom typing import Annotated\n\nfrom nonebot.adapters import Event\nfrom nonebot.params import LastReceived\n\n@matcher.receive(\"any\")\nasync def _(foo: Annotated[Event, LastReceived()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {5}\nfrom nonebot.adapters import Event\nfrom nonebot.params import LastReceived\n\n@matcher.receive(\"any\")\nasync def _(foo: Event = LastReceived()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### ReceivePromptResult\n\n获取某次 `receive` 发送提示消息的结果。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {6}\nfrom typing import Any, Annotated\n\nfrom nonebot.params import ReceivePromptResult\n\n@matcher.receive(\"id\", prompt=\"prompt\")\nasync def _(result: Annotated[Any, ReceivePromptResult(\"id\")]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {6}\nfrom typing import Any\n\nfrom nonebot.params import ReceivePromptResult\n\n@matcher.receive(\"id\", prompt=\"prompt\")\nasync def _(result: Any = ReceivePromptResult(\"id\")): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### Arg\n\n获取某次 `got` 接收的参数。如果 `Arg` 参数留空，则使用函数的参数名作为要获取的参数。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {7,8}\nfrom typing import Annotated\n\nfrom nonebot.params import Arg\nfrom nonebot.adapters import Message\n\n@matcher.got(\"key\")\nasync def _(key: Annotated[Message, Arg()]): ...\nasync def _(foo: Annotated[Message, Arg(\"key\")]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {5,6}\nfrom nonebot.params import Arg\nfrom nonebot.adapters import Message\n\n@matcher.got(\"key\")\nasync def _(key: Message = Arg()): ...\nasync def _(foo: Message = Arg(\"key\")): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### ArgStr\n\n获取某次 `got` 接收的参数，并转换为字符串。如果 `Arg` 参数留空，则使用函数的参数名作为要获取的参数。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {6,7}\nfrom typing import Annotated\n\nfrom nonebot.params import ArgStr\n\n@matcher.got(\"key\")\nasync def _(key: Annotated[str, ArgStr()]): ...\nasync def _(foo: Annotated[str, ArgStr(\"key\")]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {4,5}\nfrom nonebot.params import ArgStr\n\n@matcher.got(\"key\")\nasync def _(key: str = ArgStr()): ...\nasync def _(foo: str = ArgStr(\"key\")): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### ArgPlainText\n\n获取某次 `got` 接收的参数的纯文本部分。如果 `Arg` 参数留空，则使用函数的参数名作为要获取的参数。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {6,7}\nfrom typing import Annotated\n\nfrom nonebot.params import ArgPlainText\n\n@matcher.got(\"key\")\nasync def _(key: Annotated[str, ArgPlainText()]): ...\nasync def _(foo: Annotated[str, ArgPlainText(\"key\")]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {4,5}\nfrom nonebot.params import ArgPlainText\n\n@matcher.got(\"key\")\nasync def _(key: str = ArgPlainText()): ...\nasync def _(foo: str = ArgPlainText(\"key\")): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### ArgPromptResult\n\n获取某次 `got` 发送提示消息的结果。如果 `Arg` 参数留空，则使用函数的参数名作为要获取的参数。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {6,7}\nfrom typing import Any, Annotated\n\nfrom nonebot.params import ArgPromptResult\n\n@matcher.got(\"key\", prompt=\"prompt\")\nasync def _(result: Annotated[Any, ArgPromptResult()]): ...\nasync def _(result: Annotated[Any, ArgPromptResult(\"key\")]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {6,7}\nfrom typing import Any\n\nfrom nonebot.params import ArgPromptResult\n\n@matcher.got(\"key\", prompt=\"prompt\")\nasync def _(result: Any = ArgPromptResult()): ...\nasync def _(result: Any = ArgPromptResult(\"key\")): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### PausePromptResult\n\n获取最近一次 `pause` 发送提示消息的结果。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {6}\nfrom typing import Any, Annotated\n\nfrom nonebot.params import PausePromptResult\n\n@matcher.handle()\nasync def _():\n    await matcher.pause(prompt=\"prompt\")\n\n@matcher.handle()\nasync def _(result: Annotated[Any, PausePromptResult()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {6}\nfrom typing import Any\n\nfrom nonebot.params import PausePromptResult\n\n@matcher.handle()\nasync def _():\n    await matcher.pause(prompt=\"prompt\")\n\n@matcher.handle()\nasync def _(result: Any = PausePromptResult()): ...\n```\n\n  </TabItem>\n</Tabs>\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/advanced/driver.md",
    "content": "---\nsidebar_position: 0\ndescription: 选择合适的驱动器运行机器人\n\noptions:\n  menu:\n    - category: advanced\n      weight: 10\n---\n\n# 选择驱动器\n\n驱动器 (Driver) 是机器人运行的基石，它是机器人初始化的第一步，主要负责数据收发。\n\n:::important 提示\n驱动器的选择通常与机器人所使用的协议适配器相关，如果不知道该选择哪个驱动器，可以先阅读相关协议适配器文档说明。\n:::\n\n:::tip 提示\n如何**安装**驱动器请参考[安装驱动器](../tutorial/store.mdx#安装驱动器)。\n:::\n\n## 驱动器类型\n\n驱动器类型大体上可以分为两种：\n\n- `Forward`：即客户端型驱动器，多用于使用 HTTP 轮询，连接 WebSocket 服务器等情形。\n- `Reverse`：即服务端型驱动器，多用于使用 WebHook，接收 WebSocket 客户端连接等情形。\n\n客户端型驱动器可以分为以下两种：\n\n1. 异步发送 HTTP 请求，自定义 `HTTP Method`、`URL`、`Header`、`Body`、`Cookie`、`Proxy`、`Timeout` 等。\n2. 异步建立 WebSocket 连接上下文，自定义 `WebSocket URL`、`Header`、`Cookie`、`Proxy`、`Timeout` 等。\n\n服务端型驱动器目前有：\n\n1. ASGI 应用框架，具有以下功能：\n   - 协议适配器自定义 HTTP 上报地址以及对上报数据处理的回调函数。\n   - 协议适配器自定义 WebSocket 连接请求地址以及对 WebSocket 请求处理的回调函数。\n   - 用户可以向 ASGI 应用添加任何服务端相关功能，如：[添加自定义路由](./routing.md)。\n\n## 配置驱动器\n\n驱动器的配置方法已经在[配置](../appendices/config.mdx)章节中简单进行了介绍，这里将详细介绍驱动器配置的格式。\n\nNoneBot 中的客户端和服务端型驱动器可以相互配合使用，但服务端型驱动器**仅能选择一个**。所有驱动器模块都会包含一个 `Driver` 子类，即驱动器类，他可以作为驱动器单独运行。同时，客户端驱动器模块中还会提供一个 `Mixin` 子类，用于在与其他驱动器配合使用时加载。因此，驱动器配置格式采用特殊语法：`<module>[:<Driver>][+<module>[:<Mixin>]]*`。\n\n其中，`<module>` 代表**驱动器模块路径**；`<Driver>` 代表**驱动器类名**，默认为 `Driver`；`<Mixin>` 代表**驱动器混入类名**，默认为 `Mixin`。即，我们需要选择一个主要驱动器，然后在其基础上配合使用其他驱动器的功能。主要驱动器可以为客户端或服务端类型，但混入类驱动器只能为客户端类型。\n\n特别的，为了简化内置驱动器模块路径，我们可以使用 `~` 符号作为内置驱动器模块路径的前缀，如 `~fastapi` 代表使用内置驱动器 `fastapi`。NoneBot 内置了多个驱动器适配，但需要安装额外依赖才能使用，具体请参考[安装驱动器](../tutorial/store.mdx#安装驱动器)。常见的驱动器配置如下：\n\n```dotenv\nDRIVER=~fastapi\nDRIVER=~aiohttp\nDRIVER=~httpx+~websockets\nDRIVER=~fastapi+~httpx+~websockets\n```\n\n## 获取驱动器\n\n在 NoneBot 框架初始化完成后，我们就可以通过 `get_driver()` 方法获取全局驱动器实例：\n\n```python\nfrom nonebot import get_driver\n\ndriver = get_driver()\n```\n\n## 内置驱动器\n\n### None\n\n**类型：**服务端驱动器\n\nNoneBot 内置的空驱动器，不提供任何收发数据功能，可以在不需要外部网络连接时使用。\n\n```env\nDRIVER=~none\n```\n\n### FastAPI（默认）\n\n**类型：**ASGI 服务端驱动器\n\n> FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints.\n\n[FastAPI](https://fastapi.tiangolo.com/) 是一个易上手、高性能的异步 Web 框架，具有极佳的编写体验。 FastAPI 可以通过类型注解、依赖注入等方式实现输入参数校验、自动生成 API 文档等功能，也可以挂载其他 ASGI、WSGI 应用。\n\n```env\nDRIVER=~fastapi\n```\n\n#### FastAPI 配置项\n\n##### `fastapi_openapi_url`\n\n类型：`str | None`  \n默认值：`None`  \n说明：`FastAPI` 提供的 `OpenAPI` JSON 定义地址，如果为 `None`，则不提供 `OpenAPI` JSON 定义。\n\n##### `fastapi_docs_url`\n\n类型：`str | None`  \n默认值：`None`  \n说明：`FastAPI` 提供的 `Swagger` 文档地址，如果为 `None`，则不提供 `Swagger` 文档。\n\n##### `fastapi_redoc_url`\n\n类型：`str | None`  \n默认值：`None`  \n说明：`FastAPI` 提供的 `ReDoc` 文档地址，如果为 `None`，则不提供 `ReDoc` 文档。\n\n##### `fastapi_include_adapter_schema`\n\n类型：`bool`  \n默认值：`True`  \n说明：`FastAPI` 提供的 `OpenAPI` JSON 定义中是否包含适配器路由的 `Schema`。\n\n##### `fastapi_reload`\n\n:::caution 警告\n不推荐开启该配置项，在 Windows 平台上开启该功能有可能会造成预料之外的影响！替代方案：使用 `nb-cli` 命令行工具以及参数 `--reload` 启动 NoneBot。\n\n```bash\nnb run --reload\n```\n\n开启该功能后，在 uvicorn 运行时（FastAPI 提供的 ASGI 底层，即 reload 功能的实际来源），asyncio 使用的事件循环会被 uvicorn 从默认的 `ProactorEventLoop` 强制切换到 `SelectorEventLoop`。\n\n> 相关信息参考 [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)\n\n后者（`SelectorEventLoop`）在 Windows 平台的可使用性不如前者（`ProactorEventLoop`），包括但不限于\n\n1. 不支持创建子进程\n2. 最多只支持 512 个套接字\n3. ...\n\n> 具体信息参考 [Python 文档](https://docs.python.org/zh-cn/3/library/asyncio-platforms.html#windows)\n\n所以，一些使用了 asyncio 的库因此可能无法正常工作，如：\n\n1. [playwright](https://playwright.dev/python/docs/library#incompatible-with-selectoreventloop-of-asyncio-on-windows)\n\n如果在开启该功能后，原本**正常运行**的代码报错，且打印的异常堆栈信息和 asyncio 有关（异常一般为 `NotImplementedError`），\n你可能就需要考虑相关库对事件循环的支持，以及是否启用该功能。\n:::\n\n类型：`bool`  \n默认值：`False`  \n说明：是否开启 `uvicorn` 的 `reload` 功能，需要在机器人入口文件提供 ASGI 应用路径。\n\n```python title=bot.py\napp = nonebot.get_asgi()\nnonebot.run(app=\"bot:app\")\n```\n\n##### `fastapi_reload_dirs`\n\n类型：`List[str] | None`  \n默认值：`None`  \n说明：重载监控文件夹列表，默认为 uvicorn 默认值\n\n##### `fastapi_reload_delay`\n\n类型：`float | None`  \n默认值：`None`  \n说明：重载延迟，默认为 uvicorn 默认值\n\n##### `fastapi_reload_includes`\n\n类型：`List[str] | None`  \n默认值：`None`  \n说明：要监听的文件列表，支持 glob pattern，默认为 uvicorn 默认值\n\n##### `fastapi_reload_excludes`\n\n类型：`List[str] | None`  \n默认值：`None`  \n说明：不要监听的文件列表，支持 glob pattern，默认为 uvicorn 默认值\n\n##### `fastapi_extra`\n\n类型：`Dist[str, Any]`  \n默认值：`{}`  \n说明：传递给 `FastAPI` 的其他参数\n\n### Quart\n\n**类型：**ASGI 服务端驱动器\n\n> Quart is an asyncio reimplementation of the popular Flask microframework API.\n\n[Quart](https://quart.palletsprojects.com/) 是一个类 Flask 的异步版本，拥有与 Flask 非常相似的接口和使用方法。\n\n```env\nDRIVER=~quart\n```\n\n#### Quart 配置项\n\n##### `quart_reload`\n\n:::caution 警告\n不推荐开启该配置项，在 Windows 平台上开启该功能有可能会造成预料之外的影响！替代方案：使用 `nb-cli` 命令行工具以及参数 `--reload` 启动 NoneBot。\n\n```bash\nnb run --reload\n```\n\n:::\n\n类型：`bool`  \n默认值：`False`  \n说明：是否开启 `uvicorn` 的 `reload` 功能，需要在机器人入口文件提供 ASGI 应用路径。\n\n```python title=bot.py\napp = nonebot.get_asgi()\nnonebot.run(app=\"bot:app\")\n```\n\n##### `quart_reload_dirs`\n\n类型：`List[str] | None`  \n默认值：`None`  \n说明：重载监控文件夹列表，默认为 uvicorn 默认值\n\n##### `quart_reload_delay`\n\n类型：`float | None`  \n默认值：`None`  \n说明：重载延迟，默认为 uvicorn 默认值\n\n##### `quart_reload_includes`\n\n类型：`List[str] | None`  \n默认值：`None`  \n说明：要监听的文件列表，支持 glob pattern，默认为 uvicorn 默认值\n\n##### `quart_reload_excludes`\n\n类型：`List[str] | None`  \n默认值：`None`  \n说明：不要监听的文件列表，支持 glob pattern，默认为 uvicorn 默认值\n\n##### `quart_extra`\n\n类型：`Dist[str, Any]`  \n默认值：`{}`  \n说明：传递给 `Quart` 的其他参数\n\n### HTTPX\n\n**类型：**HTTP 客户端驱动器\n\n:::caution 注意\n本驱动器仅支持 HTTP 请求，不支持 WebSocket 连接请求。\n:::\n\n> [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.\n\n```env\nDRIVER=~httpx\n```\n\n### websockets\n\n**类型：**WebSocket 客户端驱动器\n\n:::caution 注意\n本驱动器仅支持 WebSocket 连接请求，不支持 HTTP 请求。\n:::\n\n> [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.\n\n```env\nDRIVER=~websockets\n```\n\n### AIOHTTP\n\n**类型：**HTTP/WebSocket 客户端驱动器\n\n> [AIOHTTP](https://docs.aiohttp.org/): Asynchronous HTTP Client/Server for asyncio and Python.\n\n```env\nDRIVER=~aiohttp\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/advanced/matcher-provider.md",
    "content": "---\nsidebar_position: 10\ndescription: 自定义事件响应器存储\n\noptions:\n  menu:\n    - category: advanced\n      weight: 110\n---\n\n# 事件响应器存储\n\n事件响应器是 NoneBot 处理事件的核心，它们默认存储在一个字典中。在进入会话状态后，事件响应器将会转为临时响应器，作为最高优先级同样存储于该字典中。因此，事件响应器的存储类似于会话存储，它决定了整个 NoneBot 对事件的处理行为。\n\nNoneBot 默认使用 Python 的字典将事件响应器存储于内存中，但是我们也可以自定义事件响应器存储，将事件响应器存储于其他地方，例如 Redis 等。这样我们就可以实现持久化、在多实例间共享会话状态等功能。\n\n## 编写存储提供者\n\n事件响应器的存储提供者 `MatcherProvider` 抽象类继承自 `MutableMapping[int, list[type[Matcher]]]`，即以优先级为键，以事件响应器列表为值的映射。我们可以方便地进行逐优先级事件传播。\n\n编写一个自定义的存储提供者，只需要继承并实现 `MatcherProvider` 抽象类：\n\n```python\nfrom nonebot.matcher import MatcherProvider\n\nclass CustomProvider(MatcherProvider):\n    ...\n```\n\n## 设置存储提供者\n\n我们可以通过 `matchers.set_provider` 方法设置存储提供者：\n\n```python {3}\nfrom nonebot.matcher import matchers\n\nmatchers.set_provider(CustomProvider)\n\nassert isinstance(matchers.provider, CustomProvider)\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/advanced/matcher.md",
    "content": "---\nsidebar_position: 5\ndescription: 事件响应器组成与内置响应规则\n\noptions:\n  menu:\n    - category: advanced\n      weight: 60\n---\n\n# 事件响应器进阶\n\n在[指南](../tutorial/matcher.md)与[深入](../appendices/rule.md)中，我们已经介绍了事件响应器的基本用法以及响应规则、权限控制等功能。在这一节中，我们将介绍事件响应器的组成，内置的响应规则，与第三方响应规则拓展。\n\n:::tip 提示\n事件响应器允许继承，你可以通过直接继承 `Matcher` 类来创建一个新的事件响应器。\n:::\n\n## 事件响应器组成\n\n### 事件响应器类型\n\n事件响应器类型 `type` 即是该响应器所要响应的事件类型，只有在接收到的事件类型与该响应器的类型相同时，才会触发该响应器。如果类型为空字符串 `\"\"`，则响应器将会响应所有类型的事件。事件响应器类型的检查在所有其他检查（权限控制、响应规则）之前进行。\n\nNoneBot 内置了四种常用事件类型：`meta_event`、`message`、`notice`、`request`，分别对应元事件、消息、通知、请求。通常情况下，协议适配器会将事件合理地分类至这四种类型中。如果有其他类型的事件需要响应，可以自行定义新的类型。\n\n### 事件触发权限\n\n事件触发权限 `permission` 是一个 `Permission` 对象，这在[权限控制](../appendices/permission.mdx)一节中已经介绍过。事件触发权限会在事件响应器的类型检查通过后进行检查，如果权限检查通过，则执行响应规则检查。\n\n### 事件响应规则\n\n事件响应规则 `rule` 是一个 `Rule` 对象，这在[响应规则](../appendices/rule.md)一节中已经介绍过。事件响应器的响应规则会在事件响应器的权限检查通过后进行匹配，如果响应规则检查通过，则触发该响应器。\n\n### 响应优先级\n\n响应优先级 `priority` 是一个正整数，用于指定响应器的优先级。响应器的优先级越小，越先被触发。如果响应器的优先级相同，则按照响应器的注册顺序进行触发。\n\n### 阻断\n\n阻断 `block` 是一个布尔值，用于指定响应器是否阻断事件的传播。如果阻断为 `True`，则在该响应器被触发后，事件将不会再传播给其他下一优先级的响应器。\n\nNoneBot 内置的事件响应器中，所有非 `command` 规则的 `message` 类型的事件响应器都会阻断事件传递，其他则不会。\n\n在部分情况中，可以使用 [`stop_propagation`](../appendices/session-control.mdx#stop_propagation) 方法动态阻止事件传播，该方法需要 handler 在参数中获取 matcher 实例后调用方法。\n\n### 有效期\n\n事件响应器的有效期分为 `temp` 和 `expire_time` 。`temp` 是一个布尔值，用于指定响应器是否为临时响应器。如果为 `True`，则该响应器在被触发后会被自动销毁。`expire_time` 是一个 `datetime` 对象，用于指定响应器的过期时间。如果 `expire_time` 不为 `None`，则在该时间点后，该响应器会被自动销毁。\n\n### 默认状态\n\n事件响应器的默认状态 `default_state` 是一个 `dict` 对象，用于指定响应器的默认状态。在响应器被触发时，响应器将会初始化默认状态然后开始执行事件处理流程。\n\n## 基本辅助函数\n\nNoneBot 为四种类型的事件响应器提供了五个基本的辅助函数：\n\n- `on`：创建任何类型的事件响应器。\n- `on_metaevent`：创建元事件响应器。\n- `on_message`：创建消息事件响应器。\n- `on_request`：创建请求事件响应器。\n- `on_notice`：创建通知事件响应器。\n\n除了 `on` 函数具有一个 `type` 参数外，其余参数均相同：\n\n- `rule`：响应规则，可以是 `Rule` 对象或者 `RuleChecker` 函数。\n- `permission`：事件触发权限，可以是 `Permission` 对象或者 `PermissionChecker` 函数。\n- `handlers`：事件处理函数列表。\n- `temp`：是否为临时响应器。\n- `expire_time`：响应器的过期时间。\n- `priority`：响应器的优先级。\n- `block`：是否阻断事件传播。\n- `state`：响应器的默认状态。\n\n在消息类型的事件响应器的基础上，NoneBot 还内置了一些常用的响应规则，并结合为辅助函数来方便我们快速创建指定功能的响应器。下面我们逐个介绍。\n\n## 内置响应规则\n\n:::tip\n响应规则的使用方法可以参考 [深入 - 响应规则](../appendices/rule.md)。\n:::\n\n### `startswith`\n\n`startswith` 响应规则用于匹配消息纯文本部分的开头是否与指定字符串（或一系列字符串）相同。可选参数 `ignorecase` 用于指定是否忽略大小写，默认为 `False`。\n\n例如，我们可以创建一个匹配消息开头为 `!` 或者 `/` 的规则：\n\n```python\nfrom nonebot.rule import startswith\n\nrule = startswith((\"!\", \"/\"), ignorecase=False)\n```\n\n也可以直接使用辅助函数新建一个响应器：\n\n```python\nfrom nonebot import on_startswith\n\nmatcher = on_startswith((\"!\", \"/\"), ignorecase=False)\n```\n\n### `endswith`\n\n`endswith` 响应规则用于匹配消息纯文本部分的结尾是否与指定字符串（或一系列字符串）相同。可选参数 `ignorecase` 用于指定是否忽略大小写，默认为 `False`。\n\n例如，我们可以创建一个匹配消息结尾为 `.` 或者 `。` 的规则：\n\n```python\nfrom nonebot.rule import endswith\n\nrule = endswith((\".\", \"。\"), ignorecase=False)\n```\n\n也可以直接使用辅助函数新建一个响应器：\n\n```python\nfrom nonebot import on_endswith\n\nmatcher = on_endswith((\".\", \"。\"), ignorecase=False)\n```\n\n### `fullmatch`\n\n`fullmatch` 响应规则用于匹配消息纯文本部分是否与指定字符串（或一系列字符串）完全相同。可选参数 `ignorecase` 用于指定是否忽略大小写，默认为 `False`。\n\n例如，我们可以创建一个匹配消息为 `ping` 或者 `pong` 的规则：\n\n```python\nfrom nonebot.rule import fullmatch\n\nrule = fullmatch((\"ping\", \"pong\"), ignorecase=False)\n```\n\n也可以直接使用辅助函数新建一个响应器：\n\n```python\nfrom nonebot import on_fullmatch\n\nmatcher = on_fullmatch((\"ping\", \"pong\"), ignorecase=False)\n```\n\n### `keyword`\n\n`keyword` 响应规则用于匹配消息纯文本部分是否包含指定字符串（或一系列字符串）。\n\n例如，我们可以创建一个匹配消息中包含 `hello` 或者 `hi` 的规则：\n\n```python\nfrom nonebot.rule import keyword\n\nrule = keyword(\"hello\", \"hi\")\n```\n\n也可以直接使用辅助函数新建一个响应器：\n\n```python\nfrom nonebot import on_keyword\n\nmatcher = on_keyword({\"hello\", \"hi\"})\n```\n\n### `command`\n\n`command` 是最常用的响应规则，它用于匹配消息是否为命令。它会根据配置中的 [Command Start 和 Command Separator](../appendices/config.mdx#command-start-和-command-separator) 来判断消息是否为命令。\n\n例如，当我们配置了 `Command Start` 为 `/`，`Command Separator` 为 `.` 时：\n\n```python\nfrom nonebot.rule import command\n\n# 匹配 \"/help\" 或者 \"/帮助\" 开头的消息\nrule = command(\"help\", \"帮助\")\n# 匹配 \"/help.cmd\" 开头的消息\nrule = command((\"help\", \"cmd\"))\n```\n\n也可以直接使用辅助函数新建一个响应器：\n\n```python\nfrom nonebot import on_command\n\nmatcher = on_command(\"help\", aliases={\"帮助\"})\n```\n\n此外，`command` 响应规则默认允许消息命令与参数间不加空格，如果需要严格匹配命令与参数间的空白符，可以使用 `command` 函数的 `force_whitespace` 参数。`force_whitespace` 参数可以是 bool 类型或者具体的字符串，默认为 `False`。如果为 `True`，则命令与参数间必须有任意个数的空白符；如果为字符串，则命令与参数间必须有且与给定字符串一致的空白符。\n\n```python\nrule = command(\"help\", force_whitespace=True)\nrule = command(\"help\", force_whitespace=\" \")\n```\n\n命令解析后的结果可以通过 [`Command`](./dependency.mdx#command)、[`RawCommand`](./dependency.mdx#rawcommand)、[`CommandArg`](./dependency.mdx#commandarg)、[`CommandStart`](./dependency.mdx#commandstart)、[`CommandWhitespace`](./dependency.mdx#commandwhitespace) 依赖注入获取。\n\n### `shell_command`\n\n`shell_command` 响应规则用于匹配类 shell 命令形式的消息。它首先与 [`command`](#command) 响应规则一样进行命令匹配，如果匹配成功，则会进行进一步的参数解析。参数解析采用 `argparse` 标准库进行，在此基础上添加了消息序列 `Message` 支持。\n\n例如，我们可以创建一个匹配 `/cmd` 命令并且带有 `-v` 选项与默认 `-h` 帮助选项的规则：\n\n```python\nfrom nonebot.rule import shell_command, ArgumentParser\n\nparser = ArgumentParser()\nparser.add_argument(\"-v\", \"--verbose\", action=\"store_true\")\n\nrule = shell_command(\"cmd\", parser=parser)\n```\n\n更多关于 `argparse` 的使用方法请参考 [argparse 文档](https://docs.python.org/zh-cn/3/library/argparse.html)。我们也可以选择不提供 `parser` 参数，这样 `shell_command` 将不会解析参数，但会提供参数列表 `argv`。\n\n直接使用辅助函数新建一个响应器：\n\n```python\nfrom nonebot import on_shell_command\nfrom nonebot.rule import ArgumentParser\n\nparser = ArgumentParser()\nparser.add_argument(\"-v\", \"--verbose\", action=\"store_true\")\n\nmatcher = on_shell_command(\"cmd\", parser=parser)\n```\n\n参数解析后的结果可以通过 [`ShellCommandArgv`](./dependency.mdx#shellcommandargv)、[`ShellCommandArgs`](./dependency.mdx#shellcommandargs) 依赖注入获取。\n\n### `regex`\n\n`regex` 响应规则用于匹配消息是否与指定正则表达式匹配。\n\n:::tip 提示\n正则表达式匹配使用 search 而非 match，如需从头匹配请使用 `r\"^xxx\"` 模式来确保匹配开头。\n:::\n\n例如，我们可以创建一个匹配消息中包含字母并且忽略大小写的规则：\n\n```python\nfrom nonebot.rule import regex\n\nrule = regex(r\"[a-z]+\", flags=re.IGNORECASE)\n```\n\n也可以直接使用辅助函数新建一个响应器：\n\n```python\nfrom nonebot import on_regex\n\nmatcher = on_regex(r\"[a-z]+\", flags=re.IGNORECASE)\n```\n\n正则匹配后的结果可以通过 [`RegexStr`](./dependency.mdx#regexstr)、[`RegexGroup`](./dependency.mdx#regexgroup)、[`RegexDict`](./dependency.mdx#regexdict) 依赖注入获取。\n\n### `to_me`\n\n`to_me` 响应规则用于匹配事件是否与机器人相关。\n\n例如：\n\n```python\nfrom nonebot.rule import to_me\n\nrule = to_me()\n```\n\n### `is_type`\n\n`is_type` 响应规则用于匹配事件类型是否为指定类型（或者一系列类型）。\n\n例如，我们可以创建一个匹配 OneBot v11 私聊和群聊消息事件的规则：\n\n```python\nfrom nonebot.rule import is_type\nfrom nonebot.adapters.onebot.v11 import PrivateMessageEvent, GroupMessageEvent\n\nrule = is_type(PrivateMessageEvent, GroupMessageEvent)\n```\n\n## 响应器组\n\n为了更方便的管理一系列功能相近的响应器，NoneBot 提供了两种响应器组，它们可以帮助我们进行响应器的统一管理。\n\n### `CommandGroup`\n\n`CommandGroup` 可以用于管理一系列具有相同前置命令的子命令响应器。\n\n例如，我们创建 `/cmd`、`/cmd.sub`、`/cmd.help` 三个命令，他们具有相同的优先级：\n\n```python\nfrom nonebot import CommandGroup\n\ngroup = CommandGroup(\"cmd\", priority=10)\n\ncmd = group.command(tuple())\nsub_cmd = group.command(\"sub\")\nhelp_cmd = group.command(\"help\")\n```\n\n命令别名 aliases 默认不会添加 `CommandGroup` 设定的前缀，如果需要为 aliases 添加前缀，可以添加 `prefix_aliases=True` 参数:\n\n```python\nfrom nonebot import CommandGroup\n\ngroup = CommandGroup(\"cmd\", prefix_aliases=True)\n\ncmd = group.command(tuple())\nhelp_cmd = group.command(\"help\", aliases={\"帮助\"})\n```\n\n这样就能成功匹配 `/cmd`、`/cmd.help`、`/cmd.帮助` 命令。如果未设置，将默认匹配 `/cmd`、`/cmd.help`、`/帮助` 命令。\n\n### `MatcherGroup`\n\n`MatcherGroup` 可以用于管理一系列具有相同属性的响应器。\n\n例如，我们创建一个具有相同响应规则的响应器组：\n\n```python\nfrom nonebot.rule import to_me\nfrom nonebot import MatcherGroup\n\ngroup = MatcherGroup(rule=to_me())\n\nmatcher1 = group.on_message()\nmatcher2 = group.on_message()\n```\n\n## 第三方响应规则\n\n### Alconna\n\n[`nonebot-plugin-alconna`](https://github.com/nonebot/plugin-alconna) 是一类提供了拓展响应规则的插件。\n该插件使用 [Alconna](https://github.com/ArcletProject/Alconna) 作为命令解析器，\n是一个简单、灵活、高效的命令参数解析器, 并且不局限于解析命令式字符串。\n\n该插件提供了一类新的事件响应器辅助函数 `on_alconna`，以及 `AlconnaResult` 等依赖注入函数。\n\n基于 `Alconna` 的特性，该插件同时提供了一系列便捷的消息段标注。\n标注可用于在 `Alconna` 中匹配消息中除 text 外的其他消息段，也可用于快速创建各适配器下的消息段。所有标注位于 `nonebot_plugin_alconna.adapters` 中。\n\n该插件同时通过提供 `UniMessage` (通用消息模型) 实现了**跨平台接收和发送消息**的功能。\n\n详情请阅读最佳实践中的 [命令解析拓展](../best-practice/alconna/README.mdx) 章节。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/advanced/plugin-info.md",
    "content": "---\nsidebar_position: 2\ndescription: 填写与获取插件相关的信息\n\noptions:\n  menu:\n    - category: advanced\n      weight: 30\n---\n\n# 插件信息\n\nNoneBot 是一个插件化的框架，可以通过加载插件来扩展功能。同时，我们也可以通过 NoneBot 的插件系统来获取相关信息，例如插件的名称、使用方法，用于收集帮助信息等。下面我们将介绍如何为插件添加元数据，以及如何获取插件信息。\n\n## 插件元数据\n\n在 NoneBot 中，插件 [`Plugin`](../api/plugin/model.md#Plugin) 对象中存储了插件系统所需要的一系列信息。包括插件的索引名称、插件模块、插件中的事件响应器、插件父子关系等。通常，只有插件开发者才需要关心这些信息，而插件使用者或者机器人用户想要看到的是插件使用方法等帮助信息。因此，我们可以为插件添加插件元数据 `PluginMetadata`，它允许插件开发者为插件添加一些额外的信息。这些信息编写于插件模块的顶层，可以直接通过源码查看，或者通过 NoneBot 插件系统获取收集到的信息，通过其他方式发送给机器人用户等。\n\n现在，假设我们有一个插件 `example`, 它的模块结构如下：\n\n```tree {4-6} title=Project\n📦 awesome-bot\n├── 📂 awesome_bot\n│   └── 📂 plugins\n|       └── 📂 example\n|           ├── 📜 __init__.py\n|           └── 📜 config.py\n├── 📜 pyproject.toml\n└── 📜 README.md\n```\n\n我们需要在插件顶层模块 `example/__init__.py` 中添加插件元数据，如下所示：\n\n```python {1,5-12} title=example/__init__.py\nfrom nonebot.plugin import PluginMetadata\n\nfrom .config import Config\n\n__plugin_meta__ = PluginMetadata(\n    name=\"示例插件\",\n    description=\"这是一个示例插件\",\n    usage=\"没什么用\",\n    type=\"application\",\n    config=Config,\n    extra={},\n)\n```\n\n我们可以看到，插件元数据 `PluginMetadata` 有三个基本属性：插件名称、插件描述、插件使用方法。除此之外，还有几个可选的属性（具体填写见[发布插件](../developer/plugin-publishing.mdx#插件元数据)章节）：\n\n- `type`：插件类别，发布插件必填。当前有效类别有：`library`（为其他插件编写提供功能），`application`（向机器人用户提供功能）；\n- `homepage`：插件项目主页，发布插件必填；\n- `config`：插件的[配置类](../appendices/config.mdx#插件配置)，发布插件时如有配置类则必须填写；\n- `supported_adapters`：支持的适配器模块名集合，若插件只使用了 NoneBot 基本抽象，应显式填写 `None`；\n- `extra`：一个字典，可以用于存储任意信息。其他插件可以通过约定 `extra` 字典的键名来达成收集某些特殊信息的目的。\n\n请注意，这里的**插件名称**是供使用者或机器人用户查看的人类可读名称，与插件索引名称无关。**插件索引名称（插件模块名称）**仅用于 NoneBot 插件系统**内部索引**。\n\n## 获取插件信息\n\nNoneBot 提供了多种获取插件对象的方法，例如获取当前所有已导入的插件：\n\n```python\nimport nonebot\n\nplugins: set[Plugin] = nonebot.get_loaded_plugins()\n```\n\n也可以通过插件索引名称获取插件对象：\n\n```python\nimport nonebot\n\nplugin: Plugin | None = nonebot.get_plugin(\"example\")\n```\n\n或者通过模块路径获取插件对象：\n\n```python\nimport nonebot\n\nplugin: Plugin | None = nonebot.get_plugin_by_module_name(\"awesome_bot.plugins.example\")\n```\n\n如果需要获取所有当前声明的插件名称（可能还未加载），可以使用 `get_available_plugin_names` 函数：\n\n```python\nimport nonebot\n\nplugin_names: set[str] = nonebot.get_available_plugin_names()\n```\n\n插件对象 `Plugin` 中包含了多个属性：\n\n- `name`：插件索引名称\n- `module`：插件模块\n- `module_name`：插件模块路径\n- `manager`：插件管理器\n- `matcher`：插件中定义的事件响应器\n- `parent_plugin`：插件的父插件\n- `sub_plugins`：插件的子插件集合\n- `metadata`：插件元数据\n\n通过这些属性以及插件元数据，我们就可以收集所需要的插件信息了。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/advanced/plugin-nesting.md",
    "content": "---\nsidebar_position: 3\ndescription: 编写与加载嵌套插件\n\noptions:\n  menu:\n    - category: advanced\n      weight: 40\n---\n\n# 嵌套插件\n\nNoneBot 支持嵌套插件，即一个插件可以包含其他插件。通过这种方式，我们可以将一个大型插件拆分成多个功能子插件，使得插件更加清晰、易于维护。我们可以直接在插件中使用 NoneBot 加载插件的方法来加载子插件。\n\n## 创建嵌套插件\n\n我们可以在使用 `nb-cli` 命令[创建插件](../tutorial/create-plugin.md#创建插件)时，选择直接通过模板创建一个嵌套插件：\n\n```bash\n$ nb plugin create\n[?] 插件名称: parent\n[?] 使用嵌套插件? (y/N) Y\n[?] 输出目录: awesome_bot/plugins\n```\n\n或者使用 `nb plugin create --sub-plugin` 选项直接创建一个嵌套插件。\n\n## 已有插件\n\n如果你已经有一个插件，想要在其中嵌套加载子插件，可以在插件的 `__init__.py` 中添加如下代码：\n\n```python title=parent/__init__.py\nimport nonebot\nfrom pathlib import Path\n\nsub_plugins = nonebot.load_plugins(\n    str(Path(__file__).parent.joinpath(\"plugins\").resolve())\n)\n```\n\n这样，`parent` 插件就会加载 `parent/plugins` 目录下的所有插件。NoneBot 会正确识别这些插件的父子关系，你可以在 `parent` 的插件信息中看到这些子插件的信息，也可以在子插件信息中看到它们的父插件信息。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/advanced/requiring.md",
    "content": "---\nsidebar_position: 4\ndescription: 使用其他插件提供的功能\n\noptions:\n  menu:\n    - category: advanced\n      weight: 50\n---\n\n# 跨插件访问\n\nNoneBot 插件化系统的设计使得插件之间可以功能独立、各司其职，我们可以更好地维护和扩展插件。但是，有时候我们可能需要在不同插件之间调用功能。NoneBot 生态中就有一类插件，它们专为其他插件提供功能支持，如：[定时任务插件](../best-practice/scheduler.md)、[数据存储插件](../best-practice/data-storing.md)等。这时候我们就需要在插件之间进行跨插件访问。\n\n## 插件跟踪\n\n由于 NoneBot 插件系统通过 [Import Hooks](https://docs.python.org/3/reference/import.html#import-hooks) 的方式实现插件加载与跟踪管理，因此我们**不能**在 NoneBot 跟踪插件前进行模块 import，这会导致插件加载失败。即，我们不能在使用 NoneBot 提供的加载插件方法前，直接使用 `import` 语句导入插件。\n\n对于在项目目录下的插件，我们通常直接使用 `load_from_toml` 等方法一次性加载所有插件。由于这些插件已经被声明，即便插件导入顺序不同，NoneBot 也能正确跟踪插件。此时，我们不需要对跨插件访问进行特殊处理。但当我们使用了外部插件，如果没有事先声明或加载插件，NoneBot 并不会将其当作插件进行跟踪，可能会出现意料之外的错误出现。\n\n简单来说，我们必须在 `import` 外部插件之前，确保依赖的外部插件已经被声明或加载。\n\n## 插件依赖声明\n\nNoneBot 提供了一种方法来确保我们依赖的插件已经被正确加载，即使用 `require` 函数。通过 `require` 函数，我们可以在当前插件中声明依赖的插件，NoneBot 会在加载当前插件时，检查依赖的插件是否已经被加载，如果没有，会尝试优先加载依赖的插件。\n\n假设我们有一个插件 `a` 依赖于插件 `b`，我们可以在插件 `a` 中使用 `require` 函数声明其依赖于插件 `b`：\n\n```python {3} title=a/__init__.py\nfrom nonebot import require\n\nrequire(\"b\")\n\nfrom b import some_function\n```\n\n其中，`require` 函数的参数为插件索引名称或者外部插件的模块名称。在完成依赖声明后，我们可以在插件 `a` 中直接导入插件 `b` 所提供的功能。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/advanced/routing.md",
    "content": "---\nsidebar_position: 9\ndescription: 添加服务端路由规则\n\noptions:\n  menu:\n    - category: advanced\n      weight: 100\n---\n\n# 添加路由\n\n在[驱动器](./driver.md)一节中，我们了解了驱动器的两种类型。既然驱动器可以作为服务端运行，那么我们就可以向驱动器添加路由规则，从而实现自定义的 API 接口等功能。在添加路由规则时，我们需要注意驱动器的类型，详情可以参考[选择驱动器](./driver.md#配置驱动器)。\n\nNoneBot 中，我们可以通过两种途径向 ASGI 驱动器添加路由规则：\n\n1. 通过 NoneBot 的兼容层建立路由规则。\n2. 直接向 ASGI 应用添加路由规则。\n\n这两种途径各有优劣，前者可以在各种服务端型驱动器下运行，但并不能直接使用 ASGI 应用框架提供的特性与功能；后者直接使用 ASGI 应用，更自由、功能完整，但只能在特定类型驱动器下运行。\n\n在向驱动器添加路由规则时，我们需要注意驱动器是否为服务端类型，我们可以通过以下方式判断：\n\n```python\nfrom nonebot import get_driver\nfrom nonebot.drivers import ASGIMixin\n\n# highlight-next-line\ncan_use = isinstance(get_driver(), ASGIMixin)\n```\n\n## 通过兼容层添加路由\n\nNoneBot 兼容层定义了两个数据类 `HTTPServerSetup` 和 `WebSocketServerSetup`，分别用于定义 HTTP 服务端和 WebSocket 服务端的路由规则。\n\n### HTTP 路由\n\n`HTTPServerSetup` 具有四个属性：\n\n- `path`：路由路径，不支持特殊占位表达式。类型为 `URL`。\n- `method`：请求方法。类型为 `str`。\n- `name`：路由名称，不可重复。类型为 `str`。\n- `handle_func`：路由处理函数。类型为 `Callable[[Request], Awaitable[Response]]`。\n\n例如，我们添加一个 `/hello` 的路由，当请求方法为 `GET` 时，返回 `200 OK` 以及返回体信息：\n\n```python\nfrom nonebot import get_driver\nfrom nonebot.drivers import URL, Request, Response, ASGIMixin, HTTPServerSetup\n\nasync def hello(request: Request) -> Response:\n    return Response(200, content=\"Hello, world!\")\n\nif isinstance((driver := get_driver()), ASGIMixin):\n    driver.setup_http_server(\n        HTTPServerSetup(\n            path=URL(\"/hello\"),\n            method=\"GET\",\n            name=\"hello\",\n            handle_func=hello,\n        )\n    )\n```\n\n对于 `Request` 和 `Response` 的详细信息，可以参考 [API 文档](../api/drivers/index.md)。\n\n### WebSocket 路由\n\n`WebSocketServerSetup` 具有三个属性：\n\n- `path`：路由路径，不支持特殊占位表达式。类型为 `URL`。\n- `name`：路由名称，不可重复。类型为 `str`。\n- `handle_func`：路由处理函数。类型为 `Callable[[WebSocket], Awaitable[Any]]`。\n\n例如，我们添加一个 `/ws` 的路由，发送所有接收到的数据：\n\n```python\nfrom nonebot import get_driver\nfrom nonebot.drivers import URL, ASGIMixin, WebSocket, WebSocketServerSetup\n\nasync def ws_handler(ws: WebSocket):\n    await ws.accept()\n    try:\n      while True:\n          data = await ws.receive()\n          await ws.send(data)\n    except WebSocketClosed as e:\n        # handle closed\n        ...\n    finally:\n        with contextlib.suppress(Exception):\n            await websocket.close()\n        # do some cleanup\n\nif isinstance((driver := get_driver()), ASGIMixin):\n    driver.setup_websocket_server(\n        WebSocketServerSetup(\n            path=URL(\"/ws\"),\n            name=\"ws\",\n            handle_func=ws_handler,\n        )\n    )\n```\n\n对于 `WebSocket` 的详细信息，可以参考 [API 文档](../api/drivers/index.md)。\n\n## 使用 ASGI 应用添加路由\n\n### 获取 ASGI 应用\n\nNoneBot 服务端类型的驱动器具有两个属性 `server_app` 和 `asgi`，分别对应驱动框架应用和 ASGI 应用。通常情况下，这两个应用是同一个对象。我们可以通过 `get_app()` 方法快速获取：\n\n```python\nimport nonebot\n\napp = nonebot.get_app()\nasgi = nonebot.get_asgi()\n```\n\n### 添加路由规则\n\n在获取到了 ASGI 应用后，我们就可以直接使用 ASGI 应用框架提供的功能来添加路由规则了。这里我们以 [FastAPI](./driver.md#fastapi默认) 为例，演示如何添加路由规则。\n\n在下面的代码中，我们添加了一个 `GET` 类型的 `/api` 路由，具体方法参考 [FastAPI 文档](https://fastapi.tiangolo.com/)。\n\n```python\nimport nonebot\nfrom fastapi import FastAPI\n\napp: FastAPI = nonebot.get_app()\n\n@app.get(\"/api\")\nasync def custom_api():\n    return {\"message\": \"Hello, world!\"}\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/advanced/runtime-hook.md",
    "content": "---\nsidebar_position: 8\ndescription: 在特定的生命周期中执行代码\n\noptions:\n  menu:\n    - category: advanced\n      weight: 90\n---\n\n# 钩子函数\n\n> [钩子编程](https://zh.wikipedia.org/wiki/%E9%92%A9%E5%AD%90%E7%BC%96%E7%A8%8B)（hooking），也称作“挂钩”，是计算机程序设计术语，指通过拦截软件模块间的函数调用、消息传递、事件传递来修改或扩展操作系统、应用程序或其他软件组件的行为的各种技术。处理被拦截的函数调用、事件、消息的代码，被称为钩子（hook）。\n\n在 NoneBot 中有一系列预定义的钩子函数，可以分为两类：**全局钩子函数**和**事件处理钩子函数**，这些钩子函数可以用装饰器的形式来使用。\n\n## 全局钩子函数\n\n全局钩子函数是指 NoneBot 针对其本身运行过程的钩子函数。\n\n这些钩子函数是由驱动器来运行的，故需要先[获得全局驱动器](./driver.md#获取驱动器)。\n\n### 启动准备\n\n这个钩子函数会在 NoneBot 启动时运行。很多时候，我们并不希望在模块被导入时就执行一些耗时操作，如：连接数据库，这时候我们可以在这个钩子函数中进行这些操作。\n\n```python\nfrom nonebot import get_driver\n\ndriver = get_driver()\n\n@driver.on_startup\nasync def do_something():\n    pass\n```\n\n### 终止处理\n\n这个钩子函数会在 NoneBot 终止时运行。我们可以在这个钩子函数中进行一些清理工作，如：关闭数据库连接。\n\n```python\nfrom nonebot import get_driver\n\ndriver = get_driver()\n\n@driver.on_shutdown\nasync def do_something():\n    pass\n```\n\n### Bot 连接处理\n\n这个钩子函数会在任何协议适配器连接 `Bot` 对象至 NoneBot 时运行。支持依赖注入，可以直接注入 `Bot` 对象。\n\n```python\nfrom nonebot import get_driver\n\ndriver = get_driver()\n\n@driver.on_bot_connect\nasync def do_something(bot: Bot):\n    pass\n```\n\n### Bot 断开处理\n\n这个钩子函数会在 `Bot` 断开与 NoneBot 的连接时运行。支持依赖注入，可以直接注入 `Bot` 对象。\n\n```python\nfrom nonebot import get_driver\n\ndriver = get_driver()\n\n@driver.on_bot_disconnect\nasync def do_something(bot: Bot):\n    pass\n```\n\n## 事件处理钩子函数\n\n这些钩子函数指的是影响 NoneBot 进行**事件处理**的函数, 这些函数可以跟普通的事件处理函数一样接受相应的参数。\n\n### 事件预处理\n\n这个钩子函数会在 NoneBot 接收到新的事件时运行。支持依赖注入，可以注入 `Bot` 对象、事件、会话状态。在这个钩子函数内抛出 `nonebot.exception.IgnoredException` 会使 NoneBot 忽略该事件。\n\n```python\nfrom nonebot.exception import IgnoredException\nfrom nonebot.message import event_preprocessor\n\n@event_preprocessor\nasync def do_something(event: Event):\n    if not event.is_tome():\n        raise IgnoredException(\"some reason\")\n```\n\n### 事件后处理\n\n这个钩子函数会在 NoneBot 处理事件完成后运行。支持依赖注入，可以注入 `Bot` 对象、事件、会话状态。\n\n```python\nfrom nonebot.message import event_postprocessor\n\n@event_postprocessor\nasync def do_something(event: Event):\n    pass\n```\n\n### 运行预处理\n\n这个钩子函数会在 NoneBot 运行事件响应器前运行。支持依赖注入，可以注入 `Bot` 对象、事件、事件响应器、会话状态。在这个钩子函数内抛出 `nonebot.exception.IgnoredException` 也会使 NoneBot 忽略本次运行。\n\n```python\nfrom nonebot.message import run_preprocessor\nfrom nonebot.exception import IgnoredException\n\n@run_preprocessor\nasync def do_something(event: Event, matcher: Matcher):\n    if not event.is_tome():\n        raise IgnoredException(\"some reason\")\n```\n\n### 运行后处理\n\n这个钩子函数会在 NoneBot 运行事件响应器后运行。支持依赖注入，可以注入 `Bot` 对象、事件、事件响应器、会话状态、运行中产生的异常。\n\n```python\nfrom nonebot.message import run_postprocessor\n\n@run_postprocessor\nasync def do_something(event: Event, matcher: Matcher, exception: Optional[Exception]):\n    pass\n```\n\n### 平台接口调用钩子\n\n这个钩子函数会在 `Bot` 对象调用平台接口时运行。在这个钩子函数中，我们可以通过引起 `MockApiException` 异常来阻止 `Bot` 对象调用平台接口并返回指定的结果。\n\n```python\nfrom nonebot.adapters import Bot\nfrom nonebot.exception import MockApiException\n\n@Bot.on_calling_api\nasync def handle_api_call(bot: Bot, api: str, data: Dict[str, Any]):\n    if api == \"send_msg\":\n        raise MockApiException(result={\"message_id\": 123})\n```\n\n### 平台接口调用后钩子\n\n这个钩子函数会在 `Bot` 对象调用平台接口后运行。在这个钩子函数中，我们可以通过引起 `MockApiException` 异常来忽略平台接口返回的结果并返回指定的结果。\n\n```python\nfrom nonebot.adapters import Bot\nfrom nonebot.exception import MockApiException\n\n@Bot.on_called_api\nasync def handle_api_result(\n    bot: Bot, exception: Optional[Exception], api: str, data: Dict[str, Any], result: Any\n):\n    if not exception and api == \"send_msg\":\n        raise MockApiException(result={**result, \"message_id\": 123})\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/advanced/session-updating.md",
    "content": "---\nsidebar_position: 7\ndescription: 控制会话响应对象\n\noptions:\n  menu:\n    - category: advanced\n      weight: 80\n---\n\n# 会话更新\n\n在 NoneBot 中，在某个事件响应器对事件响应后，即是进入了会话状态，会话状态会持续到整个事件响应流程结束。会话过程中，机器人可以与用户进行多次交互。每次需要等待用户事件时，NoneBot 将会复制一个新的临时事件响应器，并更新该事件响应器使其响应当前会话主体的消息，这个过程称为会话更新。\n\n会话更新分为两部分：**更新[事件响应器类型](./matcher.md#事件响应器类型)**和**更新[事件触发权限](./matcher.md#事件触发权限)**。\n\n## 更新事件响应器类型\n\n通常情况下，与机器人用户进行的会话都是通过消息事件进行的，因此会话更新后的默认响应事件类型为 `message`。如果希望接收一个特定类型的消息，比如 `notice` 等，我们需要自定义响应事件类型更新函数。响应事件类型更新函数是一个 `Dependent`，可以使用依赖注入。\n\n```python {3-5}\nfoo = on_message()\n\n@foo.type_updater\nasync def _() -> str:\n    return \"notice\"\n```\n\n在注册了上述响应事件类型更新函数后，当我们需要等待用户事件时，将只会响应 `notice` 类型的事件。如果希望在会话过程中的不同阶段响应不同类型的事件，我们就需要使用更复杂的逻辑来更新响应事件类型（如：根据会话状态），这里将不再展示。\n\n## 更新事件触发权限\n\n会话通常是由机器人与用户进行的一对一交互，因此会话更新后的默认触发权限为当前事件的会话 ID。这个会话 ID 由协议适配器生成，通常由用户 ID 和群 ID 等组成。如果希望实现更复杂的会话功能（如：多用户同时参与的会话），我们需要自定义触发权限更新函数。触发权限更新函数是一个 `Dependent`，可以使用依赖注入。\n\n```python {5-7}\nfrom nonebot.permission import User\n\nfoo = on_message()\n\n@foo.permission_updater\nasync def _(event: Event, matcher: Matcher) -> Permission:\n    return Permission(User.from_event(event, perm=matcher.permission))\n```\n\n上述权限更新函数是默认的权限更新函数，它将会话的触发权限更新为当前事件的会话 ID。如果我们希望响应多个用户的消息，我们可以如下修改：\n\n```python {5-7}\nfrom nonebot.permission import USER\n\nfoo = on_message()\n\n@foo.permission_updater\nasync def _(matcher: Matcher) -> Permission:\n    return USER(\"session1\", \"session2\", perm=matcher.permission)\n```\n\n请注意，此处为全大写字母的 `USER` 权限，它可以匹配多个会话 ID。通过这种方式，我们可以实现多用户同时参与的会话。\n\n我们已经了解了如何控制会话的更新，相信你已经能够实现更复杂的会话功能了，例如多人小游戏等等。欢迎将你的作品分享到[插件商店](/store/plugins)。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/api/.gitkeep",
    "content": ""
  },
  {
    "path": "website/versioned_docs/version-2.4.2/api/adapters/_category_.json",
    "content": "{\n  \"position\": 15\n}\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/api/adapters/index.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 0\ndescription: nonebot.adapters 模块\n---\n\n# nonebot.adapters\n\n本模块定义了协议适配基类，各协议请继承以下基类。\n\n使用 [Driver.register_adapter](../drivers/index.md#Driver-register-adapter) 注册适配器。\n\n## _abstract class_ `Adapter(driver, **kwargs)` {#Adapter}\n\n- **说明**\n\n  协议适配器基类。\n\n  通常，在 Adapter 中编写协议通信相关代码，如: 建立通信连接、处理接收与发送 data 等。\n\n- **参数**\n  - `driver` ([Driver](../drivers/index.md#Driver)): [Driver](../drivers/index.md#Driver) 实例\n\n  - `**kwargs` (Any): 其他由 [Driver.register_adapter](../drivers/index.md#Driver-register-adapter) 传入的额外参数\n\n### _instance-var_ `driver` {#Adapter-driver}\n\n- **类型:** [Driver](../drivers/index.md#Driver)\n\n- **说明:** 实例\n\n### _instance-var_ `bots` {#Adapter-bots}\n\n- **类型:** dict[str, [Bot](#Bot)]\n\n- **说明:** 本协议适配器已建立连接的 [Bot](#Bot) 实例\n\n### _abstract classmethod_ `get_name()` {#Adapter-get-name}\n\n- **说明:** 当前协议适配器的名称\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n### _property_ `config` {#Adapter-config}\n\n- **类型:** [Config](../config.md#Config)\n\n- **说明:** 全局 NoneBot 配置\n\n### _method_ `bot_connect(bot)` {#Adapter-bot-connect}\n\n- **说明**\n\n  告知 NoneBot 建立了一个新的 [Bot](#Bot) 连接。\n\n  当有新的 [Bot](#Bot) 实例连接建立成功时调用。\n\n- **参数**\n  - `bot` ([Bot](#Bot)): [Bot](#Bot) 实例\n\n- **返回**\n  - None\n\n### _method_ `bot_disconnect(bot)` {#Adapter-bot-disconnect}\n\n- **说明**\n\n  告知 NoneBot [Bot](#Bot) 连接已断开。\n\n  当有 [Bot](#Bot) 实例连接断开时调用。\n\n- **参数**\n  - `bot` ([Bot](#Bot)): [Bot](#Bot) 实例\n\n- **返回**\n  - None\n\n### _method_ `setup_http_server(setup)` {#Adapter-setup-http-server}\n\n- **说明:** 设置一个 HTTP 服务器路由配置\n\n- **参数**\n  - `setup` ([HTTPServerSetup](../drivers/index.md#HTTPServerSetup))\n\n- **返回**\n  - untyped\n\n### _method_ `setup_websocket_server(setup)` {#Adapter-setup-websocket-server}\n\n- **说明:** 设置一个 WebSocket 服务器路由配置\n\n- **参数**\n  - `setup` ([WebSocketServerSetup](../drivers/index.md#WebSocketServerSetup))\n\n- **返回**\n  - untyped\n\n### _async method_ `request(setup)` {#Adapter-request}\n\n- **说明:** 进行一个 HTTP 客户端请求\n\n- **参数**\n  - `setup` ([Request](../drivers/index.md#Request))\n\n- **返回**\n  - [Response](../drivers/index.md#Response)\n\n### _method_ `websocket(setup)` {#Adapter-websocket}\n\n- **说明:** 建立一个 WebSocket 客户端连接请求\n\n- **参数**\n  - `setup` ([Request](../drivers/index.md#Request))\n\n- **返回**\n  - AsyncGenerator[[WebSocket](../drivers/index.md#WebSocket), None]\n\n### _method_ `on_ready(func)` {#Adapter-on-ready}\n\n- **参数**\n  - `func` (LIFESPAN_FUNC)\n\n- **返回**\n  - LIFESPAN_FUNC\n\n## _abstract class_ `Bot(adapter, self_id)` {#Bot}\n\n- **说明**\n\n  Bot 基类。\n\n  用于处理上报消息，并提供 API 调用接口。\n\n- **参数**\n  - `adapter` ([Adapter](#Adapter)): 协议适配器实例\n\n  - `self_id` (str): 机器人 ID\n\n### _instance-var_ `adapter` {#Bot-adapter}\n\n- **类型:** [Adapter](#Adapter)\n\n- **说明:** 协议适配器实例\n\n### _instance-var_ `self_id` {#Bot-self-id}\n\n- **类型:** str\n\n- **说明:** 机器人 ID\n\n### _property_ `type` {#Bot-type}\n\n- **类型:** str\n\n- **说明:** 协议适配器名称\n\n### _property_ `config` {#Bot-config}\n\n- **类型:** [Config](../config.md#Config)\n\n- **说明:** 全局 NoneBot 配置\n\n### _async method_ `call_api(api, **data)` {#Bot-call-api}\n\n- **说明:** 调用机器人 API 接口，可以通过该函数或直接通过 bot 属性进行调用\n\n- **参数**\n  - `api` (str): API 名称\n\n  - `**data` (Any): API 数据\n\n- **返回**\n  - Any\n\n- **用法**\n\n  ```python\n  await bot.call_api(\"send_msg\", message=\"hello world\")\n  await bot.send_msg(message=\"hello world\")\n  ```\n\n### _abstract async method_ `send(event, message, **kwargs)` {#Bot-send}\n\n- **说明:** 调用机器人基础发送消息接口\n\n- **参数**\n  - `event` ([Event](#Event)): 上报事件\n\n  - `message` (str | [Message](#Message) | [MessageSegment](#MessageSegment)): 要发送的消息\n\n  - `**kwargs` (Any): 任意额外参数\n\n- **返回**\n  - Any\n\n### _classmethod_ `on_calling_api(func)` {#Bot-on-calling-api}\n\n- **说明**\n\n  调用 api 预处理。\n\n  钩子函数参数:\n  - bot: 当前 bot 对象\n  - api: 调用的 api 名称\n  - data: api 调用的参数字典\n\n- **参数**\n  - `func` ([T_CallingAPIHook](../typing.md#T-CallingAPIHook))\n\n- **返回**\n  - [T_CallingAPIHook](../typing.md#T-CallingAPIHook)\n\n### _classmethod_ `on_called_api(func)` {#Bot-on-called-api}\n\n- **说明**\n\n  调用 api 后处理。\n\n  钩子函数参数:\n  - bot: 当前 bot 对象\n  - exception: 调用 api 时发生的错误\n  - api: 调用的 api 名称\n  - data: api 调用的参数字典\n  - result: api 调用的返回\n\n- **参数**\n  - `func` ([T_CalledAPIHook](../typing.md#T-CalledAPIHook))\n\n- **返回**\n  - [T_CalledAPIHook](../typing.md#T-CalledAPIHook)\n\n## _abstract class_ `Event(<auto>)` {#Event}\n\n- **说明:** Event 基类。提供获取关键信息的方法，其余信息可直接获取。\n\n- **参数**\n\n  auto\n\n### _abstract method_ `get_type()` {#Event-get-type}\n\n- **说明:** 获取事件类型的方法，类型通常为 NoneBot 内置的四种类型。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n### _abstract method_ `get_event_name()` {#Event-get-event-name}\n\n- **说明:** 获取事件名称的方法。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n### _abstract method_ `get_event_description()` {#Event-get-event-description}\n\n- **说明:** 获取事件描述的方法，通常为事件具体内容。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n### _method_ `get_log_string()` {#Event-get-log-string}\n\n- **说明**\n\n  获取事件日志信息的方法。\n\n  通常你不需要修改这个方法，只有当希望 NoneBot 隐藏该事件日志时，\n  可以抛出 `NoLogException` 异常。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n- **异常**\n  - NoLogException: 希望 NoneBot 隐藏该事件日志\n\n### _abstract method_ `get_user_id()` {#Event-get-user-id}\n\n- **说明:** 获取事件主体 id 的方法，通常是用户 id 。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n### _abstract method_ `get_session_id()` {#Event-get-session-id}\n\n- **说明:** 获取会话 id 的方法，用于判断当前事件属于哪一个会话， 通常是用户 id、群组 id 组合。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n### _abstract method_ `get_message()` {#Event-get-message}\n\n- **说明:** 获取事件消息内容的方法。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - [Message](#Message)\n\n### _method_ `get_plaintext()` {#Event-get-plaintext}\n\n- **说明**\n\n  获取消息纯文本的方法。\n\n  通常不需要修改，默认通过 `get_message().extract_plain_text` 获取。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n### _abstract method_ `is_tome()` {#Event-is-tome}\n\n- **说明:** 获取事件是否与机器人有关的方法。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - bool\n\n## _abstract class_ `Message(<auto>)` {#Message}\n\n- **说明:** 消息序列\n\n- **参数**\n  - `message`: 消息内容\n\n### _classmethod_ `template(format_string)` {#Message-template}\n\n- **说明**\n\n  创建消息模板。\n\n  用法和 `str.format` 大致相同，支持以 `Message` 对象作为消息模板并输出消息对象。\n  并且提供了拓展的格式化控制符，\n  可以通过该消息类型的 `MessageSegment` 工厂方法创建消息。\n\n- **参数**\n  - `format_string` (str | TM): 格式化模板\n\n- **返回**\n  - [MessageTemplate](#MessageTemplate)[Self]: 消息格式化器\n\n### _abstract classmethod_ `get_segment_class()` {#Message-get-segment-class}\n\n- **说明:** 获取消息段类型\n\n- **参数**\n\n  empty\n\n- **返回**\n  - type[TMS]\n\n### _abstract staticmethod_ `_construct(msg)` {#Message--construct}\n\n- **说明:** 构造消息数组\n\n- **参数**\n  - `msg` (str)\n\n- **返回**\n  - Iterable[TMS]\n\n### _method_ `__getitem__(args)` {#Message---getitem--}\n\n- **重载**\n\n  **1.** `(args) -> Self`\n  - **参数**\n    - `args` (str): 消息段类型\n\n  - **返回**\n    - Self: 所有类型为 `args` 的消息段\n\n  **2.** `(args) -> TMS`\n  - **参数**\n    - `args` (tuple[str, int]): 消息段类型和索引\n\n  - **返回**\n    - TMS: 类型为 `args[0]` 的消息段第 `args[1]` 个\n\n  **3.** `(args) -> Self`\n  - **参数**\n    - `args` (tuple[str, slice]): 消息段类型和切片\n\n  - **返回**\n    - Self: 类型为 `args[0]` 的消息段切片 `args[1]`\n\n  **4.** `(args) -> TMS`\n  - **参数**\n    - `args` (int): 索引\n\n  - **返回**\n    - TMS: 第 `args` 个消息段\n\n  **5.** `(args) -> Self`\n  - **参数**\n    - `args` (slice): 切片\n\n  - **返回**\n    - Self: 消息切片 `args`\n\n### _method_ `__contains__(value)` {#Message---contains--}\n\n- **说明:** 检查消息段是否存在\n\n- **参数**\n  - `value` (TMS | str): 消息段或消息段类型\n\n- **返回**\n  - bool: 消息内是否存在给定消息段或给定类型的消息段\n\n### _method_ `has(value)` {#Message-has}\n\n- **说明:** 与 [`__contains__`](#Message---contains--) 相同\n\n- **参数**\n  - `value` (TMS | str)\n\n- **返回**\n  - bool\n\n### _method_ `index(value, *args)` {#Message-index}\n\n- **说明:** 索引消息段\n\n- **参数**\n  - `value` (TMS | str): 消息段或者消息段类型\n\n  - `*args` (SupportsIndex)\n\n  - `arg`: start 与 end\n\n- **返回**\n  - int: 索引 index\n\n- **异常**\n  - ValueError: 消息段不存在\n\n### _method_ `get(type_, count=None)` {#Message-get}\n\n- **说明:** 获取指定类型的消息段\n\n- **参数**\n  - `type_` (str): 消息段类型\n\n  - `count` (int | None): 获取个数\n\n- **返回**\n  - Self: 构建的新消息\n\n### _method_ `count(value)` {#Message-count}\n\n- **说明:** 计算指定消息段的个数\n\n- **参数**\n  - `value` (TMS | str): 消息段或消息段类型\n\n- **返回**\n  - int: 个数\n\n### _method_ `only(value)` {#Message-only}\n\n- **说明:** 检查消息中是否仅包含指定消息段\n\n- **参数**\n  - `value` (TMS | str): 指定消息段或消息段类型\n\n- **返回**\n  - bool: 是否仅包含指定消息段\n\n### _method_ `append(obj)` {#Message-append}\n\n- **说明:** 添加一个消息段到消息数组末尾。\n\n- **参数**\n  - `obj` (str | TMS): 要添加的消息段\n\n- **返回**\n  - Self\n\n### _method_ `extend(obj)` {#Message-extend}\n\n- **说明:** 拼接一个消息数组或多个消息段到消息数组末尾。\n\n- **参数**\n  - `obj` (Self | Iterable[TMS]): 要添加的消息数组\n\n- **返回**\n  - Self\n\n### _method_ `join(iterable)` {#Message-join}\n\n- **说明:** 将多个消息连接并将自身作为分割\n\n- **参数**\n  - `iterable` (Iterable[TMS | Self]): 要连接的消息\n\n- **返回**\n  - Self: 连接后的消息\n\n### _method_ `copy()` {#Message-copy}\n\n- **说明:** 深拷贝消息\n\n- **参数**\n\n  empty\n\n- **返回**\n  - Self\n\n### _method_ `include(*types)` {#Message-include}\n\n- **说明:** 过滤消息\n\n- **参数**\n  - `*types` (str): 包含的消息段类型\n\n- **返回**\n  - Self: 新构造的消息\n\n### _method_ `exclude(*types)` {#Message-exclude}\n\n- **说明:** 过滤消息\n\n- **参数**\n  - `*types` (str): 不包含的消息段类型\n\n- **返回**\n  - Self: 新构造的消息\n\n### _method_ `extract_plain_text()` {#Message-extract-plain-text}\n\n- **说明:** 提取消息内纯文本消息\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n## _abstract class_ `MessageSegment(<auto>)` {#MessageSegment}\n\n- **说明:** 消息段基类\n\n- **参数**\n\n  auto\n\n### _instance-var_ `type` {#MessageSegment-type}\n\n- **类型:** str\n\n- **说明:** 消息段类型\n\n### _class-var_ `data` {#MessageSegment-data}\n\n- **类型:** dict[str, Any]\n\n- **说明:** 消息段数据\n\n### _abstract classmethod_ `get_message_class()` {#MessageSegment-get-message-class}\n\n- **说明:** 获取消息数组类型\n\n- **参数**\n\n  empty\n\n- **返回**\n  - type[TM]\n\n### _abstract method_ `__str__()` {#MessageSegment---str--}\n\n- **说明:** 该消息段所代表的 str，在命令匹配部分使用\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n### _method_ `__add__(other)` {#MessageSegment---add--}\n\n- **参数**\n  - `other` (str | TMS | Iterable[TMS])\n\n- **返回**\n  - TM\n\n### _method_ `get(key, default=None)` {#MessageSegment-get}\n\n- **参数**\n  - `key` (str)\n\n  - `default` (Any)\n\n- **返回**\n  - untyped\n\n### _method_ `keys()` {#MessageSegment-keys}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - untyped\n\n### _method_ `values()` {#MessageSegment-values}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - untyped\n\n### _method_ `items()` {#MessageSegment-items}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - untyped\n\n### _method_ `join(iterable)` {#MessageSegment-join}\n\n- **参数**\n  - `iterable` (Iterable[TMS | TM])\n\n- **返回**\n  - TM\n\n### _method_ `copy()` {#MessageSegment-copy}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - Self\n\n### _abstract method_ `is_text()` {#MessageSegment-is-text}\n\n- **说明:** 当前消息段是否为纯文本\n\n- **参数**\n\n  empty\n\n- **返回**\n  - bool\n\n## _class_ `MessageTemplate(template, factory=str, private_getattr=False)` {#MessageTemplate}\n\n- **说明:** 消息模板格式化实现类。\n\n- **参数**\n  - `template` (str | TM): 模板\n\n  - `factory` (type[str] | type[TM]): 消息类型工厂，默认为 `str`\n\n  - `private_getattr` (bool): 是否允许在模板中访问私有属性，默认为 `False`\n\n### _method_ `add_format_spec(spec, name=None)` {#MessageTemplate-add-format-spec}\n\n- **参数**\n  - `spec` (FormatSpecFunc_T)\n\n  - `name` (str | None)\n\n- **返回**\n  - FormatSpecFunc_T\n\n### _method_ `format(*args, **kwargs)` {#MessageTemplate-format}\n\n- **说明:** 根据传入参数和模板生成消息对象\n\n- **参数**\n  - `*args`\n\n  - `**kwargs`\n\n- **返回**\n  - TF\n\n### _method_ `format_map(mapping)` {#MessageTemplate-format-map}\n\n- **说明:** 根据传入字典和模板生成消息对象, 在传入字段名不是有效标识符时有用\n\n- **参数**\n  - `mapping` (Mapping[str, Any])\n\n- **返回**\n  - TF\n\n### _method_ `vformat(format_string, args, kwargs)` {#MessageTemplate-vformat}\n\n- **参数**\n  - `format_string` (str)\n\n  - `args` (Sequence[Any])\n\n  - `kwargs` (Mapping[str, Any])\n\n- **返回**\n  - TF\n\n### _method_ `get_field(field_name, args, kwargs)` {#MessageTemplate-get-field}\n\n- **参数**\n  - `field_name` (str)\n\n  - `args` (Sequence[Any])\n\n  - `kwargs` (Mapping[str, Any])\n\n- **返回**\n  - tuple[Any, int | str]\n\n### _method_ `format_field(value, format_spec)` {#MessageTemplate-format-field}\n\n- **参数**\n  - `value` (Any)\n\n  - `format_spec` (str)\n\n- **返回**\n  - Any\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/api/compat.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 16\ndescription: nonebot.compat 模块\n---\n\n# nonebot.compat\n\n本模块为 Pydantic 版本兼容层模块\n\n为兼容 Pydantic V1 与 V2 版本，定义了一系列兼容函数与类供使用。\n\n## _var_ `Required` {#Required}\n\n- **类型:** untyped\n\n- **说明:** Alias of Ellipsis for compatibility with pydantic v1\n\n## _library-attr_ `PydanticUndefined` {#PydanticUndefined}\n\n- **说明:** Pydantic Undefined object\n\n## _library-attr_ `PydanticUndefinedType` {#PydanticUndefinedType}\n\n- **说明:** Pydantic Undefined type\n\n## _var_ `DEFAULT_CONFIG` {#DEFAULT-CONFIG}\n\n- **类型:** untyped\n\n- **说明:** Default config for validations\n\n## _class_ `FieldInfo(default=PydanticUndefined, **kwargs)` {#FieldInfo}\n\n- **说明:** FieldInfo class with extra property for compatibility with pydantic v1\n\n- **参数**\n  - `default` (Any)\n\n  - `**kwargs` (Any)\n\n### _property_ `extra` {#FieldInfo-extra}\n\n- **类型:** dict[str, Any]\n\n- **说明**\n\n  Extra data that is not part of the standard pydantic fields.\n\n  For compatibility with pydantic v1.\n\n## _class_ `ModelField(<auto>)` {#ModelField}\n\n- **说明:** ModelField class for compatibility with pydantic v1\n\n- **参数**\n\n  auto\n\n### _instance-var_ `name` {#ModelField-name}\n\n- **类型:** str\n\n- **说明:** The name of the field.\n\n### _instance-var_ `annotation` {#ModelField-annotation}\n\n- **类型:** Any\n\n- **说明:** The annotation of the field.\n\n### _instance-var_ `field_info` {#ModelField-field-info}\n\n- **类型:** FieldInfo\n\n- **说明:** The FieldInfo of the field.\n\n### _classmethod_ `construct(name, annotation, field_info=None)` {#ModelField-construct}\n\n- **说明:** Construct a ModelField from given infos.\n\n- **参数**\n  - `name` (str)\n\n  - `annotation` (Any)\n\n  - `field_info` (FieldInfo | None)\n\n- **返回**\n  - Self\n\n### _method_ `get_default()` {#ModelField-get-default}\n\n- **说明:** Get the default value of the field.\n\n- **参数**\n\n  empty\n\n- **返回**\n  - Any\n\n### _method_ `validate_value(value)` {#ModelField-validate-value}\n\n- **说明:** Validate the value pass to the field.\n\n- **参数**\n  - `value` (Any)\n\n- **返回**\n  - Any\n\n## _def_ `extract_field_info(field_info)` {#extract-field-info}\n\n- **说明:** Get FieldInfo init kwargs from a FieldInfo instance.\n\n- **参数**\n  - `field_info` (BaseFieldInfo)\n\n- **返回**\n  - dict[str, Any]\n\n## _def_ `model_fields(model)` {#model-fields}\n\n- **说明:** Get field list of a model.\n\n- **参数**\n  - `model` (type[BaseModel])\n\n- **返回**\n  - list[ModelField]\n\n## _def_ `model_config(model)` {#model-config}\n\n- **说明:** Get config of a model.\n\n- **参数**\n  - `model` (type[BaseModel])\n\n- **返回**\n  - Any\n\n## _def_ `model_dump(model, include=None, exclude=None, by_alias=False, exclude_unset=False, exclude_defaults=False, exclude_none=False)` {#model-dump}\n\n- **参数**\n  - `model` (BaseModel)\n\n  - `include` (set[str] | None)\n\n  - `exclude` (set[str] | None)\n\n  - `by_alias` (bool)\n\n  - `exclude_unset` (bool)\n\n  - `exclude_defaults` (bool)\n\n  - `exclude_none` (bool)\n\n- **返回**\n  - dict[str, Any]\n\n## _def_ `type_validate_python(type_, data)` {#type-validate-python}\n\n- **说明:** Validate data with given type.\n\n- **参数**\n  - `type_` (type[T])\n\n  - `data` (Any)\n\n- **返回**\n  - T\n\n## _def_ `type_validate_json(type_, data)` {#type-validate-json}\n\n- **说明:** Validate JSON with given type.\n\n- **参数**\n  - `type_` (type[T])\n\n  - `data` (str | bytes)\n\n- **返回**\n  - T\n\n## _def_ `custom_validation(class_)` {#custom-validation}\n\n- **说明:** Use pydantic v1 like validator generator in pydantic v2\n\n- **参数**\n  - `class_` (type[CVC])\n\n- **返回**\n  - type[CVC]\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/api/config.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 1\ndescription: nonebot.config 模块\n---\n\n# nonebot.config\n\n本模块定义了 NoneBot 本身运行所需的配置项。\n\nNoneBot 使用 [`pydantic`](https://pydantic-docs.helpmanual.io/) 以及\n[`python-dotenv`](https://saurabh-kumar.com/python-dotenv/) 来读取配置。\n\n配置项需符合特殊格式或 json 序列化格式\n详情见 [`pydantic Field Type`](https://pydantic-docs.helpmanual.io/usage/types/) 文档。\n\n## _class_ `Env(_env_file=ENV_FILE_SENTINEL, _env_file_encoding=None, _env_nested_delimiter=None, **values)` {#Env}\n\n- **说明**\n\n  运行环境配置。大小写不敏感。\n\n  将会从 **环境变量** > **dotenv 配置文件** 的优先级读取环境信息。\n\n- **参数**\n  - `_env_file` (DOTENV_TYPE | None)\n\n  - `_env_file_encoding` (str | None)\n\n  - `_env_nested_delimiter` (str | None)\n\n  - `**values` (Any)\n\n### _class-var_ `environment` {#Env-environment}\n\n- **类型:** str\n\n- **说明**\n\n  当前环境名。\n\n  NoneBot 将从 `.env.{environment}` 文件中加载配置。\n\n## _class_ `Config(_env_file=ENV_FILE_SENTINEL, _env_file_encoding=None, _env_nested_delimiter=None, **values)` {#Config}\n\n- **说明**\n\n  NoneBot 主要配置。大小写不敏感。\n\n  除了 NoneBot 的配置项外，还可以自行添加配置项到 `.env.{environment}` 文件中。\n  这些配置将会在 json 反序列化后一起带入 `Config` 类中。\n\n  配置方法参考: [配置](https://nonebot.dev/docs/appendices/config)\n\n- **参数**\n  - `_env_file` (DOTENV_TYPE | None)\n\n  - `_env_file_encoding` (str | None)\n\n  - `_env_nested_delimiter` (str | None)\n\n  - `**values` (Any)\n\n### _class-var_ `driver` {#Config-driver}\n\n- **类型:** str\n\n- **说明**\n\n  NoneBot 运行所使用的 `Driver` 。继承自 [Driver](drivers/index.md#Driver) 。\n\n  配置格式为 `<module>[:<Driver>][+<module>[:<Mixin>]]*`。\n\n  `~` 为 `nonebot.drivers.` 的缩写。\n\n  配置方法参考: [配置驱动器](https://nonebot.dev/docs/advanced/driver#%E9%85%8D%E7%BD%AE%E9%A9%B1%E5%8A%A8%E5%99%A8)\n\n### _class-var_ `host` {#Config-host}\n\n- **类型:** IPvAnyAddress\n\n- **说明:** NoneBot [ReverseDriver](drivers/index.md#ReverseDriver) 服务端监听的 IP/主机名。\n\n### _class-var_ `port` {#Config-port}\n\n- **类型:** int\n\n- **说明:** NoneBot [ReverseDriver](drivers/index.md#ReverseDriver) 服务端监听的端口。\n\n### _class-var_ `log_level` {#Config-log-level}\n\n- **类型:** int | str\n\n- **说明**\n\n  NoneBot 日志输出等级，可以为 `int` 类型等级或等级名称。\n\n  参考 [记录日志](https://nonebot.dev/docs/appendices/log)，[loguru 日志等级](https://loguru.readthedocs.io/en/stable/api/logger.html#levels)。\n\n  :::tip 提示\n  日志等级名称应为大写，如 `INFO`。\n  :::\n\n- **用法**\n\n  ```conf\n  LOG_LEVEL=25\n  LOG_LEVEL=INFO\n  ```\n\n### _class-var_ `api_timeout` {#Config-api-timeout}\n\n- **类型:** float | None\n\n- **说明:** API 请求超时时间，单位: 秒。\n\n### _class-var_ `superusers` {#Config-superusers}\n\n- **类型:** set[str]\n\n- **说明:** 机器人超级用户。\n\n- **用法**\n\n  ```conf\n  SUPERUSERS=[\"12345789\"]\n  ```\n\n### _class-var_ `nickname` {#Config-nickname}\n\n- **类型:** set[str]\n\n- **说明:** 机器人昵称。\n\n### _class-var_ `command_start` {#Config-command-start}\n\n- **类型:** set[str]\n\n- **说明**\n\n  命令的起始标记，用于判断一条消息是不是命令。\n\n  参考[命令响应规则](https://nonebot.dev/docs/advanced/matcher#command)。\n\n- **用法**\n\n  ```conf\n  COMMAND_START=[\"/\", \"\"]\n  ```\n\n### _class-var_ `command_sep` {#Config-command-sep}\n\n- **类型:** set[str]\n\n- **说明**\n\n  命令的分隔标记，用于将文本形式的命令切分为元组（实际的命令名）。\n\n  参考[命令响应规则](https://nonebot.dev/docs/advanced/matcher#command)。\n\n- **用法**\n\n  ```conf\n  COMMAND_SEP=[\".\"]\n  ```\n\n### _class-var_ `session_expire_timeout` {#Config-session-expire-timeout}\n\n- **类型:** timedelta\n\n- **说明:** 等待用户回复的超时时间。\n\n- **用法**\n\n  ```conf\n  SESSION_EXPIRE_TIMEOUT=[-][DD]D[,][HH:MM:]SS[.ffffff]\n  SESSION_EXPIRE_TIMEOUT=[±]P[DD]DT[HH]H[MM]M[SS]S  # ISO 8601\n  ```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/api/consts.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 9\ndescription: nonebot.consts 模块\n---\n\n# nonebot.consts\n\n本模块包含了 NoneBot 事件处理过程中使用到的常量。\n\n## _var_ `RECEIVE_KEY` {#RECEIVE-KEY}\n\n- **类型:** Literal['\\_receive\\_{id}']\n\n- **说明:** `receive` 存储 key\n\n## _var_ `LAST_RECEIVE_KEY` {#LAST-RECEIVE-KEY}\n\n- **类型:** Literal['\\_last\\_receive']\n\n- **说明:** `last_receive` 存储 key\n\n## _var_ `ARG_KEY` {#ARG-KEY}\n\n- **类型:** Literal['{key}']\n\n- **说明:** `arg` 存储 key\n\n## _var_ `REJECT_TARGET` {#REJECT-TARGET}\n\n- **类型:** Literal['\\_current\\_target']\n\n- **说明:** 当前 `reject` 目标存储 key\n\n## _var_ `REJECT_CACHE_TARGET` {#REJECT-CACHE-TARGET}\n\n- **类型:** Literal['\\_next\\_target']\n\n- **说明:** 下一个 `reject` 目标存储 key\n\n## _var_ `PAUSE_PROMPT_RESULT_KEY` {#PAUSE-PROMPT-RESULT-KEY}\n\n- **类型:** Literal['\\_pause\\_result']\n\n- **说明:** `pause` prompt 发送结果存储 key\n\n## _var_ `REJECT_PROMPT_RESULT_KEY` {#REJECT-PROMPT-RESULT-KEY}\n\n- **类型:** Literal['\\_reject\\_{key}\\_result']\n\n- **说明:** `reject` prompt 发送结果存储 key\n\n## _var_ `PREFIX_KEY` {#PREFIX-KEY}\n\n- **类型:** Literal['\\_prefix']\n\n- **说明:** 命令前缀存储 key\n\n## _var_ `CMD_KEY` {#CMD-KEY}\n\n- **类型:** Literal['command']\n\n- **说明:** 命令元组存储 key\n\n## _var_ `RAW_CMD_KEY` {#RAW-CMD-KEY}\n\n- **类型:** Literal['raw\\_command']\n\n- **说明:** 命令文本存储 key\n\n## _var_ `CMD_ARG_KEY` {#CMD-ARG-KEY}\n\n- **类型:** Literal['command\\_arg']\n\n- **说明:** 命令参数存储 key\n\n## _var_ `CMD_START_KEY` {#CMD-START-KEY}\n\n- **类型:** Literal['command\\_start']\n\n- **说明:** 命令开头存储 key\n\n## _var_ `CMD_WHITESPACE_KEY` {#CMD-WHITESPACE-KEY}\n\n- **类型:** Literal['command\\_whitespace']\n\n- **说明:** 命令与参数间空白符存储 key\n\n## _var_ `SHELL_ARGS` {#SHELL-ARGS}\n\n- **类型:** Literal['\\_args']\n\n- **说明:** shell 命令 parse 后参数字典存储 key\n\n## _var_ `SHELL_ARGV` {#SHELL-ARGV}\n\n- **类型:** Literal['\\_argv']\n\n- **说明:** shell 命令原始参数列表存储 key\n\n## _var_ `REGEX_MATCHED` {#REGEX-MATCHED}\n\n- **类型:** Literal['\\_matched']\n\n- **说明:** 正则匹配结果存储 key\n\n## _var_ `STARTSWITH_KEY` {#STARTSWITH-KEY}\n\n- **类型:** Literal['\\_startswith']\n\n- **说明:** 响应触发前缀 key\n\n## _var_ `ENDSWITH_KEY` {#ENDSWITH-KEY}\n\n- **类型:** Literal['\\_endswith']\n\n- **说明:** 响应触发后缀 key\n\n## _var_ `FULLMATCH_KEY` {#FULLMATCH-KEY}\n\n- **类型:** Literal['\\_fullmatch']\n\n- **说明:** 响应触发完整消息 key\n\n## _var_ `KEYWORD_KEY` {#KEYWORD-KEY}\n\n- **类型:** Literal['\\_keyword']\n\n- **说明:** 响应触发关键字 key\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/api/dependencies/_category_.json",
    "content": "{\n  \"position\": 13\n}\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/api/dependencies/index.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 0\ndescription: nonebot.dependencies 模块\n---\n\n# nonebot.dependencies\n\n本模块模块实现了依赖注入的定义与处理。\n\n## _abstract class_ `Param(*args, validate=False, **kwargs)` {#Param}\n\n- **说明**\n\n  依赖注入的基本单元 —— 参数。\n\n  继承自 `pydantic.fields.FieldInfo`，用于描述参数信息（不包括参数名）。\n\n- **参数**\n  - `*args`\n\n  - `validate` (bool)\n\n  - `**kwargs` (Any)\n\n## _class_ `Dependent(<auto>)` {#Dependent}\n\n- **说明:** 依赖注入容器\n\n- **参数**\n  - `call`: 依赖注入的可调用对象，可以是任何 Callable 对象\n\n  - `pre_checkers`: 依赖注入解析前的参数检查\n\n  - `params`: 具名参数列表\n\n  - `parameterless`: 匿名参数列表\n\n  - `allow_types`: 允许的参数类型\n\n### _staticmethod_ `parse_params(call, allow_types)` {#Dependent-parse-params}\n\n- **参数**\n  - `call` (\\_DependentCallable[R])\n\n  - `allow_types` (tuple[type[Param], ...])\n\n- **返回**\n  - tuple[[ModelField](../compat.md#ModelField), ...]\n\n### _staticmethod_ `parse_parameterless(parameterless, allow_types)` {#Dependent-parse-parameterless}\n\n- **参数**\n  - `parameterless` (tuple[Any, ...])\n\n  - `allow_types` (tuple[type[Param], ...])\n\n- **返回**\n  - tuple[Param, ...]\n\n### _classmethod_ `parse(*, call, parameterless=None, allow_types)` {#Dependent-parse}\n\n- **参数**\n  - `call` (\\_DependentCallable[R])\n\n  - `parameterless` (Iterable[Any] | None)\n\n  - `allow_types` (Iterable[type[Param]])\n\n- **返回**\n  - Dependent[R]\n\n### _async method_ `check(**params)` {#Dependent-check}\n\n- **参数**\n  - `**params` (Any)\n\n- **返回**\n  - None\n\n### _async method_ `solve(**params)` {#Dependent-solve}\n\n- **参数**\n  - `**params` (Any)\n\n- **返回**\n  - dict[str, Any]\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/api/dependencies/utils.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 1\ndescription: nonebot.dependencies.utils 模块\n---\n\n# nonebot.dependencies.utils\n\n## _def_ `get_typed_signature(call)` {#get-typed-signature}\n\n- **说明:** 获取可调用对象签名\n\n- **参数**\n  - `call` ((...) -> Any)\n\n- **返回**\n  - inspect.Signature\n\n## _def_ `get_typed_annotation(param, globalns)` {#get-typed-annotation}\n\n- **说明:** 获取参数的类型注解\n\n- **参数**\n  - `param` (inspect.Parameter)\n\n  - `globalns` (dict[str, Any])\n\n- **返回**\n  - Any\n\n## _def_ `check_field_type(field, value)` {#check-field-type}\n\n- **说明:** 检查字段类型是否匹配\n\n- **参数**\n  - `field` ([ModelField](../compat.md#ModelField))\n\n  - `value` (Any)\n\n- **返回**\n  - Any\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/api/drivers/_category_.json",
    "content": "{\n  \"position\": 14\n}\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/api/drivers/aiohttp.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 2\ndescription: nonebot.drivers.aiohttp 模块\n---\n\n# nonebot.drivers.aiohttp\n\n[AIOHTTP](https://aiohttp.readthedocs.io/en/stable/) 驱动适配器。\n\n```bash\nnb driver install aiohttp\n# 或者\npip install nonebot2[aiohttp]\n```\n\n:::tip 提示\n本驱动仅支持客户端连接\n:::\n\n## _class_ `Session(params=None, headers=None, cookies=None, version=HTTPVersion.H11, timeout=None, proxy=None)` {#Session}\n\n- **参数**\n  - `params` (QueryTypes)\n\n  - `headers` (HeaderTypes)\n\n  - `cookies` (CookieTypes)\n\n  - `version` (str | [HTTPVersion](index.md#HTTPVersion))\n\n  - `timeout` (float | None)\n\n  - `proxy` (str | None)\n\n### _async method_ `request(setup)` {#Session-request}\n\n- **参数**\n  - `setup` ([Request](index.md#Request))\n\n- **返回**\n  - [Response](index.md#Response)\n\n### _async method_ `setup()` {#Session-setup}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - None\n\n### _async method_ `close()` {#Session-close}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - None\n\n## _class_ `Mixin(<auto>)` {#Mixin}\n\n- **说明:** AIOHTTP Mixin\n\n- **参数**\n\n  auto\n\n### _async method_ `request(setup)` {#Mixin-request}\n\n- **参数**\n  - `setup` ([Request](index.md#Request))\n\n- **返回**\n  - [Response](index.md#Response)\n\n### _method_ `websocket(setup)` {#Mixin-websocket}\n\n- **参数**\n  - `setup` ([Request](index.md#Request))\n\n- **返回**\n  - AsyncGenerator[[WebSocket](index.md#WebSocket), None]\n\n### _method_ `get_session(params=None, headers=None, cookies=None, version=HTTPVersion.H11, timeout=None, proxy=None)` {#Mixin-get-session}\n\n- **参数**\n  - `params` (QueryTypes)\n\n  - `headers` (HeaderTypes)\n\n  - `cookies` (CookieTypes)\n\n  - `version` (str | [HTTPVersion](index.md#HTTPVersion))\n\n  - `timeout` (float | None)\n\n  - `proxy` (str | None)\n\n- **返回**\n  - Session\n\n## _class_ `WebSocket(*, request, session, websocket)` {#WebSocket}\n\n- **说明:** AIOHTTP Websocket Wrapper\n\n- **参数**\n  - `request` ([Request](index.md#Request))\n\n  - `session` (aiohttp.ClientSession)\n\n  - `websocket` (aiohttp.ClientWebSocketResponse)\n\n### _async method_ `accept()` {#WebSocket-accept}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - untyped\n\n### _async method_ `close(code=1000, reason=\"\")` {#WebSocket-close}\n\n- **参数**\n  - `code` (int)\n\n  - `reason` (str)\n\n- **返回**\n  - untyped\n\n### _async method_ `receive()` {#WebSocket-receive}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n### _async method_ `receive_text()` {#WebSocket-receive-text}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n### _async method_ `receive_bytes()` {#WebSocket-receive-bytes}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - bytes\n\n### _async method_ `send_text(data)` {#WebSocket-send-text}\n\n- **参数**\n  - `data` (str)\n\n- **返回**\n  - None\n\n### _async method_ `send_bytes(data)` {#WebSocket-send-bytes}\n\n- **参数**\n  - `data` (bytes)\n\n- **返回**\n  - None\n\n## _class_ `Driver(env, config)` {#Driver}\n\n- **参数**\n  - `env` ([Env](../config.md#Env))\n\n  - `config` ([Config](../config.md#Config))\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/api/drivers/fastapi.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 1\ndescription: nonebot.drivers.fastapi 模块\n---\n\n# nonebot.drivers.fastapi\n\n[FastAPI](https://fastapi.tiangolo.com/) 驱动适配\n\n```bash\nnb driver install fastapi\n# 或者\npip install nonebot2[fastapi]\n```\n\n:::tip 提示\n本驱动仅支持服务端连接\n:::\n\n## _class_ `Config(<auto>)` {#Config}\n\n- **说明:** FastAPI 驱动框架设置，详情参考 FastAPI 文档\n\n- **参数**\n\n  auto\n\n### _class-var_ `fastapi_openapi_url` {#Config-fastapi-openapi-url}\n\n- **类型:** str | None\n\n- **说明:** `openapi.json` 地址，默认为 `None` 即关闭\n\n### _class-var_ `fastapi_docs_url` {#Config-fastapi-docs-url}\n\n- **类型:** str | None\n\n- **说明:** `swagger` 地址，默认为 `None` 即关闭\n\n### _class-var_ `fastapi_redoc_url` {#Config-fastapi-redoc-url}\n\n- **类型:** str | None\n\n- **说明:** `redoc` 地址，默认为 `None` 即关闭\n\n### _class-var_ `fastapi_include_adapter_schema` {#Config-fastapi-include-adapter-schema}\n\n- **类型:** bool\n\n- **说明:** 是否包含适配器路由的 schema，默认为 `True`\n\n### _class-var_ `fastapi_reload` {#Config-fastapi-reload}\n\n- **类型:** bool\n\n- **说明:** 开启/关闭冷重载\n\n### _class-var_ `fastapi_reload_dirs` {#Config-fastapi-reload-dirs}\n\n- **类型:** list[str] | None\n\n- **说明:** 重载监控文件夹列表，默认为 uvicorn 默认值\n\n### _class-var_ `fastapi_reload_delay` {#Config-fastapi-reload-delay}\n\n- **类型:** float\n\n- **说明:** 重载延迟，默认为 uvicorn 默认值\n\n### _class-var_ `fastapi_reload_includes` {#Config-fastapi-reload-includes}\n\n- **类型:** list[str] | None\n\n- **说明:** 要监听的文件列表，支持 glob pattern，默认为 uvicorn 默认值\n\n### _class-var_ `fastapi_reload_excludes` {#Config-fastapi-reload-excludes}\n\n- **类型:** list[str] | None\n\n- **说明:** 不要监听的文件列表，支持 glob pattern，默认为 uvicorn 默认值\n\n### _class-var_ `fastapi_extra` {#Config-fastapi-extra}\n\n- **类型:** dict[str, Any]\n\n- **说明:** 传递给 `FastAPI` 的其他参数。\n\n## _class_ `Driver(env, config)` {#Driver}\n\n- **说明:** FastAPI 驱动框架。\n\n- **参数**\n  - `env` ([Env](../config.md#Env))\n\n  - `config` (NoneBotConfig)\n\n### _property_ `type` {#Driver-type}\n\n- **类型:** str\n\n- **说明:** 驱动名称: `fastapi`\n\n### _property_ `server_app` {#Driver-server-app}\n\n- **类型:** FastAPI\n\n- **说明:** `FastAPI APP` 对象\n\n### _property_ `asgi` {#Driver-asgi}\n\n- **类型:** FastAPI\n\n- **说明:** `FastAPI APP` 对象\n\n### _property_ `logger` {#Driver-logger}\n\n- **类型:** logging.Logger\n\n- **说明:** fastapi 使用的 logger\n\n### _method_ `setup_http_server(setup)` {#Driver-setup-http-server}\n\n- **参数**\n  - `setup` ([HTTPServerSetup](index.md#HTTPServerSetup))\n\n- **返回**\n  - untyped\n\n### _method_ `setup_websocket_server(setup)` {#Driver-setup-websocket-server}\n\n- **参数**\n  - `setup` ([WebSocketServerSetup](index.md#WebSocketServerSetup))\n\n- **返回**\n  - None\n\n### _method_ `run(host=None, port=None, *args, app=None, **kwargs)` {#Driver-run}\n\n- **说明:** 使用 `uvicorn` 启动 FastAPI\n\n- **参数**\n  - `host` (str | None)\n\n  - `port` (int | None)\n\n  - `*args`\n\n  - `app` (str | None)\n\n  - `**kwargs`\n\n- **返回**\n  - untyped\n\n## _class_ `FastAPIWebSocket(*, request, websocket)` {#FastAPIWebSocket}\n\n- **说明:** FastAPI WebSocket Wrapper\n\n- **参数**\n  - `request` (BaseRequest)\n\n  - `websocket` ([WebSocket](index.md#WebSocket))\n\n### _async method_ `accept()` {#FastAPIWebSocket-accept}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - None\n\n### _async method_ `close(code=status.WS_1000_NORMAL_CLOSURE, reason=\"\")` {#FastAPIWebSocket-close}\n\n- **参数**\n  - `code` (int)\n\n  - `reason` (str)\n\n- **返回**\n  - None\n\n### _async method_ `receive()` {#FastAPIWebSocket-receive}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str | bytes\n\n### _async method_ `receive_text()` {#FastAPIWebSocket-receive-text}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n### _async method_ `receive_bytes()` {#FastAPIWebSocket-receive-bytes}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - bytes\n\n### _async method_ `send_text(data)` {#FastAPIWebSocket-send-text}\n\n- **参数**\n  - `data` (str)\n\n- **返回**\n  - None\n\n### _async method_ `send_bytes(data)` {#FastAPIWebSocket-send-bytes}\n\n- **参数**\n  - `data` (bytes)\n\n- **返回**\n  - None\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/api/drivers/httpx.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 3\ndescription: nonebot.drivers.httpx 模块\n---\n\n# nonebot.drivers.httpx\n\n[HTTPX](https://www.python-httpx.org/) 驱动适配\n\n```bash\nnb driver install httpx\n# 或者\npip install nonebot2[httpx]\n```\n\n:::tip 提示\n本驱动仅支持客户端 HTTP 连接\n:::\n\n## _class_ `Session(params=None, headers=None, cookies=None, version=HTTPVersion.H11, timeout=None, proxy=None)` {#Session}\n\n- **参数**\n  - `params` (QueryTypes)\n\n  - `headers` (HeaderTypes)\n\n  - `cookies` (CookieTypes)\n\n  - `version` (str | [HTTPVersion](index.md#HTTPVersion))\n\n  - `timeout` (float | None)\n\n  - `proxy` (str | None)\n\n### _async method_ `request(setup)` {#Session-request}\n\n- **参数**\n  - `setup` ([Request](index.md#Request))\n\n- **返回**\n  - [Response](index.md#Response)\n\n### _async method_ `setup()` {#Session-setup}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - None\n\n### _async method_ `close()` {#Session-close}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - None\n\n## _class_ `Mixin(<auto>)` {#Mixin}\n\n- **说明:** HTTPX Mixin\n\n- **参数**\n\n  auto\n\n### _async method_ `request(setup)` {#Mixin-request}\n\n- **参数**\n  - `setup` ([Request](index.md#Request))\n\n- **返回**\n  - [Response](index.md#Response)\n\n### _method_ `get_session(params=None, headers=None, cookies=None, version=HTTPVersion.H11, timeout=None, proxy=None)` {#Mixin-get-session}\n\n- **参数**\n  - `params` (QueryTypes)\n\n  - `headers` (HeaderTypes)\n\n  - `cookies` (CookieTypes)\n\n  - `version` (str | [HTTPVersion](index.md#HTTPVersion))\n\n  - `timeout` (float | None)\n\n  - `proxy` (str | None)\n\n- **返回**\n  - Session\n\n## _class_ `Driver(env, config)` {#Driver}\n\n- **参数**\n  - `env` ([Env](../config.md#Env))\n\n  - `config` ([Config](../config.md#Config))\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/api/drivers/index.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 0\ndescription: nonebot.drivers 模块\n---\n\n# nonebot.drivers\n\n本模块定义了驱动适配器基类。\n\n各驱动请继承以下基类。\n\n## _abstract class_ `ASGIMixin(<auto>)` {#ASGIMixin}\n\n- **说明**\n\n  ASGI 服务端基类。\n\n  将后端框架封装，以满足适配器使用。\n\n- **参数**\n\n  auto\n\n### _abstract property_ `server_app` {#ASGIMixin-server-app}\n\n- **类型:** Any\n\n- **说明:** 驱动 APP 对象\n\n### _abstract property_ `asgi` {#ASGIMixin-asgi}\n\n- **类型:** Any\n\n- **说明:** 驱动 ASGI 对象\n\n### _abstract method_ `setup_http_server(setup)` {#ASGIMixin-setup-http-server}\n\n- **说明:** 设置一个 HTTP 服务器路由配置\n\n- **参数**\n  - `setup` ([HTTPServerSetup](#HTTPServerSetup))\n\n- **返回**\n  - None\n\n### _abstract method_ `setup_websocket_server(setup)` {#ASGIMixin-setup-websocket-server}\n\n- **说明:** 设置一个 WebSocket 服务器路由配置\n\n- **参数**\n  - `setup` ([WebSocketServerSetup](#WebSocketServerSetup))\n\n- **返回**\n  - None\n\n## _class_ `Cookies(cookies=None)` {#Cookies}\n\n- **参数**\n  - `cookies` (CookieTypes)\n\n### _method_ `set(name, value, domain=\"\", path=\"/\")` {#Cookies-set}\n\n- **参数**\n  - `name` (str)\n\n  - `value` (str)\n\n  - `domain` (str)\n\n  - `path` (str)\n\n- **返回**\n  - None\n\n### _method_ `get(name, default=None, domain=None, path=None)` {#Cookies-get}\n\n- **参数**\n  - `name` (str)\n\n  - `default` (str | None)\n\n  - `domain` (str | None)\n\n  - `path` (str | None)\n\n- **返回**\n  - str | None\n\n### _method_ `delete(name, domain=None, path=None)` {#Cookies-delete}\n\n- **参数**\n  - `name` (str)\n\n  - `domain` (str | None)\n\n  - `path` (str | None)\n\n- **返回**\n  - None\n\n### _method_ `clear(domain=None, path=None)` {#Cookies-clear}\n\n- **参数**\n  - `domain` (str | None)\n\n  - `path` (str | None)\n\n- **返回**\n  - None\n\n### _method_ `update(cookies=None)` {#Cookies-update}\n\n- **参数**\n  - `cookies` (CookieTypes)\n\n- **返回**\n  - None\n\n### _method_ `as_header(request)` {#Cookies-as-header}\n\n- **参数**\n  - `request` (Request)\n\n- **返回**\n  - dict[str, str]\n\n## _abstract class_ `Driver(env, config)` {#Driver}\n\n- **说明**\n\n  驱动器基类。\n\n  驱动器控制框架的启动和停止，适配器的注册，以及机器人生命周期管理。\n\n- **参数**\n  - `env` ([Env](../config.md#Env)): 包含环境信息的 Env 对象\n\n  - `config` ([Config](../config.md#Config)): 包含配置信息的 Config 对象\n\n### _instance-var_ `env` {#Driver-env}\n\n- **类型:** str\n\n- **说明:** 环境名称\n\n### _instance-var_ `config` {#Driver-config}\n\n- **类型:** [Config](../config.md#Config)\n\n- **说明:** 全局配置对象\n\n### _property_ `bots` {#Driver-bots}\n\n- **类型:** dict[str, [Bot](../adapters/index.md#Bot)]\n\n- **说明:** 获取当前所有已连接的 Bot\n\n### _method_ `register_adapter(adapter, **kwargs)` {#Driver-register-adapter}\n\n- **说明:** 注册一个协议适配器\n\n- **参数**\n  - `adapter` (type[[Adapter](../adapters/index.md#Adapter)]): 适配器类\n\n  - `**kwargs`: 其他传递给适配器的参数\n\n- **返回**\n  - None\n\n### _abstract property_ `type` {#Driver-type}\n\n- **类型:** str\n\n- **说明:** 驱动类型名称\n\n### _abstract property_ `logger` {#Driver-logger}\n\n- **类型:** untyped\n\n- **说明:** 驱动专属 logger 日志记录器\n\n### _abstract method_ `run(*args, **kwargs)` {#Driver-run}\n\n- **说明:** 启动驱动框架\n\n- **参数**\n  - `*args`\n\n  - `**kwargs`\n\n- **返回**\n  - untyped\n\n### _method_ `on_startup(func)` {#Driver-on-startup}\n\n- **说明:** 注册一个启动时执行的函数\n\n- **参数**\n  - `func` (LIFESPAN_FUNC)\n\n- **返回**\n  - LIFESPAN_FUNC\n\n### _method_ `on_shutdown(func)` {#Driver-on-shutdown}\n\n- **说明:** 注册一个停止时执行的函数\n\n- **参数**\n  - `func` (LIFESPAN_FUNC)\n\n- **返回**\n  - LIFESPAN_FUNC\n\n### _classmethod_ `on_bot_connect(func)` {#Driver-on-bot-connect}\n\n- **说明**\n\n  装饰一个函数使他在 bot 连接成功时执行。\n\n  钩子函数参数:\n  - bot: 当前连接上的 Bot 对象\n\n- **参数**\n  - `func` ([T_BotConnectionHook](../typing.md#T-BotConnectionHook))\n\n- **返回**\n  - [T_BotConnectionHook](../typing.md#T-BotConnectionHook)\n\n### _classmethod_ `on_bot_disconnect(func)` {#Driver-on-bot-disconnect}\n\n- **说明**\n\n  装饰一个函数使他在 bot 连接断开时执行。\n\n  钩子函数参数:\n  - bot: 当前连接上的 Bot 对象\n\n- **参数**\n  - `func` ([T_BotDisconnectionHook](../typing.md#T-BotDisconnectionHook))\n\n- **返回**\n  - [T_BotDisconnectionHook](../typing.md#T-BotDisconnectionHook)\n\n## _var_ `ForwardDriver` {#ForwardDriver}\n\n- **类型:** ForwardMixin\n\n- **说明**\n\n  支持客户端请求的驱动器。\n\n  **Deprecated**，请使用 [ForwardMixin](#ForwardMixin) 或其子类代替。\n\n## _abstract class_ `ForwardMixin(<auto>)` {#ForwardMixin}\n\n- **说明:** 客户端混入基类。\n\n- **参数**\n\n  auto\n\n## _abstract class_ `HTTPClientMixin(<auto>)` {#HTTPClientMixin}\n\n- **说明:** HTTP 客户端混入基类。\n\n- **参数**\n\n  auto\n\n### _abstract async method_ `request(setup)` {#HTTPClientMixin-request}\n\n- **说明:** 发送一个 HTTP 请求\n\n- **参数**\n  - `setup` ([Request](#Request))\n\n- **返回**\n  - [Response](#Response)\n\n### _abstract method_ `get_session(params=None, headers=None, cookies=None, version=HTTPVersion.H11, timeout=None, proxy=None)` {#HTTPClientMixin-get-session}\n\n- **说明:** 获取一个 HTTP 会话\n\n- **参数**\n  - `params` (QueryTypes)\n\n  - `headers` (HeaderTypes)\n\n  - `cookies` (CookieTypes)\n\n  - `version` (str | [HTTPVersion](#HTTPVersion))\n\n  - `timeout` (float | None)\n\n  - `proxy` (str | None)\n\n- **返回**\n  - HTTPClientSession\n\n## _class_ `HTTPServerSetup(<auto>)` {#HTTPServerSetup}\n\n- **说明:** HTTP 服务器路由配置。\n\n- **参数**\n\n  auto\n\n## _enum_ `HTTPVersion` {#HTTPVersion}\n\n- **说明:** An enumeration.\n\n- **参数**\n\n  auto\n  - `H10: '1.0'`\n\n  - `H11: '1.1'`\n\n  - `H2: '2'`\n\n## _abstract class_ `Mixin(<auto>)` {#Mixin}\n\n- **说明:** 可与其他驱动器共用的混入基类。\n\n- **参数**\n\n  auto\n\n### _abstract property_ `type` {#Mixin-type}\n\n- **类型:** str\n\n- **说明:** 混入驱动类型名称\n\n## _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}\n\n- **参数**\n  - `method` (str | bytes)\n\n  - `url` (URL | str | RawURL)\n\n  - `params` (QueryTypes)\n\n  - `headers` (HeaderTypes)\n\n  - `cookies` (CookieTypes)\n\n  - `content` (ContentTypes)\n\n  - `data` (DataTypes)\n\n  - `json` (Any)\n\n  - `files` (FilesTypes)\n\n  - `version` (str | HTTPVersion)\n\n  - `timeout` (float | None)\n\n  - `proxy` (str | None)\n\n## _class_ `Response(status_code, *, headers=None, content=None, request=None)` {#Response}\n\n- **参数**\n  - `status_code` (int)\n\n  - `headers` (HeaderTypes)\n\n  - `content` (ContentTypes)\n\n  - `request` (Request | None)\n\n## _var_ `ReverseDriver` {#ReverseDriver}\n\n- **类型:** ReverseMixin\n\n- **说明**\n\n  支持服务端请求的驱动器。\n\n  **Deprecated**，请使用 [ReverseMixin](#ReverseMixin) 或其子类代替。\n\n## _abstract class_ `ReverseMixin(<auto>)` {#ReverseMixin}\n\n- **说明:** 服务端混入基类。\n\n- **参数**\n\n  auto\n\n## _abstract class_ `WebSocket(*, request)` {#WebSocket}\n\n- **参数**\n  - `request` (Request)\n\n### _abstract property_ `closed` {#WebSocket-closed}\n\n- **类型:** bool\n\n- **说明:** 连接是否已经关闭\n\n### _abstract async method_ `accept()` {#WebSocket-accept}\n\n- **说明:** 接受 WebSocket 连接请求\n\n- **参数**\n\n  empty\n\n- **返回**\n  - None\n\n### _abstract async method_ `close(code=1000, reason=\"\")` {#WebSocket-close}\n\n- **说明:** 关闭 WebSocket 连接请求\n\n- **参数**\n  - `code` (int)\n\n  - `reason` (str)\n\n- **返回**\n  - None\n\n### _abstract async method_ `receive()` {#WebSocket-receive}\n\n- **说明:** 接收一条 WebSocket text/bytes 信息\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str | bytes\n\n### _abstract async method_ `receive_text()` {#WebSocket-receive-text}\n\n- **说明:** 接收一条 WebSocket text 信息\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n### _abstract async method_ `receive_bytes()` {#WebSocket-receive-bytes}\n\n- **说明:** 接收一条 WebSocket binary 信息\n\n- **参数**\n\n  empty\n\n- **返回**\n  - bytes\n\n### _async method_ `send(data)` {#WebSocket-send}\n\n- **说明:** 发送一条 WebSocket text/bytes 信息\n\n- **参数**\n  - `data` (str | bytes)\n\n- **返回**\n  - None\n\n### _abstract async method_ `send_text(data)` {#WebSocket-send-text}\n\n- **说明:** 发送一条 WebSocket text 信息\n\n- **参数**\n  - `data` (str)\n\n- **返回**\n  - None\n\n### _abstract async method_ `send_bytes(data)` {#WebSocket-send-bytes}\n\n- **说明:** 发送一条 WebSocket binary 信息\n\n- **参数**\n  - `data` (bytes)\n\n- **返回**\n  - None\n\n## _abstract class_ `WebSocketClientMixin(<auto>)` {#WebSocketClientMixin}\n\n- **说明:** WebSocket 客户端混入基类。\n\n- **参数**\n\n  auto\n\n### _abstract method_ `websocket(setup)` {#WebSocketClientMixin-websocket}\n\n- **说明:** 发起一个 WebSocket 连接\n\n- **参数**\n  - `setup` ([Request](#Request))\n\n- **返回**\n  - AsyncGenerator[[WebSocket](#WebSocket), None]\n\n## _class_ `WebSocketServerSetup(<auto>)` {#WebSocketServerSetup}\n\n- **说明:** WebSocket 服务器路由配置。\n\n- **参数**\n\n  auto\n\n## _def_ `combine_driver(driver, *mixins)` {#combine-driver}\n\n- **说明:** 将一个驱动器和多个混入类合并。\n\n- **重载**\n\n  **1.** `(driver) -> type[D]`\n  - **参数**\n    - `driver` (type[D])\n\n  - **返回**\n    - type[D]\n\n  **2.** `(driver, __m, /, *mixins) -> type[CombinedDriver]`\n  - **参数**\n    - `driver` (type[D])\n\n    - `__m` (type[[Mixin](#Mixin)])\n\n    - `*mixins` (type[[Mixin](#Mixin)])\n\n  - **返回**\n    - type[CombinedDriver]\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/api/drivers/none.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 6\ndescription: nonebot.drivers.none 模块\n---\n\n# nonebot.drivers.none\n\nNone 驱动适配\n\n:::tip 提示\n本驱动不支持任何服务器或客户端连接\n:::\n\n## _class_ `Driver(env, config)` {#Driver}\n\n- **说明:** None 驱动框架\n\n- **参数**\n  - `env` ([Env](../config.md#Env))\n\n  - `config` ([Config](../config.md#Config))\n\n### _property_ `type` {#Driver-type}\n\n- **类型:** str\n\n- **说明:** 驱动名称: `none`\n\n### _property_ `logger` {#Driver-logger}\n\n- **类型:** untyped\n\n- **说明:** none driver 使用的 logger\n\n### _method_ `run(*args, **kwargs)` {#Driver-run}\n\n- **说明:** 启动 none driver\n\n- **参数**\n  - `*args`\n\n  - `**kwargs`\n\n- **返回**\n  - untyped\n\n### _method_ `exit(force=False)` {#Driver-exit}\n\n- **说明:** 退出 none driver\n\n- **参数**\n  - `force` (bool): 强制退出\n\n- **返回**\n  - untyped\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/api/drivers/quart.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 5\ndescription: nonebot.drivers.quart 模块\n---\n\n# nonebot.drivers.quart\n\n[Quart](https://pgjones.gitlab.io/quart/index.html) 驱动适配\n\n```bash\nnb driver install quart\n# 或者\npip install nonebot2[quart]\n```\n\n:::tip 提示\n本驱动仅支持服务端连接\n:::\n\n## _class_ `Config(<auto>)` {#Config}\n\n- **说明:** Quart 驱动框架设置\n\n- **参数**\n\n  auto\n\n### _class-var_ `quart_reload` {#Config-quart-reload}\n\n- **类型:** bool\n\n- **说明:** 开启/关闭冷重载\n\n### _class-var_ `quart_reload_dirs` {#Config-quart-reload-dirs}\n\n- **类型:** list[str] | None\n\n- **说明:** 重载监控文件夹列表，默认为 uvicorn 默认值\n\n### _class-var_ `quart_reload_delay` {#Config-quart-reload-delay}\n\n- **类型:** float\n\n- **说明:** 重载延迟，默认为 uvicorn 默认值\n\n### _class-var_ `quart_reload_includes` {#Config-quart-reload-includes}\n\n- **类型:** list[str] | None\n\n- **说明:** 要监听的文件列表，支持 glob pattern，默认为 uvicorn 默认值\n\n### _class-var_ `quart_reload_excludes` {#Config-quart-reload-excludes}\n\n- **类型:** list[str] | None\n\n- **说明:** 不要监听的文件列表，支持 glob pattern，默认为 uvicorn 默认值\n\n### _class-var_ `quart_extra` {#Config-quart-extra}\n\n- **类型:** dict[str, Any]\n\n- **说明:** 传递给 `Quart` 的其他参数。\n\n## _class_ `Driver(env, config)` {#Driver}\n\n- **说明:** Quart 驱动框架\n\n- **参数**\n  - `env` ([Env](../config.md#Env))\n\n  - `config` (NoneBotConfig)\n\n### _property_ `type` {#Driver-type}\n\n- **类型:** str\n\n- **说明:** 驱动名称: `quart`\n\n### _property_ `server_app` {#Driver-server-app}\n\n- **类型:** Quart\n\n- **说明:** `Quart` 对象\n\n### _property_ `asgi` {#Driver-asgi}\n\n- **类型:** untyped\n\n- **说明:** `Quart` 对象\n\n### _property_ `logger` {#Driver-logger}\n\n- **类型:** untyped\n\n- **说明:** Quart 使用的 logger\n\n### _method_ `setup_http_server(setup)` {#Driver-setup-http-server}\n\n- **参数**\n  - `setup` ([HTTPServerSetup](index.md#HTTPServerSetup))\n\n- **返回**\n  - untyped\n\n### _method_ `setup_websocket_server(setup)` {#Driver-setup-websocket-server}\n\n- **参数**\n  - `setup` ([WebSocketServerSetup](index.md#WebSocketServerSetup))\n\n- **返回**\n  - None\n\n### _method_ `run(host=None, port=None, *args, app=None, **kwargs)` {#Driver-run}\n\n- **说明:** 使用 `uvicorn` 启动 Quart\n\n- **参数**\n  - `host` (str | None)\n\n  - `port` (int | None)\n\n  - `*args`\n\n  - `app` (str | None)\n\n  - `**kwargs`\n\n- **返回**\n  - untyped\n\n## _class_ `WebSocket(*, request, websocket_ctx)` {#WebSocket}\n\n- **说明:** Quart WebSocket Wrapper\n\n- **参数**\n  - `request` (BaseRequest)\n\n  - `websocket_ctx` (WebsocketContext)\n\n### _async method_ `accept()` {#WebSocket-accept}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - untyped\n\n### _async method_ `close(code=1000, reason=\"\")` {#WebSocket-close}\n\n- **参数**\n  - `code` (int)\n\n  - `reason` (str)\n\n- **返回**\n  - untyped\n\n### _async method_ `receive()` {#WebSocket-receive}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str | bytes\n\n### _async method_ `receive_text()` {#WebSocket-receive-text}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n### _async method_ `receive_bytes()` {#WebSocket-receive-bytes}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - bytes\n\n### _async method_ `send_text(data)` {#WebSocket-send-text}\n\n- **参数**\n  - `data` (str)\n\n- **返回**\n  - untyped\n\n### _async method_ `send_bytes(data)` {#WebSocket-send-bytes}\n\n- **参数**\n  - `data` (bytes)\n\n- **返回**\n  - untyped\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/api/drivers/websockets.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 4\ndescription: nonebot.drivers.websockets 模块\n---\n\n# nonebot.drivers.websockets\n\n[websockets](https://websockets.readthedocs.io/) 驱动适配\n\n```bash\nnb driver install websockets\n# 或者\npip install nonebot2[websockets]\n```\n\n:::tip 提示\n本驱动仅支持客户端 WebSocket 连接\n:::\n\n## _def_ `catch_closed(func)` {#catch-closed}\n\n- **参数**\n  - `func` ((P) -> CoroutineType[Any, Any, T])\n\n- **返回**\n  - (P) -> CoroutineType[Any, Any, T]\n\n## _class_ `Mixin(<auto>)` {#Mixin}\n\n- **说明:** Websockets Mixin\n\n- **参数**\n\n  auto\n\n### _method_ `websocket(setup)` {#Mixin-websocket}\n\n- **参数**\n  - `setup` ([Request](index.md#Request))\n\n- **返回**\n  - AsyncGenerator[[WebSocket](index.md#WebSocket), None]\n\n## _class_ `WebSocket(*, request, websocket)` {#WebSocket}\n\n- **说明:** Websockets WebSocket Wrapper\n\n- **参数**\n  - `request` ([Request](index.md#Request))\n\n  - `websocket` (WebSocketClientProtocol)\n\n### _async method_ `accept()` {#WebSocket-accept}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - untyped\n\n### _async method_ `close(code=1000, reason=\"\")` {#WebSocket-close}\n\n- **参数**\n  - `code` (int)\n\n  - `reason` (str)\n\n- **返回**\n  - untyped\n\n### _async method_ `receive()` {#WebSocket-receive}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str | bytes\n\n### _async method_ `receive_text()` {#WebSocket-receive-text}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n### _async method_ `receive_bytes()` {#WebSocket-receive-bytes}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - bytes\n\n### _async method_ `send_text(data)` {#WebSocket-send-text}\n\n- **参数**\n  - `data` (str)\n\n- **返回**\n  - None\n\n### _async method_ `send_bytes(data)` {#WebSocket-send-bytes}\n\n- **参数**\n  - `data` (bytes)\n\n- **返回**\n  - None\n\n## _class_ `Driver(env, config)` {#Driver}\n\n- **参数**\n  - `env` ([Env](../config.md#Env))\n\n  - `config` ([Config](../config.md#Config))\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/api/exception.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 10\ndescription: nonebot.exception 模块\n---\n\n# nonebot.exception\n\n本模块包含了所有 NoneBot 运行时可能会抛出的异常。\n\n这些异常并非所有需要用户处理，在 NoneBot 内部运行时被捕获，并进行对应操作。\n\n```bash\nNoneBotException\n├── ParserExit\n├── ProcessException\n|   ├── IgnoredException\n|   ├── SkippedException\n|   |   └── TypeMisMatch\n|   ├── MockApiException\n|   └── StopPropagation\n├── MatcherException\n|   ├── PausedException\n|   ├── RejectedException\n|   └── FinishedException\n├── AdapterException\n|   ├── NoLogException\n|   ├── ApiNotAvailable\n|   ├── NetworkError\n|   └── ActionFailed\n└── DriverException\n    └── WebSocketClosed\n```\n\n## _class_ `NoneBotException(<auto>)` {#NoneBotException}\n\n- **说明:** 所有 NoneBot 发生的异常基类。\n\n- **参数**\n\n  auto\n\n## _class_ `ParserExit(<auto>)` {#ParserExit}\n\n- **说明:** 处理消息失败时返回的异常。\n\n- **参数**\n\n  auto\n\n## _class_ `ProcessException(<auto>)` {#ProcessException}\n\n- **说明:** 事件处理过程中发生的异常基类。\n\n- **参数**\n\n  auto\n\n## _class_ `IgnoredException(<auto>)` {#IgnoredException}\n\n- **说明:** 指示 NoneBot 应该忽略该事件。可由 PreProcessor 抛出。\n\n- **参数**\n  - `reason`: 忽略事件的原因\n\n## _class_ `SkippedException(<auto>)` {#SkippedException}\n\n- **说明**\n\n  指示 NoneBot 立即结束当前 `Dependent` 的运行。\n\n  例如，可以在 `Handler` 中通过 [Matcher.skip](matcher.md#Matcher-skip) 抛出。\n\n- **参数**\n\n  auto\n\n- **用法**\n\n  ```python\n  def always_skip():\n      Matcher.skip()\n\n  @matcher.handle()\n  async def handler(dependency = Depends(always_skip)):\n      # never run\n  ```\n\n## _class_ `TypeMisMatch(<auto>)` {#TypeMisMatch}\n\n- **说明:** 当前 `Handler` 的参数类型不匹配。\n\n- **参数**\n\n  auto\n\n## _class_ `MockApiException(<auto>)` {#MockApiException}\n\n- **说明:** 指示 NoneBot 阻止本次 API 调用或修改本次调用返回值，并返回自定义内容。 可由 api hook 抛出。\n\n- **参数**\n  - `result`: 返回的内容\n\n## _class_ `StopPropagation(<auto>)` {#StopPropagation}\n\n- **说明**\n\n  指示 NoneBot 终止事件向下层传播。\n\n  在 [Matcher.block](matcher.md#Matcher-block) 为 `True`\n  或使用 [Matcher.stop_propagation](matcher.md#Matcher-stop-propagation) 方法时抛出。\n\n- **参数**\n\n  auto\n\n- **用法**\n\n  ```python\n  matcher = on_notice(block=True)\n  # 或者\n  @matcher.handle()\n  async def handler(matcher: Matcher):\n      matcher.stop_propagation()\n  ```\n\n## _class_ `MatcherException(<auto>)` {#MatcherException}\n\n- **说明:** 所有 Matcher 发生的异常基类。\n\n- **参数**\n\n  auto\n\n## _class_ `PausedException(<auto>)` {#PausedException}\n\n- **说明**\n\n  指示 NoneBot 结束当前 `Handler` 并等待下一条消息后继续下一个 `Handler`。 可用于用户输入新信息。\n\n  可以在 `Handler` 中通过 [Matcher.pause](matcher.md#Matcher-pause) 抛出。\n\n- **参数**\n\n  auto\n\n- **用法**\n\n  ```python\n  @matcher.handle()\n  async def handler():\n      await matcher.pause(\"some message\")\n  ```\n\n## _class_ `RejectedException(<auto>)` {#RejectedException}\n\n- **说明**\n\n  指示 NoneBot 结束当前 `Handler` 并等待下一条消息后重新运行当前 `Handler`。 可用于用户重新输入。\n\n  可以在 `Handler` 中通过 [Matcher.reject](matcher.md#Matcher-reject) 抛出。\n\n- **参数**\n\n  auto\n\n- **用法**\n\n  ```python\n  @matcher.handle()\n  async def handler():\n      await matcher.reject(\"some message\")\n  ```\n\n## _class_ `FinishedException(<auto>)` {#FinishedException}\n\n- **说明**\n\n  指示 NoneBot 结束当前 `Handler` 且后续 `Handler` 不再被运行。可用于结束用户会话。\n\n  可以在 `Handler` 中通过 [Matcher.finish](matcher.md#Matcher-finish) 抛出。\n\n- **参数**\n\n  auto\n\n- **用法**\n\n  ```python\n  @matcher.handle()\n  async def handler():\n      await matcher.finish(\"some message\")\n  ```\n\n## _class_ `AdapterException(<auto>)` {#AdapterException}\n\n- **说明:** 代表 `Adapter` 抛出的异常，所有的 `Adapter` 都要在内部继承自这个 `Exception`。\n\n- **参数**\n  - `adapter_name`: 标识 adapter\n\n## _class_ `NoLogException(<auto>)` {#NoLogException}\n\n- **说明**\n\n  指示 NoneBot 对当前 `Event` 进行处理但不显示 Log 信息。\n\n  可在 [Event.get_log_string](adapters/index.md#Event-get-log-string) 时抛出\n\n- **参数**\n\n  auto\n\n## _class_ `ApiNotAvailable(<auto>)` {#ApiNotAvailable}\n\n- **说明:** 在 API 连接不可用时抛出。\n\n- **参数**\n\n  auto\n\n## _class_ `NetworkError(<auto>)` {#NetworkError}\n\n- **说明:** 在网络出现问题时抛出， 如: API 请求地址不正确, API 请求无返回或返回状态非正常等。\n\n- **参数**\n\n  auto\n\n## _class_ `ActionFailed(<auto>)` {#ActionFailed}\n\n- **说明:** API 请求成功返回数据，但 API 操作失败。\n\n- **参数**\n\n  auto\n\n## _class_ `DriverException(<auto>)` {#DriverException}\n\n- **说明:** `Driver` 抛出的异常基类。\n\n- **参数**\n\n  auto\n\n## _class_ `WebSocketClosed(<auto>)` {#WebSocketClosed}\n\n- **说明:** WebSocket 连接已关闭。\n\n- **参数**\n\n  auto\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/api/index.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 0\ndescription: nonebot 模块\n---\n\n# nonebot\n\n本模块主要定义了 NoneBot 启动所需函数，供 bot 入口文件调用。\n\n## 快捷导入\n\n为方便使用，本模块从子模块导入了部分内容，以下内容可以直接通过本模块导入:\n\n- `on` => [`on`](plugin/on.md#on)\n- `on_metaevent` => [`on_metaevent`](plugin/on.md#on-metaevent)\n- `on_message` => [`on_message`](plugin/on.md#on-message)\n- `on_notice` => [`on_notice`](plugin/on.md#on-notice)\n- `on_request` => [`on_request`](plugin/on.md#on-request)\n- `on_startswith` => [`on_startswith`](plugin/on.md#on-startswith)\n- `on_endswith` => [`on_endswith`](plugin/on.md#on-endswith)\n- `on_fullmatch` => [`on_fullmatch`](plugin/on.md#on-fullmatch)\n- `on_keyword` => [`on_keyword`](plugin/on.md#on-keyword)\n- `on_command` => [`on_command`](plugin/on.md#on-command)\n- `on_shell_command` => [`on_shell_command`](plugin/on.md#on-shell-command)\n- `on_regex` => [`on_regex`](plugin/on.md#on-regex)\n- `on_type` => [`on_type`](plugin/on.md#on-type)\n- `CommandGroup` => [`CommandGroup`](plugin/on.md#CommandGroup)\n- `Matchergroup` => [`MatcherGroup`](plugin/on.md#MatcherGroup)\n- `load_plugin` => [`load_plugin`](plugin/load.md#load-plugin)\n- `load_plugins` => [`load_plugins`](plugin/load.md#load-plugins)\n- `load_all_plugins` => [`load_all_plugins`](plugin/load.md#load-all-plugins)\n- `load_from_json` => [`load_from_json`](plugin/load.md#load-from-json)\n- `load_from_toml` => [`load_from_toml`](plugin/load.md#load-from-toml)\n- `load_builtin_plugin` =>\n  [`load_builtin_plugin`](plugin/load.md#load-builtin-plugin)\n- `load_builtin_plugins` =>\n  [`load_builtin_plugins`](plugin/load.md#load-builtin-plugins)\n- `get_plugin` => [`get_plugin`](plugin/index.md#get-plugin)\n- `get_plugin_by_module_name` =>\n  [`get_plugin_by_module_name`](plugin/index.md#get-plugin-by-module-name)\n- `get_loaded_plugins` =>\n  [`get_loaded_plugins`](plugin/index.md#get-loaded-plugins)\n- `get_available_plugin_names` =>\n  [`get_available_plugin_names`](plugin/index.md#get-available-plugin-names)\n- `get_plugin_config` => [`get_plugin_config`](plugin/index.md#get-plugin-config)\n- `require` => [`require`](plugin/load.md#require)\n\n## _def_ `get_driver()` {#get-driver}\n\n- **说明**\n\n  获取全局 [Driver](drivers/index.md#Driver) 实例。\n\n  可用于在计划任务的回调等情形中获取当前 [Driver](drivers/index.md#Driver) 实例。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - [Driver](drivers/index.md#Driver): 全局 [Driver](drivers/index.md#Driver) 对象\n\n- **异常**\n  - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用)\n\n- **用法**\n\n  ```python\n  driver = nonebot.get_driver()\n  ```\n\n## _def_ `get_adapter(name)` {#get-adapter}\n\n- **说明:** 获取已注册的 [Adapter](adapters/index.md#Adapter) 实例。\n\n- **重载**\n\n  **1.** `(name) -> Adapter`\n  - **参数**\n    - `name` (str): 适配器名称\n\n  - **返回**\n    - [Adapter](adapters/index.md#Adapter): 指定名称的 [Adapter](adapters/index.md#Adapter) 对象\n\n  **2.** `(name) -> A`\n  - **参数**\n    - `name` (type[A]): 适配器类型\n\n  - **返回**\n    - A: 指定类型的 [Adapter](adapters/index.md#Adapter) 对象\n\n- **异常**\n  - ValueError: 指定的 [Adapter](adapters/index.md#Adapter) 未注册\n\n  - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用)\n\n- **用法**\n\n  ```python\n  from nonebot.adapters.console import Adapter\n  adapter = nonebot.get_adapter(Adapter)\n  ```\n\n## _def_ `get_adapters()` {#get-adapters}\n\n- **说明:** 获取所有已注册的 [Adapter](adapters/index.md#Adapter) 实例。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - dict[str, [Adapter](adapters/index.md#Adapter)]: 所有 [Adapter](adapters/index.md#Adapter) 实例字典\n\n- **异常**\n  - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用)\n\n- **用法**\n\n  ```python\n  adapters = nonebot.get_adapters()\n  ```\n\n## _def_ `get_app()` {#get-app}\n\n- **说明:** 获取全局 [ASGIMixin](drivers/index.md#ASGIMixin) 对应的 Server App 对象。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - Any: Server App 对象\n\n- **异常**\n  - AssertionError: 全局 Driver 对象不是 [ASGIMixin](drivers/index.md#ASGIMixin) 类型\n\n  - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用)\n\n- **用法**\n\n  ```python\n  app = nonebot.get_app()\n  ```\n\n## _def_ `get_asgi()` {#get-asgi}\n\n- **说明:** 获取全局 [ASGIMixin](drivers/index.md#ASGIMixin) 对应的 [ASGI](https://asgi.readthedocs.io/) 对象。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - Any: ASGI 对象\n\n- **异常**\n  - AssertionError: 全局 Driver 对象不是 [ASGIMixin](drivers/index.md#ASGIMixin) 类型\n\n  - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用)\n\n- **用法**\n\n  ```python\n  asgi = nonebot.get_asgi()\n  ```\n\n## _def_ `get_bot(self_id=None)` {#get-bot}\n\n- **说明**\n\n  获取一个连接到 NoneBot 的 [Bot](adapters/index.md#Bot) 对象。\n\n  当提供 `self_id` 时，此函数是 `get_bots()[self_id]` 的简写；\n  当不提供时，返回一个 [Bot](adapters/index.md#Bot)。\n\n- **参数**\n  - `self_id` (str | None): 用来识别 [Bot](adapters/index.md#Bot) 的 [Bot.self_id](adapters/index.md#Bot-self-id) 属性\n\n- **返回**\n  - [Bot](adapters/index.md#Bot): [Bot](adapters/index.md#Bot) 对象\n\n- **异常**\n  - KeyError: 对应 self_id 的 Bot 不存在\n\n  - ValueError: 没有传入 self_id 且没有 Bot 可用\n\n  - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用)\n\n- **用法**\n\n  ```python\n  assert nonebot.get_bot(\"12345\") == nonebot.get_bots()[\"12345\"]\n\n  another_unspecified_bot = nonebot.get_bot()\n  ```\n\n## _def_ `get_bots()` {#get-bots}\n\n- **说明:** 获取所有连接到 NoneBot 的 [Bot](adapters/index.md#Bot) 对象。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - dict[str, [Bot](adapters/index.md#Bot)]: 一个以 [Bot.self_id](adapters/index.md#Bot-self-id) 为键\n\n    [Bot](adapters/index.md#Bot) 对象为值的字典\n\n- **异常**\n  - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用)\n\n- **用法**\n\n  ```python\n  bots = nonebot.get_bots()\n  ```\n\n## _def_ `init(*, _env_file=None, **kwargs)` {#init}\n\n- **说明**\n\n  初始化 NoneBot 以及 全局 [Driver](drivers/index.md#Driver) 对象。\n\n  NoneBot 将会从 .env 文件中读取环境信息，并使用相应的 env 文件配置。\n\n  也可以传入自定义的 `_env_file` 来指定 NoneBot 从该文件读取配置。\n\n- **参数**\n  - `_env_file` (DOTENV_TYPE | None): 配置文件名，默认从 `.env.{env_name}` 中读取配置\n\n  - `**kwargs` (Any): 任意变量，将会存储到 [Driver.config](drivers/index.md#Driver-config) 对象里\n\n- **返回**\n  - None\n\n- **用法**\n\n  ```python\n  nonebot.init(database=Database(...))\n  ```\n\n## _def_ `run(*args, **kwargs)` {#run}\n\n- **说明:** 启动 NoneBot，即运行全局 [Driver](drivers/index.md#Driver) 对象。\n\n- **参数**\n  - `*args` (Any): 传入 [Driver.run](drivers/index.md#Driver-run) 的位置参数\n\n  - `**kwargs` (Any): 传入 [Driver.run](drivers/index.md#Driver-run) 的命名参数\n\n- **返回**\n  - None\n\n- **用法**\n\n  ```python\n  nonebot.run(host=\"127.0.0.1\", port=8080)\n  ```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/api/log.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 7\ndescription: nonebot.log 模块\n---\n\n# nonebot.log\n\n本模块定义了 NoneBot 的日志记录 Logger。\n\nNoneBot 使用 [`loguru`][loguru] 来记录日志信息。\n\n自定义 logger 请参考 [自定义日志](https://nonebot.dev/docs/appendices/log)\n以及 [`loguru`][loguru] 文档。\n\n[loguru]: https://github.com/Delgan/loguru\n\n## _var_ `logger` {#logger}\n\n- **类型:** Logger\n\n- **说明**\n\n  NoneBot 日志记录器对象。\n\n  默认信息:\n  - 格式: `[%(asctime)s %(name)s] %(levelname)s: %(message)s`\n  - 等级: `INFO` ，根据 `config.log_level` 配置改变\n  - 输出: 输出至 stdout\n\n- **用法**\n\n  ```python\n  from nonebot.log import logger\n  ```\n\n## _class_ `LoguruHandler(<auto>)` {#LoguruHandler}\n\n- **说明:** logging 与 loguru 之间的桥梁，将 logging 的日志转发到 loguru。\n\n- **参数**\n\n  auto\n\n### _method_ `emit(record)` {#LoguruHandler-emit}\n\n- **参数**\n  - `record` (logging.LogRecord)\n\n- **返回**\n  - untyped\n\n## _def_ `default_filter(record)` {#default-filter}\n\n- **说明:** 默认的日志过滤器，根据 `config.log_level` 配置改变日志等级。\n\n- **参数**\n  - `record` (Record)\n\n- **返回**\n  - untyped\n\n## _var_ `default_format` {#default-format}\n\n- **类型:** str\n\n- **说明:** 默认日志格式\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/api/matcher.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 3\ndescription: nonebot.matcher 模块\n---\n\n# nonebot.matcher\n\n本模块实现事件响应器的创建与运行，并提供一些快捷方法来帮助用户更好的与机器人进行对话。\n\n## _var_ `DEFAULT_PROVIDER_CLASS` {#DEFAULT-PROVIDER-CLASS}\n\n- **类型:** untyped\n\n- **说明:** 默认存储器类型\n\n## _class_ `Matcher()` {#Matcher}\n\n- **说明:** 事件响应器类\n\n- **参数**\n\n  empty\n\n### _class-var_ `type` {#Matcher-type}\n\n- **类型:** ClassVar[str]\n\n- **说明:** 事件响应器类型\n\n### _class-var_ `rule` {#Matcher-rule}\n\n- **类型:** ClassVar[[Rule](rule.md#Rule)]\n\n- **说明:** 事件响应器匹配规则\n\n### _class-var_ `permission` {#Matcher-permission}\n\n- **类型:** ClassVar[[Permission](permission.md#Permission)]\n\n- **说明:** 事件响应器触发权限\n\n### _class-var_ `handlers` {#Matcher-handlers}\n\n- **类型:** ClassVar[list[[Dependent](dependencies/index.md#Dependent)[Any]]]\n\n- **说明:** 事件响应器拥有的事件处理函数列表\n\n### _class-var_ `priority` {#Matcher-priority}\n\n- **类型:** ClassVar[int]\n\n- **说明:** 事件响应器优先级\n\n### _class-var_ `block` {#Matcher-block}\n\n- **类型:** bool\n\n- **说明:** 事件响应器是否阻止事件传播\n\n### _class-var_ `temp` {#Matcher-temp}\n\n- **类型:** ClassVar[bool]\n\n- **说明:** 事件响应器是否为临时\n\n### _class-var_ `expire_time` {#Matcher-expire-time}\n\n- **类型:** ClassVar[datetime | None]\n\n- **说明:** 事件响应器过期时间点\n\n### _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}\n\n- **说明:** 创建一个新的事件响应器，并存储至 `matchers <#matchers>`\\_\n\n- **参数**\n  - `type_` (str): 事件响应器类型，与 `event.get_type()` 一致时触发，空字符串表示任意\n\n  - `rule` ([Rule](rule.md#Rule) | None): 匹配规则\n\n  - `permission` ([Permission](permission.md#Permission) | None): 权限\n\n  - `handlers` (list[[T\\_Handler](typing.md#T-Handler) | [Dependent](dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器，即触发一次后删除\n\n  - `priority` (int): 响应优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级的响应器传播\n\n  - `plugin` ([Plugin](plugin/model.md#Plugin) | None): **Deprecated.** 事件响应器所在插件\n\n  - `module` (ModuleType | None): **Deprecated.** 事件响应器所在模块\n\n  - `source` (MatcherSource | None): 事件响应器源代码上下文信息\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `default_state` ([T_State](typing.md#T-State) | None): 默认状态 `state`\n\n  - `default_type_updater` ([T_TypeUpdater](typing.md#T-TypeUpdater) | [Dependent](dependencies/index.md#Dependent)[str] | None): 默认事件类型更新函数\n\n  - `default_permission_updater` ([T_PermissionUpdater](typing.md#T-PermissionUpdater) | [Dependent](dependencies/index.md#Dependent)[[Permission](permission.md#Permission)] | None): 默认会话权限更新函数\n\n- **返回**\n  - type[Matcher]: 新的事件响应器类\n\n### _classmethod_ `destroy()` {#Matcher-destroy}\n\n- **说明:** 销毁当前的事件响应器\n\n- **参数**\n\n  empty\n\n- **返回**\n  - None\n\n### _classmethod_ `check_perm(bot, event, stack=None, dependency_cache=None)` {#Matcher-check-perm}\n\n- **说明:** 检查是否满足触发权限\n\n- **参数**\n  - `bot` ([Bot](adapters/index.md#Bot)): Bot 对象\n\n  - `event` ([Event](adapters/index.md#Event)): 上报事件\n\n  - `stack` (AsyncExitStack | None): 异步上下文栈\n\n  - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None): 依赖缓存\n\n- **返回**\n  - bool: 是否满足权限\n\n### _classmethod_ `check_rule(bot, event, state, stack=None, dependency_cache=None)` {#Matcher-check-rule}\n\n- **说明:** 检查是否满足匹配规则\n\n- **参数**\n  - `bot` ([Bot](adapters/index.md#Bot)): Bot 对象\n\n  - `event` ([Event](adapters/index.md#Event)): 上报事件\n\n  - `state` ([T_State](typing.md#T-State)): 当前状态\n\n  - `stack` (AsyncExitStack | None): 异步上下文栈\n\n  - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None): 依赖缓存\n\n- **返回**\n  - bool: 是否满足匹配规则\n\n### _classmethod_ `type_updater(func)` {#Matcher-type-updater}\n\n- **说明:** 装饰一个函数来更改当前事件响应器的默认响应事件类型更新函数\n\n- **参数**\n  - `func` ([T_TypeUpdater](typing.md#T-TypeUpdater)): 响应事件类型更新函数\n\n- **返回**\n  - [T_TypeUpdater](typing.md#T-TypeUpdater)\n\n### _classmethod_ `permission_updater(func)` {#Matcher-permission-updater}\n\n- **说明:** 装饰一个函数来更改当前事件响应器的默认会话权限更新函数\n\n- **参数**\n  - `func` ([T_PermissionUpdater](typing.md#T-PermissionUpdater)): 会话权限更新函数\n\n- **返回**\n  - [T_PermissionUpdater](typing.md#T-PermissionUpdater)\n\n### _classmethod_ `append_handler(handler, parameterless=None)` {#Matcher-append-handler}\n\n- **参数**\n  - `handler` ([T_Handler](typing.md#T-Handler))\n\n  - `parameterless` (Iterable[Any] | None)\n\n- **返回**\n  - [Dependent](dependencies/index.md#Dependent)[Any]\n\n### _classmethod_ `handle(parameterless=None)` {#Matcher-handle}\n\n- **说明:** 装饰一个函数来向事件响应器直接添加一个处理函数\n\n- **参数**\n  - `parameterless` (Iterable[Any] | None): 非参数类型依赖列表\n\n- **返回**\n  - ([T_Handler](typing.md#T-Handler)) -> [T_Handler](typing.md#T-Handler)\n\n### _classmethod_ `receive(id=\"\", parameterless=None)` {#Matcher-receive}\n\n- **说明:** 装饰一个函数来指示 NoneBot 在接收用户新的一条消息后继续运行该函数\n\n- **参数**\n  - `id` (str): 消息 ID\n\n  - `parameterless` (Iterable[Any] | None): 非参数类型依赖列表\n\n- **返回**\n  - ([T_Handler](typing.md#T-Handler)) -> [T_Handler](typing.md#T-Handler)\n\n### _classmethod_ `got(key, prompt=None, parameterless=None)` {#Matcher-got}\n\n- **说明**\n\n  装饰一个函数来指示 NoneBot 获取一个参数 `key`\n\n  当要获取的 `key` 不存在时接收用户新的一条消息再运行该函数，\n  如果 `key` 已存在则直接继续运行\n\n- **参数**\n  - `key` (str): 参数名\n\n  - `prompt` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate) | None): 在参数不存在时向用户发送的消息\n\n  - `parameterless` (Iterable[Any] | None): 非参数类型依赖列表\n\n- **返回**\n  - ([T_Handler](typing.md#T-Handler)) -> [T_Handler](typing.md#T-Handler)\n\n### _classmethod_ `send(message, **kwargs)` {#Matcher-send}\n\n- **说明:** 发送一条消息给当前交互用户\n\n- **参数**\n  - `message` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate)): 消息内容\n\n  - `**kwargs` (Any): [Bot.send](adapters/index.md#Bot-send) 的参数， 请参考对应 adapter 的 bot 对象 api\n\n- **返回**\n  - Any\n\n### _classmethod_ `finish(message=None, **kwargs)` {#Matcher-finish}\n\n- **说明:** 发送一条消息给当前交互用户并结束当前事件响应器\n\n- **参数**\n  - `message` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate) | None): 消息内容\n\n  - `**kwargs`: [Bot.send](adapters/index.md#Bot-send) 的参数， 请参考对应 adapter 的 bot 对象 api\n\n- **返回**\n  - NoReturn\n\n### _classmethod_ `pause(prompt=None, **kwargs)` {#Matcher-pause}\n\n- **说明:** 发送一条消息给当前交互用户并暂停事件响应器，在接收用户新的一条消息后继续下一个处理函数\n\n- **参数**\n  - `prompt` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate) | None): 消息内容\n\n  - `**kwargs`: [Bot.send](adapters/index.md#Bot-send) 的参数， 请参考对应 adapter 的 bot 对象 api\n\n- **返回**\n  - NoReturn\n\n### _classmethod_ `reject(prompt=None, **kwargs)` {#Matcher-reject}\n\n- **说明:** 最近使用 `got` / `receive` 接收的消息不符合预期， 发送一条消息给当前交互用户并将当前事件处理流程中断在当前位置，在接收用户新的一个事件后从头开始执行当前处理函数\n\n- **参数**\n  - `prompt` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate) | None): 消息内容\n\n  - `**kwargs`: [Bot.send](adapters/index.md#Bot-send) 的参数， 请参考对应 adapter 的 bot 对象 api\n\n- **返回**\n  - NoReturn\n\n### _classmethod_ `reject_arg(key, prompt=None, **kwargs)` {#Matcher-reject-arg}\n\n- **说明:** 最近使用 `got` 接收的消息不符合预期， 发送一条消息给当前交互用户并将当前事件处理流程中断在当前位置，在接收用户新的一条消息后从头开始执行当前处理函数\n\n- **参数**\n  - `key` (str): 参数名\n\n  - `prompt` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate) | None): 消息内容\n\n  - `**kwargs`: [Bot.send](adapters/index.md#Bot-send) 的参数， 请参考对应 adapter 的 bot 对象 api\n\n- **返回**\n  - NoReturn\n\n### _classmethod_ `reject_receive(id=\"\", prompt=None, **kwargs)` {#Matcher-reject-receive}\n\n- **说明:** 最近使用 `receive` 接收的消息不符合预期， 发送一条消息给当前交互用户并将当前事件处理流程中断在当前位置，在接收用户新的一个事件后从头开始执行当前处理函数\n\n- **参数**\n  - `id` (str): 消息 id\n\n  - `prompt` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate) | None): 消息内容\n\n  - `**kwargs`: [Bot.send](adapters/index.md#Bot-send) 的参数， 请参考对应 adapter 的 bot 对象 api\n\n- **返回**\n  - NoReturn\n\n### _classmethod_ `skip()` {#Matcher-skip}\n\n- **说明**\n\n  跳过当前事件处理函数，继续下一个处理函数\n\n  通常在事件处理函数的依赖中使用。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - NoReturn\n\n### _method_ `get_receive(id, default=None)` {#Matcher-get-receive}\n\n- **说明**\n\n  获取一个 `receive` 事件\n\n  如果没有找到对应的事件，返回 `default` 值\n\n- **重载**\n\n  **1.** `(id) -> Event | None`\n  - **参数**\n    - `id` (str)\n\n  - **返回**\n    - [Event](adapters/index.md#Event) | None\n\n  **2.** `(id, default) -> Event | T`\n  - **参数**\n    - `id` (str)\n\n    - `default` (T)\n\n  - **返回**\n    - [Event](adapters/index.md#Event) | T\n\n### _method_ `set_receive(id, event)` {#Matcher-set-receive}\n\n- **说明:** 设置一个 `receive` 事件\n\n- **参数**\n  - `id` (str)\n\n  - `event` ([Event](adapters/index.md#Event))\n\n- **返回**\n  - None\n\n### _method_ `get_last_receive(default=None)` {#Matcher-get-last-receive}\n\n- **说明**\n\n  获取最近一次 `receive` 事件\n\n  如果没有事件，返回 `default` 值\n\n- **重载**\n\n  **1.** `() -> Event | None`\n  - **参数**\n\n    empty\n\n  - **返回**\n    - [Event](adapters/index.md#Event) | None\n\n  **2.** `(default) -> Event | T`\n  - **参数**\n    - `default` (T)\n\n  - **返回**\n    - [Event](adapters/index.md#Event) | T\n\n### _method_ `get_arg(key, default=None)` {#Matcher-get-arg}\n\n- **说明**\n\n  获取一个 `got` 消息\n\n  如果没有找到对应的消息，返回 `default` 值\n\n- **重载**\n\n  **1.** `(key) -> Message | None`\n  - **参数**\n    - `key` (str)\n\n  - **返回**\n    - [Message](adapters/index.md#Message) | None\n\n  **2.** `(key, default) -> Message | T`\n  - **参数**\n    - `key` (str)\n\n    - `default` (T)\n\n  - **返回**\n    - [Message](adapters/index.md#Message) | T\n\n### _method_ `set_arg(key, message)` {#Matcher-set-arg}\n\n- **说明:** 设置一个 `got` 消息\n\n- **参数**\n  - `key` (str)\n\n  - `message` ([Message](adapters/index.md#Message))\n\n- **返回**\n  - None\n\n### _method_ `set_target(target, cache=True)` {#Matcher-set-target}\n\n- **参数**\n  - `target` (str)\n\n  - `cache` (bool)\n\n- **返回**\n  - None\n\n### _method_ `get_target(default=None)` {#Matcher-get-target}\n\n- **重载**\n\n  **1.** `() -> str | None`\n  - **参数**\n\n    empty\n\n  - **返回**\n    - str | None\n\n  **2.** `(default) -> str | T`\n  - **参数**\n    - `default` (T)\n\n  - **返回**\n    - str | T\n\n### _method_ `stop_propagation()` {#Matcher-stop-propagation}\n\n- **说明:** 阻止事件传播\n\n- **参数**\n\n  empty\n\n- **返回**\n  - untyped\n\n### _async method_ `update_type(bot, event, stack=None, dependency_cache=None)` {#Matcher-update-type}\n\n- **参数**\n  - `bot` ([Bot](adapters/index.md#Bot))\n\n  - `event` ([Event](adapters/index.md#Event))\n\n  - `stack` (AsyncExitStack | None)\n\n  - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None)\n\n- **返回**\n  - str\n\n### _async method_ `update_permission(bot, event, stack=None, dependency_cache=None)` {#Matcher-update-permission}\n\n- **参数**\n  - `bot` ([Bot](adapters/index.md#Bot))\n\n  - `event` ([Event](adapters/index.md#Event))\n\n  - `stack` (AsyncExitStack | None)\n\n  - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None)\n\n- **返回**\n  - [Permission](permission.md#Permission)\n\n### _async method_ `resolve_reject()` {#Matcher-resolve-reject}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - untyped\n\n### _method_ `ensure_context(bot, event)` {#Matcher-ensure-context}\n\n- **参数**\n  - `bot` ([Bot](adapters/index.md#Bot))\n\n  - `event` ([Event](adapters/index.md#Event))\n\n- **返回**\n  - untyped\n\n### _async method_ `simple_run(bot, event, state, stack=None, dependency_cache=None)` {#Matcher-simple-run}\n\n- **参数**\n  - `bot` ([Bot](adapters/index.md#Bot))\n\n  - `event` ([Event](adapters/index.md#Event))\n\n  - `state` ([T_State](typing.md#T-State))\n\n  - `stack` (AsyncExitStack | None)\n\n  - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None)\n\n- **返回**\n  - untyped\n\n### _async method_ `run(bot, event, state, stack=None, dependency_cache=None)` {#Matcher-run}\n\n- **参数**\n  - `bot` ([Bot](adapters/index.md#Bot))\n\n  - `event` ([Event](adapters/index.md#Event))\n\n  - `state` ([T_State](typing.md#T-State))\n\n  - `stack` (AsyncExitStack | None)\n\n  - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None)\n\n- **返回**\n  - untyped\n\n## _class_ `MatcherManager()` {#MatcherManager}\n\n- **说明**\n\n  事件响应器管理器\n\n  实现了常用字典操作，用于管理事件响应器。\n\n- **参数**\n\n  empty\n\n### _method_ `keys()` {#MatcherManager-keys}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - KeysView[int]\n\n### _method_ `values()` {#MatcherManager-values}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - ValuesView[list[type[[Matcher](#Matcher)]]]\n\n### _method_ `items()` {#MatcherManager-items}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - ItemsView[int, list[type[[Matcher](#Matcher)]]]\n\n### _method_ `get(key, default=None)` {#MatcherManager-get}\n\n- **重载**\n\n  **1.** `(key) -> list[type[Matcher]] | None`\n  - **参数**\n    - `key` (int)\n\n  - **返回**\n    - list[type[[Matcher](#Matcher)]] | None\n\n  **2.** `(key, default) -> list[type[Matcher]] | T`\n  - **参数**\n    - `key` (int)\n\n    - `default` (T)\n\n  - **返回**\n    - list[type[[Matcher](#Matcher)]] | T\n\n### _method_ `pop(key)` {#MatcherManager-pop}\n\n- **参数**\n  - `key` (int)\n\n- **返回**\n  - list[type[[Matcher](#Matcher)]]\n\n### _method_ `popitem()` {#MatcherManager-popitem}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - tuple[int, list[type[[Matcher](#Matcher)]]]\n\n### _method_ `clear()` {#MatcherManager-clear}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - None\n\n### _method_ `update(m, /)` {#MatcherManager-update}\n\n- **参数**\n  - `m` (MutableMapping[int, list[type[[Matcher](#Matcher)]]])\n\n- **返回**\n  - None\n\n### _method_ `setdefault(key, default)` {#MatcherManager-setdefault}\n\n- **参数**\n  - `key` (int)\n\n  - `default` (list[type[[Matcher](#Matcher)]])\n\n- **返回**\n  - list[type[[Matcher](#Matcher)]]\n\n### _method_ `set_provider(provider_class)` {#MatcherManager-set-provider}\n\n- **说明:** 设置事件响应器存储器\n\n- **参数**\n  - `provider_class` (type[[MatcherProvider](#MatcherProvider)]): 事件响应器存储器类\n\n- **返回**\n  - None\n\n## _abstract class_ `MatcherProvider(matchers)` {#MatcherProvider}\n\n- **说明:** 事件响应器存储器基类\n\n- **参数**\n  - `matchers` (Mapping[int, list[type[[Matcher](#Matcher)]]]): 当前存储器中已有的事件响应器\n\n## _var_ `matchers` {#matchers}\n\n- **类型:** untyped\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/api/message.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 2\ndescription: nonebot.message 模块\n---\n\n# nonebot.message\n\n本模块定义了事件处理主要流程。\n\nNoneBot 内部处理并按优先级分发事件给所有事件响应器，提供了多个插槽以进行事件的预处理等。\n\n## _def_ `event_preprocessor(func)` {#event-preprocessor}\n\n- **说明**\n\n  事件预处理。\n\n  装饰一个函数，使它在每次接收到事件并分发给各响应器之前执行。\n\n- **参数**\n  - `func` ([T_EventPreProcessor](typing.md#T-EventPreProcessor))\n\n- **返回**\n  - [T_EventPreProcessor](typing.md#T-EventPreProcessor)\n\n## _def_ `event_postprocessor(func)` {#event-postprocessor}\n\n- **说明**\n\n  事件后处理。\n\n  装饰一个函数，使它在每次接收到事件并分发给各响应器之后执行。\n\n- **参数**\n  - `func` ([T_EventPostProcessor](typing.md#T-EventPostProcessor))\n\n- **返回**\n  - [T_EventPostProcessor](typing.md#T-EventPostProcessor)\n\n## _def_ `run_preprocessor(func)` {#run-preprocessor}\n\n- **说明**\n\n  运行预处理。\n\n  装饰一个函数，使它在每次事件响应器运行前执行。\n\n- **参数**\n  - `func` ([T_RunPreProcessor](typing.md#T-RunPreProcessor))\n\n- **返回**\n  - [T_RunPreProcessor](typing.md#T-RunPreProcessor)\n\n## _def_ `run_postprocessor(func)` {#run-postprocessor}\n\n- **说明**\n\n  运行后处理。\n\n  装饰一个函数，使它在每次事件响应器运行后执行。\n\n- **参数**\n  - `func` ([T_RunPostProcessor](typing.md#T-RunPostProcessor))\n\n- **返回**\n  - [T_RunPostProcessor](typing.md#T-RunPostProcessor)\n\n## _async def_ `check_and_run_matcher(Matcher, bot, event, state, stack=None, dependency_cache=None)` {#check-and-run-matcher}\n\n- **说明:** 检查并运行事件响应器。\n\n- **参数**\n  - `Matcher` (type[[Matcher](matcher.md#Matcher)]): 事件响应器\n\n  - `bot` ([Bot](adapters/index.md#Bot)): Bot 对象\n\n  - `event` ([Event](adapters/index.md#Event)): Event 对象\n\n  - `state` ([T_State](typing.md#T-State)): 会话状态\n\n  - `stack` (AsyncExitStack | None): 异步上下文栈\n\n  - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None): 依赖缓存\n\n- **返回**\n  - None\n\n## _async def_ `handle_event(bot, event)` {#handle-event}\n\n- **说明:** 处理一个事件。调用该函数以实现分发事件。\n\n- **参数**\n  - `bot` ([Bot](adapters/index.md#Bot)): Bot 对象\n\n  - `event` ([Event](adapters/index.md#Event)): Event 对象\n\n- **返回**\n  - None\n\n- **用法**\n\n  ```python\n  driver.task_group.start_soon(handle_event, bot, event)\n  ```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/api/params.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 4\ndescription: nonebot.params 模块\n---\n\n# nonebot.params\n\n本模块定义了依赖注入的各类参数。\n\n## _def_ `Arg(key=None)` {#Arg}\n\n- **说明:** Arg 参数消息\n\n- **参数**\n  - `key` (str | None)\n\n- **返回**\n  - Any\n\n## _class_ `ArgParam(*args, key, type, **kwargs)` {#ArgParam}\n\n- **说明**\n\n  Arg 注入参数\n\n  本注入解析事件响应器操作 `got` 所获取的参数。\n\n  可以通过 `Arg`、`ArgStr`、`ArgPlainText` 等函数参数 `key` 指定获取的参数，\n  留空则会根据参数名称获取。\n\n- **参数**\n  - `*args`\n\n  - `key` (str)\n\n  - `type` (Literal['message', 'str', 'plaintext', 'prompt'])\n\n  - `**kwargs` (Any)\n\n## _def_ `ArgPlainText(key=None)` {#ArgPlainText}\n\n- **说明:** Arg 参数消息纯文本\n\n- **参数**\n  - `key` (str | None)\n\n- **返回**\n  - str\n\n## _def_ `ArgPromptResult(key=None)` {#ArgPromptResult}\n\n- **说明:** `arg` prompt 发送结果\n\n- **参数**\n  - `key` (str | None)\n\n- **返回**\n  - Any\n\n## _def_ `ArgStr(key=None)` {#ArgStr}\n\n- **说明:** Arg 参数消息文本\n\n- **参数**\n  - `key` (str | None)\n\n- **返回**\n  - str\n\n## _class_ `BotParam(*args, checker=None, **kwargs)` {#BotParam}\n\n- **说明**\n\n  注入参数。\n\n  本注入解析所有类型为且仅为 [Bot](adapters/index.md#Bot) 及其子类或 `None` 的参数。\n\n  为保证兼容性，本注入还会解析名为 `bot` 且没有类型注解的参数。\n\n- **参数**\n  - `*args`\n\n  - `checker` ([ModelField](compat.md#ModelField) | None)\n\n  - `**kwargs` (Any)\n\n## _class_ `DefaultParam(*args, validate=False, **kwargs)` {#DefaultParam}\n\n- **说明**\n\n  默认值注入参数\n\n  本注入解析所有剩余未能解析且具有默认值的参数。\n\n  本注入参数应该具有最低优先级，因此应该在所有其他注入参数之后使用。\n\n- **参数**\n  - `*args`\n\n  - `validate` (bool)\n\n  - `**kwargs` (Any)\n\n## _class_ `DependParam(*args, dependent, use_cache, **kwargs)` {#DependParam}\n\n- **说明**\n\n  子依赖注入参数。\n\n  本注入解析所有子依赖注入，然后将它们的返回值作为参数值传递给父依赖。\n\n  本注入应该具有最高优先级，因此应该在其他参数之前检查。\n\n- **参数**\n  - `*args`\n\n  - `dependent` ([Dependent](dependencies/index.md#Dependent)[Any])\n\n  - `use_cache` (bool)\n\n  - `**kwargs` (Any)\n\n## _def_ `Depends(dependency=None, *, use_cache=True, validate=False)` {#Depends}\n\n- **说明:** 子依赖装饰器\n\n- **参数**\n  - `dependency` ([T_Handler](typing.md#T-Handler) | None): 依赖函数。默认为参数的类型注释。\n\n  - `use_cache` (bool): 是否使用缓存。默认为 `True`。\n\n  - `validate` (bool | PydanticFieldInfo): 是否使用 Pydantic 类型校验。默认为 `False`。\n\n- **返回**\n  - Any\n\n- **用法**\n\n  ```python\n  def depend_func() -> Any:\n      return ...\n\n  def depend_gen_func():\n      try:\n          yield ...\n      finally:\n          ...\n\n  async def handler(\n      param_name: Any = Depends(depend_func),\n      gen: Any = Depends(depend_gen_func),\n  ):\n      ...\n  ```\n\n## _class_ `EventParam(*args, checker=None, **kwargs)` {#EventParam}\n\n- **说明**\n\n  注入参数\n\n  本注入解析所有类型为且仅为 [Event](adapters/index.md#Event) 及其子类或 `None` 的参数。\n\n  为保证兼容性，本注入还会解析名为 `event` 且没有类型注解的参数。\n\n- **参数**\n  - `*args`\n\n  - `checker` ([ModelField](compat.md#ModelField) | None)\n\n  - `**kwargs` (Any)\n\n## _class_ `ExceptionParam(*args, validate=False, **kwargs)` {#ExceptionParam}\n\n- **说明**\n\n  的异常注入参数\n\n  本注入解析所有类型为 `Exception` 或 `None` 的参数。\n\n  为保证兼容性，本注入还会解析名为 `exception` 且没有类型注解的参数。\n\n- **参数**\n  - `*args`\n\n  - `validate` (bool)\n\n  - `**kwargs` (Any)\n\n## _class_ `MatcherParam(*args, checker=None, **kwargs)` {#MatcherParam}\n\n- **说明**\n\n  事件响应器实例注入参数\n\n  本注入解析所有类型为且仅为 [Matcher](matcher.md#Matcher) 及其子类或 `None` 的参数。\n\n  为保证兼容性，本注入还会解析名为 `matcher` 且没有类型注解的参数。\n\n- **参数**\n  - `*args`\n\n  - `checker` ([ModelField](compat.md#ModelField) | None)\n\n  - `**kwargs` (Any)\n\n## _class_ `StateParam(*args, validate=False, **kwargs)` {#StateParam}\n\n- **说明**\n\n  事件处理状态注入参数\n\n  本注入解析所有类型为 `T_State` 的参数。\n\n  为保证兼容性，本注入还会解析名为 `state` 且没有类型注解的参数。\n\n- **参数**\n  - `*args`\n\n  - `validate` (bool)\n\n  - `**kwargs` (Any)\n\n## _def_ `EventType()` {#EventType}\n\n- **说明:** 类型参数\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n## _def_ `EventMessage()` {#EventMessage}\n\n- **说明:** 消息参数\n\n- **参数**\n\n  empty\n\n- **返回**\n  - Any\n\n## _def_ `EventPlainText()` {#EventPlainText}\n\n- **说明:** 纯文本消息参数\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n## _def_ `EventToMe()` {#EventToMe}\n\n- **说明:** `to_me` 参数\n\n- **参数**\n\n  empty\n\n- **返回**\n  - bool\n\n## _def_ `Command()` {#Command}\n\n- **说明:** 消息命令元组\n\n- **参数**\n\n  empty\n\n- **返回**\n  - tuple[str, ...]\n\n## _def_ `RawCommand()` {#RawCommand}\n\n- **说明:** 消息命令文本\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n## _def_ `CommandArg()` {#CommandArg}\n\n- **说明:** 消息命令参数\n\n- **参数**\n\n  empty\n\n- **返回**\n  - Any\n\n## _def_ `CommandStart()` {#CommandStart}\n\n- **说明:** 消息命令开头\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n## _def_ `CommandWhitespace()` {#CommandWhitespace}\n\n- **说明:** 消息命令与参数之间的空白\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n## _def_ `ShellCommandArgs()` {#ShellCommandArgs}\n\n- **说明:** shell 命令解析后的参数字典\n\n- **参数**\n\n  empty\n\n- **返回**\n  - Any\n\n## _def_ `ShellCommandArgv()` {#ShellCommandArgv}\n\n- **说明:** shell 命令原始参数列表\n\n- **参数**\n\n  empty\n\n- **返回**\n  - Any\n\n## _def_ `RegexMatched()` {#RegexMatched}\n\n- **说明:** 正则匹配结果\n\n- **参数**\n\n  empty\n\n- **返回**\n  - Match[str]\n\n## _def_ `RegexStr(*groups)` {#RegexStr}\n\n- **说明:** 正则匹配结果文本\n\n- **重载**\n\n  **1.** `(group, /) -> str`\n  - **参数**\n    - `group` (Literal[0])\n\n  - **返回**\n    - str\n\n  **2.** `(group, /) -> str | Any`\n  - **参数**\n    - `group` (str | int)\n\n  - **返回**\n    - str | Any\n\n  **3.** `(group1, group2, /, *groups) -> tuple[str | Any, ...]`\n  - **参数**\n    - `group1` (str | int)\n\n    - `group2` (str | int)\n\n    - `*groups` (str | int)\n\n  - **返回**\n    - tuple[str | Any, ...]\n\n## _def_ `RegexGroup()` {#RegexGroup}\n\n- **说明:** 正则匹配结果 group 元组\n\n- **参数**\n\n  empty\n\n- **返回**\n  - tuple[Any, ...]\n\n## _def_ `RegexDict()` {#RegexDict}\n\n- **说明:** 正则匹配结果 group 字典\n\n- **参数**\n\n  empty\n\n- **返回**\n  - dict[str, Any]\n\n## _def_ `Startswith()` {#Startswith}\n\n- **说明:** 响应触发前缀\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n## _def_ `Endswith()` {#Endswith}\n\n- **说明:** 响应触发后缀\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n## _def_ `Fullmatch()` {#Fullmatch}\n\n- **说明:** 响应触发完整消息\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n## _def_ `Keyword()` {#Keyword}\n\n- **说明:** 响应触发关键字\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n## _def_ `Received(id=None, default=None)` {#Received}\n\n- **说明:** `receive` 事件参数\n\n- **参数**\n  - `id` (str | None)\n\n  - `default` (Any)\n\n- **返回**\n  - Any\n\n## _def_ `LastReceived(default=None)` {#LastReceived}\n\n- **说明:** `last_receive` 事件参数\n\n- **参数**\n  - `default` (Any)\n\n- **返回**\n  - Any\n\n## _def_ `ReceivePromptResult(id=None)` {#ReceivePromptResult}\n\n- **说明:** `receive` prompt 发送结果\n\n- **参数**\n  - `id` (str | None)\n\n- **返回**\n  - Any\n\n## _def_ `PausePromptResult()` {#PausePromptResult}\n\n- **说明:** `pause` prompt 发送结果\n\n- **参数**\n\n  empty\n\n- **返回**\n  - Any\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/api/permission.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 6\ndescription: nonebot.permission 模块\n---\n\n# nonebot.permission\n\n本模块是 [Matcher.permission](matcher.md#Matcher-permission) 的类型定义。\n\n每个[事件响应器](matcher.md#Matcher)\n拥有一个 [Permission](#Permission)，其中是 `PermissionChecker` 的集合。\n只要有一个 `PermissionChecker` 检查结果为 `True` 时就会继续运行。\n\n## _def_ `USER(*users, perm=None)` {#USER}\n\n- **说明**\n\n  匹配当前事件属于指定会话。\n\n  如果 `perm` 中仅有 `User` 类型的权限检查函数，则会去除原有检查函数的会话 ID 限制。\n\n- **参数**\n  - `*users` (str)\n\n  - `perm` (Permission | None): 需要同时满足的权限\n\n  - `user`: 会话白名单\n\n- **返回**\n  - untyped\n\n## _class_ `Permission(*checkers)` {#Permission}\n\n- **说明**\n\n  权限类。\n\n  当事件传递时，在 [Matcher](matcher.md#Matcher) 运行前进行检查。\n\n- **参数**\n  - `*checkers` ([T_PermissionChecker](typing.md#T-PermissionChecker) | [Dependent](dependencies/index.md#Dependent)[bool]): PermissionChecker\n\n- **用法**\n\n  ```python\n  Permission(async_function) | sync_function\n  # 等价于\n  Permission(async_function, sync_function)\n  ```\n\n### _instance-var_ `checkers` {#Permission-checkers}\n\n- **类型:** set[[Dependent](dependencies/index.md#Dependent)[bool]]\n\n- **说明:** 存储 `PermissionChecker`\n\n### _async method_ `__call__(bot, event, stack=None, dependency_cache=None)` {#Permission---call--}\n\n- **说明:** 检查是否满足某个权限。\n\n- **参数**\n  - `bot` ([Bot](adapters/index.md#Bot)): Bot 对象\n\n  - `event` ([Event](adapters/index.md#Event)): Event 对象\n\n  - `stack` (AsyncExitStack | None): 异步上下文栈\n\n  - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None): 依赖缓存\n\n- **返回**\n  - bool\n\n## _class_ `User(users, perm=None)` {#User}\n\n- **说明:** 检查当前事件是否属于指定会话。\n\n- **参数**\n  - `users` (tuple[str, ...]): 会话 ID 元组\n\n  - `perm` (Permission | None): 需同时满足的权限\n\n### _classmethod_ `from_event(event, perm=None)` {#User-from-event}\n\n- **说明**\n\n  从事件中获取会话 ID。\n\n  如果 `perm` 中仅有 `User` 类型的权限检查函数，则会去除原有的会话 ID 限制。\n\n- **参数**\n  - `event` ([Event](adapters/index.md#Event)): Event 对象\n\n  - `perm` (Permission | None): 需同时满足的权限\n\n- **返回**\n  - Self\n\n### _classmethod_ `from_permission(*users, perm=None)` {#User-from-permission}\n\n- **说明**\n\n  指定会话与权限。\n\n  如果 `perm` 中仅有 `User` 类型的权限检查函数，则会去除原有的会话 ID 限制。\n\n- **参数**\n  - `*users` (str): 会话白名单\n\n  - `perm` (Permission | None): 需同时满足的权限\n\n- **返回**\n  - Self\n\n## _class_ `Message(<auto>)` {#Message}\n\n- **说明:** 检查是否为消息事件\n\n- **参数**\n\n  auto\n\n## _class_ `Notice(<auto>)` {#Notice}\n\n- **说明:** 检查是否为通知事件\n\n- **参数**\n\n  auto\n\n## _class_ `Request(<auto>)` {#Request}\n\n- **说明:** 检查是否为请求事件\n\n- **参数**\n\n  auto\n\n## _class_ `MetaEvent(<auto>)` {#MetaEvent}\n\n- **说明:** 检查是否为元事件\n\n- **参数**\n\n  auto\n\n## _var_ `MESSAGE` {#MESSAGE}\n\n- **类型:** [Permission](#Permission)\n\n- **说明**\n\n  匹配任意 `message` 类型事件\n\n  仅在需要同时捕获不同类型事件时使用，优先使用 message type 的 Matcher。\n\n## _var_ `NOTICE` {#NOTICE}\n\n- **类型:** [Permission](#Permission)\n\n- **说明**\n\n  匹配任意 `notice` 类型事件\n\n  仅在需要同时捕获不同类型事件时使用，优先使用 notice type 的 Matcher。\n\n## _var_ `REQUEST` {#REQUEST}\n\n- **类型:** [Permission](#Permission)\n\n- **说明**\n\n  匹配任意 `request` 类型事件\n\n  仅在需要同时捕获不同类型事件时使用，优先使用 request type 的 Matcher。\n\n## _var_ `METAEVENT` {#METAEVENT}\n\n- **类型:** [Permission](#Permission)\n\n- **说明**\n\n  匹配任意 `meta_event` 类型事件\n\n  仅在需要同时捕获不同类型事件时使用，优先使用 meta_event type 的 Matcher。\n\n## _class_ `SuperUser(<auto>)` {#SuperUser}\n\n- **说明:** 检查当前事件是否是消息事件且属于超级管理员\n\n- **参数**\n\n  auto\n\n## _var_ `SUPERUSER` {#SUPERUSER}\n\n- **类型:** [Permission](#Permission)\n\n- **说明:** 匹配任意超级用户事件\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/api/plugin/_category_.json",
    "content": "{\n  \"position\": 12\n}\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/api/plugin/index.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 0\ndescription: nonebot.plugin 模块\n---\n\n# nonebot.plugin\n\n本模块为 NoneBot 插件开发提供便携的定义函数。\n\n## 快捷导入\n\n为方便使用，本模块从子模块导入了部分内容，以下内容可以直接通过本模块导入:\n\n- `on` => [`on`](on.md#on)\n- `on_metaevent` => [`on_metaevent`](on.md#on-metaevent)\n- `on_message` => [`on_message`](on.md#on-message)\n- `on_notice` => [`on_notice`](on.md#on-notice)\n- `on_request` => [`on_request`](on.md#on-request)\n- `on_startswith` => [`on_startswith`](on.md#on-startswith)\n- `on_endswith` => [`on_endswith`](on.md#on-endswith)\n- `on_fullmatch` => [`on_fullmatch`](on.md#on-fullmatch)\n- `on_keyword` => [`on_keyword`](on.md#on-keyword)\n- `on_command` => [`on_command`](on.md#on-command)\n- `on_shell_command` => [`on_shell_command`](on.md#on-shell-command)\n- `on_regex` => [`on_regex`](on.md#on-regex)\n- `on_type` => [`on_type`](on.md#on-type)\n- `CommandGroup` => [`CommandGroup`](on.md#CommandGroup)\n- `Matchergroup` => [`MatcherGroup`](on.md#MatcherGroup)\n- `load_plugin` => [`load_plugin`](load.md#load-plugin)\n- `load_plugins` => [`load_plugins`](load.md#load-plugins)\n- `load_all_plugins` => [`load_all_plugins`](load.md#load-all-plugins)\n- `load_from_json` => [`load_from_json`](load.md#load-from-json)\n- `load_from_toml` => [`load_from_toml`](load.md#load-from-toml)\n- `load_builtin_plugin` =>\n  [`load_builtin_plugin`](load.md#load-builtin-plugin)\n- `load_builtin_plugins` =>\n  [`load_builtin_plugins`](load.md#load-builtin-plugins)\n- `require` => [`require`](load.md#require)\n- `PluginMetadata` => [`PluginMetadata`](model.md#PluginMetadata)\n\n## _def_ `get_plugin(plugin_id)` {#get-plugin}\n\n- **说明**\n\n  获取已经导入的某个插件。\n\n  如果为 `load_plugins` 文件夹导入的插件，则为文件(夹)名。\n\n  如果为嵌套的子插件，标识符为 `父插件标识符:子插件文件(夹)名`。\n\n- **参数**\n  - `plugin_id` (str): 插件标识符，即 [Plugin.id\\_](model.md#Plugin-id-)。\n\n- **返回**\n  - [Plugin](model.md#Plugin) | None\n\n## _def_ `get_plugin_by_module_name(module_name)` {#get-plugin-by-module-name}\n\n- **说明**\n\n  通过模块名获取已经导入的某个插件。\n\n  如果提供的模块名为某个插件的子模块，同样会返回该插件。\n\n- **参数**\n  - `module_name` (str): 模块名，即 [Plugin.module_name](model.md#Plugin-module-name)。\n\n- **返回**\n  - [Plugin](model.md#Plugin) | None\n\n## _def_ `get_loaded_plugins()` {#get-loaded-plugins}\n\n- **说明:** 获取当前已导入的所有插件。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - set[[Plugin](model.md#Plugin)]\n\n## _def_ `get_available_plugin_names()` {#get-available-plugin-names}\n\n- **说明:** 获取当前所有可用的插件标识符（包含尚未加载的插件）。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - set[str]\n\n## _def_ `get_plugin_config(config)` {#get-plugin-config}\n\n- **说明:** 从全局配置获取当前插件需要的配置项。\n\n- **参数**\n  - `config` (type[C])\n\n- **返回**\n  - C\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/api/plugin/load.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 1\ndescription: nonebot.plugin.load 模块\n---\n\n# nonebot.plugin.load\n\n本模块定义插件加载接口。\n\n## _def_ `load_plugin(module_path)` {#load-plugin}\n\n- **说明:** 加载单个插件，可以是本地插件或是通过 `pip` 安装的插件。\n\n- **参数**\n  - `module_path` (str | Path): 插件名称 `path.to.your.plugin` 或插件路径 `pathlib.Path(path/to/your/plugin)`\n\n- **返回**\n  - [Plugin](model.md#Plugin) | None\n\n## _def_ `load_plugins(*plugin_dir)` {#load-plugins}\n\n- **说明:** 导入文件夹下多个插件，以 `_` 开头的插件不会被导入!\n\n- **参数**\n  - `*plugin_dir` (str): 文件夹路径\n\n- **返回**\n  - set[[Plugin](model.md#Plugin)]\n\n## _def_ `load_all_plugins(module_path, plugin_dir)` {#load-all-plugins}\n\n- **说明:** 导入指定列表中的插件以及指定目录下多个插件，以 `_` 开头的插件不会被导入!\n\n- **参数**\n  - `module_path` (Iterable[str]): 指定插件集合\n\n  - `plugin_dir` (Iterable[str]): 指定文件夹路径集合\n\n- **返回**\n  - set[[Plugin](model.md#Plugin)]\n\n## _def_ `load_from_json(file_path, encoding=\"utf-8\")` {#load-from-json}\n\n- **说明:** 导入指定 json 文件中的 `plugins` 以及 `plugin_dirs` 下多个插件。 以 `_` 开头的插件不会被导入!\n\n- **参数**\n  - `file_path` (str): 指定 json 文件路径\n\n  - `encoding` (str): 指定 json 文件编码\n\n- **返回**\n  - set[[Plugin](model.md#Plugin)]\n\n- **用法**\n\n  ```json title=plugins.json\n  {\n    \"plugins\": [\"some_plugin\"],\n    \"plugin_dirs\": [\"some_dir\"]\n  }\n  ```\n\n  ```python\n  nonebot.load_from_json(\"plugins.json\")\n  ```\n\n## _def_ `load_from_toml(file_path, encoding=\"utf-8\")` {#load-from-toml}\n\n- **说明:** 导入指定 toml 文件 `[tool.nonebot]` 中的 `plugins` 以及 `plugin_dirs` 下多个插件。 以 `_` 开头的插件不会被导入!\n\n- **参数**\n  - `file_path` (str): 指定 toml 文件路径\n\n  - `encoding` (str): 指定 toml 文件编码\n\n- **返回**\n  - set[[Plugin](model.md#Plugin)]\n\n- **用法**\n\n  ```toml title=pyproject.toml\n  [tool.nonebot]\n  plugins = [\"some_plugin\"]\n  plugin_dirs = [\"some_dir\"]\n  ```\n\n  ```python\n  nonebot.load_from_toml(\"pyproject.toml\")\n  ```\n\n## _def_ `load_builtin_plugin(name)` {#load-builtin-plugin}\n\n- **说明:** 导入 NoneBot 内置插件。\n\n- **参数**\n  - `name` (str): 插件名称\n\n- **返回**\n  - [Plugin](model.md#Plugin) | None\n\n## _def_ `load_builtin_plugins(*plugins)` {#load-builtin-plugins}\n\n- **说明:** 导入多个 NoneBot 内置插件。\n\n- **参数**\n  - `*plugins` (str): 插件名称列表\n\n- **返回**\n  - set[[Plugin](model.md#Plugin)]\n\n## _def_ `require(name)` {#require}\n\n- **说明:** 声明依赖插件。\n\n- **参数**\n  - `name` (str): 插件模块名或插件标识符，仅在已声明插件的情况下可使用标识符。\n\n- **返回**\n  - ModuleType\n\n- **异常**\n  - RuntimeError: 插件无法加载\n\n## _def_ `inherit_supported_adapters(*names)` {#inherit-supported-adapters}\n\n- **说明**\n\n  获取已加载插件的适配器支持状态集合。\n\n  如果传入了多个插件名称，返回值会自动取交集。\n\n- **参数**\n  - `*names` (str): 插件名称列表。\n\n- **返回**\n  - set[str] | None\n\n- **异常**\n  - RuntimeError: 插件未加载\n\n  - ValueError: 插件缺少元数据\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/api/plugin/manager.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 5\ndescription: nonebot.plugin.manager 模块\n---\n\n# nonebot.plugin.manager\n\n本模块实现插件加载流程。\n\n参考: [import hooks](https://docs.python.org/3/reference/import.html#import-hooks), [PEP302](https://www.python.org/dev/peps/pep-0302/)\n\n## _class_ `PluginManager(plugins=None, search_path=None)` {#PluginManager}\n\n- **说明:** 插件管理器。\n\n- **参数**\n  - `plugins` (Iterable[str] | None): 独立插件模块名集合。\n\n  - `search_path` (Iterable[str] | None): 插件搜索路径（文件夹），相对于当前工作目录。\n\n### _property_ `third_party_plugins` {#PluginManager-third-party-plugins}\n\n- **类型:** set[str]\n\n- **说明:** 返回所有独立插件标识符。\n\n### _property_ `searched_plugins` {#PluginManager-searched-plugins}\n\n- **类型:** set[str]\n\n- **说明:** 返回已搜索到的插件标识符。\n\n### _property_ `available_plugins` {#PluginManager-available-plugins}\n\n- **类型:** set[str]\n\n- **说明:** 返回当前插件管理器中可用的插件标识符。\n\n### _property_ `controlled_modules` {#PluginManager-controlled-modules}\n\n- **类型:** dict[str, str]\n\n- **说明:** 返回当前插件管理器中控制的插件标识符与模块路径映射字典。\n\n### _method_ `load_plugin(name)` {#PluginManager-load-plugin}\n\n- **说明**\n\n  加载指定插件。\n\n  可以使用完整插件模块名或者插件标识符加载。\n\n- **参数**\n  - `name` (str): 插件名称或插件标识符。\n\n- **返回**\n  - [Plugin](model.md#Plugin) | None\n\n### _method_ `load_all_plugins()` {#PluginManager-load-all-plugins}\n\n- **说明:** 加载所有可用插件。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - set[[Plugin](model.md#Plugin)]\n\n## _class_ `PluginFinder(<auto>)` {#PluginFinder}\n\n- **参数**\n\n  auto\n\n### _method_ `find_spec(fullname, path, target=None)` {#PluginFinder-find-spec}\n\n- **参数**\n  - `fullname` (str)\n\n  - `path` (Sequence[str] | None)\n\n  - `target` (ModuleType | None)\n\n- **返回**\n  - untyped\n\n## _class_ `PluginLoader(manager, fullname, path)` {#PluginLoader}\n\n- **参数**\n  - `manager` (PluginManager)\n\n  - `fullname` (str)\n\n  - `path` (str)\n\n### _method_ `create_module(spec)` {#PluginLoader-create-module}\n\n- **参数**\n  - `spec`\n\n- **返回**\n  - ModuleType | None\n\n### _method_ `exec_module(module)` {#PluginLoader-exec-module}\n\n- **参数**\n  - `module` (ModuleType)\n\n- **返回**\n  - None\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/api/plugin/model.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 3\ndescription: nonebot.plugin.model 模块\n---\n\n# nonebot.plugin.model\n\n本模块定义插件相关信息。\n\n## _class_ `PluginMetadata(<auto>)` {#PluginMetadata}\n\n- **说明:** 插件元信息，由插件编写者提供\n\n- **参数**\n\n  auto\n\n### _instance-var_ `name` {#PluginMetadata-name}\n\n- **类型:** str\n\n- **说明:** 插件名称\n\n### _instance-var_ `description` {#PluginMetadata-description}\n\n- **类型:** str\n\n- **说明:** 插件功能介绍\n\n### _instance-var_ `usage` {#PluginMetadata-usage}\n\n- **类型:** str\n\n- **说明:** 插件使用方法\n\n### _class-var_ `type` {#PluginMetadata-type}\n\n- **类型:** str | None\n\n- **说明:** 插件类型，用于商店分类\n\n### _class-var_ `homepage` {#PluginMetadata-homepage}\n\n- **类型:** str | None\n\n- **说明:** 插件主页\n\n### _class-var_ `config` {#PluginMetadata-config}\n\n- **类型:** type[BaseModel] | None\n\n- **说明:** 插件配置项\n\n### _class-var_ `supported_adapters` {#PluginMetadata-supported-adapters}\n\n- **类型:** set[str] | None\n\n- **说明**\n\n  插件支持的适配器模块路径\n\n  格式为 `<module>[:<Adapter>]`，`~` 为 `nonebot.adapters.` 的缩写。\n\n  `None` 表示支持**所有适配器**。\n\n### _class-var_ `extra` {#PluginMetadata-extra}\n\n- **类型:** dict[Any, Any]\n\n- **说明:** 插件额外信息，可由插件编写者自由扩展定义\n\n### _method_ `get_supported_adapters()` {#PluginMetadata-get-supported-adapters}\n\n- **说明:** 获取当前已安装的插件支持适配器类列表\n\n- **参数**\n\n  empty\n\n- **返回**\n  - set[type[[Adapter](../adapters/index.md#Adapter)]] | None\n\n## _class_ `Plugin(<auto>)` {#Plugin}\n\n- **说明:** 存储插件信息\n\n- **参数**\n\n  auto\n\n### _instance-var_ `name` {#Plugin-name}\n\n- **类型:** str\n\n- **说明:** 插件名称，NoneBot 使用 文件/文件夹 名称作为插件名称\n\n### _instance-var_ `module` {#Plugin-module}\n\n- **类型:** ModuleType\n\n- **说明:** 插件模块对象\n\n### _instance-var_ `module_name` {#Plugin-module-name}\n\n- **类型:** str\n\n- **说明:** 点分割模块路径\n\n### _instance-var_ `manager` {#Plugin-manager}\n\n- **类型:** [PluginManager](manager.md#PluginManager)\n\n- **说明:** 导入该插件的插件管理器\n\n### _class-var_ `matcher` {#Plugin-matcher}\n\n- **类型:** set[type[[Matcher](../matcher.md#Matcher)]]\n\n- **说明:** 插件加载时定义的 `Matcher`\n\n### _class-var_ `parent_plugin` {#Plugin-parent-plugin}\n\n- **类型:** Plugin | None\n\n- **说明:** 父插件\n\n### _class-var_ `sub_plugins` {#Plugin-sub-plugins}\n\n- **类型:** set[Plugin]\n\n- **说明:** 子插件集合\n\n### _property_ `id_` {#Plugin-id-}\n\n- **类型:** str\n\n- **说明:** 插件索引标识\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/api/plugin/on.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 2\ndescription: nonebot.plugin.on 模块\n---\n\n# nonebot.plugin.on\n\n本模块定义事件响应器便携定义函数。\n\n## _def_ `store_matcher(matcher)` {#store-matcher}\n\n- **说明:** 存储一个事件响应器到插件。\n\n- **参数**\n  - `matcher` (type[[Matcher](../matcher.md#Matcher)]): 事件响应器\n\n- **返回**\n  - None\n\n## _def_ `get_matcher_plugin(depth=...)` {#get-matcher-plugin}\n\n- **说明**\n\n  获取事件响应器定义所在插件。\n\n  **Deprecated**, 请使用 [get_matcher_source](#get-matcher-source) 获取信息。\n\n- **参数**\n  - `depth` (int): 调用栈深度\n\n- **返回**\n  - [Plugin](model.md#Plugin) | None\n\n## _def_ `get_matcher_module(depth=...)` {#get-matcher-module}\n\n- **说明**\n\n  获取事件响应器定义所在模块。\n\n  **Deprecated**, 请使用 [get_matcher_source](#get-matcher-source) 获取信息。\n\n- **参数**\n  - `depth` (int): 调用栈深度\n\n- **返回**\n  - ModuleType | None\n\n## _def_ `get_matcher_source(depth=...)` {#get-matcher-source}\n\n- **说明:** 获取事件响应器定义所在源码信息。\n\n- **参数**\n  - `depth` (int): 调用栈深度\n\n- **返回**\n  - MatcherSource | None\n\n## _def_ `on(type=\"\", rule=..., permission=..., *, handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on}\n\n- **说明:** 注册一个基础事件响应器，可自定义类型。\n\n- **参数**\n  - `type` (str): 事件响应器类型\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n## _def_ `on_metaevent(rule=..., permission=..., *, handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-metaevent}\n\n- **说明:** 注册一个元事件响应器。\n\n- **参数**\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n## _def_ `on_message(rule=..., permission=..., *, handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-message}\n\n- **说明:** 注册一个消息事件响应器。\n\n- **参数**\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n## _def_ `on_notice(rule=..., permission=..., *, handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-notice}\n\n- **说明:** 注册一个通知事件响应器。\n\n- **参数**\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n## _def_ `on_request(rule=..., permission=..., *, handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-request}\n\n- **说明:** 注册一个请求事件响应器。\n\n- **参数**\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n## _def_ `on_startswith(msg, rule=..., ignorecase=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-startswith}\n\n- **说明:** 注册一个消息事件响应器，并且当消息的**文本部分**以指定内容开头时响应。\n\n- **参数**\n  - `msg` (str | tuple[str, ...]): 指定消息开头内容\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `ignorecase` (bool): 是否忽略大小写\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n## _def_ `on_endswith(msg, rule=..., ignorecase=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-endswith}\n\n- **说明:** 注册一个消息事件响应器，并且当消息的**文本部分**以指定内容结尾时响应。\n\n- **参数**\n  - `msg` (str | tuple[str, ...]): 指定消息结尾内容\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `ignorecase` (bool): 是否忽略大小写\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n## _def_ `on_fullmatch(msg, rule=..., ignorecase=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-fullmatch}\n\n- **说明:** 注册一个消息事件响应器，并且当消息的**文本部分**与指定内容完全一致时响应。\n\n- **参数**\n  - `msg` (str | tuple[str, ...]): 指定消息全匹配内容\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `ignorecase` (bool): 是否忽略大小写\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n## _def_ `on_keyword(keywords, rule=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-keyword}\n\n- **说明:** 注册一个消息事件响应器，并且当消息纯文本部分包含关键词时响应。\n\n- **参数**\n  - `keywords` (set[str]): 关键词列表\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n## _def_ `on_command(cmd, rule=..., aliases=..., force_whitespace=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-command}\n\n- **说明**\n\n  注册一个消息事件响应器，并且当消息以指定命令开头时响应。\n\n  命令匹配规则参考: `命令形式匹配 <rule.md#command-command>`\\_\n\n- **参数**\n  - `cmd` (str | tuple[str, ...]): 指定命令内容\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `aliases` (set[str | tuple[str, ...]] | None): 命令别名\n\n  - `force_whitespace` (str | bool | None): 是否强制命令后必须有指定空白符\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n## _def_ `on_shell_command(cmd, rule=..., aliases=..., parser=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-shell-command}\n\n- **说明**\n\n  注册一个支持 `shell_like` 解析参数的命令消息事件响应器。\n\n  与普通的 `on_command` 不同的是，在添加 `parser` 参数时, 响应器会自动处理消息。\n\n  可以通过 [ShellCommandArgv](../params.md#ShellCommandArgv) 获取原始参数列表，\n  通过 [ShellCommandArgs](../params.md#ShellCommandArgs) 获取解析后的参数字典。\n\n- **参数**\n  - `cmd` (str | tuple[str, ...]): 指定命令内容\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `aliases` (set[str | tuple[str, ...]] | None): 命令别名\n\n  - `parser` ([ArgumentParser](../rule.md#ArgumentParser) | None): `nonebot.rule.ArgumentParser` 对象\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n## _def_ `on_regex(pattern, flags=..., rule=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-regex}\n\n- **说明**\n\n  注册一个消息事件响应器，并且当消息匹配正则表达式时响应。\n\n  命令匹配规则参考: `正则匹配 <rule.md#regex-regex-flags-0>`\\_\n\n- **参数**\n  - `pattern` (str): 正则表达式\n\n  - `flags` (int | re.RegexFlag): 正则匹配标志\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n## _def_ `on_type(types, rule=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-type}\n\n- **说明:** 注册一个事件响应器，并且当事件为指定类型时响应。\n\n- **参数**\n  - `types` (type[[Event](../adapters/index.md#Event)] | tuple[type[[Event](../adapters/index.md#Event)], ...]): 事件类型\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n## _class_ `CommandGroup(cmd, prefix_aliases=..., *, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#CommandGroup}\n\n- **参数**\n  - `cmd` (str | tuple[str, ...])\n\n  - `prefix_aliases` (bool)\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None)\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None)\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None)\n\n  - `temp` (bool)\n\n  - `expire_time` (datetime | timedelta | None)\n\n  - `priority` (int)\n\n  - `block` (bool)\n\n  - `state` ([T_State](../typing.md#T-State) | None)\n\n### _method_ `command(cmd, *, rule=..., aliases=..., force_whitespace=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#CommandGroup-command}\n\n- **说明:** 注册一个新的命令。新参数将会覆盖命令组默认值\n\n- **参数**\n  - `cmd` (str | tuple[str, ...]): 指定命令内容\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `aliases` (set[str | tuple[str, ...]] | None): 命令别名\n\n  - `force_whitespace` (str | bool | None): 是否强制命令后必须有指定空白符\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n### _method_ `shell_command(cmd, *, rule=..., aliases=..., parser=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#CommandGroup-shell-command}\n\n- **说明:** 注册一个新的 `shell_like` 命令。新参数将会覆盖命令组默认值\n\n- **参数**\n  - `cmd` (str | tuple[str, ...]): 指定命令内容\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `aliases` (set[str | tuple[str, ...]] | None): 命令别名\n\n  - `parser` ([ArgumentParser](../rule.md#ArgumentParser) | None): `nonebot.rule.ArgumentParser` 对象\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n## _class_ `MatcherGroup(*, type=..., rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup}\n\n- **参数**\n  - `type` (str)\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None)\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None)\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None)\n\n  - `temp` (bool)\n\n  - `expire_time` (datetime | timedelta | None)\n\n  - `priority` (int)\n\n  - `block` (bool)\n\n  - `state` ([T_State](../typing.md#T-State) | None)\n\n### _method_ `on(*, type=..., rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on}\n\n- **说明:** 注册一个基础事件响应器，可自定义类型。\n\n- **参数**\n  - `type` (str): 事件响应器类型\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n### _method_ `on_metaevent(*, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-metaevent}\n\n- **说明:** 注册一个元事件响应器。\n\n- **参数**\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n### _method_ `on_message(*, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-message}\n\n- **说明:** 注册一个消息事件响应器。\n\n- **参数**\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n### _method_ `on_notice(*, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-notice}\n\n- **说明:** 注册一个通知事件响应器。\n\n- **参数**\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n### _method_ `on_request(*, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-request}\n\n- **说明:** 注册一个请求事件响应器。\n\n- **参数**\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n### _method_ `on_startswith(msg, *, ignorecase=..., rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-startswith}\n\n- **说明:** 注册一个消息事件响应器，并且当消息的**文本部分**以指定内容开头时响应。\n\n- **参数**\n  - `msg` (str | tuple[str, ...]): 指定消息开头内容\n\n  - `ignorecase` (bool): 是否忽略大小写\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n### _method_ `on_endswith(msg, *, ignorecase=..., rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-endswith}\n\n- **说明:** 注册一个消息事件响应器，并且当消息的**文本部分**以指定内容结尾时响应。\n\n- **参数**\n  - `msg` (str | tuple[str, ...]): 指定消息结尾内容\n\n  - `ignorecase` (bool): 是否忽略大小写\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n### _method_ `on_fullmatch(msg, *, ignorecase=..., rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-fullmatch}\n\n- **说明:** 注册一个消息事件响应器，并且当消息的**文本部分**与指定内容完全一致时响应。\n\n- **参数**\n  - `msg` (str | tuple[str, ...]): 指定消息全匹配内容\n\n  - `ignorecase` (bool): 是否忽略大小写\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n### _method_ `on_keyword(keywords, *, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-keyword}\n\n- **说明:** 注册一个消息事件响应器，并且当消息纯文本部分包含关键词时响应。\n\n- **参数**\n  - `keywords` (set[str]): 关键词列表\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n### _method_ `on_command(cmd, aliases=..., force_whitespace=..., *, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-command}\n\n- **说明**\n\n  注册一个消息事件响应器，并且当消息以指定命令开头时响应。\n\n  命令匹配规则参考: `命令形式匹配 <rule.md#command-command>`\\_\n\n- **参数**\n  - `cmd` (str | tuple[str, ...]): 指定命令内容\n\n  - `aliases` (set[str | tuple[str, ...]] | None): 命令别名\n\n  - `force_whitespace` (str | bool | None): 是否强制命令后必须有指定空白符\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n### _method_ `on_shell_command(cmd, aliases=..., parser=..., *, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-shell-command}\n\n- **说明**\n\n  注册一个支持 `shell_like` 解析参数的命令消息事件响应器。\n\n  与普通的 `on_command` 不同的是，在添加 `parser` 参数时, 响应器会自动处理消息。\n\n  可以通过 [ShellCommandArgv](../params.md#ShellCommandArgv) 获取原始参数列表，\n  通过 [ShellCommandArgs](../params.md#ShellCommandArgs) 获取解析后的参数字典。\n\n- **参数**\n  - `cmd` (str | tuple[str, ...]): 指定命令内容\n\n  - `aliases` (set[str | tuple[str, ...]] | None): 命令别名\n\n  - `parser` ([ArgumentParser](../rule.md#ArgumentParser) | None): `nonebot.rule.ArgumentParser` 对象\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n### _method_ `on_regex(pattern, flags=..., *, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-regex}\n\n- **说明**\n\n  注册一个消息事件响应器，并且当消息匹配正则表达式时响应。\n\n  命令匹配规则参考: `正则匹配 <rule.md#regex-regex-flags-0>`\\_\n\n- **参数**\n  - `pattern` (str): 正则表达式\n\n  - `flags` (int | re.RegexFlag): 正则匹配标志\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n### _method_ `on_type(types, *, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-type}\n\n- **说明:** 注册一个事件响应器，并且当事件为指定类型时响应。\n\n- **参数**\n  - `types` (type[[Event](../adapters/index.md#Event)] | tuple[type[[Event](../adapters/index.md#Event)]]): 事件类型\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/api/rule.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 5\ndescription: nonebot.rule 模块\n---\n\n# nonebot.rule\n\n本模块是 [Matcher.rule](matcher.md#Matcher-rule) 的类型定义。\n\n每个[事件响应器](matcher.md#Matcher)拥有一个\n[Rule](#Rule)，其中是 `RuleChecker` 的集合。\n只有当所有 `RuleChecker` 检查结果为 `True` 时继续运行。\n\n## _class_ `Rule(*checkers)` {#Rule}\n\n- **说明**\n\n  规则类。\n\n  当事件传递时，在 [Matcher](matcher.md#Matcher) 运行前进行检查。\n\n- **参数**\n  - `*checkers` ([T_RuleChecker](typing.md#T-RuleChecker) | [Dependent](dependencies/index.md#Dependent)[bool]): RuleChecker\n\n- **用法**\n\n  ```python\n  Rule(async_function) & sync_function\n  # 等价于\n  Rule(async_function, sync_function)\n  ```\n\n### _instance-var_ `checkers` {#Rule-checkers}\n\n- **类型:** set[[Dependent](dependencies/index.md#Dependent)[bool]]\n\n- **说明:** 存储 `RuleChecker`\n\n### _async method_ `__call__(bot, event, state, stack=None, dependency_cache=None)` {#Rule---call--}\n\n- **说明:** 检查是否符合所有规则\n\n- **参数**\n  - `bot` ([Bot](adapters/index.md#Bot)): Bot 对象\n\n  - `event` ([Event](adapters/index.md#Event)): Event 对象\n\n  - `state` ([T_State](typing.md#T-State)): 当前 State\n\n  - `stack` (AsyncExitStack | None): 异步上下文栈\n\n  - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None): 依赖缓存\n\n- **返回**\n  - bool\n\n## _class_ `CMD_RESULT(<auto>)` {#CMD-RESULT}\n\n- **参数**\n\n  auto\n\n## _class_ `TRIE_VALUE(<auto>)` {#TRIE-VALUE}\n\n- **说明:** TRIE_VALUE(command_start, command)\n\n- **参数**\n\n  auto\n\n## _class_ `StartswithRule(msg, ignorecase=False)` {#StartswithRule}\n\n- **说明:** 检查消息纯文本是否以指定字符串开头。\n\n- **参数**\n  - `msg` (tuple[str, ...]): 指定消息开头字符串元组\n\n  - `ignorecase` (bool): 是否忽略大小写\n\n## _def_ `startswith(msg, ignorecase=False)` {#startswith}\n\n- **说明:** 匹配消息纯文本开头。\n\n- **参数**\n  - `msg` (str | tuple[str, ...]): 指定消息开头字符串元组\n\n  - `ignorecase` (bool): 是否忽略大小写\n\n- **返回**\n  - [Rule](#Rule)\n\n## _class_ `EndswithRule(msg, ignorecase=False)` {#EndswithRule}\n\n- **说明:** 检查消息纯文本是否以指定字符串结尾。\n\n- **参数**\n  - `msg` (tuple[str, ...]): 指定消息结尾字符串元组\n\n  - `ignorecase` (bool): 是否忽略大小写\n\n## _def_ `endswith(msg, ignorecase=False)` {#endswith}\n\n- **说明:** 匹配消息纯文本结尾。\n\n- **参数**\n  - `msg` (str | tuple[str, ...]): 指定消息开头字符串元组\n\n  - `ignorecase` (bool): 是否忽略大小写\n\n- **返回**\n  - [Rule](#Rule)\n\n## _class_ `FullmatchRule(msg, ignorecase=False)` {#FullmatchRule}\n\n- **说明:** 检查消息纯文本是否与指定字符串全匹配。\n\n- **参数**\n  - `msg` (tuple[str, ...]): 指定消息全匹配字符串元组\n\n  - `ignorecase` (bool): 是否忽略大小写\n\n## _def_ `fullmatch(msg, ignorecase=False)` {#fullmatch}\n\n- **说明:** 完全匹配消息。\n\n- **参数**\n  - `msg` (str | tuple[str, ...]): 指定消息全匹配字符串元组\n\n  - `ignorecase` (bool): 是否忽略大小写\n\n- **返回**\n  - [Rule](#Rule)\n\n## _class_ `KeywordsRule(*keywords)` {#KeywordsRule}\n\n- **说明:** 检查消息纯文本是否包含指定关键字。\n\n- **参数**\n  - `*keywords` (str): 指定关键字元组\n\n## _def_ `keyword(*keywords)` {#keyword}\n\n- **说明:** 匹配消息纯文本关键词。\n\n- **参数**\n  - `*keywords` (str): 指定关键字元组\n\n- **返回**\n  - [Rule](#Rule)\n\n## _class_ `CommandRule(cmds, force_whitespace=None)` {#CommandRule}\n\n- **说明:** 检查消息是否为指定命令。\n\n- **参数**\n  - `cmds` (list[tuple[str, ...]]): 指定命令元组列表\n\n  - `force_whitespace` (str | bool | None): 是否强制命令后必须有指定空白符\n\n## _def_ `command(*cmds, force_whitespace=None)` {#command}\n\n- **说明**\n\n  匹配消息命令。\n\n  根据配置里提供的 [`command_start`](config.md#Config-command-start),\n  [`command_sep`](config.md#Config-command-sep) 判断消息是否为命令。\n\n  可以通过 [Command](params.md#Command) 获取匹配成功的命令（例: `(\"test\",)`），\n  通过 [RawCommand](params.md#RawCommand) 获取匹配成功的原始命令文本（例: `\"/test\"`），\n  通过 [CommandArg](params.md#CommandArg) 获取匹配成功的命令参数。\n\n- **参数**\n  - `*cmds` (str | tuple[str, ...]): 命令文本或命令元组\n\n  - `force_whitespace` (str | bool | None): 是否强制命令后必须有指定空白符\n\n- **返回**\n  - [Rule](#Rule)\n\n- **用法**\n\n  使用默认 `command_start`, `command_sep` 配置情况下：\n\n  命令 `(\"test\",)` 可以匹配: `/test` 开头的消息\n  命令 `(\"test\", \"sub\")` 可以匹配: `/test.sub` 开头的消息\n\n:::tip 提示\n命令内容与后续消息间无需空格!\n:::\n\n## _class_ `ArgumentParser(<auto>)` {#ArgumentParser}\n\n- **说明**\n\n  `shell_like` 命令参数解析器，解析出错时不会退出程序。\n\n  支持 [Message](adapters/index.md#Message) 富文本解析。\n\n- **参数**\n\n  auto\n\n- **用法**\n\n  用法与 `argparse.ArgumentParser` 相同，\n  参考文档: [argparse](https://docs.python.org/3/library/argparse.html)\n\n### _method_ `parse_known_args(args=None, namespace=None)` {#ArgumentParser-parse-known-args}\n\n- **重载**\n\n  **1.** `(args=None, namespace=None) -> tuple[Namespace, list[str | MessageSegment]]`\n  - **参数**\n    - `args` (Sequence[str | [MessageSegment](adapters/index.md#MessageSegment)] | None)\n\n    - `namespace` (None)\n\n  - **返回**\n    - tuple[Namespace, list[str | [MessageSegment](adapters/index.md#MessageSegment)]]\n\n  **2.** `(args, namespace) -> tuple[T, list[str | MessageSegment]]`\n  - **参数**\n    - `args` (Sequence[str | [MessageSegment](adapters/index.md#MessageSegment)] | None)\n\n    - `namespace` (T)\n\n  - **返回**\n    - tuple[T, list[str | [MessageSegment](adapters/index.md#MessageSegment)]]\n\n  **3.** `(*, namespace) -> tuple[T, list[str | MessageSegment]]`\n  - **参数**\n    - `namespace` (T)\n\n  - **返回**\n    - tuple[T, list[str | [MessageSegment](adapters/index.md#MessageSegment)]]\n\n## _class_ `ShellCommandRule(cmds, parser)` {#ShellCommandRule}\n\n- **说明:** 检查消息是否为指定 shell 命令。\n\n- **参数**\n  - `cmds` (list[tuple[str, ...]]): 指定命令元组列表\n\n  - `parser` (ArgumentParser | None): 可选参数解析器\n\n## _def_ `shell_command(*cmds, parser=None)` {#shell-command}\n\n- **说明**\n\n  匹配 `shell_like` 形式的消息命令。\n\n  根据配置里提供的 [`command_start`](config.md#Config-command-start),\n  [`command_sep`](config.md#Config-command-sep) 判断消息是否为命令。\n\n  可以通过 [Command](params.md#Command) 获取匹配成功的命令\n  （例: `(\"test\",)`），\n  通过 [RawCommand](params.md#RawCommand) 获取匹配成功的原始命令文本\n  （例: `\"/test\"`），\n  通过 [ShellCommandArgv](params.md#ShellCommandArgv) 获取解析前的参数列表\n  （例: `[\"arg\", \"-h\"]`），\n  通过 [ShellCommandArgs](params.md#ShellCommandArgs) 获取解析后的参数字典\n  （例: `{\"arg\": \"arg\", \"h\": True}`）。\n\n  :::caution 警告\n  如果参数解析失败，则通过 [ShellCommandArgs](params.md#ShellCommandArgs)\n  获取的将是 [ParserExit](exception.md#ParserExit) 异常。\n  :::\n\n- **参数**\n  - `*cmds` (str | tuple[str, ...]): 命令文本或命令元组\n\n  - `parser` (ArgumentParser | None): [ArgumentParser](#ArgumentParser) 对象\n\n- **返回**\n  - [Rule](#Rule)\n\n- **用法**\n\n  使用默认 `command_start`, `command_sep` 配置，更多示例参考\n  [argparse](https://docs.python.org/3/library/argparse.html) 标准库文档。\n\n  ```python\n  from nonebot.rule import ArgumentParser\n\n  parser = ArgumentParser()\n  parser.add_argument(\"-a\", action=\"store_true\")\n\n  rule = shell_command(\"ls\", parser=parser)\n  ```\n\n:::tip 提示\n命令内容与后续消息间无需空格!\n:::\n\n## _class_ `RegexRule(regex, flags=0)` {#RegexRule}\n\n- **说明:** 检查消息字符串是否符合指定正则表达式。\n\n- **参数**\n  - `regex` (str): 正则表达式\n\n  - `flags` (int): 正则表达式标记\n\n## _def_ `regex(regex, flags=0)` {#regex}\n\n- **说明**\n\n  匹配符合正则表达式的消息字符串。\n\n  可以通过 [RegexStr](params.md#RegexStr) 获取匹配成功的字符串，\n  通过 [RegexGroup](params.md#RegexGroup) 获取匹配成功的 group 元组，\n  通过 [RegexDict](params.md#RegexDict) 获取匹配成功的 group 字典。\n\n- **参数**\n  - `regex` (str): 正则表达式\n\n  - `flags` (int | re.RegexFlag): 正则表达式标记\n\n- **返回**\n  - [Rule](#Rule)\n\n:::tip 提示\n正则表达式匹配使用 search 而非 match，如需从头匹配请使用 `r\"^xxx\"` 来确保匹配开头\n:::\n:::tip 提示\n正则表达式匹配使用 `EventMessage` 的 `str` 字符串，\n而非 `EventMessage` 的 `PlainText` 纯文本字符串\n:::\n\n## _class_ `ToMeRule(<auto>)` {#ToMeRule}\n\n- **说明:** 检查事件是否与机器人有关。\n\n- **参数**\n\n  auto\n\n## _def_ `to_me()` {#to-me}\n\n- **说明:** 匹配与机器人有关的事件。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - [Rule](#Rule)\n\n## _class_ `IsTypeRule(*types)` {#IsTypeRule}\n\n- **说明:** 检查事件类型是否为指定类型。\n\n- **参数**\n  - `*types` (type[[Event](adapters/index.md#Event)])\n\n## _def_ `is_type(*types)` {#is-type}\n\n- **说明:** 匹配事件类型。\n\n- **参数**\n  - `*types` (type[[Event](adapters/index.md#Event)]): 事件类型\n\n- **返回**\n  - [Rule](#Rule)\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/api/typing.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 11\ndescription: nonebot.typing 模块\n---\n\n# nonebot.typing\n\n本模块定义了 NoneBot 模块中共享的一些类型。\n\n使用 Python 的 Type Hint 语法，\n参考 [`PEP 484`](https://www.python.org/dev/peps/pep-0484/),\n[`PEP 526`](https://www.python.org/dev/peps/pep-0526/) 和\n[`typing`](https://docs.python.org/3/library/typing.html)。\n\n## _def_ `overrides(InterfaceClass)` {#overrides}\n\n- **说明:** 标记一个方法为父类 interface 的 implement\n\n- **参数**\n  - `InterfaceClass` (object)\n\n- **返回**\n  - untyped\n\n## _def_ `type_has_args(type_)` {#type-has-args}\n\n- **参数**\n  - `type_` (type[Any])\n\n- **返回**\n  - bool\n\n## _def_ `origin_is_union(origin)` {#origin-is-union}\n\n- **参数**\n  - `origin` (type[Any] | None)\n\n- **返回**\n  - bool\n\n## _def_ `origin_is_literal(origin)` {#origin-is-literal}\n\n- **说明:** 判断是否是 Literal 类型\n\n- **参数**\n  - `origin` (type[Any] | None)\n\n- **返回**\n  - bool\n\n## _def_ `all_literal_values(type_)` {#all-literal-values}\n\n- **说明:** 获取 Literal 类型包含的所有值\n\n- **参数**\n  - `type_` (type[Any])\n\n- **返回**\n  - list[Any]\n\n## _def_ `origin_is_annotated(origin)` {#origin-is-annotated}\n\n- **说明:** 判断是否是 Annotated 类型\n\n- **参数**\n  - `origin` (type[Any] | None)\n\n- **返回**\n  - bool\n\n## _def_ `is_none_type(type_)` {#is-none-type}\n\n- **说明:** 判断是否是 None 类型\n\n- **参数**\n  - `type_` (type[Any])\n\n- **返回**\n  - bool\n\n## _def_ `evaluate_forwardref(ref, globalns, localns)` {#evaluate-forwardref}\n\n- **参数**\n  - `ref` (ForwardRef)\n\n  - `globalns` (dict[str, Any])\n\n  - `localns` (dict[str, Any])\n\n- **返回**\n  - Any\n\n## _class_ `StateFlag(<auto>)` {#StateFlag}\n\n- **参数**\n\n  auto\n\n## _var_ `T_State` {#T-State}\n\n- **类型:** dict[Any, Any]\n\n- **说明:** 事件处理状态 State 类型\n\n## _var_ `T_BotConnectionHook` {#T-BotConnectionHook}\n\n- **类型:** \\_DependentCallable[Any]\n\n- **说明**\n\n  Bot 连接建立时钩子函数\n\n  依赖参数:\n  - DependParam: 子依赖参数\n  - BotParam: Bot 对象\n  - DefaultParam: 带有默认值的参数\n\n## _var_ `T_BotDisconnectionHook` {#T-BotDisconnectionHook}\n\n- **类型:** \\_DependentCallable[Any]\n\n- **说明**\n\n  Bot 连接断开时钩子函数\n\n  依赖参数:\n  - DependParam: 子依赖参数\n  - BotParam: Bot 对象\n  - DefaultParam: 带有默认值的参数\n\n## _var_ `T_CallingAPIHook` {#T-CallingAPIHook}\n\n- **类型:** ([Bot](adapters/index.md#Bot), str, dict[str, Any]) -> Awaitable[Any]\n\n- **说明:** `bot.call_api` 钩子函数\n\n## _var_ `T_CalledAPIHook` {#T-CalledAPIHook}\n\n- **类型:** ([Bot](adapters/index.md#Bot), Exception | None, str, dict[str, Any], Any) -> Awaitable[Any]\n\n- **说明:** `bot.call_api` 后执行的函数，参数分别为 bot, exception, api, data, result\n\n## _var_ `T_EventPreProcessor` {#T-EventPreProcessor}\n\n- **类型:** \\_DependentCallable[Any]\n\n- **说明**\n\n  事件预处理函数 EventPreProcessor 类型\n\n  依赖参数:\n  - DependParam: 子依赖参数\n  - BotParam: Bot 对象\n  - EventParam: Event 对象\n  - StateParam: State 对象\n  - DefaultParam: 带有默认值的参数\n\n## _var_ `T_EventPostProcessor` {#T-EventPostProcessor}\n\n- **类型:** \\_DependentCallable[Any]\n\n- **说明**\n\n  事件后处理函数 EventPostProcessor 类型\n\n  依赖参数:\n  - DependParam: 子依赖参数\n  - BotParam: Bot 对象\n  - EventParam: Event 对象\n  - StateParam: State 对象\n  - DefaultParam: 带有默认值的参数\n\n## _var_ `T_RunPreProcessor` {#T-RunPreProcessor}\n\n- **类型:** \\_DependentCallable[Any]\n\n- **说明**\n\n  事件响应器运行前预处理函数 RunPreProcessor 类型\n\n  依赖参数:\n  - DependParam: 子依赖参数\n  - BotParam: Bot 对象\n  - EventParam: Event 对象\n  - StateParam: State 对象\n  - MatcherParam: Matcher 对象\n  - DefaultParam: 带有默认值的参数\n\n## _var_ `T_RunPostProcessor` {#T-RunPostProcessor}\n\n- **类型:** \\_DependentCallable[Any]\n\n- **说明**\n\n  事件响应器运行后后处理函数 RunPostProcessor 类型\n\n  依赖参数:\n  - DependParam: 子依赖参数\n  - BotParam: Bot 对象\n  - EventParam: Event 对象\n  - StateParam: State 对象\n  - MatcherParam: Matcher 对象\n  - ExceptionParam: 异常对象（可能为 None）\n  - DefaultParam: 带有默认值的参数\n\n## _var_ `T_RuleChecker` {#T-RuleChecker}\n\n- **类型:** \\_DependentCallable[bool]\n\n- **说明**\n\n  RuleChecker 即判断是否响应事件的处理函数。\n\n  依赖参数:\n  - DependParam: 子依赖参数\n  - BotParam: Bot 对象\n  - EventParam: Event 对象\n  - StateParam: State 对象\n  - DefaultParam: 带有默认值的参数\n\n## _var_ `T_PermissionChecker` {#T-PermissionChecker}\n\n- **类型:** \\_DependentCallable[bool]\n\n- **说明**\n\n  PermissionChecker 即判断事件是否满足权限的处理函数。\n\n  依赖参数:\n  - DependParam: 子依赖参数\n  - BotParam: Bot 对象\n  - EventParam: Event 对象\n  - DefaultParam: 带有默认值的参数\n\n## _var_ `T_Handler` {#T-Handler}\n\n- **类型:** \\_DependentCallable[Any]\n\n- **说明:** Handler 处理函数。\n\n## _var_ `T_TypeUpdater` {#T-TypeUpdater}\n\n- **类型:** \\_DependentCallable[str]\n\n- **说明**\n\n  TypeUpdater 在 Matcher.pause, Matcher.reject 时被运行，用于更新响应的事件类型。 默认会更新为 `message`。\n\n  依赖参数:\n  - DependParam: 子依赖参数\n  - BotParam: Bot 对象\n  - EventParam: Event 对象\n  - StateParam: State 对象\n  - MatcherParam: Matcher 对象\n  - DefaultParam: 带有默认值的参数\n\n## _var_ `T_PermissionUpdater` {#T-PermissionUpdater}\n\n- **类型:** \\_DependentCallable[[Permission](permission.md#Permission)]\n\n- **说明**\n\n  PermissionUpdater 在 Matcher.pause, Matcher.reject 时被运行，用于更新会话对象权限。 默认会更新为当前事件的触发对象。\n\n  依赖参数:\n  - DependParam: 子依赖参数\n  - BotParam: Bot 对象\n  - EventParam: Event 对象\n  - StateParam: State 对象\n  - MatcherParam: Matcher 对象\n  - DefaultParam: 带有默认值的参数\n\n## _var_ `T_DependencyCache` {#T-DependencyCache}\n\n- **类型:** dict[\\_DependentCallable[Any], DependencyCache]\n\n- **说明:** 依赖缓存, 用于存储依赖函数的返回值\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/api/utils.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 8\ndescription: nonebot.utils 模块\n---\n\n# nonebot.utils\n\n本模块包含了 NoneBot 的一些工具函数\n\n## _def_ `escape_tag(s)` {#escape-tag}\n\n- **说明**\n\n  用于记录带颜色日志时转义 `<tag>` 类型特殊标签\n\n  参考: [loguru color 标签](https://loguru.readthedocs.io/en/stable/api/logger.html#color)\n\n- **参数**\n  - `s` (str): 需要转义的字符串\n\n- **返回**\n  - str\n\n## _def_ `deep_update(mapping, *updating_mappings)` {#deep-update}\n\n- **说明:** 深度更新合并字典\n\n- **参数**\n  - `mapping` (dict[K, Any])\n\n  - `*updating_mappings` (dict[K, Any])\n\n- **返回**\n  - dict[K, Any]\n\n## _def_ `lenient_issubclass(cls, class_or_tuple)` {#lenient-issubclass}\n\n- **说明:** 检查 cls 是否是 class_or_tuple 中的一个类型子类并忽略类型错误。\n\n- **参数**\n  - `cls` (Any)\n\n  - `class_or_tuple` (type[Any] | tuple[type[Any], ...])\n\n- **返回**\n  - bool\n\n## _def_ `generic_check_issubclass(cls, class_or_tuple)` {#generic-check-issubclass}\n\n- **说明**\n\n  检查 cls 是否是 class_or_tuple 中的一个类型子类。\n\n  特别的：\n  - 如果 cls 是 `typing.Union` 或 `types.UnionType` 类型，\n    则会检查其中的所有类型是否是 class_or_tuple 中一个类型的子类或 None。\n  - 如果 cls 是 `typing.Literal` 类型，\n    则会检查其中的所有值是否是 class_or_tuple 中一个类型的实例。\n  - 如果 cls 是 `typing.TypeVar` 类型，\n    则会检查其 `__bound__` 或 `__constraints__`\n    是否是 class_or_tuple 中一个类型的子类或 None。\n\n- **参数**\n  - `cls` (Any)\n\n  - `class_or_tuple` (type[Any] | tuple[type[Any], ...])\n\n- **返回**\n  - bool\n\n## _def_ `type_is_complex(type_)` {#type-is-complex}\n\n- **说明:** 检查 type\\_ 是否是复杂类型\n\n- **参数**\n  - `type_` (type[Any])\n\n- **返回**\n  - bool\n\n## _def_ `is_coroutine_callable(call)` {#is-coroutine-callable}\n\n- **说明:** 检查 call 是否是一个 callable 协程函数\n\n- **参数**\n  - `call` ((...) -> Any)\n\n- **返回**\n  - bool\n\n## _def_ `is_gen_callable(call)` {#is-gen-callable}\n\n- **说明:** 检查 call 是否是一个生成器函数\n\n- **参数**\n  - `call` ((...) -> Any)\n\n- **返回**\n  - bool\n\n## _def_ `is_async_gen_callable(call)` {#is-async-gen-callable}\n\n- **说明:** 检查 call 是否是一个异步生成器函数\n\n- **参数**\n  - `call` ((...) -> Any)\n\n- **返回**\n  - bool\n\n## _def_ `run_sync(call)` {#run-sync}\n\n- **说明:** 一个用于包装 sync function 为 async function 的装饰器\n\n- **参数**\n  - `call` ((P) -> R): 被装饰的同步函数\n\n- **返回**\n  - (P) -> Coroutine[None, None, R]\n\n## _def_ `run_sync_ctx_manager(cm)` {#run-sync-ctx-manager}\n\n- **说明:** 一个用于包装 sync context manager 为 async context manager 的执行函数\n\n- **参数**\n  - `cm` (AbstractContextManager[T])\n\n- **返回**\n  - AsyncGenerator[T, None]\n\n## _async def_ `run_coro_with_catch(coro, exc, return_on_err=None)` {#run-coro-with-catch}\n\n- **说明:** 运行协程并当遇到指定异常时返回指定值。\n\n- **重载**\n\n  **1.** `(coro, exc, return_on_err=None) -> T | None`\n  - **参数**\n    - `coro` (Coroutine[Any, Any, T])\n\n    - `exc` (tuple[type[Exception], ...])\n\n    - `return_on_err` (None)\n\n  - **返回**\n    - T | None\n\n  **2.** `(coro, exc, return_on_err) -> T | R`\n  - **参数**\n    - `coro` (Coroutine[Any, Any, T])\n\n    - `exc` (tuple[type[Exception], ...])\n\n    - `return_on_err` (R)\n\n  - **返回**\n    - T | R\n\n- **参数**\n  - `coro`: 要运行的协程\n\n  - `exc`: 要捕获的异常\n\n  - `return_on_err`: 当发生异常时返回的值\n\n- **返回**\n\n  协程的返回值或发生异常时的指定值\n\n## _async def_ `run_coro_with_shield(coro)` {#run-coro-with-shield}\n\n- **说明:** 运行协程并在取消时屏蔽取消异常。\n\n- **参数**\n  - `coro` (Coroutine[Any, Any, T]): 要运行的协程\n\n- **返回**\n  - T: 协程的返回值\n\n## _def_ `flatten_exception_group(exc_group)` {#flatten-exception-group}\n\n- **参数**\n  - `exc_group` (BaseExceptionGroup[E])\n\n- **返回**\n  - Generator[E, None, None]\n\n## _def_ `get_name(obj)` {#get-name}\n\n- **说明:** 获取对象的名称\n\n- **参数**\n  - `obj` (Any)\n\n- **返回**\n  - str\n\n## _def_ `path_to_module_name(path)` {#path-to-module-name}\n\n- **说明:** 转换路径为模块名\n\n- **参数**\n  - `path` (Path)\n\n- **返回**\n  - str\n\n## _def_ `resolve_dot_notation(obj_str, default_attr, default_prefix=None)` {#resolve-dot-notation}\n\n- **说明:** 解析并导入点分表示法的对象\n\n- **参数**\n  - `obj_str` (str)\n\n  - `default_attr` (str)\n\n  - `default_prefix` (str | None)\n\n- **返回**\n  - Any\n\n## _class_ `classproperty(func)` {#classproperty}\n\n- **说明:** 类属性装饰器\n\n- **参数**\n  - `func` ((Any) -> T)\n\n## _class_ `DataclassEncoder(<auto>)` {#DataclassEncoder}\n\n- **说明:** 可以序列化 [Message](adapters/index.md#Message)(List[Dataclass]) 的 `JSONEncoder`\n\n- **参数**\n\n  auto\n\n### _method_ `default(o)` {#DataclassEncoder-default}\n\n- **参数**\n  - `o`\n\n- **返回**\n  - untyped\n\n## _def_ `logger_wrapper(logger_name)` {#logger-wrapper}\n\n- **说明:** 用于打印 adapter 的日志。\n\n- **参数**\n  - `logger_name` (str): adapter 的名称\n\n- **返回**\n  - untyped: 日志记录函数\n\n    日志记录函数的参数:\n    - level: 日志等级\n    - message: 日志信息\n    - exception: 异常信息\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/appendices/api-calling.mdx",
    "content": "---\nsidebar_position: 4\ndescription: 使用平台接口，完成更多功能\n\noptions:\n  menu:\n    - category: appendices\n      weight: 50\n---\n\n# 使用平台接口\n\nimport Messenger from \"@/components/Messenger\";\n\n在 NoneBot 中，除了使用事件响应器操作发送文本消息外，我们还可以直接通过使用协议适配器提供的方法来使用平台特定的接口，完成发送特殊消息、获取信息等其他平台提供的功能。同时，在部分无法使用事件响应器的情况中，例如[定时任务](../best-practice/scheduler.md)，我们也可以使用平台接口来完成需要的功能。\n\n## 发送平台特殊消息\n\n在之前的章节中，我们介绍了如何向用户发送文本消息以及[如何处理平台消息](../tutorial/message.md)，现在我们来向用户发送平台特殊消息。\n\n:::caution 注意\n在以下的示例中，我们将使用 `Console` 协议适配器来演示如何发送平台消息。在实际使用中，你需要确保你使用的**消息序列类型**与你所要发送的**平台类型**一致。\n:::\n\n```python {4,7-17} title=weather/__init__.py\nimport inspect\nfrom nonebot.adapters.console import MessageSegment\n\n@weather.got(\"location\", prompt=MessageSegment.emoji(\"question\") + \"请输入地名\")\nasync def got_location(location: str = ArgPlainText()):\n    result = await weather.send(\n        MessageSegment.markdown(\n            inspect.cleandoc(\n                f\"\"\"\n                # {location}\n\n                - 今天\n\n                   ⛅ 多云 20℃~24℃\n                \"\"\"\n            )\n        )\n    )\n```\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/天气\" },\n    { position: \"left\", msg: \"❓请输入地名\" },\n    { position: \"right\", msg: \"北京\" },\n    {\n      position: \"left\",\n      monospace: true,\n      msg: \"┏━━━━━━━━━━━━━━━━┓\\n┃      北京       ┃\\n┗━━━━━━━━━━━━━━━━┛\\n• 今天\\n⛅ 多云 20℃~24℃\",\n    },\n  ]}\n/>\n\n在上面的示例中，我们使用了 `Console` 协议适配器提供的 `MessageSegment` 类来发送平台特定的消息 `emoji` 和 `markdown`。这两种消息可以显示在终端中，但是无法在其他平台上使用。在事件响应器操作中，我们可以使用 `str`、消息序列、消息段、消息模板四种类型来发送消息，但其中只有 `str` 和[纯文本形式的消息模板类型](../tutorial/message.md#使用消息模板)消息可以在所有平台上使用。\n\n`send` 事件响应器操作实际上是由协议适配器通过调用平台 API 来实现的，通常会将 API 调用的结果作为返回值返回。\n\n## 调用平台 API\n\n在 NoneBot 中，我们可以通过 `Bot` 对象来调用协议适配器支持的平台 API，来完成更多的功能。\n\n### 获取 Bot\n\n在调用平台 API 之前，我们首先要获得 Bot 对象。有两种方式可以获得 Bot 对象。\n\n在事件处理流程的上下文中，我们可以直接使用依赖注入 Bot 来获取：\n\n```python {1,4} title=weather/__init__.py\nfrom nonebot.adapters import Bot\n\n@weather.got(\"location\", prompt=\"请输入地名\")\nasync def got_location(bot: Bot, location: str = ArgPlainText()):\n    ...\n```\n\n依赖注入会确保你获得的 Bot 对象与类型注解的 Bot 类型一致。也就是说，如果你使用的是 Bot 基类，将会允许任何平台的 Bot 对象；如果你使用的是平台特定的 Bot 类型，将会只允许该平台的 Bot 对象，其他类型的 Bot 将会跳过这个事件处理函数。更多详情请参考[事件处理重载](./overload.md)。\n\n在其他情况下，我们可以通过 NoneBot 提供的方法来获取 Bot 对象，这些方法将会在[使用适配器](../advanced/adapter.md#获取-bot-对象)中详细介绍：\n\n```python {4,6}\nfrom nonebot import get_bot\n\n# 获取当前所有 Bot 中的第一个\nbot = get_bot()\n# 获取指定 ID 的 Bot\nbot = get_bot(\"bot_id\")\n```\n\n### 调用 API\n\n在获得 Bot 对象后，我们可以通过 Bot 的实例方法来调用平台 API：\n\n```python {2,5}\n# 通过 bot.api_name(**kwargs) 的方法调用 API\nresult = await bot.get_user_info(user_id=12345678)\n\n# 通过 bot.call_api(api_name, **kwargs) 的方法调用 API\nresult = await bot.call_api(\"get_user_info\", user_id=12345678)\n```\n\n:::caution 注意\n实际可以使用的 API 以及参数取决于平台提供的接口以及协议适配器的实现，请参考协议适配器以及平台文档。\n:::\n\n在了解了如何调用 API 后，我们可以来改进 `weather` 插件，使得消息发送后，调用 `Console` 接口响铃提醒机器人用户：\n\n```python {4,18} title=weather/__init__.py\nfrom nonebot.adapters.console import Bot, MessageSegment\n\n@weather.got(\"location\", prompt=MessageSegment.emoji(\"question\") + \"请输入地名\")\nasync def got_location(bot: Bot, location: str = ArgPlainText()):\n    await weather.send(\n        MessageSegment.markdown(\n            inspect.cleandoc(\n                f\"\"\"\n                # {location}\n\n                - 今天\n\n                   ⛅ 多云 20℃~24℃\n                \"\"\"\n            )\n        )\n    )\n    await bot.bell()\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/appendices/config.mdx",
    "content": "---\nsidebar_position: 0\ndescription: 读取用户配置来控制插件行为\n\noptions:\n  menu:\n    - category: appendices\n      weight: 10\n---\n\n# 配置\n\nimport Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\n\n配置是项目中非常重要的一部分，为了方便我们控制机器人的行为，NoneBot 提供了一套配置系统。下面我们将会补充[指南](../quick-start.mdx)中的天气插件，使其能够读取用户配置。在这之前，我们需要先了解一下配置系统，如果你已经了解了 NoneBot 中的配置方法，可以跳转到[编写插件配置](#插件配置)。\n\nNoneBot 使用 [`pydantic`](https://docs.pydantic.dev/) 以及 [`python-dotenv`](https://saurabh-kumar.com/python-dotenv/) 来读取 dotenv 配置文件以及环境变量，从而控制机器人行为。配置文件需要符合 dotenv 格式，复杂数据类型需使用 JSON 格式或 [pydantic 支持格式](https://docs.pydantic.dev/usage/types/)填写。\n\nNoneBot 内置的配置项列表及含义可以在[内置配置项](#内置配置项)中查看。\n\n:::caution 注意\n\nNoneBot 自 2.2.0 起兼容了 Pydantic v1 与 v2 版本，以下文档中 Pydantic 相关示例均采用 v2 版本用法。\n\n如果在使用商店或其他第三方插件的过程中遇到 Pydantic 相关警告或报错，例如：\n\n```python\npydantic_core._pydantic_core.ValidationError: 1 validation error for Config\n  Input should be a valid dictionary or instance of Config [type=model_type, input_value=Config(...), input_type=Config]\n```\n\n请考虑降级 Pydantic 至 v1 版本：\n\n```bash\npip install --force-reinstall 'pydantic~=1.10'\n```\n\n:::\n\n## 配置项的加载\n\n在 NoneBot 中，我们可以把配置途径分为 **直接传入**、**系统环境变量**、**dotenv 配置文件** 三种，其加载优先级依次由高到低。\n\n### 直接传入\n\n在 NoneBot 初始化的过程中，可以通过 `nonebot.init()` 传入任意合法的 Python 变量，也可以在初始化完成后直接赋值。\n\n通常，在初始化前的传参会在机器人的入口文件（如 `bot.py`）中进行，而初始化后的赋值可以在任何地方进行。\n\n```python {4,8,9} title=bot.py\nimport nonebot\n\n# 初始化时\nnonebot.init(custom_config1=\"config on init\")\n\n# 初始化后\nconfig = nonebot.get_driver().config\nconfig.custom_config1 = \"changed after init\"\nconfig.custom_config2 = \"new config after init\"\n```\n\n### 系统环境变量\n\n在 dotenv 配置文件中定义的配置项，也会在环境变量中进行寻找。如果在环境变量中发现同名配置项（大小写不敏感），将会覆盖 dotenv 中所填值。\n\n例如，在 dotenv 配置文件中存在配置项 `custom_config`：\n\n```dotenv\nCUSTOM_CONFIG=config in dotenv\n```\n\n同时，设置环境变量：\n\n```bash\n# windows cmd\nset CUSTOM_CONFIG 'config in environment variables'\n# windows powershell\n$Env:CUSTOM_CONFIG='config in environment variables'\n# linux/macOS\nexport CUSTOM_CONFIG='config in environment variables'\n```\n\n那最终 NoneBot 所读取的内容为环境变量中的内容，即 `config in environment variables`。\n\n:::caution 注意\nNoneBot 不会自发读取未被定义的配置项的环境变量，如果需要读取某一环境变量需要在 dotenv 配置文件中进行声明。\n:::\n\n### dotenv 配置文件\n\ndotenv 是一种便捷的跨平台配置通用模式，也是我们推荐的配置方式。\n\nNoneBot 在启动时将会从系统环境变量或者 `.env` 文件中寻找配置项 `ENVIRONMENT` （大小写不敏感），默认值为 `prod`。这将决定 NoneBot 后续进一步加载环境配置的文件路径 `.env.{ENVIRONMENT}`。\n\n#### 配置项解析\n\ndotenv 文件中的配置值使用 JSON 进行解析。如果配置项值无法被解析，将作为**字符串**处理。例如：\n\n```dotenv\nSTRING_CONFIG=some string\nLIST_CONFIG=[1, 2, 3]\nDICT_CONFIG={\"key\": \"value\"}\nMULTILINE_CONFIG='\n[\n  {\n    \"item_key\": \"item_value\"\n  }\n]\n'\nEMPTY_CONFIG=\nNULL_CONFIG\n```\n\n将被解析为：\n\n```python\ndotenv_config = {\n    \"string_config\": \"some string\",\n    \"list_config\": [1, 2, 3],\n    \"dict_config\": {\"key\": \"value\"},\n    \"multiline_config\": [{\"item_key\": \"item_value\"}],\n    \"empty_config\": \"\",\n    \"null_config\": None\n}\n```\n\n特别的，NoneBot 支持使用 `env_nested_delimiter` 配置嵌套字典，在层与层之间使用 `__` 分隔即可：\n\n```dotenv\nDICT={\"k1\": \"v1\", \"k2\": null}\nDICT__K2=v2\nDICT__K3=v3\nDICT__INNER__K4=v4\n```\n\n将被解析为：\n\n```python\ndotenv_config = {\n    \"dict\": {\n        \"k1\": \"v1\",\n        \"k2\": \"v2\",\n        \"k3\": \"v3\",\n        \"inner\": {\n            \"k4\": \"v4\"\n        }\n    }\n}\n```\n\n#### .env 文件\n\n`.env` 文件是基础配置文件，该文件中的配置项在不同环境下都会被加载，但会被 `.env.{ENVIRONMENT}` 文件中的配置所**覆盖**。\n\n我们可以在 `.env` 文件中写入当前的环境信息：\n\n```dotenv\nENVIRONMENT=dev\nCOMMON_CONFIG=common config  # 这个配置项在任何环境中都会被加载\n```\n\n这样，我们在启动 NoneBot 时就会从 `.env.dev` 文件中加载剩余配置项。\n\n:::tip 提示\n在生产环境中，可以通过设置环境变量 `ENVIRONMENT=prod` 来确保 NoneBot 读取正确的环境配置。\n:::\n\n#### .env.\\{ENVIRONMENT\\} 文件\n\n`.env.{ENVIRONMENT}` 文件类似于预设，可以让我们在多套不同的配置方案中灵活切换，默认 NoneBot 会读取 `.env.prod` 配置。如果你使用了 `nb-cli` 创建 `simple` 项目，那么将含有两套预设配置：`.env.dev` 和 `.env.prod`。\n\n在 NoneBot 初始化时，可以指定加载某个环境配置文件：\n\n```python\nnonebot.init(_env_file=\".env.dev\")\n```\n\n这将忽略在 `.env` 文件或环境变量中指定的 `ENVIRONMENT` 配置项。\n\n## 读取全局配置项\n\nNoneBot 的全局配置对象可以通过 `driver` 获取，如：\n\n```python\nimport nonebot\n\nconfig = nonebot.get_driver().config\n```\n\n如果我们需要获取某个配置项，可以直接通过 `config` 对象的属性访问：\n\n```python\nsuperusers = config.superusers\n```\n\n如果配置项不存在，将会抛出异常。\n\n## 插件配置\n\n在一个涉及大量配置项的项目中，通过直接读取全局配置项的方式显然并不高效。同时，由于额外的全局配置项没有预先定义，开发时编辑器将无法提示字段与类型，并且运行时没有对配置项直接进行合法性检查。那么就需要一种方式来规范定义插件配置项。\n\n在 NoneBot 中，我们使用强大高效的 `pydantic` 来定义配置模型，这个模型可以被用于配置的读取和类型检查等。例如在 `weather` 插件目录中新建 `config.py` 来定义一个模型：\n\n```python title=weather/config.py\nfrom pydantic import BaseModel, field_validator\n\nclass Config(BaseModel):\n    weather_api_key: str\n    weather_command_priority: int = 10\n    weather_plugin_enabled: bool = True\n\n    @field_validator(\"weather_command_priority\")\n    @classmethod\n    def check_priority(cls, v: int) -> int:\n        if v >= 1:\n            return v\n        raise ValueError(\"weather command priority must greater than 1\")\n```\n\n在 `config.py` 中，我们定义了一个 `Config` 类，它继承自 `pydantic.BaseModel`，并定义了一些配置项。在 `Config` 类中，我们还定义了一个 `check_priority` 方法，它用于检查 `weather_command_priority` 配置项的合法性。更多关于 `pydantic` 的编写方式，可以参考 [pydantic 官方文档](https://docs.pydantic.dev/)。\n\n在定义好配置模型后，我们可以在插件加载时通过配置模型获取插件配置：\n\n```python {5,11} title=weather/__init__.py\nfrom nonebot import get_plugin_config\n\nfrom .config import Config\n\nplugin_config = get_plugin_config(Config)\n\nweather = on_command(\n    \"天气\",\n    rule=to_me(),\n    aliases={\"weather\", \"查天气\"},\n    priority=plugin_config.weather_command_priority,\n    block=True,\n)\n```\n\n然后，我们便可以从 `plugin_config` 中读取配置了，例如 `plugin_config.weather_api_key`。\n\n这种方式可以简洁、高效地读取配置项，同时也可以设置默认值或者在运行时对配置项进行合法性检查，防止由于配置项导致的插件出错等情况出现。\n\n:::tip 提示\n发布插件应该为自身的事件响应器提供可配置的优先级，以便插件使用者可以自定义多个插件间的响应顺序。\n:::\n\n由于插件配置项是从全局配置中读取的，通常我们需要在配置项名称前面添加前缀名，以防止配置项冲突。例如在上方的示例中，我们就添加了配置项前缀 `weather_`。但是这样会导致在使用配置项时过长的变量名，因此我们可以使用 `pydantic` 的 `alias` 或者通过配置 scope 来简化配置项名称。这里我们以 scope 配置为例：\n\n```python title=weather/config.py\nfrom pydantic import BaseModel\n\nclass ScopedConfig(BaseModel):\n    api_key: str\n    command_priority: int = 10\n    plugin_enabled: bool = True\n\nclass Config(BaseModel):\n    weather: ScopedConfig\n```\n\n```python title=weather/__init__.py\nfrom nonebot import get_plugin_config\n\nfrom .config import Config\n\nplugin_config = get_plugin_config(Config).weather\n```\n\n这样我们就可以省略插件配置项名称中的前缀 `weather_` 了。但需要注意的是，如果我们使用了 scope 配置，那么在配置文件中也需要使用 [`env_nested_delimiter` 格式](#配置项解析)，例如：\n\n```dotenv\nWEATHER__API_KEY=123456\nWEATHER__COMMAND_PRIORITY=10\n```\n\n## 内置配置项\n\n配置项 API 文档可以前往 [Config 类](../api/config.md#Config)查看。\n\n### Driver\n\n- **类型**: `str`\n- **默认值**: `\"~fastapi\"`\n\nNoneBot 运行所使用的驱动器。具体配置方法可以参考[安装驱动器](../tutorial/store.mdx#安装驱动器)和[选择驱动器](../advanced/driver.md)。\n\n<Tabs groupId=\"configMethod\">\n  <TabItem value=\"dotenv\" label=\"dotenv\" default>\n\n```dotenv\nDRIVER=~fastapi+~httpx+~websockets\n```\n\n  </TabItem>\n  <TabItem value=\"env\" label=\"系统环境变量\">\n\n```bash\n# windows cmd\nset DRIVER '~fastapi+~httpx+~websockets'\n# windows powershell\n$Env:DRIVER='~fastapi+~httpx+~websockets'\n# linux/macOS\nexport DRIVER='~fastapi+~httpx+~websockets'\n```\n\n  </TabItem>\n  <TabItem value=\"init\" label=\"直接传入\">\n\n```python title=bot.py\nimport nonebot\n\nnonebot.init(driver=\"~fastapi+~httpx+~websockets\")\n```\n\n  </TabItem>\n</Tabs>\n\n### Host\n\n- **类型**: `IPvAnyAddress`\n- **默认值**: `127.0.0.1`\n\n当 NoneBot 作为服务端时，监听的 IP / 主机名。\n\n<Tabs groupId=\"configMethod\">\n  <TabItem value=\"dotenv\" label=\"dotenv\" default>\n\n```dotenv\nHOST=127.0.0.1\n```\n\n  </TabItem>\n  <TabItem value=\"env\" label=\"系统环境变量\">\n\n```bash\n# windows cmd\nset HOST '127.0.0.1'\n# windows powershell\n$Env:HOST='127.0.0.1'\n# linux/macOS\nexport HOST='127.0.0.1'\n```\n\n  </TabItem>\n  <TabItem value=\"init\" label=\"直接传入\">\n\n```python title=bot.py\nimport nonebot\n\nnonebot.init(host=\"127.0.0.1\")\n```\n\n  </TabItem>\n</Tabs>\n\n### Port\n\n- **类型**: `int` (1 ~ 65535)\n- **默认值**: `8080`\n\n当 NoneBot 作为服务端时，监听的端口。\n\n<Tabs groupId=\"configMethod\">\n  <TabItem value=\"dotenv\" label=\"dotenv\" default>\n\n```dotenv\nPORT=8080\n```\n\n  </TabItem>\n  <TabItem value=\"env\" label=\"系统环境变量\">\n\n```bash\n# windows cmd\nset PORT '8080'\n# windows powershell\n$Env:PORT='8080'\n# linux/macOS\nexport PORT='8080'\n```\n\n  </TabItem>\n  <TabItem value=\"init\" label=\"直接传入\">\n\n```python title=bot.py\nimport nonebot\n\nnonebot.init(port=8080)\n```\n\n  </TabItem>\n</Tabs>\n\n### Log Level\n\n- **类型**: `int | str`\n- **默认值**: `INFO`\n\nNoneBot 日志输出等级，可以为 `int` 类型等级或等级名称。具体等级对照表参考 [loguru 日志等级](https://loguru.readthedocs.io/en/stable/api/logger.html#levels)。\n\n:::tip 提示\n日志等级名称应为大写，如 `INFO`。\n:::\n\n<Tabs groupId=\"configMethod\">\n  <TabItem value=\"dotenv\" label=\"dotenv\" default>\n\n```dotenv\nLOG_LEVEL=DEBUG\n```\n\n  </TabItem>\n  <TabItem value=\"env\" label=\"系统环境变量\">\n\n```bash\n# windows cmd\nset LOG_LEVEL 'DEBUG'\n# windows powershell\n$Env:LOG_LEVEL='DEBUG'\n# linux/macOS\nexport LOG_LEVEL='DEBUG'\n```\n\n  </TabItem>\n  <TabItem value=\"init\" label=\"直接传入\">\n\n```python title=bot.py\nimport nonebot\n\nnonebot.init(log_level=\"DEBUG\")\n```\n\n  </TabItem>\n</Tabs>\n\n### API Timeout\n\n- **类型**: `float | None`\n- **默认值**: `30.0`\n\n调用平台接口的超时时间，单位为秒。`None` 表示不设置超时时间。\n\n<Tabs groupId=\"configMethod\">\n  <TabItem value=\"dotenv\" label=\"dotenv\" default>\n\n```dotenv\nAPI_TIMEOUT=10.0\n```\n\n  </TabItem>\n  <TabItem value=\"env\" label=\"系统环境变量\">\n\n```bash\n# windows cmd\nset API_TIMEOUT '10.0'\n# windows powershell\n$Env:API_TIMEOUT='10.0'\n# linux/macOS\nexport API_TIMEOUT='10.0'\n```\n\n  </TabItem>\n  <TabItem value=\"init\" label=\"直接传入\">\n\n```python title=bot.py\nimport nonebot\n\nnonebot.init(api_timeout=10.0)\n```\n\n  </TabItem>\n</Tabs>\n\n### SuperUsers\n\n- **类型**: `set[str]`\n- **默认值**: `set()`\n\n机器人超级用户，可以使用权限 [`SUPERUSER`](../api/permission.md#SUPERUSER)。\n\n<Tabs groupId=\"configMethod\">\n  <TabItem value=\"dotenv\" label=\"dotenv\" default>\n\n```dotenv\nSUPERUSERS=[\"123123123\"]\n```\n\n  </TabItem>\n  <TabItem value=\"env\" label=\"系统环境变量\">\n\n```bash\n# windows cmd\nset SUPERUSERS '[\"123123123\"]'\n# windows powershell\n$Env:SUPERUSERS='[\"123123123\"]'\n# linux/macOS\nexport SUPERUSERS='[\"123123123\"]'\n```\n\n  </TabItem>\n  <TabItem value=\"init\" label=\"直接传入\">\n\n```python title=bot.py\nimport nonebot\n\nnonebot.init(superusers={\"123123123\"})\n```\n\n  </TabItem>\n</Tabs>\n\n### Nickname\n\n- **类型**: `set[str]`\n- **默认值**: `set()`\n\n机器人昵称，通常协议适配器会根据用户是否 @bot 或者是否以机器人昵称开头来判断是否是向机器人发送的消息。\n\n<Tabs groupId=\"configMethod\">\n  <TabItem value=\"dotenv\" label=\"dotenv\" default>\n\n```dotenv\nNICKNAME=[\"bot\"]\n```\n\n  </TabItem>\n  <TabItem value=\"env\" label=\"系统环境变量\">\n\n```bash\n# windows cmd\nset NICKNAME '[\"bot\"]'\n# windows powershell\n$Env:NICKNAME='[\"bot\"]'\n# linux/macOS\nexport NICKNAME='[\"bot\"]'\n```\n\n  </TabItem>\n  <TabItem value=\"init\" label=\"直接传入\">\n\n```python title=bot.py\nimport nonebot\n\nnonebot.init(nickname={\"bot\"})\n```\n\n  </TabItem>\n</Tabs>\n\n### Command Start 和 Command Separator\n\n- **类型**: `set[str]`\n- **默认值**:\n  - Command Start: `{\"/\"}`\n  - Command Separator: `{\".\"}`\n\n命令消息的起始符和分隔符。用于 [`command`](../advanced/matcher.md#command) 规则。\n\n<Tabs groupId=\"configMethod\">\n  <TabItem value=\"dotenv\" label=\"dotenv\" default>\n\n```dotenv\nCOMMAND_START=[\"/\", \"\"]\nCOMMAND_SEP=[\".\", \" \"]\n```\n\n  </TabItem>\n  <TabItem value=\"env\" label=\"系统环境变量\">\n\n```bash\n# windows cmd\nset COMMAND_START '[\"/\", \"\"]'\nset COMMAND_SEP '[\".\", \" \"]'\n# windows powershell\n$Env:COMMAND_START='[\"/\", \"\"]'\n$Env:COMMAND_SEP='[\".\", \" \"]'\n# linux/macOS\nexport COMMAND_START='[\"/\", \"\"]'\nexport COMMAND_SEP='[\".\", \" \"]'\n```\n\n  </TabItem>\n  <TabItem value=\"init\" label=\"直接传入\">\n\n```python title=bot.py\nimport nonebot\n\nnonebot.init(command_start={\"/\", \"\"}, command_sep={\".\", \" \"})\n```\n\n  </TabItem>\n</Tabs>\n\n### Session Expire Timeout\n\n- **类型**: `timedelta`\n- **默认值**: `timedelta(minutes=2)`\n\n用户会话超时时间，配置格式参考 [Datetime Types](https://docs.pydantic.dev/latest/api/standard_library_types/#datetimetimedelta)。\n\n<Tabs groupId=\"configMethod\">\n  <TabItem value=\"dotenv\" label=\"dotenv\" default>\n\n```dotenv\nSESSION_EXPIRE_TIMEOUT=00:02:00\n```\n\n  </TabItem>\n  <TabItem value=\"env\" label=\"系统环境变量\">\n\n```bash\n# windows cmd\nset SESSION_EXPIRE_TIMEOUT '00:02:00'\n# windows powershell\n$Env:SESSION_EXPIRE_TIMEOUT='00:02:00'\n# linux/macOS\nexport SESSION_EXPIRE_TIMEOUT='00:02:00'\n```\n\n  </TabItem>\n  <TabItem value=\"init\" label=\"直接传入\">\n\n```python title=bot.py\nimport nonebot\n\nnonebot.init(session_expire_timeout=120)\n```\n\n  </TabItem>\n</Tabs>\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/appendices/log.md",
    "content": "---\nsidebar_position: 6\ndescription: 记录与控制日志\n\noptions:\n  menu:\n    - category: appendices\n      weight: 70\n---\n\n# 日志\n\n无论是在开发还是在生产环境中，日志都是一个重要的功能，可以帮助我们了解运行状况、排查问题等。虽然我们可以使用 `print` 来将需要的信息输出到控制台，但是这种方式难以控制，而且不利于日志的归档、分析等。NoneBot 使用优秀的 [Loguru](https://loguru.readthedocs.io/) 库来进行日志记录。\n\n## 记录日志\n\n我们可以从 NoneBot 中导入 `logger` 对象，然后使用 `logger` 对象的方法来记录日志。\n\n```python\nfrom nonebot import logger\n\nlogger.trace(\"This is a trace message\")\nlogger.debug(\"This is a debug message\")\nlogger.info(\"This is an info message\")\nlogger.success(\"This is a success message\")\nlogger.warning(\"This is a warning message\")\nlogger.error(\"This is an error message\")\nlogger.critical(\"This is a critical message\")\n```\n\n我们仅需一行代码即可记录对应级别的日志。日志可以通过配置 [`LOG_LEVEL` 配置项](./config.mdx#log-level)来过滤输出等级，控制台中仅会输出大于等于 `LOG_LEVEL` 的日志。默认的 `LOG_LEVEL` 为 `INFO`，即只会输出 `INFO`、`SUCCESS`、`WARNING`、`ERROR`、`CRITICAL` 级别的日志。\n\n如果需要记录 `Exception traceback` 日志，可以向 `logger` 添加 `exception` 选项：\n\n```python {4}\ntry:\n    1 / 0\nexcept ZeroDivisionError:\n    logger.opt(exception=True).error(\"ZeroDivisionError\")\n```\n\n如果需要输出彩色日志，可以向 `logger` 添加 `colors` 选项：\n\n```python\nlogger.opt(colors=True).warning(\"We got a <red>BIG</red> problem\")\n```\n\n更多日志记录方法请参考 [Loguru 文档](https://loguru.readthedocs.io/)。\n\n## 自定义日志输出\n\nNoneBot 在启动时会添加一个默认的日志处理器，该处理器会将日志输出到**stdout**，并且根据 `LOG_LEVEL` 配置项过滤日志等级。\n\n默认的日志格式为：\n\n```text\n<g>{time:MM-DD HH:mm:ss}</g> [<lvl>{level}</lvl>] <c><u>{name}</u></c> | {message}\n```\n\n我们可以从 `nonebot.log` 模块导入以使用 NoneBot 的默认格式和过滤器：\n\n```python\nfrom nonebot.log import default_format, default_filter\n```\n\n如果需要自定义日志格式，我们需要移除 NoneBot 默认的日志处理器并添加新的日志处理器。例如，在机器人入口文件中 `nonebot.init` 之前添加以下内容：\n\n```python title=bot.py\nfrom nonebot.log import logger_id\n\n# 移除 NoneBot 默认的日志处理器\nlogger.remove(logger_id)\n# 添加新的日志处理器\nlogger.add(\n    sys.stdout,\n    level=0,\n    diagnose=True,\n    format=\"<g>{time:MM-DD HH:mm:ss}</g> [<lvl>{level}</lvl>] <c><u>{name}</u></c> | {message}\",\n    filter=default_filter\n)\n```\n\n如果想要输出日志到文件，我们可以使用 `logger.add` 方法添加文件处理器：\n\n```python title=bot.py\nlogger.add(\"error.log\", level=\"ERROR\", format=default_format, rotation=\"1 week\")\n```\n\n更多日志处理器的使用方法请参考 [Loguru 文档](https://loguru.readthedocs.io/)。\n\n## 重定向 logging 日志\n\n`logging` 是 Python 标准库中的日志模块，NoneBot 提供了一个 logging handler 用于将 `logging` 日志重定向到 `loguru` 处理。\n\n```python\nfrom nonebot.log import LoguruHandler\n\n# root logger 添加 LoguruHandler\nlogging.basicConfig(handlers=[LoguruHandler()])\n# 或者为其他 logging.Logger 添加 LoguruHandler\nlogger.addHandler(LoguruHandler())\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/appendices/overload.md",
    "content": "---\nsidebar_position: 7\ndescription: 根据事件类型进行不同的处理\n\noptions:\n  menu:\n    - category: appendices\n      weight: 80\n---\n\n# 事件类型与重载\n\n在之前的示例中，我们已经了解了如何[获取事件信息](../tutorial/event-data.mdx)以及[使用平台接口](./api-calling.mdx)。但是，事件信息通常不仅仅包含消息这一个内容，还有其他平台提供的信息，例如消息发送时间、消息发送者等等。同时，在使用平台接口时，我们需要确保使用的**平台接口**与所要发送的**平台类型**一致，对不同类型的事件需要做出不同的处理。在本章节中，我们将介绍如何获取事件更多的信息以及根据事件类型进行不同的处理。\n\n## 事件类型\n\n在 NoneBot 中，事件均是 `nonebot.adapters.Event` 基类的子类型，基类对一些必要的属性进行了抽象，子类型则根据不同的平台进行了实现。在[自定义权限](./permission.mdx#自定义权限)一节中，我们就使用了 `Event` 的抽象方法 `get_user_id` 来获取事件发送者 ID，这个方法由协议适配器进行了实现，返回机器人用户对应的平台 ID。更多的基类抽象方法可以在[使用适配器](../advanced/adapter.md#获取事件通用信息)中查看。\n\n既然事件是基类的子类型，我们实际可以获得的信息通常多于基类抽象方法所提供的。如果我们不满足于基类能获得的信息，我们可以小小的修改一下事件处理函数的事件参数类型注解，使其变为子类型，这样我们就可以通过协议适配器定义的子类型来获取更多的信息。我们以 `Console` 协议适配器为例：\n\n```python {4} title=weather/__init__.py\nfrom nonebot.adapters.console import MessageEvent\n\n@weather.got(\"location\", prompt=\"请输入地名\")\nasync def got_location(event: MessageEvent, location: str = ArgPlainText()):\n    await weather.finish(f\"{event.time.strftime('%Y-%m-%d')} {location} 的天气是...\")\n```\n\n在上面的代码中，我们获取了 `Console` 协议适配器的消息事件提供的发送时间 `time` 属性。\n\n:::caution 注意\n如果**基类**就能满足你的需求，那么就**不要修改**事件参数类型注解，这样可以使你的代码更加**通用**，可以在更多平台上运行。如何根据不同平台事件类型进行不同的处理，我们将在[重载](#重载)一节中介绍。\n:::\n\n## 重载\n\n我们在编写机器人时，常常会遇到这样一个问题：如何对私聊和群聊消息进行不同的处理？如何对不同平台的事件进行不同的处理？针对这些问题，NoneBot 提供了一个便捷而高效的解决方案 ── 重载。简单来说，依赖函数会根据其参数的类型注解来决定是否执行，忽略不符合其参数类型注解的情况。这样，我们就可以通过修改事件参数类型注解来实现对不同事件的处理，或者修改 `Bot` 参数类型注解来实现使用不同平台的接口。我们以 `OneBot` 协议适配器为例：\n\n```python {4,8}\nfrom nonebot.adapters.onebot.v11 import PrivateMessageEvent, GroupMessageEvent\n\n@matcher.handle()\nasync def handle_private(event: PrivateMessageEvent):\n    await matcher.finish(\"私聊消息\")\n\n@matcher.handle()\nasync def handle_group(event: GroupMessageEvent):\n    await matcher.finish(\"群聊消息\")\n```\n\n这样，机器人用户就会在私聊和群聊中分别收到不同的回复。同样的，我们也可以通过修改 `Bot` 参数类型注解来实现使用不同平台的接口：\n\n```python\nfrom nonebot.adapters.console import Bot as ConsoleBot\nfrom nonebot.adapters.onebot.v11 import Bot as OneBot\n\n@matcher.handle()\nasync def handle_console(bot: ConsoleBot):\n    await bot.bell()\n\n@matcher.handle()\nasync def handle_onebot(bot: OneBot):\n    await bot.send_group_message(group_id=123123, message=\"OneBot\")\n```\n\n:::caution 注意\n重载机制对所有的参数类型注解都有效，因此，依赖注入也可以使用这个特性来对不同的返回值进行处理。\n\n但 Bot、Event 和 Matcher 三者的参数类型注解具有最高检查优先级，如果三者任一类型注解不匹配，那么其他依赖注入将不会执行（如：`Depends`）。\n:::\n\n:::tip 提示\n如何更好地编写一个跨平台的插件，我们将在[最佳实践](../best-practice/multi-adapter.mdx)中介绍。\n:::\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/appendices/permission.mdx",
    "content": "---\nsidebar_position: 5\ndescription: 控制事件响应器的权限\n\noptions:\n  menu:\n    - category: appendices\n      weight: 60\n---\n\n# 权限控制\n\nimport Messenger from \"@site/src/components/Messenger\";\n\n**权限控制**是机器人在实际应用中需要解决的重点问题之一，NoneBot 提供了灵活的权限控制机制 —— `Permission`。\n\n类似于响应规则 `Rule`，`Permission` 是由非负整数个 `PermissionChecker` 所共同组成的**用于筛选事件**的对象。但需要特别说明的是，权限和响应规则有如下区别：\n\n1. 权限检查**先于**响应规则检查\n2. `Permission` 只需**其中一个** `PermissionChecker` 返回 `True` 时就会检查通过\n3. 权限检查进行时，上下文中并不存在会话状态 `state`\n4. `Rule` 仅在**初次触发**事件响应器时进行检查，在余下的会话中并不会限制事件；而 `Permission` 会**持续生效**，在连续对话中一直对事件主体加以限制。\n\n## 基础使用\n\n通常情况下，`Permission` 更侧重于对于**触发事件的机器人用户**的筛选，例如由 NoneBot 自身提供的 `SUPERUSER` 权限，便是筛选出会话发起者是否为超级用户。它可以对输入的用户进行鉴别，如果符合要求则会被认为通过并返回 `True`，反之则返回 `False`。\n\n简单来说，`Permission` 是一个用于筛选出符合要求的用户的机制，可以通过 `Permission` 精确的控制响应对象的覆盖范围，从而拒绝掉我们所不希望的事件。\n\n例如，我们可以在 `weather` 插件中添加一个超级用户可用的指令：\n\n```python {3,9} title=weather/__init__.py\nfrom typing import Tuple\nfrom nonebot.params import Command\nfrom nonebot.permission import SUPERUSER\n\nmanage = on_command(\n    (\"天气\", \"启用\"),\n    rule=to_me(),\n    aliases={(\"天气\", \"禁用\")},\n    permission=SUPERUSER,\n)\n\n@manage.handle()\nasync def control(cmd: Tuple[str, str] = Command()):\n    _, action = cmd\n    if action == \"启用\":\n        plugin_config.weather_plugin_enabled = True\n    elif action == \"禁用\":\n        plugin_config.weather_plugin_enabled = False\n    await manage.finish(f\"天气插件已{action}\")\n```\n\n如上方示例所示，在注册事件响应器时，我们设置了 `permission` 参数，那么这个事件处理器在触发事件前的检查阶段会对用户身份进行验证，如果不符合我们设置的条件（此处即为**超级用户**）则不会响应。此时，我们向机器人发送 `/天气.禁用` 指令，机器人不会有任何响应，因为我们还不是机器人的超级管理员。我们在 dotenv 文件中设置了 `SUPERUSERS` 配置项之后，机器人就会响应我们的指令了。\n\n```dotenv title=.env\nSUPERUSERS=[\"console_user\"]\n```\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/天气.禁用\" },\n    { position: \"left\", msg: \"天气插件已禁用\" },\n    { position: \"right\", msg: \"/天气.启用\" },\n    { position: \"left\", msg: \"天气插件已启用\" },\n  ]}\n/>\n\n## 自定义权限\n\n与事件响应规则类似，`PermissionChecker` 也是一个返回值为 `bool` 类型的依赖函数，即 `PermissionChecker` 支持依赖注入。例如，我们可以限制用户的指令调用次数：\n\n```python title=weather/__init__.py\nfrom nonebot.adapters import Event\n\nfake_db: Dict[str, int] = {}\n\nasync def limit_permission(event: Event):\n    count = fake_db.setdefault(event.get_user_id(), 100)\n    if count > 0:\n        fake_db[event.get_user_id()] -= 1\n        return True\n    return False\n\nweather = on_command(\"天气\", permission=limit_permission)\n```\n\n## 权限组合\n\n权限之间可以通过 `|` 运算符进行组合，使得任意一个权限检查返回 `True` 时通过。例如：\n\n```python {4-6}\nperm1 = Permission(foo_checker)\nperm2 = Permission(bar_checker)\n\nperm = perm1 | perm2\nperm = perm1 | bar_checker\nperm = foo_checker | perm2\n```\n\n同样的，我们也无需担心组合了一个 `None` 值，`Permission` 会自动忽略 `None` 值。\n\n```python\nassert (perm | None) is perm\n```\n\n## 主动使用权限\n\n除了在事件响应器中使用权限外，我们也可以主动使用权限来判断事件是否符合条件。例如：\n\n```python {3}\nperm = Permission(some_checker)\n\nresult: bool = await perm(bot, event)\n```\n\n我们只需要传入 `Bot` 实例、事件，`Permission` 会并发调用所有 `PermissionChecker` 进行检查，并返回结果。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/appendices/rule.md",
    "content": "---\nsidebar_position: 1\ndescription: 自定义响应规则\n\noptions:\n  menu:\n    - category: appendices\n      weight: 20\n---\n\n# 响应规则\n\n机器人在实际应用中，往往会接收到多种多样的事件类型，NoneBot 通过响应规则来控制事件的处理。\n\n在[指南](../tutorial/matcher.md#为事件响应器添加参数)中，我们为 `weather` 命令添加了一个 `rule=to_me()` 参数，这个参数就是一个响应规则，确保只有在私聊或者 `@bot` 时才会响应。\n\n响应规则是一个 `Rule` 对象，它由一系列的 `RuleChecker` 函数组成，每个 `RuleChecker` 函数都会检查事件是否符合条件，如果所有的检查都通过，则事件会被处理。\n\n## RuleChecker\n\n`RuleChecker` 是一个返回值为 `bool` 类型的依赖函数，即 `RuleChecker` 支持依赖注入。我们可以根据上一节中添加的[配置项](./config.mdx#插件配置)，在 `weather` 插件目录中编写一个响应规则：\n\n```python {7,8} title=weather/__init__.py\nfrom nonebot import get_plugin_config\n\nfrom .config import Config\n\nplugin_config = get_plugin_config(Config)\n\nasync def is_enable() -> bool:\n    return plugin_config.weather_plugin_enabled\n\nweather = on_command(\"天气\", rule=is_enable)\n```\n\n在上面的代码中，我们定义了一个函数 `is_enable`，它会检查配置项 `weather_plugin_enabled` 是否为 `True`。这个函数 `is_enable` 即为一个 `RuleChecker`。\n\n## Rule\n\n`Rule` 是若干个 `RuleChecker` 的集合，它会并发调用每个 `RuleChecker`，只有当所有 `RuleChecker` 检查通过时匹配成功。例如：我们可以组合两个 `RuleChecker`，一个用于检查插件是否启用，一个用于检查用户是否在黑名单中：\n\n```python {10}\nfrom nonebot.rule import Rule\nfrom nonebot.adapters import Event\n\nasync def is_enable() -> bool:\n    return plugin_config.weather_plugin_enabled\n\nasync def is_blacklisted(event: Event) -> bool:\n    return event.get_user_id() not in BLACKLIST\n\nrule = Rule(is_enable, is_blacklisted)\n\nweather = on_command(\"天气\", rule=rule)\n```\n\n## 合并响应规则\n\n在定义响应规则时，我们可以将规则进行细分，来更好地复用规则。而在使用时，我们需要合并多个规则。除了使用 `Rule` 对象来组合多个 `RuleChecker` 外，我们还可以对 `Rule` 对象进行合并。在原 `weather` 插件中，我们可以将 `rule=to_me()` 与 `rule=is_enable` 使用 `&` 运算符合并：\n\n```python {13} title=weather/__init__.py\nfrom nonebot.rule import to_me\nfrom nonebot import get_plugin_config\n\nfrom .config import Config\n\nplugin_config = get_plugin_config(Config)\n\nasync def is_enable() -> bool:\n    return plugin_config.weather_plugin_enabled\n\nweather = on_command(\n    \"天气\",\n    rule=to_me() & is_enable,\n    aliases={\"weather\", \"查天气\"},\n    priority=plugin_config.weather_command_priority,\n    block=True,\n)\n```\n\n这样，`weather` 命令就只会在插件启用且在私聊或者 `@bot` 时才会响应。\n\n合并响应规则可以有多种形式，例如：\n\n```python {4-6}\nrule1 = Rule(foo_checker)\nrule2 = Rule(bar_checker)\n\nrule = rule1 & rule2\nrule = rule1 & bar_checker\nrule = foo_checker & rule2\n```\n\n同时，我们也无需担心合并了一个 `None` 值，`Rule` 会忽略 `None` 值。\n\n```python\nassert (rule & None) is rule\n```\n\n## 主动使用响应规则\n\n除了在事件响应器中使用响应规则外，我们也可以主动使用响应规则来判断事件是否符合条件。例如：\n\n```python {3}\nrule = Rule(some_checker)\n\nresult: bool = await rule(bot, event, state)\n```\n\n我们只需要传入 `Bot` 对象、事件和会话状态，`Rule` 会并发调用所有 `RuleChecker` 进行检查，并返回结果。\n\n## 内置响应规则\n\nNoneBot 内置了一些常用的响应规则，可以直接通过事件响应器辅助函数或者自行合并其他规则使用。内置响应规则列表可以参考[事件响应器进阶](../advanced/matcher.md)\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/appendices/session-control.mdx",
    "content": "---\nsidebar_position: 2\ndescription: 更灵活的会话控制\n\noptions:\n  menu:\n    - category: appendices\n      weight: 30\n---\n\n# 会话控制\n\nimport Messenger from \"@site/src/components/Messenger\";\n\n在[指南](../tutorial/event-data.mdx#使用依赖注入)的 `weather` 插件中，我们使用依赖注入获取了机器人用户发送的地名参数，并根据地名参数进行相应的回复。但是，一问一答的对话模式仅仅适用于简单的对话场景，如果我们想要实现更复杂的对话模式，就需要使用会话控制。\n\n## 询问并获取用户输入\n\n在 `weather` 插件中，我们对于用户未输入地名参数的情况直接回复了 `请输入地名` 并结束了事件流程。但是，这样用户体验并不好，需要重新输入指令和地名参数才能获取天气回复。我们现在来实现询问并获取用户地名参数的功能。\n\n### 询问用户\n\n我们可以使用事件响应器操作中的 `got` 装饰器来表示当前事件处理流程需要询问并获取用户输入的消息：\n\n```python {6} title=weather/__init__.py\n@weather.handle()\nasync def handle_function(args: Message = CommandArg()):\n    if location := args.extract_plain_text():\n        await weather.finish(f\"今天{location}的天气是...\")\n\n@weather.got(\"location\", prompt=\"请输入地名\")\nasync def got_location():\n    ...\n```\n\n在上面的代码中，我们使用 `got` 事件响应器操作来向用户发送 `prompt` 消息，并等待用户的回复。用户的回复消息将会被作为 `location` 参数存储于事件响应器状态中。\n\n:::tip 提示\n事件处理函数根据定义的顺序依次执行。\n:::\n\n### 获取用户输入\n\n在询问以及用户回复之后，我们就可以获取到我们需要的 `location` 参数了。我们使用 `ArgPlainText` 依赖注入来获取参数纯文本信息：\n\n```python {9} title=weather/__init__.py\nfrom nonebot.params import ArgPlainText\n\n@weather.handle()\nasync def handle_function(args: Message = CommandArg()):\n    if location := args.extract_plain_text():\n        await weather.finish(f\"今天{location}的天气是...\")\n\n@weather.got(\"location\", prompt=\"请输入地名\")\nasync def got_location(location: str = ArgPlainText()):\n    await weather.finish(f\"今天{location}的天气是...\")\n```\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/天气\" },\n    { position: \"left\", msg: \"请输入地名\" },\n    { position: \"right\", msg: \"北京\" },\n    { position: \"left\", msg: \"今天北京的天气是...\" },\n  ]}\n/>\n\n在上面的代码中，我们在 `got_location` 函数中定义了一个依赖注入参数 `location`，他的值将会是用户回复的消息纯文本信息。获取到用户输入的地名参数后，我们就可以进行天气查询并回复了。\n\n:::tip 提示\n如果想要获取用户回复的消息对象 `Message` ，可以使用 `Arg` 依赖注入。\n:::\n\n### 跳过询问\n\n在上面的代码中，如果用户在输入天气指令时，同时提供了地名参数，我们直接回复了天气信息，这部分的逻辑是和询问用户地名参数之后的逻辑一致的。如果在复杂的业务场景下，我们希望这部分代码应该复用以减少代码冗余。我们可以使用事件响应器操作中的 `set_arg` 来主动设置一个参数：\n\n```python {4,6} title=weather/__init__.py\nfrom nonebot.matcher import Matcher\n\n@weather.handle()\nasync def handle_function(matcher: Matcher, args: Message = CommandArg()):\n    if args.extract_plain_text():\n        matcher.set_arg(\"location\", args)\n\n@weather.got(\"location\", prompt=\"请输入地名\")\nasync def got_location(location: str = ArgPlainText()):\n    await weather.finish(f\"今天{location}的天气是...\")\n```\n\n请注意，设置参数需要使用依赖注入来获取 `Matcher` 实例以确保上下文正确，且参数值应为 `Message` 对象。\n\n在 `location` 参数被设置之后，`got` 事件响应器操作将不再会询问并等待用户的回复，而是直接进入 `got_location` 函数。\n\n## 请求重新输入\n\n在实际的业务场景中，用户的输入很有可能并非是我们所期望的，而结束事件处理流程让用户重新发送指令也不是一个好的体验。这时我们可以使用 `reject` 事件响应器操作来请求用户重新输入：\n\n```python {8,9} title=weather/__init__.py\n@weather.handle()\nasync def handle_function(matcher: Matcher, args: Message = CommandArg()):\n    if args.extract_plain_text():\n        matcher.set_arg(\"location\", args)\n\n@weather.got(\"location\", prompt=\"请输入地名\")\nasync def got_location(location: str = ArgPlainText()):\n    if location not in [\"北京\", \"上海\", \"广州\", \"深圳\"]:\n        await weather.reject(f\"你想查询的城市 {location} 暂不支持，请重新输入！\")\n    await weather.finish(f\"今天{location}的天气是...\")\n```\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/天气\" },\n    { position: \"left\", msg: \"请输入地名\" },\n    { position: \"right\", msg: \"南京\" },\n    { position: \"left\", msg: \"你想查询的城市 南京 暂不支持，请重新输入！\" },\n    { position: \"right\", msg: \"北京\" },\n    { position: \"left\", msg: \"今天北京的天气是...\" },\n  ]}\n/>\n\n在上面的代码中，我们在 `got_location` 函数中判断用户输入的地名是否在支持的城市列表中，如果不在，则使用 `reject` 事件响应器操作。操作将会向用户发送 `reject` 参数中的消息，并等待用户回复后，重新执行 `got_location` 函数。通过 `got` 和 `reject` 事件响应器操作，我们实现了类似于**循环**的执行方式。\n\n`reject` 事件响应器操作与 `finish` 类似，NoneBot 会在向机器人用户发送消息内容后抛出 `RejectedException` 异常来暂停事件响应流程以等待用户输入。也就是说，在 `reject` 被执行后，后续的程序同样是不会被执行的。\n\n## 更多事件响应器操作\n\n在之前的章节中，我们已经大致了解了五个事件响应器操作：`handle`、`got`、`finish`、`send` 和 `reject`。现在我们来完整地介绍一下这些操作。\n\n事件响应器操作可以分为两大类：**交互操作**和**流程控制操作**。我们可以通过交互操作来与用户进行交互，而流程控制操作则可以用来控制事件处理流程的执行。\n\n:::tip 提示\n事件处理流程按照事件处理函数添加顺序执行，已经结束的事件处理函数不可能被恢复执行。\n:::\n\n### handle\n\n`handle` 事件响应器操作是一个装饰器，用于向事件处理流程添加一个事件处理函数。\n\n```python\n@matcher.handle()\nasync def handle_func():\n    ...\n```\n\n`handle` 装饰器支持嵌套操作，即一个事件处理函数可以被添加多次：\n\n```python\n@matcher.handle()\n@matcher.handle()\nasync def handle_func():\n    # 这个函数会被执行两次\n    ...\n```\n\n### got\n\n`got` 事件响应器操作也是一个装饰器，它会在当前装饰的事件处理函数运行之前，中断当前事件处理流程，等待接收一个新的事件。它可以通过 `prompt` 参数来向用户发送询问消息，然后等待用户的回复消息，贴近对话形式会话。\n\n`got` 装饰器接受一个参数 `key` 和一个可选参数 `prompt`。当会话状态中不存在 `key` 对应的消息时，会向用户发送 `prompt` 参数的消息，并等待用户回复。`prompt` 参数的类型和 [`send`](#send) 事件响应器操作的参数类型一致。\n\n在事件处理函数中，可以通过依赖注入的方式来获取接收到的消息，参考：[`Arg`](../advanced/dependency.mdx#arg)、[`ArgStr`](../advanced/dependency.mdx#argstr)、[`ArgPlainText`](../advanced/dependency.mdx#argplaintext)。\n\n```python\n@matcher.got(\"key\", prompt=\"请输入...\")\nasync def got_func(key: Message = Arg()):\n    ...\n```\n\n`got` 装饰器支持与 `got` 和 `receive` 装饰器嵌套操作，即一个事件处理函数可以在接收多个事件或消息后执行：\n\n```python\n@matcher.got(\"key1\", prompt=\"请输入key1...\")\n@matcher.got(\"key2\", prompt=\"请输入key2...\")\n@matcher.receive(\"key3\")\nasync def got_func(key1: Message = Arg(), key2: Message = Arg(), key3: Event = Received(\"key3\")):\n    ...\n```\n\n### receive\n\n`receive` 事件响应器操作也是一个装饰器，它会在当前装饰的事件处理函数运行之前，中断当前事件处理流程，等待接收一个新的事件。与 `got` 不同的是，`receive` 不会向用户发送询问消息，并且等待一个用户事件。可以接收的事件类型取决于[会话更新](../advanced/session-updating.md)。\n\n`receive` 装饰器接受一个可选参数 id，用于标识当前需要接收的事件，如果不指定，则默认为空 `\"\"`。\n\n在事件处理函数中，可以通过依赖注入的方式来获取接收到的事件，参考：[`Received`](../advanced/dependency.mdx#received)、[`LastReceived`](../advanced/dependency.mdx#lastreceived)。\n\n```python\n@matcher.receive(\"id\")\nasync def receive_func(event: Event = Received(\"id\")):\n    ...\n```\n\n`receive` 装饰器支持与 `got` 和 `receive` 装饰器嵌套操作，即一个事件处理函数可以在接收多个事件或消息后执行：\n\n```python\n@matcher.receive(\"key1\")\n@matcher.got(\"key2\", prompt=\"请输入key2...\")\n@matcher.got(\"key3\", prompt=\"请输入key3...\")\nasync def receive_func(key1: Event = Received(\"key1\"), key2: Message = Arg(), key3: Message = Arg()):\n    ...\n```\n\n### send\n\n`send` 事件响应器操作用于向用户回复一条消息。协议适配器会根据当前 event 选择回复的途径。\n\n`send` 操作接受一个参数 message 和其他任何协议适配器接受的参数。message 参数类型可以是字符串、消息序列、消息段或者消息模板。消息模板将会使用会话状态字典进行渲染后发送。\n\n这个操作等同于使用 `bot.send(event, message, **kwargs)`，但不需要自行传入 `event`。\n\n```python\n@matcher.handle()\nasync def _():\n    await matcher.send(\"Hello world!\")\n```\n\n### finish\n\n向用户回复一条消息（可选），并立即结束**整个处理流程**。\n\n参数与 [`send`](#send) 相同。\n\n```python\n@matcher.handle()\nasync def _():\n    await matcher.finish(\"Hello world!\")\n    # 下面的代码不会被执行\n```\n\n### pause\n\n向用户回复一条消息（可选），立即结束**当前**事件处理函数，等待接收一个新的事件后进入**下一个**事件处理函数。\n\n参数与 [`send`](#send) 相同。\n\n```python\n@matcher.handle()\nasync def _():\n    if need_confirm:\n        await matcher.pause(\"请在两分钟内确认执行\")\n\n@matcher.handle()\nasync def _():\n    ...\n```\n\n### reject\n\n向用户回复一条消息（可选），立即结束**当前**事件处理函数，等待接收一个新的事件后再次执行**当前**事件处理函数。\n\n`reject` 可以用于拒绝当前 `receive` 接收的事件或 `got` 接收的参数。通常在用户回复不符合格式或标准需要重新输入，或者用于循环进行用户交互。\n\n参数与 [`send`](#send) 相同。\n\n```python\n@matcher.got(\"arg\")\nasync def _(arg: str = ArgPlainText()):\n    if not is_valid(arg):\n        await matcher.reject(\"Invalid arg!\")\n```\n\n### reject_arg\n\n向用户回复一条消息（可选），立即结束**当前**事件处理函数，等待接收一个新的消息后再次执行**当前**事件处理函数。\n\n`reject_arg` 用于拒绝指定 `got` 接收的参数，通常在嵌套装饰器时使用。\n\n`reject_arg` 操作接受一个 key 参数以及可选的 prompt 参数。prompt 参数与 [`send`](#send) 相同。\n\n```python\n@matcher.got(\"a\")\n@matcher.got(\"b\")\nasync def _(a: str = ArgPlainText(), b: str = ArgPlainText()):\n    if a not in b:\n        await matcher.reject_arg(\"a\", \"Invalid a!\")\n```\n\n### reject_receive\n\n向用户回复一条消息（可选），立即结束**当前**事件处理函数，等待接收一个新的事件后再次执行**当前**事件处理函数。\n\n`reject_receive` 用于拒绝指定 `receive` 接收的事件，通常在嵌套装饰器时使用。\n\n`reject_receive` 操作接受一个可选的 id 参数以及可选的 prompt 参数。id 参数默认为空 `\"\"`，prompt 参数与 [`send`](#send) 相同。\n\n```python\n@matcher.receive(\"a\")\n@matcher.receive(\"b\")\nasync def _(a: Event = Received(\"a\"), b: Event = Received(\"b\")):\n    if a.get_user_id() != b.get_user_id():\n        await matcher.reject_receive(\"a\")\n```\n\n### skip\n\n立即结束当前事件处理函数，进入下一个事件处理函数。\n\n通常在依赖注入中使用，用于跳过当前事件处理函数的执行。\n\n```python\nfrom nonebot.params import Depends\n\nasync def dependency():\n    matcher.skip()\n\n@matcher.handle()\nasync def _(check=Depends(dependency)):\n    # 这个函数不会被执行\n```\n\n### stop_propagation\n\n阻止事件向更低优先级的事件响应器传播。\n\n```python\nfrom nonebot.matcher import Matcher\n\n@foo.handle()\nasync def _(matcher: Matcher):\n    matcher.stop_propagation()\n```\n\n:::caution 注意\n`stop_propagation` 操作是实例方法，需要先通过依赖注入获取事件响应器实例再进行调用。\n:::\n\n### get_arg\n\n获取一个 `got` 接收的参数。\n\n`get_arg` 操作接受一个 key 参数和一个可选的 default 参数。当参数不存在时，将返回 default 或 `None`。\n\n```python\nfrom nonebot.matcher import Matcher\n\n@matcher.handle()\nasync def _(matcher: Matcher):\n    key = matcher.get_arg(\"key\", default=None)\n```\n\n### set_arg\n\n设置 / 覆盖一个 `got` 接收的参数。\n\n`set_arg` 操作接受一个 key 参数和一个 value 参数。请注意，value 参数必须是消息序列对象，如需存储其他数据请使用[会话状态](./session-state.md)。\n\n```python\nfrom nonebot.matcher import Matcher\n\n@matcher.handle()\nasync def _(matcher: Matcher):\n    matcher.set_arg(\"key\", Message(\"value\"))\n```\n\n### get_receive\n\n获取一个 `receive` 接收的事件。\n\n`get_receive` 操作接受一个 id 参数和一个可选的 default 参数。当事件不存在时，将返回 default 或 `None`。\n\n```python\nfrom nonebot.matcher import Matcher\n\n@matcher.handle()\nasync def _(matcher: Matcher):\n    event = matcher.get_receive(\"id\", default=None)\n```\n\n### get_last_receive\n\n获取最近的一个 `receive` 接收的事件。\n\n`get_last_receive` 操作接受一个可选的 default 参数。当事件不存在时，将返回 default 或 `None`。\n\n```python\nfrom nonebot.matcher import Matcher\n\n@matcher.handle()\nasync def _(matcher: Matcher):\n    event = matcher.get_last_receive(default=None)\n```\n\n### set_receive\n\n设置 / 覆盖一个 `receive` 接收的事件。\n\n`set_receive` 操作接受一个 id 参数和一个 event 参数。请注意，event 参数必须是事件对象，如需存储其他数据请使用[会话状态](./session-state.md)。\n\n```python\nfrom nonebot.matcher import Matcher\n\n@matcher.handle()\nasync def _(matcher: Matcher):\n    matcher.set_receive(\"key\", Event())\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/appendices/session-state.md",
    "content": "---\nsidebar_position: 3\ndescription: 会话状态信息\n\noptions:\n  menu:\n    - category: appendices\n      weight: 40\n---\n\n# 会话状态\n\n在事件处理流程中，和用户交互的过程即是会话。在会话中，我们可能需要记录一些信息，例如用户的重试次数等等，以便在会话中的不同阶段进行判断和处理。这些信息都可以存储于会话状态中。\n\nNoneBot 中的会话状态是一个字典，可以通过类型 `T_State` 来获取。字典内可以存储任意类型的数据，但是要注意的是，NoneBot 本身会在会话状态中存储一些信息，因此不要使用 [NoneBot 使用的键名](../api/consts.md)。\n\n```python\nfrom nonebot.typing import T_State\n\n@matcher.got(\"key\", prompt=\"请输入密码\")\nasync def _(state: T_State, key: str = ArgPlainText()):\n    if key != \"some password\":\n        try_count = state.get(\"try_count\", 1)\n        if try_count >= 3:\n            await matcher.finish(\"密码错误次数过多\")\n        else:\n            state[\"try_count\"] = try_count + 1\n            await matcher.reject(\"密码错误，请重新输入\")\n    await matcher.finish(\"密码正确\")\n```\n\n会话状态的生命周期与事件处理流程相同，在期间的任何一个事件处理函数都可以进行读写。\n\n```python\nfrom nonebot.typing import T_State\n\n@matcher.handle()\nasync def _(state: T_State):\n    state[\"key\"] = \"value\"\n\n@matcher.handle()\nasync def _(state: T_State):\n    await matcher.finish(state[\"key\"])\n```\n\n会话状态还可以用于发送动态消息，消息模板在发送时会使用会话状态字典进行渲染。消息模板的使用方法已经在[消息处理](../tutorial/message.md#使用消息模板)中介绍过，这里不再赘述。\n\n```python\nfrom nonebot.typing import T_State\nfrom nonebot.adapters import MessageTemplate\n\n@matcher.handle()\nasync def _(state: T_State):\n    state[\"username\"] = \"user\"\n\n@matcher.got(\"password\", prompt=MessageTemplate(\"请输入 {username} 的密码\"))\nasync def _():\n    await matcher.finish(MessageTemplate(\"密码为 {password}\"))\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/appendices/whats-next.md",
    "content": "---\nsidebar_position: 99\ndescription: 下一步──进阶！\n---\n\n# 下一步\n\n至此，我们已经了解了 NoneBot 的大多数功能用法，相信你已经可以独自写出一个插件了。现在你可以选择：\n\n- 即刻开始插件编写！\n- 更深入地了解 NoneBot 的[更多功能和原理](../advanced/plugin-info.md)！\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/best-practice/alconna/README.mdx",
    "content": "---\nsidebar_position: 1\ndescription: Alconna 命令解析拓展\n\nslug: /best-practice/alconna/\n---\n\nimport Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\n\n# Alconna 插件\n\n[`nonebot-plugin-alconna`](https://github.com/nonebot/plugin-alconna) 是一类提供了拓展响应规则的插件。\n该插件使用 [Alconna](https://github.com/ArcletProject/Alconna) 作为命令解析器，\n是一个简单、灵活、高效的命令参数解析器，并且不局限于解析命令式字符串。\n\n该插件提供了一类新的事件响应器辅助函数 `on_alconna`，以及 `AlconnaResult` 等依赖注入函数。\n\n该插件声明了一个 `Matcher` 的子类 `AlconnaMatcher`，并在 `AlconnaMatcher` 中添加了一些新的方法，例如：\n\n- `assign`：基于 `Alconna` 解析结果，执行满足目标路径的处理函数\n- `dispatch`：类似 `CommandGroup`，对目标路径创建一个新的 `AlconnaMatcher`，并将解析结果分配给该 `AlconnaMatcher`\n- `got_path`：类似 `got`，但是可以指定目标路径，并且能够验证解析结果是否可用\n- ...\n\n基于 `Alconna` 的特性，该插件同时提供了一系列便捷的消息段标注。\n标注可用于在 `Alconna` 中匹配消息中除 text 外的其他消息段，也可用于快速创建各适配器下的消息段。所有标注位于 `nonebot_plugin_alconna.adapters` 中。\n\n该插件同时通过提供 `UniMessage` (通用消息模型) 实现了**跨平台接收和发送消息**的功能。\n\n## 安装插件\n\n在使用前请先安装 `nonebot-plugin-alconna` 插件至项目环境中，可参考[获取商店插件](../../tutorial/store.mdx#安装插件)来了解并选择安装插件的方式。如：\n\n在**项目目录**下执行以下命令：\n\n<Tabs groupId=\"install\">\n<TabItem value=\"cli\" label=\"使用 nb-cli\">\n\n```shell\nnb plugin install nonebot-plugin-alconna\n```\n\n</TabItem>\n<TabItem value=\"pip\" label=\"使用 pip\">\n\n```shell\npip install nonebot-plugin-alconna\n```\n\n</TabItem>\n\n<TabItem value=\"pdm\" label=\"使用 pdm\">\n\n```shell\npdm add nonebot-plugin-alconna\n```\n\n</TabItem>\n</Tabs>\n\n## 导入插件\n\n由于 `nonebot-plugin-alconna` 作为插件，因此需要在使用前对其进行**加载**并**导入**其中的 `on_alconna` 来使用命令拓展。使用 `require` 方法可轻松完成这一过程，可参考 [跨插件访问](../../advanced/requiring.md) 一节进行了解。\n\n```python\nfrom nonebot import require\n\nrequire(\"nonebot_plugin_alconna\")\n\nfrom nonebot_plugin_alconna import on_alconna\n```\n\n## 使用插件\n\n在前面的[深入指南](../../appendices/session-control.mdx)中，我们已经得到了一个天气插件。\n现在我们将使用 `Alconna` 来改写这个插件。\n\n<details>\n  <summary>插件示例</summary>\n\n```python title=weather/__init__.py\nfrom nonebot import on_command\nfrom nonebot.rule import to_me\nfrom nonebot.matcher import Matcher\nfrom nonebot.adapters import Message\nfrom nonebot.params import CommandArg, ArgPlainText\n\nweather = on_command(\"天气\", rule=to_me(), aliases={\"weather\", \"天气预报\"})\n\n@weather.handle()\nasync def handle_function(matcher: Matcher, args: Message = CommandArg()):\n    if args.extract_plain_text():\n        matcher.set_arg(\"location\", args)\n\n@weather.got(\"location\", prompt=\"请输入地名\")\nasync def got_location(location: str = ArgPlainText()):\n    if location not in [\"北京\", \"上海\", \"广州\", \"深圳\"]:\n        await weather.reject(f\"你想查询的城市 {location} 暂不支持，请重新输入！\")\n    await weather.finish(f\"今天{location}的天气是...\")\n```\n\n</details>\n\n```python {5-9,13-15,17-18}\nfrom nonebot.rule import to_me\nfrom arclet.alconna import Alconna, Args\nfrom nonebot_plugin_alconna import Match, on_alconna\n\nweather = on_alconna(\n    Alconna(\"天气\", Args[\"location?\", str]),\n    aliases={\"weather\", \"天气预报\"},\n    rule=to_me(),\n)\n\n\n@weather.handle()\nasync def handle_function(location: Match[str]):\n    if location.available:\n        weather.set_path_arg(\"location\", location.result)\n\n@weather.got_path(\"location\", prompt=\"请输入地名\")\nasync def got_location(location: str):\n    if location not in [\"北京\", \"上海\", \"广州\", \"深圳\"]:\n        await weather.reject(f\"你想查询的城市 {location} 暂不支持，请重新输入！\")\n    await weather.finish(f\"今天{location}的天气是...\")\n```\n\n在上面的代码中，我们使用 `Alconna` 来解析命令，`on_alconna` 用来创建响应器，使用 `Match` 来获取解析结果。\n\n关于更多 `Alconna` 的使用方法，可参考 [Alconna 文档](https://arclet.top/docs/tutorial/alconna)，\n或阅读 [Alconna 基本介绍](./command.md) 一节。\n\n关于更多 `on_alconna` 的使用方法，可参考 [插件文档](https://github.com/nonebot/plugin-alconna/blob/master/docs.md)，\n或阅读 [响应规则的使用](./matcher.mdx) 一节。\n\n## 交流与反馈\n\nQQ 交流群: [🔗 链接](https://jq.qq.com/?_wv=1027&k=PUPOnCSH)\n\n友链: [📚 文档](https://graiax.cn/guide/message_parser/alconna.html)\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/best-practice/alconna/_category_.json",
    "content": "{\n  \"label\": \"Alconna 命令解析拓展\",\n  \"position\": 6\n}\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/best-practice/alconna/command.md",
    "content": "---\nsidebar_position: 2\ndescription: Alconna 基本介绍\n---\n\n# Alconna 本体\n\n[`Alconna`](https://github.com/ArcletProject/Alconna) 隶属于 `ArcletProject`，是一个简单、灵活、高效的命令参数解析器, 并且不局限于解析命令式字符串。\n\n我们通过一个例子来讲解 **Alconna** 的核心 —— `Args`, `Subcommand`, `Option`：\n\n```python\nfrom arclet.alconna import Alconna, Args, Subcommand, Option\n\n\nalc = Alconna(\n    \"pip\",\n    Subcommand(\n        \"install\",\n        Args[\"package\", str],\n        Option(\"-r|--requirement\", Args[\"file\", str]),\n        Option(\"-i|--index-url\", Args[\"url\", str]),\n    )\n)\n\nres = alc.parse(\"pip install nonebot2 -i URL\")\n\nprint(res)\n# 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'}\n\nprint(res.all_matched_args)\n# {'package': 'nonebot2', 'url': 'URL'}\n```\n\n这段代码通过`Alconna`创捷了一个接受主命令名为`pip`, 子命令为`install`且子命令接受一个 **Args** 参数`package`和二个 **Option** 参数`-r`和`-i`的命令参数解析器, 通过`parse`方法返回解析结果 **Arparma** 的实例。\n\n## 命令头\n\n命令头是指命令的前缀 (Prefix) 与命令名 (Command) 的组合，例如 !help 中的 ! 与 help。\n\n|             前缀             |   命令名   |                          匹配内容                           |       说明       |\n| :--------------------------: | :--------: | :---------------------------------------------------------: | :--------------: |\n|              -               |   \"foo\"    |                           `\"foo\"`                           | 无前缀的纯文字头 |\n|              -               |    123     |                            `123`                            |  无前缀的元素头  |\n|              -               | \"re:\\d{2}\" |                           `\"32\"`                            |  无前缀的正则头  |\n|              -               |    int     |                      `123` 或 `\"456\"`                       |  无前缀的类型头  |\n|         [int, bool]          |     -      |                       `True` 或 `123`                       |  无名的元素类头  |\n|        [\"foo\", \"bar\"]        |     -      |                     `\"foo\"` 或 `\"bar\"`                      |  无名的纯文字头  |\n|        [\"foo\", \"bar\"]        |   \"baz\"    |                  `\"foobaz\"` 或 `\"barbaz\"`                   |     纯文字头     |\n|         [int, bool]          |   \"foo\"    |             `[123, \"foo\"]` 或 `[False, \"foo\"]`              |      类型头      |\n|         [123, 4567]          |   \"foo\"    |              `[123, \"foo\"]` 或 `[4567, \"foo\"]`              |      元素头      |\n|      [nepattern.NUMBER]      |   \"bar\"    |            `[123, \"bar\"]` 或 `[123.456, \"bar\"]`             |     表达式头     |\n|         [123, \"foo\"]         |   \"bar\"    |      `[123, \"bar\"]` 或 `\"foobar\"` 或 `[\"foo\", \"bar\"]`       |      混合头      |\n| [(int, \"foo\"), (456, \"bar\")] |   \"baz\"    | `[123, \"foobaz\"]` 或 `[456, \"foobaz\"]` 或 `[456, \"barbaz\"]` |       对头       |\n\n对于无前缀的类型头，此时会将传入的值尝试转为 BasePattern，例如 `int` 会转为 `nepattern.INTEGER`。如此该命令头会匹配对应的类型， 例如 `int` 会匹配 `123` 或 `\"456\"`，但不会匹配 `\"foo\"`。解析后，Alconna 会将命令头匹配到的值转为对应的类型，例如 `int` 会将 `\"123\"` 转为 `123`。\n\n:::tip\n\n**正则内容只在命令名上生效，前缀中的正则会被转义**\n\n:::\n\n除了通过传入 `re:xxx` 来使用正则表达式外，Alconna 还提供了一种更加简洁的方式来使用正则表达式，称为 Bracket Header：\n\n```python\nfrom alconna import Alconna\n\n\nalc = Alconna(\".rd{roll:int}\")\nassert alc.parse(\".rd123\").header[\"roll\"] == 123\n```\n\nBracket Header 类似 python 里的 f-string 写法，通过 `\"{}\"` 声明匹配类型\n\n`\"{}\"` 中的内容为 \"name:type or pat\"：\n\n- `\"{}\"`, `\"{:}\"` ⇔ `\"(.+)\"`, 占位符\n- `\"{foo}\"` ⇔ `\"(?P&lt;foo&gt;.+)\"`\n- `\"{:\\d+}\"` ⇔ `\"(\\d+)\"`\n- `\"{foo:int}\"` ⇔ `\"(?P&lt;foo&gt;\\d+)\"`，其中 `\"int\"` 部分若能转为 `BasePattern` 则读取里面的表达式\n\n## 参数声明(Args)\n\n`Args` 是用于声明命令参数的组件， 可以通过以下几种方式构造 **Args** ：\n\n- `Args[key, var, default][key1, var1, default1][...]`\n- `Args[(key, var, default)]`\n- `Args.key[var, default]`\n\n其中，key **一定**是字符串，而 var 一般为参数的类型，default 为具体的值或者 **arclet.alconna.args.Field**\n\n其与函数签名类似，但是允许含有默认值的参数在前；同时支持 keyword-only 参数不依照构造顺序传入 （但是仍需要在非 keyword-only 参数之后）。\n\n### key\n\n`key` 的作用是用以标记解析出来的参数并存放于 **Arparma** 中，以方便用户调用。\n\n其有三种为 Args 注解的标识符: `?`、`/`、 `!`, 标识符与 key 之间建议以 `;` 分隔：\n\n- `!` 标识符表示该处传入的参数应**不是**规定的类型，或**不在**指定的值中。\n- `?` 标识符表示该参数为**可选**参数，会在无参数匹配时跳过。\n- `/` 标识符表示该参数的类型注解需要隐藏。\n\n另外，对于参数的注释也可以标记在 `key` 中，其与 key 或者标识符 以 `#` 分割：  \n`foo#这是注释;?` 或 `foo?#这是注释`\n\n:::tip\n\n`Args` 中的 `key` 在实际命令中并不需要传入（keyword 参数除外）：\n\n```python\nfrom arclet.alconna import Alconna, Args\n\n\nalc = Alconna(\"test\", Args[\"foo\", str])\nalc.parse(\"test --foo abc\") # 错误\nalc.parse(\"test abc\") # 正确\n```\n\n若需要 `test --foo abc`，你应该使用 `Option`：\n\n```python\nfrom arclet.alconna import Alconna, Args, Option\n\n\nalc = Alconna(\"test\", Option(\"--foo\", Args[\"foo\", str]))\n```\n\n:::\n\n### var\n\nvar 负责命令参数的**类型检查**与**类型转化**\n\n`Args` 的`var`表面上看需要传入一个 `type`，但实际上它需要的是一个 `nepattern.BasePattern` 的实例：\n\n```python\nfrom arclet.alconna import Args\nfrom nepattern import BasePattern\n\n\n# 表示 foo 参数需要匹配一个 @number 样式的字符串\nargs = Args[\"foo\", BasePattern(\"@\\d+\")]\n```\n\n`pip` 示例中可以传入 `str` 是因为 `str` 已经注册在了 `nepattern.global_patterns` 中，因此会替换为 `nepattern.global_patterns[str]`\n\n`nepattern.global_patterns`默认支持的类型有：\n\n- `str`: 匹配任意字符串\n- `int`: 匹配整数\n- `float`: 匹配浮点数\n- `bool`: 匹配 `True` 与 `False` 以及他们小写形式\n- `hex`: 匹配 `0x` 开头的十六进制字符串\n- `url`: 匹配网址\n- `email`: 匹配 `xxxx@xxx` 的字符串\n- `ipv4`: 匹配 `xxx.xxx.xxx.xxx` 的字符串\n- `list`: 匹配类似 `[\"foo\",\"bar\",\"baz\"]` 的字符串\n- `dict`: 匹配类似 `{\"foo\":\"bar\",\"baz\":\"qux\"}` 的字符串\n- `datetime`: 传入一个 `datetime` 支持的格式字符串，或时间戳\n- `Any`: 匹配任意类型\n- `AnyString`: 匹配任意类型，转为 `str`\n- `Number`: 匹配 `int` 与 `float`，转为 `int`\n\n同时可以使用 typing 中的类型：\n\n- `Literal[X]`: 匹配其中的任意一个值\n- `Union[X, Y]`: 匹配其中的任意一个类型\n- `Optional[xxx]`: 会自动将默认值设为 `None`，并在解析失败时使用默认值\n- `List[X]`: 匹配一个列表，其中的元素为 `X` 类型\n- `Dict[X, Y]`: 匹配一个字典，其中的 key 为 `X` 类型，value 为 `Y` 类型\n- ...\n\n:::tip\n\n几类特殊的传入标记：\n\n- `\"foo\"`: 匹配字符串 \"foo\" (若没有某个 `BasePattern` 与之关联)\n- `RawStr(\"foo\")`: 匹配字符串 \"foo\" (即使有 `BasePattern` 与之关联也不会被替换)\n- `\"foo|bar|baz\"`: 匹配 \"foo\" 或 \"bar\" 或 \"baz\"\n- `[foo, bar, Baz, ...]`: 匹配其中的任意一个值或类型\n- `Callable[[X], Y]`: 匹配一个参数为 `X` 类型的值，并返回通过该函数调用得到的 `Y` 类型的值\n- `\"re:xxx\"`: 匹配一个正则表达式 `xxx`，会返回 Match[0]\n- `\"rep:xxx\"`: 匹配一个正则表达式 `xxx`，会返回 `re.Match` 对象\n- `{foo: bar, baz: qux}`: 匹配字典中的任意一个键, 并返回对应的值 (特殊的键 ... 会匹配任意的值)\n- ...\n\n**特别的**，你可以不传入 `var`，此时会使用 `key` 作为 `var`, 匹配 `key` 字符串。\n\n:::\n\n#### MultiVar 与 KeyWordVar\n\n`MultiVar` 是一个特殊的标注，用于告知解析器该参数可以接受多个值，类似于函数中的 `*args`，其构造方法形如 `MultiVar(str)`。\n\n同样的还有 `KeyWordVar`，类似于函数中的 `*, name: type`，其构造方法形如 `KeyWordVar(str)`，用于告知解析器该参数为一个 keyword-only 参数。\n\n:::tip\n\n`MultiVar` 与 `KeyWordVar` 组合时，代表该参数为一个可接受多个 key-value 的参数，类似于函数中的 `**kwargs`，其构造方法形如 `MultiVar(KeyWordVar(str))`\n\n`MultiVar` 与 `KeyWordVar` 也可以传入 `default` 参数，用于指定默认值\n\n`MultiVar` 不能在 `KeyWordVar` 之后传入\n\n:::\n\n### default\n\n`default` 传入的是该参数的默认值或者 `Field`，以携带对于该参数的更多信息。\n\n默认情况下 (即不声明) `default` 的值为特殊值 `Empty`。这也意味着你可以将默认值设置为 `None` 表示默认值为空值。\n\n`Field` 构造需要的参数说明如下：\n\n- default: 参数单元的默认值\n- alias: 参数单元默认值的别名\n- completion: 参数单元的补全说明生成函数\n- unmatch_tips: 参数单元的错误提示生成函数，其接收一个表示匹配失败的元素的参数\n- missing_tips: 参数单元的缺失提示生成函数\n\n## 选项与子命令(Option & Subcommand)\n\n`Option` 和 `Subcommand` 可以传入一组 `alias`，如 `Option(\"--foo|-F|--FOO|-f\")`，`Subcommand(\"foo\", alias=[\"F\"])`\n\n传入别名后，选项与子命令会选择其中长度最长的作为其名称。若传入为 \"--foo|-f\"，则命令名称为 \"--foo\"\n\n:::tip 特别提醒!!!\n\nOption 的名字或别名**没有要求**必须在前面写上 `-`\n\nOption 与 Subcommand 的唯一区别在于 Subcommand 可以传入自己的 **Option** 与 **Subcommand**\n\n:::\n\n他们拥有如下共同参数：\n\n- `help_text`: 传入该组件的帮助信息\n- `dest`: 被指定为解析完成时标注匹配结果的标识符，不传入时默认为选项或子命令的名称 (name)\n- `requires`: 一段指定顺序的字符串列表，作为唯一的前置序列与命令嵌套替换\n  对于命令 `test foo bar baz qux <a:int>` 来讲，因为`foo bar baz` 仅需要判断是否相等, 所以可以这么编写：\n\n```python\nAlconna(\"test\", Option(\"qux\", Args.a[int], requires=[\"foo\", \"bar\", \"baz\"]))\n```\n\n- `default`: 默认值，在该组件未被解析时使用使用该值替换。\n  特别的，使用 `OptionResult` 或 `SubcomanndResult` 可以设置包括参数字典在内的默认值：\n\n```python\nfrom arclet.alconna import Option, OptionResult\n\nopt1 = Option(\"--foo\", default=False)\nopt2 = Option(\"--foo\", default=OptionResult(value=False, args={\"bar\": 1}))\n```\n\n### Action\n\n`Option` 可以特别设置传入一类 `Action`，作为解析操作\n\n`Action` 分为三类：\n\n- `store`: 无 Args 时， 仅存储一个值， 默认为 Ellipsis； 有 Args 时， 后续的解析结果会覆盖之前的值\n- `append`: 无 Args 时， 将多个值存为列表， 默认为 Ellipsis； 有 Args 时， 每个解析结果会追加到列表中, 当存在默认值并且不为列表时， 会自动将默认值变成列表， 以保证追加的正确性\n- `count`: 无 Args 时， 计数器加一； 有 Args 时， 表现与 STORE 相同, 当存在默认值并且不为数字时， 会自动将默认值变成 1， 以保证计数器的正确性。\n\n`Alconna` 提供了预制的几类 `Action`：\n\n- `store`(默认)，`store_value`，`store_true`，`store_false`\n- `append`，`append_value`\n- `count`\n\n## 解析结果(Arparma)\n\n`Alconna.parse` 会返回由 **Arparma** 承载的解析结果\n\n`Arparma` 有如下属性：\n\n- 调试类\n  - matched: 是否匹配成功\n  - error_data: 解析失败时剩余的数据\n  - error_info: 解析失败时的异常内容\n  - origin: 原始命令，可以类型标注\n\n- 分析类\n  - header_match: 命令头部的解析结果，包括原始头部、解析后头部、解析结果与可能的正则匹配组\n  - main_args: 命令的主参数的解析结果\n  - options: 命令所有选项的解析结果\n  - subcommands: 命令所有子命令的解析结果\n  - other_args: 除主参数外的其他解析结果\n  - all_matched_args: 所有 Args 的解析结果\n\n`Arparma` 同时提供了便捷的查询方法 `query[type]()`，会根据传入的 `path` 查找参数并返回\n\n`path` 支持如下:\n\n- `main_args`, `options`, ...: 返回对应的属性\n- `args`: 返回 all_matched_args\n- `main_args.xxx`, `options.xxx`, ...: 返回字典中 `xxx`键对应的值\n- `args.xxx`: 返回 all_matched_args 中 `xxx`键对应的值\n- `options.foo`, `foo`: 返回选项 `foo` 的解析结果 (OptionResult)\n- `options.foo.value`, `foo.value`: 返回选项 `foo` 的解析值\n- `options.foo.args`, `foo.args`: 返回选项 `foo` 的解析参数字典\n- `options.foo.args.bar`, `foo.bar`: 返回选项 `foo` 的参数字典中 `bar` 键对应的值 ...\n\n## 元数据(CommandMeta)\n\n`Alconna` 的元数据相当于其配置，拥有以下条目：\n\n- `description`: 命令的描述\n- `usage`: 命令的用法\n- `example`: 命令的使用样例\n- `author`: 命令的作者\n- `fuzzy_match`: 命令是否开启模糊匹配\n- `fuzzy_threshold`: 模糊匹配阈值\n- `raise_exception`: 命令是否抛出异常\n- `hide`: 命令是否对 manager 隐藏\n- `hide_shortcut`: 命令的快捷指令是否在 help 信息中隐藏\n- `keep_crlf`: 命令解析时是否保留换行字符\n- `compact`: 命令是否允许第一个参数紧随头部\n- `strict`: 命令是否严格匹配，若为 False 则未知参数将作为名为 $extra 的参数\n- `context_style`: 命令上下文插值的风格，None 为关闭，bracket 为 `{...}`，parentheses 为 `$(...)`\n- `extra`: 命令的自定义额外信息\n\n元数据一定使用 `meta=...` 形式传入：\n\n```python\nfrom arclet.alconna import Alconna, CommandMeta\n\nalc = Alconna(..., meta=CommandMeta(\"foo\", example=\"bar\"))\n```\n\n## 命名空间配置\n\n命名空间配置 （以下简称命名空间） 相当于 `Alconna` 的默认配置，其优先度低于 `CommandMeta`。\n\n`Alconna` 默认使用 \"Alconna\" 命名空间。\n\n命名空间有以下几个属性：\n\n- name: 命名空间名称\n- prefixes: 默认前缀配置\n- separators: 默认分隔符配置\n- formatter_type: 默认格式化器类型\n- fuzzy_match: 默认是否开启模糊匹配\n- raise_exception: 默认是否抛出异常\n- builtin_option_name: 默认的内置选项名称(--help, --shortcut, --comp)\n- disable_builtin_options: 默认禁用的内置选项(--help, --shortcut, --comp)\n- enable_message_cache: 默认是否启用消息缓存\n- compact: 默认是否开启紧凑模式\n- strict: 命令是否严格匹配\n- context_style: 命令上下文插值的风格\n- ...\n\n### 新建命名空间并替换\n\n```python\nfrom arclet.alconna import Alconna, namespace, Namespace, Subcommand, Args, config\n\n\nns = Namespace(\"foo\", prefixes=[\"/\"])  # 创建 \"foo\"命名空间配置, 它要求创建的Alconna的主命令前缀必须是/\n\nalc = Alconna(\"pip\", Subcommand(\"install\", Args[\"package\", str]), namespace=ns) # 在创建Alconna时候传入命名空间以替换默认命名空间\n\n# 可以通过with方式创建命名空间\nwith namespace(\"bar\") as np1:\n    np1.prefixes = [\"!\"]    # 以上下文管理器方式配置命名空间，此时配置会自动注入上下文内创建的命令\n    np1.formatter_type = ShellTextFormatter  # 设置此命名空间下的命令的 formatter 默认为 ShellTextFormatter\n    np1.builtin_option_name[\"help\"] = {\"帮助\", \"-h\"}  # 设置此命名空间下的命令的帮助选项名称\n\n# 你还可以使用config来管理所有命名空间并切换至任意命名空间\nconfig.namespaces[\"foo\"] = ns  # 将命名空间挂载到 config 上\n\nalc = Alconna(\"pip\", Subcommand(\"install\", Args[\"package\", str]), namespace=config.namespaces[\"foo\"]) # 也是同样可以切换到\"foo\"命名空间\n```\n\n### 修改默认的命名空间\n\n```python\nfrom arclet.alconna import config, namespace, Namespace\n\n\nconfig.default_namespace.prefixes = [...]  # 直接修改默认配置\n\nnp = Namespace(\"xxx\", prefixes=[...])\nconfig.default_namespace = np  # 更换默认的命名空间\n\nwith namespace(config.default_namespace.name) as np:\n    np.prefixes = [...]\n```\n\n## 快捷指令\n\n快捷命令可以做到标识一段命令, 并且传递参数给原命令\n\n一般情况下你可以通过 `Alconna.shortcut` 进行快捷指令操作 (创建，删除)\n\n`shortcut` 的第一个参数为快捷指令名称，第二个参数为 `ShortcutArgs`，作为快捷指令的配置：\n\n```python\nclass ShortcutArgs(TypedDict):\n    \"\"\"快捷指令参数\"\"\"\n\n    command: NotRequired[str]\n    \"\"\"快捷指令的命令\"\"\"\n    args: NotRequired[list[Any]]\n    \"\"\"快捷指令的附带参数\"\"\"\n    fuzzy: NotRequired[bool]\n    \"\"\"是否允许命令后随参数\"\"\"\n    prefix: NotRequired[bool]\n    \"\"\"是否调用时保留指令前缀\"\"\"\n    wrapper: NotRequired[ShortcutRegWrapper]\n    \"\"\"快捷指令的正则匹配结果的额外处理函数\"\"\"\n    humanized: NotRequired[str]\n    \"\"\"快捷指令的人类可读描述\"\"\"\n```\n\n### args的使用\n\n```python\nfrom arclet.alconna import Alconna, Args\n\n\nalc = Alconna(\"setu\", Args[\"count\", int])\n\nalc.shortcut(\"涩图(\\d+)张\", {\"args\": [\"{0}\"]})\n# 'Alconna::setu 的快捷指令: \"涩图(\\\\d+)张\" 添加成功'\n\nalc.parse(\"涩图3张\").query(\"count\")\n# 3\n```\n\n### command的使用\n\n```python\nfrom arclet.alconna import Alconna, Args\n\n\nalc = Alconna(\"eval\", Args[\"content\", str])\n\nalc.shortcut(\"echo\", {\"command\": \"eval print(\\\\'{*}\\\\')\"})\n# 'Alconna::eval 的快捷指令: \"echo\" 添加成功'\n\nalc.shortcut(\"echo\", delete=True) # 删除快捷指令\n# 'Alconna::eval 的快捷指令: \"echo\" 删除成功'\n\n@alc.bind() # 绑定一个命令执行器, 若匹配成功则会传入参数, 自动执行命令执行器\ndef cb(content: str):\n    eval(content, {}, {})\n\nalc.parse('eval print(\\\\\"hello world\\\\\")')\n# hello world\n\nalc.parse(\"echo hello world!\")\n# hello world!\n```\n\n当 `fuzzy` 为 False 时，第一个例子中传入 `\"涩图1张 abc\"` 之类的快捷指令将视为解析失败\n\n快捷指令允许三类特殊的 placeholder：\n\n- `{%X}`: 如 `setu {%0}`，表示此处填入快捷指令后随的第 X 个参数。\n\n例如，若快捷指令为 `涩图`, 配置为 `{\"command\": \"setu {%0}\"}`, 则指令 `涩图 1` 相当于 `setu 1`\n\n- `{*}`: 表示此处填入所有后随参数，并且可以通过 `{*X}` 的方式指定组合参数之间的分隔符。\n\n- `{X}`: 表示此处填入可能的正则匹配的组：\n\n- 若 `command` 中存在匹配组 `(xxx)`，则 `{X}` 表示第 X 个匹配组的内容\n- 若 `command` 中存储匹配组 `(?P<xxx>...)`, 则 `{X}` 表示 **名字** 为 X 的匹配结果\n\n除此之外, 通过 **Alconna** 内置选项 `--shortcut` 可以动态操作快捷指令\n\n例如：\n\n- `cmd --shortcut <key> <cmd>` 来增加一个快捷指令\n- `cmd --shortcut list` 来列出当前指令的所有快捷指令\n- `cmd --shortcut delete key` 来删除一个快捷指令\n\n```python\nfrom arclet.alconna import Alconna, Args\n\n\nalc = Alconna(\"eval\", Args[\"content\", str])\n\nalc.shortcut(\"echo\", {\"command\": \"eval print(\\\\'{*}\\\\')\"})\n\nalc.parse(\"eval --shortcut list\")\n# 'echo'\n```\n\n## 紧凑命令\n\n`Alconna`, `Option` 与 `Subcommand` 可以设置 `compact=True` 使得解析命令时允许名称与后随参数之间没有分隔：\n\n```python\nfrom arclet.alconna import Alconna, Option, CommandMeta, Args\n\n\nalc = Alconna(\"test\", Args[\"foo\", int], Option(\"BAR\", Args[\"baz\", str], compact=True), meta=CommandMeta(compact=True))\n\nassert alc.parse(\"test123 BARabc\").matched\n```\n\n这使得我们可以实现如下命令：\n\n```python\nfrom arclet.alconna import Alconna, Option, Args, append\n\n\nalc = Alconna(\"gcc\", Option(\"--flag|-F\", Args[\"content\", str], action=append, compact=True))\nprint(alc.parse(\"gcc -Fabc -Fdef -Fxyz\").query[list](\"flag.content\"))\n# ['abc', 'def', 'xyz']\n```\n\n当 `Option` 的 `action` 为 `count` 时，其自动支持 `compact` 特性：\n\n```python\nfrom arclet.alconna import Alconna, Option, count\n\n\nalc = Alconna(\"pp\", Option(\"--verbose|-v\", action=count, default=0))\nprint(alc.parse(\"pp -vvv\").query[int](\"verbose.value\"))\n# 3\n```\n\n## 模糊匹配\n\n模糊匹配会应用在任意需要进行名称判断的地方，如 **命令名称**，**选项名称** 和 **参数名称** (如指定需要传入参数名称)。\n\n```python\nfrom arclet.alconna import Alconna, CommandMeta\n\n\nalc = Alconna(\"test_fuzzy\", meta=CommandMeta(fuzzy_match=True))\n\nalc.parse(\"test_fuzy\")\n# test_fuzy is not matched. Do you mean \"test_fuzzy\"?\n```\n\n## 半自动补全\n\n半自动补全为用户提供了推荐后续输入的功能\n\n补全默认通过 `--comp` 或 `-cp` 或 `?` 触发：（命名空间配置可修改名称）\n\n```python\nfrom arclet.alconna import Alconna, Args, Option\n\n\nalc = Alconna(\"test\", Args[\"abc\", int]) + Option(\"foo\") + Option(\"bar\")\nalc.parse(\"test --comp\")\n\n'''\noutput\n\n以下是建议的输入：\n* <abc: int>\n* --help\n* -h\n* -sct\n* --shortcut\n* foo\n* bar\n'''\n```\n\n## Duplication\n\n**Duplication** 用来提供更好的自动补全，类似于 **ArgParse** 的 **Namespace**\n\n普通情况下使用，需要利用到 **ArgsStub**、**OptionStub** 和 **SubcommandStub** 三个部分\n\n以pip为例，其对应的 Duplication 应如下构造:\n\n```python\nfrom arclet.alconna import Alconna, Args, Option, OptionResult, Duplication, SubcommandStub, Subcommand, count\n\n\nclass MyDup(Duplication):\n    verbose: OptionResult\n    install: SubcommandStub\n\n\nalc = Alconna(\n    \"pip\",\n    Subcommand(\n        \"install\",\n        Args[\"package\", str],\n        Option(\"-r|--requirement\", Args[\"file\", str]),\n        Option(\"-i|--index-url\", Args[\"url\", str]),\n    ),\n    Option(\"-v|--version\"),\n    Option(\"-v|--verbose\", action=count),\n)\n\nres = alc.parse(\"pip -v install ...\") # 不使用duplication获得的提示较少\nprint(res.query(\"install\"))\n# (value=Ellipsis args={'package': '...'} options={} subcommands={})\n\nresult = alc.parse(\"pip -v install ...\", duplication=MyDup)\nprint(result.install)\n# 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')\n```\n\n**Duplication** 也可以如 **Namespace** 一样直接标明参数名称和类型：\n\n```python\nfrom typing import Optional\nfrom arclet.alconna import Duplication\n\n\nclass MyDup(Duplication):\n    package: str\n    file: Optional[str] = None\n    url: Optional[str] = None\n```\n\n## 上下文插值\n\n当 `context_style` 条目被设置后，传入的命令中符合上下文插值的字段会被自动替换成当前上下文中的信息。\n\n上下文可以在 `parse` 中传入：\n\n```python\nfrom arclet.alconna import Alconna, Args, CommandMeta\n\nalc = Alconna(\"test\", Args[\"foo\", int], meta=CommandMeta(context_style=\"parentheses\"))\n\nalc.parse(\"test $(bar)\", {\"bar\": 123})\n# {\"foo\": 123}\n```\n\ncontext_style 的值分两种：\n\n- `\"bracket\"`: 插值格式为 `{...}`，例如 `{foo}`\n- `\"parentheses\"`: 插值格式为 `$(...)`，例如 `$(bar)`\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/best-practice/alconna/config.md",
    "content": "---\nsidebar_position: 4\ndescription: 配置项\n---\n\n# 配置项\n\n## alconna_auto_send_output\n\n- **类型**: `bool`\n- **默认值**: `False`\n\n是否全局启用输出信息自动发送，不启用则会在触发特殊内置选项后仍然将解析结果传递至响应器。\n\n## alconna_use_command_start\n\n- **类型**: `bool`\n- **默认值**: `False`\n\n是否读取 Nonebot 的配置项 `COMMAND_START` 来作为全局的 Alconna 命令前缀\n\n## alconna_auto_completion\n\n- **类型**: `bool`\n- **默认值**: `False`\n\n是否全局启用命令自动补全，启用后会在参数缺失或触发 `--comp` 选项时自自动启用交互式补全。\n\n## alconna_use_origin\n\n- **类型**: `bool`\n- **默认值**: `False`\n\n是否全局使用原始消息 (即未经过 to_me 等处理的)，该选项会影响到 Alconna 的匹配行为。\n\n## alconna_use_command_sep\n\n- **类型**: `bool`\n- **默认值**: `False`\n\n是否读取 Nonebot 的配置项 `COMMAND_SEP` 来作为全局的 Alconna 命令分隔符。\n\n## alconna_global_extensions\n\n- **类型**: `List[str]`\n- **默认值**: `[]`\n\n全局加载的扩展，路径以 . 分隔，如 `foo.bar.baz:DemoExtension`。\n\n## alconna_context_style\n\n- **类型**: `Optional[Literal[\"bracket\", \"parentheses\"]]`\n- **默认值**: `None`\n\n全局命令上下文插值的风格，None 为关闭，bracket 为 `{...}`，parentheses 为 `$(...)`。\n\n## alconna_enable_saa_patch\n\n- **类型**: `bool`\n- **默认值**: `False`\n\n是否启用 SAA 补丁。\n\n## alconna_apply_filehost\n\n- **类型**: `bool`\n- **默认值**: `False`\n\n是否启用文件托管。\n\n## alconna_apply_fetch_targets\n\n- **类型**: `bool`\n- **默认值**: `False`\n\n是否启动时拉取一次发送对象列表。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/best-practice/alconna/matcher.mdx",
    "content": "---\nsidebar_position: 3\ndescription: 响应规则的使用\n---\n\nimport Messenger from \"@site/src/components/Messenger\";\n\n# Alconna 插件\n\n展示：\n\n```python\nfrom nonebot_plugin_alconna import At, Image, on_alconna\nfrom arclet.alconna import Args, Option, Alconna, Arparma, MultiVar, Subcommand\n\n\nalc = Alconna(\n    [\"/\", \"!\"],\n    \"role-group\",\n    Subcommand(\n        \"add\",\n        Args[\"name\", str],\n        Option(\"member\", Args[\"target\", MultiVar(At)]),\n    ),\n    Option(\"list\"),\n    Option(\"icon\", Args[\"icon\", Image])\n)\nrg = on_alconna(alc, auto_send_output=True)\n\n\n@rg.handle()\nasync def _(result: Arparma):\n    if result.find(\"list\"):\n        img: bytes = await gen_role_group_list_image()\n        await rg.finish(Image(raw=img))\n    if result.find(\"add\"):\n        group = await create_role_group(result.query[str](\"add.name\"))\n        if result.find(\"add.member\"):\n            ats = result.query[tuple[At, ...]](\"add.member.target\")\n            group.extend(member.target for member in ats)\n        await rg.finish(\"添加成功\")\n```\n\n## 响应器使用\n\n本插件基于 **Alconna**，为 **Nonebot** 提供了一类新的事件响应器辅助函数 `on_alconna`：\n\n```python\ndef on_alconna(\n    command: Alconna | str,\n    skip_for_unmatch: bool = True,\n    auto_send_output: bool = False,\n    aliases: set[str | tuple[str, ...]] | None = None,\n    comp_config: CompConfig | None = None,\n    extensions: list[type[Extension] | Extension] | None = None,\n    exclude_ext: list[type[Extension] | str] | None = None,\n    use_origin: bool = False,\n    use_cmd_start: bool = False,\n    use_cmd_sep: bool = False,\n    **kwargs,\n    ...,\n):\n```\n\n- `command`: Alconna 命令或字符串，字符串将通过 `AlconnaFormat` 转换为 Alconna 命令\n- `skip_for_unmatch`: 是否在命令不匹配时跳过该响应\n- `auto_send_output`: 是否自动发送输出信息并跳过响应\n- `aliases`: 命令别名， 作用类似于 `on_command` 中的 aliases\n- `comp_config`: 补全会话配置， 不传入则不启用补全会话\n- `extensions`: 需要加载的匹配扩展, 可以是扩展类或扩展实例\n- `exclude_ext`: 需要排除的匹配扩展, 可以是扩展类或扩展的id\n- `use_origin`: 是否使用未经 to_me 等处理过的消息\n- `use_cmd_start`: 是否使用 COMMAND_START 作为命令前缀\n- `use_cmd_sep`: 是否使用 COMMAND_SEP 作为命令分隔符\n\n`on_alconna` 返回的是 `Matcher` 的子类 `AlconnaMatcher` ，其拓展了如下方法：\n\n- `.assign(path, value, or_not)`: 用于对包含多个选项/子命令的命令的分派处理（具体请看[条件控制](./matcher.mdx#条件控制)）\n- `.got_path(path, prompt, middleware)`: 在 `got` 方法的基础上，会以 path 对应的参数为准，读取传入 message 的最后一个消息段并验证转换\n- `.set_path_arg(key, value)`, `.get_path_arg(key)`: 类似 `set_arg` 和 `got_arg`，为 `got_path` 的特化版本\n- `.reject_path(path[, prompt, fallback])`: 类似于 `reject_arg`，对应 `got_path`\n- `.dispatch`: 同样的分派处理，但是是类似 `CommandGroup` 一样返回新的 `AlconnaMatcher`\n- `.got`, `send`, `reject`, ... : 拓展了 prompt 类型，即支持使用 `UniMessage` 作为 prompt\n\n实例：\n\n```python\nfrom nonebot import require\nrequire(\"nonebot_plugin_alconna\")\n\nfrom arclet.alconna import Alconna, Option, Args\nfrom nonebot_plugin_alconna import on_alconna, Match, UniMessage\n\n\nlogin = on_alconna(Alconna([\"/\"], \"login\", Args[\"password?\", str], Option(\"-r|--recall\"))) # 这里[\"/\"]指命令前缀必须是/\n\n# /login -r 触发\n@login.assign(\"recall\")\nasync def login_exit():\n    await login.finish(\"已退出\")\n\n# /login xxx 触发\n@login.assign(\"password\")\nasync def login_handle(pw: Match[str]):\n    if pw.available:\n        login.set_path_arg(\"password\", pw.result)\n\n# /login 触发\n@login.got_path(\"password\", prompt=UniMessage.template(\"{:At(user, $event.get_user_id())} 请输入密码\"))\nasync def login_got(password: str):\n\tassert password\n\tawait login.send(\"登录成功\")\n```\n\n## 依赖注入\n\n本插件提供了一系列依赖注入函数，便于在响应函数中获取解析结果：\n\n- `AlconnaResult`: `CommandResult` 类型的依赖注入函数\n- `AlconnaMatches`: `Arparma` 类型的依赖注入函数\n- `AlconnaDuplication`: `Duplication` 类型的依赖注入函数\n- `AlconnaMatch`: `Match` 类型的依赖注入函数\n- `AlconnaQuery`: `Query` 类型的依赖注入函数\n\n同时，基于 [`Annotated` 支持](https://github.com/nonebot/nonebot2/pull/1832)，添加了两类注解：\n\n- `AlcMatches`：同 `AlconnaMatches`\n- `AlcResult`：同 `AlconnaResult`\n\n可以看到，本插件提供了几类额外的模型：\n\n- `CommandResult`: 解析结果，包括了源命令 `source: Alconna` ，解析结果 `result: Arparma`，以及可能的输出信息 `output: str | None` 字段\n- `Match`: 匹配项，表示参数是否存在于 `all_matched_args` 内，可用 `Match.available` 判断是否匹配，`Match.result` 获取匹配的值\n- `Query`: 查询项，表示参数是否可由 `Arparma.query` 查询并获得结果，可用 `Query.available` 判断是否查询成功，`Query.result` 获取查询结果\n\n**Alconna** 默认依赖注入的目标参数皆不需要使用依赖注入函数， 该效果对于 `AlconnaMatcher.got_path` 下的 Arg 同样有效：\n\n```python\nasync def handle(\n    result: CommandResult,\n    arp: Arparma,\n    dup: Duplication,\n    source: Alconna,\n    abc: str,  # 类似 Match, 但是若匹配结果不存在对应字段则跳过该 handler\n    foo: Match[str],\n    bar: Query[int] = Query(\"ttt.bar\", 0)  # Query 仍然需要一个默认值来传递 path 参数\n):\n    ...\n```\n\n:::note\n\n如果你更喜欢 Depends 式的依赖注入，`nonebot_plugin_alconna` 同时提供了一系列的依赖注入函数，他们包括：\n\n- `AlconnaResult`: `CommandResult` 类型的依赖注入函数\n- `AlconnaMatches`: `Arparma` 类型的依赖注入函数\n- `AlconnaDuplication`: `Duplication` 类型的依赖注入函数\n- `AlconnaMatch`: `Match` 类型的依赖注入函数，其能够额外传入一个 middleware 函数来处理得到的参数\n- `AlconnaQuery`: `Query` 类型的依赖注入函数，其能够额外传入一个 middleware 函数来处理得到的参数\n- `AlconnaExecResult`: 提供挂载在命令上的 callback 的返回结果 (`Dict[str, Any]`) 的依赖注入函数\n- `AlconnaExtension`: 提供指定类型的 `Extension` 的依赖注入函数\n\n:::\n\n实例:\n\n```python\nfrom nonebot import require\nrequire(\"nonebot_plugin_alconna\")\n\nfrom nonebot_plugin_alconna import (\n    on_alconna,\n    Match,\n    Query,\n    AlconnaMatch,\n    AlcResult\n)\nfrom arclet.alconna import Alconna, Args, Option, Arparma\n\n\ntest = on_alconna(\n    Alconna(\n        \"test\",\n        Option(\"foo\", Args[\"bar\", int]),\n        Option(\"baz\", Args[\"qux\", bool, False])\n    ),\n    auto_send_output=True\n)\n\n@test.handle()\nasync def handle_test1(result: AlcResult):\n    await test.send(f\"matched: {result.matched}\")\n    await test.send(f\"maybe output: {result.output}\")\n\n@test.handle()\nasync def handle_test2(result: Arparma):\n    await test.send(f\"head result: {result.header_result}\")\n    await test.send(f\"args: {result.all_matched_args}\")\n\n@test.handle()\nasync def handle_test3(bar: Match[int] = AlconnaMatch(\"bar\")):\n    if bar.available:\n        await test.send(f\"foo={bar.result}\")\n\n@test.handle()\nasync def handle_test4(qux: Query[bool] = Query(\"baz.qux\", False)):\n    if qux.available:\n        await test.send(f\"baz.qux={qux.result}\")\n```\n\n## 多平台适配\n\n本插件提供了通用消息段标注， 通用消息段序列， 使插件使用者可以忽略平台之间字段的差异\n\n响应器使用示例中使用了消息段标注，其中 `At` 属于通用标注，而 `Image` 属于 `onebot12` 适配器下的标注。\n\n具体介绍和使用请查看 [通用信息组件](./uniseg.mdx#通用消息段)\n\n本插件为以下适配器提供了专门的适配器标注：\n\n| 协议名称                                                            | 路径                                 |\n| ------------------------------------------------------------------- | ------------------------------------ |\n| [OneBot 协议](https://github.com/nonebot/adapter-onebot)            | adapters.onebot11, adapters.onebot12 |\n| [Telegram](https://github.com/nonebot/adapter-telegram)             | adapters.telegram                    |\n| [飞书](https://github.com/nonebot/adapter-feishu)                   | adapters.feishu                      |\n| [GitHub](https://github.com/nonebot/adapter-github)                 | adapters.github                      |\n| [QQ bot](https://github.com/nonebot/adapter-qq)                     | adapters.qq                          |\n| [钉钉](https://github.com/nonebot/adapter-ding)                     | adapters.ding                        |\n| [Dodo](https://github.com/nonebot/adapter-dodo)                     | adapters.dodo                        |\n| [Console](https://github.com/nonebot/adapter-console)               | adapters.console                     |\n| [开黑啦](https://github.com/Tian-que/nonebot-adapter-kaiheila)      | adapters.kook                        |\n| [Mirai](https://github.com/ieew/nonebot_adapter_mirai2)             | adapters.mirai                       |\n| [Ntchat](https://github.com/JustUndertaker/adapter-ntchat)          | adapters.ntchat                      |\n| [MineCraft](https://github.com/17TheWord/nonebot-adapter-minecraft) | adapters.minecraft                   |\n| [BiliBili Live](https://github.com/wwweww/adapter-bilibili)         | adapters.bilibili                    |\n| [Walle-Q](https://github.com/onebot-walle/nonebot_adapter_walleq)   | adapters.onebot12                    |\n| [Discord](https://github.com/nonebot/adapter-discord)               | adapters.discord                     |\n| [Red 协议](https://github.com/nonebot/adapter-red)                  | adapters.red                         |\n| [Satori 协议](https://github.com/nonebot/adapter-satori)            | adapters.satori                      |\n\n## 条件控制\n\n本插件可以通过 `assign` 来控制一个具体的响应函数是否在不满足条件时跳过响应。\n\n```python\n...\nfrom nonebot import require\nrequire(\"nonebot_plugin_alconna\")\n...\n\nfrom arclet.alconna import Alconna, Subcommand, Option, Args\nfrom nonebot_plugin_alconna import on_alconna, CommandResult\n\n\npip = Alconna(\n    \"pip\",\n    Subcommand(\n        \"install\", Args[\"pak\", str],\n        Option(\"--upgrade\"),\n        Option(\"--force-reinstall\")\n    ),\n    Subcommand(\"list\", Option(\"--out-dated\"))\n)\n\npip_cmd = on_alconna(pip)\n\n# 仅在命令为 `pip install pip` 时响应\n@pip_cmd.assign(\"install.pak\", \"pip\")\nasync def update(res: CommandResult):\n    ...\n\n# 仅在命令为 `pip list` 时响应\n@pip_cmd.assign(\"list\")\nasync def list_(res: CommandResult):\n    ...\n\n# 在命令为 `pip install xxx` 时响应\n@pip_cmd.assign(\"install\")\nasync def install(res: CommandResult):\n    ...\n```\n\n此外，使用 `AlconnaMatcher.dispatch` 还能像 `CommandGroup` 一样为每个分发设置独立的 matcher：\n\n```python\nupdate_cmd = pip_cmd.dispatch(\"install.pak\", \"pip\")\n\n@update_cmd.handle()\nasync def update(arp: CommandResult):\n    ...\n```\n\n另外，`AlconnaMatcher` 有类似于 `got` 的 `got_path`：\n\n```python\nfrom nonebot_plugin_alconna import At, Match, UniMessage, on_alconna\n\n\ntest_cmd = on_alconna(Alconna(\"test\", Args[\"target?\", Union[str, At]]))\n\n@test_cmd.handle()\nasync def tt_h(target: Match[Union[str, At]]):\n    if target.available:\n        test_cmd.set_path_arg(\"target\", target.result)\n\n@test_cmd.got_path(\"target\", prompt=\"请输入目标\")\nasync def tt(target: Union[str, At]):\n    await test_cmd.send(UniMessage([\"ok\\n\", target]))\n```\n\n`got_path` 与 `assign`，`Match`，`Query` 等地方一样，都需要指明 `path` 参数 (即对应 Arg 验证的路径)\n\n`got_path` 会获取消息的最后一个消息段并转为 path 对应的类型，例如示例中 `target` 对应的 Arg 里要求 str 或 At，则 got 后用户输入的消息只有为 text 或 at 才能进入处理函数。\n\n:::tip\n\n`path` 支持 ~XXX 语法，其会把 ~ 替换为可能的父级路径：\n\n```python\n pip = Alconna(\n     \"pip\",\n     Subcommand(\n         \"install\",\n         Args[\"pak\", str],\n         Option(\"--upgrade|-U\"),\n         Option(\"--force-reinstall\"),\n     ),\n     Subcommand(\"list\", Option(\"--out-dated\")),\n )\n\n pipcmd = on_alconna(pip)\n pip_install_cmd = pipcmd.dispatch(\"install\")\n\n\n @pip_install_cmd.assign(\"~upgrade\")\n async def pip1_u(pak: Query[str] = Query(\"~pak\")):\n     await pip_install_cmd.finish(f\"pip upgrading {pak.result}...\")\n```\n\n:::\n\n## 响应器创建装饰\n\n本插件提供了一个 `funcommand` 装饰器, 其用于将一个接受任意参数， 返回 `str` 或 `Message` 或 `MessageSegment` 的函数转换为命令响应器：\n\n```python\nfrom nonebot_plugin_alconna import funcommand\n\n\n@funcommand()\nasync def echo(msg: str):\n    return msg\n```\n\n其等同于：\n\n```python\nfrom arclet.alconna import Alconna, Args\nfrom nonebot_plugin_alconna import on_alconna, AlconnaMatch, Match\n\n\necho = on_alconna(Alconna(\"echo\", Args[\"msg\", str]))\n\n@echo.handle()\nasync def echo_exit(msg: Match[str] = AlconnaMatch(\"msg\")):\n    await echo.finish(msg.result)\n\n```\n\n## 类Koishi构造器\n\n本插件提供了一个 `Command` 构造器，其基于 `arclet.alconna.tools` 中的 `AlconnaString`， 以类似 `Koishi` 中注册命令的方式来构建一个 **AlconnaMatcher** ：\n\n```python\nfrom nonebot_plugin_alconna import Command, Arparma\n\n\nbook = (\n    Command(\"book\", \"测试\")\n    .option(\"writer\", \"-w <id:int>\")\n    .option(\"writer\", \"--anonymous\", {\"id\": 0})\n    .usage(\"book [-w <id:int> | --anonymous]\")\n    .shortcut(\"测试\", {\"args\": [\"--anonymous\"]})\n    .build()\n)\n\n@book.handle()\nasync def _(arp: Arparma):\n    await book.send(str(arp.options))\n```\n\n甚至，你可以设置 `action` 来设定响应行为：\n\n```python\nbook = (\n    Command(\"book\", \"测试\")\n    .option(\"writer\", \"-w <id:int>\")\n    .option(\"writer\", \"--anonymous\", {\"id\": 0})\n    .usage(\"book [-w <id:int> | --anonymous]\")\n    .shortcut(\"测试\", {\"args\": [\"--anonymous\"]})\n    .action(lambda options: str(options))  # 会自动通过 bot.send 发送\n    .build()\n)\n```\n\n## 返回值中间件\n\n在 `AlconnaMatch`，`AlconnaQuery` 或 `got_path` 中，你可以使用 `middleware` 参数来传入一个对返回值进行处理的函数：\n\n```python\nfrom nonebot_plugin_alconna import image_fetch\n\n\nmask_cmd = on_alconna(\n    Alconna(\"search\", Args[\"img?\", Image]),\n)\n\n\n@mask_cmd.handle()\nasync def mask_h(matcher: AlconnaMatcher, img: Match[bytes] = AlconnaMatch(\"img\", image_fetch)):\n    result = await search_img(img.result)\n    await matcher.send(result.content)\n```\n\n其中，`image_fetch` 是一个中间件，其接受一个 `Image` 对象，并提取图片的二进制数据返回。\n\n## 匹配拓展\n\n本插件提供了一个 `Extension` 类，其用于自定义 AlconnaMatcher 的部分行为\n\n例如一个 `LLMExtension` 可以如下实现 (仅举例)：\n\n```python\nfrom nonebot_plugin_alconna import Extension, Alconna, on_alconna, Interface\n\n\nclass LLMExtension(Extension):\n    @property\n    def priority(self) -> int:\n        return 10\n\n    @property\n    def id(self) -> str:\n        return \"LLMExtension\"\n\n    def __init__(self, llm):\n      self.llm = llm\n\n    def post_init(self, alc: Alconna) -> None:\n        self.llm.add_context(alc.command, alc.meta.description)\n\n    async def receive_wrapper(self, bot, event, receive):\n        resp = await self.llm.input(str(receive))\n        return receive.__class__(resp.content)\n\n    def before_catch(self, name, annotation, default):\n        return name == \"llm\"\n\n    def catch(self, interface: Interface):\n        if interface.name == \"llm\":\n            return self.llm\n\nmatcher = on_alconna(\n    Alconna(...),\n    extensions=[LLMExtension(LLM)]\n)\n...\n```\n\n那么添加了 `LLMExtension` 的响应器便能接受任何能通过 llm 翻译为具体命令的自然语言消息，同时可以在响应器中为所有 `llm` 参数注入模型变量。\n\n目前 `Extension` 的功能有：\n\n- `validate`: 对于事件的来源适配器或 bot 选择是否接受响应\n- `output_converter`: 输出信息的自定义转换方法\n- `message_provider`: 从传入事件中自定义提取消息的方法\n- `receive_provider`: 对传入的消息 (Message 或 UniMessage) 的额外处理\n- `context_provider`: 对命令上下文的额外处理\n- `permission_check`: 命令对消息解析并确认头部匹配（即确认选择响应）时对发送者的权限判断\n- `parse_wrapper`: 对命令解析结果的额外处理\n- `send_wrapper`: 对发送的消息 (Message 或 UniMessage) 的额外处理\n- `before_catch`: 自定义依赖注入的绑定确认函数\n- `catch`: 自定义依赖注入处理函数\n- `post_init`: 响应器创建后对命令对象的额外处理\n\n例如内置的 `DiscordSlashExtension`，其可自动将 Alconna 对象翻译成 slash 指令并注册，且将收到的指令交互事件转为指令供命令解析：\n\n```python\nfrom nonebot_plugin_alconna import Match, on_alconna\nfrom nonebot_plugin_alconna.builtins.extensions.discord import DiscordSlashExtension\n\n\nalc = Alconna(\n    [\"/\"],\n    \"permission\",\n    Subcommand(\"add\", Args[\"plugin\", str][\"priority?\", int]),\n    Option(\"remove\", Args[\"plugin\", str][\"time?\", int]),\n    meta=CommandMeta(description=\"权限管理\"),\n)\n\nmatcher = on_alconna(alc, extensions=[DiscordSlashExtension()])\n\n@matcher.assign(\"add\")\nasync def add(plugin: Match[str], priority: Match[int]):\n    await matcher.finish(f\"added {plugin.result} with {priority.result if priority.available else 0}\")\n\n@matcher.assign(\"remove\")\nasync def remove(plugin: Match[str], time: Match[int]):\n    await matcher.finish(f\"removed {plugin.result} with {time.result if time.available else -1}\")\n```\n\n目前插件提供了 4 个内置的 `Extension`，它们在 `nonebot_plugin_alconna.builtins.extensions` 下：\n\n- `ReplyRecordExtension`: 将消息事件中的回复暂存在 extension 中，使得解析用的消息不带回复信息，同时可以在后续的处理中获取回复信息。\n- `DiscordSlashExtension`: 将 Alconna 的命令自动转换为 Discord 的 Slash Command，并将 Slash Command 的交互事件转换为消息交给 Alconna 处理。\n- `MarkdownOutputExtension`: 将 Alconna 的自动输出转换为 Markdown 格式\n- `TelegramSlashExtension`: 将 Alconna 的命令注册在 Telegram 上以获得提示。\n\n:::tip\n\n全局的 Extension 可延迟加载 (即若有全局拓展加载于部分 AlconnaMatcher 之后，这部分响应器会被追加拓展)\n\n:::\n\n## 补全会话\n\n补全会话基于 [`半自动补全`](./command.md#半自动补全)，用于指令参数缺失或参数错误时给予交互式提示，类似于 `got-reject`：\n\n```python\nfrom nonebot_plugin_alconna import Alconna, Args, Field, At, on_alconna\n\nalc = Alconna(\n    \"添加教师\",\n    Args[\"name\", str, Field(completion=lambda: \"请输入姓名\")],\n    Args[\"phone\", int, Field(completion=lambda: \"请输入手机号\")],\n    Args[\"at\", [str, At], Field(completion=lambda: \"请输入教师号\")],\n)\n\ncmd = on_alconna(alc, comp_config={\"lite\": True}, skip_for_unmatch=False)\n\n@cmd.handle()\nasync def handle(result: Arparma):\n    cmd.finish(\"添加成功\")\n```\n\n此时，当用户输入 `添加教师` 时，会自动提示用户输入姓名，手机号和教师号，用户输入后会自动进入下一个提示：\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"添加教师\" },\n    { position: \"left\", msg: \"以下是建议的输入： \\n- name: 请输入姓名\" },\n    { position: \"right\", msg: \"foo\" },\n    { position: \"left\", msg: \"以下是建议的输入： \\n- phone: 请输入手机号\" },\n    { position: \"right\", msg: \"12345\" },\n    { position: \"left\", msg: \"以下是建议的输入： \\n- at: 请输入教师号\" },\n    { position: \"right\", msg: \"@me\" },\n    { position: \"left\", msg: \"添加成功\" },\n  ]}\n/>\n\n补全会话配置如下：\n\n```python\nclass CompConfig(TypedDict):\n    tab: NotRequired[str]\n    \"\"\"用于切换提示的指令的名称\"\"\"\n    enter: NotRequired[str]\n    \"\"\"用于输入提示的指令的名称\"\"\"\n    exit: NotRequired[str]\n    \"\"\"用于退出会话的指令的名称\"\"\"\n    timeout: NotRequired[int]\n    \"\"\"超时时间\"\"\"\n    hide_tabs: NotRequired[bool]\n    \"\"\"是否隐藏所有提示\"\"\"\n    hides: NotRequired[Set[Literal[\"tab\", \"enter\", \"exit\"]]]\n    \"\"\"隐藏的指令\"\"\"\n    disables: NotRequired[Set[Literal[\"tab\", \"enter\", \"exit\"]]]\n    \"\"\"禁用的指令\"\"\"\n    lite: NotRequired[bool]\n    \"\"\"是否使用简洁版本的补全会话（相当于同时配置 disables、hides、hide_tabs）\"\"\"\n```\n\n## 内置插件\n\n类似于 Nonebot 本身提供的内置插件，`nonebot_plugin_alconna` 提供了两个内置插件：`echo` 和 `help`。\n\n你可以用本插件的 `load_builtin_plugin(s)` 来加载它们：\n\n```python\nfrom nonebot_plugin_alconna import load_builtin_plugins\n\nload_builtin_plugins(\"echo\", \"help\")\n```\n\n其中 `help` 仅能列出所有 Alconna 指令。\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/帮助\" },\n    {\n      position: \"left\",\n      msg: \"# 当前可用的命令有:\\n 0 /echo : echo 指令\\n 1 /help : 显示所有命令帮助\\n# 输入'命令名 -h|--help' 查看特定命令的语法\",\n    },\n    { position: \"right\", msg: \"/echo [图片]\" },\n    { position: \"left\", msg: \"[图片]\" },\n  ]}\n/>\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/best-practice/alconna/uniseg.mdx",
    "content": "---\nsidebar_position: 5\ndescription: 通用消息组件\n---\n\nimport Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\n\n# 通用消息组件\n\n`uniseg` 模块属于 `nonebot-plugin-alconna` 的子插件，其提供了一套通用的消息组件，用于在 `nonebot-plugin-alconna` 下构建通用消息。\n\n## 通用消息段\n\n适配器下的消息段标注会匹配适配器特定的 `MessageSegment`， 而通用消息段与适配器消息段的区别在于：\n通用消息段会匹配多个适配器中相似类型的消息段，并返回 `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), 以达到**跨平台接收消息**的作用。\n\n`nonebot-plugin-alconna.uniseg` 提供了类似 `MessageSegment` 的通用消息段，并可在 `Alconna` 下直接标注使用：\n\n```python\nclass Segment:\n    \"\"\"基类标注\"\"\"\n    children: List[\"Segment\"]\n\nclass Text(Segment):\n    \"\"\"Text对象, 表示一类文本元素\"\"\"\n    text: str\n    styles: Dict[Tuple[int, int], List[str]]\n\nclass At(Segment):\n    \"\"\"At对象, 表示一类提醒某用户的元素\"\"\"\n    flag: Literal[\"user\", \"role\", \"channel\"]\n    target: str\n    display: Optional[str]\n\nclass AtAll(Segment):\n    \"\"\"AtAll对象, 表示一类提醒所有人的元素\"\"\"\n    here: bool\n\nclass Emoji(Segment):\n    \"\"\"Emoji对象, 表示一类表情元素\"\"\"\n    id: str\n    name: Optional[str]\n\nclass Media(Segment):\n    url: Optional[str]\n    id: Optional[str]\n    path: Optional[Union[str, Path]]\n    raw: Optional[Union[bytes, BytesIO]]\n    mimetype: Optional[str]\n    name: str\n\n    to_url: ClassVar[Optional[MediaToUrl]]\n\nclass Image(Media):\n    \"\"\"Image对象, 表示一类图片元素\"\"\"\n\nclass Audio(Media):\n    \"\"\"Audio对象, 表示一类音频元素\"\"\"\n    duration: Optional[int]\n\nclass Voice(Media):\n    \"\"\"Voice对象, 表示一类语音元素\"\"\"\n    duration: Optional[int]\n\nclass Video(Media):\n    \"\"\"Video对象, 表示一类视频元素\"\"\"\n\nclass File(Segment):\n    \"\"\"File对象, 表示一类文件元素\"\"\"\n    id: str\n    name: Optional[str]\n\nclass Reply(Segment):\n    \"\"\"Reply对象，表示一类回复消息\"\"\"\n    id: str\n    \"\"\"此处不一定是消息ID，可能是其他ID，如消息序号等\"\"\"\n    msg: Optional[Union[Message, str]]\n    origin: Optional[Any]\n\nclass Reference(Segment):\n    \"\"\"Reference对象，表示一类引用消息。转发消息 (Forward) 也属于此类\"\"\"\n    id: Optional[str]\n    \"\"\"此处不一定是消息ID，可能是其他ID，如消息序号等\"\"\"\n    children: List[Union[RefNode, CustomNode]]\n\nclass Hyper(Segment):\n    \"\"\"Hyper对象，表示一类超级消息。如卡片消息、ark消息、小程序等\"\"\"\n    format: Literal[\"xml\", \"json\"]\n    raw: Optional[str]\n    content: Optional[Union[dict, list]]\n\nclass Other(Segment):\n    \"\"\"其他 Segment\"\"\"\n    origin: MessageSegment\n\n```\n\n:::tip\n\n或许你注意到了 `Segment` 上有一个 `children` 属性。\n\n这是因为在 [`Satori`](https://satori.js.org/zh-CN/) 协议的规定下，一类元素可以用其子元素来代表一类兼容性消息\n（例如，qq 的商场表情在某些平台上可以用图片代替）。\n\n为此，本插件提供了两种方式来表达 \"获取子元素\" 的方法：\n\n```python\nfrom nonebot_plugin_alconna.builtins.uniseg.chronocat import MarketFace\nfrom nonebot_plugin_alconna import Args, Image, Alconna, select, select_first\n\n# 表示这个指令需要的图片要么直接是 Image 要么是在 MarketFace 元素内的 Image\nalc1 = Alconna(\"make_meme\", Args[\"img\", [Image, Image.from_(MarketFace)]])\n\n# 表示这个指令需要的图片会在目标元素下进行搜索，将所有符合 Image 的元素选出来并将第一个作为结果\nalc2 = Alconna(\"make_meme\", Args[\"img\", select(Image, index=0)])  # 也可以使用 select_first(Image)\n```\n\n:::\n\n## 通用消息序列\n\n`nonebot-plugin-alconna.uniseg` 同时提供了一个类似于 `Message` 的 `UniMessage` 类型，其元素为经过通用标注转换后的通用消息段。\n\n你可以用如下方式获取 `UniMessage`：\n\n<Tabs groupId=\"get_unimsg\">\n<TabItem value=\"depend\" label=\"使用依赖注入\">\n\n通过提供的 `UniversalMessage` 或 `UniMsg` 依赖注入器来获取 `UniMessage`。\n\n```python\nfrom nonebot_plugin_alconna.uniseg import UniMsg, At, Reply\n\n\nmatcher = on_xxx(...)\n\n@matcher.handle()\nasync def _(msg: UniMsg):\n    reply = msg[Reply, 0]\n    print(reply.origin)\n    if msg.has(At):\n        ats = msg.get(At)\n        print(ats)\n    ...\n```\n\n</TabItem>\n<TabItem value=\"method\" label=\"使用 UniMessage.generate\">\n\n注意，`generate` 方法在响应器以外的地方如果不传入 `event` 与 `bot` 则无法处理 reply。\n\n```python\nfrom nonebot import Message, EventMessage\nfrom nonebot_plugin_alconna.uniseg import UniMessage\n\n\nmatcher = on_xxx(...)\n\n@matcher.handle()\nasync def _(message: Message = EventMessage()):\n    msg = await UniMessage.generate(message=message)\n    msg1 = UniMessage.generate_without_reply(message=message)\n```\n\n</TabItem>\n</Tabs>\n\n不仅如此，你还可以通过 `UniMessage` 的 `export` 与 `send` 方法来**跨平台发送消息**。\n\n`UniMessage.export` 会通过传入的 `bot: Bot` 参数，或上下文中的 `Bot` 对象读取适配器信息，并使用对应的生成方法把通用消息转为适配器对应的消息序列：\n\n```python\nfrom nonebot import Bot, on_command\nfrom nonebot_plugin_alconna.uniseg import Image, UniMessage\n\n\ntest = on_command(\"test\")\n\n@test.handle()\nasync def handle_test():\n    await test.send(await UniMessage(Image(path=\"path/to/img\")).export())\n```\n\n除此之外 `UniMessage.send` 方法基于 `UniMessage.export` 并调用各适配器下的发送消息方法，返回一个 `Receipt` 对象，用于修改/撤回消息：\n\n```python\nfrom nonebot import Bot, on_command\nfrom nonebot_plugin_alconna.uniseg import UniMessage\n\n\ntest = on_command(\"test\")\n\n@test.handle()\nasync def handle():\n    receipt = await UniMessage.text(\"hello!\").send(at_sender=True, reply_to=True)\n    await receipt.recall(delay=1)\n```\n\n而在 `AlconnaMatcher` 下，`got`, `send`, `reject` 等可以发送消息的方法皆支持使用 `UniMessage`，不需要手动调用 export 方法：\n\n```python\nfrom arclet.alconna import Alconna, Args\nfrom nonebot_plugin_alconna import Match, AlconnaMatcher, on_alconna\nfrom nonebot_plugin_alconna.uniseg import At,  UniMessage\n\n\ntest_cmd = on_alconna(Alconna(\"test\", Args[\"target?\", At]))\n\n@test_cmd.handle()\nasync def tt_h(matcher: AlconnaMatcher, target: Match[At]):\n    if target.available:\n        matcher.set_path_arg(\"target\", target.result)\n\n@test_cmd.got_path(\"target\", prompt=\"请输入目标\")\nasync def tt(target: At):\n    await test_cmd.send(UniMessage([target, \"\\ndone.\"]))\n```\n\n:::caution\n\n在响应器以外的地方，除非启用了 `alconna_apply_fetch_targets` 配置项，否则 `bot` 参数必须手动传入。\n\n:::\n\n### 构造\n\n如同 `Message`, `UniMessage` 可以传入单个字符串/消息段，或可迭代的字符串/消息段：\n\n```python\nfrom nonebot_plugin_alconna.uniseg import UniMessage, At\n\n\nmsg = UniMessage(\"Hello\")\nmsg1 = UniMessage(At(\"user\", \"124\"))\nmsg2 = UniMessage([\"Hello\", At(\"user\", \"124\")])\n```\n\n`UniMessage` 上同时存在便捷方法，令其可以链式地添加消息段：\n\n```python\nfrom nonebot_plugin_alconna.uniseg import UniMessage, At, Image\n\n\nmsg = UniMessage.text(\"Hello\").at(\"124\").image(path=\"/path/to/img\")\nassert msg == UniMessage(\n    [\"Hello\", At(\"user\", \"124\"), Image(path=\"/path/to/img\")]\n)\n```\n\n### 拼接消息\n\n`str`、`UniMessage`、`Segment` 对象之间可以直接相加，相加均会返回一个新的 `UniMessage` 对象：\n\n```python\n# 消息序列与消息段相加\nUniMessage(\"text\") + Text(\"text\")\n# 消息序列与字符串相加\nUniMessage([Text(\"text\")]) + \"text\"\n# 消息序列与消息序列相加\nUniMessage(\"text\") + UniMessage([Text(\"text\")])\n# 字符串与消息序列相加\n\"text\" + UniMessage([Text(\"text\")])\n# 消息段与消息段相加\nText(\"text\") + Text(\"text\")\n# 消息段与字符串相加\nText(\"text\") + \"text\"\n# 消息段与消息序列相加\nText(\"text\") + UniMessage([Text(\"text\")])\n# 字符串与消息段相加\n\"text\" + Text(\"text\")\n```\n\n如果需要在当前消息序列后直接拼接新的消息段，可以使用 `Message.append`、`Message.extend` 方法，或者使用自加：\n\n```python\nmsg = UniMessage([Text(\"text\")])\n# 自加\nmsg += \"text\"\nmsg += Text(\"text\")\nmsg += UniMessage([Text(\"text\")])\n# 附加\nmsg.append(Text(\"text\"))\n# 扩展\nmsg.extend([Text(\"text\")])\n```\n\n### 使用消息模板\n\n`UniMessage.template` 同样类似于 `Message.template`，可以用于格式化消息，大体用法参考 [消息模板](../../tutorial/message#使用消息模板)。\n\n这里额外说明 `UniMessage.template` 的拓展控制符\n\n相比 `Message`，UniMessage 对于 `{:XXX}` 做了另一类拓展。其能够识别例如 At(xxx, yyy) 或 Emoji(aaa, bbb)的字符串并执行\n\n以 At(...) 为例：\n\n```python title=使用通用消息段的拓展控制符\n>>> from nonebot_plugin_alconna.uniseg import UniMessage\n>>>  UniMessage.template(\"{:At(user, target)}\").format(target=\"123\")\nUniMessage(At(\"user\", \"123\"))\n>>> UniMessage.template(\"{:At(type=user, target=id)}\").format(id=\"123\")\nUniMessage(At(\"user\", \"123\"))\n>>> UniMessage.template(\"{:At(type=user, target=123)}\").format()\nUniMessage(At(\"user\", \"123\"))\n```\n\n而在 `AlconnaMatcher` 中，`{:XXX}` 更进一步地提供了获取 `event` 和 `bot` 中的属性的功能：\n\n```python title=在AlconnaMatcher中使用通用消息段的拓展控制符\nfrom arclet.alconna import Alconna, Args\nfrom nonebot_plugin_alconna import At, Match, UniMessage, AlconnaMatcher, on_alconna\n\n\ntest_cmd = on_alconna(Alconna(\"test\", Args[\"target?\", At]))\n\n@test_cmd.handle()\nasync def tt_h(matcher: AlconnaMatcher, target: Match[At]):\n    if target.available:\n        matcher.set_path_arg(\"target\", target.result)\n\n@test_cmd.got_path(\n    \"target\",\n    prompt=UniMessage.template(\"{:At(user, $event.get_user_id())} 请确认目标\")\n)\nasync def tt():\n    await test_cmd.send(\n      UniMessage.template(\"{:At(user, $event.get_user_id())} 已确认目标为 {target}\")\n    )\n```\n\n另外也有 `$message_id` 与 `$target` 两个特殊值。\n\n### 检查消息段\n\n我们可以通过 `in` 运算符或消息序列的 `has` 方法来：\n\n```python\n# 是否存在消息段\nAt(\"user\", \"1234\") in message\n# 是否存在指定类型的消息段\nAt in message\n```\n\n我们还可以使用 `only` 方法来检查消息中是否仅包含指定的消息段：\n\n```python\n# 是否都为 \"test\"\nmessage.only(\"test\")\n# 是否仅包含指定类型的消息段\nmessage.only(Text)\n```\n\n### 获取消息纯文本\n\n类似于 `Message.extract_plain_text()`，用于获取通用消息的纯文本：\n\n```python\nfrom nonebot_plugin_alconna.uniseg import UniMessage, At\n\n\n# 提取消息纯文本字符串\nassert UniMessage(\n    [At(\"user\", \"1234\"), \"text\"]\n).extract_plain_text() == \"text\"\n```\n\n### 遍历\n\n通用消息序列继承自 `List[Segment]` ，因此可以使用 `for` 循环遍历消息段：\n\n```python\nfor segment in message:  # type: Segment\n    ...\n```\n\n### 过滤、索引与切片\n\n消息序列对列表的索引与切片进行了增强，在原有列表 `int` 索引与 `slice` 切片的基础上，支持 `type` 过滤索引与切片：\n\n```python\nfrom nonebot_plugin_alconna.uniseg import UniMessage, At, Text, Reply\n\n\nmessage = UniMessage(\n    [\n        Reply(...),\n        \"text1\",\n        At(\"user\", \"1234\"),\n        \"text2\"\n    ]\n)\n# 索引\nmessage[0] == Reply(...)\n# 切片\nmessage[0:2] == UniMessage([Reply(...), Text(\"text1\")])\n# 类型过滤\nmessage[At] == Message([At(\"user\", \"1234\")])\n# 类型索引\nmessage[At, 0] == At(\"user\", \"1234\")\n# 类型切片\nmessage[Text, 0:2] == UniMessage([Text(\"text1\"), Text(\"text2\")])\n```\n\n我们也可以通过消息序列的 `include`、`exclude` 方法进行类型过滤：\n\n```python\nmessage.include(Text, At)\nmessage.exclude(Reply)\n```\n\n同样的，消息序列对列表的 `index`、`count` 方法也进行了增强，可以用于索引指定类型的消息段：\n\n```python\n# 指定类型首个消息段索引\nmessage.index(Text) == 1\n# 指定类型消息段数量\nmessage.count(Text) == 2\n```\n\n此外，消息序列添加了一个 `get` 方法，可以用于获取指定类型指定个数的消息段：\n\n```python\n# 获取指定类型指定个数的消息段\nmessage.get(Text, 1) == UniMessage([Text(\"test1\")])\n```\n\n## 消息发送\n\n前面提到，通用消息可用 `UniMessage.send` 发送自身：\n\n```python\nasync def send(\n    self,\n    target: Union[Event, Target, None] = None,\n    bot: Optional[Bot] = None,\n    fallback: bool = True,\n    at_sender: Union[str, bool] = False,\n    reply_to: Union[str, bool] = False,\n) -> Receipt:\n```\n\n实际上，`UniMessage` 同时提供了获取消息事件 id 与消息发送对象的方法:\n\n<Tabs groupId=\"get_unimsg\">\n<TabItem value=\"depend\" label=\"使用依赖注入\">\n\n通过提供的 `MessageTarget`, `MessageId` 或 `MsgTarget`, `MsgId` 依赖注入器来获取消息事件 id 与消息发送对象。\n\n```python\nfrom nonebot_plugin_alconna.uniseg import MessageId, MsgTarget\n\n\nmatcher = on_xxx(...)\n\n@matcher.handle()\nasycn def _(target: MsgTarget, msg_id: MessageId):\n    ...\n```\n\n</TabItem>\n<TabItem value=\"method\" label=\"使用 UniMessage 的方法\">\n\n```python\nfrom nonebot import Event, Bot\nfrom nonebot_plugin_alconna.uniseg import UniMessage, Target\n\n\nmatcher = on_xxx(...)\n\n@matcher.handle()\nasycn def _(bot: Bot, event: Event):\n    target: Target = UniMessage.get_target(event, bot)\n    msg_id: str = UniMessage.get_message_id(event, bot)\n\n```\n\n</TabItem>\n</Tabs>\n\n`send`, `get_target`, `get_message_id` 中与 `event`, `bot` 相关的参数都会尝试从上下文中获取对象。\n\n### 消息发送对象\n\n消息发送对象是用来描述响应消息时的发送对象或者主动发送消息时的目标对象的对象，它包含了以下属性：\n\n```python\nclass Target:\n    id: str\n    \"\"\"目标id；若为群聊则为group_id或者channel_id，若为私聊则为user_id\"\"\"\n    parent_id: str\n    \"\"\"父级id；若为频道则为guild_id，其他情况下可能为空字符串（例如 Feishu 下可作为部门 id）\"\"\"\n    channel: bool\n    \"\"\"是否为频道，仅当目标平台符合频道概念时\"\"\"\n    private: bool\n    \"\"\"是否为私聊\"\"\"\n    source: str\n    \"\"\"可能的事件id\"\"\"\n    self_id: Union[str, None]\n    \"\"\"机器人id，若为 None 则 Bot 对象会随机选择\"\"\"\n    selector: Union[Callable[[Bot], Awaitable[bool]], None]\n    \"\"\"选择器，用于在多个 Bot 对象中选择特定 Bot\"\"\"\n    extra: Dict[str, Any]\n    \"\"\"额外信息，用于适配器扩展\"\"\"\n```\n\n其构造时需要如下参数：\n\n- `id` 为目标id；若为群聊则为 group_id 或者 channel_id，若为私聊则为user_id\n- `parent_id` 为父级id；若为频道则为 guild_id，其他情况下可能为空字符串（例如 Feishu 下可作为部门 id）\n- `channel` 为是否为频道，仅当目标平台符合频道概念时\n- `private` 为是否为私聊\n- `source` 为可能的事件id\n- `self_id` 为机器人id，若为 None 则 Bot 对象会随机选择\n- `selector` 为选择器，用于在多个 Bot 对象中选择特定 Bot\n- `scope` 为适配器范围，用于传入内置的特定选择器\n- `adapter` 为适配器名称，若为 None 则需要明确指定 Bot 对象\n- `platform` 为平台名称，仅当目标适配器存在多个平台时使用\n- `extra` 为额外信息，用于适配器扩展\n\n通过 `Target` 对象，我们可以在 `UniMessage.send` 中指定发送对象：\n\n```python\nfrom nonebot_plugin_alconna.uniseg import UniMessage, MsgTarget, Target, SupportScope\n\n\nmatcher = on_xxx(...)\n\n@matcher.handle()\nasync def _(target: MsgTarget):\n    await UniMessage(\"Hello!\").send(target=target)\n    target1 = Target(\"xxxx\", scope=SupportScope.qq_client)\n    await UniMessage(\"Hello!\").send(target=target1)\n```\n\n### 主动发送消息\n\n`UniMessage.send` 也可以用于主动发送消息：\n\n```python\nfrom nonebot_plugin_alconna.uniseg import UniMessage, Target, SupportScope\nfrom nonebot import get_driver\n\n\ndriver = get_driver()\n\n@driver.on_startup\nasync def on_startup():\n    target = Target(\"xxxx\", scope=SupportScope.qq_client)\n    await UniMessage(\"Hello!\").send(target=target)\n```\n\n## 自定义消息段\n\n`uniseg` 提供了部分方法来允许用户自定义 Segment 的序列化和反序列化：\n\n```python\nfrom dataclasses import dataclass\n\nfrom nonebot.adapters import Bot\nfrom nonebot.adapters import MessageSegment as BaseMessageSegment\nfrom nonebot.adapters.satori import Custom, Message, MessageSegment\n\nfrom nonebot_plugin_alconna.uniseg.builder import MessageBuilder\nfrom nonebot_plugin_alconna.uniseg.exporter import MessageExporter\nfrom nonebot_plugin_alconna.uniseg import Segment, custom_handler, custom_register\n\n\n@dataclass\nclass MarketFace(Segment):\n    tabId: str\n    faceId: str\n    key: str\n\n\n@custom_register(MarketFace, \"chronocat:marketface\")\ndef mfbuild(builder: MessageBuilder, seg: BaseMessageSegment):\n    if not isinstance(seg, Custom):\n        raise ValueError(\"MarketFace can only be built from Satori Message\")\n    return MarketFace(**seg.data)(*builder.generate(seg.children))\n\n\n@custom_handler(MarketFace)\nasync def mfexport(exporter: MessageExporter, seg: MarketFace, bot: Bot, fallback: bool):\n    if exporter.get_message_type() is Message:\n        return MessageSegment(\"chronocat:marketface\", seg.data)(await exporter.export(seg.children, bot, fallback))\n\n```\n\n具体而言，你可以使用 `custom_register` 来增加一个从 MessageSegment 到 Segment 的处理方法；使用 `custom_handler` 来增加一个从 Segment 到 MessageSegment 的处理方法。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/best-practice/data-storing.md",
    "content": "---\nsidebar_position: 1\ndescription: 存储数据文件到本地\n---\n\n# 数据存储\n\n在使用插件的过程中，难免会需要存储一些持久化数据，例如用户的个人信息、群组的信息等。除了使用数据库等第三方存储之外，还可以使用本地文件来自行管理数据。NoneBot 提供了 `nonebot-plugin-localstore` 插件，可用于获取正确的数据存储路径并写入数据。\n\n## 安装插件\n\n在使用前请先安装 `nonebot-plugin-localstore` 插件至项目环境中，可参考[获取商店插件](../tutorial/store.mdx#安装插件)来了解并选择安装插件的方式。如：\n\n在**项目目录**下执行以下命令：\n\n```bash\nnb plugin install nonebot-plugin-localstore\n```\n\n## 使用插件\n\n`nonebot-plugin-localstore` 插件兼容 Windows、Linux 和 macOS 等操作系统，使用时无需关心操作系统的差异。同时插件提供 `nb-cli` 脚本，可以使用 `nb localstore` 命令来检查数据存储路径。\n\n在使用本插件前同样需要使用 `require` 方法进行**加载**并**导入**需要使用的方法，可参考 [跨插件访问](../advanced/requiring.md) 一节进行了解，如：\n\n```python\nfrom nonebot import require\n\nrequire(\"nonebot_plugin_localstore\")\n\nimport nonebot_plugin_localstore as store\n\n# 获取插件缓存目录\ncache_dir = store.get_plugin_cache_dir()\n# 获取插件缓存文件\ncache_file = store.get_plugin_cache_file(\"file_name\")\n# 获取插件数据目录\ndata_dir = store.get_plugin_data_dir()\n# 获取插件数据文件\ndata_file = store.get_plugin_data_file(\"file_name\")\n# 获取插件配置目录\nconfig_dir = store.get_plugin_config_dir()\n# 获取插件配置文件\nconfig_file = store.get_plugin_config_file(\"file_name\")\n```\n\n:::danger 警告\n在 Windows 和 macOS 系统下，插件的数据目录和配置目录是同一个目录，因此在使用时需要注意避免文件名冲突。\n:::\n\n插件提供的方法均返回一个 `pathlib.Path` 路径，可以参考 [pathlib 文档](https://docs.python.org/zh-cn/3/library/pathlib.html)来了解如何使用。常用的方法有：\n\n```python\nfrom pathlib import Path\n\ndata_file = store.get_plugin_data_file(\"file_name\")\n# 写入文件内容\ndata_file.write_text(\"Hello World!\")\n# 读取文件内容\ndata = data_file.read_text()\n```\n\n:::note 提示\n\n对于嵌套插件，子插件的存储目录将位于父插件存储目录下。\n\n:::\n\n## 配置项\n\n### localstore_use_cwd\n\n使用当前工作目录作为数据存储目录，以下数据目录配置项默认值将会对应变更\n\n默认值：`False`\n\n```dotenv\nLOCALSTORE_USE_CWD=true\n```\n\n### localstore_cache_dir\n\n自定义缓存目录\n\n默认值：\n\n当 `localstore_use_cwd` 为 `True` 时，缓存目录为 `<current_working_directory>/cache`，否则：\n\n- macOS: `~/Library/Caches/nonebot2`\n- Unix: `~/.cache/nonebot2` (XDG default)\n- Windows: `C:\\Users\\<username>\\AppData\\Local\\nonebot2\\Cache`\n\n```dotenv\nLOCALSTORE_CACHE_DIR=/tmp/cache\n```\n\n### localstore_data_dir\n\n自定义数据目录\n\n默认值：\n\n当 `localstore_use_cwd` 为 `True` 时，数据目录为 `<current_working_directory>/data`，否则：\n\n- macOS: `~/Library/Application Support/nonebot2`\n- Unix: `~/.local/share/nonebot2` or in $XDG_DATA_HOME, if defined\n- Win XP (not roaming): `C:\\Documents and Settings\\<username>\\Application Data\\nonebot2`\n- Win 7 (not roaming): `C:\\Users\\<username>\\AppData\\Local\\nonebot2`\n\n```dotenv\nLOCALSTORE_DATA_DIR=/tmp/data\n```\n\n### localstore_config_dir\n\n自定义配置目录\n\n默认值：\n\n当 `localstore_use_cwd` 为 `True` 时，配置目录为 `<current_working_directory>/config`，否则：\n\n- macOS: same as user_data_dir\n- Unix: `~/.config/nonebot2`\n- Win XP (roaming): `C:\\Documents and Settings\\<username>\\Local Settings\\Application Data\\nonebot2`\n- Win 7 (roaming): `C:\\Users\\<username>\\AppData\\Roaming\\nonebot2`\n\n```dotenv\nLOCALSTORE_CONFIG_DIR=/tmp/config\n```\n\n### localstore_plugin_cache_dir\n\n自定义插件缓存目录\n\n默认值：`{}`\n\n```dotenv\nLOCALSTORE_PLUGIN_CACHE_DIR='\n{\n  \"plugin_id\": \"/tmp/plugin_cache\"\n}\n'\n```\n\n### localstore_plugin_data_dir\n\n自定义插件数据目录\n\n默认值：`{}`\n\n```dotenv\nLOCALSTORE_PLUGIN_DATA_DIR='\n{\n  \"plugin_id\": \"/tmp/plugin_data\"\n}\n'\n```\n\n### localstore_plugin_config_dir\n\n自定义插件配置目录\n\n默认值：`{}`\n\n```dotenv\nLOCALSTORE_PLUGIN_CONFIG_DIR='\n{\n  \"plugin_id\": \"/tmp/plugin_config\"\n}\n'\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/best-practice/database/README.mdx",
    "content": "import TabItem from \"@theme/TabItem\";\nimport Tabs from \"@theme/Tabs\";\n\n# 数据库\n\n[`nonebot-plugin-orm`](https://github.com/nonebot/plugin-orm) 是 NoneBot 的数据库支持插件。\n本插件基于 [SQLAlchemy](https://www.sqlalchemy.org/) 和 [Alembic](https://alembic.sqlalchemy.org/)，提供了许多与 NoneBot 紧密集成的功能：\n\n- 多 Engine / Connection 支持\n- Session 管理\n- 关系模型管理、依赖注入支持\n- 数据库迁移\n\n## 安装\n\n<Tabs groupId=\"install\">\n<TabItem value=\"cli\" label=\"使用 nb-cli\">\n\n```shell\nnb plugin install nonebot-plugin-orm\n```\n\n</TabItem>\n<TabItem value=\"pip\" label=\"使用 pip\">\n\n```shell\npip install nonebot-plugin-orm\n```\n\n</TabItem>\n\n<TabItem value=\"pdm\" label=\"使用 pdm\">\n\n```shell\npdm add nonebot-plugin-orm\n```\n\n</TabItem>\n</Tabs>\n\n## 数据库驱动和后端\n\n本插件只提供了 ORM 功能，没有数据库后端，也没有直接连接数据库后端的能力。\n所以你需要另行安装数据库驱动和数据库后端，并且配置数据库连接信息。\n\n### SQLite\n\n[SQLite](https://www.sqlite.org/) 是一个轻量级的嵌入式数据库，它的数据以单文件的形式存储在本地，不需要单独的数据库后端。\nSQLite 非常适合用于开发环境和小型应用，但是不适合用于大型应用的生产环境。\n\n虽然不需要另行安装数据库后端，但你仍然需要安装数据库驱动：\n\n<Tabs groupId=\"install\">\n<TabItem value=\"pip\" label=\"使用 pip\">\n\n```shell\npip install \"nonebot-plugin-orm[sqlite]\"\n```\n\n</TabItem>\n\n<TabItem value=\"pdm\" label=\"使用 pdm\">\n\n```shell\npdm add \"nonebot-plugin-orm[sqlite]\"\n```\n\n</TabItem>\n</Tabs>\n\n默认情况下，数据库文件为 `<data path>/nonebot-plugin-orm/db.sqlite3`（数据目录由 [nonebot-plugin-localstore](../data-storing) 提供）。\n或者，你可以通过配置 `SQLALCHEMY_DATABASE_URL` 来指定数据库文件路径：\n\n```shell\nSQLALCHEMY_DATABASE_URL=sqlite+aiosqlite:///file_path\n```\n\n### PostgreSQL\n\n[PostgreSQL](https://www.postgresql.org/) 是世界上最先进的开源关系数据库之一，对各种高级且广泛应用的功能有最好的支持，是中小型应用的首选数据库。\n\n<Tabs groupId=\"install\">\n<TabItem value=\"pip\" label=\"使用 pip\">\n\n```shell\npip install nonebot-plugin-orm[postgresql]\n```\n\n</TabItem>\n\n<TabItem value=\"pdm\" label=\"使用 pdm\">\n\n```shell\npdm add nonebot-plugin-orm[postgresql]\n```\n\n</TabItem>\n</Tabs>\n\n```shell\nSQLALCHEMY_DATABASE_URL=postgresql+psycopg://user:password@host:port/dbname[?key=value&key=value...]\n```\n\n### MySQL / MariaDB\n\n[MySQL](https://www.mysql.com/) 和 [MariaDB](https://mariadb.com/) 是经典的开源关系数据库，适合用于中小型应用。\n\n<Tabs groupId=\"install\">\n<TabItem value=\"pip\" label=\"使用 pip\">\n\n```shell\npip install nonebot-plugin-orm[mysql]\n```\n\n</TabItem>\n\n<TabItem value=\"pdm\" label=\"使用 pdm\">\n\n```shell\npdm add nonebot-plugin-orm[mysql]\n```\n\n</TabItem>\n</Tabs>\n\n```shell\nSQLALCHEMY_DATABASE_URL=mysql+aiomysql://user:password@host:port/dbname[?key=value&key=value...]\n```\n\n## 使用\n\n本插件提供了数据库迁移功能（此功能依赖于 [nb-cli 脚手架](../../quick-start#安装脚手架)）。\n在安装了新的插件或机器人之后，你需要执行一次数据库迁移操作，将数据库同步至与机器人一致的状态：\n\n```shell\nnb orm upgrade\n```\n\n运行完毕后，可以检查一下：\n\n```shell\nnb orm check\n```\n\n如果输出是 `没有检测到新的升级操作`，那么恭喜你，数据库已经迁移完成了，你可以启动机器人了。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/best-practice/database/_category_.json",
    "content": "{\n  \"label\": \"数据库\",\n  \"position\": 7\n}\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/best-practice/database/developer/README.md",
    "content": "# 开发者指南\n\n开发者指南内容较多，故分为了一个示例以及数个专题。\n阅读（并且最好跟随实践）示例后，你将会对使用 `nonebot-plugin-orm` 开发插件有一个基本的认识。\n如果想要更深入地学习关于 [SQLAlchemy](https://www.sqlalchemy.org/) 和 [Alembic](https://alembic.sqlalchemy.org/) 的知识，或者在使用过程中遇到了问题，可以查阅专题以及其官方文档。\n\n## 示例\n\n### 模型定义\n\n首先，我们需要设计存储的数据的结构。\n例如天气插件，需要存储**什么地方 (`location`)** 的**天气是什么 (`weather`)**。\n其中，一个地方只会有一种天气，而不同地方可能有相同的天气。\n所以，我们可以设计出如下的模型：\n\n```python title=weather/__init__.py showLineNumbers\nfrom nonebot_plugin_orm import Model\nfrom sqlalchemy.orm import Mapped, mapped_column\n\n\nclass Weather(Model):\n    location: Mapped[str] = mapped_column(primary_key=True)\n    weather: Mapped[str]\n```\n\n其中，`primary_key=True` 意味着此列 (`location`) 是主键，即内容是唯一的且非空的。\n每一个模型必须有至少一个主键。\n\n我们可以用以下代码检查模型生成的数据库模式是否正确：\n\n```python\nfrom sqlalchemy.schema import CreateTable\n\nprint(CreateTable(Weather.__table__))\n```\n\n```sql\nCREATE TABLE weather_weather (\n        location VARCHAR NOT NULL,\n        weather VARCHAR NOT NULL,\n        CONSTRAINT pk_weather_weather PRIMARY KEY (location)\n)\n```\n\n可以注意到表名是 `weather_weather` 而不是 `Weather` 或者 `weather`。\n这是因为 `nonebot-plugin-orm` 会自动为模型生成一个表名，规则是：`<插件模块名>_<类名小写>`。\n\n你也可以通过指定 `__tablename__` 属性来自定义表名：\n\n```python {2}\nclass Weather(Model):\n    __tablename__ = \"weather\"\n    ...\n```\n\n```sql {1}\nCREATE TABLE weather (\n    ...\n)\n```\n\n但是，并不推荐你这么做，因为这可能会导致不同插件间的表名重复，引发冲突。\n特别是当你会发布插件时，你并不知道其他插件会不会使用相同的表名。\n\n### 首次迁移\n\n我们成功定义了模型，现在启动机器人试试吧：\n\n```shell\n$ nb run\n01-02 15:04:05 [SUCCESS] nonebot | NoneBot is initializing...\n01-02 15:04:05 [ERROR] nonebot_plugin_orm | 启动检查失败\n01-02 15:04:05 [ERROR] nonebot | Application startup failed. Exiting.\nTraceback (most recent call last):\n  ...\nclick.exceptions.UsageError: 检测到新的升级操作:\n[('add_table',\n  Table('weather', MetaData(), Column('location', String(), table=<weather>, primary_key=True, nullable=False), Column('weather', String(), table=<weather>, nullable=False), schema=None))]\n```\n\n咦，发生了什么？\n`nonebot-plugin-orm` 试图阻止我们启动机器人。\n原来是我们定义了模型，但是数据库中并没有对应的表，这会导致插件不能正常运行。\n所以，我们需要迁移数据库。\n\n首先，我们需要创建一个迁移脚本：\n\n```shell\nnb orm revision -m \"first revision\" --branch-label weather\n```\n\n其中，`-m` 参数是迁移脚本的描述，`--branch-label` 参数是迁移脚本的分支，一般为插件模块名。\n执行命令过后，出现了一个 `weather/migrations` 目录，其中有一个 `xxxxxxxxxxxx_first_revision.py` 文件：\n\n```shell {4,5}\nweather\n├── __init__.py\n├── config.py\n└── migrations\n    └── xxxxxxxxxxxx_first_revision.py\n```\n\n这就是我们创建的迁移脚本，它记录了数据库模式的变化。\n我们可以查看一下它的内容：\n\n```python title=weather/migrations/xxxxxxxxxxxx_first_revision.py {25-33,39-41} showLineNumbers\n\"\"\"first revision\n\n迁移 ID: xxxxxxxxxxxx\n父迁移:\n创建时间: 2006-01-02 15:04:05.999999\n\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom collections.abc import Sequence\n\nimport sqlalchemy as sa\nfrom alembic import op\n\nrevision: str = \"xxxxxxxxxxxx\"\ndown_revision: str | Sequence[str] | None = None\nbranch_labels: str | Sequence[str] | None = (\"weather\",)\ndepends_on: str | Sequence[str] | None = None\n\n\ndef upgrade(name: str = \"\") -> None:\n    if name:\n        return\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.create_table(\n        \"weather_weather\",\n        sa.Column(\"location\", sa.String(), nullable=False),\n        sa.Column(\"weather\", sa.String(), nullable=False),\n        sa.PrimaryKeyConstraint(\"location\", name=op.f(\"pk_weather_weather\")),\n        info={\"bind_key\": \"weather\"},\n    )\n    # ### end Alembic commands ###\n\n\ndef downgrade(name: str = \"\") -> None:\n    if name:\n        return\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_table(\"weather_weather\")\n    # ### end Alembic commands ###\n```\n\n可以注意到脚本的主体部分（其余是模版代码，请勿修改）是：\n\n```python\n# ### commands auto generated by Alembic - please adjust! ###\nop.create_table(  # CREATE TABLE\n    \"weather_weather\",  # weather_weather\n    sa.Column(\"location\", sa.String(), nullable=False),  # location VARCHAR NOT NULL,\n    sa.Column(\"weather\", sa.String(), nullable=False),  # weather VARCHAR NOT NULL,\n    sa.PrimaryKeyConstraint(\"location\", name=op.f(\"pk_weather_weather\")),  # CONSTRAINT pk_weather_weather PRIMARY KEY (location)\n    info={\"bind_key\": \"weather\"},\n)\n# ### end Alembic commands ###\n```\n\n```python\n# ### commands auto generated by Alembic - please adjust! ###\nop.drop_table(\"weather_weather\")  # DROP TABLE weather_weather;\n# ### end Alembic commands ###\n```\n\n虽然我们不是很懂这些代码的意思，但是可以注意到它们几乎与 SQL 语句 (DDL) 一一对应。\n显然，它们是用来创建和删除表的。\n\n我们还可以注意到，`upgrade()` 和 `downgrade()` 函数中的代码是**互逆**的。\n也就是说，执行一次 `upgrade()` 函数，再执行一次 `downgrade()` 函数后，数据库的模式就会回到原来的状态。\n\n这就是迁移脚本的作用：记录数据库模式的变化，以便我们在不同的环境中（例如开发环境和生产环境）**可复现地**、**可逆地**同步数据库模式，正如 git 对我们的代码做的事情那样。\n\n对了，不要忘记还有一段注释：`commands auto generated by Alembic - please adjust!`。\n它在提醒我们，这些代码是由 Alembic 自动生成的，我们应该检查它们，并且根据需要进行调整。\n\n:::caution 注意\n迁移脚本冗长且繁琐，我们一般不会手写它们，而是由 Alembic 自动生成。\n一般情况下，Alembic 足够智能，可以正确地生成迁移脚本。\n但是，在复杂或有歧义的情况下，我们可能需要手动调整迁移脚本。\n所以，**永远要检查迁移脚本，并且在开发环境中测试！**\n\n**迁移脚本中任何一处错误都足以使数据付之东流！**\n:::\n\n确定迁移脚本正确后，我们就可以执行迁移脚本，将数据库模式同步到数据库中：\n\n```shell\nnb orm upgrade\n```\n\n现在，我们可以正常启动机器人了。\n\n开发过程中，我们可能会频繁地修改模型，这意味着我们需要频繁地创建并执行迁移脚本，非常繁琐。\n实际上，此时我们不在乎数据安全，只需要数据库模式与模型定义一致即可。\n所以，我们可以关闭 `nonebot-plugin-orm` 的启动检查：\n\n```shell title=.env.dev\nALEMBIC_STARTUP_CHECK=false\n```\n\n现在，每次启动机器人时，数据库模式会自动与模型定义同步，无需手动迁移。\n\n### 会话管理\n\n我们已经成功定义了模型，并且迁移了数据库，现在可以开始使用数据库了……吗？\n并不能，因为模型只是数据结构的定义，并不能通过它操作数据（如果你曾经使用过 [Tortoise ORM](https://tortoise.github.io/)，可能会知道 `await Weather.get(location=\"上海\")` 这样的面向对象编程。\n但是 SQLAlchemy 不同，选择了命令式编程）。\n我们需要使用**会话**操作数据：\n\n```python title=weather/__init__.py {10,13} showLineNumbers\nfrom nonebot import on_command\nfrom nonebot.adapters import Message\nfrom nonebot.params import CommandArg\nfrom nonebot_plugin_orm import async_scoped_session\n\nweather = on_command(\"天气\")\n\n\n@weather.handle()\nasync def _(session: async_scoped_session, args: Message = CommandArg()):\n    location = args.extract_plain_text()\n\n    if wea := await session.get(Weather, location):\n        await weather.finish(f\"今天{location}的天气是{wea.weather}\")\n\n    await weather.finish(f\"未查询到{location}的天气\")\n```\n\n我们通过 `session: async_scoped_session` 依赖注入获得了一个会话，然后使用 `await session.get(Weather, location)` 查询数据库。\n`async_scoped_session` 是一个有作用域限制的会话，作用域为当前事件、当前事件响应器。\n会话产生的模型实例（例如此处的 `wea := await session.get(Weather, location)`）作用域与会话相同。\n\n:::caution 注意\n此处提到的“会话”指的是 ORM 会话，而非 [NoneBot 会话](../../../appendices/session-control)，两者的生命周期也是不同的（NoneBot 会话的生命周期中可能包含多个事件，不同的事件也会有不同的事件响应器）。\n具体而言，就是不要将 ORM 会话和模型实例存储在 NoneBot 会话状态中：\n\n```python {12}\nfrom nonebot.params import ArgPlainText\nfrom nonebot.typing import T_State\n\n\n@weather.got(\"location\", prompt=\"请输入地名\")\nasync def _(state: T_State, session: async_scoped_session, location: str = ArgPlainText()):\n    wea = await session.get(Weather, location)\n\n    if not wea:\n        await weather.finish(f\"未查询到{location}的天气\")\n\n    state[\"weather\"] = wea  # 不要这么做，除非你知道自己在做什么\n```\n\n当然非要这么做也不是不可以：\n\n```python {6}\n@weather.handle()\nasync def _(state: T_State, session: async_scoped_session):\n    # 通过 await session.merge(state[\"weather\"]) 获得了此 ORM 会话中的相应模型实例，\n    # 而非直接使用会话状态中的模型实例，\n    # 因为先前的 ORM 会话已经关闭了。\n    wea = await session.merge(state[\"weather\"])\n    await weather.finish(f\"今天{state['location']}的天气是{wea.weather}\")\n```\n\n:::\n\n当有数据更改时，我们需要提交事务，也要注意会话作用域问题：\n\n```python title=weather/__init__.py {12,20} showLineNumbers\nfrom nonebot.params import Depends\n\n\nasync def get_weather(\n    session: async_scoped_session, args: Message = CommandArg()\n) -> Weather:\n    location = args.extract_plain_text()\n\n    if not (wea := await session.get(Weather, location)):\n        wea = Weather(location=location, weather=\"未知\")\n        session.add(wea)\n        # await session.commit()  # 不应该在其他地方提交事务\n\n    return wea\n\n\n@weather.handle()\nasync def _(session: async_scoped_session, wea: Weather = Depends(get_weather)):\n    await weather.send(f\"今天的天气是{wea.weather}\")\n    await session.commit()  # 而应该在事件响应器结束前提交事务\n```\n\n当然我们也可以获得一个新的会话，不过此时就要手动管理会话了：\n\n```python title=weather/__init__.py {5-6} showLineNumbers\nfrom nonebot_plugin_orm import get_session\n\n\nasync def get_weather(location: str) -> str:\n    session = get_session()\n    async with session.begin():\n        wea = await session.get(Weather, location)\n\n        if not wea:\n            wea = Weather(location=location, weather=\"未知\")\n            session.add(wea)\n\n        return wea.weather\n\n\n@weather.handle()\nasync def _(args: Message = CommandArg()):\n    wea = await get_weather(args.extract_plain_text())\n    await weather.send(f\"今天的天气是{wea}\")\n```\n\n### 依赖注入\n\n在上面的示例中，我们都是通过会话获得数据的。\n不过，我们也可以通过依赖注入获得数据：\n\n```python title=weather/__init__.py {12-14} showLineNumbers\nfrom sqlalchemy import select\nfrom nonebot.params import Depends\nfrom nonebot_plugin_orm import SQLDepends\n\n\ndef extract_arg_plain_text(args: Message = CommandArg()) -> str:\n    return args.extract_plain_text()\n\n\n@weather.handle()\nasync def _(\n    wea: Weather = SQLDepends(\n        select(Weather).where(Weather.location == Depends(extract_arg_plain_text))\n    ),\n):\n    await weather.send(f\"今天的天气是{wea.weather}\")\n```\n\n其中，`SQLDepends` 是一个特殊的依赖注入，它会根据类型标注和 SQL 语句提供数据，SQL 语句中也可以有子依赖。\n\n不同的类型标注也会获得不同形式的数据：\n\n```python title=weather/__init__.py {5} showLineNumbers\nfrom collections.abc import Sequence\n\n@weather.handle()\nasync def _(\n    weas: Sequence[Weather] = SQLDepends(\n        select(Weather).where(Weather.weather == Depends(extract_arg_plain_text))\n    ),\n):\n    await weather.send(f\"今天的天气是{weas[0].weather}的城市有{'，'.join(wea.location for wea in weas)}\")\n```\n\n支持的类型标注请参见 [依赖注入](dependency)。\n\n我们也可以像 [类作为依赖](../../../advanced/dependency#类作为依赖) 那样，在类属性中声明子依赖：\n\n```python title=weather/__init__.py {5-6,10} showLineNumbers\nfrom collections.abc import Sequence\n\nclass Weather(Model):\n    location: Mapped[str] = mapped_column(primary_key=True)\n    weather: Mapped[str] = Depends(extract_arg_plain_text)\n    # weather: Annotated[Mapped[str], Depends(extract_arg_plain_text)]  # Annotated 支持\n\n\n@weather.handle()\nasync def _(weas: Sequence[Weather]):\n    await weather.send(\n        f\"今天的天气是{weas[0].weather}的城市有{'，'.join(wea.location for wea in weas)}\"\n    )\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/best-practice/database/developer/_category_.json",
    "content": "{\n  \"label\": \"开发者指南\",\n  \"position\": 3\n}\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/best-practice/database/developer/dependency.md",
    "content": "---\nsidebar_position: 3\ndescription: 依赖注入\n---\n\n# 依赖注入\n\n`nonebot-plugin-orm` 提供了强大且灵活的依赖注入，可以方便地帮助你获取数据库会话和查询数据。\n\n## 数据库会话\n\n### AsyncSession\n\n新数据库会话，常用于有独立的数据库操作逻辑的插件。\n\n```python {13,26}\nfrom nonebot import on_message\nfrom nonebot.params import Depends\nfrom nonebot_plugin_orm import AsyncSession, Model, async_scoped_session\nfrom sqlalchemy.orm import Mapped, mapped_column\n\nmessage = on_message()\n\n\nclass Message(Model):\n    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)\n\n\nasync def get_message(session: AsyncSession) -> Message:\n    # 等价于 session = get_session()\n    async with session:\n        msg = Message()\n\n        session.add(msg)\n        await session.commit()\n        await session.refresh(msg)\n\n        return msg\n\n\n@message.handle()\nasync def _(session: async_scoped_session, msg: Message = Depends(get_message)):\n    await session.rollback()  # 无法回退 get_message() 中的更改\n    await message.send(str(msg.id))  # msg 被存储，msg.id 递增\n```\n\n### async_scoped_session\n\n数据库作用域会话，常用于事件响应器和有与响应逻辑相关的数据库操作逻辑的插件。\n\n```python {13，26}\nfrom nonebot import on_message\nfrom nonebot.params import Depends\nfrom nonebot_plugin_orm import Model, async_scoped_session\nfrom sqlalchemy.orm import Mapped, mapped_column\n\nmessage = on_message()\n\n\nclass Message(Model):\n    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)\n\n\nasync def get_message(session: async_scoped_session) -> Message:\n    # 等价于 session = get_scoped_session()\n    msg = Message()\n\n    session.add(msg)\n    await session.flush()\n    await session.refresh(msg)\n\n    return msg\n\n\n@message.handle()\nasync def _(session: async_scoped_session, msg: Message = Depends(get_message)):\n    await session.rollback()  # 可以回退 get_message() 中的更改\n    await message.send(str(msg.id))  # msg 没有被存储，msg.id 不变\n```\n\n## 查询数据\n\n### Model\n\n支持类作为依赖。\n\n```python\nfrom typing import Annotated\n\nfrom nonebot.params import Depends\nfrom nonebot_plugin_orm import Model\nfrom sqlalchemy.orm import Mapped, mapped_column\n\n\ndef get_id() -> int: ...\n\n\nclass Message(Model):\n    id: Annotated[Mapped[int], Depends(get_id)] = mapped_column(\n        primary_key=True, autoincrement=True\n    )\n\n\nasync def _(msg: Message):\n    # 等价于 msg = (\n    #     await (await session.stream(select(Message).where(Message.id == get_id())))\n    #     .scalars()\n    #     .one_or_none()\n    # )\n    ...\n```\n\n### SQLDepends\n\n参数为一个 SQL 语句，决定依赖注入的内容，SQL 语句中可以使用子依赖。\n\n```python {11-13}\nfrom nonebot.params import Depends\nfrom nonebot_plugin_orm import Model, SQLDepends\nfrom sqlalchemy import select\n\n\ndef get_id() -> int: ...\n\n\nasync def _(\n    model: Model = SQLDepends(select(Model).where(Model.id == Depends(get_id))),\n): ...\n```\n\n参数可以是任意 SQL 语句，但不建议使用 `select` 以外的语句，因为语句可能没有返回值（`returning` 除外），而且代码不清晰。\n\n### 类型标注\n\n类型标注决定依赖注入的数据结构，主要影响以下几个层面：\n\n- 迭代器（`session.execute()`）或异步迭代器（`session.stream()`）\n- 标量（`session.execute().scalars()`）或元组（`session.execute()`）\n- 一个（`session.execute().one_or_none()`，注意 `None` 时可能触发 [重载](../../../appendices/overload#重载)）或全部（`session.execute()` / `session.execute().all()`）\n- 连续（`session().execute()`）或分块（`session.execute().partitions()`）\n\n具体如下（可以使用父类型作为类型标注）：\n\n- ```python\n  async def _(rows_partitions: AsyncIterator[Sequence[Tuple[Model, ...]]]):\n      # 等价于 rows_partitions = await (await session.stream(sql).partitions())\n\n      async for partition in rows_partitions:\n          for row in partition:\n              print(row[0], row[1], ...)\n  ```\n\n- ```python\n  async def _(model_partitions: AsyncIterator[Sequence[Model]]):\n      # 等价于 model_partitions = await (await session.stream(sql).scalars().partitions())\n\n      async for partition in model_partitions:\n          for model in partition:\n              print(model)\n  ```\n\n- ```python\n  async def _(row_partitions: Iterator[Sequence[Tuple[Model, ...]]]):\n      # 等价于 row_partitions = await session.execute(sql).partitions()\n\n      for partition in rows_partitions:\n          for row in partition:\n              print(row[0], row[1], ...)\n  ```\n\n- ```python\n  async def _(model_partitions: Iterator[Sequence[Model]]):\n      # 等价于 model_partitions = await (await session.execute(sql).scalars().partitions())\n\n      for partition in model_partitions:\n          for model in partition:\n              print(model)\n  ```\n\n- ```python\n  async def _(rows: sa_async.AsyncResult[Tuple[Model, ...]]):\n      # 等价于 rows = await session.stream(sql)\n\n      async for row in rows:\n          print(row[0], row[1], ...)\n  ```\n\n- ```python\n  async def _(models: sa_async.AsyncScalarResult[Model]):\n      # 等价于 models = await session.stream(sql).scalars()\n\n      async for model in models:\n          print(model)\n  ```\n\n- ```python\n  async def _(rows: sa.Result[Tuple[Model, ...]]):\n      # 等价于 rows = await session.execute(sql)\n\n      for row in rows:\n          print(row[0], row[1], ...)\n  ```\n\n- ```python\n  async def _(models: sa.ScalarResult[Model]):\n      # 等价于 models = await session.execute(sql).scalars()\n\n      for model in models:\n          print(model)\n  ```\n\n- ```python\n  async def _(rows: Sequence[Tuple[Model, ...]]):\n      # 等价于 rows = await (await session.stream(sql).all())\n\n      for row in rows:\n            print(row[0], row[1], ...)\n  ```\n\n- ```python\n  async def _(models: Sequence[Model]):\n      # 等价于 models = await (await session.stream(sql).scalars().all())\n\n      for model in models:\n          print(model)\n  ```\n\n- ```python\n  async def _(row: Tuple[Model, ...]):\n      # 等价于 row = await (await session.stream(sql).one_or_none())\n\n      print(row[0], row[1], ...)\n  ```\n\n- ```python\n  async def _(model: Model):\n      # 等价于 model = await (await session.stream(sql).scalars().one_or_none())\n\n      print(model)\n  ```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/best-practice/database/developer/test.md",
    "content": "---\nsidebar_position: 2\ndescription: 测试\n---\n\n# 测试\n\n百思不如一试，测试是发现问题的最佳方式。\n\n不同的用户会有不同的配置，为了提高项目的兼容性，我们需要在不同数据库后端上测试。\n手动进行大量的、重复的测试不可靠，也不现实，因此我们推荐使用 [GitHub Actions](https://github.com/features/actions) 进行自动化测试：\n\n```yaml title=.github/workflows/test.yml {12-42,52-53} showLineNumbers\nname: Test\n\non:\n  push:\n    branches:\n      - main\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n\n    strategy:\n      matrix:\n        db:\n          - sqlite+aiosqlite:///db.sqlite3\n          - postgresql+psycopg://postgres:postgres@localhost:5432/postgres\n          - mysql+aiomysql://mysql:mysql@localhost:3306/mymysql\n\n      fail-fast: false\n\n    env:\n      SQLALCHEMY_DATABASE_URL: ${{ matrix.db }}\n\n    services:\n      postgresql:\n        image: ${{ startsWith(matrix.db, 'postgresql') && 'postgres' || '' }}\n        env:\n          POSTGRES_USER: postgres\n          POSTGRES_PASSWORD: postgres\n          POSTGRES_DB: postgres\n        ports:\n          - 5432:5432\n\n      mysql:\n        image: ${{ startsWith(matrix.db, 'mysql') && 'mysql' || '' }}\n        env:\n          MYSQL_ROOT_PASSWORD: mysql\n          MYSQL_USER: mysql\n          MYSQL_PASSWORD: mysql\n          MYSQL_DATABASE: mymysql\n        ports:\n          - 3306:3306\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - uses: actions/setup-python@v5\n\n      - name: Install dependencies\n        run: pip install -r requirements.txt\n\n      - name: Run migrations\n        run: pipx run nb-cli orm upgrade\n\n      - name: Run tests\n        run: pytest\n```\n\n如果项目还需要考虑跨平台和跨 Python 版本兼容，测试矩阵中还需要增加这两个维度。\n但是，我们没必要在所有平台和 Python 版本上运行所有数据库的测试，因为很显然，PostgreSQL 和 MySQL 这类独立的数据库后端不会受平台和 Python 影响，而且 Github Actions 的非 Linux 平台不支持运行独立服务：\n\n|             | Python 3.9 | Python 3.10 | Python 3.11 | Python 3.12                 |\n| ----------- | ---------- | ----------- | ----------- | --------------------------- |\n| **Linux**   | SQLite     | SQLite      | SQLite      | SQLite / PostgreSQL / MySQL |\n| **Windows** | SQLite     | SQLite      | SQLite      | SQLite                      |\n| **macOS**   | SQLite     | SQLite      | SQLite      | SQLite                      |\n\n```yaml title=.github/workflows/test.yml {12-24} showLineNumbers\nname: Test\n\non:\n  push:\n    branches:\n      - main\n\njobs:\n  test:\n    runs-on: ${{ matrix.os }}\n\n    strategy:\n      matrix:\n        os: [ubuntu-latest, windows-latest, macos-latest]\n        python-version: [\"3.9\", \"3.10\", \"3.11\", \"3.12\"]\n        db: [\"sqlite+aiosqlite:///db.sqlite3\"]\n\n        include:\n          - os: ubuntu-latest\n            python-version: \"3.12\"\n            db: postgresql+psycopg://postgres:postgres@localhost:5432/postgres\n          - os: ubuntu-latest\n            python-version: \"3.12\"\n            db: mysql+aiomysql://mysql:mysql@localhost:3306/mymysql\n\n      fail-fast: false\n\n    env:\n      SQLALCHEMY_DATABASE_URL: ${{ matrix.db }}\n\n    services:\n      postgresql:\n        image: ${{ startsWith(matrix.db, 'postgresql') && 'postgres' || '' }}\n        env:\n          POSTGRES_USER: postgres\n          POSTGRES_PASSWORD: postgres\n          POSTGRES_DB: postgres\n        ports:\n          - 5432:5432\n\n      mysql:\n        image: ${{ startsWith(matrix.db, 'mysql') && 'mysql' || '' }}\n        env:\n          MYSQL_ROOT_PASSWORD: mysql\n          MYSQL_USER: mysql\n          MYSQL_PASSWORD: mysql\n          MYSQL_DATABASE: mymysql\n        ports:\n          - 3306:3306\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - uses: actions/setup-python@v5\n        with:\n          python-version: ${{ matrix.python-version }}\n\n      - name: Install dependencies\n        run: pip install -r requirements.txt\n\n      - name: Run migrations\n        run: pipx run nb-cli orm upgrade\n\n      - name: Run tests\n        run: pytest\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/best-practice/database/user.md",
    "content": "---\nsidebar_position: 2\ndescription: 用户指南\n---\n\n# 用户指南\n\n`nonebot-plugin-orm` 功能强大且复杂，使用上有一定难度。\n不过，对于用户而言，只需要掌握部分功能即可。\n\n:::caution 注意\n请注意区分插件的项目名（如：`nonebot-plugin-wordcloud`）和模块名（如：`nonebot_plugin_wordcloud`）。`nonebot-plugin-orm` 中统一使用插件模块名。参见 [插件命名规范](../../developer/plugin-publishing#插件命名规范)。\n:::\n\n## 示例\n\n### 创建新机器人\n\n我们想要创建一个机器人，并安装 `nonebot-plugin-wordcloud` 插件，只需要执行以下命令：\n\n```shell\nnb init  # 初始化项目文件夹\n\npip install nonebot-plugin-orm[sqlite]  # 安装 nonebot-plugin-orm，并附带 SQLite 支持\n\nnb plugin install nonebot-plugin-wordcloud  # 安装插件\n\n# nb orm heads  # 查看有什么插件使用到了数据库（可选）\n\nnb orm upgrade  # 升级数据库\n\n# nb orm check  # 检查一下数据库模式是否与模型定义一致（可选）\n\nnb run  # 启动机器人\n```\n\n### 卸载插件\n\n我们已经安装了 `nonebot-plugin-wordcloud` 插件，但是现在想要卸载它，并且**删除它的数据**，只需要执行以下命令：\n\n```shell\nnb plugin uninstall nonebot-plugin-wordcloud  # 卸载插件\n\n# nb orm heads  # 查看有什么插件使用到了数据库。（可选）\n\nnb orm downgrade nonebot_plugin_wordcloud@base  # 降级数据库，删除数据\n\n# nb orm check  # 检查一下数据库模式是否与模型定义一致（可选）\n```\n\n## CLI\n\n接下来，让我们了解下示例中出现的 CLI 命令的含义：\n\n### heads\n\n显示所有的分支头。一般一个分支对应一个插件。\n\n```shell\nnb orm heads\n```\n\n输出格式为 `<迁移 ID> (<插件模块名>) (<头部类型>)`：\n\n```\n46327b837dd8 (nonebot_plugin_chatrecorder) (head)\n9492159f98f7 (nonebot_plugin_user) (head)\n71a72119935f (nonebot_plugin_session_orm) (effective head)\nade8cdca5470 (nonebot_plugin_wordcloud) (head)\n```\n\n### upgrade\n\n升级数据库。每次安装新的插件或更新插件版本后，都需要执行此命令。\n\n```shell\nnb orm upgrade <插件模块名>@<迁移 ID>\n```\n\n其中，`<插件模块名>@<迁移 ID>` 是可选参数。如果不指定，则会将所有分支升级到最新版本，这也是最常见的用法：\n\n```shell\nnb orm upgrade\n```\n\n### downgrade\n\n降级数据库。当需要回滚插件版本或删除插件时，可以执行此命令。\n\n```shell\nnb orm downgrade <插件模块名>@<迁移 ID>\n```\n\n其中，`<迁移 ID>` 也可以是 `base`，即回滚到初始状态。常用于卸载插件后删除其数据：\n\n```shell\nnb orm downgrade <插件模块名>@base\n```\n\n### check\n\n检查数据库模式是否与模型定义一致。机器人启动前会自动运行此命令（`ALEMBIC_STARTUP_CHECK=true` 时），并在检查失败时阻止启动。\n\n```shell\nnb orm check\n```\n\n## 配置\n\n### sqlalchemy_database_url\n\n默认数据库连接 URL。参见 [数据库驱动和后端](.#数据库驱动和后端) 和 [引擎配置 — SQLAlchemy 2.0 文档](https://docs.sqlalchemy.org/en/20/core/engines.html#database-urls)。\n\n```shell\nSQLALCHEMY_DATABASE_URL=dialect+driver://username:password@host:port/database\n```\n\n### sqlalchemy_bind\n\nbind 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) 实例的字典。\n例如，我们想要让 `nonebot-plugin-wordcloud` 插件使用一个 SQLite 数据库，并开启 [Echo 选项](https://docs.sqlalchemy.org/en/20/core/engines.html#sqlalchemy.create_engine.params.echo) 便于 debug，而其他插件使用默认的 PostgreSQL 数据库，可以这样配置：\n\n```shell\nSQLALCHEMY_BINDS='{\n    \"\": \"postgresql+psycopg://scott:tiger@localhost/mydatabase\",\n    \"nonebot_plugin_wordcloud\": {\n        \"url\": \"sqlite+aiosqlite://\",\n        \"echo\": true\n    }\n}'\n```\n\n### sqlalchemy_engine_options\n\n[`create_async_engine()`](https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html#sqlalchemy.ext.asyncio.create_async_engine) 默认参数字典。\n\n```shell\nSQLALCHEMY_ENGINE_OPTIONS='{\n    \"pool_size\": 5,\n    \"max_overflow\": 10,\n    \"pool_timeout\": 30,\n    \"pool_recycle\": 3600,\n    \"echo\": true\n}'\n```\n\n### sqlalchemy_echo\n\n开启 [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。\n\n```shell\nSQLALCHEMY_ECHO=true\n```\n\n:::caution 注意\n以上配置之间有覆盖关系，遵循特殊优先于一般的原则，具体为 [`sqlalchemy_database_url`](#sqlalchemy_database_url) > [`sqlalchemy_bind`](#sqlalchemy_bind) > [`sqlalchemy_echo`](#sqlalchemy_echo) > [`sqlalchemy_engine_options`](#sqlalchemy_engine_options)。\n但覆盖顺序并非显而易见，出于清晰考虑，请只配置必要的选项。\n:::\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/best-practice/deployment.mdx",
    "content": "---\nsidebar_position: 3\ndescription: 部署你的机器人\n---\n\n# 部署\n\nimport Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\n\n在编写完成各类插件后，我们需要长期运行机器人来使得用户能够正常使用。通常，我们会使用云服务器来部署机器人。\n\n我们在开发插件时，机器人运行的环境称为开发环境；而在部署后，机器人运行的环境称为生产环境。与开发环境不同的是，在生产环境中，开发者通常不能随意地修改/添加/删除代码，开启或停止服务。\n\n## 部署前准备\n\n### 项目依赖管理\n\n由于部署后的机器人运行在生产环境中，因此，为确保机器人能够正常运行，我们需要保证机器人的运行环境与开发环境一致。我们可以通过以下几种方式来进行依赖管理：\n\n<Tabs groupId=\"tool\">\n  <TabItem value=\"poetry\" label=\"Poetry\" default>\n\n[Poetry](https://python-poetry.org/) 是一个 Python 项目的依赖管理工具。它可以通过声明项目所依赖的库，为你管理（安装/更新）它们。Poetry 提供了一个 `poetry.lock` 文件，以确保可重复安装，并可以构建用于分发的项目。\n\nPoetry 会在安装依赖时自动生成 `poetry.lock` 文件，在**项目目录**下执行以下命令：\n\n```bash\n# 初始化 poetry 配置\npoetry init\n# 添加项目依赖，这里以 nonebot2[fastapi] 为例\npoetry add nonebot2[fastapi]\n```\n\n  </TabItem>\n  <TabItem value=\"pdm\" label=\"PDM\">\n\n[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` 文件，以确保可重复安装，并可以构建用于分发的项目。\n\nPDM 会在安装依赖时自动生成 `pdm.lock` 文件，在**项目目录**下执行以下命令：\n\n```bash\n# 初始化 pdm 配置\npdm init\n# 添加项目依赖，这里以 nonebot2[fastapi] 为例\npdm add nonebot2[fastapi]\n```\n\n  </TabItem>\n  <TabItem value=\"pip\" label=\"pip\">\n\n[pip](https://pip.pypa.io/) 是 Python 包管理工具。他并不是一个依赖管理工具，为了尽可能保证环境的一致性，我们可以使用 `requirements.txt` 文件来声明依赖。\n\n```bash\npip freeze > requirements.txt\n```\n\n  </TabItem>\n</Tabs>\n\n### 安装 Docker\n\n[Docker](https://www.docker.com/) 是一个应用容器引擎，可以让开发者打包应用以及依赖包到一个可移植的镜像中，然后发布到服务器上。\n\n我们可以参考 [Docker 官方文档](https://docs.docker.com/get-docker/) 来安装 Docker 。\n\n在 Linux 上，我们可以使用以下一键脚本来安装 Docker 以及 Docker Compose Plugin：\n\n```bash\ncurl -fsSL https://get.docker.com | sh -s -- --mirror Aliyun\n```\n\n在 Windows/macOS 上，我们可以使用 [Docker Desktop](https://docs.docker.com/desktop/) 来安装 Docker 以及 Docker Compose Plugin。\n\n### 安装脚手架 Docker 插件\n\n我们可以使用 [nb-cli-plugin-docker](https://github.com/nonebot/cli-plugin-docker) 来快速部署机器人。\n\n插件可以帮助我们生成配置文件并构建 Docker 镜像，以及启动/停止/重启机器人。使用以下命令安装脚手架 Docker 插件：\n\n```bash\nnb self install nb-cli-plugin-docker\n```\n\n## Docker 部署\n\n### 快速部署\n\n使用脚手架命令即可一键生成配置并部署：\n\n```bash\nnb docker up\n```\n\n当看到 `Running` 字样时，说明机器人已经启动成功。我们可以通过以下命令来查看机器人的运行日志：\n\n<Tabs groupId=\"deploy-tool\">\n  <TabItem value=\"nb-cli\" label=\"NB CLI\" default>\n\n```bash\nnb docker logs\n```\n\n  </TabItem>\n  <TabItem value=\"docker-compose\" label=\"Docker Compose\">\n\n```bash\ndocker compose logs\n```\n\n  </TabItem>\n</Tabs>\n\n如果需要停止机器人，我们可以使用以下命令：\n\n<Tabs groupId=\"deploy-tool\">\n  <TabItem value=\"nb-cli\" label=\"NB CLI\" default>\n\n```bash\nnb docker down\n```\n\n  </TabItem>\n  <TabItem value=\"docker-compose\" label=\"Docker Compose\">\n\n```bash\ndocker compose down\n```\n\n  </TabItem>\n</Tabs>\n\n### 自定义部署\n\n在部分情况下，我们需要事先生成 Docker 配置文件，再到生产环境进行部署；或者自动生成的配置文件并不能满足复杂场景，需要根据实际需求手动修改配置文件。我们可以使用以下命令来生成基础配置文件：\n\n```bash\nnb docker generate\n```\n\nnb-cli 将会在项目目录下生成 `docker-compose.yml` 和 `Dockerfile` 等配置文件。在 nb-cli 完成配置文件的生成后，我们可以根据部署环境的实际情况使用 nb-cli 或者 Docker Compose 来启动机器人。\n\n我们可以参考 [Dockerfile 文件规范](https://docs.docker.com/engine/reference/builder/)和 [Compose 文件规范](https://docs.docker.com/compose/compose-file/)修改这两个文件。\n\n修改完成后我们可以直接启动或者手动构建镜像：\n\n<Tabs groupId=\"deploy-tool\">\n  <TabItem value=\"nb-cli\" label=\"NB CLI\" default>\n\n```bash\n# 启动机器人\nnb docker up\n# 手动构建镜像\nnb docker build\n```\n\n  </TabItem>\n  <TabItem value=\"docker-compose\" label=\"Docker Compose\">\n\n```bash\n# 启动机器人\ndocker compose up -d\n# 手动构建镜像\ndocker compose build\n```\n\n  </TabItem>\n</Tabs>\n\n### 持续集成\n\n我们可以使用 GitHub Actions 来实现持续集成（CI），我们只需要在 GitHub 上发布 Release 即可自动构建镜像并推送至镜像仓库。\n\n首先，我们需要在 [Docker Hub](https://hub.docker.com/) （或者其他平台，如：[GitHub Packages](https://github.com/features/packages)、[阿里云容器镜像服务](https://www.alibabacloud.com/zh/product/container-registry)等）上创建镜像仓库，用于存放镜像。\n\n前往项目仓库的 `Settings` > `Secrets` > `actions` 栏目 `New Repository Secret` 添加构建所需的密钥：\n\n- `DOCKERHUB_USERNAME`: 你的 Docker Hub 用户名\n- `DOCKERHUB_TOKEN`: 你的 Docker Hub PAT（[创建方法](https://docs.docker.com/docker-hub/access-tokens/)）\n\n将以下文件添加至**项目目录**下的 `.github/workflows/` 目录下，并将文件中高亮行中的仓库名称替换为你的仓库名称：\n\n```yaml title=.github/workflows/build.yml\nname: Docker Hub Release\n\non:\n  push:\n    tags:\n      - \"v*\"\n\njobs:\n  docker:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v3\n\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v2\n\n      - name: Setup Docker\n        uses: docker/setup-buildx-action@v2\n\n      - name: Login to DockerHub\n        uses: docker/login-action@v2\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n      - name: Generate Tags\n        uses: docker/metadata-action@v4\n        id: metadata\n        with:\n          images: |\n            # highlight-next-line\n            {organization}/{repository}\n          tags: |\n            type=semver,pattern={{version}}\n            type=semver,pattern={{major}}.{{minor}}\n            type=sha\n            type=raw,value=latest\n\n      - name: Build and Publish\n        uses: docker/build-push-action@v4\n        with:\n          context: .\n          push: true\n          tags: ${{ steps.metadata.outputs.tags }}\n          labels: ${{ steps.metadata.outputs.labels }}\n          cache-from: type=gha\n          cache-to: type=gha,mode=max\n```\n\n### 持续部署\n\n在完成发布并构建镜像后，我们可以自动将镜像部署到服务器上。\n\n前往项目仓库的 `Settings` > `Secrets` > `actions` 栏目 `New Repository Secret` 添加部署所需的密钥：\n\n- `DEPLOY_HOST`: 部署服务器的 SSH 地址\n- `DEPLOY_USER`: 部署服务器用户名\n- `DEPLOY_KEY`: 部署服务器私钥（[创建方法](https://github.com/appleboy/ssh-action#setting-up-a-ssh-key)）\n- `DEPLOY_PATH`: 部署服务器上的项目路径\n\n将以下文件添加至**项目目录**下的 `.github/workflows/` 目录下，在构建成功后触发部署：\n\n```yaml title=.github/workflows/deploy.yml\nname: Deploy\n\non:\n  workflow_run:\n    workflows:\n      - Docker Hub Release\n    types:\n      - completed\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    if: ${{ github.event.workflow_run.conclusion == 'success' }}\n    steps:\n      - name: Start Deployment\n        uses: bobheadxi/deployments@v1\n        id: deployment\n        with:\n          step: start\n          token: ${{ secrets.GITHUB_TOKEN }}\n          env: bot\n\n      - name: Run Remote SSH Command\n        uses: appleboy/ssh-action@master\n        env:\n          DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}\n        with:\n          host: ${{ secrets.DEPLOY_HOST }}\n          username: ${{ secrets.DEPLOY_USER }}\n          key: ${{ secrets.DEPLOY_KEY }}\n          envs: DEPLOY_PATH\n          script: |\n            cd $DEPLOY_PATH\n            docker compose up -d --pull always\n\n      - name: update deployment status\n        uses: bobheadxi/deployments@v0.6.2\n        if: always()\n        with:\n          step: finish\n          token: ${{ secrets.GITHUB_TOKEN }}\n          status: ${{ job.status }}\n          env: ${{ steps.deployment.outputs.env }}\n          deployment_id: ${{ steps.deployment.outputs.deployment_id }}\n```\n\n将上一部分的 `docker-compose.yml` 文件以及 `.env.prod` 配置文件添加至 `DEPLOY_PATH` 目录下，并修改 `docker-compose.yml` 文件中的镜像配置，替换为 Docker Hub 的仓库名称：\n\n```diff\n- build: .\n+ image: {organization}/{repository}:latest\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/best-practice/error-tracking.md",
    "content": "---\nsidebar_position: 2\ndescription: 使用 sentry 进行错误跟踪\n---\n\n# 错误跟踪\n\n在应用实际运行过程中，可能会出现各种各样的错误。可能是由于代码逻辑错误，也可能是由于用户输入错误，甚至是由于第三方服务的错误。这些错误都会导致应用的运行出现问题，这时候就需要对错误进行跟踪，以便及时发现问题并进行修复。NoneBot 提供了 `nonebot-plugin-sentry` 插件，支持 [sentry](https://sentry.io/) 平台，可以方便地进行错误跟踪。\n\n## 安装插件\n\n在使用前请先安装 `nonebot-plugin-sentry` 插件至项目环境中，可参考[获取商店插件](../tutorial/store.mdx#安装插件)来了解并选择安装插件的方式。如：\n\n在**项目目录**下执行以下命令：\n\n```bash\nnb plugin install nonebot-plugin-sentry\n```\n\n## 使用插件\n\n在安装完成之后，仅需要对插件进行简单的配置即可使用。\n\n### 获取 sentry DSN\n\n前往 [sentry](https://sentry.io/) 平台，注册并创建一个新的项目，然后在项目设置中找到 `Client Keys (DSN)`，复制其中的 `DSN` 值。\n\n### 配置插件\n\n:::caution 注意\n错误跟踪通常在生产环境中使用，因此开发环境中 `sentry_dsn` 留空即会停用插件。\n:::\n\n在项目 dotenv 配置文件中添加以下配置即可使用：\n\n```dotenv\nSENTRY_DSN=<your_sentry_dsn>\n```\n\n## 配置项\n\n配置项具体含义参考 [Sentry Docs](https://docs.sentry.io/platforms/python/configuration/options/)。\n\n- `sentry_dsn: str`\n- `sentry_debug: bool = False`\n- `sentry_release: str | None = None`\n- `sentry_release: str | None = None`\n- `sentry_environment: str | None = nonebot env`\n- `sentry_server_name: str | None = None`\n- `sentry_sample_rate: float = 1.`\n- `sentry_max_breadcrumbs: int = 100`\n- `sentry_attach_stacktrace: bool = False`\n- `sentry_send_default_pii: bool = False`\n- `sentry_in_app_include: List[str] = Field(default_factory=list)`\n- `sentry_in_app_exclude: List[str] = Field(default_factory=list)`\n- `sentry_request_bodies: str = \"medium\"`\n- `sentry_with_locals: bool = True`\n- `sentry_ca_certs: str | None = None`\n- `sentry_before_send: Callable[[Any, Any], Any | None] | None = None`\n- `sentry_before_breadcrumb: Callable[[Any, Any], Any | None] | None = None`\n- `sentry_transport: Any | None = None`\n- `sentry_http_proxy: str | None = None`\n- `sentry_https_proxy: str | None = None`\n- `sentry_shutdown_timeout: int = 2`\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/best-practice/htmlkit-render.md",
    "content": "---\nsidebar_position: 8\ndescription: 轻量化 HTML 绘图\n---\n\n# 轻量化 HTML 绘图\n\n图片是机器人交互中不可或缺的一部分，对于信息展示的直观性、美观性有很大的作用。\n基于 PIL 直接绘制图片具有良好的性能和存储开销，但是难以调试、维护过程式的绘图代码。\n使用浏览器渲染类插件可以方便地绘制网页，且能够直接通过 JS 对网页效果进行编程，但是它占用的存储和内存空间相对可观。\n\nNoneBot 提供的 `nonebot-plugin-htmlkit` 提供了另一种基于 HTML 和 CSS 语法的轻量化绘图选择：它基于 `litehtml` 解析库，无须安装额外的依赖即可使用，没有进程间通信带来的额外开销，且在支持 `webp` `avif` 等丰富图片格式的前提下，安装用的 wheel 文件大小仅有约 10 MB。\n\n作为粗略的性能参考，在一台 Ryzen 7 9700X 的 Windows 电脑上，渲染 [PEP 7](https://peps.python.org/pep-0007/) 的 HTML 页面（分辨率为 800x5788，大小约 1.4MB，从本地文件系统读取 CSS）大约需要 100ms，每个渲染任务内存最高占用约为 40MB.\n\n## 安装插件\n\n在使用前请先安装 `nonebot-plugin-htmlkit` 插件至项目环境中，可参考[获取商店插件](../tutorial/store.mdx#安装插件)来了解并选择安装插件的方式。如：\n\n在**项目目录**下执行以下命令：\n\n```bash\nnb plugin install nonebot-plugin-htmlkit\n```\n\n`nonebot-plugin-htmlkit` 插件目前兼容以下系统架构：\n\n- Windows x64\n- macOS arm64（M-系列芯片）\n- Linux x64 （非 Alpine 等 musl 系发行版）\n- Linux arm64 （非 Alpine 等 musl 系发行版）\n\n:::caution 访问网络内容\n\n如果需要访问网络资源（如 http(s) 网页内容），NoneBot 需要客户端型驱动器（Forward）。内置的驱动器有 `~httpx` 与 `~aiohttp`。\n\n详见[选择驱动器](../advanced/driver.md)。\n\n:::\n\n## 使用插件\n\n### 加载插件\n\n在使用本插件前同样需要使用 `require` 方法进行**加载**并**导入**需要使用的方法，可参考 [跨插件访问](../advanced/requiring.md) 一节进行了解，如：\n\n```python\nfrom nonebot import require\n\nrequire(\"nonebot_plugin_htmlkit\")\n\nfrom nonebot_plugin_htmlkit import html_to_pic, md_to_pic, template_to_pic, text_to_pic\n```\n\n插件会自动使用[配置中的参数](#配置-fontconfig)初始化 `fontconfig` 以提供字体查找功能。\n\n### 渲染 API\n\n`nonebot-plugin-htmlkit` 主要提供以下**异步**渲染函数：\n\n#### html_to_pic\n\n```python\nasync def html_to_pic(\n    html: str,\n    *,\n    base_url: str = \"\",\n    dpi: float = 144.0,\n    max_width: float = 800.0,\n    device_height: float = 600.0,\n    default_font_size: float = 12.0,\n    font_name: str = \"sans-serif\",\n    allow_refit: bool = True,\n    image_format: Literal[\"png\", \"jpeg\"] = \"png\",\n    jpeg_quality: int = 100,\n    lang: str = \"zh\",\n    culture: str = \"CN\",\n    img_fetch_fn: ImgFetchFn = combined_img_fetcher,\n    css_fetch_fn: CSSFetchFn = combined_css_fetcher,\n    urljoin_fn: Callable[[str, str], str] = urllib3.parse.urljoin,\n) -> bytes:\n    ...\n```\n\n最核心的渲染函数。\n\n`base_url` 和 `urljoin_fn` 控制着传入 `image_fetch_fn` 和 `css_fetch_fn` 回调的 url 内容。\n\n`allow_refit` 如果为真，渲染时会自动缩小产出图片的宽度到最适合的宽度，否则必定产出 `max_width` 宽度的图片。\n\n`max_width` 与 `device_height` 会在 `@media` 判断中被使用。\n\n`img_fetch_fn` 预期为一个异步可调用对象（函数），接收图片 url 并返回对应 url 的 jpeg 或 png 二进制数据（`bytes`），可在拒绝加载时返回 `None`.\n\n`css_fetch_fn` 预期为一个异步可调用对象（函数），接收目标 CSS url 并返回对应 url 的 CSS 文本（`str`），可在拒绝加载时返回 `None`.\n\n以下为辅助的封装函数，关键字参数若未特殊说明均与 `html_to_pic` 含义相同。\n\n#### text_to_pic\n\n```python\nasync def text_to_pic(\n    text: str,\n    css_path: str = \"\",\n    *,\n    max_width: int = 500,\n    allow_refit: bool = True,\n    image_format: Literal[\"png\", \"jpeg\"] = \"png\",\n    jpeg_quality: int = 100,\n) -> bytes:\n    ...\n```\n\n可用于渲染多行文本。\n\n`text` 会被放置于 `<div id=\"main\" class=\"main-box\"> <div class=\"text\">` 中，可据此编写 CSS 来改变文本表现。\n\n#### md_to_pic\n\n```python\nasync def md_to_pic(\n    md: str = \"\",\n    md_path: str = \"\",\n    css_path: str = \"\",\n    *,\n    max_width: int = 500,\n    img_fetch_fn: ImgFetchFn = combined_img_fetcher,\n    allow_refit: bool = True,\n    image_format: Literal[\"png\", \"jpeg\"] = \"png\",\n    jpeg_quality: int = 100,\n) -> bytes:\n    ...\n```\n\n可用于渲染 Markdown 文本。默认为 GitHub Markdown Light 风格，支持基于 `pygments` 的代码高亮。\n\n`md` 和 `md_path` 二选一，前者设置时应为 Markdown 的文本，后者设置时应为指向 Markdown 文本文件的路径。\n\n#### template_to_pic\n\n```python\nasync def template_to_pic(\n    template_path: str | PathLike[str] | Sequence[str | PathLike[str]],\n    template_name: str,\n    templates: Mapping[Any, Any],\n    filters: None | Mapping[str, Any] = None,\n    *,\n    max_width: int = 500,\n    device_height: int = 600,\n    base_url: str | None = None,\n    img_fetch_fn: ImgFetchFn = combined_img_fetcher,\n    css_fetch_fn: CSSFetchFn = combined_css_fetcher,\n    allow_refit: bool = True,\n    image_format: Literal[\"png\", \"jpeg\"] = \"png\",\n    jpeg_quality: int = 100,\n) -> bytes:\n    ...\n```\n\n渲染 jinja2 模板。\n\n`template_path` 为 jinja2 环境的路径，`template_name` 是环境中要加载模板的名字，`templates` 为传入模板的参数，`filters` 为过滤器名 -> 自定义过滤器的映射。\n\n### 控制外部资源获取\n\n通过传入 `img_fetch_fn` 与 `css_fetch_fn`，我们可以在实际访问资源前进行审查，修改资源的来源，或是对 IO 操作进行缓存。\n\n`img_fetch_fn` 预期为一个异步可调用对象（函数），接收图片 url 并返回对应 url 的 jpeg 或 png 二进制数据（`bytes`），可在拒绝加载时返回 `None`.\n\n`css_fetch_fn` 预期为一个异步可调用对象（函数），接收目标 CSS url 并返回对应 url 的 CSS 文本（`str`），可在拒绝加载时返回 `None`.\n\n如果你想要禁用外部资源加载/只从文件系统加载/只从网络加载，可以使用 `none_fetcher` `filesystem_***_fetcher` `network_***_fetcher`。\n\n默认的 fetcher 行为（对于 `file://` 从文件系统加载，其余从网络加载）位于 `combined_***_fetcher`，可以通过对其封装实现缓存等操作。\n\n## 配置项\n\n### 配置 fontconfig\n\n`htmlkit` 使用 `fontconfig` 查找字体，请参阅 [`fontconfig 用户手册`](https://fontconfig.pages.freedesktop.org/fontconfig/fontconfig-user) 了解环境变量的具体含义、如何通过编写配置文件修改字体配置等。\n\n#### fontconfig_file\n\n- **类型**: `str | None`\n- **默认值**: `None`\n\n覆盖默认的配置文件路径。\n\n#### fontconfig_path\n\n- **类型**: `str | None`\n- **默认值**: `None`\n\n覆盖默认的配置目录。\n\n#### fontconfig_sysroot\n\n- **类型**: `str | None`\n- **默认值**: `None`\n\n覆盖默认的 sysroot。\n\n#### fc_debug\n\n- **类型**: `str | None`\n- **默认值**: `None`\n\n设置 Fontconfig 的 debug 级别。\n\n#### fc_dbg_match_filter\n\n- **类型**: `str | None`\n- **默认值**: `None`\n\n当 `FC_DEBUG` 设置为 `MATCH2` 时，过滤 debug 输出。\n\n#### fc_lang\n\n- **类型**: `str | None`\n- **默认值**: `None`\n\n设置默认语言，否则从 `LOCALE` 环境变量获取。\n\n#### fontconfig_use_mmap\n\n- **类型**: `str | None`\n- **默认值**: `None`\n\n是否使用 `mmap(2)` 读取字体缓存。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/best-practice/multi-adapter.mdx",
    "content": "---\nsidebar_position: 4\ndescription: 插件跨平台支持\n---\n\n# 插件跨平台支持\n\nimport Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\n\n由于不同平台的事件与接口之间，存在着极大的差异性，NoneBot 通过[重载](../appendices/overload.md)的方式，使得插件可以在不同平台上正确响应。但为了减少跨平台的兼容性问题，我们应该尽可能的使用基类方法实现原生跨平台，而不是使用特定平台的方法。当基类方法无法满足需求时，我们可以使用依赖注入的方式，将特定平台的事件或机器人注入到事件处理函数中，实现针对特定平台的处理。\n\n:::tip 提示\n如果需要在多平台上**使用**跨平台插件，首先应该根据[注册适配器](../advanced/adapter.md#注册适配器)一节，为机器人注册各平台对应的适配器。\n:::\n\n## 基于基类的跨平台\n\n在[事件通用信息](../advanced/adapter.md#获取事件通用信息)中，我们了解了事件基类能够提供的通用信息。同时，[事件响应器操作](../appendices/session-control.mdx#更多事件响应器操作)也为我们提供了基本的用户交互方式。使用这些方法，可以让我们的插件运行在任何平台上。例如，一个简单的命令处理插件：\n\n```python {5,11}\nfrom nonebot import on_command\nfrom nonebot.adapters import Event\n\nasync def is_blacklisted(event: Event) -> bool:\n    return event.get_user_id() not in BLACKLIST\n\nweather = on_command(\"天气\", rule=is_blacklisted, priority=10, block=True)\n\n@weather.handle()\nasync def handle_function():\n    await weather.finish(\"今天的天气是...\")\n```\n\n由于此插件仅使用了事件通用信息和事件响应器操作的纯文本交互方式，这些方法不使用特定平台的信息或接口，因此是原生跨平台的，并不需要额外处理。但在一些较为复杂的需求下，例如发送图片消息时，并非所有平台都具有统一的接口，因此基类便无能为力，我们需要引入特定平台的适配器了。\n\n## 基于重载的跨平台\n\n重载是 NoneBot 跨平台操作的核心，在[事件类型与重载](../appendices/overload.md#重载)一节中，我们初步了解了如何通过类型注解来实现针对不同平台事件的处理方式。在[依赖注入](../advanced/dependency.mdx)一节中，我们又对依赖注入的使用方法进行了详细的介绍。结合这两节内容，我们可以实现更复杂的跨平台操作。\n\n### 处理近似事件\n\n对于一系列**差异不大**的事件，我们往往具有相同的处理逻辑。这时，我们不希望将相同的逻辑编写两遍，而应该复用代码，以实现在同一个事件处理函数中处理多个近似事件。我们可以使用[事件重载](../advanced/dependency.mdx#event)的特性来实现这一功能。例如：\n\n<Tabs groupId=\"python\">\n  <TabItem value=\"3.10\" label=\"Python 3.10+\" default>\n\n```python\nfrom nonebot import on_command\nfrom nonebot.adapters import Message\nfrom nonebot.params import CommandArg\nfrom nonebot.adapters.onebot.v11 import MessageEvent as OnebotV11MessageEvent\nfrom nonebot.adapters.onebot.v12 import MessageEvent as OnebotV12MessageEvent\n\necho = on_command(\"echo\", priority=10, block=True)\n\n@echo.handle()\nasync def handle_function(event: OnebotV11MessageEvent | OnebotV12MessageEvent, args: Message = CommandArg()):\n    await echo.finish(args)\n```\n\n  </TabItem>\n  <TabItem value=\"3.9\" label=\"Python 3.9\">\n\n```python\nfrom typing import Union\n\nfrom nonebot import on_command\nfrom nonebot.adapters import Message\nfrom nonebot.params import CommandArg\nfrom nonebot.adapters.onebot.v11 import MessageEvent as OnebotV11MessageEvent\nfrom nonebot.adapters.onebot.v12 import MessageEvent as OnebotV12MessageEvent\n\necho = on_command(\"echo\", priority=10, block=True)\n\n@echo.handle()\nasync def handle_function(event: Union[OnebotV11MessageEvent, OnebotV12MessageEvent], args: Message = CommandArg()):\n    await echo.finish(args)\n```\n\n  </TabItem>\n</Tabs>\n\n### 在依赖注入中使用重载\n\nNoneBot 依赖注入系统提供了自定义子依赖的方法，子依赖的类型同样会影响到事件处理函数的重载行为。例如：\n\n```python\nfrom datetime import datetime\n\nfrom nonebot import on_command\nfrom nonebot.adapters.console import MessageEvent\n\necho = on_command(\"echo\", priority=10, block=True)\n\ndef get_event_time(event: MessageEvent):\n    return event.time\n\n# 处理控制台消息事件\n@echo.handle()\nasync def handle_function(time: datetime = Depends(get_event_time)):\n    await echo.finish(time.strftime(\"%Y-%m-%d %H:%M:%S\"))\n```\n\n示例中 ，我们为 `handle_function` 事件处理函数注入了自定义的 `get_event_time` 子依赖，而此子依赖注入参数为 Console 适配器的 `MessageEvent`。因此 `handle_function` 仅会响应 Console 适配器的 `MessageEvent` ，而不能响应其他事件。\n\n### 处理多平台事件\n\n不同平台的事件之间，往往存在着极大的差异性。为了满足我们插件的跨平台运行，通常我们需要抽离业务逻辑，以保证代码的复用性。一个合理的做法是，在事件响应器的处理流程中，首先先针对不同平台的事件分别进行处理，提取出核心业务逻辑所需要的信息；然后再将这些信息传递给业务逻辑处理函数；最后将业务逻辑的输出以各平台合适的方式返回给用户。也就是说，与平台绑定的处理部分应该与平台无关部分尽量分离。例如：\n\n```python\nimport inspect\n\nfrom nonebot import on_command\nfrom nonebot.typing import T_State\nfrom nonebot.matcher import Matcher\nfrom nonebot.adapters import Message\nfrom nonebot.params import CommandArg, ArgPlainText\nfrom nonebot.adapters.console import Bot as ConsoleBot\nfrom nonebot.adapters.onebot.v11 import Bot as OnebotBot\nfrom nonebot.adapters.console import MessageSegment as ConsoleMessageSegment\n\nweather = on_command(\"天气\", priority=10, block=True)\n\n@weather.handle()\nasync def handle_function(matcher: Matcher, args: Message = CommandArg()):\n    if args.extract_plain_text():\n        matcher.set_arg(\"location\", args)\n\n\nasync def get_weather(state: T_State, location: str = ArgPlainText()):\n    if location not in [\"北京\", \"上海\", \"广州\", \"深圳\"]:\n        await weather.reject(f\"你想查询的城市 {location} 暂不支持，请重新输入！\")\n\n    state[\"weather\"] = \"⛅ 多云 20℃~24℃\"\n\n\n# 处理控制台询问\n@weather.got(\n    \"location\",\n    prompt=ConsoleMessageSegment.emoji(\"question\") + \"请输入地名\",\n    parameterless=[Depends(get_weather)],\n)\nasync def handle_console(bot: ConsoleBot):\n    pass\n\n# 处理 OneBot 询问\n@weather.got(\n    \"location\",\n    prompt=\"请输入地名\",\n    parameterless=[Depends(get_weather)],\n)\nasync def handle_onebot(bot: OnebotBot):\n    pass\n\n# 通过依赖注入或事件处理函数来进行业务逻辑处理\n\n# 处理控制台回复\n@weather.handle()\nasync def handle_console_reply(bot: ConsoleBot, state: T_State, location: str = ArgPlainText()):\n    await weather.send(\n        ConsoleMessageSegment.markdown(\n            inspect.cleandoc(\n                f\"\"\"\n                # {location}\n\n                - 今天\n\n                   {state['weather']}\n                \"\"\"\n            )\n        )\n    )\n\n# 处理 OneBot 回复\n@weather.handle()\nasync def handle_onebot_reply(bot: OnebotBot, state: T_State, location: str = ArgPlainText()):\n    await weather.send(f\"今天{location}的天气是{state['weather']}\")\n```\n\n:::tip 提示\nNoneBot 社区中有一些插件，例如[all4one](https://github.com/nonepkg/nonebot-plugin-all4one)、[send-anything-anywhere](https://github.com/felinae98/nonebot-plugin-send-anything-anywhere)，可以帮助你更好地处理跨平台功能，包括事件处理和消息发送等。\n:::\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/best-practice/scheduler.md",
    "content": "---\nsidebar_position: 0\ndescription: 定时执行任务\n---\n\n# 定时任务\n\n[APScheduler](https://apscheduler.readthedocs.io/en/3.x/) (Advanced Python Scheduler) 是一个 Python 第三方库，其强大的定时任务功能被广泛应用于各个场景。在 NoneBot 中，定时任务作为一个额外功能，依赖于基于 APScheduler 开发的 [`nonebot-plugin-apscheduler`](https://github.com/nonebot/plugin-apscheduler) 插件进行支持。\n\n## 安装插件\n\n在使用前请先安装 `nonebot-plugin-apscheduler` 插件至项目环境中，可参考[获取商店插件](../tutorial/store.mdx#安装插件)来了解并选择安装插件的方式。如：\n\n在**项目目录**下执行以下命令：\n\n```bash\nnb plugin install nonebot-plugin-apscheduler\n```\n\n## 使用插件\n\n`nonebot-plugin-apscheduler` 本质上是对 [APScheduler](https://apscheduler.readthedocs.io/en/3.x/) 进行了封装以适用于 NoneBot 开发，因此其使用方式与 APScheduler 本身并无显著区别。在此我们会简要介绍其调用方法，更多的使用方面的功能请参考[APScheduler 官方文档](https://apscheduler.readthedocs.io/en/3.x/userguide.html)。\n\n### 导入调度器\n\n由于 `nonebot_plugin_apscheduler` 作为插件，因此需要在使用前对其进行**加载**并**导入**其中的 `scheduler` 调度器来创建定时任务。使用 `require` 方法可轻松完成这一过程，可参考 [跨插件访问](../advanced/requiring.md) 一节进行了解。\n\n```python\nfrom nonebot import require\n\nrequire(\"nonebot_plugin_apscheduler\")\n\nfrom nonebot_plugin_apscheduler import scheduler\n```\n\n### 添加定时任务\n\n在 [APScheduler 官方文档](https://apscheduler.readthedocs.io/en/3.x/userguide.html#adding-jobs) 中提供了以下两种直接添加任务的方式：\n\n```python\nfrom nonebot import require\n\nrequire(\"nonebot_plugin_apscheduler\")\n\nfrom nonebot_plugin_apscheduler import scheduler\n\n# 基于装饰器的方式\n@scheduler.scheduled_job(\"cron\", hour=\"*/2\", id=\"job_0\", args=[1], kwargs={arg2: 2})\nasync def run_every_2_hour(arg1: int, arg2: int):\n    pass\n\n# 基于 add_job 方法的方式\ndef run_every_day(arg1: int, arg2: int):\n    pass\n\nscheduler.add_job(\n    run_every_day, \"interval\", days=1, id=\"job_1\", args=[1], kwargs={arg2: 2}\n)\n```\n\n:::caution 注意\n由于 APScheduler 的定时任务并不是**由事件响应器所触发的事件**，因此其任务函数无法同[事件处理函数](../tutorial/handler.mdx#事件处理函数)一样通过[依赖注入](../tutorial/event-data.mdx#认识依赖注入)获取上下文信息，也无法通过事件响应器对象的方法进行任何操作，因此我们需要使用[调用平台 API](../appendices/api-calling.mdx#调用平台-api)的方式来获取信息或收发消息。\n\n相对于事件处理依赖而言，编写定时任务更像是编写普通的函数，需要我们自行获取信息以及发送信息，请**不要**将事件处理依赖的特殊语法用于定时任务！\n:::\n\n关于 APScheduler 的更多使用方法，可以参考 [APScheduler 官方文档](https://apscheduler.readthedocs.io/en/3.x/index.html) 进行了解。\n\n### 配置项\n\n#### apscheduler_autostart\n\n- **类型**: `bool`\n- **默认值**: `True`\n\n是否自动启动 `scheduler` ，若不启动需要自行调用 `scheduler.start()`。\n\n#### apscheduler_log_level\n\n- **类型**: `int`\n- **默认值**: `30`\n\napscheduler 输出的日志等级\n\n- `WARNING` = `30` (默认)\n- `INFO` = `20`\n- `DEBUG` = `10` (只有在开启 nonebot 的 debug 模式才会显示 debug 日志)\n\n#### apscheduler_config\n\n- **类型**: `dict`\n- **默认值**: `{ \"apscheduler.timezone\": \"Asia/Shanghai\" }`\n\n`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)\n\n配置需要包含 `apscheduler.` 作为前缀，例如 `apscheduler.timezone`。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/best-practice/testing/README.mdx",
    "content": "---\nsidebar_position: 1\ndescription: 使用 NoneBug 进行单元测试\n\nslug: /best-practice/testing/\n---\n\n# 配置与测试事件响应器\n\nimport Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\n\n> 在计算机编程中，单元测试（Unit Testing）又称为模块测试，是针对程序模块（软件设计的最小单位）来进行正确性检验的测试工作。\n\n为了保证代码的正确运行，我们不仅需要对错误进行跟踪，还需要对代码进行正确性检验，也就是测试。NoneBot 提供了一个测试工具——NoneBug，它是一个 [pytest](https://docs.pytest.org/en/stable/) 插件，可以帮助我们便捷地进行单元测试。\n\n:::tip 提示\n建议在阅读本文档前先阅读 [pytest 官方文档](https://docs.pytest.org/en/stable/)来了解 pytest 的相关术语和基本用法。\n:::\n\n## 安装 NoneBug\n\n在**项目目录**下激活虚拟环境后运行以下命令安装 NoneBug：\n\n<Tabs groupId=\"tool\">\n  <TabItem value=\"poetry\" label=\"Poetry\" default>\n\n```bash\npoetry add nonebug -G test\n```\n\n  </TabItem>\n  <TabItem value=\"pdm\" label=\"PDM\">\n\n```bash\npdm add nonebug -dG test\n```\n\n  </TabItem>\n  <TabItem value=\"pip\" label=\"pip\">\n\n```bash\npip install nonebug\n```\n\n  </TabItem>\n</Tabs>\n\n要运行 NoneBug 测试，还需要额外安装 pytest 异步插件 `pytest-asyncio` 或 `anyio` 以支持异步测试。文档中，我们以 `pytest-asyncio` 为例：\n\n<Tabs groupId=\"tool\">\n  <TabItem value=\"poetry\" label=\"Poetry\" default>\n\n```bash\npoetry add pytest-asyncio -G test\n```\n\n  </TabItem>\n  <TabItem value=\"pdm\" label=\"PDM\">\n\n```bash\npdm add pytest-asyncio -dG test\n```\n\n  </TabItem>\n  <TabItem value=\"pip\" label=\"pip\">\n\n```bash\npip install pytest-asyncio\n```\n\n  </TabItem>\n</Tabs>\n\n## 配置测试\n\n在开始测试之前，我们需要对测试进行一些配置，以正确启动我们的机器人。\n\n首先我们需要配置 pytest-asyncio，在 `pyproject.toml` 的 pytest 配置部分添加：\n\n```toml\n[tool.pytest.ini_options]\nasyncio_mode = \"auto\"\nasyncio_default_fixture_loop_scope = \"session\"\n```\n\n然后，我们在 `tests` 目录下新建 `conftest.py` 文件，添加以下内容：\n\n```python title=tests/conftest.py\nimport pytest\nimport nonebot\nfrom pytest_asyncio import is_async_test\n# 导入适配器\nfrom nonebot.adapters.console import Adapter as ConsoleAdapter\n\ndef pytest_collection_modifyitems(items: list[pytest.Item]):\n    pytest_asyncio_tests = (item for item in items if is_async_test(item))\n    session_scope_marker = pytest.mark.asyncio(loop_scope=\"session\")\n    for async_test in pytest_asyncio_tests:\n        async_test.add_marker(session_scope_marker, append=False)\n\n@pytest.fixture(scope=\"session\", autouse=True)\nasync def after_nonebot_init(after_nonebot_init: None):\n    # 加载适配器\n    driver = nonebot.get_driver()\n    driver.register_adapter(ConsoleAdapter)\n\n    # 加载插件\n    nonebot.load_from_toml(\"pyproject.toml\")\n```\n\n这样，我们就可以在测试中使用机器人的插件了。通常，我们不需要自行初始化 NoneBot，NoneBug 已经为我们运行了 `nonebot.init()`。如果需要自定义 NoneBot 初始化的参数，我们可以在 `conftest.py` 中添加 `pytest_configure` 钩子函数。例如，我们可以修改 NoneBot 配置环境为 `test` 并从环境变量中输入配置：\n\n```python {4,6,8-10} title=tests/conftest.py\nimport os\n\nimport pytest\nfrom nonebug import NONEBOT_INIT_KWARGS\n\nos.environ[\"ENVIRONMENT\"] = \"test\"\n\ndef pytest_configure(config: pytest.Config):\n    config.stash[NONEBOT_INIT_KWARGS] = {\"secret\": os.getenv(\"INPUT_SECRET\")}\n```\n\nNoneBug 默认也会为我们管理 lifespan 的 startup 与 shutdown。如果不希望 NoneBug 管理 lifespan，你可以在 `pytest_configure` 里添加以下配置：\n\n```python\nimport pytest\nfrom nonebug import NONEBOT_START_LIFESPAN\n\ndef pytest_configure(config: pytest.Config):\n    config.stash[NONEBOT_START_LIFESPAN] = False\n```\n\n## 编写插件测试\n\n在配置完成插件加载后，我们就可以在测试中使用插件了。NoneBug 通过 pytest fixture `app` 提供各种测试方法，我们可以在测试中使用它来测试插件。现在，我们创建一个测试脚本来测试[深入指南](../../appendices/session-control.mdx)中编写的天气插件。首先，我们先要导入我们需要的模块：\n\n<details>\n  <summary>插件示例</summary>\n\n```python title=weather/__init__.py\nfrom nonebot import on_command\nfrom nonebot.rule import to_me\nfrom nonebot.matcher import Matcher\nfrom nonebot.adapters import Message\nfrom nonebot.params import CommandArg, ArgPlainText\n\nweather = on_command(\"天气\", rule=to_me(), aliases={\"weather\", \"天气预报\"})\n\n@weather.handle()\nasync def handle_function(matcher: Matcher, args: Message = CommandArg()):\n    if args.extract_plain_text():\n        matcher.set_arg(\"location\", args)\n\n@weather.got(\"location\", prompt=\"请输入地名\")\nasync def got_location(location: str = ArgPlainText()):\n    if location not in [\"北京\", \"上海\", \"广州\", \"深圳\"]:\n        await weather.reject(f\"你想查询的城市 {location} 暂不支持，请重新输入！\")\n    await weather.finish(f\"今天{location}的天气是...\")\n```\n\n</details>\n\n```python {4,5,9,11-16} title=tests/test_weather.py\nfrom datetime import datetime\n\nimport pytest\nfrom nonebug import App\nfrom nonebot.adapters.console import User, Message, MessageEvent\n\n@pytest.mark.asyncio\nasync def test_weather(app: App):\n    from awesome_bot.plugins.weather import weather\n\n    event = MessageEvent(\n        time=datetime.now(),\n        self_id=\"test\",\n        message=Message(\"/天气 北京\"),\n        user=User(id=\"user\"),\n    )\n```\n\n在上面的代码中，我们引入了 NoneBug 的测试 `App` 对象，以及必要的适配器消息与事件定义等。在测试函数 `test_weather` 中，我们导入了要进行测试的事件响应器 `weather`。请注意，由于需要等待 NoneBot 初始化并加载插件完毕，插件内容必须在**测试函数内部**进行导入。然后，我们创建了一个 `MessageEvent` 事件对象，它模拟了一个用户发送了 `/天气 北京` 的消息。接下来，我们使用 `app.test_matcher` 方法来测试 `weather` 事件响应器：\n\n```python {11-15} title=tests/test_weather.py\n@pytest.mark.asyncio\nasync def test_weather(app: App):\n    from awesome_bot.plugins.weather import weather\n\n    event = MessageEvent(\n        time=datetime.now(),\n        self_id=\"test\",\n        message=Message(\"/天气 北京\"),\n        user=User(id=\"user\"),\n    )\n    async with app.test_matcher(weather) as ctx:\n        bot = ctx.create_bot()\n        ctx.receive_event(bot, event)\n        ctx.should_call_send(event, \"今天北京的天气是...\", result=None)\n        ctx.should_finished(weather)\n```\n\n这里我们使用 `async with` 语句并通过参数指定要测试的事件响应器 `weather` 来进入测试上下文。在测试上下文中，我们可以使用 `ctx.create_bot` 方法创建一个虚拟的机器人实例，并使用 `ctx.receive_event` 方法来模拟机器人接收到消息事件。然后，我们就可以定义预期行为来测试机器人是否正确运行。在上面的代码中，我们使用 `ctx.should_call_send` 方法来断言机器人应该发送 `今天北京的天气是...` 这条消息，并且将发送函数的调用结果作为第三个参数返回给事件处理函数。如果断言失败，测试将会不通过。我们也可以使用 `ctx.should_finished` 方法来断言机器人应该结束会话。\n\n为了测试更复杂的情况，我们可以为添加更多的测试用例。例如，我们可以测试用户输入了一个不支持的地名时机器人的反应：\n\n```python {17-21,23-26} title=tests/test_weather.py\ndef make_event(message: str = \"\") -> MessageEvent:\n    return MessageEvent(\n        time=datetime.now(),\n        self_id=\"test\",\n        message=Message(message),\n        user=User(id=\"user\"),\n    )\n\n@pytest.mark.asyncio\nasync def test_weather(app: App):\n    from awesome_bot.plugins.weather import weather\n\n    async with app.test_matcher(weather) as ctx:\n        ...  # 省略前面的测试用例\n\n    async with app.test_matcher(weather) as ctx:\n        bot = ctx.create_bot()\n        event = make_event(\"/天气 南京\")\n        ctx.receive_event(bot, event)\n        ctx.should_call_send(event, \"你想查询的城市 南京 暂不支持，请重新输入！\", result=None)\n        ctx.should_rejected(weather)\n\n        event = make_event(\"北京\")\n        ctx.receive_event(bot, event)\n        ctx.should_call_send(event, \"今天北京的天气是...\", result=None)\n        ctx.should_finished(weather)\n```\n\n在上面的代码中，我们使用 `ctx.should_rejected` 来断言机器人应该请求用户重新输入。然后，我们再次使用 `ctx.receive_event` 方法来模拟用户回复了 `北京`，并使用 `ctx.should_finished` 来断言机器人应该结束会话。\n\n更多的 NoneBug 用法将在后续章节中介绍。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/best-practice/testing/_category_.json",
    "content": "{\n  \"label\": \"单元测试\",\n  \"position\": 5\n}\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/best-practice/testing/behavior.mdx",
    "content": "---\nsidebar_position: 2\ndescription: 测试事件响应、平台接口调用和会话控制\n---\n\n# 测试事件响应与会话操作\n\nimport Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\n\n在 NoneBot 接收到事件时，事件响应器根据优先级依次通过权限、响应规则来判断当前事件是否应该触发。事件响应流程中，机器人可能会通过 `send` 发送消息或者调用平台接口来执行预期的操作。因此，我们需要对这两种操作进行单元测试。\n\n在上一节中，我们对单个事件响应器进行了简单测试。但是在实际场景中，机器人可能定义了多个事件响应器，由于优先级和响应规则的存在，预期的事件响应器可能并不会被触发。NoneBug 支持同时测试多个事件响应器，以此来测试机器人的整体行为。\n\n## 测试事件响应\n\nNoneBug 提供了六种定义 `Rule` 和 `Permission` 预期行为的方法：\n\n- `should_pass_rule`\n- `should_not_pass_rule`\n- `should_ignore_rule`\n- `should_pass_permission`\n- `should_not_pass_permission`\n- `should_ignore_permission`\n\n:::tip 提示\n事件响应器类型的检查属于 `Permission` 的一部分，因此可以通过 `should_pass_permission` 和 `should_not_pass_permission` 方法来断言事件响应器类型的检查。\n:::\n\n下面我们根据插件示例来测试事件响应行为，我们首先定义两个事件响应器作为测试的对象：\n\n```python title=example.py\nfrom nonebot import on_command\n\ndef never_pass():\n    return False\n\nfoo = on_command(\"foo\")\nbar = on_command(\"bar\", permission=never_pass)\n```\n\n在这两个事件响应器中，`foo` 当收到 `/foo` 消息时会执行，而 `bar` 则不会执行。我们使用 NoneBug 来测试它们：\n\n<Tabs groupId=\"testScope\">\n  <TabItem value=\"separate\" label=\"独立测试\" default>\n\n```python {21,22,28,29} title=tests/test_example.py\nfrom datetime import datetime\n\nimport pytest\nfrom nonebug import App\nfrom nonebot.adapters.console import User, Message, MessageEvent\n\ndef make_event(message: str = \"\") -> MessageEvent:\n    return MessageEvent(\n        time=datetime.now(),\n        self_id=\"test\",\n        message=Message(message),\n        user=User(id=\"user\"),\n    )\n\n@pytest.mark.asyncio\nasync def test_example(app: App):\n    from awesome_bot.plugins.example import foo, bar\n\n    async with app.test_matcher(foo) as ctx:\n        bot = ctx.create_bot()\n        event = make_event(\"/foo\")\n        ctx.receive_event(bot, event)\n        ctx.should_pass_rule()\n        ctx.should_pass_permission()\n\n    async with app.test_matcher(bar) as ctx:\n        bot = ctx.create_bot()\n        event = make_event(\"/foo\")\n        ctx.receive_event(bot, event)\n        ctx.should_not_pass_rule()\n        ctx.should_not_pass_permission()\n```\n\n在上面的代码中，我们分别对 `foo` 和 `bar` 事件响应器进行响应测试。我们使用 `ctx.should_pass_rule` 和 `ctx.should_pass_permission` 断言 `foo` 事件响应器应该被触发，使用 `ctx.should_not_pass_rule` 和 `ctx.should_not_pass_permission` 断言 `bar` 事件响应器应该被忽略。\n\n  </TabItem>\n  <TabItem value=\"global\" label=\"集成测试\">\n\n```python title=tests/test_example.py\nfrom datetime import datetime\n\nimport pytest\nfrom nonebug import App\nfrom nonebot.adapters.console import User, Message, MessageEvent\n\ndef make_event(message: str = \"\") -> MessageEvent:\n    return MessageEvent(\n        time=datetime.now(),\n        self_id=\"test\",\n        message=Message(message),\n        user=User(id=\"user\"),\n    )\n\n@pytest.mark.asyncio\nasync def test_example(app: App):\n    from awesome_bot.plugins.example import foo, bar\n\n    async with app.test_matcher() as ctx:\n        bot = ctx.create_bot()\n        event = make_event(\"/foo\")\n        ctx.receive_event(bot, event)\n        ctx.should_pass_rule(foo)\n        ctx.should_pass_permission(foo)\n        ctx.should_not_pass_rule(bar)\n        ctx.should_not_pass_permission(bar)\n```\n\n在上面的代码中，我们对 `foo` 和 `bar` 事件响应器一起进行响应测试。我们使用 `ctx.should_pass_rule` 和 `ctx.should_pass_permission` 断言 `foo` 事件响应器应该被触发，使用 `ctx.should_not_pass_rule` 和 `ctx.should_not_pass_permission` 断言 `bar` 事件响应器应该被忽略。通过参数，我们可以指定断言的事件响应器。\n\n  </TabItem>\n</Tabs>\n\n当然，如果需要忽略某个事件响应器的响应规则和权限检查，强行进入响应流程，我们可以使用 `should_ignore_rule` 和 `should_ignore_permission` 方法：\n\n```python {21,22} title=tests/test_example.py\nfrom datetime import datetime\n\nimport pytest\nfrom nonebug import App\nfrom nonebot.adapters.console import User, Message, MessageEvent\n\ndef make_event(message: str = \"\") -> MessageEvent:\n    return MessageEvent(\n        time=datetime.now(),\n        self_id=\"test\",\n        message=Message(message),\n        user=User(id=\"user\"),\n    )\n\n@pytest.mark.asyncio\nasync def test_example(app: App):\n    from awesome_bot.plugins.example import foo, bar\n\n    async with app.test_matcher(bar) as ctx:\n        bot = ctx.create_bot()\n        event = make_event(\"/foo\")\n        ctx.receive_event(bot, event)\n        ctx.should_ignore_rule(bar)\n        ctx.should_ignore_permission(bar)\n```\n\n在忽略了响应规则和权限检查之后，就会进入 `bar` 事件响应器的响应流程。\n\n## 测试平台接口使用\n\n上一节的示例插件测试中，我们已经尝试了测试插件对事件的消息回复。通常情况下，事件处理流程中对平台接口的使用会通过事件响应器操作或者调用平台 API 两种途径进行。针对这两种途径，NoneBug 分别提供了 `ctx.should_call_send` 和 `ctx.should_call_api` 方法来测试平台接口的使用情况。\n\n1. `should_call_send`\n\n   定义事件响应器预期发送的消息，即通过[事件响应器操作 send](../../appendices/session-control.mdx#send)进行的操作。`should_call_send` 有四个参数：\n   - `event`：回复的目标事件。\n   - `message`：预期的消息对象，可以是 `str`、`Message` 或 `MessageSegment`。\n   - `result`：send 的返回值，将会返回给插件。\n   - `bot`（可选）：发送消息的 bot 对象。\n   - `**kwargs`：send 方法的额外参数。\n\n2. `should_call_api`\n   定义事件响应器预期调用的平台 API 接口，即通过[调用平台 API](../../appendices/api-calling.mdx#调用平台-api)进行的操作。`should_call_api` 有四个参数：\n   - `api`：API 名称。\n   - `data`：预期的请求数据。\n   - `result`：call_api 的返回值，将会返回给插件。\n   - `adapter`（可选）：调用 API 的平台适配器对象。\n   - `**kwargs`：call_api 方法的额外参数。\n\n下面是一个使用 `should_call_send` 和 `should_call_api` 方法的示例：\n\n我们先定义一个测试插件，在响应流程中向用户发送一条消息并调用 `Console` 适配器的 `bell` API。\n\n```python {8,9} title=example.py\nfrom nonebot import on_command\nfrom nonebot.adapters.console import Bot\n\nfoo = on_command(\"foo\")\n\n@foo.handle()\nasync def _(bot: Bot):\n    await foo.send(\"message\")\n    await bot.bell()\n```\n\n然后我们对该插件进行测试：\n\n```python title=tests/test_example.py\nfrom datetime import datetime\n\nimport pytest\nimport nonebot\nfrom nonebug import App\nfrom nonebot.adapters.console import Bot, User, Adapter, Message, MessageEvent\n\ndef make_event(message: str = \"\") -> MessageEvent:\n    return MessageEvent(\n        time=datetime.now(),\n        self_id=\"test\",\n        message=Message(message),\n        user=User(id=\"user\"),\n    )\n\n@pytest.mark.asyncio\nasync def test_example(app: App):\n    from awesome_bot.plugins.example import foo\n\n    async with app.test_matcher(foo) as ctx:\n        # highlight-start\n        adapter = nonebot.get_adapter(Adapter)\n        bot = ctx.create_bot(base=Bot, adapter=adapter)\n        # highlight-end\n        event = make_event(\"/foo\")\n        ctx.receive_event(bot, event)\n        # highlight-start\n        ctx.should_call_send(event, \"message\", result=None, bot=bot)\n        ctx.should_call_api(\"bell\", {}, result=None, adapter=adapter)\n        # highlight-end\n```\n\n请注意，对于在依赖注入中使用了非基类对象的情况，我们需要在 `create_bot` 方法中指定 `base` 和 `adapter` 参数，确保不会因为重载功能而出现非预期情况。\n\n## 测试会话控制\n\n在[会话控制](../../appendices/session-control.mdx)一节中，我们介绍了如何使用事件响应器操作来实现对用户的交互式会话。在上一节的示例插件测试中，我们其实已经使用了 `ctx.should_finished` 来断言会话结束。NoneBug 针对各种流程控制操作分别提供了相应的方法来定义预期的会话处理行为。它们分别是：\n\n- `should_finished`：断言会话结束，对应 `matcher.finish` 操作。\n- `should_rejected`：断言会话等待用户输入并重新执行当前事件处理函数，对应 `matcher.reject` 系列操作。\n- `should_paused`: 断言会话等待用户输入并执行下一个事件处理函数，对应 `matcher.pause` 操作。\n\n我们仅需在测试用例中的正确位置调用这些方法，就可以断言会话的预期行为。例如：\n\n```python title=example.py\nfrom nonebot import on_command\nfrom nonebot.typing import T_State\n\nfoo = on_command(\"foo\")\n\n@foo.got(\"key\", prompt=\"请输入密码\")\nasync def _(state: T_State, key: str = ArgPlainText()):\n    if key != \"some password\":\n        try_count = state.get(\"try_count\", 1)\n        if try_count >= 3:\n            await foo.finish(\"密码错误次数过多\")\n        else:\n            state[\"try_count\"] = try_count + 1\n            await foo.reject(\"密码错误，请重新输入\")\n    await foo.finish(\"密码正确\")\n```\n\n```python title=tests/test_example.py\nfrom datetime import datetime\n\nimport pytest\nfrom nonebug import App\nfrom nonebot.adapters.console import User, Message, MessageEvent\n\ndef make_event(message: str = \"\") -> MessageEvent:\n    return MessageEvent(\n        time=datetime.now(),\n        self_id=\"test\",\n        message=Message(message),\n        user=User(id=\"user\"),\n    )\n\n@pytest.mark.asyncio\nasync def test_example(app: App):\n    from awesome_bot.plugins.example import foo\n\n    async with app.test_matcher(foo) as ctx:\n        bot = ctx.create_bot()\n        event = make_event(\"/foo\")\n        ctx.receive_event(bot, event)\n        ctx.should_call_send(event, \"请输入密码\", result=None)\n        ctx.should_rejected(foo)\n        event = make_event(\"wrong password\")\n        ctx.receive_event(bot, event)\n        ctx.should_call_send(event, \"密码错误，请重新输入\", result=None)\n        ctx.should_rejected(foo)\n        event = make_event(\"wrong password\")\n        ctx.receive_event(bot, event)\n        ctx.should_call_send(event, \"密码错误，请重新输入\", result=None)\n        ctx.should_rejected(foo)\n        event = make_event(\"wrong password\")\n        ctx.receive_event(bot, event)\n        ctx.should_call_send(event, \"密码错误次数过多\", result=None)\n        ctx.should_finished(foo)\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/best-practice/testing/mock-network.md",
    "content": "---\nsidebar_position: 3\ndescription: 模拟网络通信以进行测试\n---\n\n# 模拟网络通信\n\nNoneBot 驱动器提供了多种方法来帮助适配器进行网络通信，主要包括客户端和服务端两种类型。模拟网络通信可以帮助我们更加接近实际机器人应用场景，进行更加真实的集成测试。同时，通过这种途径，我们还可以完成对适配器的测试。\n\nNoneBot 中的网络通信主要包括以下几种：\n\n- HTTP 服务端（WebHook）\n- WebSocket 服务端\n- HTTP 客户端\n- WebSocket 客户端\n\n下面我们将分别介绍如何使用 NoneBug 来模拟这几种通信方式。\n\n## 测试 HTTP 服务端\n\n当 NoneBot 作为 ASGI 服务端应用时，我们可以定义一系列的路由来处理 HTTP 请求，适配器同样也可以通过定义路由来响应机器人相关的网络通信。下面假设我们使用了一个适配器 `fake` ，它定义了一个路由 `/fake/http` ，用于接收平台 WebHook 并处理。实际应用测试时，应将该路由地址替换为**真实适配器注册的路由地址**。\n\n我们首先需要获取测试用模拟客户端：\n\n```python {5,6} title=tests/test_http_server.py\nfrom nonebug import App\n\n@pytest.mark.asyncio\nasync def test_http_server(app: App):\n    async with app.test_server() as ctx:\n        client = ctx.get_client()\n```\n\n默认情况下，`app.test_server()` 会通过 `nonebot.get_asgi` 获取测试对象，我们也可以通过参数指定 ASGI 应用：\n\n```python\nasync with app.test_server(asgi=asgi_app) as ctx:\n    ...\n```\n\n获取到模拟客户端后，即可像 `requests`、`httpx` 等库类似的方法进行使用：\n\n```python {3,11-14,16} title=tests/test_http_server.py\nimport nonebot\nfrom nonebug import App\nfrom nonebot.adapters.fake import Adapter\n\n@pytest.mark.asyncio\nasync def test_http_server(app: App):\n    adapter = nonebot.get_adapter(Adapter)\n\n    async with app.test_server() as ctx:\n        client = ctx.get_client()\n        response = await client.post(\"/fake/http\", json={\"bot_id\": \"fake\"})\n        assert response.status_code == 200\n        assert response.json() == {\"status\": \"success\"}\n        assert \"fake\" in nonebot.get_bots()\n\n    adapter.bot_disconnect(nonebot.get_bot(\"fake\"))\n```\n\n在上面的测试中，我们向 `/fake/http` 发送了一个模拟 POST 请求，适配器将会对该请求进行处理，我们可以通过检查请求返回是否正确、Bot 对象是否创建等途径来验证机器人是否正确运行。在完成测试后，我们通常需要对 Bot 对象进行清理，以避免对其他测试产生影响。\n\n## 测试 WebSocket 服务端\n\n当 NoneBot 作为 ASGI 服务端应用时，我们还可以定义一系列的路由来处理 WebSocket 通信。下面假设我们使用了一个适配器 `fake` ，它定义了一个路由 `/fake/ws` ，用于处理平台 WebSocket 连接信息。实际应用测试时，应将该路由地址替换为**真实适配器注册的路由地址**。\n\n我们同样需要通过 `app.test_server()` 获取测试用模拟客户端，这里就不再赘述。在获取到模拟客户端后，我们可以通过 `client.websocket_connect` 方法来模拟 WebSocket 连接：\n\n```python {3,11-15} title=tests/test_ws_server.py\nimport nonebot\nfrom nonebug import App\nfrom nonebot.adapters.fake import Adapter\n\n@pytest.mark.asyncio\nasync def test_ws_server(app: App):\n    adapter = nonebot.get_adapter(Adapter)\n\n    async with app.test_server() as ctx:\n        client = ctx.get_client()\n        async with client.websocket_connect(\"/fake/ws\") as ws:\n            await ws.send_json({\"bot_id\": \"fake\"})\n            response = await ws.receive_json()\n            assert response == {\"status\": \"success\"}\n            assert \"fake\" in nonebot.get_bots()\n```\n\n在上面的测试中，我们向 `/fake/ws` 进行了 WebSocket 模拟通信，通过发送消息与机器人进行交互，然后检查机器人发送的信息是否正确。\n\n## 测试 HTTP 客户端\n\n~~暂不支持~~\n\n## 测试 WebSocket 客户端\n\n~~暂不支持~~\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/community/contact.md",
    "content": "---\nsidebar-position: 0\ndescription: 遇到问题如何获取帮助\n---\n\n# 参与讨论\n\n如果在安装或者开发 NoneBot 过程中遇到了任何问题，或者有新奇的点子，欢迎参与我们的社区讨论：\n\n1. 点击下方链接前往 GitHub，前往 Issues 页面，在 `New Issue` Template 中选择 `Question`\n\n   NoneBot：[![NoneBot project link](https://img.shields.io/github/stars/nonebot/nonebot2?style=social)](https://github.com/nonebot/nonebot2)\n\n2. 通过 QQ 群（点击下方链接直达）\n\n   [![QQ Chat Group](https://img.shields.io/badge/QQ%E7%BE%A4-768887710-orange?style=social)](https://jq.qq.com/?_wv=1027&k=5OFifDh)\n\n3. 通过 QQ 频道\n\n   [![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)\n\n4. 通过 Discord 服务器（点击下方链接直达）\n\n   [![Discord Server](https://discordapp.com/api/guilds/847819937858584596/widget.png?style=shield)](https://discord.gg/VKtE6Gdc4h)\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/community/contributing.md",
    "content": "---\nsidebar-position: 1\ndescription: 如何为 NoneBot 贡献代码\n---\n\n# 贡献指南\n\n## Code of Conduct\n\n请参阅 [Code of Conduct](https://github.com/nonebot/nonebot2/blob/master/CODE_OF_CONDUCT.md)。\n\n## 参与开发\n\n请参阅 [Contributing](https://github.com/nonebot/nonebot2/blob/master/CONTRIBUTING.md)。\n\n## 鸣谢\n\n感谢以下开发者对 NoneBot2 作出的贡献：\n\n<a href=\"https://github.com/nonebot/nonebot2/graphs/contributors\">\n  <img src=\"https://contrib.rocks/image?repo=nonebot/nonebot2&max=1000\" />\n</a>\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/developer/adapter-writing.md",
    "content": "---\nsidebar_position: 1\ndescription: 编写适配器对接新的平台\n---\n\n# 编写适配器\n\n在编写适配器之前，我们需要先了解[适配器的功能与组成](../advanced/adapter#适配器功能与组成)，适配器通常由 `Adapter`、`Bot`、`Event` 和 `Message` 四个部分组成，在编写适配器时，我们需要继承 NoneBot 中的基类，并根据实际平台来编写每个部分功能。\n\n## 组织结构\n\nNoneBot 适配器项目通常以 `nonebot-adapter-{adapter-name}` 作为项目名，并以**命名空间包**的形式编写，即在 `nonebot/adapters/{adapter-name}` 目录中编写实际代码，例如：\n\n```tree\n📦 nonebot-adapter-{adapter-name}\n├── 📂 nonebot\n│   ├── 📂 adapters\n│   │   ├── 📂 {adapter-name}\n│   │   │   ├── 📜 __init__.py\n│   │   │   ├── 📜 adapter.py\n│   │   │   ├── 📜 bot.py\n│   │   │   ├── 📜 config.py\n│   │   │   ├── 📜 event.py\n│   │   │   └── 📜 message.py\n├── 📜 pyproject.toml\n└── 📜 README.md\n```\n\n:::tip 提示\n\n上述的项目结构仅作推荐，不做强制要求，保证实际可用性即可。\n\n:::\n\n### 使用 NB-CLI 创建项目\n\n我们可以使用脚手架快速创建项目：\n\n```shell\nnb adapter create\n```\n\n按照指引，输入适配器名称以及存储位置，即可创建一个带有基本结构的适配器项目。\n\n## 组成部分\n\n:::tip 提示\n\n本章节的代码中提到的 `Adapter`、`Bot`、`Event` 和 `Message` 等，均为下文中适配器所编写的类，而非 NoneBot 中的基类。\n\n:::\n\n### Log\n\n适配器在处理时通常需要打印日志，但直接使用 NoneBot 的默认 `logger` 不方便区分适配器输出和其它日志。因此我们可以使用 NoneBot 提供的 `logger_wrapper` 方法，自定义一个 `log` 函数用于快捷打印适配器日志：\n\n```python {3} title=log.py\nfrom nonebot.utils import logger_wrapper\n\nlog = logger_wrapper(\"your_adapter_name\")\n```\n\n这个 `log` 函数会在默认 `logger` 中添加适配器名称前缀，它接收三个参数：日志等级、日志内容以及可选的异常，具体用法如下：\n\n```python\nfrom .log import log\n\nlog(\"DEBUG\", \"A DEBUG log.\")\nlog(\"INFO\", \"A INFO log.\")\n\ntry:\n    ...\nexcept Exception as e:\n    log(\"ERROR\", \"something error.\", e)\n```\n\n### Config\n\n通常适配器需要一些配置项，例如平台连接密钥等。适配器的配置方法与[插件配置](../appendices/config#%E6%8F%92%E4%BB%B6%E9%85%8D%E7%BD%AE)类似，例如：\n\n```python title=config.py\nfrom pydantic import BaseModel\n\nclass Config(BaseModel):\n    xxx_id: str\n    xxx_token: str\n```\n\n配置项的读取将在下方 [Adapter](#adapter) 中介绍。\n\n### Adapter\n\nAdapter 负责转换事件、调用接口，以及正确创建 Bot 对象并注册到 NoneBot 中。在编写平台相关内容之前，我们需要继承基类，并实现适配器的基本信息：\n\n```python {9,11,14,18} title=adapter.py\nfrom typing import Any\nfrom typing_extensions import override\n\nfrom nonebot.drivers import Driver\nfrom nonebot import get_plugin_config\nfrom nonebot.adapters import Adapter as BaseAdapter\n\nfrom .config import Config\n\nclass Adapter(BaseAdapter):\n    @override\n    def __init__(self, driver: Driver, **kwargs: Any):\n        super().__init__(driver, **kwargs)\n        # 读取适配器所需的配置项\n        self.adapter_config: Config = get_plugin_config(Config)\n\n    @classmethod\n    @override\n    def get_name(cls) -> str:\n        \"\"\"适配器名称\"\"\"\n        return \"your_adapter_name\"\n```\n\n#### 与平台交互\n\nNoneBot 提供了多种 [Driver](../advanced/driver) 来帮助适配器进行网络通信，主要分为客户端和服务端两种类型。我们需要**根据平台文档和特性**选择合适的通信方式，并编写相关方法用于初始化适配器，与平台建立连接和进行交互：\n\n##### 客户端通信方式\n\n```python {12,23,24} title=adapter.py\nimport asyncio\nfrom typing_extensions import override\n\nfrom nonebot import get_plugin_config\nfrom nonebot.exception import WebSocketClosed\nfrom nonebot.drivers import Request, WebSocketClientMixin\n\nclass Adapter(BaseAdapter):\n    @override\n    def __init__(self, driver: Driver, **kwargs: Any):\n        super().__init__(driver, **kwargs)\n        self.adapter_config: Config = get_plugin_config(Config)\n        self.task: Optional[asyncio.Task] = None  # 存储 ws 任务\n        self.setup()\n\n    def setup(self) -> None:\n        if not isinstance(self.driver, WebSocketClientMixin):\n            # 判断用户配置的Driver类型是否符合适配器要求，不符合时应抛出异常\n            raise RuntimeError(\n                f\"Current driver {self.config.driver} doesn't support websocket client connections!\"\n                f\"{self.get_name()} Adapter need a WebSocket Client Driver to work.\"\n            )\n        # 在 NoneBot 启动和关闭时进行相关操作\n        self.driver.on_startup(self.startup)\n        self.driver.on_shutdown(self.shutdown)\n\n    async def startup(self) -> None:\n        \"\"\"定义启动时的操作，例如和平台建立连接\"\"\"\n        self.task = asyncio.create_task(self._forward_ws())  # 建立 ws 连接\n\n    async def _forward_ws(self):\n        request = Request(\n            method=\"GET\",\n            url=\"your_platform_websocket_url\",\n            headers={\"token\": \"...\"},  # 鉴权请求头\n        )\n        while True:\n            try:\n                async with self.websocket(request) as ws:\n                    try:\n                        # 处理 websocket\n                        ...\n                    except WebSocketClosed as e:\n                        log(\n                            \"ERROR\",\n                            \"<r><bg #f8bbd0>WebSocket Closed</bg #f8bbd0></r>\",\n                            e,\n                        )\n                    except Exception as e:\n                        log(\n                            \"ERROR\",\n                            \"<r><bg #f8bbd0>Error while process data from \"\n                            \"websocket platform_websocket_url. \"\n                            \"Trying to reconnect...</bg #f8bbd0></r>\",\n                            e,\n                        )\n                    finally:\n                        # 这里要断开 Bot 连接\n            except Exception as e:\n                # 尝试重连\n                log(\n                    \"ERROR\",\n                    \"<r><bg #f8bbd0>Error while setup websocket to \"\n                    \"platform_websocket_url. Trying to reconnect...</bg #f8bbd0></r>\",\n                    e,\n                )\n                await asyncio.sleep(3)  # 重连间隔\n\n    async def shutdown(self) -> None:\n        \"\"\"定义关闭时的操作，例如停止任务、断开连接\"\"\"\n\n        # 断开 ws 连接\n        if self.task is not None and not self.task.done():\n            self.task.cancel()\n```\n\n##### 服务端通信方式\n\n```python {30,38} title=adapter.py\nfrom nonebot import get_plugin_config\nfrom nonebot.drivers import (\n    Request,\n    ASGIMixin,\n    WebSocket,\n    HTTPServerSetup,\n    WebSocketServerSetup\n)\n\nclass Adapter(BaseAdapter):\n    @override\n    def __init__(self, driver: Driver, **kwargs: Any):\n        super().__init__(driver, **kwargs)\n        self.adapter_config: Config = get_plugin_config(Config)\n        self.setup()\n\n    def setup(self) -> None:\n        if not isinstance(self.driver, ASGIMixin):\n            raise RuntimeError(\n                f\"Current driver {self.config.driver} doesn't support asgi server!\"\n                f\"{self.get_name()} Adapter need a asgi server driver to work.\"\n            )\n        # 建立服务端路由\n        # HTTP Webhook 路由\n        http_setup = HTTPServerSetup(\n            URL(\"your_webhook_url\"),  # 路由地址\n            \"POST\",  # 接收的方法\n            \"WEBHOOK name\",  # 路由名称\n            self._handle_http,  # 处理函数\n        )\n        self.setup_http_server(http_setup)\n\n        # 反向 Websocket 路由\n        ws_setup = WebSocketServerSetup(\n            URL(\"your_websocket_url\"),  # 路由地址\n            \"WebSocket name\",  # 路由名称\n            self._handle_ws,  # 处理函数\n        )\n        self.setup_websocket_server(ws_setup)\n\n\n    async def _handle_http(self, request: Request) -> Response:\n        \"\"\"HTTP 路由处理函数，只有一个类型为 Request 的参数，且返回值类型为 Response\"\"\"\n        ...\n        return Response(\n            status_code=200,  # 状态码\n            headers={\"something\": \"something\"},  # 响应头\n            content=\"xxx\",  # 响应内容\n        )\n\n    async def _handle_ws(self, websocket: WebSocket) -> Any:\n        \"\"\"WebSocket 路由处理函数，只有一个类型为 WebSocket 的参数\"\"\"\n        ...\n```\n\n更多通信交互方式可以参考以下适配器：\n\n- [OneBot](https://github.com/nonebot/adapter-onebot/blob/master/nonebot/adapters/onebot/v11/adapter.py) - `WebSocket 客户端`、`WebSocket 服务端`、`HTTP WEBHOOK`、`HTTP POST`\n- [QQ](https://github.com/nonebot/adapter-qq/blob/master/nonebot/adapters/qq/adapter.py) - `WebSocket 服务端`、`HTTP WEBHOOK`\n- [Telegram](https://github.com/nonebot/adapter-telegram/blob/beta/nonebot/adapters/telegram/adapter.py) - `HTTP WEBHOOK`\n\n#### 建立 Bot 连接\n\n在与平台建立连接后，我们需要将 [Bot](#bot) 实例化，并调用适配器提供的的 `bot_connect` 方法告知 NoneBot 建立了 Bot 连接。在与平台断开连接或出现某些异常进行重连时，我们需要调用 `bot_disconnect` 方法告知 NoneBot 断开了 Bot 连接。\n\n```python {7,8,11} title=adapter.py\nfrom .bot import Bot\n\nclass Adapter(BaseAdapter):\n\n    def _handle_connect(self):\n        bot_id = ...  # 通过配置或者平台 API 等方式，获取到 Bot 的 ID\n        bot = Bot(self, self_id=bot_id)  # 实例化 Bot\n        self.bot_connect(bot)  # 建立 Bot 连接\n\n    def _handle_disconnect(self):\n        self.bot_disconnect(bot)  # 断开 Bot 连接\n```\n\n#### 转换 Event 事件\n\n在接收到来自平台的事件数据后，我们需要将其转为适配器的 [Event](#event)，并调用 Bot 的 `handle_event` 方法来让 Bot 对事件进行处理：\n\n```python title=adapter.py\nimport asyncio\nfrom typing import Any, Dict\n\nfrom nonebot.compat import type_validate_python\n\nfrom .bot import Bot\nfrom .event import Event\nfrom .log import log\n\nclass Adapter(BaseAdapter):\n\n    @classmethod\n    def payload_to_event(cls, payload: Dict[str, Any]) -> Event:\n        \"\"\"根据平台事件的特性，转换平台 payload 为具体 Event\n\n        Event 模型继承自 pydantic.BaseModel，具体请参考 pydantic 文档\n        \"\"\"\n\n        # 做一层异常处理，以应对平台事件数据的变更\n        try:\n            return type_validate_python(your_event_class, payload)\n        except Exception as e:\n            # 无法正常解析为具体 Event 时，给出日志提示\n            log(\n                \"WARNING\",\n                f\"Parse event error: {str(payload)}\",\n            )\n            # 也可以尝试转为基础 Event 进行处理\n            return type_validate_python(Event, payload)\n\n\n    async def _forward(self, bot: Bot):\n\n        payload: Dict[str, Any]  # 接收到的事件数据\n        event = self.payload_to_event(payload)\n        # 让 bot 对事件进行处理\n        asyncio.create_task(bot.handle_event(event))\n```\n\n#### 调用平台 API\n\n我们需要实现 `Adapter` 的 `_call_api` 方法，使开发者能够调用平台提供的 API。如果通过 WebSocket 通信可以通过 `send` 方法来发送数据，如果采用 HTTP 请求，则需要通过 NoneBot 提供的 `Request` 对象，调用 `driver` 的 `request` 方法来发送请求。\n\n```python {11} title=adapter.py\nfrom typing import Any\nfrom typing_extensions import override\n\nfrom nonebot.drivers import Request, WebSocket\n\nfrom .bot import Bot\n\nclass Adapter(BaseAdapter):\n\n    @override\n    async def _call_api(self, bot: Bot, api: str, **data: Any) -> Any:\n        log(\"DEBUG\", f\"Calling API <y>{api}</y>\")  # 给予日志提示\n        platform_data = your_handle_data_method(data)  # 自行将数据转为平台所需要的格式\n\n        # 采用 HTTP 请求的方式，需要构造一个 Request 对象\n        request = Request(\n            method=\"GET\",  # 请求方法\n            url=api,  # 接口地址\n            headers=...,  # 请求头，通常需要包含鉴权信息\n            params=platform_data,  # 自行处理数据的传输形式\n            # json=platform_data,\n            # data=platform_data,\n        )\n        # 发送请求，返回结果\n        return await self.driver.request(request)\n\n\n        # 采用 WebSocket 通信的方式，可以直接调用 send 方法发送数据\n        # 通过某种方式获取到 bot 对应的 websocket 对象\n        ws: WebSocket = your_get_websocket_method(bot.self_id)\n\n        await ws.send_text(platform_data)  # 发送 str 类型的数据\n        await ws.send_bytes(platform_data)  # 发送 bytes 类型的数据\n        await ws.send(platform_data)  # 是以上两种方式的合体\n\n        # 接收并返回结果，同样的，也有 str 和 bytes 的区别\n        return await ws.receive_text()\n        return await ws.receive_bytes()\n        return await ws.receive()\n```\n\n`调用平台 API` 实现方式具体可以参考以下适配器：\n\nWebsocket:\n\n- [OneBot V11](https://github.com/nonebot/adapter-onebot/blob/54270edbbdb2a71332d744f90b1a3d7f4bf6463a/nonebot/adapters/onebot/v11/adapter.py#L167-L177)\n- [OneBot V12](https://github.com/nonebot/adapter-onebot/blob/54270edbbdb2a71332d744f90b1a3d7f4bf6463a/nonebot/adapters/onebot/v12/adapter.py#L204-L218)\n\nHTTP:\n\n- [OneBot V11](https://github.com/nonebot/adapter-onebot/blob/54270edbbdb2a71332d744f90b1a3d7f4bf6463a/nonebot/adapters/onebot/v11/adapter.py#L179-L215)\n- [OneBot V12](https://github.com/nonebot/adapter-onebot/blob/54270edbbdb2a71332d744f90b1a3d7f4bf6463a/nonebot/adapters/onebot/v12/adapter.py#L220-L266)\n- [QQ](https://github.com/nonebot/adapter-qq/blob/dc5d437e101f0e3db542de3300758a035ed7036e/nonebot/adapters/qq/adapter.py#L599-L605)\n- [Telegram](https://github.com/nonebot/adapter-telegram/blob/4a8633627e619245516767f5503dec2f58fe2193/nonebot/adapters/telegram/adapter.py#L148-L253)\n- [飞书](https://github.com/nonebot/adapter-feishu/blob/f8ab05e6d57a5e9013b944b0d019ca777725dfb0/nonebot/adapters/feishu/adapter.py#L201-L218)\n\n### Bot\n\nBot 是机器人开发者能够直接获取并使用的核心对象，负责存储平台机器人相关信息，并提供回复事件、调用 API 的上层方法。我们需要继承基类 `Bot`，并实现相关方法：\n\n```python {20,25,34} title=bot.py\nfrom typing import TYPE_CHECKING, Any, Union\nfrom typing_extensions import override\n\nfrom nonebot.message import handle_event\nfrom nonebot.adapters import Bot as BaseBot\n\nfrom .event import Event\nfrom .message import Message, MessageSegment\n\nif TYPE_CHECKING:\n    from .adapter import Adapter\n\n\nclass Bot(BaseBot):\n    \"\"\"\n    your_adapter_name 协议 Bot 适配。\n    \"\"\"\n\n    @override\n    def __init__(self, adapter: Adapter, self_id: str, **kwargs: Any):\n        super().__init__(adapter, self_id)\n        self.adapter: Adapter = adapter\n        # 一些有关 Bot 的信息也可以在此定义和存储\n\n    async def handle_event(self, event: Event):\n        # 根据需要，对事件进行某些预处理，例如：\n        # 检查事件是否和机器人有关操作，去除事件消息首尾的 @bot\n        # 检查事件是否有回复消息，调用平台 API 获取原始消息的消息内容\n        ...\n        # 调用 handle_event 让 NoneBot 对事件进行处理\n        await handle_event(self, event)\n\n    @override\n    async def send(\n        self,\n        event: Event,\n        message: Union[str, Message, MessageSegment],\n        **kwargs: Any,\n    ) -> Any:\n        # 根据平台实现 Bot 回复事件的方法\n\n        # 将消息处理为平台所需的格式后，调用发送消息接口进行发送，例如：\n        data = message_to_platform_data(message)\n        await self.send_message(\n            data=data,\n            ...\n        )\n```\n\n### Event\n\nEvent 是 NoneBot 中的事件主体对象，所有平台消息在进入处理流程前需要转换为 NoneBot 事件。我们需要继承基类 `Event`，并实现相关方法：\n\n```python {5,8,13,18,23,28,33} title=event.py\nfrom typing_extensions import override\n\nfrom nonebot.compat import model_dump\nfrom nonebot.adapters import Event as BaseEvent\n\nclass Event(BaseEvent):\n\n    @override\n    def get_event_name(self) -> str:\n        # 返回事件的名称，用于日志打印\n        return \"event name\"\n\n    @override\n    def get_event_description(self) -> str:\n        # 返回事件的描述，用于日志打印，请注意转义 loguru tag\n        return escape_tag(repr(model_dump(self)))\n\n    @override\n    def get_message(self):\n        # 获取事件消息的方法，根据事件具体实现，如果事件非消息类型事件，则抛出异常\n        raise ValueError(\"Event has no message!\")\n\n    @override\n    def get_user_id(self) -> str:\n        # 获取用户 ID 的方法，根据事件具体实现，如果事件没有用户 ID，则抛出异常\n        raise ValueError(\"Event has no context!\")\n\n    @override\n    def get_session_id(self) -> str:\n        # 获取事件会话 ID 的方法，根据事件具体实现，如果事件没有相关 ID，则抛出异常\n        raise ValueError(\"Event has no context!\")\n\n    @override\n    def is_tome(self) -> bool:\n        # 判断事件是否和机器人有关\n        return False\n```\n\n然后根据平台消息的类型，编写各种不同的事件，并且注意要根据事件类型实现 `get_type` 方法，具体请参考[事件类型](../advanced/adapter#事件类型)。消息类型事件还应重写 `get_message` 和 `get_user_id` 等方法，例如：\n\n```python {7,16,20,25,34,42} title=event.py\nfrom .message import Message\n\nclass HeartbeatEvent(Event):\n    \"\"\"心跳时间，通常为元事件\"\"\"\n\n    @override\n    def get_type(self) -> str:\n        return \"meta_event\"\n\nclass MessageEvent(Event):\n    \"\"\"消息事件\"\"\"\n    message_id: str\n    user_id: str\n\n    @override\n    def get_type(self) -> str:\n        return \"message\"\n\n    @override\n    def get_message(self) -> Message:\n        # 返回事件消息对应的 NoneBot Message 对象\n        return self.message\n\n    @override\n    def get_user_id(self) -> str:\n        return self.user_id\n\nclass JoinRoomEvent(Event):\n    \"\"\"加入房间事件，通常为通知事件\"\"\"\n    user_id: str\n    room_id: str\n\n    @override\n    def get_type(self) -> str:\n        return \"notice\"\n\nclass ApplyAddFriendEvent(Event):\n    \"\"\"申请添加好友事件，通常为请求事件\"\"\"\n    user_id: str\n\n    @override\n    def get_type(self) -> str:\n        return \"request\"\n```\n\n### Message\n\nMessage 负责正确序列化消息，以便机器人插件处理。我们需要继承 `MessageSegment` 和 `Message` 两个类，并实现相关方法：\n\n```python {9,12,17,22,27,30,36} title=message.py\nfrom typing import Type, Iterable\nfrom typing_extensions import override\n\nfrom nonebot.utils import escape_tag\n\nfrom nonebot.adapters import Message as BaseMessage\nfrom nonebot.adapters import MessageSegment as BaseMessageSegment\n\nclass MessageSegment(BaseMessageSegment[\"Message\"]):\n    @classmethod\n    @override\n    def get_message_class(cls) -> Type[\"Message\"]:\n        # 返回适配器的 Message 类型本身\n        return Message\n\n    @override\n    def __str__(self) -> str:\n        # 返回该消息段的纯文本表现形式，通常在日志中展示\n        return \"text of MessageSegment\"\n\n    @override\n    def is_text(self) -> bool:\n        # 判断该消息段是否为纯文本\n        return self.type == \"text\"\n\n\nclass Message(BaseMessage[MessageSegment]):\n    @classmethod\n    @override\n    def get_segment_class(cls) -> Type[MessageSegment]:\n        # 返回适配器的 MessageSegment 类型本身\n        return MessageSegment\n\n    @staticmethod\n    @override\n    def _construct(msg: str) -> Iterable[MessageSegment]:\n        # 实现从字符串中构造消息数组，如无字符串嵌入格式可直接返回文本类型 MessageSegment\n        ...\n```\n\n然后根据平台具体的消息类型，来实现各种 `MessageSegment` 消息段，具体可以参考以下适配器：\n\n- [OneBot V11](https://github.com/nonebot/adapter-onebot/blob/54270edbbdb2a71332d744f90b1a3d7f4bf6463a/nonebot/adapters/onebot/v11/message.py#L25-L259)\n- [QQ](https://github.com/nonebot/adapter-qq/blob/dc5d437e101f0e3db542de3300758a035ed7036e/nonebot/adapters/qq/message.py#L30-L520)\n- [Telegram](https://github.com/nonebot/adapter-telegram/blob/4a8633627e619245516767f5503dec2f58fe2193/nonebot/adapters/telegram/message.py#L13-L414)\n\n## 适配器测试\n\n关于适配器测试相关内容在这里不再展开，开发者可以根据需要进行合适的测试。这里为开发者提供几个常见问题的解决方法：\n\n1. 在测试中无法导入 editable 模式安装的适配器代码。在 pytest 的 `conftest.py` 内添加如下代码：\n\n   ```python title=tests/conftest.py\n   from pathlib import Path\n   import nonebot.adapters\n   nonebot.adapters.__path__.append(  # type: ignore\n       str((Path(__file__).parent.parent / \"nonebot\" / \"adapters\").resolve())\n   )\n   ```\n\n2. 需要计算适配器测试覆盖率，请在 `pyproject.toml` 中添加 pytest 配置：\n\n   ```toml title=pyproject.toml\n   [tool.pytest.ini_options]\n   addopts = \"--cov nonebot/adapters/{adapter-name} --cov-report term-missing\"\n   ```\n\n## 后续工作\n\n在完成适配器代码的编写后，如果想要将适配器发布到 NoneBot 商店，我们需要将适配器发布到 PyPI 中，然后前往[商店](/store/adapters)页面，切换到适配器页签，点击**发布适配器**按钮，填写适配器相关信息并提交。\n\n另外建议编写适配器文档或者一些插件开发示例，以便其他开发者使用我们的适配器。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/developer/plugin-publishing.mdx",
    "content": "---\nsidebar_position: 0\ndescription: 在商店发布自己的插件\n---\n\n# 发布插件\n\nimport Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\n\nNoneBot 为开发者提供了分享插件的官方商店。本指南囊括**从创建项目到发布到 PyPI，最终提交商店审核**的全过程。\n\n:::warning 警告\n如果你的插件只是满足自用需求，则完全可以选择**不发布插件**。发布插件**不是**使用插件的必要条件。\n\nNoneBot 社区对于插件有一定质量要求，对于不符合要求的插件，社区成员将会要求修改，直至符合要求后才能通过审核；如果长期未更新修改，社区将会关闭当前请求，之后如需发布请重新提交发布插件请求。相应的要求会在本章节以下部分介绍。\n:::\n\n:::tip 提示\n本章节仅包含插件发布流程指导，插件开发请查阅前述章节。\n:::\n\n## 准备工作\n\n### 插件命名规范\n\nNoneBot 插件使用下述命名规范：\n\n- 对于**项目名**，建议统一以 `nonebot-plugin-` 开头，之后为拟定的插件名字，词间用横杠 `-` 分隔；\n  - **项目名**用于代码仓库名称、PyPI 包的发布名称等；\n  - 本文使用 `nonebot-plugin-{your-plugin-name}` 为例。\n- 对于**模块名**，建议与**项目名**一致，但词间用下划线 `_` 分隔，即统一以 `nonebot_plugin_` 开头，之后为拟定的插件名字；\n  - **模块名**用于程序导入使用，应为插件文件（夹）的名称；\n  - 本文使用 `nonebot_plugin_{your_plugin_name}` 为例。\n\n### 项目结构\n\n:::tip 提示\n本段所述的项目结构仅作推荐，不做强制要求。\n:::\n\n插件程序本身结构可参考[插件结构](../tutorial/create-plugin.md#插件结构)一节，唯一区别在于，插件包可以直接处于项目顶层。\n\n插件项目的一种组织结构如下：\n\n```tree\n📦 nonebot-plugin-{your-plugin-name}\n├── 📂 nonebot_plugin_{your_plugin_name}\n│   ├── 📜 __init__.py\n│   └── 📜 config.py\n├── 📜 pyproject.toml\n└── 📜 README.md\n```\n\n功能开发可以在 `__init__.py` 中进行或在包内部创建其他模块并在 `__init__.py` 中导入。\n\n### 从项目模板开始\n\n为降低新手门槛，我们提供三条清晰、完整、可复制的发布路径。\n\n:::tip 提示\n你只需选择一条与你习惯一致的路径，**完整跟随即可成功发布**。无需在不同工具间切换或猜测配置。\n:::\n\nNoneBot 生态目前有如下插件项目模板：\n\n- [RF-Tar-Railt/nonebot-plugin-template](https://github.com/RF-Tar-Railt/nonebot-plugin-template)\n\n  此路径使用 **PDM** 项目管理器，符合 PEP 621 标准，自动化程度高。\n\n- [fllesser/nonebot-plugin-template](https://github.com/fllesser/nonebot-plugin-template)\n\n  此路径使用 **uv** 项目管理器和 **PoeThePoet** 任务运行器，构建速度快，适合追求效率的开发者。\n\n- [A-kirami/nonebot-plugin-template](https://github.com/A-kirami/nonebot-plugin-template)\n\n  此路径使用 **Poetry** 项目管理器，适合熟悉传统 Python 生态的开发者。\n\n#### 1. 创建项目\n\n1. 访问上述三个模板之一。\n2. 点击 **“Use this template”** → **“Create a new repository”**。\n3. 仓库名称填写：`nonebot-plugin-{your-plugin-name}`（此部分以 `nonebot-plugin-weather` 为例）。\n4. 点击 **“Create repository from template”**。\n\n#### 2. 配置发布权限\n\n1. 进入新仓库 → **Settings** → **Actions** → **General**。\n2. 在 **Workflow permissions** 下，勾选 **“Read and write permissions”** → 点击 **Save**。\n\n#### 3. 全局替换项目信息\n\n在仓库中点击 **“Add file”** → **“Create new file”**，创建一个空文件 `LICENSE`，选择开源协议并提交（此操作会触发工作流）。\n\n然后在本地克隆仓库，使用编辑器对以下内容进行**全局替换**：\n\n:::tip 提示\n此部分以“天气插件”为例，实际的替换内容应该根据你所创建的插件名称等相应调整。\n:::\n\n| 原内容                         | 替换为                             |\n| ------------------------------ | ---------------------------------- |\n| `nonebot-plugin-template`      | `nonebot-plugin-weather`           |\n| `nonebot_plugin_template`      | `nonebot_plugin_weather`           |\n| `<your_plugin_humanized_name>` | `天气查询`                         |\n| `<your_plugin_description>`    | `查询指定城市的实时天气与未来预报` |\n| `<your_github>`                | `你的GitHub用户名`                 |\n| `<your_email>`                 | `你的邮箱`                         |\n\n#### 4. 安装依赖与开发\n\n<Tabs groupId=\"publish-path\" defaultValue=\"pdm\" values={[\n  {label: 'PDM + RF-Tar-Railt 模板', value: 'pdm'},\n  {label: 'uv + fllesser 模板', value: 'uv'},\n  {label: 'Poetry + A-kirami 模板', value: 'poetry'},\n]}>\n  <TabItem value=\"pdm\" label=\"PDM + RF-Tar-Railt 模板\">\n\n```bash\n# 安装 PDM（若未安装）\ncurl -sSL https://pdm-project.org/install-pdm.py | python3 -\n\n# 安装项目依赖（自动创建虚拟环境）\npdm sync\n\n# 添加新依赖（如 httpx）\npdm add httpx\n```\n\n</TabItem>\n\n  <TabItem value=\"uv\" label=\"uv + fllesser 模板\">\n\n```bash\n# 安装 uv（Windows）\npowershell -c \"irm https://astral.sh/uv/install.ps1 | iex\"\n\n# 安装 uv（macOS/Linux）\ncurl -LsSf https://astral.sh/uv/install.sh | sh\n\n# 安装所有依赖（含 dev）\nuv sync --all-groups -p 3.12\n\n# 添加新依赖\nuv add httpx\n```\n\n</TabItem>\n\n  <TabItem value=\"poetry\" label=\"Poetry + A-kirami 模板\">\n\n```bash\n# 安装 Poetry（推荐方式）\ncurl -sSL https://install.python-poetry.org | python3 -\n\n# 安装项目依赖\npoetry install\n\n# 添加新依赖\npoetry add httpx\n```\n\n</TabItem>\n</Tabs>\n\n#### 5. 更新版本并发布\n\n<Tabs\n  groupId=\"publish-path-bump\"\n  defaultValue=\"bump-my-version\"\n  values={[\n    { label: \"使用 bump-my-version\", value: \"bump-my-version\" },\n    { label: \"使用项目管理器\", value: \"bump-manager\" },\n    { label: \"手动更新版本\", value: \"bump-manual\" },\n  ]}\n>\n  <TabItem value=\"bump-my-version\" label=\"使用 bump-my-version\">\n\n[bump-my-version](https://github.com/callowayproject/bump-my-version) 是一个功能强大、可配置的 Python 项目版本更新工具，支持自动提交到 Git 等 VCS。\n\n<Tabs groupId=\"publish-path\" defaultValue=\"pdm\" values={[\n  {label: 'PDM + RF-Tar-Railt 模板', value: 'pdm'},\n  {label: 'uv + fllesser 模板', value: 'uv'},\n  {label: 'Poetry + A-kirami 模板', value: 'poetry'},\n]}>\n  <TabItem value=\"pdm\" label=\"PDM + RF-Tar-Railt 模板\">\n\n```bash\n# 安装 bump-my-version\npdm add --dev bump-my-version\n\n# 更新 patch 版本\npdm run bump patch\n\n# 推送 tag 触发发布\ngit push origin --tags\n```\n\n</TabItem>\n\n  <TabItem value=\"uv\" label=\"uv + fllesser 模板\">\n\n```bash\n# 更新 patch 版本\nuv run poe bump patch\n\n# 推送 tag 触发发布\ngit push origin --tags\n```\n\n</TabItem>\n\n  <TabItem value=\"poetry\" label=\"Poetry + A-kirami 模板\">\n\n```bash\n# 安装 bump-my-version\npoetry add --dev bump-my-version\n\n# 更新 patch 版本\npoetry run bump patch\n\n# 推送 tag 触发发布\ngit push origin --tags\n```\n\n</TabItem>\n</Tabs>\n\n</TabItem>\n  <TabItem value=\"bump-manager\" label=\"使用项目管理器\">\n\n<Tabs groupId=\"publish-path\" defaultValue=\"pdm\" values={[\n  {label: 'PDM + RF-Tar-Railt 模板', value: 'pdm'},\n  {label: 'uv + fllesser 模板', value: 'uv'},\n  {label: 'Poetry + A-kirami 模板', value: 'poetry'},\n]}>\n  <TabItem value=\"pdm\" label=\"PDM + RF-Tar-Railt 模板\">\n\n需要安装 PDM 插件 [pdm-bump](https://github.com/carstencodes/pdm-bump)。\n\n```bash\n# 安装 pdm-bump\npdm self add pdm-bump\n\n# 更新 patch 版本\npdm bump patch\n\n# 推送 tag 触发发布\ngit push origin --tags\n```\n\n</TabItem>\n\n  <TabItem value=\"uv\" label=\"uv + fllesser 模板\">\n\n```bash\n# 更新 patch 版本\nuv version --bump patch\n\n# 创建相应提交与标签\ngit add pyproject.toml\ngit commit -m \"chore: release v0.1.1\"  # 替换为实际的版本号\ngit tag v0.1.1  # 替换为实际的版本号\n\n# 推送 tag 触发发布\ngit push origin --tags\n```\n\n</TabItem>\n\n  <TabItem value=\"poetry\" label=\"Poetry + A-kirami 模板\">\n\n```bash\n# 更新版本（自动提交并打标签）\npoetry version patch\n\n# 推送 tag 触发发布\ngit push origin --tags\n```\n\n</TabItem>\n</Tabs>\n\n</TabItem>\n  <TabItem value=\"bump-manual\" label=\"手动更新版本\">\n\n手动更新 `pyproject.toml` 中的 `version` 字段，然后推送 tag 触发发布工作流\n\n```bash\ngit add pyproject.toml\ngit commit -m \"chore: release v0.1.1\"  # 替换为实际的版本号\ngit tag v0.1.1  # 替换为实际的版本号\ngit push origin --tags\n```\n\n</TabItem>\n</Tabs>\n\n推送 `v*` 标签后，模板提供的 GitHub Actions 工作流将自动构建并发布到 PyPI。\n\n#### 6. 发布到 [PyPI](https://pypi.org)\n\n<Tabs groupId=\"publish-method\" defaultValue=\"template\" values={[\n  {label: '使用模板的自动发布工作流', value: 'template'},\n  {label: '手动发布', value: 'manual'},\n]}>\n  <TabItem value=\"template\" label=\"使用模板的自动发布工作流\">\n不同模板使用的发布方式可能不同，具体配置流程参考对应模板的详细使用指南。\n</TabItem>\n\n  <TabItem value=\"manual\" label=\"手动发布\">\n根据选用的构建系统，在项目的 `pyproject.toml` 中填入必要信息后进行构建与发布。\n\n:::tip 提示\n不同构建工具的使用可能存在差别。本文仅以 [`pdm`](https://pdm-project.org/zh/latest/),\n[`poetry`](https://python-poetry.org/docs/), [`setuptools`](https://setuptools.pypa.io/en/latest/)\n构建系统**本地构建与发布**为示例讲解，其余构建/管理工具等和自动化构建的用法请读者自行探索。\n:::\n\n<Tabs groupId=\"publishMethod\">\n  <TabItem value=\"poetry\" label=\"Poetry\" default>\n\n```bash\npoetry publish --build  # 构建并发布\n\n# 等效于以下两个命令\npoetry build            # 只构建\npoetry publish          # 只发布先前的构建\n```\n\n  </TabItem>\n\n  <TabItem value=\"pdm\" label=\"PDM\" default>\n\n```bash\npdm publish             # 构建并发布\n\n# 等效于以下两个命令\npdm build               # 只构建\npdm publish --no-build  # 只发布先前的构建\n```\n\n  </TabItem>\n\n  <TabItem value=\"setuptools\" label=\"Setuptools (PEP 517)\" default>\n\n```bash\npip install build twine             # 安装通用构建与发布工具\n\npython -m build --sdist --wheel .   # 只构建\ntwine upload dist/*                 # 只发布先前的构建\n```\n\n  </TabItem>\n</Tabs>\n\n</TabItem>\n</Tabs>\n\n:::tip 提示\n发布前建议自行测试构建包是否可用，避免遗漏代码文件或资源文件等问题。\n:::\n\n## 基本要求\n\n无论你选择哪条路径，以下内容**必须**完成，否则无法通过商店自动检查：\n\n### 能够正确加载\n\n插件包必须能够被 NoneBot 正确加载，在商店审核中会通过 **NoneFlow** 自动化加载测试进行。\n\n#### 依赖其他插件\n\n如果插件依赖其他插件提供的功能，则**必须**在代码中使用 `require()` 来引入该插件，然后才能 `import` 该插件提供的功能。具体细节参阅[跨插件访问](../advanced/requiring.md)。\n\n使用示例如下：\n\n```python title=nonebot_plugin_weather/__init__.py\nfrom nonebot import require\n\nrequire(\"nonebot_plugin_apscheduler\")\n\nfrom nonebot_plugin_apscheduler import scheduler\n```\n\n#### 不能零配置加载的插件\n\n如果插件需要必要配置项才能正常导入，则**必须**在商店提交表单中填写必要的配置项内容。\n\n但一种更好的做法是，**将插件设计为零配置即可加载**（允许缺少必要配置项时插件仍能正常导入，但不执行需要相应配置项的功能），尤其是对于一些必要配置含有敏感信息（如密钥、Token、API Key 等）的插件。这样可以避免在商店提交表单时填写敏感信息的风险。\n\n### 插件元数据\n\n插件包**必须**填写元数据才能通过 **NoneFlow** 自动化检查。\n\n下面是一个示例：\n\n```python title=nonebot_plugin_weather/__init__.py\nfrom nonebot.plugin import PluginMetadata\n\nfrom .config import Config\n\n__plugin_meta__ = PluginMetadata(\n    # 基本信息（必填）\n    name=\"天气查询\",  # 插件名称\n    description=\"查询指定城市的实时天气与未来预报\",  # 插件介绍\n    usage=\"发送【天气 城市名】获取天气信息\",  # 插件用法\n\n    # 发布额外信息\n    type=\"application\",  # 插件分类\n    # 发布必填，当前有效类型有：`library`（为其他插件编写提供功能），`application`（向机器人用户提供功能）。\n\n    homepage=\"https://github.com/你的用户名/nonebot-plugin-weather\",\n    # 发布必填。\n\n    config=Config,\n    # 插件配置项类，如果有配置类则必须填写。\n\n    supported_adapters={\"~onebot.v11\"},\n    # 支持的适配器集合，其中 `~` 在此处代表前缀 `nonebot.adapters.`，其余适配器亦按此格式填写。\n    # 若插件只使用了 NoneBot 基本抽象，应显式填写 None，否则应该列出插件支持的适配器。\n)\n```\n\n:::caution 注意\n`__plugin_meta__` 变量**必须**处于插件最外层（如 `__init__.py` 中），否则无法正常识别。\n\n一般做法是在 `__init__.py` 中定义 `__plugin_meta__`。\n:::\n\n#### 继承其他插件支持的适配器\n\n如果你的插件依赖于其他插件提供的支持功能，而其他插件可能支持更少的适配器，这时就应该使用\n[inherit_supported_adapters()](../api/plugin/load#inherit-supported-adapters) 函数来继承其他插件支持的适配器。\n\n示例用法如下：\n\n```python title=nonebot_plugin_weather/__init__.py\nfrom nonebot import require\nfrom nonebot.plugin import PluginMetadata, inherit_supported_adapters\n\nfrom .config import Config\n\nrequire(\"nonebot_plugin_alconna\")  # 必须先 require 才能被 inherit_supported_adapters 处理\n\n__plugin_meta__ = PluginMetadata(\n    name=\"天气查询\",\n    description=\"查询指定城市的实时天气与未来预报\",\n    usage=\"发送【天气 城市名】获取天气信息\",\n    type=\"application\",\n    homepage=\"https://github.com/你的用户名/nonebot-plugin-weather\",\n    config=Config,\n\n    supported_adapters=inherit_supported_adapters(\"nonebot_plugin_alconna\"),\n    # 继承 nonebot_plugin_alconna 插件的适配器支持列表\n)\n```\n\n### 准备项目主页\n\n通常可以使用 GitHub 项目页面作为项目主页，在 `README.md` 文件中编写插件介绍等内容。\n\n内容大致包括：\n\n- 插件功能介绍；\n- 安装方法\n  - **必须**有 NB-CLI 方式安装\n  - 可选依赖可以给出其他安装方式\n  - **不得**使用旧式的 `bot.py` 配置\n- 插件配置项（如 `Config` 类字段，若无可跳过）\n- 插件设置的触发规则（若无可跳过）\n- 插件的其它用法（按需编写）\n- 效果图、权限说明（按需编写）\n\n## 质量要求\n\n以下内容**强烈建议**完成，否则社区成员将会要求修改：\n\n### 依赖管理原则\n\n- **必须**包含 `nonebot2`。\n- **必须**将插件直接使用的适配器加入依赖列表，如：使用 OneBot 适配器的插件应添加 `nonebot-adapter-onebot` 依赖；\n- **禁止**使用 `==` 锁定单一版本，使用 `>=` 或 `~=`。\n- **禁止**添加 `nonebot`（V1）作为依赖。\n- 所有在代码中 `import` 的第三方库，必须在 `pyproject.toml` 的 `dependencies` 中列出。\n\n### 避免误用同步操作\n\nNoneBot 是一个异步框架，插件中**禁止**使用任何可能阻塞事件循环的同步操作，例如：\n\n- 同步 HTTP 请求（如 `requests` 库）；\n\n  **推荐**操作（以 `httpx` 为例）：\n\n  ```python\n  import httpx\n\n  async with httpx.AsyncClient() as client:\n      response = await client.get(\"https://api.example.com/data\")  # 异步操作，不阻塞机器人\n  ```\n\n  **禁止**操作：\n\n  ```python\n  import requests\n\n  requests.get(\"https://api.example.com/data\")  # 同步操作，会阻塞机器人\n  ```\n\n- 其他可能长时间运行阻塞事件循环的操作。\n\n### 本地文件存储\n\n如果插件需要在本地存储数据、配置或缓存文件，**必须**使用 [`nonebot-plugin-localstore`](https://github.com/nonebot/plugin-localstore) 管理，具体细节参阅[本地存储](../best-practice/data-storing.md)章节。\n\n参考示例：\n\n```python title=nonebot_plugin_weather/__init__.py\nfrom pathlib import Path\nfrom nonebot import require\nrequire(\"nonebot_plugin_localstore\")\n\nimport nonebot_plugin_localstore as store\n\n# 获取插件缓存文件（夹）路径\nweather_cache_dir: Path = store.get_plugin_cache_dir()\nweather_cache_file: Path = store.get_plugin_cache_file(\"cache.json\")\n\n# 获取插件配置文件（夹）路径\nweather_config_dir: Path = store.get_plugin_config_dir()\nweather_config_file: Path = store.get_plugin_config_file(\"config.toml\")\n\n# 获取插件数据文件（夹）路径\nweather_data_dir: Path = store.get_plugin_data_dir()\nweather_data_file: Path = store.get_plugin_data_file(\"resource-index.json\")\n```\n\n## 商店审核\n\n### 提交申请\n\n完成在 PyPI 的插件发布流程后，前往[商店](/store/plugins)页面，切换到插件页签，点击 **发布插件** 按钮。\n\n在弹出的插件信息提交表单内，填入您所要发布的相应插件信息。请注意，如果插件需要必要配置项才能正常导入，请在“插件配置项”中填写必要的内容（请勿填写密钥等敏感信息）。\n\n完成填写后，点击 **发布** 按钮，这将自动跳转 NoneBot 仓库内的“发布插件”页面。确认信息无误后点击页面下方的 `Submit new issue` 按钮进行最终提交即可。\n\n### 等待插件审核\n\n插件发布 Issue 创建后，将会经过 **NoneFlow Bot** 的自动前置检查，以确保插件信息正确无误、插件能被正确加载。\n\n:::tip 提示\n若插件检查未通过或信息有误，**不必**关闭当前 Issue。只需更新插件并上传到 PyPI/修改信息后勾选插件测试勾选框即可重新触发插件检查。\n:::\n\n之后，NoneBot 的维护者和一些志愿者会初步检查插件代码，帮助减少该插件的问题。\n\n完成这些步骤后，您的插件将会被自动合并到[商店](/store/plugins)，而您也将成为 [**NoneBot 贡献者**](https://github.com/nonebot/nonebot2/graphs/contributors)的一员。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/editor-support.md",
    "content": "---\nsidebar_position: 2\ndescription: 配置编辑器以获得最佳体验\n---\n\n# 编辑器支持\n\n框架基于 [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）工具进行类型检查，确保代码可以被编辑器正确解析。\n\n## 编辑器推荐配置\n\n### Visual Studio Code\n\n在 Visual Studio Code 中，可以使用 Pylance Language Server 并启用 `Type Checking` 配置以达到最佳开发体验。\n\n1. 在 VSCode 插件视图搜索并安装 `Python (ms-python.python)` 和 `Pylance (ms-python.vscode-pylance)` 插件。\n2. 修改 VSCode 配置\n   在 VSCode 设置视图搜索配置项 `Python: Language Server` 并将其值设置为 `Pylance`，搜索配置项 `Python > Analysis: Type Checking Mode` 并将其值设置为 `basic`。\n\n   或者向项目 `.vscode` 文件夹中配置文件添加以下内容：\n\n   ```json title=settings.json\n   {\n     \"python.languageServer\": \"Pylance\",\n     \"python.analysis.typeCheckingMode\": \"basic\"\n   }\n   ```\n\n### 其他\n\n欢迎提交 Pull Request 添加其他编辑器配置推荐。点击左下角 `Edit this page` 前往编辑。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/ospp/2021.md",
    "content": "---\nsidebar_position: 0\ndescription: 开源软件供应链点亮计划 - 暑期 2021\nmdx:\n  format: md\n---\n\n# 暑期 2021\n\n**开源软件供应链点亮计划 - 暑期 2021** 是**中国科学院软件研究所**与 **openEuler 社区**共同举办的一项面向高校学生的暑期活动，旨在鼓励在校学生积极参与开源软件的开发维护，促进优秀开源软件社区的蓬勃发展。关于具体的活动规划、报名方式，请查看该活动的 [官网](https://summer.iscas.ac.cn/) 和 [帮助文档](https://summer.iscas.ac.cn/help/)。\n\nNoneBot 社区有幸作为开源社区参与了本次活动，下面列出了目前我们已经发布的项目，欢迎感兴趣的同学在上面给出的活动官网报名，或通过 [contact@nonebot.dev](mailto:contact@nonebot.dev) 联系我们。\n\n## NoneBot v1\n\n### 更新 NoneBot v1 文档中的“指南”部分\n\n由于 NoneBot v1 和 aiocqhttp 最初基于的 QQ 机器人平台不再提供服务，CQHTTP 接口也转型且改名为 OneBot 标准，目前 NoneBot v1 文档的“指南”部分和 aiocqhttp 文档有部分过时内容需要更新。我们希望将其中与旧的机器人平台相关的内容改为基于 go-cqhttp 或通用的 OneBot 表述，同时对 NoneBot v1 的 awesome-bot 示例做一次全面检查，修改其中可能已经不可用的部分。\n\n**难度**：低\n\n**导师**：[@cleoold](https://github.com/cleoold)\n\n**产出要求**\n\n- 修改“指南”文档和 aiocqhttp 文档中与旧的 QQ 机器人平台相关的部分\n- 检查 awesome-bot 示例是否有已经过时/不可用的地方，并更新/修复\n- 修改“图灵机器人”案例，使用其它 AI 聊天 API 提供商（需先做简单调研）\n\n**技术要求**\n\n- 熟悉 Python 编程语言及 asyncio 机制\n- 了解 Git 基本用法\n- 了解聊天机器人基本开发过程\n- 了解 VuePress 更佳\n\n### NoneBot v1 API 文档自动生成\n\n目前 NoneBot v1 的文档中“API”部分是手动编写的，在更新代码接口的同时需要手动更新文档，可能造成文档与代码不匹配，形成额外的维护成本。我们希望将 API 文档改为直接编写在 Python docstring 中，通过工具自动生成 API 文档。\n\n**难度**：中\n\n**导师**：[@cleoold](https://github.com/cleoold)\n\n**产出要求**\n\n- 调研市面上常见的 Python API 文档生成工具\n- 在代码中补充 API 文档\n- 编写或应用开源工具自动生成 API 文档\n- 配置 GitHub Actions 或其它 CI 自动化构建和部署 API 文档\n\n**技术要求**\n\n- 熟悉 Python 编程语言及 asyncio 和 Type Hints\n- 了解 Git 基本用法\n- 了解 Sphinx 等文档生成工具更佳\n- 了解 GitHub Actions 等 CI 工具更佳\n\n## NoneBot v2\n\n### NoneBot v2 自动化测试框架“NoneBug”\n\n在聊天机器人的开发过程中，一套自动化的测试机制是非常重要的，特别是对于 NoneBot 2 这类为大型机器人开发而设计的项目来说，需要手动测试每一个边际条件是非常痛苦的。我们希望能够开发一款基于 NoneBot 2 插件机制的自动化测试框架，为 NoneBot 2 用户提供一套易用便捷、高度灵活的自动化测试框架。\n\n**难度**：高\n\n**导师**：[@yanyongyu](https://github.com/yanyongyu)\n\n**产出要求**\n\n- 调研现有的 Python 和其它语言集成测试框架\n- 设计 NoneBug 的用户 API 和实现方式\n- 实现 NoneBug 自动化测试框架\n- 编写详细的使用文档\n\n**技术要求**\n\n- 熟悉 Python 编程语言及 asyncio 和 Type Hints\n- 了解 Git 基本用法\n- 了解 NoneBot v2 的基本原理和使用方式\n- 了解主流的 Python 自动化测试框架\n\n### NoneBot v2 Telegram 适配器\n\n目前 NoneBot v2 已支持 OneBot、Mirai HTTP API、钉钉协议，社区反馈有更多的平台需求，希望能在 NoneBot v2 获得更多的跨平台支持，提高机器人的便携性。同时，我们也希望随着新平台加入，提升现有 NoneBot v2 核心代码的平台通用性。Telegram 是一款较为广泛使用的安全即时聊天软件，同时其官方提供了丰富的聊天机器人 API，因此我们希望为 NoneBot v2 编写一个 Telegram 适配器来支持 Telegram 机器人的开发。\n\n**难度**：中\n\n**导师**：[@yanyongyu](https://github.com/yanyongyu)\n\n**产出要求**\n\n- 调研 Telegram Bot API 以及 WebHook 等官方接口\n- 编写 Telegram 适配器并能够使用\n- 代码遵守项目 Contributing 规范\n\n**技术要求**\n\n- 熟悉 Python 编程语言及 asyncio 和 Type Hints\n- 了解 Git 基本用法\n- 了解 Web 开发相关知识\n- 了解 Sphinx 等文档生成工具更佳\n\n### NoneBot v2 飞书适配器\n\n目前 NoneBot v2 已支持 OneBot、Mirai HTTP API、钉钉协议，社区反馈有更多的平台需求，希望能在 NoneBot v2 获得更多的跨平台支持，提高机器人的便携性。同时，我们也希望随着新平台加入，提升现有 NoneBot v2 核心代码的平台通用性。飞书是目前企业用户广泛使用的即时聊天和协作软件，其官方提供了丰富的聊天机器人 API，因此我们希望为 NoneBot v2 编写一个飞书适配器来支持飞书机器人的开发。\n\n**难度**：中\n\n**导师**：[@yanyongyu](https://github.com/yanyongyu)\n\n**产出要求**\n\n- 调研飞书机器人 API 以及 WebHook 等官方接口\n- 编写飞书适配器并能够使用\n- 代码遵守项目 Contributing 规范\n\n**技术要求**\n\n- 熟悉 Python 编程语言及 asyncio 和 Type Hints\n- 了解 Git 基本用法\n- 了解 Web 开发相关知识\n- 了解 Sphinx 等文档生成工具更佳\n\n## OneBot\n\n### 设计 OneBot v12 接口标准\n\n目前的 OneBot 标准的 v11 版本仍然与 QQ 平台有较多耦合，我们希望在 v12 去掉与 QQ 耦合的历史包袱，形成一个通用的、可扩展的、易于使用的同时易于实现的聊天机器人接口标准。\n\n**难度**：中\n\n**导师**：[@richardchien](https://github.com/richardchien)\n\n**产出要求**\n\n- 调研各聊天机器人平台的官方/非官方接口特点\n- 通用化 OneBot 核心 API，分离 QQ 特定的 API，去掉无用 API\n- 优化现有的通信、消息表示机制\n- 补充 QQ 特定的缺失 API\n- 文档需符合风格指南\n\n**技术要求**\n\n- 熟悉至少两个聊天平台的聊天机器人开发\n- 了解 Git 基本用法\n- 了解使用不同语言编写聊天机器人时的常用实践\n- 对文档的优雅性与美观性有追求更佳\n\n### 实现 Rust 版 libonebot\n\n目前最常用的 OneBot 实现包括 go-cqhttp、onebot-kotlin、node-onebot 等，这些实现都各自重复实现了 Web 通信、消息解析、配置读写等功能，当社区中的开发者想针对一个新的聊天平台实现 OneBot 时，他们往往同样需要再次实现类似逻辑。我们希望使用 Rust 编写一个 libonebot 模块，该模块实现所有 OneBot 实现所共享的功能，从而方便其他开发者们使用 Rust 快速编写具体的 OneBot 实现。同时，我们希望借此项目在聊天机器人社区中推广 Rust 编程语言。\n\n> 注：这里的逻辑是 libonebot + 针对某聊天平台的对接代码 = 某聊天平台的 OneBot 实现，libonebot 要做的是让 OneBot 实现的开发者只需编写针对特定聊天平台的对接代码，而无需关心 OneBot 标准定义的通信方式、消息格式等。\n\n**难度**：高\n\n**导师**：[@richardchien](https://github.com/richardchien)\n\n**产出要求**\n\n- 实现所有 OneBot 实现所共享的功能，包括 Web 通信、消息解析、配置读写等\n- 充分考虑同时兼容 OneBot v11 和 v12 接口\n- 能够根据用户（OneBot 实现的开发者）所实现的接口自动实现类似 get_available_apis 等接口\n- 编写详细的使用文档\n- 如果可能，与 v12 设计项目联动，实现第一手 v12 支持\n\n**技术要求**\n\n- 熟悉聊天机器人开发\n- 熟悉 Rust Web 开发\n\n### 实现自选语言版 libonebot\n\n目前最常用的 OneBot 实现包括 go-cqhttp、onebot-kotlin、node-onebot 等，这些实现都各自重复实现了 Web 通信、消息解析、配置读写等功能，当社区中的开发者想针对一个新的聊天平台实现 OneBot 时，他们往往同样需要再次实现类似逻辑。我们希望使用 Python、Go、Kotlin、Node、PHP、C#.NET 等主流语言（任选一个）编写 libonebot 模块，该模块实现所有 OneBot 实现所共享的功能，从而方便其他开发者们使用对应语言快速编写具体的 OneBot 实现。\n\n> 注：这里的逻辑是 libonebot + 针对某聊天平台的对接代码 = 某聊天平台的 OneBot 实现，libonebot 要做的是让 OneBot 实现的开发者只需编写针对特定聊天平台的对接代码，而无需关心 OneBot 标准定义的通信方式、消息格式等。\n\n**难度**：中\n\n**导师**：[@richardchien](https://github.com/richardchien)\n\n**产出要求**\n\n- 实现所有 OneBot 实现所共享的功能，包括 Web 通信、消息解析、配置读写等\n- 充分考虑同时兼容 OneBot v11 和 v12 接口\n- 编写详细的使用文档\n- 如果可能，实现更多附加特性，如根据用户（OneBot 实现的开发者）所实现的接口自动实现类似 get_available_apis 等接口、实现第一手 v12 支持等\n\n**技术要求**\n\n- 熟悉聊天机器人开发\n- 熟悉所选语言的 Web 开发\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/ospp/2022.md",
    "content": "---\nsidebar_position: 1\ndescription: 开源之夏 - 暑期 2022\nmdx:\n  format: md\n---\n\n# 暑期 2022\n\n**开源之夏 - 暑期 2022** 是由**开源软件供应链点亮计划**发起、由**中国科学院软件研究所**与 **openEuler 社区**主办的一项面向高校学生的暑期活动，类似 Google Summer of Code（GSoC），旨在鼓励在校学生积极参与开源软件的开发维护，促进优秀开源软件社区的蓬勃发展。关于具体的活动规划、报名方式，请查看该活动的 [官网](https://summer-ospp.ac.cn/) 和 [帮助文档](https://summer-ospp.ac.cn/help/)。\n\nNoneBot 社区有幸作为开源社区 [参与](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) 联系我们。\n\n## NoneBot2 命令行 CLI 交互体验升级\n\nNoneBot2 为用户提供了命令行脚手架 ──`nb-cli`，辅助用户更好地上手项目以及进行开发。nb-cli 主要包括：创建项目、运行项目、安装与卸载插件、部署项目等功能。随着 NoneBot2 Beta 版本的发布，脚手架功能存在一定的定位不明确、功能体验不佳。本项目旨在重新设计 nb-cli 功能框架，完善功能，优化用户体验。\n\n**难度**：进阶\n\n**导师**：[@yanyongyu](https://github.com/yanyongyu)\n\n**产出要求**\n\n- 设计 nb-cli 功能框架\n  - 明确各功能模块\n  - 设计用户交互模式\n- 完成 nb-cli 主要功能代码\n  - 项目管理\n  - 插件管理\n  - 其它\n- 同步更新使用文档\n\n**技术要求**\n\n- 熟悉 Python 命令行交互代码编写\n- 熟悉 NoneBot2 框架功能\n- 熟悉 NoneBot2 项目组织方式\n\n**成果仓库**\n\n- <https://github.com/nonebot/nb-cli>\n- <https://github.com/nonebot/nonebot2>\n\n## NoneBot2 命令行即时交互通信设计与实现\n\nNoneBot2 在早期提供了基于网页的 nonebot-plugin-test 插件，无需平台适配接入即可对机器人进行测试，方便了开发者直观的感受机器人文本交互功能。我们希望提供一款基于命令行的适配器/驱动器，用于无平台适配接入、可以运行机器人的场景进行功能体验或测试。\n\n**难度**：进阶\n\n**导师**：[@mnixry](https://github.com/mnixry)\n\n**产出要求**\n\n- 设计命令行与 NoneBot2 通信模式\n  - 直接调用/HTTP/WebSocket\n- 设计命令行交互界面\n- 实现相应适配器/驱动器\n- 同步更新使用说明文档\n\n**技术要求**\n\n- 熟悉 Python 命令行交互代码编写\n- 熟悉 NoneBot2 框架功能\n- 熟悉 NoneBot2 项目组织方式\n\n**成果仓库**\n\n- <https://github.com/nonebot/adapter-console>\n\n## NoneBot2 用户上手与深入教程设计\n\nNoneBot2 为用户提供了详细的文档介绍，辅助用户更好的上手项目以及进行开发。文档分为基础与进阶两个部分。基础部分帮助新用户快速上手开发，主要包括：安装 NoneBot2、使用脚手架、创建配置项目、使用适配器、加载插件、定义消息事件、处理消息事件、调用平台 API 等。进阶部分向已经熟悉开发流程的用户介绍更多高级技巧，主要包括：NoneBot2 工作原理、定时任务、权限控制、钩子函数、跨插件访问、单元测试、发布插件等。目前文档对于用户而言过于费解，导致用户难以理解 NoneBot2 开发。本项目旨在优化文档内容，使其更加通俗易懂，不让文档成为用户上手的阻碍，同时完善进阶内容，让有更复杂需求的用户，同样能从文档中受益。\n\n相关 issue：\n\n- <https://github.com/nonebot/nonebot2/issues/793>\n- <https://github.com/nonebot/nonebot2/issues/295>\n\n**难度**：进阶\n\n**导师**：[@SK-415](https://github.com/SK-415)\n\n**产出要求**\n\n- 文档通俗易懂\n  - 附有适当的图片指引（如 asciinema）\n- 内容完整，由浅入深\n- 适当的界面美化，合理分配布局\n\n**技术要求**\n\n- 熟悉文档结构组织与语言表达\n- 熟悉 NoneBot2 框架功能\n- 熟悉 NoneBot2 项目组织方式\n\n**成果仓库**\n\n- <https://github.com/nonebot/nonebot2>\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/ospp/2023.md",
    "content": "---\nsidebar_position: 2\ndescription: 开源之夏 - 暑期 2023\nmdx:\n  format: md\n---\n\n# 暑期 2023\n\n**开源之夏 - 暑期 2023** 是由**开源软件供应链点亮计划**发起、由**中国科学院软件研究所**与 **openEuler 社区**主办的一项面向高校学生的暑期活动，类似 Google Summer of Code（GSoC），旨在鼓励在校学生积极参与开源软件的开发维护，促进优秀开源软件社区的蓬勃发展。关于具体的活动规划、报名方式，请查看该活动的 [官网](https://summer-ospp.ac.cn/) 和 [帮助文档](https://summer-ospp.ac.cn/help/)。\n\nNoneBot 社区有幸作为开源社区 [参与](https://summer-ospp.ac.cn/org/orgdetail/e1fb5b8d-125a-4138-b756-25bd32c0a31a?lang=zh) 了本次活动，下面列出了目前我们已经发布的项目，欢迎感兴趣的同学通过 [contact@nonebot.dev](mailto:contact@nonebot.dev) 联系我们。\n\n## NoneBot 项目管理图形化面板\n\nNoneBot 目前提供了开箱即用的命令行脚手架来帮助初次使用的用户更快的上手编写应用。但是，对于未有一定开发经验的用户，命令行的使用仍具有一定的困难。此外，其他项目如 koishi、vue 等，均可通过图形化界面的形式为用户提供更便捷的项目开发。因此，我们希望借助现有命令行脚手架的可扩展特性，提供一个项目管理面板服务，以网页的形式帮助用户开发 NoneBot 应用。\n\n**难度**：进阶\n\n**导师**：[@mnixry](https://github.com/mnixry)\n\n**产出要求**\n\n- 设计并实现项目管理面板相关功能\n  - 创建与管理项目\n  - 配置与运行项目\n  - NoneBot 插件管理\n- 实现相应 nb-cli 插件提供面板服务\n- 代码符合 NoneBot Contributing 规范\n\n**技术要求**\n\n- 熟悉 nb-cli 相关功能\n- 熟悉 NoneBot 框架功能\n- 熟悉前后端相关实现方式\n\n**成果仓库**\n\n- <https://github.com/nonebot/cli-plugin-webui>\n\n## NoneBot Discord 适配器\n\nNoneBot 作为一个跨平台聊天机器人框架，目前已有 OneBot、飞书、Telegram、QQ 频道等诸多平台的适配支持。作为众多用户期待的平台适配之一，我们希望借此机会接入 Discord 聊天机器人。\n\n**难度**：进阶\n\n**导师**：[@iyume](https://github.com/iyume)\n\n**产出要求**\n\n- 调研 Discord Bot 相关功能与接口\n- 设计与编写 NoneBot Discord 适配器\n- 代码符合 NoneBot Contributing 规范\n\n**技术要求**\n\n- 熟悉 NoneBot 框架功能\n- 熟悉 NoneBot 各模块职责与适配器编写\n\n**成果仓库**\n\n- <https://github.com/nonebot/adapter-discord>\n\n## NoneBot 数据库支持插件\n\nNoneBot 的插件系统为用户实现应用提供了极高的便捷性，但因此也增加了插件统一管理的难度。目前，我们发现许多用户发布的插件中存在文件存储结构化数据、数据存放散乱等现象，同时插件间也可能产生冲突。因此，我们希望提供一个统一的数据存储与管理方式，便于用户读写应用数据。\n\n**难度**：进阶\n\n**导师**：[@yanyongyu](https://github.com/yanyongyu)\n\n**产出要求**\n\n- 设计并实现 ORM 插件\n  - 提供关系模型定义功能\n  - 提供模型迁移与管理功能\n  - 能较好的支持 Python 类型检查与推导\n- 编写相应的用户使用文档\n- 代码符合 NoneBot Contributing 规范\n\n**技术要求**\n\n- 熟悉 NoneBot 框架功能与插件编写\n- 熟悉 SQLAlchemy 等 ORM 框架\n  - 熟悉 SQLAlchemy ORM\n  - 熟悉 alembic 等迁移工具\n- 熟悉 nb-cli 插件编写\n\n**成果仓库**\n\n- <https://github.com/nonebot/plugin-orm>\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/ospp/2024.md",
    "content": "---\nsidebar_position: 3\ndescription: 开源之夏 - 暑期 2024\nmdx:\n  format: md\n---\n\n# 暑期 2024\n\n**开源之夏 - 暑期 2024** 是**中国科学院软件研究所**发起的**开源软件供应链点亮计划**系列暑期活动，旨在鼓励高校学生积极参与开源软件的开发维护，促进优秀开源软件社区的蓬勃发展。活动联合各大开源社区，针对重要开源软件的开发与维护提供项目开发任务，并向全球高校学生开放报名。关于具体的活动规划、报名方式，请查看该活动的 [官网](https://summer-ospp.ac.cn/) 和 [帮助文档](https://summer-ospp.ac.cn/help/)。\n\nNoneBot 社区有幸作为开源社区 [参与](https://summer-ospp.ac.cn/org/orgdetail/e1fb5b8d-125a-4138-b756-25bd32c0a31a?lang=zh) 了本次活动，下面列出了目前我们已经发布的项目，欢迎感兴趣的同学通过 [contact@nonebot.dev](mailto:contact@nonebot.dev) 联系我们。\n\n## NonePress 官网组件库更新与优化\n\nNoneBot 官网目前采用基于 TailwindCSS 自研的 NonePress 组件库及 Docusaurus 框架进行构建。由于相关依赖版本迭代迅速，目前官网组件库已产生了较大的版本落后。本项目希望在跟进框架新版本的基础上，对文档整体视觉体验进行重新设计，提升页面的无障碍访问性，基于 React Hydrate 特性实现完整的静态网站生成（SSG）以提升搜索引擎优化（SEO）水平。在解决以上问题的基础上，可对网页的开发以及生产构建性能做相应的优化提升，例如在生产构建使用自有的 webpack loader、替换现有的热重载逻辑以减少开发环境启动耗时等。\n\n**难度**：进阶\n\n**导师**：[@yanyongyu](https://github.com/yanyongyu)\n\n**产出要求**\n\n- 基于 Docusaurus v3 重构 NonePress 组件库及相关插件\n  - 升级相关依赖并重新打造 Docusaurus theme（布局与组件）\n  - 根据需求实现/修改 Docusaurus 插件使得官网内容构建正常\n  - 能够提升页面渲染性能与 MDX 相关能力\n- 升级官网采用新版组件库\n  - Algolia 索引与 SEO 正常\n  - 桌面端与移动端显示正常\n  - 优化官网开发与生产构建体验\n- （可选）优化官网部分页面\n  - 优化官网过长的 changelog\n  - 优化官网插件商店的展示细节\n\n**技术要求**\n\n- 熟练掌握 TS、PostCSS、TSX、MDX等相关技术\n- 掌握 React、Docusaurus、tailwind css 等框架\n- 熟悉静态网站生成 SSG、SEO 优化与 Algolia 索引原理等\n\n**成果仓库**\n\n- <https://github.com/nonebot/docusaurus-theme-nonepress>\n\n## NoneFlow 社区自动化工作流管理优化\n\nNoneFlow 在 NoneBot 社区中承担着重要的角色，它由 NoneBot 框架基于 GitHub APP 编写而成，能够自动化的完成许多复杂流程的处理，如：用户请求提交插件到商店时进行自动化检测，并在人工审核通过后自动存储至 registry；定时自动更新 registry 内插件信息，跟进插件新版本情况等。但是，在长期的使用中发现了一些问题和不足的地方，例如：项目本身结构复杂耦合，添加新自动化流程与维护现有流程困难；目前采用了 GitHub 用户名作为插件作者名，但已有不少插件作者改名；插件存储至 registry 并定时更新，缺少统计相关信息以帮助商店更好的展示当前插件状态；插件作者想要修改插件信息时无法便捷的找到操作方式等。本项目希望针对以上问题与不足的地方进行修复与优化，提升用户体验。\n\n**难度**：进阶\n\n**导师**：[@uy/sun](https://github.com/he0119)\n\n**产出要求**\n\n- 重构现有工作流处理结构\n  - 整合现有 Issue、Pull Request、Git 相关操作\n  - 提供用户修改信息的处理方式\n  - 正确处理 PR 的 Open、Close、Draft 状态\n- 修复流程中存在的问题\n  - 插件作者名正确展示\n  - registry 定时更新中需要插件测试环境隔离\n- 在 registry 定时更新的同时提供统计数据\n\n**技术要求**\n\n- 掌握 GitHub APP 开发\n  - 熟悉 GitHub REST API、GraphQL 等\n  - 熟悉 GitHub APP 权限限制\n- 熟悉 NoneBot 框架与 Python 相关技术\n- 熟悉 Git、GitHub Action、GitHub 工作流\n\n**成果仓库**\n\n- <https://github.com/nonebot/noneflow>\n\n## NoneBlockly 低代码框架开发\n\n经过深入分析社区反馈，我们发现部分新手因不熟悉编程概念或框架本身而遇到问题。为了解决初学者在使用面向开发者的聊天机器人框架 NoneBot 时遇到的挑战，我们计划引入 Blockly 提供低代码编程支持。通过减少常见的编码错误和降低入门门槛，使框架对初学者更加友好，从而提升用户体验并有助于 NoneBot 生态的成长。本项目将基于 Blockly 实现 NoneBot 插件的低代码编写，使得用户能够快速搭建聊天机器人。\n\n**难度**：进阶\n\n**导师**：[@mnixry](https://github.com/mnixry)\n\n**产出要求**\n\n- 实现 NoneBlockly 低代码开发框架\n  - 能够基于 Alconna 编写跨平台插件\n  - 确保插件对 Python 和 NoneBot 版本的兼容性\n  - 支持对多种类型 NoneBot 事件的响应\n  - 支持对 NoneBot 消息对象的便捷操作\n  - 集成 localstore 文件存储、apscheduler 定时任务、网络请求等常用功能\n- 对接 NB-CLI 脚手架，通过脚手架扩展使用低代码框架\n\n**技术要求**\n\n- 掌握 Python 与 NoneBot 框架的使用\n  - 熟悉 NoneBot 插件的开发，包括事件响应与消息处理等\n  - 熟悉 NoneBot 生态组件（Alconna、localstore、apscheduler等）的使用\n  - 了解 NB-CLI 脚手架的扩展开发\n- 熟悉 Blockly 低代码框架的使用和开发\n\n**成果仓库**\n\n- <https://github.com/nonebot/noneblockly>\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/ospp/2025.md",
    "content": "---\nsidebar_position: 4\ndescription: 开源之夏 - 暑期 2025\nmdx:\n  format: md\n---\n\n# 暑期 2025\n\n**开源之夏 - 暑期 2025** 是**中国科学院软件研究所**发起的**开源软件供应链点亮计划**系列暑期活动，旨在鼓励高校学生积极参与开源软件的开发维护，培养和发掘更多优秀的开发者，促进优秀开源软件社区的蓬勃发展，助力开源软件供应链建设。活动联合各大开源社区，针对重要开源软件的开发与维护提供项目开发任务，并向全球高校学生开放报名。关于具体的活动规划、报名方式，请查看该活动的 [官网](https://summer-ospp.ac.cn/) 和 [帮助文档](https://summer-ospp.ac.cn/help/)。\n\nNoneBot 社区有幸作为开源社区 [参与](https://summer-ospp.ac.cn/org/orgdetail/e1fb5b8d-125a-4138-b756-25bd32c0a31a?lang=zh) 了本次活动，下面列出了目前我们已经发布的项目，欢迎感兴趣的同学通过 [contact@nonebot.dev](mailto:contact@nonebot.dev) 联系我们。\n\n## NoneBot HTML 图片渲染插件\n\n文字与图片一直是聊天机器人的两大主流交互方式，而图片的渲染一直是用户开发应用的一大痛点。常见的方式包括 PIL 图片编辑、浏览器渲染 HTML 截图等。PIL 图片编辑依赖人工构建图片布局，容易出现自适应问题，且提升图片特效、美观程度需要极大的开发成本。浏览器渲染方案通过 HTML 与 CSS 能够轻松完成美观自适应能力强的布局，但其部署门槛较高，难以支撑较大规模调用量。而其他轻量化渲染引擎通常不具有完整 HTML/CSS 现代化标准实现，且未提供 Python Binding 直接使用。\n\n本项目希望调研并实现一种高效、便捷的图片渲染方案。该方案需要在保障跨平台一致性、最大程度保证 HTML 与 CSS 现代化标准的前提下，低成本（资源消耗与吞吐量）将 HTML 渲染为对应图片。\n\n**难度**：进阶\n\n**导师**：[@MelodyKnit](https://github.com/MelodyKnit)\n\n**产出要求**\n\n- 调研 HTML/CSS 渲染引擎\n  - 调研 litehtml 等渲染引擎 标准支持能力与兼容性\n- 基于渲染引擎实现 HTML 图片渲染插件\n  - 将渲染引擎通过 binding 等方式集成为 Python 模块\n  - 基于集成模块实现 HTML 图片渲染能力\n  - 编写插件使用文档\n\n**技术要求**\n\n- 掌握 Python 及其异步编程\n- 熟悉 NoneBot 框架及其插件编写\n- 了解浏览器与 HTML 渲染原理\n\n**成果仓库**\n\n- <https://github.com/nonebot/plugin-htmlkit>\n\n## NB-CLI 命令行工具交互优化\n\nNB-CLI 作为 NoneBot 生态的核心入门与管理工具，主要负责新手引导项目创建、项目运行以及插件管理几大功能。目前该脚手架工具仍存在几点缺陷：\n\n- 作为插件管理工具，由于存储数据的局限性，无法很好地展示用户项目当前安装插件状态，并进行卸载等操作；\n- 当前插件管理高度依赖云端 registry 提供插件信息，在离线情况下完全无法使用；\n- 由于插件信息繁多，工具未能向用户展示充分的信息，交互复杂 体验较差。\n\n以上问题对用户使用 NB-CLI 管理项目插件造成了极大的阻碍。\n本项目希望重点针对插件管理部分，重构工具插件管理模块，完善框架缺陷，并通过缓存等方式确保可用性。其次，调研同类工具方案与 TUI 等相关技术，优化信息展示能力、用户交互方式，提升工具整体交互体验。\n\n**相关链接**\n\n- https://github.com/nonebot/nb-cli/issues/138\n- https://github.com/nonebot/nb-cli/issues/140\n\n**难度**：基础\n\n**导师**：[@yanyongyu](https://github.com/yanyongyu)\n\n**产出要求**\n\n- 重构 NB-CLI 插件管理模块\n  - 优化项目插件信息存储方式，支持列出、卸载插件等操作\n  - 通过缓存 registry 数据等方式确保离线场景的可用性\n- 提升 NB-CLI 交互体验\n  - 调研同类工具方案与 TUI 等相关技术\n  - 优化 registry 多字段信息展示能力\n  - 基于 TUI 等技术优化用户交互方式，提升整体交互体验\n\n**技术要求**\n\n- 熟练掌握 Python 及其异步编程\n- 熟悉 NoneBot 框架与 NB-CLI 使用方法\n- 了解 TUI 等终端交互技术\n\n**成果仓库**\n\n- <https://github.com/nonebot/nb-cli>\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/quick-start.mdx",
    "content": "---\nsidebar_position: 1\ndescription: 尝试使用 NoneBot\n\noptions:\n  menu:\n    - category: tutorial\n      weight: 10\n---\n\nimport Asciinema from \"@site/src/components/Asciinema\";\nimport Messenger from \"@site/src/components/Messenger\";\n\n# 快速上手\n\n:::caution 前提条件\n\n- 请确保你的 Python 版本 >= 3.9\n- **我们强烈建议使用虚拟环境进行开发**，如果没有使用虚拟环境，请确保已经卸载可能存在的 NoneBot v1！！！\n  ```bash\n  pip uninstall nonebot\n  ```\n\n:::\n\n在本章节中，我们将介绍如何使用脚手架来创建一个 NoneBot 简易项目。项目将基于 nb-cli 脚手架运行，并允许我们从商店安装插件。\n\n<Asciinema\n  url=\"https://asciinema.org/a/569440.cast\"\n  options={{ theme: \"monokai\", poster: \"npt:21.5\" }}\n/>\n\n## 安装脚手架\n\n确保你已经安装了 Python 3.9 及以上版本，然后在命令行中执行以下命令：\n\n1. 安装 [pipx](https://pypa.github.io/pipx/)\n\n   ```bash\n   python -m pip install --user pipx\n   python -m pipx ensurepath\n   ```\n\n   如果在此步骤的输出中出现了“open a new terminal”或者“re-login”字样，那么请关闭当前终端并重新打开一个新的终端。\n\n2. 安装脚手架\n\n   ```bash\n   pipx install nb-cli\n   ```\n\n安装完成后，你可以在命令行使用 `nb` 命令来使用脚手架。如果出现无法找到命令的情况（例如出现“Command not found”字样），请参考 [pipx 文档](https://pypa.github.io/pipx/) 检查你的环境变量。\n\n## 创建项目\n\n使用脚手架来创建一个项目：\n\n```bash\nnb create\n```\n\n这一指令将会执行创建项目的流程，你将会看到一些询问：\n\n1. 项目模板\n\n   ```bash\n   [?] 选择一个要使用的模板: bootstrap (初学者或用户)\n   ```\n\n   这里我们选择 `bootstrap` 模板，它是一个简单的项目模板，能够安装商店插件。如果你需要**自行编写插件**，这里请选择 `simple` 模板。\n\n2. 项目名称\n\n   ```bash\n   [?] 项目名称: awesome-bot\n   ```\n\n   这里我们以 `awesome-bot` 为例，作为项目名称。你可以根据自己的需要来命名。\n\n3. 其他选项\n   请注意，多选项使用**空格**选中或取消，**回车**确认。\n\n   ```bash\n   [?] 要使用哪些驱动器? FastAPI (FastAPI 驱动器)\n   [?] 要使用哪些适配器? Console (基于终端的交互式适配器)\n   [?] 立即安装依赖? (Y/n) Yes\n   [?] 创建虚拟环境? (Y/n) Yes\n   ```\n\n   这里我们选择了创建虚拟环境，nb-cli 在之后的操作中将会自动使用这个虚拟环境。如果你不需要自动创建虚拟环境或者已经创建了其他虚拟环境，nb-cli 将会安装依赖至当前激活的 Python 虚拟环境。\n\n4. 选择内置插件\n\n   ```bash\n   [?] 要使用哪些内置插件? echo\n   ```\n\n   这里我们选择 `echo` 插件作为示例。这是一个简单的复读回显插件，可以用于测试你的机器人是否正常运行。\n\n## 运行项目\n\n在项目创建完成后，你可以在**项目目录**中使用以下命令来运行项目：\n\n```bash\nnb run\n```\n\n你现在应该已经运行起来了你的第一个 NoneBot 项目了！请注意，生成的项目中使用了 `FastAPI` 驱动器和 `Console` 适配器，你之后可以自行修改配置或安装其他适配器。\n\n## 尝试使用\n\n在项目运行起来后，`Console` 适配器会在你的终端启动交互模式，你可以直接在输入框中输入 `/echo hello world` 来测试你的机器人是否正常运行。\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/echo hello world\" },\n    { position: \"left\", msg: \"hello world\" },\n  ]}\n/>\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/tutorial/application.md",
    "content": "---\nsidebar_position: 0\ndescription: 创建一个 NoneBot 项目\n\noptions:\n  menu:\n    - category: tutorial\n      weight: 20\n---\n\n# 手动创建项目\n\n在[快速上手](../quick-start.mdx)中，我们已经介绍了如何安装和使用 `nb-cli` 创建一个项目。在本章节中，我们将简要介绍如何在不使用 `nb-cli` 的方式创建一个机器人项目的**最小实例**并启动。如果你想要了解 NoneBot 的启动流程，也可以阅读本章节。\n\n:::caution 警告\n我们十分不推荐直接创建机器人项目，请优先考虑使用 nb-cli 进行项目创建。\n:::\n\n一个机器人项目的**最小实例**中**至少**需要包含以下内容:\n\n- 入口文件：初始化并运行机器人的 Python 文件\n- 配置文件：存储机器人启动所需的配置\n- 插件：为机器人提供具体的功能\n\n下面我们创建一个项目文件夹，来存放项目所需文件，以下步骤均在该文件夹中进行。\n\n## 安装依赖\n\n在创建项目前，我们首先需要将项目所需依赖安装至环境中。\n\n1. （可选）创建虚拟环境，以 venv 为例\n\n   ```bash\n   python -m venv .venv --prompt nonebot2\n   # windows\n   .venv\\Scripts\\activate\n   # linux/macOS\n   source .venv/bin/activate\n   ```\n\n2. 安装 nonebot2 以及驱动器\n\n   ```bash\n   pip install 'nonebot2[fastapi]'\n   ```\n\n   驱动器包名可以在 [驱动器商店](/store/drivers) 中找到。\n\n3. 安装适配器\n\n   ```bash\n   pip install nonebot-adapter-console\n   ```\n\n   适配器包名可以在 [适配器商店](/store/adapters) 中找到。\n\n## 创建配置文件\n\n配置文件用于存放 NoneBot 运行所需要的配置项，使用 [`pydantic`](https://docs.pydantic.dev/) 以及 [`python-dotenv`](https://saurabh-kumar.com/python-dotenv/) 来读取配置。配置项需符合 dotenv 格式，复杂类型数据需使用 JSON 格式填写。具体可选配置方式以及配置项详情参考[配置](../appendices/config.mdx)。\n\n在**项目文件夹**中创建一个 `.env` 文本文件，并写入以下内容:\n\n```bash title=.env\nHOST=0.0.0.0  # 配置 NoneBot 监听的 IP / 主机名\nPORT=8080  # 配置 NoneBot 监听的端口\nCOMMAND_START=[\"/\"]  # 配置命令起始字符\nCOMMAND_SEP=[\".\"]  # 配置命令分割字符\n```\n\n## 创建入口文件\n\n入口文件（ Entrypoint ）顾名思义，是用来初始化并运行机器人的 Python 文件。入口文件需要完成框架的初始化、注册适配器、加载插件等工作。\n\n:::tip 提示\n如果你使用 `nb-cli` 创建项目，入口文件不会被创建，该文件功能会被 `nb run` 命令代替。\n:::\n\n在**项目文件夹**中创建一个 `bot.py` 文件，并写入以下内容:\n\n```python title=bot.py\nimport nonebot\nfrom nonebot.adapters.console import Adapter as ConsoleAdapter  # 避免重复命名\n\n# 初始化 NoneBot\nnonebot.init()\n\n# 注册适配器\ndriver = nonebot.get_driver()\ndriver.register_adapter(ConsoleAdapter)\n\n# 在这里加载插件\nnonebot.load_builtin_plugins(\"echo\")  # 内置插件\n# nonebot.load_plugin(\"thirdparty_plugin\")  # 第三方插件\n# nonebot.load_plugins(\"awesome_bot/plugins\")  # 本地插件\n\nif __name__ == \"__main__\":\n    nonebot.run()\n```\n\n我们暂时不需要了解其中内容的含义，这些将会在稍后的章节中逐一介绍。在创建完成以上文件并确认已安装所需适配器和插件后，即可运行机器人。\n\n## 运行机器人\n\n在**项目文件夹**中，使用配置好环境的 Python 解释器运行入口文件（如果使用虚拟环境，请先激活虚拟环境）:\n\n```bash\npython bot.py\n```\n\n如果你后续使用了 `nb-cli` ，你仍可以使用 `nb run` 命令来运行机器人，`nb-cli` 会自动检测入口文件 `bot.py` 是否存在并运行。同时，你也可以使用 `nb run --reload` 来自动检测代码的更改并自动重新运行入口文件。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/tutorial/create-plugin.md",
    "content": "---\nsidebar_position: 3\ndescription: 创建并加载自定义插件\n\noptions:\n  menu:\n    - category: tutorial\n      weight: 50\n---\n\n# 插件编写准备\n\n在正式编写插件之前，我们需要先了解一下插件的概念。\n\n## 插件结构\n\n在 NoneBot 中，插件即是 Python 的一个[模块（module）](https://docs.python.org/zh-cn/3/glossary.html#term-module)。NoneBot 会在导入时对这些模块做一些特殊的处理使得他们成为一个插件。插件间应尽量减少耦合，可以进行有限制的相互调用，NoneBot 能够正确解析插件间的依赖关系。\n\n### 单文件插件\n\n一个普通的 `.py` 文件即可以作为一个插件，例如创建一个 `foo.py` 文件：\n\n```tree title=Project\n📂 plugins\n└── 📜 foo.py\n```\n\n这个时候模块 `foo` 已经可以被称为一个插件了，尽管它还什么都没做。\n\n### 包插件\n\n一个包含 `__init__.py` 的文件夹即是一个常规 Python [包 `package`](https://docs.python.org/zh-cn/3/glossary.html#term-regular-package)，例如创建一个 `foo` 文件夹：\n\n```tree title=Project\n📂 plugins\n└── 📂 foo\n    └── 📜 __init__.py\n```\n\n这个时候包 `foo` 同样是一个合法的插件，插件内容可以在 `__init__.py` 文件中编写。\n\n## 创建插件\n\n:::caution 注意\n如果在之前的[快速上手](../quick-start.mdx)章节中已经使用 `bootstrap` 模板创建了项目，那么你需要做出如下修改：\n\n1. 在项目目录中创建一个两层文件夹 `awesome_bot/plugins`\n\n   ```tree title=Project\n   📦 awesome-bot\n   ├── 📂 .venv\n   ├── 📂 awesome_bot\n   │   └── 📂 plugins\n   ├── 📜 .env.prod\n   ├── 📜 pyproject.toml\n   └── 📜 README.md\n   ```\n\n2. 修改 `pyproject.toml` 文件中的 `nonebot` 配置项，在 `plugin_dirs` 中添加 `awesome_bot/plugins`\n\n   ```toml title=pyproject.toml\n   [tool.nonebot]\n   plugin_dirs = [\"awesome_bot/plugins\"]\n   ```\n\n:::\n\n:::caution 注意\n如果在之前的[创建项目](./application.md)章节中手动创建了相关文件，那么你需要做出如下修改：\n\n1. 在项目目录中创建一个两层文件夹 `awesome_bot/plugins`\n\n   ```tree title=Project\n   📦 awesome-bot\n   ├── 📂 awesome_bot\n   │   └── 📂 plugins\n   └── 📜 bot.py\n   ```\n\n2. 修改 `bot.py` 文件中的加载插件部分，取消注释或者添加如下代码\n\n   ```python title=bot.py\n   # 在这里加载插件\n   nonebot.load_builtin_plugins(\"echo\")  # 内置插件\n   nonebot.load_plugins(\"awesome_bot/plugins\")  # 本地插件\n   ```\n\n:::\n\n创建插件可以通过 `nb-cli` 命令从完整模板创建，也可以手动新建空白文件。通过以下命令创建一个名为 `weather` 的插件：\n\n```bash\n$ nb plugin create\n[?] 插件名称: weather\n[?] 使用嵌套插件? (y/N) N\n[?] 请输入插件存储位置: awesome_bot/plugins\n```\n\n`nb-cli` 会在 `awesome_bot/plugins` 目录下创建一个名为 `weather` 的文件夹，其中包含的文件将在稍后章节中用到。\n\n```tree title=Project\n📦 awesome-bot\n├── 📂 .venv\n├── 📂 awesome_bot\n│   └── 📂 plugins\n|       └── 📂 weather\n|           ├── 📜 __init__.py\n|           └── 📜 config.py\n├── 📜 .env.prod\n├── 📜 pyproject.toml\n└── 📜 README.md\n```\n\n## 加载插件\n\n:::danger 警告\n请勿在插件被加载前 `import` 插件模块，这会导致 NoneBot 无法将其转换为插件而出现意料之外的情况。\n:::\n\n加载插件是在机器人入口文件中完成的，需要在框架初始化之后，运行之前进行。\n\n请注意，加载的插件模块名称（插件文件名或文件夹名）**不能相同**，且每一个插件**只能被加载一次**，重复加载将会导致异常。\n\n如果你使用 `nb-cli` 管理插件，那么你可以跳过这一节，`nb-cli` 将会自动处理加载。\n\n如果你**使用自定义的入口文件** `bot.py`，那么你需要在 `bot.py` 中加载插件。\n\n```python {5} title=bot.py\nimport nonebot\n\nnonebot.init()\n\n# 加载插件\n\nnonebot.run()\n```\n\n加载插件的方式有多种，但在底层的加载逻辑是一致的。以下是为加载插件提供的几种方式：\n\n### `load_plugin`\n\n通过点分割模块名称或使用 [`pathlib`](https://docs.python.org/zh-cn/3/library/pathlib.html) 的 `Path` 对象来加载插件。通常用于加载第三方插件或者项目插件。例如：\n\n```python\nfrom pathlib import Path\n\nnonebot.load_plugin(\"path.to.your.plugin\")  # 加载第三方插件\nnonebot.load_plugin(Path(\"./path/to/your/plugin.py\"))  # 加载项目插件\n```\n\n:::caution 注意\n请注意，本地插件的路径应该为相对机器人 **入口文件（通常为 bot.py）** 可导入的，例如在项目 `plugins` 目录下。\n:::\n\n### `load_plugins`\n\n加载传入插件目录中的所有插件，通常用于加载一系列本地编写的项目插件。例如：\n\n```python\nnonebot.load_plugins(\"src/plugins\", \"path/to/your/plugins\")\n```\n\n:::caution 注意\n请注意，插件目录应该为相对机器人 **入口文件（通常为 bot.py）** 可导入的，例如在项目 `plugins` 目录下。\n:::\n\n### `load_all_plugins`\n\n这种加载方式是以上两种方式的混合，加载所有传入的插件模块名称，以及所有给定目录下的插件。例如：\n\n```python\nnonebot.load_all_plugins([\"path.to.your.plugin\"], [\"path/to/your/plugins\"])\n```\n\n### `load_from_json`\n\n通过 JSON 文件加载插件，是 [`load_all_plugins`](#load_all_plugins) 的 JSON 变种。通过读取 JSON 文件中的 `plugins` 字段和 `plugin_dirs` 字段进行加载。例如：\n\n```json title=plugin_config.json\n{\n  \"plugins\": [\"path.to.your.plugin\"],\n  \"plugin_dirs\": [\"path/to/your/plugins\"]\n}\n```\n\n```python\nnonebot.load_from_json(\"plugin_config.json\", encoding=\"utf-8\")\n```\n\n:::tip 提示\n如果 JSON 配置文件中的字段无法满足你的需求，可以使用 [`load_all_plugins`](#load_all_plugins) 方法自行读取配置来加载插件。\n:::\n\n### `load_from_toml`\n\n通过 TOML 文件加载插件，是 [`load_all_plugins`](#load_all_plugins) 的 TOML 变种。通过读取 TOML 文件中的 `[tool.nonebot]` Table 中的 `plugin_dirs` Array 与\n`[tool.nonebot.plugins]` Table 中的多个 Array 进行加载。例如：\n\n```toml title=plugin_config.toml\n[tool.nonebot]\nplugin_dirs = [\"path/to/your/plugins\"]\n\n[tool.nonebot.plugins]\n\"@local\" = [\"path.to.your.plugin\"]  # 本地插件等非插件商店来源的插件\n\"nonebot-plugin-someplugin\" = [\"nonebot_plugin_someplugin\"]  # 插件商店来源的插件\n```\n\n```python\nnonebot.load_from_toml(\"plugin_config.toml\", encoding=\"utf-8\")\n```\n\n:::tip 提示\n如果 TOML 配置文件中的字段无法满足你的需求，可以使用 [`load_all_plugins`](#load_all_plugins) 方法自行读取配置来加载插件。\n:::\n\n### `load_builtin_plugin`\n\n加载一个内置插件，传入的插件名必须为 NoneBot 内置插件。该方法是 [`load_plugin`](#load_plugin) 的封装。例如：\n\n```python\nnonebot.load_builtin_plugin(\"echo\")\n```\n\n### `load_builtin_plugins`\n\n加载传入插件列表中的所有内置插件。例如：\n\n```python\nnonebot.load_builtin_plugins(\"echo\", \"single_session\")\n```\n\n### 其他加载方式\n\n有关其他插件加载的方式，可参考[跨插件访问](../advanced/requiring.md)和[嵌套插件](../advanced/plugin-nesting.md)。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/tutorial/event-data.mdx",
    "content": "---\nsidebar_position: 6\ndescription: 通过依赖注入获取所需事件信息\n\noptions:\n  menu:\n    - category: tutorial\n      weight: 80\n---\n\n# 获取事件信息\n\nimport Messenger from \"@site/src/components/Messenger\";\n\n在 NoneBot 事件处理流程中，获取事件信息并做出对应的操作是非常常见的场景。本章节中我们将介绍如何通过**依赖注入**获取事件信息。\n\n## 认识依赖注入\n\n在事件处理流程中，事件响应器具有自己独立的上下文，例如：当前响应的事件、收到事件的机器人或者其他处理流程中新增的信息等。这些数据可以根据我们的需求，通过依赖注入的方式，在执行事件处理流程中注入到事件处理函数中。\n\n相对于传统的信息获取方法，通过依赖注入获取信息的最大特色在于**按需获取**。如果该事件处理函数不需要任何额外信息即可运行，那么可以不进行依赖注入。如果事件处理函数需要额外的数据，可以通过依赖注入的方式灵活的标注出需要的依赖，在函数运行时便会被按需注入。\n\n## 使用依赖注入\n\n使用依赖注入获取上下文信息的方法十分简单，我们仅需要在函数的参数中声明所需的依赖，并正确的将函数添加为事件处理依赖即可。在 NoneBot 中，我们可以直接使用 `nonebot.params` 模块中定义的参数类型来声明依赖。\n\n例如，我们可以继续改进上一章节中的 `weather` 插件，使其可以获取到 `天气` 命令的地名参数，并根据地名返回天气信息。\n\n```python {9,11} title=weather/__init__.py\nfrom nonebot import on_command\nfrom nonebot.rule import to_me\nfrom nonebot.adapters import Message\nfrom nonebot.params import CommandArg\n\nweather = on_command(\"天气\", rule=to_me(), aliases={\"weather\", \"查天气\"}, priority=10, block=True)\n\n@weather.handle()\nasync def handle_function(args: Message = CommandArg()):\n    # 提取参数纯文本作为地名，并判断是否有效\n    if location := args.extract_plain_text():\n        await weather.finish(f\"今天{location}的天气是...\")\n    else:\n        await weather.finish(\"请输入地名\")\n```\n\n如上方示例所示，我们使用了 `args` 作为注入参数名，注入的内容为 `CommandArg()`，也就是**消息命令后跟随的内容**。在这个示例中，我们获得的参数会被检查是否有效，对无效参数则会结束事件。\n\n:::tip 提示\n命令与参数之间可以不需要空格，`CommandArg()` 获取的信息为命令后跟随的内容并去除了头部空白符。例如：`/天气 上海` 消息的参数为 `上海`。\n:::\n\n:::tip 提示\n`:=` 是 Python 3.8 引入的新语法 [Assignment Expressions](https://docs.python.org/zh-cn/3/reference/expressions.html#assignment-expressions)，也称为海象表达式，可以在表达式中直接赋值。\n:::\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/天气\" },\n    { position: \"left\", msg: \"请输入地名\" },\n    { position: \"right\", msg: \"/天气 上海\" },\n    { position: \"left\", msg: \"今天上海的天气是...\" },\n  ]}\n/>\n\nNoneBot 提供了多种依赖注入类型，可以获取不同的信息，具体内容可参考[依赖注入](../advanced/dependency.mdx)。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/tutorial/fundamentals.md",
    "content": "---\nsidebar_position: 1\ndescription: NoneBot 机器人构成及基本使用\n\noptions:\n  menu:\n    - category: tutorial\n      weight: 30\n---\n\n# 机器人的构成\n\n了解机器人的基本构成有助于你更好地使用 NoneBot，本章节将介绍 NoneBot 中的基本组成部分，稍后的文档中将会使用到这些概念。\n\n使用 NoneBot 框架搭建的机器人具有以下几个基本组成部分：\n\n1. NoneBot 机器人框架主体：负责连接各个组成部分，提供基本的机器人功能\n2. 驱动器 `Driver`：客户端/服务端的功能实现，负责接收和发送消息（通常为 HTTP 通信）\n3. 适配器 `Adapter`：驱动器的上层，负责将**平台消息**与 NoneBot 事件/操作系统的消息格式相互转换\n4. 插件 `Plugin`：机器人的功能实现，通常为负责处理事件并进行一系列的操作\n\n除 NoneBot 机器人框架主体外，其他部分均可按需选择、互相搭配，但由于平台的兼容性问题，部分插件可能仅在某些特定平台上可用（这由插件编写者决定）。\n\n在接下来的章节中，我们将重点介绍机器人功能实现，即插件 `Plugin` 部分。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/tutorial/handler.mdx",
    "content": "---\nsidebar_position: 5\ndescription: 处理接收到的特定事件\n\noptions:\n  menu:\n    - category: tutorial\n      weight: 70\n---\n\n# 事件处理\n\nimport Messenger from \"@site/src/components/Messenger\";\n\n在我们收到事件，并被某个事件响应器正确响应后，便正式开启了对于这个事件的**处理流程**。\n\n## 认识事件处理流程\n\n就像我们在解决问题时需要遵循流程一样，处理一个事件也需要一套流程。在事件响应器对一个事件进行响应之后，会依次执行一系列的**事件处理依赖**（通常是函数）。简单来说，事件处理流程并不是一个函数、一个对象或一个方法，而是一整套由开发者设计的流程。\n\n在这个流程中，我们**目前**只需要了解两个概念：函数形式的“事件处理依赖”（下称“事件处理函数”）和“事件响应器操作”。\n\n## 事件处理函数\n\n在事件响应器中，事件处理流程可以由一个或多个“事件处理函数”组成，这些事件处理函数将会按照顺序依次对事件进行处理，直到全部执行完成或被中断。我们可以采用事件响应器的“事件处理函数装饰器”来添加这些“事件处理函数”。\n\n顾名思义，“事件处理函数装饰器”是一个[装饰器（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)中所展示的包装用法相同。例如：\n\n```python {6-8} title=weather/__init__.py\nfrom nonebot import on_command\nfrom nonebot.rule import to_me\n\nweather = on_command(\"天气\", rule=to_me(), aliases={\"weather\", \"查天气\"}, priority=10, block=True)\n\n@weather.handle()\nasync def handle_function():\n    pass  # do something here\n```\n\n如上方示例所示，我们使用 `weather` 响应器的 `handle` 装饰器装饰了一个函数 `handle_function`。`handle_function` 函数会被添加到 `weather` 的事件处理流程中。在 `weather` 响应器被触发之后，将会依次调用 `weather` 响应器的事件处理函数，即 `handle_function` 来对事件进行处理。\n\n## 事件响应器操作\n\n在事件处理流程中，我们可以使用事件响应器操作来进行一些交互或改变事件处理流程，例如向机器人用户发送消息或提前结束事件处理流程等。\n\n事件响应器操作与事件处理函数装饰器类似，通常作为事件响应器 `Matcher` 的[类方法](https://docs.python.org/zh-cn/3/library/functions.html#classmethod)存在，因此事件响应器操作的调用方法也是 `Matcher.func()` 的形式。不过不同的是，事件响应器操作并不是装饰器，因此并不需要@进行标注。\n\n```python {8,9} title=weather/__init__.py\nfrom nonebot import on_command\nfrom nonebot.rule import to_me\n\nweather = on_command(\"天气\", rule=to_me(), aliases={\"weather\", \"查天气\"}, priority=10, block=True)\n\n@weather.handle()\nasync def handle_function():\n    # await weather.send(\"天气是...\")\n    await weather.finish(\"天气是...\")\n```\n\n如上方示例所示，我们使用 `weather` 响应器的 `finish` 操作方法向机器人用户回复了 `天气是...` 并结束了事件处理流程。效果如下：\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/天气\" },\n    { position: \"left\", msg: \"天气是...\" },\n  ]}\n/>\n\n值得注意的是，在执行 `finish` 方法时，NoneBot 会在向机器人用户发送消息内容后抛出 `FinishedException` 异常来结束事件响应流程。也就是说，在 `finish` 被执行后，后续的程序是不会被执行的。如果你需要回复机器人用户消息但不想事件处理流程结束，可以使用注释的部分中展示的 `send` 方法。\n\n:::danger 警告\n由于 `finish` 是通过抛出 `FinishedException` 异常来结束事件的，因此异常可能会被未加限制的 `try-except` 捕获，影响事件处理流程正确处理，导致无法正常结束此事件。请务必在异常捕获中指定错误类型或排除所有 [MatcherException](../api/exception.md#MatcherException) 类型的异常（如下所示），或将 `finish` 移出捕获范围进行使用。\n\n```python\nfrom nonebot.exception import MatcherException\n\ntry:\n    await weather.finish(\"天气是...\")\nexcept MatcherException:\n    raise\nexcept Exception as e:\n    pass # do something here\n```\n\n:::\n\n目前 NoneBot 提供了多种事件响应器操作，其中包括用于机器人用户交互与流程控制两大类，进阶使用方法可以查看[会话控制](../appendices/session-control.mdx)。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/tutorial/matcher.md",
    "content": "---\nsidebar_position: 4\ndescription: 响应接收到的特定事件\n\noptions:\n  menu:\n    - category: tutorial\n      weight: 60\n---\n\n# 事件响应器\n\n事件响应器（Matcher）是对接收到的事件进行响应的基本单元，所有的事件响应器都继承自 `Matcher` 基类。\n\n在 NoneBot 中，事件响应器可以通过一系列特定的规则**筛选**出**具有某种特征的事件**，并按照**特定的流程**交由**预定义的事件处理依赖**进行处理。例如，在[快速上手](../quick-start.mdx)中，我们使用了内置插件 `echo` ，它定义的事件响应器能响应机器人用户发送的“/echo hello world”消息，提取“hello world”信息并作为回复消息发送。\n\n## 事件响应器辅助函数\n\nNoneBot 中所有事件响应器均继承自 `Matcher` 基类，但直接使用 `Matcher.new()` 方法创建事件响应器过于繁琐且不能记录插件信息。因此，NoneBot 中提供了一系列“事件响应器辅助函数”（下称“辅助函数”）来辅助我们用**最简的方式**创建**带有不同规则预设**的事件响应器，提高代码可读性和书写效率。通常情况下，我们只需要使用辅助函数即可完成事件响应器的创建。\n\n在 NoneBot 中，辅助函数以 `on()` 或 `on_<type/rule>()` 形式出现（例如 `on_command()`），调用后根据不同的参数返回一个 `Type[Matcher]` 类型的新事件响应器。\n\n目前 NoneBot 提供了多种功能各异的辅助函数、具有共同命令名称前缀的命令组以及具有共同参数的响应器组，均可以从 `nonebot` 模块直接导入使用，具体内容参考[事件响应器进阶](../advanced/matcher.md)。\n\n## 创建事件响应器\n\n在上一节[创建插件](./create-plugin.md#创建插件)中，我们创建了一个 `weather` 插件，现在我们来实现他的功能。\n\n我们直接使用 `on_command()` 辅助函数来创建一个事件响应器：\n\n```python {3} title=weather/__init__.py\nfrom nonebot import on_command\n\nweather = on_command(\"天气\")\n```\n\n这样，我们就获得一个名为 `weather` 的事件响应器了，这个事件响应器会对 `/天气` 开头的消息进行响应。\n\n:::tip 提示\n如果一条消息中包含“@机器人”或以“机器人的昵称”开始，例如 `@bot /天气` 时，协议适配器会将 `event.is_tome()` 判断为 `True` ，同时也会自动去除 `@bot`，即事件响应器收到的信息内容为 `/天气`，方便进行命令匹配。\n:::\n\n### 为事件响应器添加参数\n\n在辅助函数中，我们可以添加一些参数来对事件响应器进行更加精细的调整，例如事件响应器的优先级、匹配规则等。例如：\n\n```python {4} title=weather/__init__.py\nfrom nonebot import on_command\nfrom nonebot.rule import to_me\n\nweather = on_command(\"天气\", rule=to_me(), aliases={\"weather\", \"查天气\"}, priority=10, block=True)\n```\n\n这样，我们就获得了一个可以响应 `天气`、`weather`、`查天气` 三个命令的响应规则，需要私聊或 `@bot` 时才会响应，优先级为 10（越小越优先），阻断事件向后续优先级传播的事件响应器了。这些内容的意义和使用方法将会在后续的章节中一一介绍。\n\n:::tip 提示\n需要注意的是，不同的辅助函数有不同的可选参数，在使用之前可以参考[事件响应器进阶 - 基本辅助函数](../advanced/matcher.md#基本辅助函数)或 [API 文档](../api/plugin/on.md#on)。\n:::\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/tutorial/message.md",
    "content": "---\nsidebar_position: 7\ndescription: 处理消息序列与消息段\n\noptions:\n  menu:\n    - category: tutorial\n      weight: 90\n---\n\n# 处理消息\n\n在不同平台中，一条消息可能会有承载有各种不同的表现形式，它可能是一段纯文本、一张图片、一段语音、一篇富文本文章，也有可能是多种类型的组合等等。\n\n在 NoneBot 中，为确保消息的正常处理与跨平台兼容性，采用了扁平化的消息序列形式，即 `Message` 对象。消息序列是 NoneBot 中的消息载体，无论是接收还是发送的消息，都采用消息序列的形式进行处理。\n\n## 认识消息类型\n\n### 消息序列 `Message`\n\n在 NoneBot 中，消息序列 `Message` 的主要作用是用于表达“一串消息”。由于消息序列继承自 `List[MessageSegment]`，所以 `Message` 的本质是由若干消息段所组成的序列。因此，消息序列的使用方法与 `List` 有很多相似之处，例如切片、索引、拼接等。\n\n在上一节的[使用依赖注入](./event-data.mdx#使用依赖注入)中，我们已经通过依赖注入 `CommandArg()` 获取了命令的参数，它的类型即是消息序列。我们使用了消息序列的 `extract_plain_text()` 方法来获取消息序列中的纯文本内容。\n\n### 消息段 `MessageSegment`\n\n顾名思义，消息段 `MessageSegment` 是一段消息。由于消息序列的本质是由若干消息段所组成的序列，消息段可以被认为是构成消息序列的最小单位。简单来说，消息序列类似于一个自然段，而消息段则是组成自然段的一句话。同时，作为特殊消息载体的存在，绝大多数的平台都有着**独特的消息类型**，这些独特的内容均需要由对应的**协议适配器**所提供，以适应不同平台中的消息模式。**这也意味着，你需要导入对应的协议适配器中的消息序列和消息段后才能使用其特殊的工厂方法。**\n\n:::caution 注意\n消息段的类型是由协议适配器提供的，因此你需要参考协议适配器的文档并导入对应的消息段后才能使用其特殊的消息类型。\n\n在上一节的[使用依赖注入](./event-data.mdx#使用依赖注入)中，我们导入的为 `nonebot.adapters.Message` 抽象基类，因此我们无法使用平台特有的消息类型。仅能使用 `str` 作为纯文本消息回复。\n:::\n\n## 使用消息序列\n\n:::caution 注意\n在以下的示例中，为了更好的理解多种类型的消息组成方式，我们将使用 `Console` 协议适配器来演示消息序列的使用方法。在实际使用中，你需要确保你使用的**消息序列类型**与你所要发送的**平台类型**一致。\n:::\n\n通常情况下，适配器在接收到消息时，会将消息转换为消息序列，可以通过依赖注入 [`EventMessage`](../advanced/dependency.mdx#eventmessage)，或者使用 `event.get_message()` 获取。\n\n由于消息序列是 `List[MessageSegment]` 的子类，所以你总是可以用和操作 `List` 类似的方式来处理消息序列。例如：\n\n```python\n>>> from nonebot.adapters.console import Message, MessageSegment\n>>> message = Message([\n    MessageSegment(type=\"text\", data={\"text\":\"hello\"}),\n    MessageSegment(type=\"markdown\", data={\"markup\":\"**world**\"}),\n])\n>>> for segment in message:\n...     print(segment.type, segment.data)\n...\ntext {'text': 'hello'}\nmarkdown {'markup': '**world**'}\n>>> len(message)\n2\n```\n\n### 构造消息序列\n\n在使用事件响应器操作发送消息时，既可以使用 `str` 作为消息，也可以使用 `Message`、`MessageSegment` 或者 `MessageTemplate`。那么，我们就需要先构造一个消息序列。消息序列可以通过多种方式构造：\n\n#### 直接构造\n\n`Message` 类可以直接实例化，支持 `str`、`MessageSegment`、`Iterable[MessageSegment]` 或适配器自定义类型的参数。\n\n```python\nfrom nonebot.adapters.console import Message, MessageSegment\n\n# str\nMessage(\"Hello, world!\")\n# MessageSegment\nMessage(MessageSegment.text(\"Hello, world!\"))\n# List[MessageSegment]\nMessage([MessageSegment.text(\"Hello, world!\")])\n```\n\n#### 运算构造\n\n`Message` 对象可以通过 `str`、`MessageSegment` 相加构造，详情请参考[拼接消息](#拼接消息)。\n\n#### 从字典数组构造\n\n`Message` 对象支持 Pydantic 自定义类型构造，可以使用 Pydantic 的 `TypeAdapter` 方法进行构造。\n\n```python\nfrom pydantic import TypeAdapter\nfrom nonebot.adapters.console import Message, MessageSegment\n\n# 由字典构造消息段\nTypeAdapter(MessageSegment).validate_python(\n    {\"type\": \"text\", \"data\": {\"text\": \"text\"}}\n) == MessageSegment.text(\"text\")\n\n# 由字典数组构造消息序列\nTypeAdapter(Message).validate_python(\n    [MessageSegment.text(\"text\"), {\"type\": \"text\", \"data\": {\"text\": \"text\"}}],\n) == Message([MessageSegment.text(\"text\"), MessageSegment.text(\"text\")])\n```\n\n### 获取消息纯文本\n\n由于消息中存在各种类型的消息段，因此 `str(message)` 通常**不能得到消息的纯文本**，而是一个消息序列的字符串表示。\n\nNoneBot 为消息段定义了一个方法 `is_text()` ，可以用于判断消息段是否为纯文本；也可以使用 `message.extract_plain_text()` 方法获取消息纯文本。\n\n```python\nfrom nonebot.adapters.console import Message, MessageSegment\n\n# 判断消息段是否为纯文本\nMessageSegment.text(\"text\").is_text() == True\n\n# 提取消息纯文本字符串\nMessage(\n    [MessageSegment.text(\"text\"), MessageSegment.markdown(\"**markup**\")]\n).extract_plain_text() == \"text\"\n```\n\n### 遍历\n\n消息序列继承自 `List[MessageSegment]` ，因此可以使用 `for` 循环遍历消息段。\n\n```python\nfor segment in message:\n    ...\n```\n\n### 比较\n\n消息和消息段都可以使用 `==` 或 `!=` 运算符比较是否相同。\n\n```python\nMessageSegment.text(\"text\") != MessageSegment.text(\"foo\")\n\nsome_message == Message([MessageSegment.text(\"text\")])\n```\n\n### 检查消息段\n\n我们可以通过 `in` 运算符或消息序列的 `has` 方法来：\n\n```python\n# 是否存在消息段\nMessageSegment.text(\"text\") in message\n# 是否存在指定类型的消息段\n\"text\" in message\n```\n\n我们还可以使用消息序列的 `only` 方法来检查消息中是否仅包含指定的消息段。\n\n```python\n# 是否都为指定消息段\nmessage.only(MessageSegment.text(\"test\"))\n# 是否仅包含指定类型的消息段\nmessage.only(\"text\")\n```\n\n### 过滤、索引与切片\n\n消息序列对列表的索引与切片进行了增强，在原有列表 `int` 索引与 `slice` 切片的基础上，支持 `type` 过滤索引与切片。\n\n```python\nfrom nonebot.adapters.console import Message, MessageSegment\n\nmessage = Message(\n    [\n        MessageSegment.text(\"test\"),\n        MessageSegment.markdown(\"test2\"),\n        MessageSegment.markdown(\"test3\"),\n        MessageSegment.text(\"test4\"),\n    ]\n)\n# 索引\nmessage[0] == MessageSegment.text(\"test\")\n# 切片\nmessage[0:2] == Message(\n    [MessageSegment.text(\"test\"), MessageSegment.markdown(\"test2\")]\n)\n# 类型过滤\nmessage[\"markdown\"] == Message(\n    [MessageSegment.markdown(\"test2\"), MessageSegment.markdown(\"test3\")]\n)\n# 类型索引\nmessage[\"markdown\", 0] == MessageSegment.markdown(\"test2\")\n# 类型切片\nmessage[\"markdown\", 0:2] == Message(\n    [MessageSegment.markdown(\"test2\"), MessageSegment.markdown(\"test3\")]\n)\n```\n\n我们也可以通过消息序列的 `include`、`exclude` 方法进行类型过滤。\n\n```python\nmessage.include(\"text\", \"markdown\")\nmessage.exclude(\"text\")\n```\n\n同样的，消息序列对列表的 `index`、`count` 方法也进行了增强，可以用于索引指定类型的消息段。\n\n```python\n# 指定类型首个消息段索引\nmessage.index(\"markdown\") == 1\n# 指定类型消息段数量\nmessage.count(\"markdown\") == 2\n```\n\n此外，消息序列添加了一个 `get` 方法，可以用于获取指定类型指定个数的消息段。\n\n```python\n# 获取指定类型指定个数的消息段\nmessage.get(\"markdown\", 1) == Message([MessageSegment.markdown(\"test2\")])\n```\n\n### 拼接消息\n\n`str`、`Message`、`MessageSegment` 对象之间可以直接相加，相加均会返回一个新的 `Message` 对象。\n\n```python\n# 消息序列与消息段相加\nMessage([MessageSegment.text(\"text\")]) + MessageSegment.text(\"text\")\n# 消息序列与字符串相加\nMessage([MessageSegment.text(\"text\")]) + \"text\"\n# 消息序列与消息序列相加\nMessage([MessageSegment.text(\"text\")]) + Message([MessageSegment.text(\"text\")])\n# 字符串与消息序列相加\n\"text\" + Message([MessageSegment.text(\"text\")])\n# 消息段与消息段相加\nMessageSegment.text(\"text\") + MessageSegment.text(\"text\")\n# 消息段与字符串相加\nMessageSegment.text(\"text\") + \"text\"\n# 消息段与消息序列相加\nMessageSegment.text(\"text\") + Message([MessageSegment.text(\"text\")])\n# 字符串与消息段相加\n\"text\" + MessageSegment.text(\"text\")\n```\n\n如果需要在当前消息序列后直接拼接新的消息段，可以使用 `Message.append`、`Message.extend` 方法，或者使用自加。\n\n```python\nmsg = Message([MessageSegment.text(\"text\")])\n# 自加\nmsg += \"text\"\nmsg += MessageSegment.text(\"text\")\nmsg += Message([MessageSegment.text(\"text\")])\n# 附加\nmsg.append(\"text\")\nmsg.append(MessageSegment.text(\"text\"))\n# 扩展\nmsg.extend([MessageSegment.text(\"text\")])\n```\n\n我们也可以通过消息段或消息序列的 `join` 方法来拼接一串消息：\n\n```python\nseg = MessageSegment.text(\"text\")\nmsg = seg.join(\n    [\n        MessageSegment.text(\"first\"),\n        Message(\n            [\n                MessageSegment.text(\"second\"),\n                MessageSegment.text(\"third\"),\n            ]\n        )\n    ]\n)\nmsg == Message(\n    [\n        MessageSegment.text(\"first\"),\n        MessageSegment.text(\"text\"),\n        MessageSegment.text(\"second\"),\n        MessageSegment.text(\"third\"),\n    ]\n)\n```\n\n### 使用消息模板\n\n为了提供安全可靠的跨平台模板字符，我们提供了一个消息模板功能来构建消息序列\n\n它在以下常见场景中尤其有用：\n\n- 多行富文本编排（包含图片，文字以及表情等）\n- 客制化（由 Bot 最终用户提供消息模板时）\n\n在事实上，它的用法和 `str.format` 极为相近，所以你在使用的时候，总是可以参考[Python 文档](https://docs.python.org/zh-cn/3/library/stdtypes.html#str.format)来达到你想要的效果，这里给出几个简单的例子。\n\n默认情况下，消息模板采用 `str` 纯文本形式的格式化：\n\n```python title=基础格式化用法\n>>> from nonebot.adapters import MessageTemplate\n>>> MessageTemplate(\"{} {}\").format(\"hello\", \"world\")\n'hello world'\n```\n\n如果 `Message.template` 构建消息模板，那么消息模板将采用消息序列形式的格式化，此时的消息将会是平台特定的：\n\n:::caution 注意\n使用 `Message.template` 构建消息模板时，应注意消息序列为平台适配器提供的类型，不能使用 `nonebot.adapters.Message` 基类作为模板构建。使用基类构建模板与使用 `str` 构建模板的效果是一样的，因此请使用上述的 `MessageTemplate` 类直接构建模板。：\n:::\n\n```python title=平台格式化用法\n>>> from nonebot.adapters.console import Message, MessageSegment\n>>> Message.template(\"{} {}\").format(\"hello\", \"world\")\nMessage(\n    MessageSegment.text(\"hello\"),\n    MessageSegment.text(\" \"),\n    MessageSegment.text(\"world\")\n)\n```\n\n消息模板支持使用消息段进行格式化：\n\n```python title=对消息段进行安全的拼接\n>>> from nonebot.adapters.console import Message, MessageSegment\n>>> Message.template(\"{}{}\").format(MessageSegment.markdown(\"**markup**\"), \"world\")\nMessage(\n    MessageSegment(type='markdown', data={'markup': '**markup**'}),\n    MessageSegment(type='text', data={'text': 'world'})\n)\n```\n\n消息模板同样支持使用消息序列作为模板：\n\n```python title=以消息对象作为模板\n>>> from nonebot.adapters.console import Message, MessageSegment\n>>> Message.template(\n...     MessageSegment.text(\"{user_id}\") + MessageSegment.emoji(\"tada\") +\n...     MessageSegment.text(\"{message}\")\n... ).format_map({\"user_id\": 123456, \"message\": \"hello world\"})\nMessage(\n    MessageSegment(type='text', data={'text': '123456'}),\n    MessageSegment(type='emoji', data={'emoji': 'tada'}),\n    MessageSegment(type='text', data={'text': 'hello world'})\n)\n```\n\n:::caution 注意\n只有消息序列中的文本类型消息段才能被格式化，其他类型的消息段将会原样添加。\n:::\n\n消息模板支持使用拓展控制符来控制消息段类型：\n\n```python title=使用消息段的拓展控制符\n>>> from nonebot.adapters.console import Message, MessageSegment\n>>> Message.template(\"{name:emoji}\").format(name='tada')\nMessage(MessageSegment(type='emoji', data={'name': 'tada'}))\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.2/tutorial/store.mdx",
    "content": "---\nsidebar_position: 2\ndescription: 从商店安装适配器和插件\n\noptions:\n  menu:\n    - category: tutorial\n      weight: 40\n---\n\n# 获取商店内容\n\nimport Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Asciinema from \"@site/src/components/Asciinema\";\n\n:::tip 提示\n\n如果你暂时没有获取商店内容的需求，可以跳过本章节。\n\n:::\n\nNoneBot 提供了一个[商店](/store/plugins)，商店内容均由社区开发者贡献。你可以在商店中查找你需要的适配器和插件等，进行安装或者参考其文档等。\n\n商店中每个内容的卡片都包含了其名称和简介等信息，点击**卡片右上角**链接图标即可跳转到其主页。\n\n## 安装插件\n\n<Asciinema\n  url=\"https://asciinema.org/a/569650.cast\"\n  options={{ theme: \"monokai\", poster: \"npt:16.8\" }}\n/>\n\n在商店插件页面中，点击你需要安装的插件下方的 `点击复制安装命令` 按钮，即可复制 `nb-cli` 命令。\n\n请在你的**项目目录**下执行该命令。`nb-cli` 会自动安装插件并将其添加到加载列表中。\n\n<Tabs groupId=\"cli-install\">\n  <TabItem value=\"cli\" label=\"使用命令\" default>\n\n```bash\nnb plugin install <插件名称>\n```\n\n  </TabItem>\n  <TabItem value=\"interactive\" label=\"交互式安装\">\n\n```bash\n$ nb plugin install\n[?] 想要安装的插件名称: <插件名称>\n```\n\n  </TabItem>\n  <TabItem value=\"pip\" label=\"使用 pip\">\n\n```bash\npip install <插件包名>\n```\n\n插件包名可以在商店插件卡片中找到，或者使用 `nb-cli` 搜索插件显示的详情中找到。安装完成后，需要参考[加载插件章节](./create-plugin.md#加载插件)自行加载。\n\n  </TabItem>\n</Tabs>\n\n如果想要查看插件列表，可以使用以下命令\n\n```bash\n# 列出商店所有插件\nnb plugin list\n# 搜索商店插件\nnb plugin search [可选关键词]\n```\n\n升级和卸载插件可以使用以下命令\n\n<Tabs groupId=\"cli-install\">\n  <TabItem value=\"cli\" label=\"使用命令\" default>\n\n```bash\nnb plugin update <插件名称>\nnb plugin uninstall <插件名称>\n```\n\n  </TabItem>\n  <TabItem value=\"interactive\" label=\"交互式安装\">\n\n```bash\n$ nb plugin update\n[?] 想要安装的插件名称: <插件名称>\n$ nb plugin uninstall\n[?] 想要卸载的插件名称: <插件名称>\n```\n\n  </TabItem>\n  <TabItem value=\"pip\" label=\"使用 pip\">\n\n```bash\npip install --upgrade <插件包名>\npip uninstall <插件包名>\n```\n\n插件包名可以在商店插件卡片中找到，或者使用 `nb-cli` 搜索插件显示的详情中找到。卸载完成后，需要自行移除插件加载。\n\n  </TabItem>\n</Tabs>\n\n## 安装适配器\n\n<Asciinema\n  url=\"https://asciinema.org/a/569664.cast\"\n  options={{ theme: \"monokai\", poster: \"npt:12.0\" }}\n/>\n\n安装适配器与安装插件类似，只是将命令换为 `nb adapter`，这里就不再赘述。\n\n请在你的**项目目录**下执行该命令。`nb-cli` 会自动安装适配器并将其添加到注册列表中。\n\n<Tabs groupId=\"cli-install\">\n  <TabItem value=\"cli\" label=\"使用命令\" default>\n\n```bash\nnb adapter install <适配器名称>\n```\n\n  </TabItem>\n  <TabItem value=\"interactive\" label=\"交互式安装\">\n\n```bash\n$ nb adapter install\n[?] 想要安装的适配器名称: <适配器名称>\n```\n\n  </TabItem>\n  <TabItem value=\"pip\" label=\"使用 pip\">\n\n```bash\npip install <适配器包名>\n```\n\n适配器包名可以在商店适配器卡片中找到，或者使用 `nb-cli` 搜索适配器显示的详情中找到。安装完成后，需要参考[注册适配器章节](../advanced/adapter.md#注册适配器)自行注册。\n\n  </TabItem>\n</Tabs>\n\n如果想要查看适配器列表，可以使用以下命令\n\n```bash\n# 列出商店所有适配器\nnb adapter list\n# 搜索商店适配器\nnb adapter search [可选关键词]\n```\n\n升级和卸载适配器可以使用以下命令\n\n<Tabs groupId=\"cli-install\">\n  <TabItem value=\"cli\" label=\"使用命令\" default>\n\n```bash\nnb adapter update <适配器名称>\nnb adapter uninstall <适配器名称>\n```\n\n  </TabItem>\n  <TabItem value=\"interactive\" label=\"交互式安装\">\n\n```bash\n$ nb adapter update\n[?] 想要安装的适配器名称: <适配器名称>\n$ nb adapter uninstall\n[?] 想要卸载的适配器名称: <适配器名称>\n```\n\n  </TabItem>\n  <TabItem value=\"pip\" label=\"使用 pip\">\n\n```bash\npip install --upgrade <适配器包名>\npip uninstall <适配器包名>\n```\n\n适配器包名可以在商店适配器卡片中找到，或者使用 `nb-cli` 搜索适配器显示的详情中找到。卸载完成后，需要自行移除适配器加载。\n\n  </TabItem>\n</Tabs>\n\n## 安装驱动器\n\n<Asciinema\n  url=\"https://asciinema.org/a/569665.cast\"\n  options={{ theme: \"monokai\", poster: \"npt:14.0\" }}\n/>\n\n安装驱动器与安装插件同样类似，只是将命令换为 `nb driver`，这里就不再赘述。\n\n如果你使用了虚拟环境，请在你的**项目目录**下执行该命令，`nb-cli` 会自动安装驱动器到虚拟环境中。\n\n请注意 `nb-cli` 并不会在安装驱动器后修改项目所使用的驱动器，请自行参考[配置方法](../appendices/config.mdx)章节以及 [`DRIVER` 配置项](../appendices/config.mdx#driver)修改驱动器。\n\n<Tabs groupId=\"cli-install\">\n  <TabItem value=\"cli\" label=\"使用命令\" default>\n\n```bash\nnb driver install <驱动器名称>\n```\n\n  </TabItem>\n  <TabItem value=\"interactive\" label=\"交互式安装\">\n\n```bash\n$ nb driver install\n[?] 想要安装的驱动器名称: <驱动器名称>\n```\n\n  </TabItem>\n  <TabItem value=\"pip\" label=\"使用 pip\">\n\n```bash\npip install <驱动器包名>\n```\n\n驱动器包名可以在商店驱动器卡片中找到，或者使用 `nb-cli` 搜索驱动器显示的详情中找到。\n\n  </TabItem>\n</Tabs>\n\n如果想要查看驱动器列表，可以使用以下命令\n\n```bash\n# 列出商店所有驱动器\nnb driver list\n# 搜索商店驱动器\nnb driver search [可选关键词]\n```\n\n升级和卸载驱动器可以使用以下命令\n\n<Tabs groupId=\"cli-install\">\n  <TabItem value=\"cli\" label=\"使用命令\" default>\n\n```bash\nnb driver update <驱动器名称>\nnb driver uninstall <驱动器名称>\n```\n\n  </TabItem>\n  <TabItem value=\"interactive\" label=\"交互式安装\">\n\n```bash\n$ nb driver update\n[?] 想要安装的驱动器名称: <驱动器名称>\n$ nb driver uninstall\n[?] 想要卸载的驱动器名称: <驱动器名称>\n```\n\n  </TabItem>\n  <TabItem value=\"pip\" label=\"使用 pip\">\n\n```bash\npip install --upgrade <驱动器包名>\npip uninstall <驱动器包名>\n```\n\n驱动器包名可以在商店驱动器卡片中找到，或者使用 `nb-cli` 搜索驱动器显示的详情中找到。卸载完成后，需要自行移除适配器加载。\n\n  </TabItem>\n</Tabs>\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/README.md",
    "content": "---\nsidebar_position: 0\nid: index\nslug: /\n---\n\n# 概览\n\nNoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架（下称 NoneBot），它基于 Python 的类型注解和异步优先特性（兼容同步），能够为你的需求实现提供便捷灵活的支持。同时，NoneBot 拥有大量的开发者为其开发插件，用户无需编写任何代码，仅需完成环境配置及插件安装，就可以正常使用 NoneBot。\n\n需要注意的是，NoneBot 仅支持 **Python 3.9 以上版本**\n\n## 特色\n\n### 异步优先\n\nNoneBot 基于 Python [asyncio](https://docs.python.org/zh-cn/3/library/asyncio.html) / [trio](https://trio.readthedocs.io/en/stable/) 编写，并在异步机制的基础上进行了一定程度的同步函数兼容。\n\n### 完整的类型注解\n\nNoneBot 参考 [PEP 484](https://www.python.org/dev/peps/pep-0484/) 等 PEP 完整实现了类型注解，通过 Pyright（Pylance） 检查。配合编辑器的类型推导功能，能将绝大多数的 Bug 杜绝在编辑器中（[编辑器支持](./editor-support)）。\n\n### 开箱即用\n\nNoneBot 提供了使用便捷、具有交互式功能的命令行工具--`nb-cli`，使得用户初次接触 NoneBot 时更容易上手。使用方法请阅读本文档[指南](./quick-start.mdx)以及 [CLI 文档](https://cli.nonebot.dev/)。\n\n### 插件系统\n\n插件系统是 NoneBot 的核心，通过它可以实现机器人的模块化以及功能扩展，便于维护和管理。\n\n### 依赖注入系统\n\nNoneBot 采用了一套自行定义的依赖注入系统，可以让事件的处理过程更加的简洁、清晰，增加代码的可读性，减少代码冗余。\n\n#### 什么是依赖注入\n\n[**『依赖注入』**](https://zh.wikipedia.org/wiki/%E6%8E%A7%E5%88%B6%E5%8F%8D%E8%BD%AC)意思是，在编程中，有一种方法可以让你的代码声明它工作和使用所需要的东西，即**『依赖』**。\n\n系统（在这里是指 NoneBot）将负责做任何需要的事情，为你的代码提供这些必要依赖（即**『注入』**依赖性）\n\n这在你有以下情形的需求时非常有用：\n\n- 这部分代码拥有共享的逻辑（同样的代码逻辑多次重复）\n- 共享数据库以及网络请求连接会话\n  - 比如 `httpx.AsyncClient`、`aiohttp.ClientSession` 和 `sqlalchemy.Session`\n- 机器人用户权限检查以及认证\n- 还有更多...\n\n它在完成上述工作的同时，还能尽量减少代码的耦合和重复\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/advanced/adapter.md",
    "content": "---\nsidebar_position: 1\ndescription: 注册适配器与指定平台交互\n\noptions:\n  menu:\n    - category: advanced\n      weight: 20\n---\n\n# 使用适配器\n\n适配器 (Adapter) 是机器人与平台交互的核心桥梁，它负责在驱动器和机器人插件之间转换与传递消息。\n\n## 适配器功能与组成\n\n适配器通常有两种功能，分别是**接收事件**和**调用平台接口**。其中，接收事件是指将驱动器收到的事件消息转换为 NoneBot 定义的事件模型，然后交由机器人插件处理；调用平台接口是指将机器人插件调用平台接口的数据转换为平台指定的格式，然后交由驱动器发送，并接收接口返回数据。\n\n为了实现这两种功能，适配器通常由四个部分组成：\n\n- **Adapter**：负责转换事件和调用接口，正确创建 Bot 对象并注册到 NoneBot 中。\n- **Bot**：负责存储平台机器人相关信息，并提供回复事件的方法。\n- **Event**：负责定义事件内容，以及事件主体对象。\n- **Message**：负责正确序列化消息，以便机器人插件处理。\n\n## 注册适配器\n\n在使用适配器之前，我们需要先将适配器注册到驱动器中，这样适配器就可以通过驱动器接收事件和调用接口了。我们以 Console 适配器为例，来看看如何注册适配器：\n\n```python {2,5} title=bot.py\nimport nonebot\nfrom nonebot.adapters.console import Adapter\n\ndriver = nonebot.get_driver()\ndriver.register_adapter(Adapter)\n```\n\n我们首先需要从适配器模块中导入所需要的适配器类，然后通过驱动器的 `register_adapter` 方法将适配器注册到驱动器中即可。如果我们需要多平台支持，可以多次调用 `register_adapter` 方法来注册多个适配器。\n\n## 获取已注册的适配器\n\nNoneBot 提供了 `get_adapter` 方法来获取已注册的适配器，我们可以通过适配器的名称或类型来获取指定的适配器实例：\n\n```python\nimport nonebot\nfrom nonebot.adapters.console import Adapter\n\nadapters = nonebot.get_adapters()\nconsole_adapter = nonebot.get_adapter(Adapter)\nconsole_adapter = nonebot.get_adapter(Adapter.get_name())\n```\n\n## 获取 Bot 对象\n\n当前所有适配器已连接的 Bot 对象可以通过 `get_bots` 方法获取，这是一个以机器人 ID 为键的字典：\n\n```python\nimport nonebot\n\nbots = nonebot.get_bots()\n```\n\n我们也可以通过 `get_bot` 方法获取指定 ID 的 Bot 对象。如果省略 ID 参数，将会返回所有 Bot 中的第一个：\n\n```python\nimport nonebot\n\nbot = nonebot.get_bot(\"bot_id\")\n```\n\n如果需要获取指定适配器连接的 Bot 对象，我们可以通过适配器的 `bots` 属性获取，这也是一个以机器人 ID 为键的字典：\n\n```python\nimport nonebot\nfrom nonebot.adapters.console import Adapter\n\nconsole_adapter = nonebot.get_adapter(Adapter)\nbots = console_adapter.bots\n```\n\nBot 对象都具有一个 `self_id` 属性，它是机器人的唯一 ID，由适配器填写，通常为机器人的帐号 ID 或者 APP ID。\n\n## 获取事件通用信息\n\n适配器的所有事件模型均继承自 `Event` 基类，在[事件类型与重载](../appendices/overload.md)一节中，我们也提到了如何使用基类抽象方法来获取事件通用信息。基类能提供如下信息：\n\n### 事件类型\n\n事件类型通常为 `meta_event`、`message`、`notice`、`request`。\n\n```python\ntype: str = event.get_type()\n```\n\n### 事件名称\n\n事件名称由适配器定义，通常用于日志记录。\n\n```python\nname: str = event.get_event_name()\n```\n\n### 事件描述\n\n事件描述由适配器定义，通常用于日志记录。\n\n```python\ndescription: str = event.get_event_description()\n```\n\n### 事件日志字符串\n\n事件日志字符串由事件名称和事件描述组成，用于日志记录。\n\n```python\nlog: str = event.get_log_string()\n```\n\n### 事件主体 ID\n\n事件主体 ID 通常为机器人用户 ID。\n\n```python\nuser_id: str = event.get_user_id()\n```\n\n### 事件会话 ID\n\n事件会话 ID 通常为机器人用户 ID 与群聊/频道 ID 组合而成。\n\n```python\nsession_id: str = event.get_session_id()\n```\n\n### 事件消息\n\n如果事件包含消息，则可以通过该方法获取，否则会产生异常。\n\n```python\nmessage: Message = event.get_message()\n```\n\n### 事件纯文本消息\n\n通常为事件消息的纯文本内容，如果事件不包含消息，则会产生异常。\n\n```python\ntext: str = event.get_plaintext()\n```\n\n### 事件是否与机器人有关\n\n由适配器实现的判断，通常将事件目标主体为机器人、消息中包含“@机器人”或以“机器人的昵称”开始视为与机器人有关。\n\n```python\nis_tome: bool = event.is_tome()\n```\n\n## 更多\n\n官方支持的适配器和社区贡献的适配器均可在[商店](/store/adapters)中查看。如果你想要开发自己的适配器，可以参考[开发文档](../developer/adapter-writing.md)。欢迎通过商店发布你的适配器。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/advanced/dependency.mdx",
    "content": "---\nsidebar_position: 6\ndescription: 通过依赖注入获取上下文信息\n\noptions:\n  menu:\n    - category: advanced\n      weight: 70\n---\n\n# 依赖注入\n\nimport Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\n\n在事件处理流程中，事件响应器具有自己独立的上下文，例如：当前的事件、机器人等信息。在 NoneBot 中，这些信息通过依赖注入的方式提供给事件处理函数，可以让代码更加整洁可读、提升复用能力。\n\n在了解如何使用依赖注入获取上下文信息之前，我们需要先了解两个概念：\n\n- `Dependent`：使用依赖注入的函数或其他任意可调用对象。如：事件处理函数、自定义的依赖函数等。\n- `Dependency`：依赖注入的对象。如：当前事件、机器人等。\n\n在之前的文档中，我们已经多次使用了依赖注入来获取事件信息。通过对函数参数依照一定规则填写类型注解，即可获得想要的上下文信息。任何一个事件处理函数在添加到事件处理流程时，都会根据一定规则提前将其解析成一个 `Dependent` 对象，方便运行时进行注入。如果遇到无法解析的参数，将会抛出 `ValueError(\"Unknown parameter\")` 的异常。整个依赖注入系统可以分为两部分：\n\n- 参数解析\n  - 依据一定规则解析函数参数，识别 `Dependency` 依赖。\n  - 生成 `Dependent` 对象。\n- 执行\n  - 根据已经解析的 `Dependency` 依赖，执行调用。\n  - 将所有 `Dependency` 的返回值根据参数名传入并调用 `Dependent` 。\n\n:::danger 警告\n在依赖注入中，类型注解是非常重要的，因为它不仅可以决定依赖注入的对象，还可以触发[重载机制](../appendices/overload.md#重载)。如果类型注解与实际获得数据类型不一致，将会跳过当前 `Dependent` 对象（即事件处理函数）。\n:::\n\n:::tip 提示\n如果对于依赖注入的解析流程有疑问，可以调整[日志等级配置项](../appendices/config.mdx#log-level)为 `TRACE`，查看依赖解析日志。\n:::\n\n## 同步支持\n\n对于依赖注入系统中的 `Dependent` 或者 `Dependency` 对象，均支持同步类型的函数或可调用对象。例如：\n\n```python {6,10}\nfrom nonebot import on_command\nfrom nonebot.params import Depends\n\nmatcher = on_command(\"foo\")\n\ndef dependency() -> str:\n    return \"something\"\n\n@matcher.handle()\ndef _(result: str = Depends(dependency)):\n    ...\n```\n\n## 非依赖参数\n\n在依赖注入解析中，任何无法解析的参数如果带有默认值，将会被视为非依赖参数。这些参数在依赖运行时将不会被注入而使用函数默认值。例如：\n\n```python\nasync def _(foo: str = \"bar\"): ...\n```\n\n## 类型依赖注入\n\n这一类的依赖注入仅需要在函数参数中添加对应的类型注解即可。\n\n### Bot\n\n获取当前事件的 Bot 对象。\n\n通过标注参数为 `Bot` 类型，或者一系列 `Bot` 类型，即可获取到当前事件的 Bot 对象。为兼容性考虑，如果参数名为 `bot` 且无类型注解，也会视为 Bot 依赖注入。\n\nBot 依赖注入支持重载（即：可以标注参数为子类型）且具有[重载优先检查权](../appendices/overload.md#重载)。\n\n<Tabs groupId=\"python\">\n  <TabItem value=\"3.10\" label=\"Python 3.10+\" default>\n\n```python\nfrom nonebot.adapters import Bot\nfrom nonebot.adapters.console import Bot as ConsoleBot\nfrom nonebot.adapters.onebot.v11 import Bot as OneBotV11Bot\n\nasync def _(foo: Bot): ...\nasync def _(foo: ConsoleBot | OneBotV11Bot): ...\nasync def _(bot): ...  # 兼容性处理\n```\n\n  </TabItem>\n  <TabItem value=\"3.9\" label=\"Python 3.9\">\n\n```python\nfrom typing import Union\n\nfrom nonebot.adapters import Bot\nfrom nonebot.adapters.console import Bot as ConsoleBot\nfrom nonebot.adapters.onebot.v11 import Bot as OneBotV11Bot\n\nasync def _(foo: Bot): ...\nasync def _(foo: Union[ConsoleBot, OneBotV11Bot]): ...\nasync def _(bot): ...  # 兼容性处理\n```\n\n  </TabItem>\n</Tabs>\n\n### Event\n\n获取当前事件。\n\n通过标注参数为 `Event` 类型，或者一系列 `Event` 类型，即可获取到当前事件。为兼容性考虑，如果参数名为 `event` 且无类型注解，也会视为 Event 依赖注入。\n\nEvent 依赖注入支持重载（即：可以标注参数为子类型）且具有[重载优先检查权](../appendices/overload.md#重载)。\n\n<Tabs groupId=\"python\">\n  <TabItem value=\"3.10\" label=\"Python 3.10+\" default>\n\n```python\nfrom nonebot.adapters import Event\nfrom nonebot.adapters.onebot.v11 import PrivateMessageEvent, GroupMessageEvent\n\nasync def _(foo: Event): ...\nasync def _(foo: PrivateMessageEvent | GroupMessageEvent): ...\nasync def _(event): ...  # 兼容性处理\n```\n\n  </TabItem>\n  <TabItem value=\"3.9\" label=\"Python 3.9\">\n\n```python\nfrom typing import Union\n\nfrom nonebot.adapters import Event\nfrom nonebot.adapters.onebot.v11 import PrivateMessageEvent, GroupMessageEvent\n\nasync def _(foo: Event): ...\nasync def _(foo: Union[PrivateMessageEvent, GroupMessageEvent]): ...\nasync def _(event): ...  # 兼容性处理\n```\n\n  </TabItem>\n</Tabs>\n\n### State\n\n获取当前[会话状态](../appendices/session-state.md)。\n\n通过标注参数为 `T_State` 类型，即可获取到当前会话状态。为兼容性考虑，如果参数名为 `state` 且无类型注解，也会视为 State 依赖注入。\n\n```python\nfrom nonebot.typing import T_State\n\nasync def _(foo: T_State): ...\n```\n\n### Matcher\n\n获取当前事件响应器实例。常用于使用[事件响应器操作](../appendices/session-control.mdx)。\n\n通过标注参数为 `Matcher` 类型，或者一系列 `Matcher` 类型，即可获取到当前事件。为兼容性考虑，如果参数名为 `matcher` 且无类型注解，也会视为 Matcher 依赖注入。\n\nMatcher 依赖注入支持重载（即：可以标注参数为子类型）且具有[重载优先检查权](../appendices/overload.md#重载)。\n\n```python\nfrom nonebot.matcher import Matcher\n\nasync def _(foo: Matcher): ...\nasync def _(matcher): ...  # 兼容性处理\n```\n\n### Exception\n\n获取事件响应器运行中抛出的异常。该依赖注入目前仅在事件响应器运行后处理 Hook 中可用。\n\n通过标注参数为异常类型，或者一系列异常类型，即可获取到事件响应器运行中抛出的异常。\n\n<Tabs groupId=\"python\">\n  <TabItem value=\"3.10\" label=\"Python 3.10+\" default>\n\n```python {5,8}\nfrom nonebot.message import run_postprocessor\nfrom nonebot.exception import ActionFailed, NetworkError\n\n@run_postprocessor\nasync def _(e: Exception): ...\n\n@run_postprocessor\nasync def _(e: ActionFailed | NetworkError): ...\n```\n\n  </TabItem>\n  <TabItem value=\"3.9\" label=\"Python 3.9\">\n\n```python {6,9}\nfrom typing import Union\nfrom nonebot.message import run_postprocessor\nfrom nonebot.exception import ActionFailed, NetworkError\n\n@run_postprocessor\nasync def _(e: Exception): ...\n\n@run_postprocessor\nasync def _(e: Union[ActionFailed, NetworkError]): ...\n```\n\n  </TabItem>\n</Tabs>\n\n## 子依赖\n\n在依赖注入系统中，我们可以定义一个子依赖，来执行自定义的操作，提高代码复用性以及处理性能。\n\n### 定义子依赖\n\n子依赖使用 `Depends` 标记进行定义，其参数即依赖的函数或可调用对象，同样会被解析为 `Dependent` 对象，将会在依赖注入期间执行。我们来看一个例子：\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {5,15}\nfrom typing import Annotated\n\nfrom nonebot import on_command\nfrom nonebot.adapters import Event\nfrom nonebot.params import Depends\n\ntest = on_command(\"test\")\n\nasync def check(event: Event) -> Event:\n    if event.get_user_id() in BLACKLIST:\n        await test.finish()\n    return event\n\n@test.handle()\nasync def _(event: Annotated[Event, Depends(check)]):\n    ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {3,13}\nfrom nonebot import on_command\nfrom nonebot.adapters import Event\nfrom nonebot.params import Depends\n\ntest = on_command(\"test\")\n\nasync def check(event: Event) -> Event:\n    if event.get_user_id() in BLACKLIST:\n        await test.finish()\n    return event\n\n@test.handle()\nasync def _(event: Event = Depends(check)):\n    ...\n```\n\n  </TabItem>\n</Tabs>\n\n在上面的代码中，我们使用 `Depends` 标记定义了一个子依赖 `check`。它判断事件主体用户是否在黑名单中，如果在，则直接结束事件处理流程。如果不在，则返回事件对象，以便事件处理函数可以继续执行。\n\n通过将 `Depends` 包裹的子依赖作为参数的默认值，我们就可以在执行事件处理函数之前执行子依赖，并将其返回值作为参数传入事件处理函数。子依赖和普通的事件处理函数并没有区别，同样可以使用依赖注入，并且可以返回任何类型的值。但需要注意的是，如果事件处理函数参数的类型注解与子依赖返回值的类型**不一致**，将会触发[重载](../appendices/overload.md)而跳过当前事件处理函数。\n\n特别的，我们可以为 `Dependent` 对象定义一系列前置子依赖，它们会在参数执行前被顺序执行，且返回值将会被忽略，例如：\n\n```python {11}\nfrom nonebot import on_command\nfrom nonebot.adapters import Event\nfrom nonebot.params import Depends\n\ntest = on_command(\"test\")\n\nasync def check(event: Event):\n    if event.get_user_id() in BLACKLIST:\n        await test.finish()\n\n@test.handle(parameterless=[Depends(check)])\nasync def _():\n    ...\n```\n\n### 依赖缓存\n\nNoneBot 在执行子依赖时，会将其返回值缓存起来。当我们在使用子依赖时，`Depends` 具有一个参数 `use_cache`，默认为 `True`。此时在事件处理流程中，多次使用同一个子依赖时，将会使用缓存中的结果而不会重复执行。这在很多情景中非常有用，例如：\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {7}\nimport random\nfrom typing import Annotated\n\nasync def random_result() -> int:\n    return random.randint(1, 100)\n\nasync def _(x: Annotated[int, Depends(random_result)]):\n    print(x)\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {6}\nimport random\n\nasync def random_result() -> int:\n    return random.randint(1, 100)\n\nasync def _(x: int = Depends(random_result)):\n    print(x)\n```\n\n  </TabItem>\n</Tabs>\n\n此时，在同一事件处理流程中，这个随机函数的返回值将会保持一致。如果我们希望每次都重新执行子依赖，可以将 `use_cache` 设置为 `False`。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {7}\nimport random\nfrom typing import Annotated\n\nasync def random_result() -> int:\n    return random.randint(1, 100)\n\nasync def _(x: Annotated[int, Depends(random_result, use_cache=False)]):\n    print(x)\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {6}\nimport random\n\nasync def random_result() -> int:\n    return random.randint(1, 100)\n\nasync def _(x: int = Depends(random_result, use_cache=False)):\n    print(x)\n```\n\n  </TabItem>\n</Tabs>\n\n:::tip 提示\n缓存的生命周期与当前接收到的事件相同。接收到事件后，子依赖在首次执行时缓存，在该事件处理完成后，缓存就会被清除。\n:::\n\n### 类型转换与校验\n\n在依赖注入系统中，我们可以对子依赖的返回值进行自动类型转换与校验。这个功能由 Pydantic 支持，因此我们通过参数类型注解自动使用 Pydantic 支持的类型转换。例如：\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {6,9}\nfrom typing import Annotated\n\nfrom nonebot.params import Depends\nfrom nonebot.adapters import Event\n\ndef get_user_id(event: Event) -> str:\n    return event.get_user_id()\n\nasync def _(user_id: Annotated[int, Depends(get_user_id, validate=True)]):\n    print(user_id)\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {4,7}\nfrom nonebot.params import Depends\nfrom nonebot.adapters import Event\n\ndef get_user_id(event: Event) -> str:\n    return event.get_user_id()\n\nasync def _(user_id: int = Depends(get_user_id, validate=True)):\n    print(user_id)\n```\n\n  </TabItem>\n</Tabs>\n\n在进行类型自动转换的同时，Pydantic 还支持对数据进行更多的限制，如：大于、小于、长度等。使用方法如下：\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {7,10}\nfrom typing import Annotated\n\nfrom pydantic import Field\nfrom nonebot.params import Depends\nfrom nonebot.adapters import Event\n\ndef get_user_id(event: Event) -> str:\n    return event.get_user_id()\n\nasync def _(user_id: Annotated[int, Depends(get_user_id, validate=Field(gt=100))]):\n    print(user_id)\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {5,8}\nfrom pydantic import Field\nfrom nonebot.params import Depends\nfrom nonebot.adapters import Event\n\ndef get_user_id(event: Event) -> str:\n    return event.get_user_id()\n\nasync def _(user_id: int = Depends(get_user_id, validate=Field(gt=100))):\n    print(user_id)\n```\n\n  </TabItem>\n</Tabs>\n\n### 类作为依赖\n\n在前面的事例中，我们使用了函数作为子依赖。实际上，我们还可以使用类作为依赖。当我们在实例化一个类的时候，其实我们就在调用它，类本身也是一个可调用对象。例如：\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {16}\nfrom typing import Annotated\nfrom dataclasses import dataclass\n\nfrom nonebot.params import Depends\nfrom nonebot.adapters import Event\nfrom nonebot.typing import T_State\n\ndef get_context(state: T_State) -> dict:\n    return state.setdefault(\"context\", {})\n\n@dataclass\nclass ClassDependency:\n    event: Event\n    context: dict = Depends(get_context)\n\nasync def _(data: Annotated[ClassDependency, Depends(ClassDependency)]):\n    print(data.event, data.context)\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {15}\nfrom dataclasses import dataclass\n\nfrom nonebot.params import Depends\nfrom nonebot.adapters import Event\nfrom nonebot.typing import T_State\n\ndef get_context(state: T_State) -> dict:\n    return state.setdefault(\"context\", {})\n\n@dataclass\nclass ClassDependency:\n    event: Event\n    context: dict = Depends(get_context)\n\nasync def _(data: ClassDependency = Depends(ClassDependency)):\n    print(data.event, data.context)\n```\n\n  </TabItem>\n</Tabs>\n\n可以看到，我们使用 `dataclass` 定义了一个类。由于这个类的 `__init__` 方法可以被依赖注入系统解析，因此，我们可以将其作为子依赖进行声明。特别地，对于类依赖，`Depends` 的参数可以为空，NoneBot 将会使用参数的类型注解进行解析与推断：\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python\nfrom typing import Annotated\n\nasync def _(data: Annotated[ClassDependency, Depends()]):\n    print(data.event, data.context)\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python\nasync def _(data: ClassDependency = Depends()):\n    print(data.event, data.context)\n```\n\n  </TabItem>\n</Tabs>\n\n### 生成器作为依赖\n\nNoneBot 的依赖注入支持依赖项在事件处理流程结束后进行一些额外的工作，比如数据库 session 或者网络 IO 的关闭，互斥锁的解锁等等。同时，由于[依赖缓存](#依赖缓存)的存在，我们可以通过这种方式来实现共享一个 session 等功能。\n\n要实现上述功能，我们可以用生成器函数作为依赖项，我们用 `yield` 关键字取代 `return` 关键字，并在 `yield` 之后进行额外的工作。\n\n我们可以看下述代码段, 使用 `httpx.AsyncClient` 异步网络 IO，并在事件处理流程中共用一个 client：\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {15}\nfrom typing import Annotated\nfrom collections.abc import AsyncGenerator\n\nimport httpx\nfrom nonebot.params import Depends\n\nasync def get_client() -> AsyncGenerator[httpx.AsyncClient, None]:\n    try:\n        async with httpx.AsyncClient() as client:\n            yield client\n    finally:\n        # 在这里进行额外的工作\n\n\n@test.handle()\nasync def _(x: Annotated[httpx.AsyncClient, Depends(get_client)]):\n    resp = await x.get(\"https://nonebot.dev\")\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {15}\nfrom collections.abc import AsyncGenerator\n\nimport httpx\nfrom nonebot.params import Depends\n\nasync def get_client() -> AsyncGenerator[httpx.AsyncClient, None]:\n    try:\n        async with httpx.AsyncClient() as client:\n            yield client\n    finally:\n        # 在这里进行额外的工作\n\n\n@test.handle()\nasync def _(x: httpx.AsyncClient = Depends(get_client)):\n    resp = await x.get(\"https://nonebot.dev\")\n```\n\n  </TabItem>\n</Tabs>\n\n:::caution 注意\n生成器作为依赖时，其中只能进行一次 `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 内部就使用了这两个装饰器。\n:::\n\n### 可调用对象作为依赖\n\n在 Python 里，为类定义 `__call__` 方法就可以使得这个类的实例成为一个可调用对象。因此，我们也可以将定义了 `__call__` 方法的类的实例作为依赖。事实上，NoneBot 的[内置响应规则](./matcher.md#内置响应规则)就广泛使用了这种方式，以 `is_type` 规则为例：\n\n```python\nfrom nonebot.adapters import Event\n\nclass IsTypeRule:\n    def __init__(self, *types: type[Event]):\n        self.types = types\n\n    async def __call__(self, event: Event) -> bool:\n        return isinstance(event, self.types)\n```\n\n我们在使用 `is_type` 时，即实例化了 `IsTypeRule` 类，然后将实例作为响应规则依赖项传入。\n\n## 其他依赖注入\n\n这一类的依赖注入通常基于子依赖编写，为我们开发者提供更方便的途径获取上下文信息。\n\n### EventType\n\n获取当前事件的类型。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot.params import EventType\n\nasync def _(foo: Annotated[str, EventType()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {3}\nfrom nonebot.params import EventType\n\nasync def _(foo: str = EventType()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### EventMessage\n\n获取当前事件的消息。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {5}\nfrom typing import Annotated\nfrom nonebot.adapters import Message\nfrom nonebot.params import EventMessage\n\nasync def _(foo: Annotated[Message, EventMessage()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {4}\nfrom nonebot.adapters import Message\nfrom nonebot.params import EventMessage\n\nasync def _(foo: Message = EventMessage()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### EventPlainText\n\n获取当前事件的消息纯文本部分。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot.params import EventPlainText\n\nasync def _(foo: Annotated[str, EventPlainText()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {3}\nfrom nonebot.params import EventPlainText\n\nasync def _(foo: str = EventPlainText()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### EventToMe\n\n获取当前事件是否与机器人相关。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot.params import EventToMe\n\nasync def _(foo: Annotated[bool, EventToMe()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {3}\nfrom nonebot.params import EventToMe\n\nasync def _(foo: bool = EventToMe()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### Command\n\n获取当前命令型消息的元组形式命令名。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot.params import Command\n\nasync def _(foo: Annotated[tuple[str, ...], Command()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {4}\nfrom nonebot.params import Command\n\nasync def _(foo: tuple[str, ...] = Command()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n:::tip 提示\n命令详情只能在**触发命令型事件响应器时**获取。如果在事件处理后续流程中获取，则会获取到不同的值。\n:::\n\n### RawCommand\n\n获取当前命令型消息的文本形式命令名。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot.params import RawCommand\n\nasync def _(foo: Annotated[str, RawCommand()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {3}\nfrom nonebot.params import RawCommand\n\nasync def _(foo: str = RawCommand()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n:::tip 提示\n命令详情只能在**触发命令型事件响应器时**获取。如果在事件处理后续流程中获取，则会获取到不同的值。\n:::\n\n### CommandArg\n\n获取命令型消息命令后跟随的参数。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {5}\nfrom typing import Annotated\nfrom nonebot.adapters import Message\nfrom nonebot.params import CommandArg\n\nasync def _(foo: Annotated[Message, CommandArg()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {4}\nfrom nonebot.adapters import Message\nfrom nonebot.params import CommandArg\n\nasync def _(foo: Message = CommandArg()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n:::tip 提示\n命令详情只能在**触发命令型事件响应器时**获取。如果在事件处理后续流程中获取，则会获取到不同的值。\n:::\n\n### CommandStart\n\n获取命令型消息命令前缀。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot.params import CommandStart\n\nasync def _(foo: Annotated[str, CommandStart()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {3}\nfrom nonebot.params import CommandStart\n\nasync def _(foo: str = CommandStart()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n:::tip 提示\n命令详情只能在**触发命令型事件响应器时**获取。如果在事件处理后续流程中获取，则会获取到不同的值。\n:::\n\n### CommandWhitespace\n\n获取命令型消息命令与参数间空白符。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot.params import CommandWhitespace\n\nasync def _(foo: Annotated[str, CommandWhitespace()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {3}\nfrom nonebot.params import CommandWhitespace\n\nasync def _(foo: str = CommandWhitespace()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n:::tip 提示\n命令详情只能在**触发命令型事件响应器时**获取。如果在事件处理后续流程中获取，则会获取到不同的值。\n:::\n\n### ShellCommandArgv\n\n获取 shell 命令解析前的参数列表，列表中可能包含文本字符串和富文本消息段（如：图片）。当词法解析出错的时候，返回值将为 `None`。通过重载机制即可处理两种不同的情况。\n\n<Tabs groupId=\"python\">\n  <TabItem value=\"3.10\" label=\"Python 3.10+\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot import on_shell_command\nfrom nonebot.params import ShellCommandArgv\n\nmatcher = on_shell_command(\"cmd\")\n\n# 解析失败\n@matcher.handle()\nasync def _(foo: Annotated[None, ShellCommandArgv()]): ...\n\n# 解析成功\n@matcher.handle()\nasync def _(foo: Annotated[list[str | MessageSegment], ShellCommandArgv()]): ...\n```\n\n```python {4}\nfrom nonebot import on_shell_command\nfrom nonebot.params import ShellCommandArgv\n\nmatcher = on_shell_command(\"cmd\")\n\n# 解析失败\n@matcher.handle()\nasync def _(foo: None = ShellCommandArgv()): ...\n\n# 解析成功\n@matcher.handle()\nasync def _(foo: list[str | MessageSegment] = ShellCommandArgv()): ...\n```\n\n  </TabItem>\n  <TabItem value=\"3.9\" label=\"Python 3.9\">\n\n```python {4}\nfrom typing import Union, Annotated\nfrom nonebot import on_shell_command\nfrom nonebot.params import ShellCommandArgv\n\nmatcher = on_shell_command(\"cmd\")\n\n# 解析失败\n@matcher.handle()\nasync def _(foo: Annotated[None, ShellCommandArgv()]): ...\n\n# 解析成功\n@matcher.handle()\nasync def _(foo: Annotated[list[Union[str, MessageSegment]], ShellCommandArgv()]): ...\n```\n\n```python {4}\nfrom typing import Union\nfrom nonebot import on_shell_command\nfrom nonebot.params import ShellCommandArgv\n\nmatcher = on_shell_command(\"cmd\")\n\n# 解析失败\n@matcher.handle()\nasync def _(foo: None = ShellCommandArgv()): ...\n\n# 解析成功\n@matcher.handle()\nasync def _(foo: list[Union[str, MessageSegment]] = ShellCommandArgv()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### ShellCommandArgs\n\n获取 shell 命令解析后的参数 Namespace，支持 MessageSegment 富文本（如：图片）。\n\n:::tip 提示\n如果参数解析成功，则为 parser 返回的 Namespace；如果参数解析失败，则为 [`ParserExit`](../api/exception.md#ParserExit) 异常，并携带错误码与错误信息。在前置词法解析失败时，返回值也为 [`ParserExit`](../api/exception.md#ParserExit) 异常。通过重载机制即可处理两种不同的情况。\n\n由于 `ArgumentParser` 在解析到 `--help` 参数时也会抛出异常，这种情况下错误码为 `0` 且错误信息即为帮助信息。\n:::\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {14,22}\nfrom typing import Annotated\n\nfrom nonebot import on_shell_command\nfrom nonebot.exception import ParserExit\nfrom nonebot.params import ShellCommandArgs\nfrom nonebot.rule import Namespace, ArgumentParser\n\nparser = ArgumentParser(\"demo\")\n# parser.add_argument ...\nmatcher = on_shell_command(\"cmd\", parser=parser)\n\n# 解析失败\n@matcher.handle()\nasync def _(foo: Annotated[ParserExit, ShellCommandArgs()]):\n    if foo.status == 0:\n        foo.message  # help message\n    else:\n        foo.message  # error message\n\n# 解析成功\n@matcher.handle()\nasync def _(foo: Annotated[Namespace, ShellCommandArgs()]):\n    arg_dict = vars(foo)\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {12,20}\nfrom nonebot import on_shell_command\nfrom nonebot.exception import ParserExit\nfrom nonebot.params import ShellCommandArgs\nfrom nonebot.rule import Namespace, ArgumentParser\n\nparser = ArgumentParser(\"demo\")\n# parser.add_argument ...\nmatcher = on_shell_command(\"cmd\", parser=parser)\n\n# 解析失败\n@matcher.handle()\nasync def _(foo: ParserExit = ShellCommandArgs()):\n    if foo.status == 0:\n        foo.message  # help message\n    else:\n        foo.message  # error message\n\n# 解析成功\n@matcher.handle()\nasync def _(foo: Namespace = ShellCommandArgs()):\n    arg_dict = vars(foo)\n```\n\n  </TabItem>\n</Tabs>\n\n### RegexMatched\n\n获取正则匹配结果的对象。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {5}\nfrom re import Match\nfrom typing import Annotated\nfrom nonebot.params import RegexMatched\n\nasync def _(foo: Annotated[Match[str], RegexMatched()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {4}\nfrom re import Match\nfrom nonebot.params import RegexMatched\n\nasync def _(foo: Match[str] = RegexMatched()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### RegexStr\n\n获取正则匹配结果的文本。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot.params import RegexStr\n\nasync def _(foo: Annotated[str, RegexStr()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {3}\nfrom nonebot.params import RegexStr\n\nasync def _(foo: str = RegexStr()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### RegexGroup\n\n获取正则匹配结果的 group 元组。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Any, Annotated\nfrom nonebot.params import RegexGroup\n\nasync def _(foo: Annotated[tuple[Any, ...], RegexGroup()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {4}\nfrom typing import Any\nfrom nonebot.params import RegexGroup\n\nasync def _(foo: tuple[Any, ...] = RegexGroup()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### RegexDict\n\n获取正则匹配结果的 group 字典。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Any, Annotated\nfrom nonebot.params import RegexDict\n\nasync def _(foo: Annotated[dict[str, Any], RegexDict()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {4}\nfrom typing import Any\nfrom nonebot.params import RegexDict\n\nasync def _(foo: dict[str, Any] = RegexDict()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### Startswith\n\n获取触发响应器的消息前缀字符串。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot.params import Startswith\n\nasync def _(foo: Annotated[str, Startswith()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {3}\nfrom nonebot.params import Startswith\n\nasync def _(foo: str = Startswith()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### Endswith\n\n获取触发响应器的消息后缀字符串。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot.params import Endswith\n\nasync def _(foo: Annotated[str, Endswith()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {3}\nfrom nonebot.params import Endswith\n\nasync def _(foo: str = Endswith()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### Fullmatch\n\n获取触发响应器的消息字符串。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot.params import Fullmatch\n\nasync def _(foo: Annotated[str, Fullmatch()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {3}\nfrom nonebot.params import Fullmatch\n\nasync def _(foo: str = Fullmatch()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### Keyword\n\n获取触发响应器的关键字字符串。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot.params import Keyword\n\nasync def _(foo: Annotated[str, Keyword()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {3}\nfrom nonebot.params import Keyword\n\nasync def _(foo: str = Keyword()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### Received\n\n获取某次 `receive` 接收的事件。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {7}\nfrom typing import Annotated\n\nfrom nonebot.adapters import Event\nfrom nonebot.params import Received\n\n@matcher.receive(\"id\")\nasync def _(foo: Annotated[Event, Received(\"id\")]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {5}\nfrom nonebot.adapters import Event\nfrom nonebot.params import Received\n\n@matcher.receive(\"id\")\nasync def _(foo: Event = Received(\"id\")): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### LastReceived\n\n获取最近一次 `receive` 接收的事件。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {7}\nfrom typing import Annotated\n\nfrom nonebot.adapters import Event\nfrom nonebot.params import LastReceived\n\n@matcher.receive(\"any\")\nasync def _(foo: Annotated[Event, LastReceived()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {5}\nfrom nonebot.adapters import Event\nfrom nonebot.params import LastReceived\n\n@matcher.receive(\"any\")\nasync def _(foo: Event = LastReceived()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### ReceivePromptResult\n\n获取某次 `receive` 发送提示消息的结果。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {6}\nfrom typing import Any, Annotated\n\nfrom nonebot.params import ReceivePromptResult\n\n@matcher.receive(\"id\", prompt=\"prompt\")\nasync def _(result: Annotated[Any, ReceivePromptResult(\"id\")]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {6}\nfrom typing import Any\n\nfrom nonebot.params import ReceivePromptResult\n\n@matcher.receive(\"id\", prompt=\"prompt\")\nasync def _(result: Any = ReceivePromptResult(\"id\")): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### Arg\n\n获取某次 `got` 接收的参数。如果 `Arg` 参数留空，则使用函数的参数名作为要获取的参数。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {7,8}\nfrom typing import Annotated\n\nfrom nonebot.params import Arg\nfrom nonebot.adapters import Message\n\n@matcher.got(\"key\")\nasync def _(key: Annotated[Message, Arg()]): ...\nasync def _(foo: Annotated[Message, Arg(\"key\")]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {5,6}\nfrom nonebot.params import Arg\nfrom nonebot.adapters import Message\n\n@matcher.got(\"key\")\nasync def _(key: Message = Arg()): ...\nasync def _(foo: Message = Arg(\"key\")): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### ArgStr\n\n获取某次 `got` 接收的参数，并转换为字符串。如果 `Arg` 参数留空，则使用函数的参数名作为要获取的参数。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {6,7}\nfrom typing import Annotated\n\nfrom nonebot.params import ArgStr\n\n@matcher.got(\"key\")\nasync def _(key: Annotated[str, ArgStr()]): ...\nasync def _(foo: Annotated[str, ArgStr(\"key\")]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {4,5}\nfrom nonebot.params import ArgStr\n\n@matcher.got(\"key\")\nasync def _(key: str = ArgStr()): ...\nasync def _(foo: str = ArgStr(\"key\")): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### ArgPlainText\n\n获取某次 `got` 接收的参数的纯文本部分。如果 `Arg` 参数留空，则使用函数的参数名作为要获取的参数。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {6,7}\nfrom typing import Annotated\n\nfrom nonebot.params import ArgPlainText\n\n@matcher.got(\"key\")\nasync def _(key: Annotated[str, ArgPlainText()]): ...\nasync def _(foo: Annotated[str, ArgPlainText(\"key\")]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {4,5}\nfrom nonebot.params import ArgPlainText\n\n@matcher.got(\"key\")\nasync def _(key: str = ArgPlainText()): ...\nasync def _(foo: str = ArgPlainText(\"key\")): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### ArgPromptResult\n\n获取某次 `got` 发送提示消息的结果。如果 `Arg` 参数留空，则使用函数的参数名作为要获取的参数。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {6,7}\nfrom typing import Any, Annotated\n\nfrom nonebot.params import ArgPromptResult\n\n@matcher.got(\"key\", prompt=\"prompt\")\nasync def _(result: Annotated[Any, ArgPromptResult()]): ...\nasync def _(result: Annotated[Any, ArgPromptResult(\"key\")]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {6,7}\nfrom typing import Any\n\nfrom nonebot.params import ArgPromptResult\n\n@matcher.got(\"key\", prompt=\"prompt\")\nasync def _(result: Any = ArgPromptResult()): ...\nasync def _(result: Any = ArgPromptResult(\"key\")): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### PausePromptResult\n\n获取最近一次 `pause` 发送提示消息的结果。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {6}\nfrom typing import Any, Annotated\n\nfrom nonebot.params import PausePromptResult\n\n@matcher.handle()\nasync def _():\n    await matcher.pause(prompt=\"prompt\")\n\n@matcher.handle()\nasync def _(result: Annotated[Any, PausePromptResult()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {6}\nfrom typing import Any\n\nfrom nonebot.params import PausePromptResult\n\n@matcher.handle()\nasync def _():\n    await matcher.pause(prompt=\"prompt\")\n\n@matcher.handle()\nasync def _(result: Any = PausePromptResult()): ...\n```\n\n  </TabItem>\n</Tabs>\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/advanced/driver.md",
    "content": "---\nsidebar_position: 0\ndescription: 选择合适的驱动器运行机器人\n\noptions:\n  menu:\n    - category: advanced\n      weight: 10\n---\n\n# 选择驱动器\n\n驱动器 (Driver) 是机器人运行的基石，它是机器人初始化的第一步，主要负责数据收发。\n\n:::important 提示\n驱动器的选择通常与机器人所使用的协议适配器相关，如果不知道该选择哪个驱动器，可以先阅读相关协议适配器文档说明。\n:::\n\n:::tip 提示\n如何**安装**驱动器请参考[安装驱动器](../tutorial/store.mdx#安装驱动器)。\n:::\n\n## 驱动器类型\n\n驱动器类型大体上可以分为两种：\n\n- `Forward`：即客户端型驱动器，多用于使用 HTTP 轮询，连接 WebSocket 服务器等情形。\n- `Reverse`：即服务端型驱动器，多用于使用 WebHook，接收 WebSocket 客户端连接等情形。\n\n客户端型驱动器可以分为以下两种：\n\n1. 异步发送 HTTP 请求，自定义 `HTTP Method`、`URL`、`Header`、`Body`、`Cookie`、`Proxy`、`Timeout` 等。\n2. 异步建立 WebSocket 连接上下文，自定义 `WebSocket URL`、`Header`、`Cookie`、`Proxy`、`Timeout` 等。\n\n服务端型驱动器目前有：\n\n1. ASGI 应用框架，具有以下功能：\n   - 协议适配器自定义 HTTP 上报地址以及对上报数据处理的回调函数。\n   - 协议适配器自定义 WebSocket 连接请求地址以及对 WebSocket 请求处理的回调函数。\n   - 用户可以向 ASGI 应用添加任何服务端相关功能，如：[添加自定义路由](./routing.md)。\n\n## 配置驱动器\n\n驱动器的配置方法已经在[配置](../appendices/config.mdx)章节中简单进行了介绍，这里将详细介绍驱动器配置的格式。\n\nNoneBot 中的客户端和服务端型驱动器可以相互配合使用，但服务端型驱动器**仅能选择一个**。所有驱动器模块都会包含一个 `Driver` 子类，即驱动器类，他可以作为驱动器单独运行。同时，客户端驱动器模块中还会提供一个 `Mixin` 子类，用于在与其他驱动器配合使用时加载。因此，驱动器配置格式采用特殊语法：`<module>[:<Driver>][+<module>[:<Mixin>]]*`。\n\n其中，`<module>` 代表**驱动器模块路径**；`<Driver>` 代表**驱动器类名**，默认为 `Driver`；`<Mixin>` 代表**驱动器混入类名**，默认为 `Mixin`。即，我们需要选择一个主要驱动器，然后在其基础上配合使用其他驱动器的功能。主要驱动器可以为客户端或服务端类型，但混入类驱动器只能为客户端类型。\n\n特别的，为了简化内置驱动器模块路径，我们可以使用 `~` 符号作为内置驱动器模块路径的前缀，如 `~fastapi` 代表使用内置驱动器 `fastapi`。NoneBot 内置了多个驱动器适配，但需要安装额外依赖才能使用，具体请参考[安装驱动器](../tutorial/store.mdx#安装驱动器)。常见的驱动器配置如下：\n\n```dotenv\nDRIVER=~fastapi\nDRIVER=~aiohttp\nDRIVER=~httpx+~websockets\nDRIVER=~fastapi+~httpx+~websockets\n```\n\n## 获取驱动器\n\n在 NoneBot 框架初始化完成后，我们就可以通过 `get_driver()` 方法获取全局驱动器实例：\n\n```python\nfrom nonebot import get_driver\n\ndriver = get_driver()\n```\n\n## 内置驱动器\n\n### None\n\n**类型：**服务端驱动器\n\nNoneBot 内置的空驱动器，不提供任何收发数据功能，可以在不需要外部网络连接时使用。\n\n```env\nDRIVER=~none\n```\n\n### FastAPI（默认）\n\n**类型：**ASGI 服务端驱动器\n\n> FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints.\n\n[FastAPI](https://fastapi.tiangolo.com/) 是一个易上手、高性能的异步 Web 框架，具有极佳的编写体验。 FastAPI 可以通过类型注解、依赖注入等方式实现输入参数校验、自动生成 API 文档等功能，也可以挂载其他 ASGI、WSGI 应用。\n\n```env\nDRIVER=~fastapi\n```\n\n#### FastAPI 配置项\n\n##### `fastapi_openapi_url`\n\n类型：`str | None`  \n默认值：`None`  \n说明：`FastAPI` 提供的 `OpenAPI` JSON 定义地址，如果为 `None`，则不提供 `OpenAPI` JSON 定义。\n\n##### `fastapi_docs_url`\n\n类型：`str | None`  \n默认值：`None`  \n说明：`FastAPI` 提供的 `Swagger` 文档地址，如果为 `None`，则不提供 `Swagger` 文档。\n\n##### `fastapi_redoc_url`\n\n类型：`str | None`  \n默认值：`None`  \n说明：`FastAPI` 提供的 `ReDoc` 文档地址，如果为 `None`，则不提供 `ReDoc` 文档。\n\n##### `fastapi_include_adapter_schema`\n\n类型：`bool`  \n默认值：`True`  \n说明：`FastAPI` 提供的 `OpenAPI` JSON 定义中是否包含适配器路由的 `Schema`。\n\n##### `fastapi_reload`\n\n:::caution 警告\n不推荐开启该配置项，在 Windows 平台上开启该功能有可能会造成预料之外的影响！替代方案：使用 `nb-cli` 命令行工具以及参数 `--reload` 启动 NoneBot。\n\n```bash\nnb run --reload\n```\n\n开启该功能后，在 uvicorn 运行时（FastAPI 提供的 ASGI 底层，即 reload 功能的实际来源），asyncio 使用的事件循环会被 uvicorn 从默认的 `ProactorEventLoop` 强制切换到 `SelectorEventLoop`。\n\n> 相关信息参考 [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)\n\n后者（`SelectorEventLoop`）在 Windows 平台的可使用性不如前者（`ProactorEventLoop`），包括但不限于\n\n1. 不支持创建子进程\n2. 最多只支持 512 个套接字\n3. ...\n\n> 具体信息参考 [Python 文档](https://docs.python.org/zh-cn/3/library/asyncio-platforms.html#windows)\n\n所以，一些使用了 asyncio 的库因此可能无法正常工作，如：\n\n1. [playwright](https://playwright.dev/python/docs/library#incompatible-with-selectoreventloop-of-asyncio-on-windows)\n\n如果在开启该功能后，原本**正常运行**的代码报错，且打印的异常堆栈信息和 asyncio 有关（异常一般为 `NotImplementedError`），\n你可能就需要考虑相关库对事件循环的支持，以及是否启用该功能。\n:::\n\n类型：`bool`  \n默认值：`False`  \n说明：是否开启 `uvicorn` 的 `reload` 功能，需要在机器人入口文件提供 ASGI 应用路径。\n\n```python title=bot.py\napp = nonebot.get_asgi()\nnonebot.run(app=\"bot:app\")\n```\n\n##### `fastapi_reload_dirs`\n\n类型：`List[str] | None`  \n默认值：`None`  \n说明：重载监控文件夹列表，默认为 uvicorn 默认值\n\n##### `fastapi_reload_delay`\n\n类型：`float | None`  \n默认值：`None`  \n说明：重载延迟，默认为 uvicorn 默认值\n\n##### `fastapi_reload_includes`\n\n类型：`List[str] | None`  \n默认值：`None`  \n说明：要监听的文件列表，支持 glob pattern，默认为 uvicorn 默认值\n\n##### `fastapi_reload_excludes`\n\n类型：`List[str] | None`  \n默认值：`None`  \n说明：不要监听的文件列表，支持 glob pattern，默认为 uvicorn 默认值\n\n##### `fastapi_extra`\n\n类型：`Dist[str, Any]`  \n默认值：`{}`  \n说明：传递给 `FastAPI` 的其他参数\n\n### Quart\n\n**类型：**ASGI 服务端驱动器\n\n> Quart is an asyncio reimplementation of the popular Flask microframework API.\n\n[Quart](https://quart.palletsprojects.com/) 是一个类 Flask 的异步版本，拥有与 Flask 非常相似的接口和使用方法。\n\n```env\nDRIVER=~quart\n```\n\n#### Quart 配置项\n\n##### `quart_reload`\n\n:::caution 警告\n不推荐开启该配置项，在 Windows 平台上开启该功能有可能会造成预料之外的影响！替代方案：使用 `nb-cli` 命令行工具以及参数 `--reload` 启动 NoneBot。\n\n```bash\nnb run --reload\n```\n\n:::\n\n类型：`bool`  \n默认值：`False`  \n说明：是否开启 `uvicorn` 的 `reload` 功能，需要在机器人入口文件提供 ASGI 应用路径。\n\n```python title=bot.py\napp = nonebot.get_asgi()\nnonebot.run(app=\"bot:app\")\n```\n\n##### `quart_reload_dirs`\n\n类型：`List[str] | None`  \n默认值：`None`  \n说明：重载监控文件夹列表，默认为 uvicorn 默认值\n\n##### `quart_reload_delay`\n\n类型：`float | None`  \n默认值：`None`  \n说明：重载延迟，默认为 uvicorn 默认值\n\n##### `quart_reload_includes`\n\n类型：`List[str] | None`  \n默认值：`None`  \n说明：要监听的文件列表，支持 glob pattern，默认为 uvicorn 默认值\n\n##### `quart_reload_excludes`\n\n类型：`List[str] | None`  \n默认值：`None`  \n说明：不要监听的文件列表，支持 glob pattern，默认为 uvicorn 默认值\n\n##### `quart_extra`\n\n类型：`Dist[str, Any]`  \n默认值：`{}`  \n说明：传递给 `Quart` 的其他参数\n\n### HTTPX\n\n**类型：**HTTP 客户端驱动器\n\n:::caution 注意\n本驱动器仅支持 HTTP 请求，不支持 WebSocket 连接请求。\n:::\n\n> [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.\n\n```env\nDRIVER=~httpx\n```\n\n### websockets\n\n**类型：**WebSocket 客户端驱动器\n\n:::caution 注意\n本驱动器仅支持 WebSocket 连接请求，不支持 HTTP 请求。\n:::\n\n> [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.\n\n```env\nDRIVER=~websockets\n```\n\n### AIOHTTP\n\n**类型：**HTTP/WebSocket 客户端驱动器\n\n> [AIOHTTP](https://docs.aiohttp.org/): Asynchronous HTTP Client/Server for asyncio and Python.\n\n```env\nDRIVER=~aiohttp\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/advanced/matcher-provider.md",
    "content": "---\nsidebar_position: 10\ndescription: 自定义事件响应器存储\n\noptions:\n  menu:\n    - category: advanced\n      weight: 110\n---\n\n# 事件响应器存储\n\n事件响应器是 NoneBot 处理事件的核心，它们默认存储在一个字典中。在进入会话状态后，事件响应器将会转为临时响应器，作为最高优先级同样存储于该字典中。因此，事件响应器的存储类似于会话存储，它决定了整个 NoneBot 对事件的处理行为。\n\nNoneBot 默认使用 Python 的字典将事件响应器存储于内存中，但是我们也可以自定义事件响应器存储，将事件响应器存储于其他地方，例如 Redis 等。这样我们就可以实现持久化、在多实例间共享会话状态等功能。\n\n## 编写存储提供者\n\n事件响应器的存储提供者 `MatcherProvider` 抽象类继承自 `MutableMapping[int, list[type[Matcher]]]`，即以优先级为键，以事件响应器列表为值的映射。我们可以方便地进行逐优先级事件传播。\n\n编写一个自定义的存储提供者，只需要继承并实现 `MatcherProvider` 抽象类：\n\n```python\nfrom nonebot.matcher import MatcherProvider\n\nclass CustomProvider(MatcherProvider):\n    ...\n```\n\n## 设置存储提供者\n\n我们可以通过 `matchers.set_provider` 方法设置存储提供者：\n\n```python {3}\nfrom nonebot.matcher import matchers\n\nmatchers.set_provider(CustomProvider)\n\nassert isinstance(matchers.provider, CustomProvider)\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/advanced/matcher.md",
    "content": "---\nsidebar_position: 5\ndescription: 事件响应器组成与内置响应规则\n\noptions:\n  menu:\n    - category: advanced\n      weight: 60\n---\n\n# 事件响应器进阶\n\n在[指南](../tutorial/matcher.md)与[深入](../appendices/rule.md)中，我们已经介绍了事件响应器的基本用法以及响应规则、权限控制等功能。在这一节中，我们将介绍事件响应器的组成，内置的响应规则，与第三方响应规则拓展。\n\n:::tip 提示\n事件响应器允许继承，你可以通过直接继承 `Matcher` 类来创建一个新的事件响应器。\n:::\n\n## 事件响应器组成\n\n### 事件响应器类型\n\n事件响应器类型 `type` 即是该响应器所要响应的事件类型，只有在接收到的事件类型与该响应器的类型相同时，才会触发该响应器。如果类型为空字符串 `\"\"`，则响应器将会响应所有类型的事件。事件响应器类型的检查在所有其他检查（权限控制、响应规则）之前进行。\n\nNoneBot 内置了四种常用事件类型：`meta_event`、`message`、`notice`、`request`，分别对应元事件、消息、通知、请求。通常情况下，协议适配器会将事件合理地分类至这四种类型中。如果有其他类型的事件需要响应，可以自行定义新的类型。\n\n### 事件触发权限\n\n事件触发权限 `permission` 是一个 `Permission` 对象，这在[权限控制](../appendices/permission.mdx)一节中已经介绍过。事件触发权限会在事件响应器的类型检查通过后进行检查，如果权限检查通过，则执行响应规则检查。\n\n### 事件响应规则\n\n事件响应规则 `rule` 是一个 `Rule` 对象，这在[响应规则](../appendices/rule.md)一节中已经介绍过。事件响应器的响应规则会在事件响应器的权限检查通过后进行匹配，如果响应规则检查通过，则触发该响应器。\n\n### 响应优先级\n\n响应优先级 `priority` 是一个正整数，用于指定响应器的优先级。响应器的优先级越小，越先被触发。如果响应器的优先级相同，则按照响应器的注册顺序进行触发。\n\n### 阻断\n\n阻断 `block` 是一个布尔值，用于指定响应器是否阻断事件的传播。如果阻断为 `True`，则在该响应器被触发后，事件将不会再传播给其他下一优先级的响应器。\n\nNoneBot 内置的事件响应器中，所有非 `command` 规则的 `message` 类型的事件响应器都会阻断事件传递，其他则不会。\n\n在部分情况中，可以使用 [`stop_propagation`](../appendices/session-control.mdx#stop_propagation) 方法动态阻止事件传播，该方法需要 handler 在参数中获取 matcher 实例后调用方法。\n\n### 有效期\n\n事件响应器的有效期分为 `temp` 和 `expire_time` 。`temp` 是一个布尔值，用于指定响应器是否为临时响应器。如果为 `True`，则该响应器在被触发后会被自动销毁。`expire_time` 是一个 `datetime` 对象，用于指定响应器的过期时间。如果 `expire_time` 不为 `None`，则在该时间点后，该响应器会被自动销毁。\n\n### 默认状态\n\n事件响应器的默认状态 `default_state` 是一个 `dict` 对象，用于指定响应器的默认状态。在响应器被触发时，响应器将会初始化默认状态然后开始执行事件处理流程。\n\n## 基本辅助函数\n\nNoneBot 为四种类型的事件响应器提供了五个基本的辅助函数：\n\n- `on`：创建任何类型的事件响应器。\n- `on_metaevent`：创建元事件响应器。\n- `on_message`：创建消息事件响应器。\n- `on_request`：创建请求事件响应器。\n- `on_notice`：创建通知事件响应器。\n\n除了 `on` 函数具有一个 `type` 参数外，其余参数均相同：\n\n- `rule`：响应规则，可以是 `Rule` 对象或者 `RuleChecker` 函数。\n- `permission`：事件触发权限，可以是 `Permission` 对象或者 `PermissionChecker` 函数。\n- `handlers`：事件处理函数列表。\n- `temp`：是否为临时响应器。\n- `expire_time`：响应器的过期时间。\n- `priority`：响应器的优先级。\n- `block`：是否阻断事件传播。\n- `state`：响应器的默认状态。\n\n在消息类型的事件响应器的基础上，NoneBot 还内置了一些常用的响应规则，并结合为辅助函数来方便我们快速创建指定功能的响应器。下面我们逐个介绍。\n\n## 内置响应规则\n\n:::tip\n响应规则的使用方法可以参考 [深入 - 响应规则](../appendices/rule.md)。\n:::\n\n### `startswith`\n\n`startswith` 响应规则用于匹配消息纯文本部分的开头是否与指定字符串（或一系列字符串）相同。可选参数 `ignorecase` 用于指定是否忽略大小写，默认为 `False`。\n\n例如，我们可以创建一个匹配消息开头为 `!` 或者 `/` 的规则：\n\n```python\nfrom nonebot.rule import startswith\n\nrule = startswith((\"!\", \"/\"), ignorecase=False)\n```\n\n也可以直接使用辅助函数新建一个响应器：\n\n```python\nfrom nonebot import on_startswith\n\nmatcher = on_startswith((\"!\", \"/\"), ignorecase=False)\n```\n\n### `endswith`\n\n`endswith` 响应规则用于匹配消息纯文本部分的结尾是否与指定字符串（或一系列字符串）相同。可选参数 `ignorecase` 用于指定是否忽略大小写，默认为 `False`。\n\n例如，我们可以创建一个匹配消息结尾为 `.` 或者 `。` 的规则：\n\n```python\nfrom nonebot.rule import endswith\n\nrule = endswith((\".\", \"。\"), ignorecase=False)\n```\n\n也可以直接使用辅助函数新建一个响应器：\n\n```python\nfrom nonebot import on_endswith\n\nmatcher = on_endswith((\".\", \"。\"), ignorecase=False)\n```\n\n### `fullmatch`\n\n`fullmatch` 响应规则用于匹配消息纯文本部分是否与指定字符串（或一系列字符串）完全相同。可选参数 `ignorecase` 用于指定是否忽略大小写，默认为 `False`。\n\n例如，我们可以创建一个匹配消息为 `ping` 或者 `pong` 的规则：\n\n```python\nfrom nonebot.rule import fullmatch\n\nrule = fullmatch((\"ping\", \"pong\"), ignorecase=False)\n```\n\n也可以直接使用辅助函数新建一个响应器：\n\n```python\nfrom nonebot import on_fullmatch\n\nmatcher = on_fullmatch((\"ping\", \"pong\"), ignorecase=False)\n```\n\n### `keyword`\n\n`keyword` 响应规则用于匹配消息纯文本部分是否包含指定字符串（或一系列字符串）。\n\n例如，我们可以创建一个匹配消息中包含 `hello` 或者 `hi` 的规则：\n\n```python\nfrom nonebot.rule import keyword\n\nrule = keyword(\"hello\", \"hi\")\n```\n\n也可以直接使用辅助函数新建一个响应器：\n\n```python\nfrom nonebot import on_keyword\n\nmatcher = on_keyword({\"hello\", \"hi\"})\n```\n\n### `command`\n\n`command` 是最常用的响应规则，它用于匹配消息是否为命令。它会根据配置中的 [Command Start 和 Command Separator](../appendices/config.mdx#command-start-和-command-separator) 来判断消息是否为命令。\n\n例如，当我们配置了 `Command Start` 为 `/`，`Command Separator` 为 `.` 时：\n\n```python\nfrom nonebot.rule import command\n\n# 匹配 \"/help\" 或者 \"/帮助\" 开头的消息\nrule = command(\"help\", \"帮助\")\n# 匹配 \"/help.cmd\" 开头的消息\nrule = command((\"help\", \"cmd\"))\n```\n\n也可以直接使用辅助函数新建一个响应器：\n\n```python\nfrom nonebot import on_command\n\nmatcher = on_command(\"help\", aliases={\"帮助\"})\n```\n\n此外，`command` 响应规则默认允许消息命令与参数间不加空格，如果需要严格匹配命令与参数间的空白符，可以使用 `command` 函数的 `force_whitespace` 参数。`force_whitespace` 参数可以是 bool 类型或者具体的字符串，默认为 `False`。如果为 `True`，则命令与参数间必须有任意个数的空白符；如果为字符串，则命令与参数间必须有且与给定字符串一致的空白符。\n\n```python\nrule = command(\"help\", force_whitespace=True)\nrule = command(\"help\", force_whitespace=\" \")\n```\n\n命令解析后的结果可以通过 [`Command`](./dependency.mdx#command)、[`RawCommand`](./dependency.mdx#rawcommand)、[`CommandArg`](./dependency.mdx#commandarg)、[`CommandStart`](./dependency.mdx#commandstart)、[`CommandWhitespace`](./dependency.mdx#commandwhitespace) 依赖注入获取。\n\n### `shell_command`\n\n`shell_command` 响应规则用于匹配类 shell 命令形式的消息。它首先与 [`command`](#command) 响应规则一样进行命令匹配，如果匹配成功，则会进行进一步的参数解析。参数解析采用 `argparse` 标准库进行，在此基础上添加了消息序列 `Message` 支持。\n\n例如，我们可以创建一个匹配 `/cmd` 命令并且带有 `-v` 选项与默认 `-h` 帮助选项的规则：\n\n```python\nfrom nonebot.rule import shell_command, ArgumentParser\n\nparser = ArgumentParser()\nparser.add_argument(\"-v\", \"--verbose\", action=\"store_true\")\n\nrule = shell_command(\"cmd\", parser=parser)\n```\n\n更多关于 `argparse` 的使用方法请参考 [argparse 文档](https://docs.python.org/zh-cn/3/library/argparse.html)。我们也可以选择不提供 `parser` 参数，这样 `shell_command` 将不会解析参数，但会提供参数列表 `argv`。\n\n直接使用辅助函数新建一个响应器：\n\n```python\nfrom nonebot import on_shell_command\nfrom nonebot.rule import ArgumentParser\n\nparser = ArgumentParser()\nparser.add_argument(\"-v\", \"--verbose\", action=\"store_true\")\n\nmatcher = on_shell_command(\"cmd\", parser=parser)\n```\n\n参数解析后的结果可以通过 [`ShellCommandArgv`](./dependency.mdx#shellcommandargv)、[`ShellCommandArgs`](./dependency.mdx#shellcommandargs) 依赖注入获取。\n\n### `regex`\n\n`regex` 响应规则用于匹配消息是否与指定正则表达式匹配。\n\n:::tip 提示\n正则表达式匹配使用 search 而非 match，如需从头匹配请使用 `r\"^xxx\"` 模式来确保匹配开头。\n:::\n\n例如，我们可以创建一个匹配消息中包含字母并且忽略大小写的规则：\n\n```python\nfrom nonebot.rule import regex\n\nrule = regex(r\"[a-z]+\", flags=re.IGNORECASE)\n```\n\n也可以直接使用辅助函数新建一个响应器：\n\n```python\nfrom nonebot import on_regex\n\nmatcher = on_regex(r\"[a-z]+\", flags=re.IGNORECASE)\n```\n\n正则匹配后的结果可以通过 [`RegexStr`](./dependency.mdx#regexstr)、[`RegexGroup`](./dependency.mdx#regexgroup)、[`RegexDict`](./dependency.mdx#regexdict) 依赖注入获取。\n\n### `to_me`\n\n`to_me` 响应规则用于匹配事件是否与机器人相关。\n\n例如：\n\n```python\nfrom nonebot.rule import to_me\n\nrule = to_me()\n```\n\n### `is_type`\n\n`is_type` 响应规则用于匹配事件类型是否为指定类型（或者一系列类型）。\n\n例如，我们可以创建一个匹配 OneBot v11 私聊和群聊消息事件的规则：\n\n```python\nfrom nonebot.rule import is_type\nfrom nonebot.adapters.onebot.v11 import PrivateMessageEvent, GroupMessageEvent\n\nrule = is_type(PrivateMessageEvent, GroupMessageEvent)\n```\n\n## 响应器组\n\n为了更方便的管理一系列功能相近的响应器，NoneBot 提供了两种响应器组，它们可以帮助我们进行响应器的统一管理。\n\n### `CommandGroup`\n\n`CommandGroup` 可以用于管理一系列具有相同前置命令的子命令响应器。\n\n例如，我们创建 `/cmd`、`/cmd.sub`、`/cmd.help` 三个命令，他们具有相同的优先级：\n\n```python\nfrom nonebot import CommandGroup\n\ngroup = CommandGroup(\"cmd\", priority=10)\n\ncmd = group.command(tuple())\nsub_cmd = group.command(\"sub\")\nhelp_cmd = group.command(\"help\")\n```\n\n命令别名 aliases 默认不会添加 `CommandGroup` 设定的前缀，如果需要为 aliases 添加前缀，可以添加 `prefix_aliases=True` 参数:\n\n```python\nfrom nonebot import CommandGroup\n\ngroup = CommandGroup(\"cmd\", prefix_aliases=True)\n\ncmd = group.command(tuple())\nhelp_cmd = group.command(\"help\", aliases={\"帮助\"})\n```\n\n这样就能成功匹配 `/cmd`、`/cmd.help`、`/cmd.帮助` 命令。如果未设置，将默认匹配 `/cmd`、`/cmd.help`、`/帮助` 命令。\n\n### `MatcherGroup`\n\n`MatcherGroup` 可以用于管理一系列具有相同属性的响应器。\n\n例如，我们创建一个具有相同响应规则的响应器组：\n\n```python\nfrom nonebot.rule import to_me\nfrom nonebot import MatcherGroup\n\ngroup = MatcherGroup(rule=to_me())\n\nmatcher1 = group.on_message()\nmatcher2 = group.on_message()\n```\n\n## 第三方响应规则\n\n### Alconna\n\n[`nonebot-plugin-alconna`](https://github.com/nonebot/plugin-alconna) 是一类提供了拓展响应规则的插件。\n该插件使用 [Alconna](https://github.com/ArcletProject/Alconna) 作为命令解析器，\n是一个简单、灵活、高效的命令参数解析器, 并且不局限于解析命令式字符串。\n\n该插件提供了一类新的事件响应器辅助函数 `on_alconna`，以及 `AlconnaResult` 等依赖注入函数。\n\n基于 `Alconna` 的特性，该插件同时提供了一系列便捷的消息段标注。\n标注可用于在 `Alconna` 中匹配消息中除 text 外的其他消息段，也可用于快速创建各适配器下的消息段。所有标注位于 `nonebot_plugin_alconna.adapters` 中。\n\n该插件同时通过提供 `UniMessage` (通用消息模型) 实现了**跨平台接收和发送消息**的功能。\n\n详情请阅读最佳实践中的 [命令解析拓展](../best-practice/alconna/README.mdx) 章节。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/advanced/plugin-info.md",
    "content": "---\nsidebar_position: 2\ndescription: 填写与获取插件相关的信息\n\noptions:\n  menu:\n    - category: advanced\n      weight: 30\n---\n\n# 插件信息\n\nNoneBot 是一个插件化的框架，可以通过加载插件来扩展功能。同时，我们也可以通过 NoneBot 的插件系统来获取相关信息，例如插件的名称、使用方法，用于收集帮助信息等。下面我们将介绍如何为插件添加元数据，以及如何获取插件信息。\n\n## 插件元数据\n\n在 NoneBot 中，插件 [`Plugin`](../api/plugin/model.md#Plugin) 对象中存储了插件系统所需要的一系列信息。包括插件的索引名称、插件模块、插件中的事件响应器、插件父子关系等。通常，只有插件开发者才需要关心这些信息，而插件使用者或者机器人用户想要看到的是插件使用方法等帮助信息。因此，我们可以为插件添加插件元数据 `PluginMetadata`，它允许插件开发者为插件添加一些额外的信息。这些信息编写于插件模块的顶层，可以直接通过源码查看，或者通过 NoneBot 插件系统获取收集到的信息，通过其他方式发送给机器人用户等。\n\n现在，假设我们有一个插件 `example`, 它的模块结构如下：\n\n```tree {4-6} title=Project\n📦 awesome-bot\n├── 📂 awesome_bot\n│   └── 📂 plugins\n|       └── 📂 example\n|           ├── 📜 __init__.py\n|           └── 📜 config.py\n├── 📜 pyproject.toml\n└── 📜 README.md\n```\n\n我们需要在插件顶层模块 `example/__init__.py` 中添加插件元数据，如下所示：\n\n```python {1,5-12} title=example/__init__.py\nfrom nonebot.plugin import PluginMetadata\n\nfrom .config import Config\n\n__plugin_meta__ = PluginMetadata(\n    name=\"示例插件\",\n    description=\"这是一个示例插件\",\n    usage=\"没什么用\",\n    type=\"application\",\n    config=Config,\n    extra={},\n)\n```\n\n我们可以看到，插件元数据 `PluginMetadata` 有三个基本属性：插件名称、插件描述、插件使用方法。除此之外，还有几个可选的属性（具体填写见[发布插件](../developer/plugin-publishing.mdx#插件元数据)章节）：\n\n- `type`：插件类别，发布插件必填。当前有效类别有：`library`（为其他插件编写提供功能），`application`（向机器人用户提供功能）；\n- `homepage`：插件项目主页，发布插件必填；\n- `config`：插件的[配置类](../appendices/config.mdx#插件配置)，发布插件时如有配置类则必须填写；\n- `supported_adapters`：支持的适配器模块名集合，若插件只使用了 NoneBot 基本抽象，应显式填写 `None`；\n- `extra`：一个字典，可以用于存储任意信息。其他插件可以通过约定 `extra` 字典的键名来达成收集某些特殊信息的目的。\n\n请注意，这里的**插件名称**是供使用者或机器人用户查看的人类可读名称，与插件索引名称无关。**插件索引名称（插件模块名称）**仅用于 NoneBot 插件系统**内部索引**。\n\n## 获取插件信息\n\nNoneBot 提供了多种获取插件对象的方法，例如获取当前所有已导入的插件：\n\n```python\nimport nonebot\n\nplugins: set[Plugin] = nonebot.get_loaded_plugins()\n```\n\n也可以通过插件索引名称获取插件对象：\n\n```python\nimport nonebot\n\nplugin: Plugin | None = nonebot.get_plugin(\"example\")\n```\n\n或者通过模块路径获取插件对象：\n\n```python\nimport nonebot\n\nplugin: Plugin | None = nonebot.get_plugin_by_module_name(\"awesome_bot.plugins.example\")\n```\n\n如果需要获取所有当前声明的插件名称（可能还未加载），可以使用 `get_available_plugin_names` 函数：\n\n```python\nimport nonebot\n\nplugin_names: set[str] = nonebot.get_available_plugin_names()\n```\n\n插件对象 `Plugin` 中包含了多个属性：\n\n- `name`：插件索引名称\n- `module`：插件模块\n- `module_name`：插件模块路径\n- `manager`：插件管理器\n- `matcher`：插件中定义的事件响应器\n- `parent_plugin`：插件的父插件\n- `sub_plugins`：插件的子插件集合\n- `metadata`：插件元数据\n\n通过这些属性以及插件元数据，我们就可以收集所需要的插件信息了。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/advanced/plugin-nesting.md",
    "content": "---\nsidebar_position: 3\ndescription: 编写与加载嵌套插件\n\noptions:\n  menu:\n    - category: advanced\n      weight: 40\n---\n\n# 嵌套插件\n\nNoneBot 支持嵌套插件，即一个插件可以包含其他插件。通过这种方式，我们可以将一个大型插件拆分成多个功能子插件，使得插件更加清晰、易于维护。我们可以直接在插件中使用 NoneBot 加载插件的方法来加载子插件。\n\n## 创建嵌套插件\n\n我们可以在使用 `nb-cli` 命令[创建插件](../tutorial/create-plugin.md#创建插件)时，选择直接通过模板创建一个嵌套插件：\n\n```bash\n$ nb plugin create\n[?] 插件名称: parent\n[?] 使用嵌套插件? (y/N) Y\n[?] 输出目录: awesome_bot/plugins\n```\n\n或者使用 `nb plugin create --sub-plugin` 选项直接创建一个嵌套插件。\n\n## 已有插件\n\n如果你已经有一个插件，想要在其中嵌套加载子插件，可以在插件的 `__init__.py` 中添加如下代码：\n\n```python title=parent/__init__.py\nimport nonebot\nfrom pathlib import Path\n\nsub_plugins = nonebot.load_plugins(\n    str(Path(__file__).parent.joinpath(\"plugins\").resolve())\n)\n```\n\n这样，`parent` 插件就会加载 `parent/plugins` 目录下的所有插件。NoneBot 会正确识别这些插件的父子关系，你可以在 `parent` 的插件信息中看到这些子插件的信息，也可以在子插件信息中看到它们的父插件信息。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/advanced/requiring.md",
    "content": "---\nsidebar_position: 4\ndescription: 使用其他插件提供的功能\n\noptions:\n  menu:\n    - category: advanced\n      weight: 50\n---\n\n# 跨插件访问\n\nNoneBot 插件化系统的设计使得插件之间可以功能独立、各司其职，我们可以更好地维护和扩展插件。但是，有时候我们可能需要在不同插件之间调用功能。NoneBot 生态中就有一类插件，它们专为其他插件提供功能支持，如：[定时任务插件](../best-practice/scheduler.md)、[数据存储插件](../best-practice/data-storing.md)等。这时候我们就需要在插件之间进行跨插件访问。\n\n## 插件跟踪\n\n由于 NoneBot 插件系统通过 [Import Hooks](https://docs.python.org/3/reference/import.html#import-hooks) 的方式实现插件加载与跟踪管理，因此我们**不能**在 NoneBot 跟踪插件前进行模块 import，这会导致插件加载失败。即，我们不能在使用 NoneBot 提供的加载插件方法前，直接使用 `import` 语句导入插件。\n\n对于在项目目录下的插件，我们通常直接使用 `load_from_toml` 等方法一次性加载所有插件。由于这些插件已经被声明，即便插件导入顺序不同，NoneBot 也能正确跟踪插件。此时，我们不需要对跨插件访问进行特殊处理。但当我们使用了外部插件，如果没有事先声明或加载插件，NoneBot 并不会将其当作插件进行跟踪，可能会出现意料之外的错误出现。\n\n简单来说，我们必须在 `import` 外部插件之前，确保依赖的外部插件已经被声明或加载。\n\n## 插件依赖声明\n\nNoneBot 提供了一种方法来确保我们依赖的插件已经被正确加载，即使用 `require` 函数。通过 `require` 函数，我们可以在当前插件中声明依赖的插件，NoneBot 会在加载当前插件时，检查依赖的插件是否已经被加载，如果没有，会尝试优先加载依赖的插件。\n\n假设我们有一个插件 `a` 依赖于插件 `b`，我们可以在插件 `a` 中使用 `require` 函数声明其依赖于插件 `b`：\n\n```python {3} title=a/__init__.py\nfrom nonebot import require\n\nrequire(\"b\")\n\nfrom b import some_function\n```\n\n其中，`require` 函数的参数为插件索引名称或者外部插件的模块名称。在完成依赖声明后，我们可以在插件 `a` 中直接导入插件 `b` 所提供的功能。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/advanced/routing.md",
    "content": "---\nsidebar_position: 9\ndescription: 添加服务端路由规则\n\noptions:\n  menu:\n    - category: advanced\n      weight: 100\n---\n\n# 添加路由\n\n在[驱动器](./driver.md)一节中，我们了解了驱动器的两种类型。既然驱动器可以作为服务端运行，那么我们就可以向驱动器添加路由规则，从而实现自定义的 API 接口等功能。在添加路由规则时，我们需要注意驱动器的类型，详情可以参考[选择驱动器](./driver.md#配置驱动器)。\n\nNoneBot 中，我们可以通过两种途径向 ASGI 驱动器添加路由规则：\n\n1. 通过 NoneBot 的兼容层建立路由规则。\n2. 直接向 ASGI 应用添加路由规则。\n\n这两种途径各有优劣，前者可以在各种服务端型驱动器下运行，但并不能直接使用 ASGI 应用框架提供的特性与功能；后者直接使用 ASGI 应用，更自由、功能完整，但只能在特定类型驱动器下运行。\n\n在向驱动器添加路由规则时，我们需要注意驱动器是否为服务端类型，我们可以通过以下方式判断：\n\n```python\nfrom nonebot import get_driver\nfrom nonebot.drivers import ASGIMixin\n\n# highlight-next-line\ncan_use = isinstance(get_driver(), ASGIMixin)\n```\n\n## 通过兼容层添加路由\n\nNoneBot 兼容层定义了两个数据类 `HTTPServerSetup` 和 `WebSocketServerSetup`，分别用于定义 HTTP 服务端和 WebSocket 服务端的路由规则。\n\n### HTTP 路由\n\n`HTTPServerSetup` 具有四个属性：\n\n- `path`：路由路径，不支持特殊占位表达式。类型为 `URL`。\n- `method`：请求方法。类型为 `str`。\n- `name`：路由名称，不可重复。类型为 `str`。\n- `handle_func`：路由处理函数。类型为 `Callable[[Request], Awaitable[Response]]`。\n\n例如，我们添加一个 `/hello` 的路由，当请求方法为 `GET` 时，返回 `200 OK` 以及返回体信息：\n\n```python\nfrom nonebot import get_driver\nfrom nonebot.drivers import URL, Request, Response, ASGIMixin, HTTPServerSetup\n\nasync def hello(request: Request) -> Response:\n    return Response(200, content=\"Hello, world!\")\n\nif isinstance((driver := get_driver()), ASGIMixin):\n    driver.setup_http_server(\n        HTTPServerSetup(\n            path=URL(\"/hello\"),\n            method=\"GET\",\n            name=\"hello\",\n            handle_func=hello,\n        )\n    )\n```\n\n对于 `Request` 和 `Response` 的详细信息，可以参考 [API 文档](../api/drivers/index.md)。\n\n### WebSocket 路由\n\n`WebSocketServerSetup` 具有三个属性：\n\n- `path`：路由路径，不支持特殊占位表达式。类型为 `URL`。\n- `name`：路由名称，不可重复。类型为 `str`。\n- `handle_func`：路由处理函数。类型为 `Callable[[WebSocket], Awaitable[Any]]`。\n\n例如，我们添加一个 `/ws` 的路由，发送所有接收到的数据：\n\n```python\nfrom nonebot import get_driver\nfrom nonebot.drivers import URL, ASGIMixin, WebSocket, WebSocketServerSetup\n\nasync def ws_handler(ws: WebSocket):\n    await ws.accept()\n    try:\n      while True:\n          data = await ws.receive()\n          await ws.send(data)\n    except WebSocketClosed as e:\n        # handle closed\n        ...\n    finally:\n        with contextlib.suppress(Exception):\n            await websocket.close()\n        # do some cleanup\n\nif isinstance((driver := get_driver()), ASGIMixin):\n    driver.setup_websocket_server(\n        WebSocketServerSetup(\n            path=URL(\"/ws\"),\n            name=\"ws\",\n            handle_func=ws_handler,\n        )\n    )\n```\n\n对于 `WebSocket` 的详细信息，可以参考 [API 文档](../api/drivers/index.md)。\n\n## 使用 ASGI 应用添加路由\n\n### 获取 ASGI 应用\n\nNoneBot 服务端类型的驱动器具有两个属性 `server_app` 和 `asgi`，分别对应驱动框架应用和 ASGI 应用。通常情况下，这两个应用是同一个对象。我们可以通过 `get_app()` 方法快速获取：\n\n```python\nimport nonebot\n\napp = nonebot.get_app()\nasgi = nonebot.get_asgi()\n```\n\n### 添加路由规则\n\n在获取到了 ASGI 应用后，我们就可以直接使用 ASGI 应用框架提供的功能来添加路由规则了。这里我们以 [FastAPI](./driver.md#fastapi默认) 为例，演示如何添加路由规则。\n\n在下面的代码中，我们添加了一个 `GET` 类型的 `/api` 路由，具体方法参考 [FastAPI 文档](https://fastapi.tiangolo.com/)。\n\n```python\nimport nonebot\nfrom fastapi import FastAPI\n\napp: FastAPI = nonebot.get_app()\n\n@app.get(\"/api\")\nasync def custom_api():\n    return {\"message\": \"Hello, world!\"}\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/advanced/runtime-hook.md",
    "content": "---\nsidebar_position: 8\ndescription: 在特定的生命周期中执行代码\n\noptions:\n  menu:\n    - category: advanced\n      weight: 90\n---\n\n# 钩子函数\n\n> [钩子编程](https://zh.wikipedia.org/wiki/%E9%92%A9%E5%AD%90%E7%BC%96%E7%A8%8B)（hooking），也称作“挂钩”，是计算机程序设计术语，指通过拦截软件模块间的函数调用、消息传递、事件传递来修改或扩展操作系统、应用程序或其他软件组件的行为的各种技术。处理被拦截的函数调用、事件、消息的代码，被称为钩子（hook）。\n\n在 NoneBot 中有一系列预定义的钩子函数，可以分为两类：**全局钩子函数**和**事件处理钩子函数**，这些钩子函数可以用装饰器的形式来使用。\n\n## 全局钩子函数\n\n全局钩子函数是指 NoneBot 针对其本身运行过程的钩子函数。\n\n这些钩子函数是由驱动器来运行的，故需要先[获得全局驱动器](./driver.md#获取驱动器)。\n\n### 启动准备\n\n这个钩子函数会在 NoneBot 启动时运行。很多时候，我们并不希望在模块被导入时就执行一些耗时操作，如：连接数据库，这时候我们可以在这个钩子函数中进行这些操作。\n\n```python\nfrom nonebot import get_driver\n\ndriver = get_driver()\n\n@driver.on_startup\nasync def do_something():\n    pass\n```\n\n### 终止处理\n\n这个钩子函数会在 NoneBot 终止时运行。我们可以在这个钩子函数中进行一些清理工作，如：关闭数据库连接。\n\n```python\nfrom nonebot import get_driver\n\ndriver = get_driver()\n\n@driver.on_shutdown\nasync def do_something():\n    pass\n```\n\n### Bot 连接处理\n\n这个钩子函数会在任何协议适配器连接 `Bot` 对象至 NoneBot 时运行。支持依赖注入，可以直接注入 `Bot` 对象。\n\n```python\nfrom nonebot import get_driver\n\ndriver = get_driver()\n\n@driver.on_bot_connect\nasync def do_something(bot: Bot):\n    pass\n```\n\n### Bot 断开处理\n\n这个钩子函数会在 `Bot` 断开与 NoneBot 的连接时运行。支持依赖注入，可以直接注入 `Bot` 对象。\n\n```python\nfrom nonebot import get_driver\n\ndriver = get_driver()\n\n@driver.on_bot_disconnect\nasync def do_something(bot: Bot):\n    pass\n```\n\n## 事件处理钩子函数\n\n这些钩子函数指的是影响 NoneBot 进行**事件处理**的函数, 这些函数可以跟普通的事件处理函数一样接受相应的参数。\n\n### 事件预处理\n\n这个钩子函数会在 NoneBot 接收到新的事件时运行。支持依赖注入，可以注入 `Bot` 对象、事件、会话状态。在这个钩子函数内抛出 `nonebot.exception.IgnoredException` 会使 NoneBot 忽略该事件。\n\n```python\nfrom nonebot.exception import IgnoredException\nfrom nonebot.message import event_preprocessor\n\n@event_preprocessor\nasync def do_something(event: Event):\n    if not event.is_tome():\n        raise IgnoredException(\"some reason\")\n```\n\n### 事件后处理\n\n这个钩子函数会在 NoneBot 处理事件完成后运行。支持依赖注入，可以注入 `Bot` 对象、事件、会话状态。\n\n```python\nfrom nonebot.message import event_postprocessor\n\n@event_postprocessor\nasync def do_something(event: Event):\n    pass\n```\n\n### 运行预处理\n\n这个钩子函数会在 NoneBot 运行事件响应器前运行。支持依赖注入，可以注入 `Bot` 对象、事件、事件响应器、会话状态。在这个钩子函数内抛出 `nonebot.exception.IgnoredException` 也会使 NoneBot 忽略本次运行。\n\n```python\nfrom nonebot.message import run_preprocessor\nfrom nonebot.exception import IgnoredException\n\n@run_preprocessor\nasync def do_something(event: Event, matcher: Matcher):\n    if not event.is_tome():\n        raise IgnoredException(\"some reason\")\n```\n\n### 运行后处理\n\n这个钩子函数会在 NoneBot 运行事件响应器后运行。支持依赖注入，可以注入 `Bot` 对象、事件、事件响应器、会话状态、运行中产生的异常。\n\n```python\nfrom nonebot.message import run_postprocessor\n\n@run_postprocessor\nasync def do_something(event: Event, matcher: Matcher, exception: Optional[Exception]):\n    pass\n```\n\n### 平台接口调用钩子\n\n这个钩子函数会在 `Bot` 对象调用平台接口时运行。在这个钩子函数中，我们可以通过引起 `MockApiException` 异常来阻止 `Bot` 对象调用平台接口并返回指定的结果。\n\n```python\nfrom nonebot.adapters import Bot\nfrom nonebot.exception import MockApiException\n\n@Bot.on_calling_api\nasync def handle_api_call(bot: Bot, api: str, data: Dict[str, Any]):\n    if api == \"send_msg\":\n        raise MockApiException(result={\"message_id\": 123})\n```\n\n### 平台接口调用后钩子\n\n这个钩子函数会在 `Bot` 对象调用平台接口后运行。在这个钩子函数中，我们可以通过引起 `MockApiException` 异常来忽略平台接口返回的结果并返回指定的结果。\n\n```python\nfrom nonebot.adapters import Bot\nfrom nonebot.exception import MockApiException\n\n@Bot.on_called_api\nasync def handle_api_result(\n    bot: Bot, exception: Optional[Exception], api: str, data: Dict[str, Any], result: Any\n):\n    if not exception and api == \"send_msg\":\n        raise MockApiException(result={**result, \"message_id\": 123})\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/advanced/session-updating.md",
    "content": "---\nsidebar_position: 7\ndescription: 控制会话响应对象\n\noptions:\n  menu:\n    - category: advanced\n      weight: 80\n---\n\n# 会话更新\n\n在 NoneBot 中，在某个事件响应器对事件响应后，即是进入了会话状态，会话状态会持续到整个事件响应流程结束。会话过程中，机器人可以与用户进行多次交互。每次需要等待用户事件时，NoneBot 将会复制一个新的临时事件响应器，并更新该事件响应器使其响应当前会话主体的消息，这个过程称为会话更新。\n\n会话更新分为两部分：**更新[事件响应器类型](./matcher.md#事件响应器类型)**和**更新[事件触发权限](./matcher.md#事件触发权限)**。\n\n## 更新事件响应器类型\n\n通常情况下，与机器人用户进行的会话都是通过消息事件进行的，因此会话更新后的默认响应事件类型为 `message`。如果希望接收一个特定类型的消息，比如 `notice` 等，我们需要自定义响应事件类型更新函数。响应事件类型更新函数是一个 `Dependent`，可以使用依赖注入。\n\n```python {3-5}\nfoo = on_message()\n\n@foo.type_updater\nasync def _() -> str:\n    return \"notice\"\n```\n\n在注册了上述响应事件类型更新函数后，当我们需要等待用户事件时，将只会响应 `notice` 类型的事件。如果希望在会话过程中的不同阶段响应不同类型的事件，我们就需要使用更复杂的逻辑来更新响应事件类型（如：根据会话状态），这里将不再展示。\n\n## 更新事件触发权限\n\n会话通常是由机器人与用户进行的一对一交互，因此会话更新后的默认触发权限为当前事件的会话 ID。这个会话 ID 由协议适配器生成，通常由用户 ID 和群 ID 等组成。如果希望实现更复杂的会话功能（如：多用户同时参与的会话），我们需要自定义触发权限更新函数。触发权限更新函数是一个 `Dependent`，可以使用依赖注入。\n\n```python {5-7}\nfrom nonebot.permission import User\n\nfoo = on_message()\n\n@foo.permission_updater\nasync def _(event: Event, matcher: Matcher) -> Permission:\n    return Permission(User.from_event(event, perm=matcher.permission))\n```\n\n上述权限更新函数是默认的权限更新函数，它将会话的触发权限更新为当前事件的会话 ID。如果我们希望响应多个用户的消息，我们可以如下修改：\n\n```python {5-7}\nfrom nonebot.permission import USER\n\nfoo = on_message()\n\n@foo.permission_updater\nasync def _(matcher: Matcher) -> Permission:\n    return USER(\"session1\", \"session2\", perm=matcher.permission)\n```\n\n请注意，此处为全大写字母的 `USER` 权限，它可以匹配多个会话 ID。通过这种方式，我们可以实现多用户同时参与的会话。\n\n我们已经了解了如何控制会话的更新，相信你已经能够实现更复杂的会话功能了，例如多人小游戏等等。欢迎将你的作品分享到[插件商店](/store/plugins)。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/api/.gitkeep",
    "content": ""
  },
  {
    "path": "website/versioned_docs/version-2.4.3/api/adapters/_category_.json",
    "content": "{\n  \"position\": 15\n}\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/api/adapters/index.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 0\ndescription: nonebot.adapters 模块\n---\n\n# nonebot.adapters\n\n本模块定义了协议适配基类，各协议请继承以下基类。\n\n使用 [Driver.register_adapter](../drivers/index.md#Driver-register-adapter) 注册适配器。\n\n## _abstract class_ `Adapter(driver, **kwargs)` {#Adapter}\n\n- **说明**\n\n  协议适配器基类。\n\n  通常，在 Adapter 中编写协议通信相关代码，如: 建立通信连接、处理接收与发送 data 等。\n\n- **参数**\n  - `driver` ([Driver](../drivers/index.md#Driver)): [Driver](../drivers/index.md#Driver) 实例\n\n  - `**kwargs` (Any): 其他由 [Driver.register_adapter](../drivers/index.md#Driver-register-adapter) 传入的额外参数\n\n### _instance-var_ `driver` {#Adapter-driver}\n\n- **类型:** [Driver](../drivers/index.md#Driver)\n\n- **说明:** 实例\n\n### _instance-var_ `bots` {#Adapter-bots}\n\n- **类型:** dict[str, [Bot](#Bot)]\n\n- **说明:** 本协议适配器已建立连接的 [Bot](#Bot) 实例\n\n### _abstract classmethod_ `get_name()` {#Adapter-get-name}\n\n- **说明:** 当前协议适配器的名称\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n### _property_ `config` {#Adapter-config}\n\n- **类型:** [Config](../config.md#Config)\n\n- **说明:** 全局 NoneBot 配置\n\n### _method_ `bot_connect(bot)` {#Adapter-bot-connect}\n\n- **说明**\n\n  告知 NoneBot 建立了一个新的 [Bot](#Bot) 连接。\n\n  当有新的 [Bot](#Bot) 实例连接建立成功时调用。\n\n- **参数**\n  - `bot` ([Bot](#Bot)): [Bot](#Bot) 实例\n\n- **返回**\n  - None\n\n### _method_ `bot_disconnect(bot)` {#Adapter-bot-disconnect}\n\n- **说明**\n\n  告知 NoneBot [Bot](#Bot) 连接已断开。\n\n  当有 [Bot](#Bot) 实例连接断开时调用。\n\n- **参数**\n  - `bot` ([Bot](#Bot)): [Bot](#Bot) 实例\n\n- **返回**\n  - None\n\n### _method_ `setup_http_server(setup)` {#Adapter-setup-http-server}\n\n- **说明:** 设置一个 HTTP 服务器路由配置\n\n- **参数**\n  - `setup` ([HTTPServerSetup](../drivers/index.md#HTTPServerSetup))\n\n- **返回**\n  - untyped\n\n### _method_ `setup_websocket_server(setup)` {#Adapter-setup-websocket-server}\n\n- **说明:** 设置一个 WebSocket 服务器路由配置\n\n- **参数**\n  - `setup` ([WebSocketServerSetup](../drivers/index.md#WebSocketServerSetup))\n\n- **返回**\n  - untyped\n\n### _async method_ `request(setup)` {#Adapter-request}\n\n- **说明:** 进行一个 HTTP 客户端请求\n\n- **参数**\n  - `setup` ([Request](../drivers/index.md#Request))\n\n- **返回**\n  - [Response](../drivers/index.md#Response)\n\n### _method_ `websocket(setup)` {#Adapter-websocket}\n\n- **说明:** 建立一个 WebSocket 客户端连接请求\n\n- **参数**\n  - `setup` ([Request](../drivers/index.md#Request))\n\n- **返回**\n  - AsyncGenerator[[WebSocket](../drivers/index.md#WebSocket), None]\n\n### _method_ `on_ready(func)` {#Adapter-on-ready}\n\n- **参数**\n  - `func` (LIFESPAN_FUNC)\n\n- **返回**\n  - LIFESPAN_FUNC\n\n## _abstract class_ `Bot(adapter, self_id)` {#Bot}\n\n- **说明**\n\n  Bot 基类。\n\n  用于处理上报消息，并提供 API 调用接口。\n\n- **参数**\n  - `adapter` ([Adapter](#Adapter)): 协议适配器实例\n\n  - `self_id` (str): 机器人 ID\n\n### _instance-var_ `adapter` {#Bot-adapter}\n\n- **类型:** [Adapter](#Adapter)\n\n- **说明:** 协议适配器实例\n\n### _instance-var_ `self_id` {#Bot-self-id}\n\n- **类型:** str\n\n- **说明:** 机器人 ID\n\n### _property_ `type` {#Bot-type}\n\n- **类型:** str\n\n- **说明:** 协议适配器名称\n\n### _property_ `config` {#Bot-config}\n\n- **类型:** [Config](../config.md#Config)\n\n- **说明:** 全局 NoneBot 配置\n\n### _async method_ `call_api(api, **data)` {#Bot-call-api}\n\n- **说明:** 调用机器人 API 接口，可以通过该函数或直接通过 bot 属性进行调用\n\n- **参数**\n  - `api` (str): API 名称\n\n  - `**data` (Any): API 数据\n\n- **返回**\n  - Any\n\n- **用法**\n\n  ```python\n  await bot.call_api(\"send_msg\", message=\"hello world\")\n  await bot.send_msg(message=\"hello world\")\n  ```\n\n### _abstract async method_ `send(event, message, **kwargs)` {#Bot-send}\n\n- **说明:** 调用机器人基础发送消息接口\n\n- **参数**\n  - `event` ([Event](#Event)): 上报事件\n\n  - `message` (str | [Message](#Message) | [MessageSegment](#MessageSegment)): 要发送的消息\n\n  - `**kwargs` (Any): 任意额外参数\n\n- **返回**\n  - Any\n\n### _classmethod_ `on_calling_api(func)` {#Bot-on-calling-api}\n\n- **说明**\n\n  调用 api 预处理。\n\n  钩子函数参数:\n  - bot: 当前 bot 对象\n  - api: 调用的 api 名称\n  - data: api 调用的参数字典\n\n- **参数**\n  - `func` ([T_CallingAPIHook](../typing.md#T-CallingAPIHook))\n\n- **返回**\n  - [T_CallingAPIHook](../typing.md#T-CallingAPIHook)\n\n### _classmethod_ `on_called_api(func)` {#Bot-on-called-api}\n\n- **说明**\n\n  调用 api 后处理。\n\n  钩子函数参数:\n  - bot: 当前 bot 对象\n  - exception: 调用 api 时发生的错误\n  - api: 调用的 api 名称\n  - data: api 调用的参数字典\n  - result: api 调用的返回\n\n- **参数**\n  - `func` ([T_CalledAPIHook](../typing.md#T-CalledAPIHook))\n\n- **返回**\n  - [T_CalledAPIHook](../typing.md#T-CalledAPIHook)\n\n## _abstract class_ `Event(<auto>)` {#Event}\n\n- **说明:** Event 基类。提供获取关键信息的方法，其余信息可直接获取。\n\n- **参数**\n\n  auto\n\n### _abstract method_ `get_type()` {#Event-get-type}\n\n- **说明:** 获取事件类型的方法，类型通常为 NoneBot 内置的四种类型。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n### _abstract method_ `get_event_name()` {#Event-get-event-name}\n\n- **说明:** 获取事件名称的方法。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n### _abstract method_ `get_event_description()` {#Event-get-event-description}\n\n- **说明:** 获取事件描述的方法，通常为事件具体内容。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n### _method_ `get_log_string()` {#Event-get-log-string}\n\n- **说明**\n\n  获取事件日志信息的方法。\n\n  通常你不需要修改这个方法，只有当希望 NoneBot 隐藏该事件日志时，\n  可以抛出 `NoLogException` 异常。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n- **异常**\n  - NoLogException: 希望 NoneBot 隐藏该事件日志\n\n### _abstract method_ `get_user_id()` {#Event-get-user-id}\n\n- **说明:** 获取事件主体 id 的方法，通常是用户 id 。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n### _abstract method_ `get_session_id()` {#Event-get-session-id}\n\n- **说明:** 获取会话 id 的方法，用于判断当前事件属于哪一个会话， 通常是用户 id、群组 id 组合。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n### _abstract method_ `get_message()` {#Event-get-message}\n\n- **说明:** 获取事件消息内容的方法。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - [Message](#Message)\n\n### _method_ `get_plaintext()` {#Event-get-plaintext}\n\n- **说明**\n\n  获取消息纯文本的方法。\n\n  通常不需要修改，默认通过 `get_message().extract_plain_text` 获取。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n### _abstract method_ `is_tome()` {#Event-is-tome}\n\n- **说明:** 获取事件是否与机器人有关的方法。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - bool\n\n## _abstract class_ `Message(<auto>)` {#Message}\n\n- **说明:** 消息序列\n\n- **参数**\n  - `message`: 消息内容\n\n### _classmethod_ `template(format_string)` {#Message-template}\n\n- **说明**\n\n  创建消息模板。\n\n  用法和 `str.format` 大致相同，支持以 `Message` 对象作为消息模板并输出消息对象。\n  并且提供了拓展的格式化控制符，\n  可以通过该消息类型的 `MessageSegment` 工厂方法创建消息。\n\n- **参数**\n  - `format_string` (str | TM): 格式化模板\n\n- **返回**\n  - [MessageTemplate](#MessageTemplate)[Self]: 消息格式化器\n\n### _abstract classmethod_ `get_segment_class()` {#Message-get-segment-class}\n\n- **说明:** 获取消息段类型\n\n- **参数**\n\n  empty\n\n- **返回**\n  - type[TMS]\n\n### _abstract staticmethod_ `_construct(msg)` {#Message--construct}\n\n- **说明:** 构造消息数组\n\n- **参数**\n  - `msg` (str)\n\n- **返回**\n  - Iterable[TMS]\n\n### _method_ `__getitem__(args)` {#Message---getitem--}\n\n- **重载**\n\n  **1.** `(args) -> Self`\n  - **参数**\n    - `args` (str): 消息段类型\n\n  - **返回**\n    - Self: 所有类型为 `args` 的消息段\n\n  **2.** `(args) -> TMS`\n  - **参数**\n    - `args` (tuple[str, int]): 消息段类型和索引\n\n  - **返回**\n    - TMS: 类型为 `args[0]` 的消息段第 `args[1]` 个\n\n  **3.** `(args) -> Self`\n  - **参数**\n    - `args` (tuple[str, slice]): 消息段类型和切片\n\n  - **返回**\n    - Self: 类型为 `args[0]` 的消息段切片 `args[1]`\n\n  **4.** `(args) -> TMS`\n  - **参数**\n    - `args` (int): 索引\n\n  - **返回**\n    - TMS: 第 `args` 个消息段\n\n  **5.** `(args) -> Self`\n  - **参数**\n    - `args` (slice): 切片\n\n  - **返回**\n    - Self: 消息切片 `args`\n\n### _method_ `__contains__(value)` {#Message---contains--}\n\n- **说明:** 检查消息段是否存在\n\n- **参数**\n  - `value` (TMS | str): 消息段或消息段类型\n\n- **返回**\n  - bool: 消息内是否存在给定消息段或给定类型的消息段\n\n### _method_ `has(value)` {#Message-has}\n\n- **说明:** 与 [`__contains__`](#Message---contains--) 相同\n\n- **参数**\n  - `value` (TMS | str)\n\n- **返回**\n  - bool\n\n### _method_ `index(value, *args)` {#Message-index}\n\n- **说明:** 索引消息段\n\n- **参数**\n  - `value` (TMS | str): 消息段或者消息段类型\n\n  - `*args` (SupportsIndex)\n\n  - `arg`: start 与 end\n\n- **返回**\n  - int: 索引 index\n\n- **异常**\n  - ValueError: 消息段不存在\n\n### _method_ `get(type_, count=None)` {#Message-get}\n\n- **说明:** 获取指定类型的消息段\n\n- **参数**\n  - `type_` (str): 消息段类型\n\n  - `count` (int | None): 获取个数\n\n- **返回**\n  - Self: 构建的新消息\n\n### _method_ `count(value)` {#Message-count}\n\n- **说明:** 计算指定消息段的个数\n\n- **参数**\n  - `value` (TMS | str): 消息段或消息段类型\n\n- **返回**\n  - int: 个数\n\n### _method_ `only(value)` {#Message-only}\n\n- **说明:** 检查消息中是否仅包含指定消息段\n\n- **参数**\n  - `value` (TMS | str): 指定消息段或消息段类型\n\n- **返回**\n  - bool: 是否仅包含指定消息段\n\n### _method_ `append(obj)` {#Message-append}\n\n- **说明:** 添加一个消息段到消息数组末尾。\n\n- **参数**\n  - `obj` (str | TMS): 要添加的消息段\n\n- **返回**\n  - Self\n\n### _method_ `extend(obj)` {#Message-extend}\n\n- **说明:** 拼接一个消息数组或多个消息段到消息数组末尾。\n\n- **参数**\n  - `obj` (Self | Iterable[TMS]): 要添加的消息数组\n\n- **返回**\n  - Self\n\n### _method_ `join(iterable)` {#Message-join}\n\n- **说明:** 将多个消息连接并将自身作为分割\n\n- **参数**\n  - `iterable` (Iterable[TMS | Self]): 要连接的消息\n\n- **返回**\n  - Self: 连接后的消息\n\n### _method_ `copy()` {#Message-copy}\n\n- **说明:** 深拷贝消息\n\n- **参数**\n\n  empty\n\n- **返回**\n  - Self\n\n### _method_ `include(*types)` {#Message-include}\n\n- **说明:** 过滤消息\n\n- **参数**\n  - `*types` (str): 包含的消息段类型\n\n- **返回**\n  - Self: 新构造的消息\n\n### _method_ `exclude(*types)` {#Message-exclude}\n\n- **说明:** 过滤消息\n\n- **参数**\n  - `*types` (str): 不包含的消息段类型\n\n- **返回**\n  - Self: 新构造的消息\n\n### _method_ `extract_plain_text()` {#Message-extract-plain-text}\n\n- **说明:** 提取消息内纯文本消息\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n## _abstract class_ `MessageSegment(<auto>)` {#MessageSegment}\n\n- **说明:** 消息段基类\n\n- **参数**\n\n  auto\n\n### _instance-var_ `type` {#MessageSegment-type}\n\n- **类型:** str\n\n- **说明:** 消息段类型\n\n### _class-var_ `data` {#MessageSegment-data}\n\n- **类型:** dict[str, Any]\n\n- **说明:** 消息段数据\n\n### _abstract classmethod_ `get_message_class()` {#MessageSegment-get-message-class}\n\n- **说明:** 获取消息数组类型\n\n- **参数**\n\n  empty\n\n- **返回**\n  - type[TM]\n\n### _abstract method_ `__str__()` {#MessageSegment---str--}\n\n- **说明:** 该消息段所代表的 str，在命令匹配部分使用\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n### _method_ `__add__(other)` {#MessageSegment---add--}\n\n- **参数**\n  - `other` (str | Self | Iterable[Self])\n\n- **返回**\n  - TM\n\n### _method_ `get(key, default=None)` {#MessageSegment-get}\n\n- **参数**\n  - `key` (str)\n\n  - `default` (Any)\n\n- **返回**\n  - untyped\n\n### _method_ `keys()` {#MessageSegment-keys}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - untyped\n\n### _method_ `values()` {#MessageSegment-values}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - untyped\n\n### _method_ `items()` {#MessageSegment-items}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - untyped\n\n### _method_ `join(iterable)` {#MessageSegment-join}\n\n- **参数**\n  - `iterable` (Iterable[Self | TM])\n\n- **返回**\n  - TM\n\n### _method_ `copy()` {#MessageSegment-copy}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - Self\n\n### _abstract method_ `is_text()` {#MessageSegment-is-text}\n\n- **说明:** 当前消息段是否为纯文本\n\n- **参数**\n\n  empty\n\n- **返回**\n  - bool\n\n## _class_ `MessageTemplate(template, factory=str, private_getattr=False)` {#MessageTemplate}\n\n- **说明:** 消息模板格式化实现类。\n\n- **参数**\n  - `template` (str | TM): 模板\n\n  - `factory` (type[str] | type[TM]): 消息类型工厂，默认为 `str`\n\n  - `private_getattr` (bool): 是否允许在模板中访问私有属性，默认为 `False`\n\n### _method_ `add_format_spec(spec, name=None)` {#MessageTemplate-add-format-spec}\n\n- **参数**\n  - `spec` (FormatSpecFunc_T)\n\n  - `name` (str | None)\n\n- **返回**\n  - FormatSpecFunc_T\n\n### _method_ `format(*args, **kwargs)` {#MessageTemplate-format}\n\n- **说明:** 根据传入参数和模板生成消息对象\n\n- **参数**\n  - `*args`\n\n  - `**kwargs`\n\n- **返回**\n  - TF\n\n### _method_ `format_map(mapping)` {#MessageTemplate-format-map}\n\n- **说明:** 根据传入字典和模板生成消息对象, 在传入字段名不是有效标识符时有用\n\n- **参数**\n  - `mapping` (Mapping[str, Any])\n\n- **返回**\n  - TF\n\n### _method_ `vformat(format_string, args, kwargs)` {#MessageTemplate-vformat}\n\n- **参数**\n  - `format_string` (str)\n\n  - `args` (Sequence[Any])\n\n  - `kwargs` (Mapping[str, Any])\n\n- **返回**\n  - TF\n\n### _method_ `get_field(field_name, args, kwargs)` {#MessageTemplate-get-field}\n\n- **参数**\n  - `field_name` (str)\n\n  - `args` (Sequence[Any])\n\n  - `kwargs` (Mapping[str, Any])\n\n- **返回**\n  - tuple[Any, int | str]\n\n### _method_ `format_field(value, format_spec)` {#MessageTemplate-format-field}\n\n- **参数**\n  - `value` (Any)\n\n  - `format_spec` (str)\n\n- **返回**\n  - Any\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/api/compat.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 16\ndescription: nonebot.compat 模块\n---\n\n# nonebot.compat\n\n本模块为 Pydantic 版本兼容层模块\n\n为兼容 Pydantic V1 与 V2 版本，定义了一系列兼容函数与类供使用。\n\n## _var_ `Required` {#Required}\n\n- **类型:** untyped\n\n- **说明:** Alias of Ellipsis for compatibility with pydantic v1\n\n## _library-attr_ `PydanticUndefined` {#PydanticUndefined}\n\n- **说明:** Pydantic Undefined object\n\n## _library-attr_ `PydanticUndefinedType` {#PydanticUndefinedType}\n\n- **说明:** Pydantic Undefined type\n\n## _var_ `DEFAULT_CONFIG` {#DEFAULT-CONFIG}\n\n- **类型:** untyped\n\n- **说明:** Default config for validations\n\n## _class_ `FieldInfo(default=PydanticUndefined, **kwargs)` {#FieldInfo}\n\n- **说明:** FieldInfo class with extra property for compatibility with pydantic v1\n\n- **参数**\n  - `default` (Any)\n\n  - `**kwargs` (Any)\n\n### _property_ `extra` {#FieldInfo-extra}\n\n- **类型:** dict[str, Any]\n\n- **说明**\n\n  Extra data that is not part of the standard pydantic fields.\n\n  For compatibility with pydantic v1.\n\n## _class_ `ModelField(<auto>)` {#ModelField}\n\n- **说明:** ModelField class for compatibility with pydantic v1\n\n- **参数**\n\n  auto\n\n### _instance-var_ `name` {#ModelField-name}\n\n- **类型:** str\n\n- **说明:** The name of the field.\n\n### _instance-var_ `annotation` {#ModelField-annotation}\n\n- **类型:** Any\n\n- **说明:** The annotation of the field.\n\n### _instance-var_ `field_info` {#ModelField-field-info}\n\n- **类型:** FieldInfo\n\n- **说明:** The FieldInfo of the field.\n\n### _classmethod_ `construct(name, annotation, field_info=None)` {#ModelField-construct}\n\n- **说明:** Construct a ModelField from given infos.\n\n- **参数**\n  - `name` (str)\n\n  - `annotation` (Any)\n\n  - `field_info` (FieldInfo | None)\n\n- **返回**\n  - Self\n\n### _method_ `get_default()` {#ModelField-get-default}\n\n- **说明:** Get the default value of the field.\n\n- **参数**\n\n  empty\n\n- **返回**\n  - Any\n\n### _method_ `validate_value(value)` {#ModelField-validate-value}\n\n- **说明:** Validate the value pass to the field.\n\n- **参数**\n  - `value` (Any)\n\n- **返回**\n  - Any\n\n## _def_ `extract_field_info(field_info)` {#extract-field-info}\n\n- **说明:** Get FieldInfo init kwargs from a FieldInfo instance.\n\n- **参数**\n  - `field_info` (BaseFieldInfo)\n\n- **返回**\n  - dict[str, Any]\n\n## _def_ `model_fields(model)` {#model-fields}\n\n- **说明:** Get field list of a model.\n\n- **参数**\n  - `model` (type[BaseModel])\n\n- **返回**\n  - list[ModelField]\n\n## _def_ `model_config(model)` {#model-config}\n\n- **说明:** Get config of a model.\n\n- **参数**\n  - `model` (type[BaseModel])\n\n- **返回**\n  - Any\n\n## _def_ `model_dump(model, include=None, exclude=None, by_alias=False, exclude_unset=False, exclude_defaults=False, exclude_none=False)` {#model-dump}\n\n- **参数**\n  - `model` (BaseModel)\n\n  - `include` (set[str] | None)\n\n  - `exclude` (set[str] | None)\n\n  - `by_alias` (bool)\n\n  - `exclude_unset` (bool)\n\n  - `exclude_defaults` (bool)\n\n  - `exclude_none` (bool)\n\n- **返回**\n  - dict[str, Any]\n\n## _def_ `type_validate_python(type_, data)` {#type-validate-python}\n\n- **说明:** Validate data with given type.\n\n- **参数**\n  - `type_` (type[T])\n\n  - `data` (Any)\n\n- **返回**\n  - T\n\n## _def_ `type_validate_json(type_, data)` {#type-validate-json}\n\n- **说明:** Validate JSON with given type.\n\n- **参数**\n  - `type_` (type[T])\n\n  - `data` (str | bytes)\n\n- **返回**\n  - T\n\n## _def_ `custom_validation(class_)` {#custom-validation}\n\n- **说明:** Use pydantic v1 like validator generator in pydantic v2\n\n- **参数**\n  - `class_` (type[CVC])\n\n- **返回**\n  - type[CVC]\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/api/config.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 1\ndescription: nonebot.config 模块\n---\n\n# nonebot.config\n\n本模块定义了 NoneBot 本身运行所需的配置项。\n\nNoneBot 使用 [`pydantic`](https://pydantic-docs.helpmanual.io/) 以及\n[`python-dotenv`](https://saurabh-kumar.com/python-dotenv/) 来读取配置。\n\n配置项需符合特殊格式或 json 序列化格式\n详情见 [`pydantic Field Type`](https://pydantic-docs.helpmanual.io/usage/types/) 文档。\n\n## _class_ `Env(_env_file=ENV_FILE_SENTINEL, _env_file_encoding=None, _env_nested_delimiter=None, **values)` {#Env}\n\n- **说明**\n\n  运行环境配置。大小写不敏感。\n\n  将会从 **环境变量** > **dotenv 配置文件** 的优先级读取环境信息。\n\n- **参数**\n  - `_env_file` (DOTENV_TYPE | None)\n\n  - `_env_file_encoding` (str | None)\n\n  - `_env_nested_delimiter` (str | None)\n\n  - `**values` (Any)\n\n### _class-var_ `environment` {#Env-environment}\n\n- **类型:** str\n\n- **说明**\n\n  当前环境名。\n\n  NoneBot 将从 `.env.{environment}` 文件中加载配置。\n\n## _class_ `Config(_env_file=ENV_FILE_SENTINEL, _env_file_encoding=None, _env_nested_delimiter=None, **values)` {#Config}\n\n- **说明**\n\n  NoneBot 主要配置。大小写不敏感。\n\n  除了 NoneBot 的配置项外，还可以自行添加配置项到 `.env.{environment}` 文件中。\n  这些配置将会在 json 反序列化后一起带入 `Config` 类中。\n\n  配置方法参考: [配置](https://nonebot.dev/docs/appendices/config)\n\n- **参数**\n  - `_env_file` (DOTENV_TYPE | None)\n\n  - `_env_file_encoding` (str | None)\n\n  - `_env_nested_delimiter` (str | None)\n\n  - `**values` (Any)\n\n### _class-var_ `driver` {#Config-driver}\n\n- **类型:** str\n\n- **说明**\n\n  NoneBot 运行所使用的 `Driver` 。继承自 [Driver](drivers/index.md#Driver) 。\n\n  配置格式为 `<module>[:<Driver>][+<module>[:<Mixin>]]*`。\n\n  `~` 为 `nonebot.drivers.` 的缩写。\n\n  配置方法参考: [配置驱动器](https://nonebot.dev/docs/advanced/driver#%E9%85%8D%E7%BD%AE%E9%A9%B1%E5%8A%A8%E5%99%A8)\n\n### _class-var_ `host` {#Config-host}\n\n- **类型:** IPvAnyAddress\n\n- **说明:** NoneBot [ReverseDriver](drivers/index.md#ReverseDriver) 服务端监听的 IP/主机名。\n\n### _class-var_ `port` {#Config-port}\n\n- **类型:** int\n\n- **说明:** NoneBot [ReverseDriver](drivers/index.md#ReverseDriver) 服务端监听的端口。\n\n### _class-var_ `log_level` {#Config-log-level}\n\n- **类型:** int | str\n\n- **说明**\n\n  NoneBot 日志输出等级，可以为 `int` 类型等级或等级名称。\n\n  参考 [记录日志](https://nonebot.dev/docs/appendices/log)，[loguru 日志等级](https://loguru.readthedocs.io/en/stable/api/logger.html#levels)。\n\n  :::tip 提示\n  日志等级名称应为大写，如 `INFO`。\n  :::\n\n- **用法**\n\n  ```conf\n  LOG_LEVEL=25\n  LOG_LEVEL=INFO\n  ```\n\n### _class-var_ `api_timeout` {#Config-api-timeout}\n\n- **类型:** float | None\n\n- **说明:** API 请求超时时间，单位: 秒。\n\n### _class-var_ `superusers` {#Config-superusers}\n\n- **类型:** set[str]\n\n- **说明:** 机器人超级用户。\n\n- **用法**\n\n  ```conf\n  SUPERUSERS=[\"12345789\"]\n  ```\n\n### _class-var_ `nickname` {#Config-nickname}\n\n- **类型:** set[str]\n\n- **说明:** 机器人昵称。\n\n### _class-var_ `command_start` {#Config-command-start}\n\n- **类型:** set[str]\n\n- **说明**\n\n  命令的起始标记，用于判断一条消息是不是命令。\n\n  参考[命令响应规则](https://nonebot.dev/docs/advanced/matcher#command)。\n\n- **用法**\n\n  ```conf\n  COMMAND_START=[\"/\", \"\"]\n  ```\n\n### _class-var_ `command_sep` {#Config-command-sep}\n\n- **类型:** set[str]\n\n- **说明**\n\n  命令的分隔标记，用于将文本形式的命令切分为元组（实际的命令名）。\n\n  参考[命令响应规则](https://nonebot.dev/docs/advanced/matcher#command)。\n\n- **用法**\n\n  ```conf\n  COMMAND_SEP=[\".\"]\n  ```\n\n### _class-var_ `session_expire_timeout` {#Config-session-expire-timeout}\n\n- **类型:** timedelta\n\n- **说明:** 等待用户回复的超时时间。\n\n- **用法**\n\n  ```conf\n  SESSION_EXPIRE_TIMEOUT=[-][DD]D[,][HH:MM:]SS[.ffffff]\n  SESSION_EXPIRE_TIMEOUT=[±]P[DD]DT[HH]H[MM]M[SS]S  # ISO 8601\n  ```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/api/consts.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 9\ndescription: nonebot.consts 模块\n---\n\n# nonebot.consts\n\n本模块包含了 NoneBot 事件处理过程中使用到的常量。\n\n## _var_ `RECEIVE_KEY` {#RECEIVE-KEY}\n\n- **类型:** Literal['\\_receive\\_{id}']\n\n- **说明:** `receive` 存储 key\n\n## _var_ `LAST_RECEIVE_KEY` {#LAST-RECEIVE-KEY}\n\n- **类型:** Literal['\\_last\\_receive']\n\n- **说明:** `last_receive` 存储 key\n\n## _var_ `ARG_KEY` {#ARG-KEY}\n\n- **类型:** Literal['{key}']\n\n- **说明:** `arg` 存储 key\n\n## _var_ `REJECT_TARGET` {#REJECT-TARGET}\n\n- **类型:** Literal['\\_current\\_target']\n\n- **说明:** 当前 `reject` 目标存储 key\n\n## _var_ `REJECT_CACHE_TARGET` {#REJECT-CACHE-TARGET}\n\n- **类型:** Literal['\\_next\\_target']\n\n- **说明:** 下一个 `reject` 目标存储 key\n\n## _var_ `PAUSE_PROMPT_RESULT_KEY` {#PAUSE-PROMPT-RESULT-KEY}\n\n- **类型:** Literal['\\_pause\\_result']\n\n- **说明:** `pause` prompt 发送结果存储 key\n\n## _var_ `REJECT_PROMPT_RESULT_KEY` {#REJECT-PROMPT-RESULT-KEY}\n\n- **类型:** Literal['\\_reject\\_{key}\\_result']\n\n- **说明:** `reject` prompt 发送结果存储 key\n\n## _var_ `PREFIX_KEY` {#PREFIX-KEY}\n\n- **类型:** Literal['\\_prefix']\n\n- **说明:** 命令前缀存储 key\n\n## _var_ `CMD_KEY` {#CMD-KEY}\n\n- **类型:** Literal['command']\n\n- **说明:** 命令元组存储 key\n\n## _var_ `RAW_CMD_KEY` {#RAW-CMD-KEY}\n\n- **类型:** Literal['raw\\_command']\n\n- **说明:** 命令文本存储 key\n\n## _var_ `CMD_ARG_KEY` {#CMD-ARG-KEY}\n\n- **类型:** Literal['command\\_arg']\n\n- **说明:** 命令参数存储 key\n\n## _var_ `CMD_START_KEY` {#CMD-START-KEY}\n\n- **类型:** Literal['command\\_start']\n\n- **说明:** 命令开头存储 key\n\n## _var_ `CMD_WHITESPACE_KEY` {#CMD-WHITESPACE-KEY}\n\n- **类型:** Literal['command\\_whitespace']\n\n- **说明:** 命令与参数间空白符存储 key\n\n## _var_ `SHELL_ARGS` {#SHELL-ARGS}\n\n- **类型:** Literal['\\_args']\n\n- **说明:** shell 命令 parse 后参数字典存储 key\n\n## _var_ `SHELL_ARGV` {#SHELL-ARGV}\n\n- **类型:** Literal['\\_argv']\n\n- **说明:** shell 命令原始参数列表存储 key\n\n## _var_ `REGEX_MATCHED` {#REGEX-MATCHED}\n\n- **类型:** Literal['\\_matched']\n\n- **说明:** 正则匹配结果存储 key\n\n## _var_ `STARTSWITH_KEY` {#STARTSWITH-KEY}\n\n- **类型:** Literal['\\_startswith']\n\n- **说明:** 响应触发前缀 key\n\n## _var_ `ENDSWITH_KEY` {#ENDSWITH-KEY}\n\n- **类型:** Literal['\\_endswith']\n\n- **说明:** 响应触发后缀 key\n\n## _var_ `FULLMATCH_KEY` {#FULLMATCH-KEY}\n\n- **类型:** Literal['\\_fullmatch']\n\n- **说明:** 响应触发完整消息 key\n\n## _var_ `KEYWORD_KEY` {#KEYWORD-KEY}\n\n- **类型:** Literal['\\_keyword']\n\n- **说明:** 响应触发关键字 key\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/api/dependencies/_category_.json",
    "content": "{\n  \"position\": 13\n}\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/api/dependencies/index.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 0\ndescription: nonebot.dependencies 模块\n---\n\n# nonebot.dependencies\n\n本模块模块实现了依赖注入的定义与处理。\n\n## _abstract class_ `Param(*args, validate=False, **kwargs)` {#Param}\n\n- **说明**\n\n  依赖注入的基本单元 —— 参数。\n\n  继承自 `pydantic.fields.FieldInfo`，用于描述参数信息（不包括参数名）。\n\n- **参数**\n  - `*args`\n\n  - `validate` (bool)\n\n  - `**kwargs` (Any)\n\n## _class_ `Dependent(<auto>)` {#Dependent}\n\n- **说明:** 依赖注入容器\n\n- **参数**\n  - `call`: 依赖注入的可调用对象，可以是任何 Callable 对象\n\n  - `pre_checkers`: 依赖注入解析前的参数检查\n\n  - `params`: 具名参数列表\n\n  - `parameterless`: 匿名参数列表\n\n  - `allow_types`: 允许的参数类型\n\n### _staticmethod_ `parse_params(call, allow_types)` {#Dependent-parse-params}\n\n- **参数**\n  - `call` (\\_DependentCallable[R])\n\n  - `allow_types` (tuple[type[Param], ...])\n\n- **返回**\n  - tuple[[ModelField](../compat.md#ModelField), ...]\n\n### _staticmethod_ `parse_parameterless(parameterless, allow_types)` {#Dependent-parse-parameterless}\n\n- **参数**\n  - `parameterless` (tuple[Any, ...])\n\n  - `allow_types` (tuple[type[Param], ...])\n\n- **返回**\n  - tuple[Param, ...]\n\n### _classmethod_ `parse(*, call, parameterless=None, allow_types)` {#Dependent-parse}\n\n- **参数**\n  - `call` (\\_DependentCallable[R])\n\n  - `parameterless` (Iterable[Any] | None)\n\n  - `allow_types` (Iterable[type[Param]])\n\n- **返回**\n  - Dependent[R]\n\n### _async method_ `check(**params)` {#Dependent-check}\n\n- **参数**\n  - `**params` (Any)\n\n- **返回**\n  - None\n\n### _async method_ `solve(**params)` {#Dependent-solve}\n\n- **参数**\n  - `**params` (Any)\n\n- **返回**\n  - dict[str, Any]\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/api/dependencies/utils.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 1\ndescription: nonebot.dependencies.utils 模块\n---\n\n# nonebot.dependencies.utils\n\n## _def_ `get_typed_signature(call)` {#get-typed-signature}\n\n- **说明:** 获取可调用对象签名\n\n- **参数**\n  - `call` ((...) -> Any)\n\n- **返回**\n  - inspect.Signature\n\n## _def_ `get_typed_annotation(param, globalns)` {#get-typed-annotation}\n\n- **说明:** 获取参数的类型注解\n\n- **参数**\n  - `param` (inspect.Parameter)\n\n  - `globalns` (dict[str, Any])\n\n- **返回**\n  - Any\n\n## _def_ `check_field_type(field, value)` {#check-field-type}\n\n- **说明:** 检查字段类型是否匹配\n\n- **参数**\n  - `field` ([ModelField](../compat.md#ModelField))\n\n  - `value` (Any)\n\n- **返回**\n  - Any\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/api/drivers/_category_.json",
    "content": "{\n  \"position\": 14\n}\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/api/drivers/aiohttp.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 2\ndescription: nonebot.drivers.aiohttp 模块\n---\n\n# nonebot.drivers.aiohttp\n\n[AIOHTTP](https://aiohttp.readthedocs.io/en/stable/) 驱动适配器。\n\n```bash\nnb driver install aiohttp\n# 或者\npip install nonebot2[aiohttp]\n```\n\n:::tip 提示\n本驱动仅支持客户端连接\n:::\n\n## _class_ `Session(params=None, headers=None, cookies=None, version=HTTPVersion.H11, timeout=None, proxy=None)` {#Session}\n\n- **参数**\n  - `params` (QueryTypes)\n\n  - `headers` (HeaderTypes)\n\n  - `cookies` (CookieTypes)\n\n  - `version` (str | [HTTPVersion](index.md#HTTPVersion))\n\n  - `timeout` (TimeoutTypes)\n\n  - `proxy` (str | None)\n\n### _async method_ `request(setup)` {#Session-request}\n\n- **参数**\n  - `setup` ([Request](index.md#Request))\n\n- **返回**\n  - [Response](index.md#Response)\n\n### _method_ `stream_request(setup, *, chunk_size=1024)` {#Session-stream-request}\n\n- **参数**\n  - `setup` ([Request](index.md#Request))\n\n  - `chunk_size` (int)\n\n- **返回**\n  - AsyncGenerator[[Response](index.md#Response), None]\n\n### _async method_ `setup()` {#Session-setup}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - None\n\n### _async method_ `close()` {#Session-close}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - None\n\n## _class_ `Mixin(<auto>)` {#Mixin}\n\n- **说明:** AIOHTTP Mixin\n\n- **参数**\n\n  auto\n\n### _async method_ `request(setup)` {#Mixin-request}\n\n- **参数**\n  - `setup` ([Request](index.md#Request))\n\n- **返回**\n  - [Response](index.md#Response)\n\n### _method_ `stream_request(setup, *, chunk_size=1024)` {#Mixin-stream-request}\n\n- **参数**\n  - `setup` ([Request](index.md#Request))\n\n  - `chunk_size` (int)\n\n- **返回**\n  - AsyncGenerator[[Response](index.md#Response), None]\n\n### _method_ `websocket(setup)` {#Mixin-websocket}\n\n- **参数**\n  - `setup` ([Request](index.md#Request))\n\n- **返回**\n  - AsyncGenerator[[WebSocket](index.md#WebSocket), None]\n\n### _method_ `get_session(params=None, headers=None, cookies=None, version=HTTPVersion.H11, timeout=None, proxy=None)` {#Mixin-get-session}\n\n- **参数**\n  - `params` (QueryTypes)\n\n  - `headers` (HeaderTypes)\n\n  - `cookies` (CookieTypes)\n\n  - `version` (str | [HTTPVersion](index.md#HTTPVersion))\n\n  - `timeout` (TimeoutTypes)\n\n  - `proxy` (str | None)\n\n- **返回**\n  - Session\n\n## _class_ `WebSocket(*, request, session, websocket)` {#WebSocket}\n\n- **说明:** AIOHTTP Websocket Wrapper\n\n- **参数**\n  - `request` ([Request](index.md#Request))\n\n  - `session` (aiohttp.ClientSession)\n\n  - `websocket` (aiohttp.ClientWebSocketResponse)\n\n### _async method_ `accept()` {#WebSocket-accept}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - untyped\n\n### _async method_ `close(code=1000, reason=\"\")` {#WebSocket-close}\n\n- **参数**\n  - `code` (int)\n\n  - `reason` (str)\n\n- **返回**\n  - untyped\n\n### _async method_ `receive()` {#WebSocket-receive}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n### _async method_ `receive_text()` {#WebSocket-receive-text}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n### _async method_ `receive_bytes()` {#WebSocket-receive-bytes}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - bytes\n\n### _async method_ `send_text(data)` {#WebSocket-send-text}\n\n- **参数**\n  - `data` (str)\n\n- **返回**\n  - None\n\n### _async method_ `send_bytes(data)` {#WebSocket-send-bytes}\n\n- **参数**\n  - `data` (bytes)\n\n- **返回**\n  - None\n\n## _class_ `Driver(env, config)` {#Driver}\n\n- **参数**\n  - `env` ([Env](../config.md#Env))\n\n  - `config` ([Config](../config.md#Config))\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/api/drivers/fastapi.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 1\ndescription: nonebot.drivers.fastapi 模块\n---\n\n# nonebot.drivers.fastapi\n\n[FastAPI](https://fastapi.tiangolo.com/) 驱动适配\n\n```bash\nnb driver install fastapi\n# 或者\npip install nonebot2[fastapi]\n```\n\n:::tip 提示\n本驱动仅支持服务端连接\n:::\n\n## _class_ `Config(<auto>)` {#Config}\n\n- **说明:** FastAPI 驱动框架设置，详情参考 FastAPI 文档\n\n- **参数**\n\n  auto\n\n### _class-var_ `fastapi_openapi_url` {#Config-fastapi-openapi-url}\n\n- **类型:** str | None\n\n- **说明:** `openapi.json` 地址，默认为 `None` 即关闭\n\n### _class-var_ `fastapi_docs_url` {#Config-fastapi-docs-url}\n\n- **类型:** str | None\n\n- **说明:** `swagger` 地址，默认为 `None` 即关闭\n\n### _class-var_ `fastapi_redoc_url` {#Config-fastapi-redoc-url}\n\n- **类型:** str | None\n\n- **说明:** `redoc` 地址，默认为 `None` 即关闭\n\n### _class-var_ `fastapi_include_adapter_schema` {#Config-fastapi-include-adapter-schema}\n\n- **类型:** bool\n\n- **说明:** 是否包含适配器路由的 schema，默认为 `True`\n\n### _class-var_ `fastapi_reload` {#Config-fastapi-reload}\n\n- **类型:** bool\n\n- **说明:** 开启/关闭冷重载\n\n### _class-var_ `fastapi_reload_dirs` {#Config-fastapi-reload-dirs}\n\n- **类型:** list[str] | None\n\n- **说明:** 重载监控文件夹列表，默认为 uvicorn 默认值\n\n### _class-var_ `fastapi_reload_delay` {#Config-fastapi-reload-delay}\n\n- **类型:** float\n\n- **说明:** 重载延迟，默认为 uvicorn 默认值\n\n### _class-var_ `fastapi_reload_includes` {#Config-fastapi-reload-includes}\n\n- **类型:** list[str] | None\n\n- **说明:** 要监听的文件列表，支持 glob pattern，默认为 uvicorn 默认值\n\n### _class-var_ `fastapi_reload_excludes` {#Config-fastapi-reload-excludes}\n\n- **类型:** list[str] | None\n\n- **说明:** 不要监听的文件列表，支持 glob pattern，默认为 uvicorn 默认值\n\n### _class-var_ `fastapi_extra` {#Config-fastapi-extra}\n\n- **类型:** dict[str, Any]\n\n- **说明:** 传递给 `FastAPI` 的其他参数。\n\n## _class_ `Driver(env, config)` {#Driver}\n\n- **说明:** FastAPI 驱动框架。\n\n- **参数**\n  - `env` ([Env](../config.md#Env))\n\n  - `config` (NoneBotConfig)\n\n### _property_ `type` {#Driver-type}\n\n- **类型:** str\n\n- **说明:** 驱动名称: `fastapi`\n\n### _property_ `server_app` {#Driver-server-app}\n\n- **类型:** FastAPI\n\n- **说明:** `FastAPI APP` 对象\n\n### _property_ `asgi` {#Driver-asgi}\n\n- **类型:** FastAPI\n\n- **说明:** `FastAPI APP` 对象\n\n### _property_ `logger` {#Driver-logger}\n\n- **类型:** logging.Logger\n\n- **说明:** fastapi 使用的 logger\n\n### _method_ `setup_http_server(setup)` {#Driver-setup-http-server}\n\n- **参数**\n  - `setup` ([HTTPServerSetup](index.md#HTTPServerSetup))\n\n- **返回**\n  - untyped\n\n### _method_ `setup_websocket_server(setup)` {#Driver-setup-websocket-server}\n\n- **参数**\n  - `setup` ([WebSocketServerSetup](index.md#WebSocketServerSetup))\n\n- **返回**\n  - None\n\n### _method_ `run(host=None, port=None, *args, app=None, **kwargs)` {#Driver-run}\n\n- **说明:** 使用 `uvicorn` 启动 FastAPI\n\n- **参数**\n  - `host` (str | None)\n\n  - `port` (int | None)\n\n  - `*args`\n\n  - `app` (str | None)\n\n  - `**kwargs`\n\n- **返回**\n  - untyped\n\n## _class_ `FastAPIWebSocket(*, request, websocket)` {#FastAPIWebSocket}\n\n- **说明:** FastAPI WebSocket Wrapper\n\n- **参数**\n  - `request` (BaseRequest)\n\n  - `websocket` ([WebSocket](index.md#WebSocket))\n\n### _async method_ `accept()` {#FastAPIWebSocket-accept}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - None\n\n### _async method_ `close(code=status.WS_1000_NORMAL_CLOSURE, reason=\"\")` {#FastAPIWebSocket-close}\n\n- **参数**\n  - `code` (int)\n\n  - `reason` (str)\n\n- **返回**\n  - None\n\n### _async method_ `receive()` {#FastAPIWebSocket-receive}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str | bytes\n\n### _async method_ `receive_text()` {#FastAPIWebSocket-receive-text}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n### _async method_ `receive_bytes()` {#FastAPIWebSocket-receive-bytes}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - bytes\n\n### _async method_ `send_text(data)` {#FastAPIWebSocket-send-text}\n\n- **参数**\n  - `data` (str)\n\n- **返回**\n  - None\n\n### _async method_ `send_bytes(data)` {#FastAPIWebSocket-send-bytes}\n\n- **参数**\n  - `data` (bytes)\n\n- **返回**\n  - None\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/api/drivers/httpx.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 3\ndescription: nonebot.drivers.httpx 模块\n---\n\n# nonebot.drivers.httpx\n\n[HTTPX](https://www.python-httpx.org/) 驱动适配\n\n```bash\nnb driver install httpx\n# 或者\npip install nonebot2[httpx]\n```\n\n:::tip 提示\n本驱动仅支持客户端 HTTP 连接\n:::\n\n## _class_ `Session(params=None, headers=None, cookies=None, version=HTTPVersion.H11, timeout=None, proxy=None)` {#Session}\n\n- **参数**\n  - `params` (QueryTypes)\n\n  - `headers` (HeaderTypes)\n\n  - `cookies` (CookieTypes)\n\n  - `version` (str | [HTTPVersion](index.md#HTTPVersion))\n\n  - `timeout` (TimeoutTypes)\n\n  - `proxy` (str | None)\n\n### _async method_ `request(setup)` {#Session-request}\n\n- **参数**\n  - `setup` ([Request](index.md#Request))\n\n- **返回**\n  - [Response](index.md#Response)\n\n### _method_ `stream_request(setup, *, chunk_size=1024)` {#Session-stream-request}\n\n- **参数**\n  - `setup` ([Request](index.md#Request))\n\n  - `chunk_size` (int)\n\n- **返回**\n  - AsyncGenerator[[Response](index.md#Response), None]\n\n### _async method_ `setup()` {#Session-setup}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - None\n\n### _async method_ `close()` {#Session-close}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - None\n\n## _class_ `Mixin(<auto>)` {#Mixin}\n\n- **说明:** HTTPX Mixin\n\n- **参数**\n\n  auto\n\n### _async method_ `request(setup)` {#Mixin-request}\n\n- **参数**\n  - `setup` ([Request](index.md#Request))\n\n- **返回**\n  - [Response](index.md#Response)\n\n### _method_ `stream_request(setup, *, chunk_size=1024)` {#Mixin-stream-request}\n\n- **参数**\n  - `setup` ([Request](index.md#Request))\n\n  - `chunk_size` (int)\n\n- **返回**\n  - AsyncGenerator[[Response](index.md#Response), None]\n\n### _method_ `get_session(params=None, headers=None, cookies=None, version=HTTPVersion.H11, timeout=None, proxy=None)` {#Mixin-get-session}\n\n- **参数**\n  - `params` (QueryTypes)\n\n  - `headers` (HeaderTypes)\n\n  - `cookies` (CookieTypes)\n\n  - `version` (str | [HTTPVersion](index.md#HTTPVersion))\n\n  - `timeout` (TimeoutTypes)\n\n  - `proxy` (str | None)\n\n- **返回**\n  - Session\n\n## _class_ `Driver(env, config)` {#Driver}\n\n- **参数**\n  - `env` ([Env](../config.md#Env))\n\n  - `config` ([Config](../config.md#Config))\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/api/drivers/index.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 0\ndescription: nonebot.drivers 模块\n---\n\n# nonebot.drivers\n\n本模块定义了驱动适配器基类。\n\n各驱动请继承以下基类。\n\n## _abstract class_ `ASGIMixin(<auto>)` {#ASGIMixin}\n\n- **说明**\n\n  ASGI 服务端基类。\n\n  将后端框架封装，以满足适配器使用。\n\n- **参数**\n\n  auto\n\n### _abstract property_ `server_app` {#ASGIMixin-server-app}\n\n- **类型:** Any\n\n- **说明:** 驱动 APP 对象\n\n### _abstract property_ `asgi` {#ASGIMixin-asgi}\n\n- **类型:** Any\n\n- **说明:** 驱动 ASGI 对象\n\n### _abstract method_ `setup_http_server(setup)` {#ASGIMixin-setup-http-server}\n\n- **说明:** 设置一个 HTTP 服务器路由配置\n\n- **参数**\n  - `setup` ([HTTPServerSetup](#HTTPServerSetup))\n\n- **返回**\n  - None\n\n### _abstract method_ `setup_websocket_server(setup)` {#ASGIMixin-setup-websocket-server}\n\n- **说明:** 设置一个 WebSocket 服务器路由配置\n\n- **参数**\n  - `setup` ([WebSocketServerSetup](#WebSocketServerSetup))\n\n- **返回**\n  - None\n\n## _class_ `Cookies(cookies=None)` {#Cookies}\n\n- **参数**\n  - `cookies` (CookieTypes)\n\n### _method_ `set(name, value, domain=\"\", path=\"/\")` {#Cookies-set}\n\n- **参数**\n  - `name` (str)\n\n  - `value` (str)\n\n  - `domain` (str)\n\n  - `path` (str)\n\n- **返回**\n  - None\n\n### _method_ `get(name, default=None, domain=None, path=None)` {#Cookies-get}\n\n- **参数**\n  - `name` (str)\n\n  - `default` (str | None)\n\n  - `domain` (str | None)\n\n  - `path` (str | None)\n\n- **返回**\n  - str | None\n\n### _method_ `delete(name, domain=None, path=None)` {#Cookies-delete}\n\n- **参数**\n  - `name` (str)\n\n  - `domain` (str | None)\n\n  - `path` (str | None)\n\n- **返回**\n  - None\n\n### _method_ `clear(domain=None, path=None)` {#Cookies-clear}\n\n- **参数**\n  - `domain` (str | None)\n\n  - `path` (str | None)\n\n- **返回**\n  - None\n\n### _method_ `update(cookies=None)` {#Cookies-update}\n\n- **参数**\n  - `cookies` (CookieTypes)\n\n- **返回**\n  - None\n\n### _method_ `as_header(request)` {#Cookies-as-header}\n\n- **参数**\n  - `request` (Request)\n\n- **返回**\n  - dict[str, str]\n\n## _abstract class_ `Driver(env, config)` {#Driver}\n\n- **说明**\n\n  驱动器基类。\n\n  驱动器控制框架的启动和停止，适配器的注册，以及机器人生命周期管理。\n\n- **参数**\n  - `env` ([Env](../config.md#Env)): 包含环境信息的 Env 对象\n\n  - `config` ([Config](../config.md#Config)): 包含配置信息的 Config 对象\n\n### _instance-var_ `env` {#Driver-env}\n\n- **类型:** str\n\n- **说明:** 环境名称\n\n### _instance-var_ `config` {#Driver-config}\n\n- **类型:** [Config](../config.md#Config)\n\n- **说明:** 全局配置对象\n\n### _property_ `bots` {#Driver-bots}\n\n- **类型:** dict[str, [Bot](../adapters/index.md#Bot)]\n\n- **说明:** 获取当前所有已连接的 Bot\n\n### _method_ `register_adapter(adapter, **kwargs)` {#Driver-register-adapter}\n\n- **说明:** 注册一个协议适配器\n\n- **参数**\n  - `adapter` (type[[Adapter](../adapters/index.md#Adapter)]): 适配器类\n\n  - `**kwargs`: 其他传递给适配器的参数\n\n- **返回**\n  - None\n\n### _abstract property_ `type` {#Driver-type}\n\n- **类型:** str\n\n- **说明:** 驱动类型名称\n\n### _abstract property_ `logger` {#Driver-logger}\n\n- **类型:** untyped\n\n- **说明:** 驱动专属 logger 日志记录器\n\n### _abstract method_ `run(*args, **kwargs)` {#Driver-run}\n\n- **说明:** 启动驱动框架\n\n- **参数**\n  - `*args`\n\n  - `**kwargs`\n\n- **返回**\n  - untyped\n\n### _method_ `on_startup(func)` {#Driver-on-startup}\n\n- **说明:** 注册一个启动时执行的函数\n\n- **参数**\n  - `func` (LIFESPAN_FUNC)\n\n- **返回**\n  - LIFESPAN_FUNC\n\n### _method_ `on_shutdown(func)` {#Driver-on-shutdown}\n\n- **说明:** 注册一个停止时执行的函数\n\n- **参数**\n  - `func` (LIFESPAN_FUNC)\n\n- **返回**\n  - LIFESPAN_FUNC\n\n### _classmethod_ `on_bot_connect(func)` {#Driver-on-bot-connect}\n\n- **说明**\n\n  装饰一个函数使他在 bot 连接成功时执行。\n\n  钩子函数参数:\n  - bot: 当前连接上的 Bot 对象\n\n- **参数**\n  - `func` ([T_BotConnectionHook](../typing.md#T-BotConnectionHook))\n\n- **返回**\n  - [T_BotConnectionHook](../typing.md#T-BotConnectionHook)\n\n### _classmethod_ `on_bot_disconnect(func)` {#Driver-on-bot-disconnect}\n\n- **说明**\n\n  装饰一个函数使他在 bot 连接断开时执行。\n\n  钩子函数参数:\n  - bot: 当前连接上的 Bot 对象\n\n- **参数**\n  - `func` ([T_BotDisconnectionHook](../typing.md#T-BotDisconnectionHook))\n\n- **返回**\n  - [T_BotDisconnectionHook](../typing.md#T-BotDisconnectionHook)\n\n## _var_ `ForwardDriver` {#ForwardDriver}\n\n- **类型:** ForwardMixin\n\n- **说明**\n\n  支持客户端请求的驱动器。\n\n  **Deprecated**，请使用 [ForwardMixin](#ForwardMixin) 或其子类代替。\n\n## _abstract class_ `ForwardMixin(<auto>)` {#ForwardMixin}\n\n- **说明:** 客户端混入基类。\n\n- **参数**\n\n  auto\n\n## _abstract class_ `HTTPClientMixin(<auto>)` {#HTTPClientMixin}\n\n- **说明:** HTTP 客户端混入基类。\n\n- **参数**\n\n  auto\n\n### _abstract async method_ `request(setup)` {#HTTPClientMixin-request}\n\n- **说明:** 发送一个 HTTP 请求\n\n- **参数**\n  - `setup` ([Request](#Request))\n\n- **返回**\n  - [Response](#Response)\n\n### _abstract method_ `stream_request(setup, *, chunk_size=1024)` {#HTTPClientMixin-stream-request}\n\n- **说明:** 发送一个 HTTP 流式请求\n\n- **参数**\n  - `setup` ([Request](#Request))\n\n  - `chunk_size` (int)\n\n- **返回**\n  - AsyncGenerator[[Response](#Response), None]\n\n### _abstract method_ `get_session(params=None, headers=None, cookies=None, version=HTTPVersion.H11, timeout=None, proxy=None)` {#HTTPClientMixin-get-session}\n\n- **说明:** 获取一个 HTTP 会话\n\n- **参数**\n  - `params` (QueryTypes)\n\n  - `headers` (HeaderTypes)\n\n  - `cookies` (CookieTypes)\n\n  - `version` (str | [HTTPVersion](#HTTPVersion))\n\n  - `timeout` (TimeoutTypes)\n\n  - `proxy` (str | None)\n\n- **返回**\n  - HTTPClientSession\n\n## _class_ `HTTPServerSetup(<auto>)` {#HTTPServerSetup}\n\n- **说明:** HTTP 服务器路由配置。\n\n- **参数**\n\n  auto\n\n## _enum_ `HTTPVersion` {#HTTPVersion}\n\n- **参数**\n\n  auto\n  - `H10: '1.0'`\n\n  - `H11: '1.1'`\n\n  - `H2: '2'`\n\n## _abstract class_ `Mixin(<auto>)` {#Mixin}\n\n- **说明:** 可与其他驱动器共用的混入基类。\n\n- **参数**\n\n  auto\n\n### _abstract property_ `type` {#Mixin-type}\n\n- **类型:** str\n\n- **说明:** 混入驱动类型名称\n\n## _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}\n\n- **参数**\n  - `method` (str | bytes)\n\n  - `url` (URL | str | RawURL)\n\n  - `params` (QueryTypes)\n\n  - `headers` (HeaderTypes)\n\n  - `cookies` (CookieTypes)\n\n  - `content` (ContentTypes)\n\n  - `data` (DataTypes)\n\n  - `json` (Any)\n\n  - `files` (FilesTypes)\n\n  - `version` (str | HTTPVersion)\n\n  - `timeout` (TimeoutTypes)\n\n  - `proxy` (str | None)\n\n## _class_ `Response(status_code, *, headers=None, content=None, request=None)` {#Response}\n\n- **参数**\n  - `status_code` (int)\n\n  - `headers` (HeaderTypes)\n\n  - `content` (ContentTypes)\n\n  - `request` (Request | None)\n\n## _var_ `ReverseDriver` {#ReverseDriver}\n\n- **类型:** ReverseMixin\n\n- **说明**\n\n  支持服务端请求的驱动器。\n\n  **Deprecated**，请使用 [ReverseMixin](#ReverseMixin) 或其子类代替。\n\n## _abstract class_ `ReverseMixin(<auto>)` {#ReverseMixin}\n\n- **说明:** 服务端混入基类。\n\n- **参数**\n\n  auto\n\n## _class_ `Timeout(<auto>)` {#Timeout}\n\n- **说明:** Request 超时配置。\n\n- **参数**\n\n  auto\n\n## _abstract class_ `WebSocket(*, request)` {#WebSocket}\n\n- **参数**\n  - `request` (Request)\n\n### _abstract property_ `closed` {#WebSocket-closed}\n\n- **类型:** bool\n\n- **说明:** 连接是否已经关闭\n\n### _abstract async method_ `accept()` {#WebSocket-accept}\n\n- **说明:** 接受 WebSocket 连接请求\n\n- **参数**\n\n  empty\n\n- **返回**\n  - None\n\n### _abstract async method_ `close(code=1000, reason=\"\")` {#WebSocket-close}\n\n- **说明:** 关闭 WebSocket 连接请求\n\n- **参数**\n  - `code` (int)\n\n  - `reason` (str)\n\n- **返回**\n  - None\n\n### _abstract async method_ `receive()` {#WebSocket-receive}\n\n- **说明:** 接收一条 WebSocket text/bytes 信息\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str | bytes\n\n### _abstract async method_ `receive_text()` {#WebSocket-receive-text}\n\n- **说明:** 接收一条 WebSocket text 信息\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n### _abstract async method_ `receive_bytes()` {#WebSocket-receive-bytes}\n\n- **说明:** 接收一条 WebSocket binary 信息\n\n- **参数**\n\n  empty\n\n- **返回**\n  - bytes\n\n### _async method_ `send(data)` {#WebSocket-send}\n\n- **说明:** 发送一条 WebSocket text/bytes 信息\n\n- **参数**\n  - `data` (str | bytes)\n\n- **返回**\n  - None\n\n### _abstract async method_ `send_text(data)` {#WebSocket-send-text}\n\n- **说明:** 发送一条 WebSocket text 信息\n\n- **参数**\n  - `data` (str)\n\n- **返回**\n  - None\n\n### _abstract async method_ `send_bytes(data)` {#WebSocket-send-bytes}\n\n- **说明:** 发送一条 WebSocket binary 信息\n\n- **参数**\n  - `data` (bytes)\n\n- **返回**\n  - None\n\n## _abstract class_ `WebSocketClientMixin(<auto>)` {#WebSocketClientMixin}\n\n- **说明:** WebSocket 客户端混入基类。\n\n- **参数**\n\n  auto\n\n### _abstract method_ `websocket(setup)` {#WebSocketClientMixin-websocket}\n\n- **说明:** 发起一个 WebSocket 连接\n\n- **参数**\n  - `setup` ([Request](#Request))\n\n- **返回**\n  - AsyncGenerator[[WebSocket](#WebSocket), None]\n\n## _class_ `WebSocketServerSetup(<auto>)` {#WebSocketServerSetup}\n\n- **说明:** WebSocket 服务器路由配置。\n\n- **参数**\n\n  auto\n\n## _def_ `combine_driver(driver, *mixins)` {#combine-driver}\n\n- **说明:** 将一个驱动器和多个混入类合并。\n\n- **重载**\n\n  **1.** `(driver) -> type[D]`\n  - **参数**\n    - `driver` (type[D])\n\n  - **返回**\n    - type[D]\n\n  **2.** `(driver, __m, /, *mixins) -> type[CombinedDriver]`\n  - **参数**\n    - `driver` (type[D])\n\n    - `__m` (type[[Mixin](#Mixin)])\n\n    - `*mixins` (type[[Mixin](#Mixin)])\n\n  - **返回**\n    - type[CombinedDriver]\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/api/drivers/none.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 6\ndescription: nonebot.drivers.none 模块\n---\n\n# nonebot.drivers.none\n\nNone 驱动适配\n\n:::tip 提示\n本驱动不支持任何服务器或客户端连接\n:::\n\n## _class_ `Driver(env, config)` {#Driver}\n\n- **说明:** None 驱动框架\n\n- **参数**\n  - `env` ([Env](../config.md#Env))\n\n  - `config` ([Config](../config.md#Config))\n\n### _property_ `type` {#Driver-type}\n\n- **类型:** str\n\n- **说明:** 驱动名称: `none`\n\n### _property_ `logger` {#Driver-logger}\n\n- **类型:** untyped\n\n- **说明:** none driver 使用的 logger\n\n### _method_ `run(*args, **kwargs)` {#Driver-run}\n\n- **说明:** 启动 none driver\n\n- **参数**\n  - `*args`\n\n  - `**kwargs`\n\n- **返回**\n  - untyped\n\n### _method_ `exit(force=False)` {#Driver-exit}\n\n- **说明:** 退出 none driver\n\n- **参数**\n  - `force` (bool): 强制退出\n\n- **返回**\n  - untyped\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/api/drivers/quart.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 5\ndescription: nonebot.drivers.quart 模块\n---\n\n# nonebot.drivers.quart\n\n[Quart](https://pgjones.gitlab.io/quart/index.html) 驱动适配\n\n```bash\nnb driver install quart\n# 或者\npip install nonebot2[quart]\n```\n\n:::tip 提示\n本驱动仅支持服务端连接\n:::\n\n## _class_ `Config(<auto>)` {#Config}\n\n- **说明:** Quart 驱动框架设置\n\n- **参数**\n\n  auto\n\n### _class-var_ `quart_reload` {#Config-quart-reload}\n\n- **类型:** bool\n\n- **说明:** 开启/关闭冷重载\n\n### _class-var_ `quart_reload_dirs` {#Config-quart-reload-dirs}\n\n- **类型:** list[str] | None\n\n- **说明:** 重载监控文件夹列表，默认为 uvicorn 默认值\n\n### _class-var_ `quart_reload_delay` {#Config-quart-reload-delay}\n\n- **类型:** float\n\n- **说明:** 重载延迟，默认为 uvicorn 默认值\n\n### _class-var_ `quart_reload_includes` {#Config-quart-reload-includes}\n\n- **类型:** list[str] | None\n\n- **说明:** 要监听的文件列表，支持 glob pattern，默认为 uvicorn 默认值\n\n### _class-var_ `quart_reload_excludes` {#Config-quart-reload-excludes}\n\n- **类型:** list[str] | None\n\n- **说明:** 不要监听的文件列表，支持 glob pattern，默认为 uvicorn 默认值\n\n### _class-var_ `quart_extra` {#Config-quart-extra}\n\n- **类型:** dict[str, Any]\n\n- **说明:** 传递给 `Quart` 的其他参数。\n\n## _class_ `Driver(env, config)` {#Driver}\n\n- **说明:** Quart 驱动框架\n\n- **参数**\n  - `env` ([Env](../config.md#Env))\n\n  - `config` (NoneBotConfig)\n\n### _property_ `type` {#Driver-type}\n\n- **类型:** str\n\n- **说明:** 驱动名称: `quart`\n\n### _property_ `server_app` {#Driver-server-app}\n\n- **类型:** Quart\n\n- **说明:** `Quart` 对象\n\n### _property_ `asgi` {#Driver-asgi}\n\n- **类型:** untyped\n\n- **说明:** `Quart` 对象\n\n### _property_ `logger` {#Driver-logger}\n\n- **类型:** untyped\n\n- **说明:** Quart 使用的 logger\n\n### _method_ `setup_http_server(setup)` {#Driver-setup-http-server}\n\n- **参数**\n  - `setup` ([HTTPServerSetup](index.md#HTTPServerSetup))\n\n- **返回**\n  - untyped\n\n### _method_ `setup_websocket_server(setup)` {#Driver-setup-websocket-server}\n\n- **参数**\n  - `setup` ([WebSocketServerSetup](index.md#WebSocketServerSetup))\n\n- **返回**\n  - None\n\n### _method_ `run(host=None, port=None, *args, app=None, **kwargs)` {#Driver-run}\n\n- **说明:** 使用 `uvicorn` 启动 Quart\n\n- **参数**\n  - `host` (str | None)\n\n  - `port` (int | None)\n\n  - `*args`\n\n  - `app` (str | None)\n\n  - `**kwargs`\n\n- **返回**\n  - untyped\n\n## _class_ `WebSocket(*, request, websocket_ctx)` {#WebSocket}\n\n- **说明:** Quart WebSocket Wrapper\n\n- **参数**\n  - `request` (BaseRequest)\n\n  - `websocket_ctx` (WebsocketContext)\n\n### _async method_ `accept()` {#WebSocket-accept}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - untyped\n\n### _async method_ `close(code=1000, reason=\"\")` {#WebSocket-close}\n\n- **参数**\n  - `code` (int)\n\n  - `reason` (str)\n\n- **返回**\n  - untyped\n\n### _async method_ `receive()` {#WebSocket-receive}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str | bytes\n\n### _async method_ `receive_text()` {#WebSocket-receive-text}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n### _async method_ `receive_bytes()` {#WebSocket-receive-bytes}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - bytes\n\n### _async method_ `send_text(data)` {#WebSocket-send-text}\n\n- **参数**\n  - `data` (str)\n\n- **返回**\n  - untyped\n\n### _async method_ `send_bytes(data)` {#WebSocket-send-bytes}\n\n- **参数**\n  - `data` (bytes)\n\n- **返回**\n  - untyped\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/api/drivers/websockets.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 4\ndescription: nonebot.drivers.websockets 模块\n---\n\n# nonebot.drivers.websockets\n\n[websockets](https://websockets.readthedocs.io/) 驱动适配\n\n```bash\nnb driver install websockets\n# 或者\npip install nonebot2[websockets]\n```\n\n:::tip 提示\n本驱动仅支持客户端 WebSocket 连接\n:::\n\n## _def_ `catch_closed(func)` {#catch-closed}\n\n- **参数**\n  - `func` ((P) -> CoroutineType[Any, Any, T])\n\n- **返回**\n  - (P) -> CoroutineType[Any, Any, T]\n\n## _class_ `Mixin(<auto>)` {#Mixin}\n\n- **说明:** Websockets Mixin\n\n- **参数**\n\n  auto\n\n### _method_ `websocket(setup)` {#Mixin-websocket}\n\n- **参数**\n  - `setup` ([Request](index.md#Request))\n\n- **返回**\n  - AsyncGenerator[[WebSocket](index.md#WebSocket), None]\n\n## _class_ `WebSocket(*, request, websocket)` {#WebSocket}\n\n- **说明:** Websockets WebSocket Wrapper\n\n- **参数**\n  - `request` ([Request](index.md#Request))\n\n  - `websocket` (ClientConnection)\n\n### _async method_ `accept()` {#WebSocket-accept}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - untyped\n\n### _async method_ `close(code=1000, reason=\"\")` {#WebSocket-close}\n\n- **参数**\n  - `code` (int)\n\n  - `reason` (str)\n\n- **返回**\n  - untyped\n\n### _async method_ `receive()` {#WebSocket-receive}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str | bytes\n\n### _async method_ `receive_text()` {#WebSocket-receive-text}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n### _async method_ `receive_bytes()` {#WebSocket-receive-bytes}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - bytes\n\n### _async method_ `send_text(data)` {#WebSocket-send-text}\n\n- **参数**\n  - `data` (str)\n\n- **返回**\n  - None\n\n### _async method_ `send_bytes(data)` {#WebSocket-send-bytes}\n\n- **参数**\n  - `data` (bytes)\n\n- **返回**\n  - None\n\n## _class_ `Driver(env, config)` {#Driver}\n\n- **参数**\n  - `env` ([Env](../config.md#Env))\n\n  - `config` ([Config](../config.md#Config))\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/api/exception.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 10\ndescription: nonebot.exception 模块\n---\n\n# nonebot.exception\n\n本模块包含了所有 NoneBot 运行时可能会抛出的异常。\n\n这些异常并非所有需要用户处理，在 NoneBot 内部运行时被捕获，并进行对应操作。\n\n```bash\nNoneBotException\n├── ParserExit\n├── ProcessException\n|   ├── IgnoredException\n|   ├── SkippedException\n|   |   └── TypeMisMatch\n|   ├── MockApiException\n|   └── StopPropagation\n├── MatcherException\n|   ├── PausedException\n|   ├── RejectedException\n|   └── FinishedException\n├── AdapterException\n|   ├── NoLogException\n|   ├── ApiNotAvailable\n|   ├── NetworkError\n|   └── ActionFailed\n└── DriverException\n    └── WebSocketClosed\n```\n\n## _class_ `NoneBotException(<auto>)` {#NoneBotException}\n\n- **说明:** 所有 NoneBot 发生的异常基类。\n\n- **参数**\n\n  auto\n\n## _class_ `ParserExit(<auto>)` {#ParserExit}\n\n- **说明:** 处理消息失败时返回的异常。\n\n- **参数**\n\n  auto\n\n## _class_ `ProcessException(<auto>)` {#ProcessException}\n\n- **说明:** 事件处理过程中发生的异常基类。\n\n- **参数**\n\n  auto\n\n## _class_ `IgnoredException(<auto>)` {#IgnoredException}\n\n- **说明:** 指示 NoneBot 应该忽略该事件。可由 PreProcessor 抛出。\n\n- **参数**\n  - `reason`: 忽略事件的原因\n\n## _class_ `SkippedException(<auto>)` {#SkippedException}\n\n- **说明**\n\n  指示 NoneBot 立即结束当前 `Dependent` 的运行。\n\n  例如，可以在 `Handler` 中通过 [Matcher.skip](matcher.md#Matcher-skip) 抛出。\n\n- **参数**\n\n  auto\n\n- **用法**\n\n  ```python\n  def always_skip():\n      Matcher.skip()\n\n  @matcher.handle()\n  async def handler(dependency = Depends(always_skip)):\n      # never run\n  ```\n\n## _class_ `TypeMisMatch(<auto>)` {#TypeMisMatch}\n\n- **说明:** 当前 `Handler` 的参数类型不匹配。\n\n- **参数**\n\n  auto\n\n## _class_ `MockApiException(<auto>)` {#MockApiException}\n\n- **说明:** 指示 NoneBot 阻止本次 API 调用或修改本次调用返回值，并返回自定义内容。 可由 api hook 抛出。\n\n- **参数**\n  - `result`: 返回的内容\n\n## _class_ `StopPropagation(<auto>)` {#StopPropagation}\n\n- **说明**\n\n  指示 NoneBot 终止事件向下层传播。\n\n  在 [Matcher.block](matcher.md#Matcher-block) 为 `True`\n  或使用 [Matcher.stop_propagation](matcher.md#Matcher-stop-propagation) 方法时抛出。\n\n- **参数**\n\n  auto\n\n- **用法**\n\n  ```python\n  matcher = on_notice(block=True)\n  # 或者\n  @matcher.handle()\n  async def handler(matcher: Matcher):\n      matcher.stop_propagation()\n  ```\n\n## _class_ `MatcherException(<auto>)` {#MatcherException}\n\n- **说明:** 所有 Matcher 发生的异常基类。\n\n- **参数**\n\n  auto\n\n## _class_ `PausedException(<auto>)` {#PausedException}\n\n- **说明**\n\n  指示 NoneBot 结束当前 `Handler` 并等待下一条消息后继续下一个 `Handler`。 可用于用户输入新信息。\n\n  可以在 `Handler` 中通过 [Matcher.pause](matcher.md#Matcher-pause) 抛出。\n\n- **参数**\n\n  auto\n\n- **用法**\n\n  ```python\n  @matcher.handle()\n  async def handler():\n      await matcher.pause(\"some message\")\n  ```\n\n## _class_ `RejectedException(<auto>)` {#RejectedException}\n\n- **说明**\n\n  指示 NoneBot 结束当前 `Handler` 并等待下一条消息后重新运行当前 `Handler`。 可用于用户重新输入。\n\n  可以在 `Handler` 中通过 [Matcher.reject](matcher.md#Matcher-reject) 抛出。\n\n- **参数**\n\n  auto\n\n- **用法**\n\n  ```python\n  @matcher.handle()\n  async def handler():\n      await matcher.reject(\"some message\")\n  ```\n\n## _class_ `FinishedException(<auto>)` {#FinishedException}\n\n- **说明**\n\n  指示 NoneBot 结束当前 `Handler` 且后续 `Handler` 不再被运行。可用于结束用户会话。\n\n  可以在 `Handler` 中通过 [Matcher.finish](matcher.md#Matcher-finish) 抛出。\n\n- **参数**\n\n  auto\n\n- **用法**\n\n  ```python\n  @matcher.handle()\n  async def handler():\n      await matcher.finish(\"some message\")\n  ```\n\n## _class_ `AdapterException(<auto>)` {#AdapterException}\n\n- **说明:** 代表 `Adapter` 抛出的异常，所有的 `Adapter` 都要在内部继承自这个 `Exception`。\n\n- **参数**\n  - `adapter_name`: 标识 adapter\n\n## _class_ `NoLogException(<auto>)` {#NoLogException}\n\n- **说明**\n\n  指示 NoneBot 对当前 `Event` 进行处理但不显示 Log 信息。\n\n  可在 [Event.get_log_string](adapters/index.md#Event-get-log-string) 时抛出\n\n- **参数**\n\n  auto\n\n## _class_ `ApiNotAvailable(<auto>)` {#ApiNotAvailable}\n\n- **说明:** 在 API 连接不可用时抛出。\n\n- **参数**\n\n  auto\n\n## _class_ `NetworkError(<auto>)` {#NetworkError}\n\n- **说明:** 在网络出现问题时抛出， 如: API 请求地址不正确, API 请求无返回或返回状态非正常等。\n\n- **参数**\n\n  auto\n\n## _class_ `ActionFailed(<auto>)` {#ActionFailed}\n\n- **说明:** API 请求成功返回数据，但 API 操作失败。\n\n- **参数**\n\n  auto\n\n## _class_ `DriverException(<auto>)` {#DriverException}\n\n- **说明:** `Driver` 抛出的异常基类。\n\n- **参数**\n\n  auto\n\n## _class_ `WebSocketClosed(<auto>)` {#WebSocketClosed}\n\n- **说明:** WebSocket 连接已关闭。\n\n- **参数**\n\n  auto\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/api/index.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 0\ndescription: nonebot 模块\n---\n\n# nonebot\n\n本模块主要定义了 NoneBot 启动所需函数，供 bot 入口文件调用。\n\n## 快捷导入\n\n为方便使用，本模块从子模块导入了部分内容，以下内容可以直接通过本模块导入:\n\n- `on` => [`on`](plugin/on.md#on)\n- `on_metaevent` => [`on_metaevent`](plugin/on.md#on-metaevent)\n- `on_message` => [`on_message`](plugin/on.md#on-message)\n- `on_notice` => [`on_notice`](plugin/on.md#on-notice)\n- `on_request` => [`on_request`](plugin/on.md#on-request)\n- `on_startswith` => [`on_startswith`](plugin/on.md#on-startswith)\n- `on_endswith` => [`on_endswith`](plugin/on.md#on-endswith)\n- `on_fullmatch` => [`on_fullmatch`](plugin/on.md#on-fullmatch)\n- `on_keyword` => [`on_keyword`](plugin/on.md#on-keyword)\n- `on_command` => [`on_command`](plugin/on.md#on-command)\n- `on_shell_command` => [`on_shell_command`](plugin/on.md#on-shell-command)\n- `on_regex` => [`on_regex`](plugin/on.md#on-regex)\n- `on_type` => [`on_type`](plugin/on.md#on-type)\n- `CommandGroup` => [`CommandGroup`](plugin/on.md#CommandGroup)\n- `Matchergroup` => [`MatcherGroup`](plugin/on.md#MatcherGroup)\n- `load_plugin` => [`load_plugin`](plugin/load.md#load-plugin)\n- `load_plugins` => [`load_plugins`](plugin/load.md#load-plugins)\n- `load_all_plugins` => [`load_all_plugins`](plugin/load.md#load-all-plugins)\n- `load_from_json` => [`load_from_json`](plugin/load.md#load-from-json)\n- `load_from_toml` => [`load_from_toml`](plugin/load.md#load-from-toml)\n- `load_builtin_plugin` =>\n  [`load_builtin_plugin`](plugin/load.md#load-builtin-plugin)\n- `load_builtin_plugins` =>\n  [`load_builtin_plugins`](plugin/load.md#load-builtin-plugins)\n- `get_plugin` => [`get_plugin`](plugin/index.md#get-plugin)\n- `get_plugin_by_module_name` =>\n  [`get_plugin_by_module_name`](plugin/index.md#get-plugin-by-module-name)\n- `get_loaded_plugins` =>\n  [`get_loaded_plugins`](plugin/index.md#get-loaded-plugins)\n- `get_available_plugin_names` =>\n  [`get_available_plugin_names`](plugin/index.md#get-available-plugin-names)\n- `get_plugin_config` => [`get_plugin_config`](plugin/index.md#get-plugin-config)\n- `require` => [`require`](plugin/load.md#require)\n\n## _def_ `get_driver()` {#get-driver}\n\n- **说明**\n\n  获取全局 [Driver](drivers/index.md#Driver) 实例。\n\n  可用于在计划任务的回调等情形中获取当前 [Driver](drivers/index.md#Driver) 实例。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - [Driver](drivers/index.md#Driver): 全局 [Driver](drivers/index.md#Driver) 对象\n\n- **异常**\n  - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用)\n\n- **用法**\n\n  ```python\n  driver = nonebot.get_driver()\n  ```\n\n## _def_ `get_adapter(name)` {#get-adapter}\n\n- **说明:** 获取已注册的 [Adapter](adapters/index.md#Adapter) 实例。\n\n- **重载**\n\n  **1.** `(name) -> Adapter`\n  - **参数**\n    - `name` (str): 适配器名称\n\n  - **返回**\n    - [Adapter](adapters/index.md#Adapter): 指定名称的 [Adapter](adapters/index.md#Adapter) 对象\n\n  **2.** `(name) -> A`\n  - **参数**\n    - `name` (type[A]): 适配器类型\n\n  - **返回**\n    - A: 指定类型的 [Adapter](adapters/index.md#Adapter) 对象\n\n- **异常**\n  - ValueError: 指定的 [Adapter](adapters/index.md#Adapter) 未注册\n\n  - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用)\n\n- **用法**\n\n  ```python\n  from nonebot.adapters.console import Adapter\n  adapter = nonebot.get_adapter(Adapter)\n  ```\n\n## _def_ `get_adapters()` {#get-adapters}\n\n- **说明:** 获取所有已注册的 [Adapter](adapters/index.md#Adapter) 实例。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - dict[str, [Adapter](adapters/index.md#Adapter)]: 所有 [Adapter](adapters/index.md#Adapter) 实例字典\n\n- **异常**\n  - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用)\n\n- **用法**\n\n  ```python\n  adapters = nonebot.get_adapters()\n  ```\n\n## _def_ `get_app()` {#get-app}\n\n- **说明:** 获取全局 [ASGIMixin](drivers/index.md#ASGIMixin) 对应的 Server App 对象。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - Any: Server App 对象\n\n- **异常**\n  - AssertionError: 全局 Driver 对象不是 [ASGIMixin](drivers/index.md#ASGIMixin) 类型\n\n  - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用)\n\n- **用法**\n\n  ```python\n  app = nonebot.get_app()\n  ```\n\n## _def_ `get_asgi()` {#get-asgi}\n\n- **说明:** 获取全局 [ASGIMixin](drivers/index.md#ASGIMixin) 对应的 [ASGI](https://asgi.readthedocs.io/) 对象。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - Any: ASGI 对象\n\n- **异常**\n  - AssertionError: 全局 Driver 对象不是 [ASGIMixin](drivers/index.md#ASGIMixin) 类型\n\n  - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用)\n\n- **用法**\n\n  ```python\n  asgi = nonebot.get_asgi()\n  ```\n\n## _def_ `get_bot(self_id=None)` {#get-bot}\n\n- **说明**\n\n  获取一个连接到 NoneBot 的 [Bot](adapters/index.md#Bot) 对象。\n\n  当提供 `self_id` 时，此函数是 `get_bots()[self_id]` 的简写；\n  当不提供时，返回一个 [Bot](adapters/index.md#Bot)。\n\n- **参数**\n  - `self_id` (str | None): 用来识别 [Bot](adapters/index.md#Bot) 的 [Bot.self_id](adapters/index.md#Bot-self-id) 属性\n\n- **返回**\n  - [Bot](adapters/index.md#Bot): [Bot](adapters/index.md#Bot) 对象\n\n- **异常**\n  - KeyError: 对应 self_id 的 Bot 不存在\n\n  - ValueError: 没有传入 self_id 且没有 Bot 可用\n\n  - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用)\n\n- **用法**\n\n  ```python\n  assert nonebot.get_bot(\"12345\") == nonebot.get_bots()[\"12345\"]\n\n  another_unspecified_bot = nonebot.get_bot()\n  ```\n\n## _def_ `get_bots()` {#get-bots}\n\n- **说明:** 获取所有连接到 NoneBot 的 [Bot](adapters/index.md#Bot) 对象。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - dict[str, [Bot](adapters/index.md#Bot)]: 一个以 [Bot.self_id](adapters/index.md#Bot-self-id) 为键\n\n    [Bot](adapters/index.md#Bot) 对象为值的字典\n\n- **异常**\n  - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用)\n\n- **用法**\n\n  ```python\n  bots = nonebot.get_bots()\n  ```\n\n## _def_ `init(*, _env_file=None, **kwargs)` {#init}\n\n- **说明**\n\n  初始化 NoneBot 以及 全局 [Driver](drivers/index.md#Driver) 对象。\n\n  NoneBot 将会从 .env 文件中读取环境信息，并使用相应的 env 文件配置。\n\n  也可以传入自定义的 `_env_file` 来指定 NoneBot 从该文件读取配置。\n\n- **参数**\n  - `_env_file` (DOTENV_TYPE | None): 配置文件名，默认从 `.env.{env_name}` 中读取配置\n\n  - `**kwargs` (Any): 任意变量，将会存储到 [Driver.config](drivers/index.md#Driver-config) 对象里\n\n- **返回**\n  - None\n\n- **用法**\n\n  ```python\n  nonebot.init(database=Database(...))\n  ```\n\n## _def_ `run(*args, **kwargs)` {#run}\n\n- **说明:** 启动 NoneBot，即运行全局 [Driver](drivers/index.md#Driver) 对象。\n\n- **参数**\n  - `*args` (Any): 传入 [Driver.run](drivers/index.md#Driver-run) 的位置参数\n\n  - `**kwargs` (Any): 传入 [Driver.run](drivers/index.md#Driver-run) 的命名参数\n\n- **返回**\n  - None\n\n- **用法**\n\n  ```python\n  nonebot.run(host=\"127.0.0.1\", port=8080)\n  ```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/api/log.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 7\ndescription: nonebot.log 模块\n---\n\n# nonebot.log\n\n本模块定义了 NoneBot 的日志记录 Logger。\n\nNoneBot 使用 [`loguru`][loguru] 来记录日志信息。\n\n自定义 logger 请参考 [自定义日志](https://nonebot.dev/docs/appendices/log)\n以及 [`loguru`][loguru] 文档。\n\n[loguru]: https://github.com/Delgan/loguru\n\n## _var_ `logger` {#logger}\n\n- **类型:** Logger\n\n- **说明**\n\n  NoneBot 日志记录器对象。\n\n  默认信息:\n  - 格式: `[%(asctime)s %(name)s] %(levelname)s: %(message)s`\n  - 等级: `INFO` ，根据 `config.log_level` 配置改变\n  - 输出: 输出至 stdout\n\n- **用法**\n\n  ```python\n  from nonebot.log import logger\n  ```\n\n## _class_ `LoguruHandler(<auto>)` {#LoguruHandler}\n\n- **说明:** logging 与 loguru 之间的桥梁，将 logging 的日志转发到 loguru。\n\n- **参数**\n\n  auto\n\n### _method_ `emit(record)` {#LoguruHandler-emit}\n\n- **参数**\n  - `record` (logging.LogRecord)\n\n- **返回**\n  - untyped\n\n## _def_ `default_filter(record)` {#default-filter}\n\n- **说明:** 默认的日志过滤器，根据 `config.log_level` 配置改变日志等级。\n\n- **参数**\n  - `record` (Record)\n\n- **返回**\n  - untyped\n\n## _var_ `default_format` {#default-format}\n\n- **类型:** str\n\n- **说明:** 默认日志格式\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/api/matcher.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 3\ndescription: nonebot.matcher 模块\n---\n\n# nonebot.matcher\n\n本模块实现事件响应器的创建与运行，并提供一些快捷方法来帮助用户更好的与机器人进行对话。\n\n## _var_ `DEFAULT_PROVIDER_CLASS` {#DEFAULT-PROVIDER-CLASS}\n\n- **类型:** untyped\n\n- **说明:** 默认存储器类型\n\n## _class_ `Matcher()` {#Matcher}\n\n- **说明:** 事件响应器类\n\n- **参数**\n\n  empty\n\n### _class-var_ `type` {#Matcher-type}\n\n- **类型:** ClassVar[str]\n\n- **说明:** 事件响应器类型\n\n### _class-var_ `rule` {#Matcher-rule}\n\n- **类型:** ClassVar[[Rule](rule.md#Rule)]\n\n- **说明:** 事件响应器匹配规则\n\n### _class-var_ `permission` {#Matcher-permission}\n\n- **类型:** ClassVar[[Permission](permission.md#Permission)]\n\n- **说明:** 事件响应器触发权限\n\n### _class-var_ `handlers` {#Matcher-handlers}\n\n- **类型:** ClassVar[list[[Dependent](dependencies/index.md#Dependent)[Any]]]\n\n- **说明:** 事件响应器拥有的事件处理函数列表\n\n### _class-var_ `priority` {#Matcher-priority}\n\n- **类型:** ClassVar[int]\n\n- **说明:** 事件响应器优先级\n\n### _class-var_ `block` {#Matcher-block}\n\n- **类型:** bool\n\n- **说明:** 事件响应器是否阻止事件传播\n\n### _class-var_ `temp` {#Matcher-temp}\n\n- **类型:** ClassVar[bool]\n\n- **说明:** 事件响应器是否为临时\n\n### _class-var_ `expire_time` {#Matcher-expire-time}\n\n- **类型:** ClassVar[datetime | None]\n\n- **说明:** 事件响应器过期时间点\n\n### _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}\n\n- **说明:** 创建一个新的事件响应器，并存储至 `matchers <#matchers>`\\_\n\n- **参数**\n  - `type_` (str): 事件响应器类型，与 `event.get_type()` 一致时触发，空字符串表示任意\n\n  - `rule` ([Rule](rule.md#Rule) | None): 匹配规则\n\n  - `permission` ([Permission](permission.md#Permission) | None): 权限\n\n  - `handlers` (list[[T\\_Handler](typing.md#T-Handler) | [Dependent](dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器，即触发一次后删除\n\n  - `priority` (int): 响应优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级的响应器传播\n\n  - `plugin` ([Plugin](plugin/model.md#Plugin) | None): **Deprecated.** 事件响应器所在插件\n\n  - `module` (ModuleType | None): **Deprecated.** 事件响应器所在模块\n\n  - `source` (MatcherSource | None): 事件响应器源代码上下文信息\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `default_state` ([T_State](typing.md#T-State) | None): 默认状态 `state`\n\n  - `default_type_updater` ([T_TypeUpdater](typing.md#T-TypeUpdater) | [Dependent](dependencies/index.md#Dependent)[str] | None): 默认事件类型更新函数\n\n  - `default_permission_updater` ([T_PermissionUpdater](typing.md#T-PermissionUpdater) | [Dependent](dependencies/index.md#Dependent)[[Permission](permission.md#Permission)] | None): 默认会话权限更新函数\n\n- **返回**\n  - type[Matcher]: 新的事件响应器类\n\n### _classmethod_ `destroy()` {#Matcher-destroy}\n\n- **说明:** 销毁当前的事件响应器\n\n- **参数**\n\n  empty\n\n- **返回**\n  - None\n\n### _classmethod_ `check_perm(bot, event, stack=None, dependency_cache=None)` {#Matcher-check-perm}\n\n- **说明:** 检查是否满足触发权限\n\n- **参数**\n  - `bot` ([Bot](adapters/index.md#Bot)): Bot 对象\n\n  - `event` ([Event](adapters/index.md#Event)): 上报事件\n\n  - `stack` (AsyncExitStack | None): 异步上下文栈\n\n  - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None): 依赖缓存\n\n- **返回**\n  - bool: 是否满足权限\n\n### _classmethod_ `check_rule(bot, event, state, stack=None, dependency_cache=None)` {#Matcher-check-rule}\n\n- **说明:** 检查是否满足匹配规则\n\n- **参数**\n  - `bot` ([Bot](adapters/index.md#Bot)): Bot 对象\n\n  - `event` ([Event](adapters/index.md#Event)): 上报事件\n\n  - `state` ([T_State](typing.md#T-State)): 当前状态\n\n  - `stack` (AsyncExitStack | None): 异步上下文栈\n\n  - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None): 依赖缓存\n\n- **返回**\n  - bool: 是否满足匹配规则\n\n### _classmethod_ `type_updater(func)` {#Matcher-type-updater}\n\n- **说明:** 装饰一个函数来更改当前事件响应器的默认响应事件类型更新函数\n\n- **参数**\n  - `func` ([T_TypeUpdater](typing.md#T-TypeUpdater)): 响应事件类型更新函数\n\n- **返回**\n  - [T_TypeUpdater](typing.md#T-TypeUpdater)\n\n### _classmethod_ `permission_updater(func)` {#Matcher-permission-updater}\n\n- **说明:** 装饰一个函数来更改当前事件响应器的默认会话权限更新函数\n\n- **参数**\n  - `func` ([T_PermissionUpdater](typing.md#T-PermissionUpdater)): 会话权限更新函数\n\n- **返回**\n  - [T_PermissionUpdater](typing.md#T-PermissionUpdater)\n\n### _classmethod_ `append_handler(handler, parameterless=None)` {#Matcher-append-handler}\n\n- **参数**\n  - `handler` ([T_Handler](typing.md#T-Handler))\n\n  - `parameterless` (Iterable[Any] | None)\n\n- **返回**\n  - [Dependent](dependencies/index.md#Dependent)[Any]\n\n### _classmethod_ `handle(parameterless=None)` {#Matcher-handle}\n\n- **说明:** 装饰一个函数来向事件响应器直接添加一个处理函数\n\n- **参数**\n  - `parameterless` (Iterable[Any] | None): 非参数类型依赖列表\n\n- **返回**\n  - ([T_Handler](typing.md#T-Handler)) -> [T_Handler](typing.md#T-Handler)\n\n### _classmethod_ `receive(id=\"\", parameterless=None)` {#Matcher-receive}\n\n- **说明:** 装饰一个函数来指示 NoneBot 在接收用户新的一条消息后继续运行该函数\n\n- **参数**\n  - `id` (str): 消息 ID\n\n  - `parameterless` (Iterable[Any] | None): 非参数类型依赖列表\n\n- **返回**\n  - ([T_Handler](typing.md#T-Handler)) -> [T_Handler](typing.md#T-Handler)\n\n### _classmethod_ `got(key, prompt=None, parameterless=None)` {#Matcher-got}\n\n- **说明**\n\n  装饰一个函数来指示 NoneBot 获取一个参数 `key`\n\n  当要获取的 `key` 不存在时接收用户新的一条消息再运行该函数，\n  如果 `key` 已存在则直接继续运行\n\n- **参数**\n  - `key` (str): 参数名\n\n  - `prompt` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate) | None): 在参数不存在时向用户发送的消息\n\n  - `parameterless` (Iterable[Any] | None): 非参数类型依赖列表\n\n- **返回**\n  - ([T_Handler](typing.md#T-Handler)) -> [T_Handler](typing.md#T-Handler)\n\n### _classmethod_ `send(message, **kwargs)` {#Matcher-send}\n\n- **说明:** 发送一条消息给当前交互用户\n\n- **参数**\n  - `message` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate)): 消息内容\n\n  - `**kwargs` (Any): [Bot.send](adapters/index.md#Bot-send) 的参数， 请参考对应 adapter 的 bot 对象 api\n\n- **返回**\n  - Any\n\n### _classmethod_ `finish(message=None, **kwargs)` {#Matcher-finish}\n\n- **说明:** 发送一条消息给当前交互用户并结束当前事件响应器\n\n- **参数**\n  - `message` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate) | None): 消息内容\n\n  - `**kwargs`: [Bot.send](adapters/index.md#Bot-send) 的参数， 请参考对应 adapter 的 bot 对象 api\n\n- **返回**\n  - NoReturn\n\n### _classmethod_ `pause(prompt=None, **kwargs)` {#Matcher-pause}\n\n- **说明:** 发送一条消息给当前交互用户并暂停事件响应器，在接收用户新的一条消息后继续下一个处理函数\n\n- **参数**\n  - `prompt` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate) | None): 消息内容\n\n  - `**kwargs`: [Bot.send](adapters/index.md#Bot-send) 的参数， 请参考对应 adapter 的 bot 对象 api\n\n- **返回**\n  - NoReturn\n\n### _classmethod_ `reject(prompt=None, **kwargs)` {#Matcher-reject}\n\n- **说明:** 最近使用 `got` / `receive` 接收的消息不符合预期， 发送一条消息给当前交互用户并将当前事件处理流程中断在当前位置，在接收用户新的一个事件后从头开始执行当前处理函数\n\n- **参数**\n  - `prompt` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate) | None): 消息内容\n\n  - `**kwargs`: [Bot.send](adapters/index.md#Bot-send) 的参数， 请参考对应 adapter 的 bot 对象 api\n\n- **返回**\n  - NoReturn\n\n### _classmethod_ `reject_arg(key, prompt=None, **kwargs)` {#Matcher-reject-arg}\n\n- **说明:** 最近使用 `got` 接收的消息不符合预期， 发送一条消息给当前交互用户并将当前事件处理流程中断在当前位置，在接收用户新的一条消息后从头开始执行当前处理函数\n\n- **参数**\n  - `key` (str): 参数名\n\n  - `prompt` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate) | None): 消息内容\n\n  - `**kwargs`: [Bot.send](adapters/index.md#Bot-send) 的参数， 请参考对应 adapter 的 bot 对象 api\n\n- **返回**\n  - NoReturn\n\n### _classmethod_ `reject_receive(id=\"\", prompt=None, **kwargs)` {#Matcher-reject-receive}\n\n- **说明:** 最近使用 `receive` 接收的消息不符合预期， 发送一条消息给当前交互用户并将当前事件处理流程中断在当前位置，在接收用户新的一个事件后从头开始执行当前处理函数\n\n- **参数**\n  - `id` (str): 消息 id\n\n  - `prompt` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate) | None): 消息内容\n\n  - `**kwargs`: [Bot.send](adapters/index.md#Bot-send) 的参数， 请参考对应 adapter 的 bot 对象 api\n\n- **返回**\n  - NoReturn\n\n### _classmethod_ `skip()` {#Matcher-skip}\n\n- **说明**\n\n  跳过当前事件处理函数，继续下一个处理函数\n\n  通常在事件处理函数的依赖中使用。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - NoReturn\n\n### _method_ `get_receive(id, default=None)` {#Matcher-get-receive}\n\n- **说明**\n\n  获取一个 `receive` 事件\n\n  如果没有找到对应的事件，返回 `default` 值\n\n- **重载**\n\n  **1.** `(id) -> Event | None`\n  - **参数**\n    - `id` (str)\n\n  - **返回**\n    - [Event](adapters/index.md#Event) | None\n\n  **2.** `(id, default) -> Event | T`\n  - **参数**\n    - `id` (str)\n\n    - `default` (T)\n\n  - **返回**\n    - [Event](adapters/index.md#Event) | T\n\n### _method_ `set_receive(id, event)` {#Matcher-set-receive}\n\n- **说明:** 设置一个 `receive` 事件\n\n- **参数**\n  - `id` (str)\n\n  - `event` ([Event](adapters/index.md#Event))\n\n- **返回**\n  - None\n\n### _method_ `get_last_receive(default=None)` {#Matcher-get-last-receive}\n\n- **说明**\n\n  获取最近一次 `receive` 事件\n\n  如果没有事件，返回 `default` 值\n\n- **重载**\n\n  **1.** `() -> Event | None`\n  - **参数**\n\n    empty\n\n  - **返回**\n    - [Event](adapters/index.md#Event) | None\n\n  **2.** `(default) -> Event | T`\n  - **参数**\n    - `default` (T)\n\n  - **返回**\n    - [Event](adapters/index.md#Event) | T\n\n### _method_ `get_arg(key, default=None)` {#Matcher-get-arg}\n\n- **说明**\n\n  获取一个 `got` 消息\n\n  如果没有找到对应的消息，返回 `default` 值\n\n- **重载**\n\n  **1.** `(key) -> Message | None`\n  - **参数**\n    - `key` (str)\n\n  - **返回**\n    - [Message](adapters/index.md#Message) | None\n\n  **2.** `(key, default) -> Message | T`\n  - **参数**\n    - `key` (str)\n\n    - `default` (T)\n\n  - **返回**\n    - [Message](adapters/index.md#Message) | T\n\n### _method_ `set_arg(key, message)` {#Matcher-set-arg}\n\n- **说明:** 设置一个 `got` 消息\n\n- **参数**\n  - `key` (str)\n\n  - `message` ([Message](adapters/index.md#Message))\n\n- **返回**\n  - None\n\n### _method_ `set_target(target, cache=True)` {#Matcher-set-target}\n\n- **参数**\n  - `target` (str)\n\n  - `cache` (bool)\n\n- **返回**\n  - None\n\n### _method_ `get_target(default=None)` {#Matcher-get-target}\n\n- **重载**\n\n  **1.** `() -> str | None`\n  - **参数**\n\n    empty\n\n  - **返回**\n    - str | None\n\n  **2.** `(default) -> str | T`\n  - **参数**\n    - `default` (T)\n\n  - **返回**\n    - str | T\n\n### _method_ `stop_propagation()` {#Matcher-stop-propagation}\n\n- **说明:** 阻止事件传播\n\n- **参数**\n\n  empty\n\n- **返回**\n  - untyped\n\n### _async method_ `update_type(bot, event, stack=None, dependency_cache=None)` {#Matcher-update-type}\n\n- **参数**\n  - `bot` ([Bot](adapters/index.md#Bot))\n\n  - `event` ([Event](adapters/index.md#Event))\n\n  - `stack` (AsyncExitStack | None)\n\n  - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None)\n\n- **返回**\n  - str\n\n### _async method_ `update_permission(bot, event, stack=None, dependency_cache=None)` {#Matcher-update-permission}\n\n- **参数**\n  - `bot` ([Bot](adapters/index.md#Bot))\n\n  - `event` ([Event](adapters/index.md#Event))\n\n  - `stack` (AsyncExitStack | None)\n\n  - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None)\n\n- **返回**\n  - [Permission](permission.md#Permission)\n\n### _async method_ `resolve_reject()` {#Matcher-resolve-reject}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - untyped\n\n### _method_ `ensure_context(bot, event)` {#Matcher-ensure-context}\n\n- **参数**\n  - `bot` ([Bot](adapters/index.md#Bot))\n\n  - `event` ([Event](adapters/index.md#Event))\n\n- **返回**\n  - untyped\n\n### _async method_ `simple_run(bot, event, state, stack=None, dependency_cache=None)` {#Matcher-simple-run}\n\n- **参数**\n  - `bot` ([Bot](adapters/index.md#Bot))\n\n  - `event` ([Event](adapters/index.md#Event))\n\n  - `state` ([T_State](typing.md#T-State))\n\n  - `stack` (AsyncExitStack | None)\n\n  - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None)\n\n- **返回**\n  - untyped\n\n### _async method_ `run(bot, event, state, stack=None, dependency_cache=None)` {#Matcher-run}\n\n- **参数**\n  - `bot` ([Bot](adapters/index.md#Bot))\n\n  - `event` ([Event](adapters/index.md#Event))\n\n  - `state` ([T_State](typing.md#T-State))\n\n  - `stack` (AsyncExitStack | None)\n\n  - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None)\n\n- **返回**\n  - untyped\n\n## _class_ `MatcherManager()` {#MatcherManager}\n\n- **说明**\n\n  事件响应器管理器\n\n  实现了常用字典操作，用于管理事件响应器。\n\n- **参数**\n\n  empty\n\n### _method_ `keys()` {#MatcherManager-keys}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - KeysView[int]\n\n### _method_ `values()` {#MatcherManager-values}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - ValuesView[list[type[[Matcher](#Matcher)]]]\n\n### _method_ `items()` {#MatcherManager-items}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - ItemsView[int, list[type[[Matcher](#Matcher)]]]\n\n### _method_ `get(key, default=None)` {#MatcherManager-get}\n\n- **重载**\n\n  **1.** `(key) -> list[type[Matcher]] | None`\n  - **参数**\n    - `key` (int)\n\n  - **返回**\n    - list[type[[Matcher](#Matcher)]] | None\n\n  **2.** `(key, default) -> list[type[Matcher]]`\n  - **参数**\n    - `key` (int)\n\n    - `default` (list[type[[Matcher](#Matcher)]])\n\n  - **返回**\n    - list[type[[Matcher](#Matcher)]]\n\n  **3.** `(key, default) -> list[type[Matcher]] | T`\n  - **参数**\n    - `key` (int)\n\n    - `default` (T)\n\n  - **返回**\n    - list[type[[Matcher](#Matcher)]] | T\n\n### _method_ `pop(key)` {#MatcherManager-pop}\n\n- **参数**\n  - `key` (int)\n\n- **返回**\n  - list[type[[Matcher](#Matcher)]]\n\n### _method_ `popitem()` {#MatcherManager-popitem}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - tuple[int, list[type[[Matcher](#Matcher)]]]\n\n### _method_ `clear()` {#MatcherManager-clear}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - None\n\n### _method_ `update(m, /)` {#MatcherManager-update}\n\n- **参数**\n  - `m` (MutableMapping[int, list[type[[Matcher](#Matcher)]]])\n\n- **返回**\n  - None\n\n### _method_ `setdefault(key, default)` {#MatcherManager-setdefault}\n\n- **参数**\n  - `key` (int)\n\n  - `default` (list[type[[Matcher](#Matcher)]])\n\n- **返回**\n  - list[type[[Matcher](#Matcher)]]\n\n### _method_ `set_provider(provider_class)` {#MatcherManager-set-provider}\n\n- **说明:** 设置事件响应器存储器\n\n- **参数**\n  - `provider_class` (type[[MatcherProvider](#MatcherProvider)]): 事件响应器存储器类\n\n- **返回**\n  - None\n\n## _abstract class_ `MatcherProvider(matchers)` {#MatcherProvider}\n\n- **说明:** 事件响应器存储器基类\n\n- **参数**\n  - `matchers` (Mapping[int, list[type[[Matcher](#Matcher)]]]): 当前存储器中已有的事件响应器\n\n## _var_ `matchers` {#matchers}\n\n- **类型:** untyped\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/api/message.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 2\ndescription: nonebot.message 模块\n---\n\n# nonebot.message\n\n本模块定义了事件处理主要流程。\n\nNoneBot 内部处理并按优先级分发事件给所有事件响应器，提供了多个插槽以进行事件的预处理等。\n\n## _def_ `event_preprocessor(func)` {#event-preprocessor}\n\n- **说明**\n\n  事件预处理。\n\n  装饰一个函数，使它在每次接收到事件并分发给各响应器之前执行。\n\n- **参数**\n  - `func` ([T_EventPreProcessor](typing.md#T-EventPreProcessor))\n\n- **返回**\n  - [T_EventPreProcessor](typing.md#T-EventPreProcessor)\n\n## _def_ `event_postprocessor(func)` {#event-postprocessor}\n\n- **说明**\n\n  事件后处理。\n\n  装饰一个函数，使它在每次接收到事件并分发给各响应器之后执行。\n\n- **参数**\n  - `func` ([T_EventPostProcessor](typing.md#T-EventPostProcessor))\n\n- **返回**\n  - [T_EventPostProcessor](typing.md#T-EventPostProcessor)\n\n## _def_ `run_preprocessor(func)` {#run-preprocessor}\n\n- **说明**\n\n  运行预处理。\n\n  装饰一个函数，使它在每次事件响应器运行前执行。\n\n- **参数**\n  - `func` ([T_RunPreProcessor](typing.md#T-RunPreProcessor))\n\n- **返回**\n  - [T_RunPreProcessor](typing.md#T-RunPreProcessor)\n\n## _def_ `run_postprocessor(func)` {#run-postprocessor}\n\n- **说明**\n\n  运行后处理。\n\n  装饰一个函数，使它在每次事件响应器运行后执行。\n\n- **参数**\n  - `func` ([T_RunPostProcessor](typing.md#T-RunPostProcessor))\n\n- **返回**\n  - [T_RunPostProcessor](typing.md#T-RunPostProcessor)\n\n## _async def_ `check_and_run_matcher(Matcher, bot, event, state, stack=None, dependency_cache=None)` {#check-and-run-matcher}\n\n- **说明:** 检查并运行事件响应器。\n\n- **参数**\n  - `Matcher` (type[[Matcher](matcher.md#Matcher)]): 事件响应器\n\n  - `bot` ([Bot](adapters/index.md#Bot)): Bot 对象\n\n  - `event` ([Event](adapters/index.md#Event)): Event 对象\n\n  - `state` ([T_State](typing.md#T-State)): 会话状态\n\n  - `stack` (AsyncExitStack | None): 异步上下文栈\n\n  - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None): 依赖缓存\n\n- **返回**\n  - None\n\n## _async def_ `handle_event(bot, event)` {#handle-event}\n\n- **说明:** 处理一个事件。调用该函数以实现分发事件。\n\n- **参数**\n  - `bot` ([Bot](adapters/index.md#Bot)): Bot 对象\n\n  - `event` ([Event](adapters/index.md#Event)): Event 对象\n\n- **返回**\n  - None\n\n- **用法**\n\n  ```python\n  driver.task_group.start_soon(handle_event, bot, event)\n  ```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/api/params.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 4\ndescription: nonebot.params 模块\n---\n\n# nonebot.params\n\n本模块定义了依赖注入的各类参数。\n\n## _def_ `Arg(key=None)` {#Arg}\n\n- **说明:** Arg 参数消息\n\n- **参数**\n  - `key` (str | None)\n\n- **返回**\n  - Any\n\n## _class_ `ArgParam(*args, key, type, **kwargs)` {#ArgParam}\n\n- **说明**\n\n  Arg 注入参数\n\n  本注入解析事件响应器操作 `got` 所获取的参数。\n\n  可以通过 `Arg`、`ArgStr`、`ArgPlainText` 等函数参数 `key` 指定获取的参数，\n  留空则会根据参数名称获取。\n\n- **参数**\n  - `*args`\n\n  - `key` (str)\n\n  - `type` (Literal['message', 'str', 'plaintext', 'prompt'])\n\n  - `**kwargs` (Any)\n\n## _def_ `ArgPlainText(key=None)` {#ArgPlainText}\n\n- **说明:** Arg 参数消息纯文本\n\n- **参数**\n  - `key` (str | None)\n\n- **返回**\n  - str\n\n## _def_ `ArgPromptResult(key=None)` {#ArgPromptResult}\n\n- **说明:** `arg` prompt 发送结果\n\n- **参数**\n  - `key` (str | None)\n\n- **返回**\n  - Any\n\n## _def_ `ArgStr(key=None)` {#ArgStr}\n\n- **说明:** Arg 参数消息文本\n\n- **参数**\n  - `key` (str | None)\n\n- **返回**\n  - str\n\n## _class_ `BotParam(*args, checker=None, **kwargs)` {#BotParam}\n\n- **说明**\n\n  注入参数。\n\n  本注入解析所有类型为且仅为 [Bot](adapters/index.md#Bot) 及其子类或 `None` 的参数。\n\n  为保证兼容性，本注入还会解析名为 `bot` 且没有类型注解的参数。\n\n- **参数**\n  - `*args`\n\n  - `checker` ([ModelField](compat.md#ModelField) | None)\n\n  - `**kwargs` (Any)\n\n## _class_ `DefaultParam(*args, validate=False, **kwargs)` {#DefaultParam}\n\n- **说明**\n\n  默认值注入参数\n\n  本注入解析所有剩余未能解析且具有默认值的参数。\n\n  本注入参数应该具有最低优先级，因此应该在所有其他注入参数之后使用。\n\n- **参数**\n  - `*args`\n\n  - `validate` (bool)\n\n  - `**kwargs` (Any)\n\n## _class_ `DependParam(*args, dependent, use_cache, **kwargs)` {#DependParam}\n\n- **说明**\n\n  子依赖注入参数。\n\n  本注入解析所有子依赖注入，然后将它们的返回值作为参数值传递给父依赖。\n\n  本注入应该具有最高优先级，因此应该在其他参数之前检查。\n\n- **参数**\n  - `*args`\n\n  - `dependent` ([Dependent](dependencies/index.md#Dependent)[Any])\n\n  - `use_cache` (bool)\n\n  - `**kwargs` (Any)\n\n## _def_ `Depends(dependency=None, *, use_cache=True, validate=False)` {#Depends}\n\n- **说明:** 子依赖装饰器\n\n- **参数**\n  - `dependency` ([T_Handler](typing.md#T-Handler) | None): 依赖函数。默认为参数的类型注释。\n\n  - `use_cache` (bool): 是否使用缓存。默认为 `True`。\n\n  - `validate` (bool | PydanticFieldInfo): 是否使用 Pydantic 类型校验。默认为 `False`。\n\n- **返回**\n  - Any\n\n- **用法**\n\n  ```python\n  def depend_func() -> Any:\n      return ...\n\n  def depend_gen_func():\n      try:\n          yield ...\n      finally:\n          ...\n\n  async def handler(\n      param_name: Any = Depends(depend_func),\n      gen: Any = Depends(depend_gen_func),\n  ):\n      ...\n  ```\n\n## _class_ `EventParam(*args, checker=None, **kwargs)` {#EventParam}\n\n- **说明**\n\n  注入参数\n\n  本注入解析所有类型为且仅为 [Event](adapters/index.md#Event) 及其子类或 `None` 的参数。\n\n  为保证兼容性，本注入还会解析名为 `event` 且没有类型注解的参数。\n\n- **参数**\n  - `*args`\n\n  - `checker` ([ModelField](compat.md#ModelField) | None)\n\n  - `**kwargs` (Any)\n\n## _class_ `ExceptionParam(*args, validate=False, **kwargs)` {#ExceptionParam}\n\n- **说明**\n\n  的异常注入参数\n\n  本注入解析所有类型为 `Exception` 或 `None` 的参数。\n\n  为保证兼容性，本注入还会解析名为 `exception` 且没有类型注解的参数。\n\n- **参数**\n  - `*args`\n\n  - `validate` (bool)\n\n  - `**kwargs` (Any)\n\n## _class_ `MatcherParam(*args, checker=None, **kwargs)` {#MatcherParam}\n\n- **说明**\n\n  事件响应器实例注入参数\n\n  本注入解析所有类型为且仅为 [Matcher](matcher.md#Matcher) 及其子类或 `None` 的参数。\n\n  为保证兼容性，本注入还会解析名为 `matcher` 且没有类型注解的参数。\n\n- **参数**\n  - `*args`\n\n  - `checker` ([ModelField](compat.md#ModelField) | None)\n\n  - `**kwargs` (Any)\n\n## _class_ `StateParam(*args, validate=False, **kwargs)` {#StateParam}\n\n- **说明**\n\n  事件处理状态注入参数\n\n  本注入解析所有类型为 `T_State` 的参数。\n\n  为保证兼容性，本注入还会解析名为 `state` 且没有类型注解的参数。\n\n- **参数**\n  - `*args`\n\n  - `validate` (bool)\n\n  - `**kwargs` (Any)\n\n## _def_ `EventType()` {#EventType}\n\n- **说明:** 类型参数\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n## _def_ `EventMessage()` {#EventMessage}\n\n- **说明:** 消息参数\n\n- **参数**\n\n  empty\n\n- **返回**\n  - Any\n\n## _def_ `EventPlainText()` {#EventPlainText}\n\n- **说明:** 纯文本消息参数\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n## _def_ `EventToMe()` {#EventToMe}\n\n- **说明:** `to_me` 参数\n\n- **参数**\n\n  empty\n\n- **返回**\n  - bool\n\n## _def_ `Command()` {#Command}\n\n- **说明:** 消息命令元组\n\n- **参数**\n\n  empty\n\n- **返回**\n  - tuple[str, ...]\n\n## _def_ `RawCommand()` {#RawCommand}\n\n- **说明:** 消息命令文本\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n## _def_ `CommandArg()` {#CommandArg}\n\n- **说明:** 消息命令参数\n\n- **参数**\n\n  empty\n\n- **返回**\n  - Any\n\n## _def_ `CommandStart()` {#CommandStart}\n\n- **说明:** 消息命令开头\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n## _def_ `CommandWhitespace()` {#CommandWhitespace}\n\n- **说明:** 消息命令与参数之间的空白\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n## _def_ `ShellCommandArgs()` {#ShellCommandArgs}\n\n- **说明:** shell 命令解析后的参数字典\n\n- **参数**\n\n  empty\n\n- **返回**\n  - Any\n\n## _def_ `ShellCommandArgv()` {#ShellCommandArgv}\n\n- **说明:** shell 命令原始参数列表\n\n- **参数**\n\n  empty\n\n- **返回**\n  - Any\n\n## _def_ `RegexMatched()` {#RegexMatched}\n\n- **说明:** 正则匹配结果\n\n- **参数**\n\n  empty\n\n- **返回**\n  - Match[str]\n\n## _def_ `RegexStr(*groups)` {#RegexStr}\n\n- **说明:** 正则匹配结果文本\n\n- **重载**\n\n  **1.** `(group, /) -> str`\n  - **参数**\n    - `group` (Literal[0])\n\n  - **返回**\n    - str\n\n  **2.** `(group, /) -> str | Any`\n  - **参数**\n    - `group` (str | int)\n\n  - **返回**\n    - str | Any\n\n  **3.** `(group1, group2, /, *groups) -> tuple[str | Any, ...]`\n  - **参数**\n    - `group1` (str | int)\n\n    - `group2` (str | int)\n\n    - `*groups` (str | int)\n\n  - **返回**\n    - tuple[str | Any, ...]\n\n## _def_ `RegexGroup()` {#RegexGroup}\n\n- **说明:** 正则匹配结果 group 元组\n\n- **参数**\n\n  empty\n\n- **返回**\n  - tuple[Any, ...]\n\n## _def_ `RegexDict()` {#RegexDict}\n\n- **说明:** 正则匹配结果 group 字典\n\n- **参数**\n\n  empty\n\n- **返回**\n  - dict[str, Any]\n\n## _def_ `Startswith()` {#Startswith}\n\n- **说明:** 响应触发前缀\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n## _def_ `Endswith()` {#Endswith}\n\n- **说明:** 响应触发后缀\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n## _def_ `Fullmatch()` {#Fullmatch}\n\n- **说明:** 响应触发完整消息\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n## _def_ `Keyword()` {#Keyword}\n\n- **说明:** 响应触发关键字\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n## _def_ `Received(id=None, default=None)` {#Received}\n\n- **说明:** `receive` 事件参数\n\n- **参数**\n  - `id` (str | None)\n\n  - `default` (Any)\n\n- **返回**\n  - Any\n\n## _def_ `LastReceived(default=None)` {#LastReceived}\n\n- **说明:** `last_receive` 事件参数\n\n- **参数**\n  - `default` (Any)\n\n- **返回**\n  - Any\n\n## _def_ `ReceivePromptResult(id=None)` {#ReceivePromptResult}\n\n- **说明:** `receive` prompt 发送结果\n\n- **参数**\n  - `id` (str | None)\n\n- **返回**\n  - Any\n\n## _def_ `PausePromptResult()` {#PausePromptResult}\n\n- **说明:** `pause` prompt 发送结果\n\n- **参数**\n\n  empty\n\n- **返回**\n  - Any\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/api/permission.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 6\ndescription: nonebot.permission 模块\n---\n\n# nonebot.permission\n\n本模块是 [Matcher.permission](matcher.md#Matcher-permission) 的类型定义。\n\n每个[事件响应器](matcher.md#Matcher)\n拥有一个 [Permission](#Permission)，其中是 `PermissionChecker` 的集合。\n只要有一个 `PermissionChecker` 检查结果为 `True` 时就会继续运行。\n\n## _def_ `USER(*users, perm=None)` {#USER}\n\n- **说明**\n\n  匹配当前事件属于指定会话。\n\n  如果 `perm` 中仅有 `User` 类型的权限检查函数，则会去除原有检查函数的会话 ID 限制。\n\n- **参数**\n  - `*users` (str)\n\n  - `perm` (Permission | None): 需要同时满足的权限\n\n  - `user`: 会话白名单\n\n- **返回**\n  - untyped\n\n## _class_ `Permission(*checkers)` {#Permission}\n\n- **说明**\n\n  权限类。\n\n  当事件传递时，在 [Matcher](matcher.md#Matcher) 运行前进行检查。\n\n- **参数**\n  - `*checkers` ([T_PermissionChecker](typing.md#T-PermissionChecker) | [Dependent](dependencies/index.md#Dependent)[bool]): PermissionChecker\n\n- **用法**\n\n  ```python\n  Permission(async_function) | sync_function\n  # 等价于\n  Permission(async_function, sync_function)\n  ```\n\n### _instance-var_ `checkers` {#Permission-checkers}\n\n- **类型:** set[[Dependent](dependencies/index.md#Dependent)[bool]]\n\n- **说明:** 存储 `PermissionChecker`\n\n### _async method_ `__call__(bot, event, stack=None, dependency_cache=None)` {#Permission---call--}\n\n- **说明:** 检查是否满足某个权限。\n\n- **参数**\n  - `bot` ([Bot](adapters/index.md#Bot)): Bot 对象\n\n  - `event` ([Event](adapters/index.md#Event)): Event 对象\n\n  - `stack` (AsyncExitStack | None): 异步上下文栈\n\n  - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None): 依赖缓存\n\n- **返回**\n  - bool\n\n## _class_ `User(users, perm=None)` {#User}\n\n- **说明:** 检查当前事件是否属于指定会话。\n\n- **参数**\n  - `users` (tuple[str, ...]): 会话 ID 元组\n\n  - `perm` (Permission | None): 需同时满足的权限\n\n### _classmethod_ `from_event(event, perm=None)` {#User-from-event}\n\n- **说明**\n\n  从事件中获取会话 ID。\n\n  如果 `perm` 中仅有 `User` 类型的权限检查函数，则会去除原有的会话 ID 限制。\n\n- **参数**\n  - `event` ([Event](adapters/index.md#Event)): Event 对象\n\n  - `perm` (Permission | None): 需同时满足的权限\n\n- **返回**\n  - Self\n\n### _classmethod_ `from_permission(*users, perm=None)` {#User-from-permission}\n\n- **说明**\n\n  指定会话与权限。\n\n  如果 `perm` 中仅有 `User` 类型的权限检查函数，则会去除原有的会话 ID 限制。\n\n- **参数**\n  - `*users` (str): 会话白名单\n\n  - `perm` (Permission | None): 需同时满足的权限\n\n- **返回**\n  - Self\n\n## _class_ `Message(<auto>)` {#Message}\n\n- **说明:** 检查是否为消息事件\n\n- **参数**\n\n  auto\n\n## _class_ `Notice(<auto>)` {#Notice}\n\n- **说明:** 检查是否为通知事件\n\n- **参数**\n\n  auto\n\n## _class_ `Request(<auto>)` {#Request}\n\n- **说明:** 检查是否为请求事件\n\n- **参数**\n\n  auto\n\n## _class_ `MetaEvent(<auto>)` {#MetaEvent}\n\n- **说明:** 检查是否为元事件\n\n- **参数**\n\n  auto\n\n## _var_ `MESSAGE` {#MESSAGE}\n\n- **类型:** [Permission](#Permission)\n\n- **说明**\n\n  匹配任意 `message` 类型事件\n\n  仅在需要同时捕获不同类型事件时使用，优先使用 message type 的 Matcher。\n\n## _var_ `NOTICE` {#NOTICE}\n\n- **类型:** [Permission](#Permission)\n\n- **说明**\n\n  匹配任意 `notice` 类型事件\n\n  仅在需要同时捕获不同类型事件时使用，优先使用 notice type 的 Matcher。\n\n## _var_ `REQUEST` {#REQUEST}\n\n- **类型:** [Permission](#Permission)\n\n- **说明**\n\n  匹配任意 `request` 类型事件\n\n  仅在需要同时捕获不同类型事件时使用，优先使用 request type 的 Matcher。\n\n## _var_ `METAEVENT` {#METAEVENT}\n\n- **类型:** [Permission](#Permission)\n\n- **说明**\n\n  匹配任意 `meta_event` 类型事件\n\n  仅在需要同时捕获不同类型事件时使用，优先使用 meta_event type 的 Matcher。\n\n## _class_ `SuperUser(<auto>)` {#SuperUser}\n\n- **说明:** 检查当前事件是否是消息事件且属于超级管理员\n\n- **参数**\n\n  auto\n\n## _var_ `SUPERUSER` {#SUPERUSER}\n\n- **类型:** [Permission](#Permission)\n\n- **说明:** 匹配任意超级用户事件\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/api/plugin/_category_.json",
    "content": "{\n  \"position\": 12\n}\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/api/plugin/index.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 0\ndescription: nonebot.plugin 模块\n---\n\n# nonebot.plugin\n\n本模块为 NoneBot 插件开发提供便携的定义函数。\n\n## 快捷导入\n\n为方便使用，本模块从子模块导入了部分内容，以下内容可以直接通过本模块导入:\n\n- `on` => [`on`](on.md#on)\n- `on_metaevent` => [`on_metaevent`](on.md#on-metaevent)\n- `on_message` => [`on_message`](on.md#on-message)\n- `on_notice` => [`on_notice`](on.md#on-notice)\n- `on_request` => [`on_request`](on.md#on-request)\n- `on_startswith` => [`on_startswith`](on.md#on-startswith)\n- `on_endswith` => [`on_endswith`](on.md#on-endswith)\n- `on_fullmatch` => [`on_fullmatch`](on.md#on-fullmatch)\n- `on_keyword` => [`on_keyword`](on.md#on-keyword)\n- `on_command` => [`on_command`](on.md#on-command)\n- `on_shell_command` => [`on_shell_command`](on.md#on-shell-command)\n- `on_regex` => [`on_regex`](on.md#on-regex)\n- `on_type` => [`on_type`](on.md#on-type)\n- `CommandGroup` => [`CommandGroup`](on.md#CommandGroup)\n- `Matchergroup` => [`MatcherGroup`](on.md#MatcherGroup)\n- `load_plugin` => [`load_plugin`](load.md#load-plugin)\n- `load_plugins` => [`load_plugins`](load.md#load-plugins)\n- `load_all_plugins` => [`load_all_plugins`](load.md#load-all-plugins)\n- `load_from_json` => [`load_from_json`](load.md#load-from-json)\n- `load_from_toml` => [`load_from_toml`](load.md#load-from-toml)\n- `load_builtin_plugin` =>\n  [`load_builtin_plugin`](load.md#load-builtin-plugin)\n- `load_builtin_plugins` =>\n  [`load_builtin_plugins`](load.md#load-builtin-plugins)\n- `require` => [`require`](load.md#require)\n- `PluginMetadata` => [`PluginMetadata`](model.md#PluginMetadata)\n\n## _def_ `get_plugin(plugin_id)` {#get-plugin}\n\n- **说明**\n\n  获取已经导入的某个插件。\n\n  如果为 `load_plugins` 文件夹导入的插件，则为文件(夹)名。\n\n  如果为嵌套的子插件，标识符为 `父插件标识符:子插件文件(夹)名`。\n\n- **参数**\n  - `plugin_id` (str): 插件标识符，即 [Plugin.id\\_](model.md#Plugin-id-)。\n\n- **返回**\n  - [Plugin](model.md#Plugin) | None\n\n## _def_ `get_plugin_by_module_name(module_name)` {#get-plugin-by-module-name}\n\n- **说明**\n\n  通过模块名获取已经导入的某个插件。\n\n  如果提供的模块名为某个插件的子模块，同样会返回该插件。\n\n- **参数**\n  - `module_name` (str): 模块名，即 [Plugin.module_name](model.md#Plugin-module-name)。\n\n- **返回**\n  - [Plugin](model.md#Plugin) | None\n\n## _def_ `get_loaded_plugins()` {#get-loaded-plugins}\n\n- **说明:** 获取当前已导入的所有插件。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - set[[Plugin](model.md#Plugin)]\n\n## _def_ `get_available_plugin_names()` {#get-available-plugin-names}\n\n- **说明:** 获取当前所有可用的插件标识符（包含尚未加载的插件）。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - set[str]\n\n## _def_ `get_plugin_config(config)` {#get-plugin-config}\n\n- **说明:** 从全局配置获取当前插件需要的配置项。\n\n- **参数**\n  - `config` (type[C])\n\n- **返回**\n  - C\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/api/plugin/load.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 1\ndescription: nonebot.plugin.load 模块\n---\n\n# nonebot.plugin.load\n\n本模块定义插件加载接口。\n\n## _def_ `load_plugin(module_path)` {#load-plugin}\n\n- **说明:** 加载单个插件，可以是本地插件或是通过 `pip` 安装的插件。\n\n- **参数**\n  - `module_path` (str | Path): 插件名称 `path.to.your.plugin` 或插件路径 `pathlib.Path(path/to/your/plugin)`\n\n- **返回**\n  - [Plugin](model.md#Plugin) | None\n\n## _def_ `load_plugins(*plugin_dir)` {#load-plugins}\n\n- **说明:** 导入文件夹下多个插件，以 `_` 开头的插件不会被导入!\n\n- **参数**\n  - `*plugin_dir` (str): 文件夹路径\n\n- **返回**\n  - set[[Plugin](model.md#Plugin)]\n\n## _def_ `load_all_plugins(module_path, plugin_dir)` {#load-all-plugins}\n\n- **说明:** 导入指定列表中的插件以及指定目录下多个插件，以 `_` 开头的插件不会被导入!\n\n- **参数**\n  - `module_path` (Iterable[str]): 指定插件集合\n\n  - `plugin_dir` (Iterable[str]): 指定文件夹路径集合\n\n- **返回**\n  - set[[Plugin](model.md#Plugin)]\n\n## _def_ `load_from_json(file_path, encoding=\"utf-8\")` {#load-from-json}\n\n- **说明:** 导入指定 json 文件中的 `plugins` 以及 `plugin_dirs` 下多个插件。 以 `_` 开头的插件不会被导入!\n\n- **参数**\n  - `file_path` (str): 指定 json 文件路径\n\n  - `encoding` (str): 指定 json 文件编码\n\n- **返回**\n  - set[[Plugin](model.md#Plugin)]\n\n- **用法**\n\n  ```json title=plugins.json\n  {\n    \"plugins\": [\"some_plugin\"],\n    \"plugin_dirs\": [\"some_dir\"]\n  }\n  ```\n\n  ```python\n  nonebot.load_from_json(\"plugins.json\")\n  ```\n\n## _def_ `load_from_toml(file_path, encoding=\"utf-8\")` {#load-from-toml}\n\n- **说明:** 导入指定 toml 文件 `[tool.nonebot]` 中的 `plugins` 以及 `plugin_dirs` 下多个插件。 以 `_` 开头的插件不会被导入!\n\n- **参数**\n  - `file_path` (str): 指定 toml 文件路径\n\n  - `encoding` (str): 指定 toml 文件编码\n\n- **返回**\n  - set[[Plugin](model.md#Plugin)]\n\n- **用法**\n\n  ```toml title=pyproject.toml\n  [tool.nonebot]\n  plugins = [\"some_plugin\"]\n  plugin_dirs = [\"some_dir\"]\n  ```\n\n  ```python\n  nonebot.load_from_toml(\"pyproject.toml\")\n  ```\n\n## _def_ `load_builtin_plugin(name)` {#load-builtin-plugin}\n\n- **说明:** 导入 NoneBot 内置插件。\n\n- **参数**\n  - `name` (str): 插件名称\n\n- **返回**\n  - [Plugin](model.md#Plugin) | None\n\n## _def_ `load_builtin_plugins(*plugins)` {#load-builtin-plugins}\n\n- **说明:** 导入多个 NoneBot 内置插件。\n\n- **参数**\n  - `*plugins` (str): 插件名称列表\n\n- **返回**\n  - set[[Plugin](model.md#Plugin)]\n\n## _def_ `require(name)` {#require}\n\n- **说明:** 声明依赖插件。\n\n- **参数**\n  - `name` (str): 插件模块名或插件标识符，仅在已声明插件的情况下可使用标识符。\n\n- **返回**\n  - ModuleType\n\n- **异常**\n  - RuntimeError: 插件无法加载\n\n## _def_ `inherit_supported_adapters(*names)` {#inherit-supported-adapters}\n\n- **说明**\n\n  获取已加载插件的适配器支持状态集合。\n\n  如果传入了多个插件名称，返回值会自动取交集。\n\n- **参数**\n  - `*names` (str): 插件名称列表。\n\n- **返回**\n  - set[str] | None\n\n- **异常**\n  - RuntimeError: 插件未加载\n\n  - ValueError: 插件缺少元数据\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/api/plugin/manager.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 5\ndescription: nonebot.plugin.manager 模块\n---\n\n# nonebot.plugin.manager\n\n本模块实现插件加载流程。\n\n参考: [import hooks](https://docs.python.org/3/reference/import.html#import-hooks), [PEP302](https://www.python.org/dev/peps/pep-0302/)\n\n## _class_ `PluginManager(plugins=None, search_path=None)` {#PluginManager}\n\n- **说明:** 插件管理器。\n\n- **参数**\n  - `plugins` (Iterable[str] | None): 独立插件模块名集合。\n\n  - `search_path` (Iterable[str] | None): 插件搜索路径（文件夹），相对于当前工作目录。\n\n### _property_ `third_party_plugins` {#PluginManager-third-party-plugins}\n\n- **类型:** set[str]\n\n- **说明:** 返回所有独立插件标识符。\n\n### _property_ `searched_plugins` {#PluginManager-searched-plugins}\n\n- **类型:** set[str]\n\n- **说明:** 返回已搜索到的插件标识符。\n\n### _property_ `available_plugins` {#PluginManager-available-plugins}\n\n- **类型:** set[str]\n\n- **说明:** 返回当前插件管理器中可用的插件标识符。\n\n### _property_ `controlled_modules` {#PluginManager-controlled-modules}\n\n- **类型:** dict[str, str]\n\n- **说明:** 返回当前插件管理器中控制的插件标识符与模块路径映射字典。\n\n### _method_ `load_plugin(name)` {#PluginManager-load-plugin}\n\n- **说明**\n\n  加载指定插件。\n\n  可以使用完整插件模块名或者插件标识符加载。\n\n- **参数**\n  - `name` (str): 插件名称或插件标识符。\n\n- **返回**\n  - [Plugin](model.md#Plugin) | None\n\n### _method_ `load_all_plugins()` {#PluginManager-load-all-plugins}\n\n- **说明:** 加载所有可用插件。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - set[[Plugin](model.md#Plugin)]\n\n## _class_ `PluginFinder(<auto>)` {#PluginFinder}\n\n- **参数**\n\n  auto\n\n### _method_ `find_spec(fullname, path, target=None)` {#PluginFinder-find-spec}\n\n- **参数**\n  - `fullname` (str)\n\n  - `path` (Sequence[str] | None)\n\n  - `target` (ModuleType | None)\n\n- **返回**\n  - untyped\n\n## _class_ `PluginLoader(manager, fullname, path)` {#PluginLoader}\n\n- **参数**\n  - `manager` (PluginManager)\n\n  - `fullname` (str)\n\n  - `path` (str)\n\n### _method_ `create_module(spec)` {#PluginLoader-create-module}\n\n- **参数**\n  - `spec`\n\n- **返回**\n  - ModuleType | None\n\n### _method_ `exec_module(module)` {#PluginLoader-exec-module}\n\n- **参数**\n  - `module` (ModuleType)\n\n- **返回**\n  - None\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/api/plugin/model.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 3\ndescription: nonebot.plugin.model 模块\n---\n\n# nonebot.plugin.model\n\n本模块定义插件相关信息。\n\n## _class_ `PluginMetadata(<auto>)` {#PluginMetadata}\n\n- **说明:** 插件元信息，由插件编写者提供\n\n- **参数**\n\n  auto\n\n### _instance-var_ `name` {#PluginMetadata-name}\n\n- **类型:** str\n\n- **说明:** 插件名称\n\n### _instance-var_ `description` {#PluginMetadata-description}\n\n- **类型:** str\n\n- **说明:** 插件功能介绍\n\n### _instance-var_ `usage` {#PluginMetadata-usage}\n\n- **类型:** str\n\n- **说明:** 插件使用方法\n\n### _class-var_ `type` {#PluginMetadata-type}\n\n- **类型:** str | None\n\n- **说明:** 插件类型，用于商店分类\n\n### _class-var_ `homepage` {#PluginMetadata-homepage}\n\n- **类型:** str | None\n\n- **说明:** 插件主页\n\n### _class-var_ `config` {#PluginMetadata-config}\n\n- **类型:** type[BaseModel] | None\n\n- **说明:** 插件配置项\n\n### _class-var_ `supported_adapters` {#PluginMetadata-supported-adapters}\n\n- **类型:** set[str] | None\n\n- **说明**\n\n  插件支持的适配器模块路径\n\n  格式为 `<module>[:<Adapter>]`，`~` 为 `nonebot.adapters.` 的缩写。\n\n  `None` 表示支持**所有适配器**。\n\n### _class-var_ `extra` {#PluginMetadata-extra}\n\n- **类型:** dict[Any, Any]\n\n- **说明:** 插件额外信息，可由插件编写者自由扩展定义\n\n### _method_ `get_supported_adapters()` {#PluginMetadata-get-supported-adapters}\n\n- **说明:** 获取当前已安装的插件支持适配器类列表\n\n- **参数**\n\n  empty\n\n- **返回**\n  - set[type[[Adapter](../adapters/index.md#Adapter)]] | None\n\n## _class_ `Plugin(<auto>)` {#Plugin}\n\n- **说明:** 存储插件信息\n\n- **参数**\n\n  auto\n\n### _instance-var_ `name` {#Plugin-name}\n\n- **类型:** str\n\n- **说明:** 插件名称，NoneBot 使用 文件/文件夹 名称作为插件名称\n\n### _instance-var_ `module` {#Plugin-module}\n\n- **类型:** ModuleType\n\n- **说明:** 插件模块对象\n\n### _instance-var_ `module_name` {#Plugin-module-name}\n\n- **类型:** str\n\n- **说明:** 点分割模块路径\n\n### _instance-var_ `manager` {#Plugin-manager}\n\n- **类型:** [PluginManager](manager.md#PluginManager)\n\n- **说明:** 导入该插件的插件管理器\n\n### _class-var_ `matcher` {#Plugin-matcher}\n\n- **类型:** set[type[[Matcher](../matcher.md#Matcher)]]\n\n- **说明:** 插件加载时定义的 `Matcher`\n\n### _class-var_ `parent_plugin` {#Plugin-parent-plugin}\n\n- **类型:** Plugin | None\n\n- **说明:** 父插件\n\n### _class-var_ `sub_plugins` {#Plugin-sub-plugins}\n\n- **类型:** set[Plugin]\n\n- **说明:** 子插件集合\n\n### _class-var_ `metadata` {#Plugin-metadata}\n\n- **类型:** PluginMetadata | None\n\n- **说明:** 插件元信息\n\n### _property_ `id_` {#Plugin-id-}\n\n- **类型:** str\n\n- **说明:** 插件索引标识\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/api/plugin/on.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 2\ndescription: nonebot.plugin.on 模块\n---\n\n# nonebot.plugin.on\n\n本模块定义事件响应器便携定义函数。\n\n## _def_ `store_matcher(matcher)` {#store-matcher}\n\n- **说明:** 存储一个事件响应器到插件。\n\n- **参数**\n  - `matcher` (type[[Matcher](../matcher.md#Matcher)]): 事件响应器\n\n- **返回**\n  - None\n\n## _def_ `get_matcher_plugin(depth=...)` {#get-matcher-plugin}\n\n- **说明**\n\n  获取事件响应器定义所在插件。\n\n  **Deprecated**, 请使用 [get_matcher_source](#get-matcher-source) 获取信息。\n\n- **参数**\n  - `depth` (int): 调用栈深度\n\n- **返回**\n  - [Plugin](model.md#Plugin) | None\n\n## _def_ `get_matcher_module(depth=...)` {#get-matcher-module}\n\n- **说明**\n\n  获取事件响应器定义所在模块。\n\n  **Deprecated**, 请使用 [get_matcher_source](#get-matcher-source) 获取信息。\n\n- **参数**\n  - `depth` (int): 调用栈深度\n\n- **返回**\n  - ModuleType | None\n\n## _def_ `get_matcher_source(depth=...)` {#get-matcher-source}\n\n- **说明:** 获取事件响应器定义所在源码信息。\n\n- **参数**\n  - `depth` (int): 调用栈深度\n\n- **返回**\n  - MatcherSource | None\n\n## _def_ `on(type=\"\", rule=..., permission=..., *, handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on}\n\n- **说明:** 注册一个基础事件响应器，可自定义类型。\n\n- **参数**\n  - `type` (str): 事件响应器类型\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n## _def_ `on_metaevent(rule=..., permission=..., *, handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-metaevent}\n\n- **说明:** 注册一个元事件响应器。\n\n- **参数**\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n## _def_ `on_message(rule=..., permission=..., *, handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-message}\n\n- **说明:** 注册一个消息事件响应器。\n\n- **参数**\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n## _def_ `on_notice(rule=..., permission=..., *, handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-notice}\n\n- **说明:** 注册一个通知事件响应器。\n\n- **参数**\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n## _def_ `on_request(rule=..., permission=..., *, handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-request}\n\n- **说明:** 注册一个请求事件响应器。\n\n- **参数**\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n## _def_ `on_startswith(msg, rule=..., ignorecase=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-startswith}\n\n- **说明:** 注册一个消息事件响应器，并且当消息的**文本部分**以指定内容开头时响应。\n\n- **参数**\n  - `msg` (str | tuple[str, ...]): 指定消息开头内容\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `ignorecase` (bool): 是否忽略大小写\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n## _def_ `on_endswith(msg, rule=..., ignorecase=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-endswith}\n\n- **说明:** 注册一个消息事件响应器，并且当消息的**文本部分**以指定内容结尾时响应。\n\n- **参数**\n  - `msg` (str | tuple[str, ...]): 指定消息结尾内容\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `ignorecase` (bool): 是否忽略大小写\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n## _def_ `on_fullmatch(msg, rule=..., ignorecase=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-fullmatch}\n\n- **说明:** 注册一个消息事件响应器，并且当消息的**文本部分**与指定内容完全一致时响应。\n\n- **参数**\n  - `msg` (str | tuple[str, ...]): 指定消息全匹配内容\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `ignorecase` (bool): 是否忽略大小写\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n## _def_ `on_keyword(keywords, rule=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-keyword}\n\n- **说明:** 注册一个消息事件响应器，并且当消息纯文本部分包含关键词时响应。\n\n- **参数**\n  - `keywords` (set[str]): 关键词列表\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n## _def_ `on_command(cmd, rule=..., aliases=..., force_whitespace=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-command}\n\n- **说明**\n\n  注册一个消息事件响应器，并且当消息以指定命令开头时响应。\n\n  命令匹配规则参考: `命令形式匹配 <rule.md#command-command>`\\_\n\n- **参数**\n  - `cmd` (str | tuple[str, ...]): 指定命令内容\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `aliases` (set[str | tuple[str, ...]] | None): 命令别名\n\n  - `force_whitespace` (str | bool | None): 是否强制命令后必须有指定空白符\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n## _def_ `on_shell_command(cmd, rule=..., aliases=..., parser=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-shell-command}\n\n- **说明**\n\n  注册一个支持 `shell_like` 解析参数的命令消息事件响应器。\n\n  与普通的 `on_command` 不同的是，在添加 `parser` 参数时, 响应器会自动处理消息。\n\n  可以通过 [ShellCommandArgv](../params.md#ShellCommandArgv) 获取原始参数列表，\n  通过 [ShellCommandArgs](../params.md#ShellCommandArgs) 获取解析后的参数字典。\n\n- **参数**\n  - `cmd` (str | tuple[str, ...]): 指定命令内容\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `aliases` (set[str | tuple[str, ...]] | None): 命令别名\n\n  - `parser` ([ArgumentParser](../rule.md#ArgumentParser) | None): `nonebot.rule.ArgumentParser` 对象\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n## _def_ `on_regex(pattern, flags=..., rule=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-regex}\n\n- **说明**\n\n  注册一个消息事件响应器，并且当消息匹配正则表达式时响应。\n\n  命令匹配规则参考: `正则匹配 <rule.md#regex-regex-flags-0>`\\_\n\n- **参数**\n  - `pattern` (str): 正则表达式\n\n  - `flags` (int | re.RegexFlag): 正则匹配标志\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n## _def_ `on_type(types, rule=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-type}\n\n- **说明:** 注册一个事件响应器，并且当事件为指定类型时响应。\n\n- **参数**\n  - `types` (type[[Event](../adapters/index.md#Event)] | tuple[type[[Event](../adapters/index.md#Event)], ...]): 事件类型\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n## _class_ `CommandGroup(cmd, prefix_aliases=..., *, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#CommandGroup}\n\n- **参数**\n  - `cmd` (str | tuple[str, ...])\n\n  - `prefix_aliases` (bool)\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None)\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None)\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None)\n\n  - `temp` (bool)\n\n  - `expire_time` (datetime | timedelta | None)\n\n  - `priority` (int)\n\n  - `block` (bool)\n\n  - `state` ([T_State](../typing.md#T-State) | None)\n\n### _method_ `command(cmd, *, rule=..., aliases=..., force_whitespace=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#CommandGroup-command}\n\n- **说明:** 注册一个新的命令。新参数将会覆盖命令组默认值\n\n- **参数**\n  - `cmd` (str | tuple[str, ...]): 指定命令内容\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `aliases` (set[str | tuple[str, ...]] | None): 命令别名\n\n  - `force_whitespace` (str | bool | None): 是否强制命令后必须有指定空白符\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n### _method_ `shell_command(cmd, *, rule=..., aliases=..., parser=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#CommandGroup-shell-command}\n\n- **说明:** 注册一个新的 `shell_like` 命令。新参数将会覆盖命令组默认值\n\n- **参数**\n  - `cmd` (str | tuple[str, ...]): 指定命令内容\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `aliases` (set[str | tuple[str, ...]] | None): 命令别名\n\n  - `parser` ([ArgumentParser](../rule.md#ArgumentParser) | None): `nonebot.rule.ArgumentParser` 对象\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n## _class_ `MatcherGroup(*, type=..., rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup}\n\n- **参数**\n  - `type` (str)\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None)\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None)\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None)\n\n  - `temp` (bool)\n\n  - `expire_time` (datetime | timedelta | None)\n\n  - `priority` (int)\n\n  - `block` (bool)\n\n  - `state` ([T_State](../typing.md#T-State) | None)\n\n### _method_ `on(*, type=..., rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on}\n\n- **说明:** 注册一个基础事件响应器，可自定义类型。\n\n- **参数**\n  - `type` (str): 事件响应器类型\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n### _method_ `on_metaevent(*, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-metaevent}\n\n- **说明:** 注册一个元事件响应器。\n\n- **参数**\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n### _method_ `on_message(*, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-message}\n\n- **说明:** 注册一个消息事件响应器。\n\n- **参数**\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n### _method_ `on_notice(*, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-notice}\n\n- **说明:** 注册一个通知事件响应器。\n\n- **参数**\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n### _method_ `on_request(*, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-request}\n\n- **说明:** 注册一个请求事件响应器。\n\n- **参数**\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n### _method_ `on_startswith(msg, *, ignorecase=..., rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-startswith}\n\n- **说明:** 注册一个消息事件响应器，并且当消息的**文本部分**以指定内容开头时响应。\n\n- **参数**\n  - `msg` (str | tuple[str, ...]): 指定消息开头内容\n\n  - `ignorecase` (bool): 是否忽略大小写\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n### _method_ `on_endswith(msg, *, ignorecase=..., rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-endswith}\n\n- **说明:** 注册一个消息事件响应器，并且当消息的**文本部分**以指定内容结尾时响应。\n\n- **参数**\n  - `msg` (str | tuple[str, ...]): 指定消息结尾内容\n\n  - `ignorecase` (bool): 是否忽略大小写\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n### _method_ `on_fullmatch(msg, *, ignorecase=..., rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-fullmatch}\n\n- **说明:** 注册一个消息事件响应器，并且当消息的**文本部分**与指定内容完全一致时响应。\n\n- **参数**\n  - `msg` (str | tuple[str, ...]): 指定消息全匹配内容\n\n  - `ignorecase` (bool): 是否忽略大小写\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n### _method_ `on_keyword(keywords, *, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-keyword}\n\n- **说明:** 注册一个消息事件响应器，并且当消息纯文本部分包含关键词时响应。\n\n- **参数**\n  - `keywords` (set[str]): 关键词列表\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n### _method_ `on_command(cmd, aliases=..., force_whitespace=..., *, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-command}\n\n- **说明**\n\n  注册一个消息事件响应器，并且当消息以指定命令开头时响应。\n\n  命令匹配规则参考: `命令形式匹配 <rule.md#command-command>`\\_\n\n- **参数**\n  - `cmd` (str | tuple[str, ...]): 指定命令内容\n\n  - `aliases` (set[str | tuple[str, ...]] | None): 命令别名\n\n  - `force_whitespace` (str | bool | None): 是否强制命令后必须有指定空白符\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n### _method_ `on_shell_command(cmd, aliases=..., parser=..., *, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-shell-command}\n\n- **说明**\n\n  注册一个支持 `shell_like` 解析参数的命令消息事件响应器。\n\n  与普通的 `on_command` 不同的是，在添加 `parser` 参数时, 响应器会自动处理消息。\n\n  可以通过 [ShellCommandArgv](../params.md#ShellCommandArgv) 获取原始参数列表，\n  通过 [ShellCommandArgs](../params.md#ShellCommandArgs) 获取解析后的参数字典。\n\n- **参数**\n  - `cmd` (str | tuple[str, ...]): 指定命令内容\n\n  - `aliases` (set[str | tuple[str, ...]] | None): 命令别名\n\n  - `parser` ([ArgumentParser](../rule.md#ArgumentParser) | None): `nonebot.rule.ArgumentParser` 对象\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n### _method_ `on_regex(pattern, flags=..., *, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-regex}\n\n- **说明**\n\n  注册一个消息事件响应器，并且当消息匹配正则表达式时响应。\n\n  命令匹配规则参考: `正则匹配 <rule.md#regex-regex-flags-0>`\\_\n\n- **参数**\n  - `pattern` (str): 正则表达式\n\n  - `flags` (int | re.RegexFlag): 正则匹配标志\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n### _method_ `on_type(types, *, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-type}\n\n- **说明:** 注册一个事件响应器，并且当事件为指定类型时响应。\n\n- **参数**\n  - `types` (type[[Event](../adapters/index.md#Event)] | tuple[type[[Event](../adapters/index.md#Event)]]): 事件类型\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/api/rule.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 5\ndescription: nonebot.rule 模块\n---\n\n# nonebot.rule\n\n本模块是 [Matcher.rule](matcher.md#Matcher-rule) 的类型定义。\n\n每个[事件响应器](matcher.md#Matcher)拥有一个\n[Rule](#Rule)，其中是 `RuleChecker` 的集合。\n只有当所有 `RuleChecker` 检查结果为 `True` 时继续运行。\n\n## _class_ `Rule(*checkers)` {#Rule}\n\n- **说明**\n\n  规则类。\n\n  当事件传递时，在 [Matcher](matcher.md#Matcher) 运行前进行检查。\n\n- **参数**\n  - `*checkers` ([T_RuleChecker](typing.md#T-RuleChecker) | [Dependent](dependencies/index.md#Dependent)[bool]): RuleChecker\n\n- **用法**\n\n  ```python\n  Rule(async_function) & sync_function\n  # 等价于\n  Rule(async_function, sync_function)\n  ```\n\n### _instance-var_ `checkers` {#Rule-checkers}\n\n- **类型:** set[[Dependent](dependencies/index.md#Dependent)[bool]]\n\n- **说明:** 存储 `RuleChecker`\n\n### _async method_ `__call__(bot, event, state, stack=None, dependency_cache=None)` {#Rule---call--}\n\n- **说明:** 检查是否符合所有规则\n\n- **参数**\n  - `bot` ([Bot](adapters/index.md#Bot)): Bot 对象\n\n  - `event` ([Event](adapters/index.md#Event)): Event 对象\n\n  - `state` ([T_State](typing.md#T-State)): 当前 State\n\n  - `stack` (AsyncExitStack | None): 异步上下文栈\n\n  - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None): 依赖缓存\n\n- **返回**\n  - bool\n\n## _class_ `CMD_RESULT(<auto>)` {#CMD-RESULT}\n\n- **参数**\n\n  auto\n\n## _class_ `TRIE_VALUE(<auto>)` {#TRIE-VALUE}\n\n- **说明:** TRIE_VALUE(command_start, command)\n\n- **参数**\n\n  auto\n\n## _class_ `StartswithRule(msg, ignorecase=False)` {#StartswithRule}\n\n- **说明:** 检查消息纯文本是否以指定字符串开头。\n\n- **参数**\n  - `msg` (tuple[str, ...]): 指定消息开头字符串元组\n\n  - `ignorecase` (bool): 是否忽略大小写\n\n## _def_ `startswith(msg, ignorecase=False)` {#startswith}\n\n- **说明:** 匹配消息纯文本开头。\n\n- **参数**\n  - `msg` (str | tuple[str, ...]): 指定消息开头字符串元组\n\n  - `ignorecase` (bool): 是否忽略大小写\n\n- **返回**\n  - [Rule](#Rule)\n\n## _class_ `EndswithRule(msg, ignorecase=False)` {#EndswithRule}\n\n- **说明:** 检查消息纯文本是否以指定字符串结尾。\n\n- **参数**\n  - `msg` (tuple[str, ...]): 指定消息结尾字符串元组\n\n  - `ignorecase` (bool): 是否忽略大小写\n\n## _def_ `endswith(msg, ignorecase=False)` {#endswith}\n\n- **说明:** 匹配消息纯文本结尾。\n\n- **参数**\n  - `msg` (str | tuple[str, ...]): 指定消息开头字符串元组\n\n  - `ignorecase` (bool): 是否忽略大小写\n\n- **返回**\n  - [Rule](#Rule)\n\n## _class_ `FullmatchRule(msg, ignorecase=False)` {#FullmatchRule}\n\n- **说明:** 检查消息纯文本是否与指定字符串全匹配。\n\n- **参数**\n  - `msg` (tuple[str, ...]): 指定消息全匹配字符串元组\n\n  - `ignorecase` (bool): 是否忽略大小写\n\n## _def_ `fullmatch(msg, ignorecase=False)` {#fullmatch}\n\n- **说明:** 完全匹配消息。\n\n- **参数**\n  - `msg` (str | tuple[str, ...]): 指定消息全匹配字符串元组\n\n  - `ignorecase` (bool): 是否忽略大小写\n\n- **返回**\n  - [Rule](#Rule)\n\n## _class_ `KeywordsRule(*keywords)` {#KeywordsRule}\n\n- **说明:** 检查消息纯文本是否包含指定关键字。\n\n- **参数**\n  - `*keywords` (str): 指定关键字元组\n\n## _def_ `keyword(*keywords)` {#keyword}\n\n- **说明:** 匹配消息纯文本关键词。\n\n- **参数**\n  - `*keywords` (str): 指定关键字元组\n\n- **返回**\n  - [Rule](#Rule)\n\n## _class_ `CommandRule(cmds, force_whitespace=None)` {#CommandRule}\n\n- **说明:** 检查消息是否为指定命令。\n\n- **参数**\n  - `cmds` (list[tuple[str, ...]]): 指定命令元组列表\n\n  - `force_whitespace` (str | bool | None): 是否强制命令后必须有指定空白符\n\n## _def_ `command(*cmds, force_whitespace=None)` {#command}\n\n- **说明**\n\n  匹配消息命令。\n\n  根据配置里提供的 [`command_start`](config.md#Config-command-start),\n  [`command_sep`](config.md#Config-command-sep) 判断消息是否为命令。\n\n  可以通过 [Command](params.md#Command) 获取匹配成功的命令（例: `(\"test\",)`），\n  通过 [RawCommand](params.md#RawCommand) 获取匹配成功的原始命令文本（例: `\"/test\"`），\n  通过 [CommandArg](params.md#CommandArg) 获取匹配成功的命令参数。\n\n- **参数**\n  - `*cmds` (str | tuple[str, ...]): 命令文本或命令元组\n\n  - `force_whitespace` (str | bool | None): 是否强制命令后必须有指定空白符\n\n- **返回**\n  - [Rule](#Rule)\n\n- **用法**\n\n  使用默认 `command_start`, `command_sep` 配置情况下：\n\n  命令 `(\"test\",)` 可以匹配: `/test` 开头的消息\n  命令 `(\"test\", \"sub\")` 可以匹配: `/test.sub` 开头的消息\n\n:::tip 提示\n命令内容与后续消息间无需空格!\n:::\n\n## _class_ `ArgumentParser(<auto>)` {#ArgumentParser}\n\n- **说明**\n\n  `shell_like` 命令参数解析器，解析出错时不会退出程序。\n\n  支持 [Message](adapters/index.md#Message) 富文本解析。\n\n- **参数**\n\n  auto\n\n- **用法**\n\n  用法与 `argparse.ArgumentParser` 相同，\n  参考文档: [argparse](https://docs.python.org/3/library/argparse.html)\n\n### _method_ `parse_known_args(args=None, namespace=None)` {#ArgumentParser-parse-known-args}\n\n- **重载**\n\n  **1.** `(args=None, namespace=None) -> tuple[Namespace, list[str | MessageSegment]]`\n  - **参数**\n    - `args` (Sequence[str | [MessageSegment](adapters/index.md#MessageSegment)] | None)\n\n    - `namespace` (None)\n\n  - **返回**\n    - tuple[Namespace, list[str | [MessageSegment](adapters/index.md#MessageSegment)]]\n\n  **2.** `(args, namespace) -> tuple[T, list[str | MessageSegment]]`\n  - **参数**\n    - `args` (Sequence[str | [MessageSegment](adapters/index.md#MessageSegment)] | None)\n\n    - `namespace` (T)\n\n  - **返回**\n    - tuple[T, list[str | [MessageSegment](adapters/index.md#MessageSegment)]]\n\n  **3.** `(*, namespace) -> tuple[T, list[str | MessageSegment]]`\n  - **参数**\n    - `namespace` (T)\n\n  - **返回**\n    - tuple[T, list[str | [MessageSegment](adapters/index.md#MessageSegment)]]\n\n## _class_ `ShellCommandRule(cmds, parser)` {#ShellCommandRule}\n\n- **说明:** 检查消息是否为指定 shell 命令。\n\n- **参数**\n  - `cmds` (list[tuple[str, ...]]): 指定命令元组列表\n\n  - `parser` (ArgumentParser | None): 可选参数解析器\n\n## _def_ `shell_command(*cmds, parser=None)` {#shell-command}\n\n- **说明**\n\n  匹配 `shell_like` 形式的消息命令。\n\n  根据配置里提供的 [`command_start`](config.md#Config-command-start),\n  [`command_sep`](config.md#Config-command-sep) 判断消息是否为命令。\n\n  可以通过 [Command](params.md#Command) 获取匹配成功的命令\n  （例: `(\"test\",)`），\n  通过 [RawCommand](params.md#RawCommand) 获取匹配成功的原始命令文本\n  （例: `\"/test\"`），\n  通过 [ShellCommandArgv](params.md#ShellCommandArgv) 获取解析前的参数列表\n  （例: `[\"arg\", \"-h\"]`），\n  通过 [ShellCommandArgs](params.md#ShellCommandArgs) 获取解析后的参数字典\n  （例: `{\"arg\": \"arg\", \"h\": True}`）。\n\n  :::caution 警告\n  如果参数解析失败，则通过 [ShellCommandArgs](params.md#ShellCommandArgs)\n  获取的将是 [ParserExit](exception.md#ParserExit) 异常。\n  :::\n\n- **参数**\n  - `*cmds` (str | tuple[str, ...]): 命令文本或命令元组\n\n  - `parser` (ArgumentParser | None): [ArgumentParser](#ArgumentParser) 对象\n\n- **返回**\n  - [Rule](#Rule)\n\n- **用法**\n\n  使用默认 `command_start`, `command_sep` 配置，更多示例参考\n  [argparse](https://docs.python.org/3/library/argparse.html) 标准库文档。\n\n  ```python\n  from nonebot.rule import ArgumentParser\n\n  parser = ArgumentParser()\n  parser.add_argument(\"-a\", action=\"store_true\")\n\n  rule = shell_command(\"ls\", parser=parser)\n  ```\n\n:::tip 提示\n命令内容与后续消息间无需空格!\n:::\n\n## _class_ `RegexRule(regex, flags=0)` {#RegexRule}\n\n- **说明:** 检查消息字符串是否符合指定正则表达式。\n\n- **参数**\n  - `regex` (str): 正则表达式\n\n  - `flags` (int): 正则表达式标记\n\n## _def_ `regex(regex, flags=0)` {#regex}\n\n- **说明**\n\n  匹配符合正则表达式的消息字符串。\n\n  可以通过 [RegexStr](params.md#RegexStr) 获取匹配成功的字符串，\n  通过 [RegexGroup](params.md#RegexGroup) 获取匹配成功的 group 元组，\n  通过 [RegexDict](params.md#RegexDict) 获取匹配成功的 group 字典。\n\n- **参数**\n  - `regex` (str): 正则表达式\n\n  - `flags` (int | re.RegexFlag): 正则表达式标记\n\n- **返回**\n  - [Rule](#Rule)\n\n:::tip 提示\n正则表达式匹配使用 search 而非 match，如需从头匹配请使用 `r\"^xxx\"` 来确保匹配开头\n:::\n:::tip 提示\n正则表达式匹配使用 `EventMessage` 的 `str` 字符串，\n而非 `EventMessage` 的 `PlainText` 纯文本字符串\n:::\n\n## _class_ `ToMeRule(<auto>)` {#ToMeRule}\n\n- **说明:** 检查事件是否与机器人有关。\n\n- **参数**\n\n  auto\n\n## _def_ `to_me()` {#to-me}\n\n- **说明:** 匹配与机器人有关的事件。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - [Rule](#Rule)\n\n## _class_ `IsTypeRule(*types)` {#IsTypeRule}\n\n- **说明:** 检查事件类型是否为指定类型。\n\n- **参数**\n  - `*types` (type[[Event](adapters/index.md#Event)])\n\n## _def_ `is_type(*types)` {#is-type}\n\n- **说明:** 匹配事件类型。\n\n- **参数**\n  - `*types` (type[[Event](adapters/index.md#Event)]): 事件类型\n\n- **返回**\n  - [Rule](#Rule)\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/api/typing.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 11\ndescription: nonebot.typing 模块\n---\n\n# nonebot.typing\n\n本模块定义了 NoneBot 模块中共享的一些类型。\n\n使用 Python 的 Type Hint 语法，\n参考 [`PEP 484`](https://www.python.org/dev/peps/pep-0484/),\n[`PEP 526`](https://www.python.org/dev/peps/pep-0526/) 和\n[`typing`](https://docs.python.org/3/library/typing.html)。\n\n## _def_ `overrides(InterfaceClass)` {#overrides}\n\n- **说明:** 标记一个方法为父类 interface 的 implement\n\n- **参数**\n  - `InterfaceClass` (object)\n\n- **返回**\n  - untyped\n\n## _def_ `type_has_args(type_)` {#type-has-args}\n\n- **参数**\n  - `type_` (type[Any])\n\n- **返回**\n  - bool\n\n## _def_ `origin_is_union(origin)` {#origin-is-union}\n\n- **参数**\n  - `origin` (type[Any] | None)\n\n- **返回**\n  - bool\n\n## _def_ `origin_is_literal(origin)` {#origin-is-literal}\n\n- **说明:** 判断是否是 Literal 类型\n\n- **参数**\n  - `origin` (type[Any] | None)\n\n- **返回**\n  - bool\n\n## _def_ `all_literal_values(type_)` {#all-literal-values}\n\n- **说明:** 获取 Literal 类型包含的所有值\n\n- **参数**\n  - `type_` (type[Any])\n\n- **返回**\n  - list[Any]\n\n## _def_ `origin_is_annotated(origin)` {#origin-is-annotated}\n\n- **说明:** 判断是否是 Annotated 类型\n\n- **参数**\n  - `origin` (type[Any] | None)\n\n- **返回**\n  - bool\n\n## _def_ `is_none_type(type_)` {#is-none-type}\n\n- **说明:** 判断是否是 None 类型\n\n- **参数**\n  - `type_` (type[Any])\n\n- **返回**\n  - bool\n\n## _def_ `is_type_alias_type(type_)` {#is-type-alias-type}\n\n- **参数**\n  - `type_` (type[Any])\n\n- **返回**\n  - bool\n\n## _def_ `evaluate_forwardref(ref, globalns, localns)` {#evaluate-forwardref}\n\n- **参数**\n  - `ref` (ForwardRef)\n\n  - `globalns` (dict[str, Any])\n\n  - `localns` (dict[str, Any])\n\n- **返回**\n  - Any\n\n## _class_ `StateFlag(<auto>)` {#StateFlag}\n\n- **参数**\n\n  auto\n\n## _var_ `T_State` {#T-State}\n\n- **类型:** dict[Any, Any]\n\n- **说明:** 事件处理状态 State 类型\n\n## _var_ `T_BotConnectionHook` {#T-BotConnectionHook}\n\n- **类型:** \\_DependentCallable[Any]\n\n- **说明**\n\n  Bot 连接建立时钩子函数\n\n  依赖参数:\n  - DependParam: 子依赖参数\n  - BotParam: Bot 对象\n  - DefaultParam: 带有默认值的参数\n\n## _var_ `T_BotDisconnectionHook` {#T-BotDisconnectionHook}\n\n- **类型:** \\_DependentCallable[Any]\n\n- **说明**\n\n  Bot 连接断开时钩子函数\n\n  依赖参数:\n  - DependParam: 子依赖参数\n  - BotParam: Bot 对象\n  - DefaultParam: 带有默认值的参数\n\n## _var_ `T_CallingAPIHook` {#T-CallingAPIHook}\n\n- **类型:** ([Bot](adapters/index.md#Bot), str, dict[str, Any]) -> Awaitable[Any]\n\n- **说明:** `bot.call_api` 钩子函数\n\n## _var_ `T_CalledAPIHook` {#T-CalledAPIHook}\n\n- **类型:** ([Bot](adapters/index.md#Bot), Exception | None, str, dict[str, Any], Any) -> Awaitable[Any]\n\n- **说明:** `bot.call_api` 后执行的函数，参数分别为 bot, exception, api, data, result\n\n## _var_ `T_EventPreProcessor` {#T-EventPreProcessor}\n\n- **类型:** \\_DependentCallable[Any]\n\n- **说明**\n\n  事件预处理函数 EventPreProcessor 类型\n\n  依赖参数:\n  - DependParam: 子依赖参数\n  - BotParam: Bot 对象\n  - EventParam: Event 对象\n  - StateParam: State 对象\n  - DefaultParam: 带有默认值的参数\n\n## _var_ `T_EventPostProcessor` {#T-EventPostProcessor}\n\n- **类型:** \\_DependentCallable[Any]\n\n- **说明**\n\n  事件后处理函数 EventPostProcessor 类型\n\n  依赖参数:\n  - DependParam: 子依赖参数\n  - BotParam: Bot 对象\n  - EventParam: Event 对象\n  - StateParam: State 对象\n  - DefaultParam: 带有默认值的参数\n\n## _var_ `T_RunPreProcessor` {#T-RunPreProcessor}\n\n- **类型:** \\_DependentCallable[Any]\n\n- **说明**\n\n  事件响应器运行前预处理函数 RunPreProcessor 类型\n\n  依赖参数:\n  - DependParam: 子依赖参数\n  - BotParam: Bot 对象\n  - EventParam: Event 对象\n  - StateParam: State 对象\n  - MatcherParam: Matcher 对象\n  - DefaultParam: 带有默认值的参数\n\n## _var_ `T_RunPostProcessor` {#T-RunPostProcessor}\n\n- **类型:** \\_DependentCallable[Any]\n\n- **说明**\n\n  事件响应器运行后后处理函数 RunPostProcessor 类型\n\n  依赖参数:\n  - DependParam: 子依赖参数\n  - BotParam: Bot 对象\n  - EventParam: Event 对象\n  - StateParam: State 对象\n  - MatcherParam: Matcher 对象\n  - ExceptionParam: 异常对象（可能为 None）\n  - DefaultParam: 带有默认值的参数\n\n## _var_ `T_RuleChecker` {#T-RuleChecker}\n\n- **类型:** \\_DependentCallable[bool]\n\n- **说明**\n\n  RuleChecker 即判断是否响应事件的处理函数。\n\n  依赖参数:\n  - DependParam: 子依赖参数\n  - BotParam: Bot 对象\n  - EventParam: Event 对象\n  - StateParam: State 对象\n  - DefaultParam: 带有默认值的参数\n\n## _var_ `T_PermissionChecker` {#T-PermissionChecker}\n\n- **类型:** \\_DependentCallable[bool]\n\n- **说明**\n\n  PermissionChecker 即判断事件是否满足权限的处理函数。\n\n  依赖参数:\n  - DependParam: 子依赖参数\n  - BotParam: Bot 对象\n  - EventParam: Event 对象\n  - DefaultParam: 带有默认值的参数\n\n## _var_ `T_Handler` {#T-Handler}\n\n- **类型:** \\_DependentCallable[Any]\n\n- **说明:** Handler 处理函数。\n\n## _var_ `T_TypeUpdater` {#T-TypeUpdater}\n\n- **类型:** \\_DependentCallable[str]\n\n- **说明**\n\n  TypeUpdater 在 Matcher.pause, Matcher.reject 时被运行，用于更新响应的事件类型。 默认会更新为 `message`。\n\n  依赖参数:\n  - DependParam: 子依赖参数\n  - BotParam: Bot 对象\n  - EventParam: Event 对象\n  - StateParam: State 对象\n  - MatcherParam: Matcher 对象\n  - DefaultParam: 带有默认值的参数\n\n## _var_ `T_PermissionUpdater` {#T-PermissionUpdater}\n\n- **类型:** \\_DependentCallable[[Permission](permission.md#Permission)]\n\n- **说明**\n\n  PermissionUpdater 在 Matcher.pause, Matcher.reject 时被运行，用于更新会话对象权限。 默认会更新为当前事件的触发对象。\n\n  依赖参数:\n  - DependParam: 子依赖参数\n  - BotParam: Bot 对象\n  - EventParam: Event 对象\n  - StateParam: State 对象\n  - MatcherParam: Matcher 对象\n  - DefaultParam: 带有默认值的参数\n\n## _var_ `T_DependencyCache` {#T-DependencyCache}\n\n- **类型:** dict[\\_DependentCallable[Any], DependencyCache]\n\n- **说明:** 依赖缓存, 用于存储依赖函数的返回值\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/api/utils.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 8\ndescription: nonebot.utils 模块\n---\n\n# nonebot.utils\n\n本模块包含了 NoneBot 的一些工具函数\n\n## _def_ `escape_tag(s)` {#escape-tag}\n\n- **说明**\n\n  用于记录带颜色日志时转义 `<tag>` 类型特殊标签\n\n  参考: [loguru color 标签](https://loguru.readthedocs.io/en/stable/api/logger.html#color)\n\n- **参数**\n  - `s` (str): 需要转义的字符串\n\n- **返回**\n  - str\n\n## _def_ `deep_update(mapping, *updating_mappings)` {#deep-update}\n\n- **说明:** 深度更新合并字典\n\n- **参数**\n  - `mapping` (dict[K, Any])\n\n  - `*updating_mappings` (dict[K, Any])\n\n- **返回**\n  - dict[K, Any]\n\n## _def_ `lenient_issubclass(cls, class_or_tuple)` {#lenient-issubclass}\n\n- **说明:** 检查 cls 是否是 class_or_tuple 中的一个类型子类并忽略类型错误。\n\n- **参数**\n  - `cls` (Any)\n\n  - `class_or_tuple` (type[Any] | tuple[type[Any], ...])\n\n- **返回**\n  - bool\n\n## _def_ `generic_check_issubclass(cls, class_or_tuple)` {#generic-check-issubclass}\n\n- **说明**\n\n  检查 cls 是否是 class_or_tuple 中的一个类型子类。\n\n  特别的：\n  - 如果 cls 是 `typing.TypeVar` 类型，\n    则会检查其 `__bound__` 或 `__constraints__`\n    是否是 class_or_tuple 中一个类型的子类或 None。\n  - 如果 cls 是 `typing.Union` 或 `types.UnionType` 类型，\n    则会检查其中的所有类型是否是 class_or_tuple 中一个类型的子类或 None。\n  - 如果 cls 是 `typing.Literal` 类型，\n    则会检查其中的所有值是否是 class_or_tuple 中一个类型的实例。\n  - 如果 cls 是 `typing.List`、`typing.Dict` 等泛型类型，\n    则会检查其原始类型是否是 class_or_tuple 中一个类型的子类。\n\n- **参数**\n  - `cls` (Any)\n\n  - `class_or_tuple` (type[Any] | tuple[type[Any], ...])\n\n- **返回**\n  - bool\n\n## _def_ `type_is_complex(type_)` {#type-is-complex}\n\n- **说明:** 检查 type\\_ 是否是复杂类型\n\n- **参数**\n  - `type_` (type[Any])\n\n- **返回**\n  - bool\n\n## _def_ `is_coroutine_callable(call)` {#is-coroutine-callable}\n\n- **说明:** 检查 call 是否是一个 callable 协程函数\n\n- **参数**\n  - `call` ((...) -> Any)\n\n- **返回**\n  - bool\n\n## _def_ `is_gen_callable(call)` {#is-gen-callable}\n\n- **说明:** 检查 call 是否是一个生成器函数\n\n- **参数**\n  - `call` ((...) -> Any)\n\n- **返回**\n  - bool\n\n## _def_ `is_async_gen_callable(call)` {#is-async-gen-callable}\n\n- **说明:** 检查 call 是否是一个异步生成器函数\n\n- **参数**\n  - `call` ((...) -> Any)\n\n- **返回**\n  - bool\n\n## _def_ `run_sync(call)` {#run-sync}\n\n- **说明:** 一个用于包装 sync function 为 async function 的装饰器\n\n- **参数**\n  - `call` ((P) -> R): 被装饰的同步函数\n\n- **返回**\n  - (P) -> Coroutine[None, None, R]\n\n## _def_ `run_sync_ctx_manager(cm)` {#run-sync-ctx-manager}\n\n- **说明:** 一个用于包装 sync context manager 为 async context manager 的执行函数\n\n- **参数**\n  - `cm` (AbstractContextManager[T])\n\n- **返回**\n  - AsyncGenerator[T, None]\n\n## _async def_ `run_coro_with_catch(coro, exc, return_on_err=None)` {#run-coro-with-catch}\n\n- **说明:** 运行协程并当遇到指定异常时返回指定值。\n\n- **重载**\n\n  **1.** `(coro, exc, return_on_err=None) -> T | None`\n  - **参数**\n    - `coro` (Coroutine[Any, Any, T])\n\n    - `exc` (tuple[type[Exception], ...])\n\n    - `return_on_err` (None)\n\n  - **返回**\n    - T | None\n\n  **2.** `(coro, exc, return_on_err) -> T | R`\n  - **参数**\n    - `coro` (Coroutine[Any, Any, T])\n\n    - `exc` (tuple[type[Exception], ...])\n\n    - `return_on_err` (R)\n\n  - **返回**\n    - T | R\n\n- **参数**\n  - `coro`: 要运行的协程\n\n  - `exc`: 要捕获的异常\n\n  - `return_on_err`: 当发生异常时返回的值\n\n- **返回**\n\n  协程的返回值或发生异常时的指定值\n\n## _async def_ `run_coro_with_shield(coro)` {#run-coro-with-shield}\n\n- **说明:** 运行协程并在取消时屏蔽取消异常。\n\n- **参数**\n  - `coro` (Coroutine[Any, Any, T]): 要运行的协程\n\n- **返回**\n  - T: 协程的返回值\n\n## _def_ `flatten_exception_group(exc_group)` {#flatten-exception-group}\n\n- **参数**\n  - `exc_group` (BaseExceptionGroup[E])\n\n- **返回**\n  - Generator[E, None, None]\n\n## _def_ `get_name(obj)` {#get-name}\n\n- **说明:** 获取对象的名称\n\n- **参数**\n  - `obj` (Any)\n\n- **返回**\n  - str\n\n## _def_ `path_to_module_name(path)` {#path-to-module-name}\n\n- **说明:** 转换路径为模块名\n\n- **参数**\n  - `path` (Path)\n\n- **返回**\n  - str\n\n## _def_ `resolve_dot_notation(obj_str, default_attr, default_prefix=None)` {#resolve-dot-notation}\n\n- **说明:** 解析并导入点分表示法的对象\n\n- **参数**\n  - `obj_str` (str)\n\n  - `default_attr` (str)\n\n  - `default_prefix` (str | None)\n\n- **返回**\n  - Any\n\n## _class_ `classproperty(func)` {#classproperty}\n\n- **说明:** 类属性装饰器\n\n- **参数**\n  - `func` ((Any) -> T)\n\n## _class_ `DataclassEncoder(<auto>)` {#DataclassEncoder}\n\n- **说明:** 可以序列化 [Message](adapters/index.md#Message)(List[Dataclass]) 的 `JSONEncoder`\n\n- **参数**\n\n  auto\n\n### _method_ `default(o)` {#DataclassEncoder-default}\n\n- **参数**\n  - `o`\n\n- **返回**\n  - untyped\n\n## _def_ `logger_wrapper(logger_name)` {#logger-wrapper}\n\n- **说明:** 用于打印 adapter 的日志。\n\n- **参数**\n  - `logger_name` (str): adapter 的名称\n\n- **返回**\n  - untyped: 日志记录函数\n\n    日志记录函数的参数:\n    - level: 日志等级\n    - message: 日志信息\n    - exception: 异常信息\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/appendices/api-calling.mdx",
    "content": "---\nsidebar_position: 4\ndescription: 使用平台接口，完成更多功能\n\noptions:\n  menu:\n    - category: appendices\n      weight: 50\n---\n\n# 使用平台接口\n\nimport Messenger from \"@/components/Messenger\";\n\n在 NoneBot 中，除了使用事件响应器操作发送文本消息外，我们还可以直接通过使用协议适配器提供的方法来使用平台特定的接口，完成发送特殊消息、获取信息等其他平台提供的功能。同时，在部分无法使用事件响应器的情况中，例如[定时任务](../best-practice/scheduler.md)，我们也可以使用平台接口来完成需要的功能。\n\n## 发送平台特殊消息\n\n在之前的章节中，我们介绍了如何向用户发送文本消息以及[如何处理平台消息](../tutorial/message.md)，现在我们来向用户发送平台特殊消息。\n\n:::caution 注意\n在以下的示例中，我们将使用 `Console` 协议适配器来演示如何发送平台消息。在实际使用中，你需要确保你使用的**消息序列类型**与你所要发送的**平台类型**一致。\n:::\n\n```python {4,7-17} title=weather/__init__.py\nimport inspect\nfrom nonebot.adapters.console import MessageSegment\n\n@weather.got(\"location\", prompt=MessageSegment.emoji(\"question\") + \"请输入地名\")\nasync def got_location(location: str = ArgPlainText()):\n    result = await weather.send(\n        MessageSegment.markdown(\n            inspect.cleandoc(\n                f\"\"\"\n                # {location}\n\n                - 今天\n\n                   ⛅ 多云 20℃~24℃\n                \"\"\"\n            )\n        )\n    )\n```\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/天气\" },\n    { position: \"left\", msg: \"❓请输入地名\" },\n    { position: \"right\", msg: \"北京\" },\n    {\n      position: \"left\",\n      monospace: true,\n      msg: \"┏━━━━━━━━━━━━━━━━┓\\n┃      北京       ┃\\n┗━━━━━━━━━━━━━━━━┛\\n• 今天\\n⛅ 多云 20℃~24℃\",\n    },\n  ]}\n/>\n\n在上面的示例中，我们使用了 `Console` 协议适配器提供的 `MessageSegment` 类来发送平台特定的消息 `emoji` 和 `markdown`。这两种消息可以显示在终端中，但是无法在其他平台上使用。在事件响应器操作中，我们可以使用 `str`、消息序列、消息段、消息模板四种类型来发送消息，但其中只有 `str` 和[纯文本形式的消息模板类型](../tutorial/message.md#使用消息模板)消息可以在所有平台上使用。\n\n`send` 事件响应器操作实际上是由协议适配器通过调用平台 API 来实现的，通常会将 API 调用的结果作为返回值返回。\n\n## 调用平台 API\n\n在 NoneBot 中，我们可以通过 `Bot` 对象来调用协议适配器支持的平台 API，来完成更多的功能。\n\n### 获取 Bot\n\n在调用平台 API 之前，我们首先要获得 Bot 对象。有两种方式可以获得 Bot 对象。\n\n在事件处理流程的上下文中，我们可以直接使用依赖注入 Bot 来获取：\n\n```python {1,4} title=weather/__init__.py\nfrom nonebot.adapters import Bot\n\n@weather.got(\"location\", prompt=\"请输入地名\")\nasync def got_location(bot: Bot, location: str = ArgPlainText()):\n    ...\n```\n\n依赖注入会确保你获得的 Bot 对象与类型注解的 Bot 类型一致。也就是说，如果你使用的是 Bot 基类，将会允许任何平台的 Bot 对象；如果你使用的是平台特定的 Bot 类型，将会只允许该平台的 Bot 对象，其他类型的 Bot 将会跳过这个事件处理函数。更多详情请参考[事件处理重载](./overload.md)。\n\n在其他情况下，我们可以通过 NoneBot 提供的方法来获取 Bot 对象，这些方法将会在[使用适配器](../advanced/adapter.md#获取-bot-对象)中详细介绍：\n\n```python {4,6}\nfrom nonebot import get_bot\n\n# 获取当前所有 Bot 中的第一个\nbot = get_bot()\n# 获取指定 ID 的 Bot\nbot = get_bot(\"bot_id\")\n```\n\n### 调用 API\n\n在获得 Bot 对象后，我们可以通过 Bot 的实例方法来调用平台 API：\n\n```python {2,5}\n# 通过 bot.api_name(**kwargs) 的方法调用 API\nresult = await bot.get_user_info(user_id=12345678)\n\n# 通过 bot.call_api(api_name, **kwargs) 的方法调用 API\nresult = await bot.call_api(\"get_user_info\", user_id=12345678)\n```\n\n:::caution 注意\n实际可以使用的 API 以及参数取决于平台提供的接口以及协议适配器的实现，请参考协议适配器以及平台文档。\n:::\n\n在了解了如何调用 API 后，我们可以来改进 `weather` 插件，使得消息发送后，调用 `Console` 接口响铃提醒机器人用户：\n\n```python {4,18} title=weather/__init__.py\nfrom nonebot.adapters.console import Bot, MessageSegment\n\n@weather.got(\"location\", prompt=MessageSegment.emoji(\"question\") + \"请输入地名\")\nasync def got_location(bot: Bot, location: str = ArgPlainText()):\n    await weather.send(\n        MessageSegment.markdown(\n            inspect.cleandoc(\n                f\"\"\"\n                # {location}\n\n                - 今天\n\n                   ⛅ 多云 20℃~24℃\n                \"\"\"\n            )\n        )\n    )\n    await bot.bell()\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/appendices/config.mdx",
    "content": "---\nsidebar_position: 0\ndescription: 读取用户配置来控制插件行为\n\noptions:\n  menu:\n    - category: appendices\n      weight: 10\n---\n\n# 配置\n\nimport Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\n\n配置是项目中非常重要的一部分，为了方便我们控制机器人的行为，NoneBot 提供了一套配置系统。下面我们将会补充[指南](../quick-start.mdx)中的天气插件，使其能够读取用户配置。在这之前，我们需要先了解一下配置系统，如果你已经了解了 NoneBot 中的配置方法，可以跳转到[编写插件配置](#插件配置)。\n\nNoneBot 使用 [`pydantic`](https://docs.pydantic.dev/) 以及 [`python-dotenv`](https://saurabh-kumar.com/python-dotenv/) 来读取 dotenv 配置文件以及环境变量，从而控制机器人行为。配置文件需要符合 dotenv 格式，复杂数据类型需使用 JSON 格式或 [pydantic 支持格式](https://docs.pydantic.dev/usage/types/)填写。\n\nNoneBot 内置的配置项列表及含义可以在[内置配置项](#内置配置项)中查看。\n\n:::caution 注意\n\nNoneBot 自 2.2.0 起兼容了 Pydantic v1 与 v2 版本，以下文档中 Pydantic 相关示例均采用 v2 版本用法。\n\n如果在使用商店或其他第三方插件的过程中遇到 Pydantic 相关警告或报错，例如：\n\n```python\npydantic_core._pydantic_core.ValidationError: 1 validation error for Config\n  Input should be a valid dictionary or instance of Config [type=model_type, input_value=Config(...), input_type=Config]\n```\n\n请考虑降级 Pydantic 至 v1 版本：\n\n```bash\npip install --force-reinstall 'pydantic~=1.10'\n```\n\n:::\n\n## 配置项的加载\n\n在 NoneBot 中，我们可以把配置途径分为 **直接传入**、**系统环境变量**、**dotenv 配置文件** 三种，其加载优先级依次由高到低。\n\n### 直接传入\n\n在 NoneBot 初始化的过程中，可以通过 `nonebot.init()` 传入任意合法的 Python 变量，也可以在初始化完成后直接赋值。\n\n通常，在初始化前的传参会在机器人的入口文件（如 `bot.py`）中进行，而初始化后的赋值可以在任何地方进行。\n\n```python {4,8,9} title=bot.py\nimport nonebot\n\n# 初始化时\nnonebot.init(custom_config1=\"config on init\")\n\n# 初始化后\nconfig = nonebot.get_driver().config\nconfig.custom_config1 = \"changed after init\"\nconfig.custom_config2 = \"new config after init\"\n```\n\n### 系统环境变量\n\n在 dotenv 配置文件中定义的配置项，也会在环境变量中进行寻找。如果在环境变量中发现同名配置项（大小写不敏感），将会覆盖 dotenv 中所填值。\n\n例如，在 dotenv 配置文件中存在配置项 `custom_config`：\n\n```dotenv\nCUSTOM_CONFIG=config in dotenv\n```\n\n同时，设置环境变量：\n\n```bash\n# windows cmd\nset CUSTOM_CONFIG 'config in environment variables'\n# windows powershell\n$Env:CUSTOM_CONFIG='config in environment variables'\n# linux/macOS\nexport CUSTOM_CONFIG='config in environment variables'\n```\n\n那最终 NoneBot 所读取的内容为环境变量中的内容，即 `config in environment variables`。\n\n:::caution 注意\nNoneBot 不会自发读取未被定义的配置项的环境变量，如果需要读取某一环境变量需要在 dotenv 配置文件中进行声明。\n:::\n\n### dotenv 配置文件\n\ndotenv 是一种便捷的跨平台配置通用模式，也是我们推荐的配置方式。\n\nNoneBot 在启动时将会从系统环境变量或者 `.env` 文件中寻找配置项 `ENVIRONMENT` （大小写不敏感），默认值为 `prod`。这将决定 NoneBot 后续进一步加载环境配置的文件路径 `.env.{ENVIRONMENT}`。\n\n#### 配置项解析\n\ndotenv 文件中的配置值使用 JSON 进行解析。如果配置项值无法被解析，将作为**字符串**处理。例如：\n\n```dotenv\nSTRING_CONFIG=some string\nLIST_CONFIG=[1, 2, 3]\nDICT_CONFIG={\"key\": \"value\"}\nMULTILINE_CONFIG='\n[\n  {\n    \"item_key\": \"item_value\"\n  }\n]\n'\nEMPTY_CONFIG=\nNULL_CONFIG\n```\n\n将被解析为：\n\n```python\ndotenv_config = {\n    \"string_config\": \"some string\",\n    \"list_config\": [1, 2, 3],\n    \"dict_config\": {\"key\": \"value\"},\n    \"multiline_config\": [{\"item_key\": \"item_value\"}],\n    \"empty_config\": \"\",\n    \"null_config\": None\n}\n```\n\n特别的，NoneBot 支持使用 `env_nested_delimiter` 配置嵌套字典，在层与层之间使用 `__` 分隔即可：\n\n```dotenv\nDICT={\"k1\": \"v1\", \"k2\": null}\nDICT__K2=v2\nDICT__K3=v3\nDICT__INNER__K4=v4\n```\n\n将被解析为：\n\n```python\ndotenv_config = {\n    \"dict\": {\n        \"k1\": \"v1\",\n        \"k2\": \"v2\",\n        \"k3\": \"v3\",\n        \"inner\": {\n            \"k4\": \"v4\"\n        }\n    }\n}\n```\n\n#### .env 文件\n\n`.env` 文件是基础配置文件，该文件中的配置项在不同环境下都会被加载，但会被 `.env.{ENVIRONMENT}` 文件中的配置所**覆盖**。\n\n我们可以在 `.env` 文件中写入当前的环境信息：\n\n```dotenv\nENVIRONMENT=dev\nCOMMON_CONFIG=common config  # 这个配置项在任何环境中都会被加载\n```\n\n这样，我们在启动 NoneBot 时就会从 `.env.dev` 文件中加载剩余配置项。\n\n:::tip 提示\n在生产环境中，可以通过设置环境变量 `ENVIRONMENT=prod` 来确保 NoneBot 读取正确的环境配置。\n:::\n\n#### .env.\\{ENVIRONMENT\\} 文件\n\n`.env.{ENVIRONMENT}` 文件类似于预设，可以让我们在多套不同的配置方案中灵活切换，默认 NoneBot 会读取 `.env.prod` 配置。如果你使用了 `nb-cli` 创建 `simple` 项目，那么将含有两套预设配置：`.env.dev` 和 `.env.prod`。\n\n在 NoneBot 初始化时，可以指定加载某个环境配置文件：\n\n```python\nnonebot.init(_env_file=\".env.dev\")\n```\n\n这将忽略在 `.env` 文件或环境变量中指定的 `ENVIRONMENT` 配置项。\n\n## 读取全局配置项\n\nNoneBot 的全局配置对象可以通过 `driver` 获取，如：\n\n```python\nimport nonebot\n\nconfig = nonebot.get_driver().config\n```\n\n如果我们需要获取某个配置项，可以直接通过 `config` 对象的属性访问：\n\n```python\nsuperusers = config.superusers\n```\n\n如果配置项不存在，将会抛出异常。\n\n## 插件配置\n\n在一个涉及大量配置项的项目中，通过直接读取全局配置项的方式显然并不高效。同时，由于额外的全局配置项没有预先定义，开发时编辑器将无法提示字段与类型，并且运行时没有对配置项直接进行合法性检查。那么就需要一种方式来规范定义插件配置项。\n\n在 NoneBot 中，我们使用强大高效的 `pydantic` 来定义配置模型，这个模型可以被用于配置的读取和类型检查等。例如在 `weather` 插件目录中新建 `config.py` 来定义一个模型：\n\n```python title=weather/config.py\nfrom pydantic import BaseModel, field_validator\n\nclass Config(BaseModel):\n    weather_api_key: str\n    weather_command_priority: int = 10\n    weather_plugin_enabled: bool = True\n\n    @field_validator(\"weather_command_priority\")\n    @classmethod\n    def check_priority(cls, v: int) -> int:\n        if v >= 1:\n            return v\n        raise ValueError(\"weather command priority must greater than 1\")\n```\n\n在 `config.py` 中，我们定义了一个 `Config` 类，它继承自 `pydantic.BaseModel`，并定义了一些配置项。在 `Config` 类中，我们还定义了一个 `check_priority` 方法，它用于检查 `weather_command_priority` 配置项的合法性。更多关于 `pydantic` 的编写方式，可以参考 [pydantic 官方文档](https://docs.pydantic.dev/)。\n\n在定义好配置模型后，我们可以在插件加载时通过配置模型获取插件配置：\n\n```python {5,11} title=weather/__init__.py\nfrom nonebot import get_plugin_config\n\nfrom .config import Config\n\nplugin_config = get_plugin_config(Config)\n\nweather = on_command(\n    \"天气\",\n    rule=to_me(),\n    aliases={\"weather\", \"查天气\"},\n    priority=plugin_config.weather_command_priority,\n    block=True,\n)\n```\n\n然后，我们便可以从 `plugin_config` 中读取配置了，例如 `plugin_config.weather_api_key`。\n\n这种方式可以简洁、高效地读取配置项，同时也可以设置默认值或者在运行时对配置项进行合法性检查，防止由于配置项导致的插件出错等情况出现。\n\n:::tip 提示\n发布插件应该为自身的事件响应器提供可配置的优先级，以便插件使用者可以自定义多个插件间的响应顺序。\n:::\n\n由于插件配置项是从全局配置中读取的，通常我们需要在配置项名称前面添加前缀名，以防止配置项冲突。例如在上方的示例中，我们就添加了配置项前缀 `weather_`。但是这样会导致在使用配置项时过长的变量名，因此我们可以使用 `pydantic` 的 `alias` 或者通过配置 scope 来简化配置项名称。这里我们以 scope 配置为例：\n\n```python title=weather/config.py\nfrom pydantic import BaseModel\n\nclass ScopedConfig(BaseModel):\n    api_key: str\n    command_priority: int = 10\n    plugin_enabled: bool = True\n\nclass Config(BaseModel):\n    weather: ScopedConfig\n```\n\n```python title=weather/__init__.py\nfrom nonebot import get_plugin_config\n\nfrom .config import Config\n\nplugin_config = get_plugin_config(Config).weather\n```\n\n这样我们就可以省略插件配置项名称中的前缀 `weather_` 了。但需要注意的是，如果我们使用了 scope 配置，那么在配置文件中也需要使用 [`env_nested_delimiter` 格式](#配置项解析)，例如：\n\n```dotenv\nWEATHER__API_KEY=123456\nWEATHER__COMMAND_PRIORITY=10\n```\n\n## 内置配置项\n\n配置项 API 文档可以前往 [Config 类](../api/config.md#Config)查看。\n\n### Driver\n\n- **类型**: `str`\n- **默认值**: `\"~fastapi\"`\n\nNoneBot 运行所使用的驱动器。具体配置方法可以参考[安装驱动器](../tutorial/store.mdx#安装驱动器)和[选择驱动器](../advanced/driver.md)。\n\n<Tabs groupId=\"configMethod\">\n  <TabItem value=\"dotenv\" label=\"dotenv\" default>\n\n```dotenv\nDRIVER=~fastapi+~httpx+~websockets\n```\n\n  </TabItem>\n  <TabItem value=\"env\" label=\"系统环境变量\">\n\n```bash\n# windows cmd\nset DRIVER '~fastapi+~httpx+~websockets'\n# windows powershell\n$Env:DRIVER='~fastapi+~httpx+~websockets'\n# linux/macOS\nexport DRIVER='~fastapi+~httpx+~websockets'\n```\n\n  </TabItem>\n  <TabItem value=\"init\" label=\"直接传入\">\n\n```python title=bot.py\nimport nonebot\n\nnonebot.init(driver=\"~fastapi+~httpx+~websockets\")\n```\n\n  </TabItem>\n</Tabs>\n\n### Host\n\n- **类型**: `IPvAnyAddress`\n- **默认值**: `127.0.0.1`\n\n当 NoneBot 作为服务端时，监听的 IP / 主机名。\n\n<Tabs groupId=\"configMethod\">\n  <TabItem value=\"dotenv\" label=\"dotenv\" default>\n\n```dotenv\nHOST=127.0.0.1\n```\n\n  </TabItem>\n  <TabItem value=\"env\" label=\"系统环境变量\">\n\n```bash\n# windows cmd\nset HOST '127.0.0.1'\n# windows powershell\n$Env:HOST='127.0.0.1'\n# linux/macOS\nexport HOST='127.0.0.1'\n```\n\n  </TabItem>\n  <TabItem value=\"init\" label=\"直接传入\">\n\n```python title=bot.py\nimport nonebot\n\nnonebot.init(host=\"127.0.0.1\")\n```\n\n  </TabItem>\n</Tabs>\n\n### Port\n\n- **类型**: `int` (1 ~ 65535)\n- **默认值**: `8080`\n\n当 NoneBot 作为服务端时，监听的端口。\n\n<Tabs groupId=\"configMethod\">\n  <TabItem value=\"dotenv\" label=\"dotenv\" default>\n\n```dotenv\nPORT=8080\n```\n\n  </TabItem>\n  <TabItem value=\"env\" label=\"系统环境变量\">\n\n```bash\n# windows cmd\nset PORT '8080'\n# windows powershell\n$Env:PORT='8080'\n# linux/macOS\nexport PORT='8080'\n```\n\n  </TabItem>\n  <TabItem value=\"init\" label=\"直接传入\">\n\n```python title=bot.py\nimport nonebot\n\nnonebot.init(port=8080)\n```\n\n  </TabItem>\n</Tabs>\n\n### Log Level\n\n- **类型**: `int | str`\n- **默认值**: `INFO`\n\nNoneBot 日志输出等级，可以为 `int` 类型等级或等级名称。具体等级对照表参考 [loguru 日志等级](https://loguru.readthedocs.io/en/stable/api/logger.html#levels)。\n\n:::tip 提示\n日志等级名称应为大写，如 `INFO`。\n:::\n\n<Tabs groupId=\"configMethod\">\n  <TabItem value=\"dotenv\" label=\"dotenv\" default>\n\n```dotenv\nLOG_LEVEL=DEBUG\n```\n\n  </TabItem>\n  <TabItem value=\"env\" label=\"系统环境变量\">\n\n```bash\n# windows cmd\nset LOG_LEVEL 'DEBUG'\n# windows powershell\n$Env:LOG_LEVEL='DEBUG'\n# linux/macOS\nexport LOG_LEVEL='DEBUG'\n```\n\n  </TabItem>\n  <TabItem value=\"init\" label=\"直接传入\">\n\n```python title=bot.py\nimport nonebot\n\nnonebot.init(log_level=\"DEBUG\")\n```\n\n  </TabItem>\n</Tabs>\n\n### API Timeout\n\n- **类型**: `float | None`\n- **默认值**: `30.0`\n\n调用平台接口的超时时间，单位为秒。`None` 表示不设置超时时间。\n\n<Tabs groupId=\"configMethod\">\n  <TabItem value=\"dotenv\" label=\"dotenv\" default>\n\n```dotenv\nAPI_TIMEOUT=10.0\n```\n\n  </TabItem>\n  <TabItem value=\"env\" label=\"系统环境变量\">\n\n```bash\n# windows cmd\nset API_TIMEOUT '10.0'\n# windows powershell\n$Env:API_TIMEOUT='10.0'\n# linux/macOS\nexport API_TIMEOUT='10.0'\n```\n\n  </TabItem>\n  <TabItem value=\"init\" label=\"直接传入\">\n\n```python title=bot.py\nimport nonebot\n\nnonebot.init(api_timeout=10.0)\n```\n\n  </TabItem>\n</Tabs>\n\n### SuperUsers\n\n- **类型**: `set[str]`\n- **默认值**: `set()`\n\n机器人超级用户，可以使用权限 [`SUPERUSER`](../api/permission.md#SUPERUSER)。\n\n<Tabs groupId=\"configMethod\">\n  <TabItem value=\"dotenv\" label=\"dotenv\" default>\n\n```dotenv\nSUPERUSERS=[\"123123123\"]\n```\n\n  </TabItem>\n  <TabItem value=\"env\" label=\"系统环境变量\">\n\n```bash\n# windows cmd\nset SUPERUSERS '[\"123123123\"]'\n# windows powershell\n$Env:SUPERUSERS='[\"123123123\"]'\n# linux/macOS\nexport SUPERUSERS='[\"123123123\"]'\n```\n\n  </TabItem>\n  <TabItem value=\"init\" label=\"直接传入\">\n\n```python title=bot.py\nimport nonebot\n\nnonebot.init(superusers={\"123123123\"})\n```\n\n  </TabItem>\n</Tabs>\n\n### Nickname\n\n- **类型**: `set[str]`\n- **默认值**: `set()`\n\n机器人昵称，通常协议适配器会根据用户是否 @bot 或者是否以机器人昵称开头来判断是否是向机器人发送的消息。\n\n<Tabs groupId=\"configMethod\">\n  <TabItem value=\"dotenv\" label=\"dotenv\" default>\n\n```dotenv\nNICKNAME=[\"bot\"]\n```\n\n  </TabItem>\n  <TabItem value=\"env\" label=\"系统环境变量\">\n\n```bash\n# windows cmd\nset NICKNAME '[\"bot\"]'\n# windows powershell\n$Env:NICKNAME='[\"bot\"]'\n# linux/macOS\nexport NICKNAME='[\"bot\"]'\n```\n\n  </TabItem>\n  <TabItem value=\"init\" label=\"直接传入\">\n\n```python title=bot.py\nimport nonebot\n\nnonebot.init(nickname={\"bot\"})\n```\n\n  </TabItem>\n</Tabs>\n\n### Command Start 和 Command Separator\n\n- **类型**: `set[str]`\n- **默认值**:\n  - Command Start: `{\"/\"}`\n  - Command Separator: `{\".\"}`\n\n命令消息的起始符和分隔符。用于 [`command`](../advanced/matcher.md#command) 规则。\n\n<Tabs groupId=\"configMethod\">\n  <TabItem value=\"dotenv\" label=\"dotenv\" default>\n\n```dotenv\nCOMMAND_START=[\"/\", \"\"]\nCOMMAND_SEP=[\".\", \" \"]\n```\n\n  </TabItem>\n  <TabItem value=\"env\" label=\"系统环境变量\">\n\n```bash\n# windows cmd\nset COMMAND_START '[\"/\", \"\"]'\nset COMMAND_SEP '[\".\", \" \"]'\n# windows powershell\n$Env:COMMAND_START='[\"/\", \"\"]'\n$Env:COMMAND_SEP='[\".\", \" \"]'\n# linux/macOS\nexport COMMAND_START='[\"/\", \"\"]'\nexport COMMAND_SEP='[\".\", \" \"]'\n```\n\n  </TabItem>\n  <TabItem value=\"init\" label=\"直接传入\">\n\n```python title=bot.py\nimport nonebot\n\nnonebot.init(command_start={\"/\", \"\"}, command_sep={\".\", \" \"})\n```\n\n  </TabItem>\n</Tabs>\n\n### Session Expire Timeout\n\n- **类型**: `timedelta`\n- **默认值**: `timedelta(minutes=2)`\n\n用户会话超时时间，配置格式参考 [Datetime Types](https://docs.pydantic.dev/latest/api/standard_library_types/#datetimetimedelta)。\n\n<Tabs groupId=\"configMethod\">\n  <TabItem value=\"dotenv\" label=\"dotenv\" default>\n\n```dotenv\nSESSION_EXPIRE_TIMEOUT=00:02:00\n```\n\n  </TabItem>\n  <TabItem value=\"env\" label=\"系统环境变量\">\n\n```bash\n# windows cmd\nset SESSION_EXPIRE_TIMEOUT '00:02:00'\n# windows powershell\n$Env:SESSION_EXPIRE_TIMEOUT='00:02:00'\n# linux/macOS\nexport SESSION_EXPIRE_TIMEOUT='00:02:00'\n```\n\n  </TabItem>\n  <TabItem value=\"init\" label=\"直接传入\">\n\n```python title=bot.py\nimport nonebot\n\nnonebot.init(session_expire_timeout=120)\n```\n\n  </TabItem>\n</Tabs>\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/appendices/log.md",
    "content": "---\nsidebar_position: 6\ndescription: 记录与控制日志\n\noptions:\n  menu:\n    - category: appendices\n      weight: 70\n---\n\n# 日志\n\n无论是在开发还是在生产环境中，日志都是一个重要的功能，可以帮助我们了解运行状况、排查问题等。虽然我们可以使用 `print` 来将需要的信息输出到控制台，但是这种方式难以控制，而且不利于日志的归档、分析等。NoneBot 使用优秀的 [Loguru](https://loguru.readthedocs.io/) 库来进行日志记录。\n\n## 记录日志\n\n我们可以从 NoneBot 中导入 `logger` 对象，然后使用 `logger` 对象的方法来记录日志。\n\n```python\nfrom nonebot import logger\n\nlogger.trace(\"This is a trace message\")\nlogger.debug(\"This is a debug message\")\nlogger.info(\"This is an info message\")\nlogger.success(\"This is a success message\")\nlogger.warning(\"This is a warning message\")\nlogger.error(\"This is an error message\")\nlogger.critical(\"This is a critical message\")\n```\n\n我们仅需一行代码即可记录对应级别的日志。日志可以通过配置 [`LOG_LEVEL` 配置项](./config.mdx#log-level)来过滤输出等级，控制台中仅会输出大于等于 `LOG_LEVEL` 的日志。默认的 `LOG_LEVEL` 为 `INFO`，即只会输出 `INFO`、`SUCCESS`、`WARNING`、`ERROR`、`CRITICAL` 级别的日志。\n\n如果需要记录 `Exception traceback` 日志，可以向 `logger` 添加 `exception` 选项：\n\n```python {4}\ntry:\n    1 / 0\nexcept ZeroDivisionError:\n    logger.opt(exception=True).error(\"ZeroDivisionError\")\n```\n\n如果需要输出彩色日志，可以向 `logger` 添加 `colors` 选项：\n\n```python\nlogger.opt(colors=True).warning(\"We got a <red>BIG</red> problem\")\n```\n\n更多日志记录方法请参考 [Loguru 文档](https://loguru.readthedocs.io/)。\n\n## 自定义日志输出\n\nNoneBot 在启动时会添加一个默认的日志处理器，该处理器会将日志输出到**stdout**，并且根据 `LOG_LEVEL` 配置项过滤日志等级。\n\n默认的日志格式为：\n\n```text\n<g>{time:MM-DD HH:mm:ss}</g> [<lvl>{level}</lvl>] <c><u>{name}</u></c> | {message}\n```\n\n我们可以从 `nonebot.log` 模块导入以使用 NoneBot 的默认格式和过滤器：\n\n```python\nfrom nonebot.log import default_format, default_filter\n```\n\n如果需要自定义日志格式，我们需要移除 NoneBot 默认的日志处理器并添加新的日志处理器。例如，在机器人入口文件中 `nonebot.init` 之前添加以下内容：\n\n```python title=bot.py\nfrom nonebot.log import logger_id\n\n# 移除 NoneBot 默认的日志处理器\nlogger.remove(logger_id)\n# 添加新的日志处理器\nlogger.add(\n    sys.stdout,\n    level=0,\n    diagnose=True,\n    format=\"<g>{time:MM-DD HH:mm:ss}</g> [<lvl>{level}</lvl>] <c><u>{name}</u></c> | {message}\",\n    filter=default_filter\n)\n```\n\n如果想要输出日志到文件，我们可以使用 `logger.add` 方法添加文件处理器：\n\n```python title=bot.py\nlogger.add(\"error.log\", level=\"ERROR\", format=default_format, rotation=\"1 week\")\n```\n\n更多日志处理器的使用方法请参考 [Loguru 文档](https://loguru.readthedocs.io/)。\n\n## 重定向 logging 日志\n\n`logging` 是 Python 标准库中的日志模块，NoneBot 提供了一个 logging handler 用于将 `logging` 日志重定向到 `loguru` 处理。\n\n```python\nfrom nonebot.log import LoguruHandler\n\n# root logger 添加 LoguruHandler\nlogging.basicConfig(handlers=[LoguruHandler()])\n# 或者为其他 logging.Logger 添加 LoguruHandler\nlogger.addHandler(LoguruHandler())\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/appendices/overload.md",
    "content": "---\nsidebar_position: 7\ndescription: 根据事件类型进行不同的处理\n\noptions:\n  menu:\n    - category: appendices\n      weight: 80\n---\n\n# 事件类型与重载\n\n在之前的示例中，我们已经了解了如何[获取事件信息](../tutorial/event-data.mdx)以及[使用平台接口](./api-calling.mdx)。但是，事件信息通常不仅仅包含消息这一个内容，还有其他平台提供的信息，例如消息发送时间、消息发送者等等。同时，在使用平台接口时，我们需要确保使用的**平台接口**与所要发送的**平台类型**一致，对不同类型的事件需要做出不同的处理。在本章节中，我们将介绍如何获取事件更多的信息以及根据事件类型进行不同的处理。\n\n## 事件类型\n\n在 NoneBot 中，事件均是 `nonebot.adapters.Event` 基类的子类型，基类对一些必要的属性进行了抽象，子类型则根据不同的平台进行了实现。在[自定义权限](./permission.mdx#自定义权限)一节中，我们就使用了 `Event` 的抽象方法 `get_user_id` 来获取事件发送者 ID，这个方法由协议适配器进行了实现，返回机器人用户对应的平台 ID。更多的基类抽象方法可以在[使用适配器](../advanced/adapter.md#获取事件通用信息)中查看。\n\n既然事件是基类的子类型，我们实际可以获得的信息通常多于基类抽象方法所提供的。如果我们不满足于基类能获得的信息，我们可以小小的修改一下事件处理函数的事件参数类型注解，使其变为子类型，这样我们就可以通过协议适配器定义的子类型来获取更多的信息。我们以 `Console` 协议适配器为例：\n\n```python {4} title=weather/__init__.py\nfrom nonebot.adapters.console import MessageEvent\n\n@weather.got(\"location\", prompt=\"请输入地名\")\nasync def got_location(event: MessageEvent, location: str = ArgPlainText()):\n    await weather.finish(f\"{event.time.strftime('%Y-%m-%d')} {location} 的天气是...\")\n```\n\n在上面的代码中，我们获取了 `Console` 协议适配器的消息事件提供的发送时间 `time` 属性。\n\n:::caution 注意\n如果**基类**就能满足你的需求，那么就**不要修改**事件参数类型注解，这样可以使你的代码更加**通用**，可以在更多平台上运行。如何根据不同平台事件类型进行不同的处理，我们将在[重载](#重载)一节中介绍。\n:::\n\n## 重载\n\n我们在编写机器人时，常常会遇到这样一个问题：如何对私聊和群聊消息进行不同的处理？如何对不同平台的事件进行不同的处理？针对这些问题，NoneBot 提供了一个便捷而高效的解决方案 ── 重载。简单来说，依赖函数会根据其参数的类型注解来决定是否执行，忽略不符合其参数类型注解的情况。这样，我们就可以通过修改事件参数类型注解来实现对不同事件的处理，或者修改 `Bot` 参数类型注解来实现使用不同平台的接口。我们以 `OneBot` 协议适配器为例：\n\n```python {4,8}\nfrom nonebot.adapters.onebot.v11 import PrivateMessageEvent, GroupMessageEvent\n\n@matcher.handle()\nasync def handle_private(event: PrivateMessageEvent):\n    await matcher.finish(\"私聊消息\")\n\n@matcher.handle()\nasync def handle_group(event: GroupMessageEvent):\n    await matcher.finish(\"群聊消息\")\n```\n\n这样，机器人用户就会在私聊和群聊中分别收到不同的回复。同样的，我们也可以通过修改 `Bot` 参数类型注解来实现使用不同平台的接口：\n\n```python\nfrom nonebot.adapters.console import Bot as ConsoleBot\nfrom nonebot.adapters.onebot.v11 import Bot as OneBot\n\n@matcher.handle()\nasync def handle_console(bot: ConsoleBot):\n    await bot.bell()\n\n@matcher.handle()\nasync def handle_onebot(bot: OneBot):\n    await bot.send_group_message(group_id=123123, message=\"OneBot\")\n```\n\n:::caution 注意\n重载机制对所有的参数类型注解都有效，因此，依赖注入也可以使用这个特性来对不同的返回值进行处理。\n\n但 Bot、Event 和 Matcher 三者的参数类型注解具有最高检查优先级，如果三者任一类型注解不匹配，那么其他依赖注入将不会执行（如：`Depends`）。\n:::\n\n:::tip 提示\n如何更好地编写一个跨平台的插件，我们将在[最佳实践](../best-practice/multi-adapter.mdx)中介绍。\n:::\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/appendices/permission.mdx",
    "content": "---\nsidebar_position: 5\ndescription: 控制事件响应器的权限\n\noptions:\n  menu:\n    - category: appendices\n      weight: 60\n---\n\n# 权限控制\n\nimport Messenger from \"@site/src/components/Messenger\";\n\n**权限控制**是机器人在实际应用中需要解决的重点问题之一，NoneBot 提供了灵活的权限控制机制 —— `Permission`。\n\n类似于响应规则 `Rule`，`Permission` 是由非负整数个 `PermissionChecker` 所共同组成的**用于筛选事件**的对象。但需要特别说明的是，权限和响应规则有如下区别：\n\n1. 权限检查**先于**响应规则检查\n2. `Permission` 只需**其中一个** `PermissionChecker` 返回 `True` 时就会检查通过\n3. 权限检查进行时，上下文中并不存在会话状态 `state`\n4. `Rule` 仅在**初次触发**事件响应器时进行检查，在余下的会话中并不会限制事件；而 `Permission` 会**持续生效**，在连续对话中一直对事件主体加以限制。\n\n## 基础使用\n\n通常情况下，`Permission` 更侧重于对于**触发事件的机器人用户**的筛选，例如由 NoneBot 自身提供的 `SUPERUSER` 权限，便是筛选出会话发起者是否为超级用户。它可以对输入的用户进行鉴别，如果符合要求则会被认为通过并返回 `True`，反之则返回 `False`。\n\n简单来说，`Permission` 是一个用于筛选出符合要求的用户的机制，可以通过 `Permission` 精确的控制响应对象的覆盖范围，从而拒绝掉我们所不希望的事件。\n\n例如，我们可以在 `weather` 插件中添加一个超级用户可用的指令：\n\n```python {3,9} title=weather/__init__.py\nfrom typing import Tuple\nfrom nonebot.params import Command\nfrom nonebot.permission import SUPERUSER\n\nmanage = on_command(\n    (\"天气\", \"启用\"),\n    rule=to_me(),\n    aliases={(\"天气\", \"禁用\")},\n    permission=SUPERUSER,\n)\n\n@manage.handle()\nasync def control(cmd: Tuple[str, str] = Command()):\n    _, action = cmd\n    if action == \"启用\":\n        plugin_config.weather_plugin_enabled = True\n    elif action == \"禁用\":\n        plugin_config.weather_plugin_enabled = False\n    await manage.finish(f\"天气插件已{action}\")\n```\n\n如上方示例所示，在注册事件响应器时，我们设置了 `permission` 参数，那么这个事件处理器在触发事件前的检查阶段会对用户身份进行验证，如果不符合我们设置的条件（此处即为**超级用户**）则不会响应。此时，我们向机器人发送 `/天气.禁用` 指令，机器人不会有任何响应，因为我们还不是机器人的超级管理员。我们在 dotenv 文件中设置了 `SUPERUSERS` 配置项之后，机器人就会响应我们的指令了。\n\n```dotenv title=.env\nSUPERUSERS=[\"console_user\"]\n```\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/天气.禁用\" },\n    { position: \"left\", msg: \"天气插件已禁用\" },\n    { position: \"right\", msg: \"/天气.启用\" },\n    { position: \"left\", msg: \"天气插件已启用\" },\n  ]}\n/>\n\n## 自定义权限\n\n与事件响应规则类似，`PermissionChecker` 也是一个返回值为 `bool` 类型的依赖函数，即 `PermissionChecker` 支持依赖注入。例如，我们可以限制用户的指令调用次数：\n\n```python title=weather/__init__.py\nfrom nonebot.adapters import Event\n\nfake_db: Dict[str, int] = {}\n\nasync def limit_permission(event: Event):\n    count = fake_db.setdefault(event.get_user_id(), 100)\n    if count > 0:\n        fake_db[event.get_user_id()] -= 1\n        return True\n    return False\n\nweather = on_command(\"天气\", permission=limit_permission)\n```\n\n## 权限组合\n\n权限之间可以通过 `|` 运算符进行组合，使得任意一个权限检查返回 `True` 时通过。例如：\n\n```python {4-6}\nperm1 = Permission(foo_checker)\nperm2 = Permission(bar_checker)\n\nperm = perm1 | perm2\nperm = perm1 | bar_checker\nperm = foo_checker | perm2\n```\n\n同样的，我们也无需担心组合了一个 `None` 值，`Permission` 会自动忽略 `None` 值。\n\n```python\nassert (perm | None) is perm\n```\n\n## 主动使用权限\n\n除了在事件响应器中使用权限外，我们也可以主动使用权限来判断事件是否符合条件。例如：\n\n```python {3}\nperm = Permission(some_checker)\n\nresult: bool = await perm(bot, event)\n```\n\n我们只需要传入 `Bot` 实例、事件，`Permission` 会并发调用所有 `PermissionChecker` 进行检查，并返回结果。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/appendices/rule.md",
    "content": "---\nsidebar_position: 1\ndescription: 自定义响应规则\n\noptions:\n  menu:\n    - category: appendices\n      weight: 20\n---\n\n# 响应规则\n\n机器人在实际应用中，往往会接收到多种多样的事件类型，NoneBot 通过响应规则来控制事件的处理。\n\n在[指南](../tutorial/matcher.md#为事件响应器添加参数)中，我们为 `weather` 命令添加了一个 `rule=to_me()` 参数，这个参数就是一个响应规则，确保只有在私聊或者 `@bot` 时才会响应。\n\n响应规则是一个 `Rule` 对象，它由一系列的 `RuleChecker` 函数组成，每个 `RuleChecker` 函数都会检查事件是否符合条件，如果所有的检查都通过，则事件会被处理。\n\n## RuleChecker\n\n`RuleChecker` 是一个返回值为 `bool` 类型的依赖函数，即 `RuleChecker` 支持依赖注入。我们可以根据上一节中添加的[配置项](./config.mdx#插件配置)，在 `weather` 插件目录中编写一个响应规则：\n\n```python {7,8} title=weather/__init__.py\nfrom nonebot import get_plugin_config\n\nfrom .config import Config\n\nplugin_config = get_plugin_config(Config)\n\nasync def is_enable() -> bool:\n    return plugin_config.weather_plugin_enabled\n\nweather = on_command(\"天气\", rule=is_enable)\n```\n\n在上面的代码中，我们定义了一个函数 `is_enable`，它会检查配置项 `weather_plugin_enabled` 是否为 `True`。这个函数 `is_enable` 即为一个 `RuleChecker`。\n\n## Rule\n\n`Rule` 是若干个 `RuleChecker` 的集合，它会并发调用每个 `RuleChecker`，只有当所有 `RuleChecker` 检查通过时匹配成功。例如：我们可以组合两个 `RuleChecker`，一个用于检查插件是否启用，一个用于检查用户是否在黑名单中：\n\n```python {10}\nfrom nonebot.rule import Rule\nfrom nonebot.adapters import Event\n\nasync def is_enable() -> bool:\n    return plugin_config.weather_plugin_enabled\n\nasync def is_blacklisted(event: Event) -> bool:\n    return event.get_user_id() not in BLACKLIST\n\nrule = Rule(is_enable, is_blacklisted)\n\nweather = on_command(\"天气\", rule=rule)\n```\n\n## 合并响应规则\n\n在定义响应规则时，我们可以将规则进行细分，来更好地复用规则。而在使用时，我们需要合并多个规则。除了使用 `Rule` 对象来组合多个 `RuleChecker` 外，我们还可以对 `Rule` 对象进行合并。在原 `weather` 插件中，我们可以将 `rule=to_me()` 与 `rule=is_enable` 使用 `&` 运算符合并：\n\n```python {13} title=weather/__init__.py\nfrom nonebot.rule import to_me\nfrom nonebot import get_plugin_config\n\nfrom .config import Config\n\nplugin_config = get_plugin_config(Config)\n\nasync def is_enable() -> bool:\n    return plugin_config.weather_plugin_enabled\n\nweather = on_command(\n    \"天气\",\n    rule=to_me() & is_enable,\n    aliases={\"weather\", \"查天气\"},\n    priority=plugin_config.weather_command_priority,\n    block=True,\n)\n```\n\n这样，`weather` 命令就只会在插件启用且在私聊或者 `@bot` 时才会响应。\n\n合并响应规则可以有多种形式，例如：\n\n```python {4-6}\nrule1 = Rule(foo_checker)\nrule2 = Rule(bar_checker)\n\nrule = rule1 & rule2\nrule = rule1 & bar_checker\nrule = foo_checker & rule2\n```\n\n同时，我们也无需担心合并了一个 `None` 值，`Rule` 会忽略 `None` 值。\n\n```python\nassert (rule & None) is rule\n```\n\n## 主动使用响应规则\n\n除了在事件响应器中使用响应规则外，我们也可以主动使用响应规则来判断事件是否符合条件。例如：\n\n```python {3}\nrule = Rule(some_checker)\n\nresult: bool = await rule(bot, event, state)\n```\n\n我们只需要传入 `Bot` 对象、事件和会话状态，`Rule` 会并发调用所有 `RuleChecker` 进行检查，并返回结果。\n\n## 内置响应规则\n\nNoneBot 内置了一些常用的响应规则，可以直接通过事件响应器辅助函数或者自行合并其他规则使用。内置响应规则列表可以参考[事件响应器进阶](../advanced/matcher.md)\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/appendices/session-control.mdx",
    "content": "---\nsidebar_position: 2\ndescription: 更灵活的会话控制\n\noptions:\n  menu:\n    - category: appendices\n      weight: 30\n---\n\n# 会话控制\n\nimport Messenger from \"@site/src/components/Messenger\";\n\n在[指南](../tutorial/event-data.mdx#使用依赖注入)的 `weather` 插件中，我们使用依赖注入获取了机器人用户发送的地名参数，并根据地名参数进行相应的回复。但是，一问一答的对话模式仅仅适用于简单的对话场景，如果我们想要实现更复杂的对话模式，就需要使用会话控制。\n\n## 询问并获取用户输入\n\n在 `weather` 插件中，我们对于用户未输入地名参数的情况直接回复了 `请输入地名` 并结束了事件流程。但是，这样用户体验并不好，需要重新输入指令和地名参数才能获取天气回复。我们现在来实现询问并获取用户地名参数的功能。\n\n### 询问用户\n\n我们可以使用事件响应器操作中的 `got` 装饰器来表示当前事件处理流程需要询问并获取用户输入的消息：\n\n```python {6} title=weather/__init__.py\n@weather.handle()\nasync def handle_function(args: Message = CommandArg()):\n    if location := args.extract_plain_text():\n        await weather.finish(f\"今天{location}的天气是...\")\n\n@weather.got(\"location\", prompt=\"请输入地名\")\nasync def got_location():\n    ...\n```\n\n在上面的代码中，我们使用 `got` 事件响应器操作来向用户发送 `prompt` 消息，并等待用户的回复。用户的回复消息将会被作为 `location` 参数存储于事件响应器状态中。\n\n:::tip 提示\n事件处理函数根据定义的顺序依次执行。\n:::\n\n### 获取用户输入\n\n在询问以及用户回复之后，我们就可以获取到我们需要的 `location` 参数了。我们使用 `ArgPlainText` 依赖注入来获取参数纯文本信息：\n\n```python {9} title=weather/__init__.py\nfrom nonebot.params import ArgPlainText\n\n@weather.handle()\nasync def handle_function(args: Message = CommandArg()):\n    if location := args.extract_plain_text():\n        await weather.finish(f\"今天{location}的天气是...\")\n\n@weather.got(\"location\", prompt=\"请输入地名\")\nasync def got_location(location: str = ArgPlainText()):\n    await weather.finish(f\"今天{location}的天气是...\")\n```\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/天气\" },\n    { position: \"left\", msg: \"请输入地名\" },\n    { position: \"right\", msg: \"北京\" },\n    { position: \"left\", msg: \"今天北京的天气是...\" },\n  ]}\n/>\n\n在上面的代码中，我们在 `got_location` 函数中定义了一个依赖注入参数 `location`，他的值将会是用户回复的消息纯文本信息。获取到用户输入的地名参数后，我们就可以进行天气查询并回复了。\n\n:::tip 提示\n如果想要获取用户回复的消息对象 `Message` ，可以使用 `Arg` 依赖注入。\n:::\n\n### 跳过询问\n\n在上面的代码中，如果用户在输入天气指令时，同时提供了地名参数，我们直接回复了天气信息，这部分的逻辑是和询问用户地名参数之后的逻辑一致的。如果在复杂的业务场景下，我们希望这部分代码应该复用以减少代码冗余。我们可以使用事件响应器操作中的 `set_arg` 来主动设置一个参数：\n\n```python {4,6} title=weather/__init__.py\nfrom nonebot.matcher import Matcher\n\n@weather.handle()\nasync def handle_function(matcher: Matcher, args: Message = CommandArg()):\n    if args.extract_plain_text():\n        matcher.set_arg(\"location\", args)\n\n@weather.got(\"location\", prompt=\"请输入地名\")\nasync def got_location(location: str = ArgPlainText()):\n    await weather.finish(f\"今天{location}的天气是...\")\n```\n\n请注意，设置参数需要使用依赖注入来获取 `Matcher` 实例以确保上下文正确，且参数值应为 `Message` 对象。\n\n在 `location` 参数被设置之后，`got` 事件响应器操作将不再会询问并等待用户的回复，而是直接进入 `got_location` 函数。\n\n## 请求重新输入\n\n在实际的业务场景中，用户的输入很有可能并非是我们所期望的，而结束事件处理流程让用户重新发送指令也不是一个好的体验。这时我们可以使用 `reject` 事件响应器操作来请求用户重新输入：\n\n```python {8,9} title=weather/__init__.py\n@weather.handle()\nasync def handle_function(matcher: Matcher, args: Message = CommandArg()):\n    if args.extract_plain_text():\n        matcher.set_arg(\"location\", args)\n\n@weather.got(\"location\", prompt=\"请输入地名\")\nasync def got_location(location: str = ArgPlainText()):\n    if location not in [\"北京\", \"上海\", \"广州\", \"深圳\"]:\n        await weather.reject(f\"你想查询的城市 {location} 暂不支持，请重新输入！\")\n    await weather.finish(f\"今天{location}的天气是...\")\n```\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/天气\" },\n    { position: \"left\", msg: \"请输入地名\" },\n    { position: \"right\", msg: \"南京\" },\n    { position: \"left\", msg: \"你想查询的城市 南京 暂不支持，请重新输入！\" },\n    { position: \"right\", msg: \"北京\" },\n    { position: \"left\", msg: \"今天北京的天气是...\" },\n  ]}\n/>\n\n在上面的代码中，我们在 `got_location` 函数中判断用户输入的地名是否在支持的城市列表中，如果不在，则使用 `reject` 事件响应器操作。操作将会向用户发送 `reject` 参数中的消息，并等待用户回复后，重新执行 `got_location` 函数。通过 `got` 和 `reject` 事件响应器操作，我们实现了类似于**循环**的执行方式。\n\n`reject` 事件响应器操作与 `finish` 类似，NoneBot 会在向机器人用户发送消息内容后抛出 `RejectedException` 异常来暂停事件响应流程以等待用户输入。也就是说，在 `reject` 被执行后，后续的程序同样是不会被执行的。\n\n## 更多事件响应器操作\n\n在之前的章节中，我们已经大致了解了五个事件响应器操作：`handle`、`got`、`finish`、`send` 和 `reject`。现在我们来完整地介绍一下这些操作。\n\n事件响应器操作可以分为两大类：**交互操作**和**流程控制操作**。我们可以通过交互操作来与用户进行交互，而流程控制操作则可以用来控制事件处理流程的执行。\n\n:::tip 提示\n事件处理流程按照事件处理函数添加顺序执行，已经结束的事件处理函数不可能被恢复执行。\n:::\n\n### handle\n\n`handle` 事件响应器操作是一个装饰器，用于向事件处理流程添加一个事件处理函数。\n\n```python\n@matcher.handle()\nasync def handle_func():\n    ...\n```\n\n`handle` 装饰器支持嵌套操作，即一个事件处理函数可以被添加多次：\n\n```python\n@matcher.handle()\n@matcher.handle()\nasync def handle_func():\n    # 这个函数会被执行两次\n    ...\n```\n\n### got\n\n`got` 事件响应器操作也是一个装饰器，它会在当前装饰的事件处理函数运行之前，中断当前事件处理流程，等待接收一个新的事件。它可以通过 `prompt` 参数来向用户发送询问消息，然后等待用户的回复消息，贴近对话形式会话。\n\n`got` 装饰器接受一个参数 `key` 和一个可选参数 `prompt`。当会话状态中不存在 `key` 对应的消息时，会向用户发送 `prompt` 参数的消息，并等待用户回复。`prompt` 参数的类型和 [`send`](#send) 事件响应器操作的参数类型一致。\n\n在事件处理函数中，可以通过依赖注入的方式来获取接收到的消息，参考：[`Arg`](../advanced/dependency.mdx#arg)、[`ArgStr`](../advanced/dependency.mdx#argstr)、[`ArgPlainText`](../advanced/dependency.mdx#argplaintext)。\n\n```python\n@matcher.got(\"key\", prompt=\"请输入...\")\nasync def got_func(key: Message = Arg()):\n    ...\n```\n\n`got` 装饰器支持与 `got` 和 `receive` 装饰器嵌套操作，即一个事件处理函数可以在接收多个事件或消息后执行：\n\n```python\n@matcher.got(\"key1\", prompt=\"请输入key1...\")\n@matcher.got(\"key2\", prompt=\"请输入key2...\")\n@matcher.receive(\"key3\")\nasync def got_func(key1: Message = Arg(), key2: Message = Arg(), key3: Event = Received(\"key3\")):\n    ...\n```\n\n### receive\n\n`receive` 事件响应器操作也是一个装饰器，它会在当前装饰的事件处理函数运行之前，中断当前事件处理流程，等待接收一个新的事件。与 `got` 不同的是，`receive` 不会向用户发送询问消息，并且等待一个用户事件。可以接收的事件类型取决于[会话更新](../advanced/session-updating.md)。\n\n`receive` 装饰器接受一个可选参数 id，用于标识当前需要接收的事件，如果不指定，则默认为空 `\"\"`。\n\n在事件处理函数中，可以通过依赖注入的方式来获取接收到的事件，参考：[`Received`](../advanced/dependency.mdx#received)、[`LastReceived`](../advanced/dependency.mdx#lastreceived)。\n\n```python\n@matcher.receive(\"id\")\nasync def receive_func(event: Event = Received(\"id\")):\n    ...\n```\n\n`receive` 装饰器支持与 `got` 和 `receive` 装饰器嵌套操作，即一个事件处理函数可以在接收多个事件或消息后执行：\n\n```python\n@matcher.receive(\"key1\")\n@matcher.got(\"key2\", prompt=\"请输入key2...\")\n@matcher.got(\"key3\", prompt=\"请输入key3...\")\nasync def receive_func(key1: Event = Received(\"key1\"), key2: Message = Arg(), key3: Message = Arg()):\n    ...\n```\n\n### send\n\n`send` 事件响应器操作用于向用户回复一条消息。协议适配器会根据当前 event 选择回复的途径。\n\n`send` 操作接受一个参数 message 和其他任何协议适配器接受的参数。message 参数类型可以是字符串、消息序列、消息段或者消息模板。消息模板将会使用会话状态字典进行渲染后发送。\n\n这个操作等同于使用 `bot.send(event, message, **kwargs)`，但不需要自行传入 `event`。\n\n```python\n@matcher.handle()\nasync def _():\n    await matcher.send(\"Hello world!\")\n```\n\n### finish\n\n向用户回复一条消息（可选），并立即结束**整个处理流程**。\n\n参数与 [`send`](#send) 相同。\n\n```python\n@matcher.handle()\nasync def _():\n    await matcher.finish(\"Hello world!\")\n    # 下面的代码不会被执行\n```\n\n### pause\n\n向用户回复一条消息（可选），立即结束**当前**事件处理函数，等待接收一个新的事件后进入**下一个**事件处理函数。\n\n参数与 [`send`](#send) 相同。\n\n```python\n@matcher.handle()\nasync def _():\n    if need_confirm:\n        await matcher.pause(\"请在两分钟内确认执行\")\n\n@matcher.handle()\nasync def _():\n    ...\n```\n\n### reject\n\n向用户回复一条消息（可选），立即结束**当前**事件处理函数，等待接收一个新的事件后再次执行**当前**事件处理函数。\n\n`reject` 可以用于拒绝当前 `receive` 接收的事件或 `got` 接收的参数。通常在用户回复不符合格式或标准需要重新输入，或者用于循环进行用户交互。\n\n参数与 [`send`](#send) 相同。\n\n```python\n@matcher.got(\"arg\")\nasync def _(arg: str = ArgPlainText()):\n    if not is_valid(arg):\n        await matcher.reject(\"Invalid arg!\")\n```\n\n### reject_arg\n\n向用户回复一条消息（可选），立即结束**当前**事件处理函数，等待接收一个新的消息后再次执行**当前**事件处理函数。\n\n`reject_arg` 用于拒绝指定 `got` 接收的参数，通常在嵌套装饰器时使用。\n\n`reject_arg` 操作接受一个 key 参数以及可选的 prompt 参数。prompt 参数与 [`send`](#send) 相同。\n\n```python\n@matcher.got(\"a\")\n@matcher.got(\"b\")\nasync def _(a: str = ArgPlainText(), b: str = ArgPlainText()):\n    if a not in b:\n        await matcher.reject_arg(\"a\", \"Invalid a!\")\n```\n\n### reject_receive\n\n向用户回复一条消息（可选），立即结束**当前**事件处理函数，等待接收一个新的事件后再次执行**当前**事件处理函数。\n\n`reject_receive` 用于拒绝指定 `receive` 接收的事件，通常在嵌套装饰器时使用。\n\n`reject_receive` 操作接受一个可选的 id 参数以及可选的 prompt 参数。id 参数默认为空 `\"\"`，prompt 参数与 [`send`](#send) 相同。\n\n```python\n@matcher.receive(\"a\")\n@matcher.receive(\"b\")\nasync def _(a: Event = Received(\"a\"), b: Event = Received(\"b\")):\n    if a.get_user_id() != b.get_user_id():\n        await matcher.reject_receive(\"a\")\n```\n\n### skip\n\n立即结束当前事件处理函数，进入下一个事件处理函数。\n\n通常在依赖注入中使用，用于跳过当前事件处理函数的执行。\n\n```python\nfrom nonebot.params import Depends\n\nasync def dependency():\n    matcher.skip()\n\n@matcher.handle()\nasync def _(check=Depends(dependency)):\n    # 这个函数不会被执行\n```\n\n### stop_propagation\n\n阻止事件向更低优先级的事件响应器传播。\n\n```python\nfrom nonebot.matcher import Matcher\n\n@foo.handle()\nasync def _(matcher: Matcher):\n    matcher.stop_propagation()\n```\n\n:::caution 注意\n`stop_propagation` 操作是实例方法，需要先通过依赖注入获取事件响应器实例再进行调用。\n:::\n\n### get_arg\n\n获取一个 `got` 接收的参数。\n\n`get_arg` 操作接受一个 key 参数和一个可选的 default 参数。当参数不存在时，将返回 default 或 `None`。\n\n```python\nfrom nonebot.matcher import Matcher\n\n@matcher.handle()\nasync def _(matcher: Matcher):\n    key = matcher.get_arg(\"key\", default=None)\n```\n\n### set_arg\n\n设置 / 覆盖一个 `got` 接收的参数。\n\n`set_arg` 操作接受一个 key 参数和一个 value 参数。请注意，value 参数必须是消息序列对象，如需存储其他数据请使用[会话状态](./session-state.md)。\n\n```python\nfrom nonebot.matcher import Matcher\n\n@matcher.handle()\nasync def _(matcher: Matcher):\n    matcher.set_arg(\"key\", Message(\"value\"))\n```\n\n### get_receive\n\n获取一个 `receive` 接收的事件。\n\n`get_receive` 操作接受一个 id 参数和一个可选的 default 参数。当事件不存在时，将返回 default 或 `None`。\n\n```python\nfrom nonebot.matcher import Matcher\n\n@matcher.handle()\nasync def _(matcher: Matcher):\n    event = matcher.get_receive(\"id\", default=None)\n```\n\n### get_last_receive\n\n获取最近的一个 `receive` 接收的事件。\n\n`get_last_receive` 操作接受一个可选的 default 参数。当事件不存在时，将返回 default 或 `None`。\n\n```python\nfrom nonebot.matcher import Matcher\n\n@matcher.handle()\nasync def _(matcher: Matcher):\n    event = matcher.get_last_receive(default=None)\n```\n\n### set_receive\n\n设置 / 覆盖一个 `receive` 接收的事件。\n\n`set_receive` 操作接受一个 id 参数和一个 event 参数。请注意，event 参数必须是事件对象，如需存储其他数据请使用[会话状态](./session-state.md)。\n\n```python\nfrom nonebot.matcher import Matcher\n\n@matcher.handle()\nasync def _(matcher: Matcher):\n    matcher.set_receive(\"key\", Event())\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/appendices/session-state.md",
    "content": "---\nsidebar_position: 3\ndescription: 会话状态信息\n\noptions:\n  menu:\n    - category: appendices\n      weight: 40\n---\n\n# 会话状态\n\n在事件处理流程中，和用户交互的过程即是会话。在会话中，我们可能需要记录一些信息，例如用户的重试次数等等，以便在会话中的不同阶段进行判断和处理。这些信息都可以存储于会话状态中。\n\nNoneBot 中的会话状态是一个字典，可以通过类型 `T_State` 来获取。字典内可以存储任意类型的数据，但是要注意的是，NoneBot 本身会在会话状态中存储一些信息，因此不要使用 [NoneBot 使用的键名](../api/consts.md)。\n\n```python\nfrom nonebot.typing import T_State\n\n@matcher.got(\"key\", prompt=\"请输入密码\")\nasync def _(state: T_State, key: str = ArgPlainText()):\n    if key != \"some password\":\n        try_count = state.get(\"try_count\", 1)\n        if try_count >= 3:\n            await matcher.finish(\"密码错误次数过多\")\n        else:\n            state[\"try_count\"] = try_count + 1\n            await matcher.reject(\"密码错误，请重新输入\")\n    await matcher.finish(\"密码正确\")\n```\n\n会话状态的生命周期与事件处理流程相同，在期间的任何一个事件处理函数都可以进行读写。\n\n```python\nfrom nonebot.typing import T_State\n\n@matcher.handle()\nasync def _(state: T_State):\n    state[\"key\"] = \"value\"\n\n@matcher.handle()\nasync def _(state: T_State):\n    await matcher.finish(state[\"key\"])\n```\n\n会话状态还可以用于发送动态消息，消息模板在发送时会使用会话状态字典进行渲染。消息模板的使用方法已经在[消息处理](../tutorial/message.md#使用消息模板)中介绍过，这里不再赘述。\n\n```python\nfrom nonebot.typing import T_State\nfrom nonebot.adapters import MessageTemplate\n\n@matcher.handle()\nasync def _(state: T_State):\n    state[\"username\"] = \"user\"\n\n@matcher.got(\"password\", prompt=MessageTemplate(\"请输入 {username} 的密码\"))\nasync def _():\n    await matcher.finish(MessageTemplate(\"密码为 {password}\"))\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/appendices/whats-next.md",
    "content": "---\nsidebar_position: 99\ndescription: 下一步──进阶！\n---\n\n# 下一步\n\n至此，我们已经了解了 NoneBot 的大多数功能用法，相信你已经可以独自写出一个插件了。现在你可以选择：\n\n- 即刻开始插件编写！\n- 更深入地了解 NoneBot 的[更多功能和原理](../advanced/plugin-info.md)！\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/best-practice/alconna/README.mdx",
    "content": "import Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\n\n# Alconna 插件\n\n[`nonebot-plugin-alconna`](https://github.com/nonebot/plugin-alconna) 是一类极大地提升了 NoneBot 开发体验的插件。\n\n该插件可分为三个部分：\n\n- 增强的命令解析: 基于 [Alconna](https://github.com/ArcletProject/Alconna), 提供一类新的事件响应器辅助函数 `on_alconna`. 相比 `on_command`, `on_shell`, `on_regex` 等函数，`on_alconna` 提供了更强大的命令解析能力与诸多特性。\n- 通用消息组件: 实现了跨平台接收、发送、撤回、编辑、表态消息的功能。\n  - `UniMessage` 通用消息模型，支持各适配器下的消息转换和导出，发送。\n  - `Text`, `Image`, `At` 等通用消息段模型，既与 `UniMessage` 配合使用，又能用于 `Alconna` 的命令解析。\n  - `message_recall`, `message_edit`, `message_reaction` 等功能函数。\n  - `Target` 通用消息目标模型，并通过该模型进行主动消息发送。\n  - `UniMsg`, `MsgId`, `MsgTarget`, `at_in`, `at_me` 等提供给 nonebot 使用的依赖注入和 `Rule`。\n- 内置功能插件：基于上述部分实现的内置功能插件。\n  - `echo`: 通过 `on_alconna` 实现的 echo 插件，支持回显回复消息。\n  - `help`: 列出所有 `on_alconna` 事件响应器的帮助信息或其对应的插件信息。\n  - `lang`: 切换 `Alconna` 使用的语言\n  - `switch`: 禁用/启用某个指令\n  - `with`: 针对具有多个子命令的指令，通过 `with` 在当前会话中载入命令头以节省输入。\n\n以最新版本为例 (v0.59), 本插件已支持 NoneBot 生态中几乎所有的适配器, 包括:\n\n| 协议名称                                                            | 路径                                 |\n| ------------------------------------------------------------------- | ------------------------------------ |\n| [OneBot 协议](https://onebot.dev/)                                  | adapters.onebot11, adapters.onebot12 |\n| [Telegram](https://core.telegram.org/bots/api)                      | adapters.telegram                    |\n| [飞书](https://open.feishu.cn/document/home/index)                  | adapters.feishu                      |\n| [GitHub](https://docs.github.com/en/developers/apps)                | adapters.github                      |\n| [QQ bot](https://github.com/nonebot/adapter-qq)                     | adapters.qq                          |\n| [钉钉](https://open.dingtalk.com/document/)                         | adapters.ding                        |\n| [Console](https://github.com/nonebot/adapter-console)               | adapters.console                     |\n| [开黑啦](https://developer.kookapp.cn/)                             | adapters.kook                        |\n| [Mirai](https://docs.mirai.mamoe.net/mirai-api-http/)               | adapters.mirai                       |\n| [Ntchat](https://github.com/JustUndertaker/adapter-ntchat)          | adapters.ntchat                      |\n| [MineCraft](https://github.com/17TheWord/nonebot-adapter-minecraft) | adapters.minecraft                   |\n| [Walle-Q](https://github.com/onebot-walle/nonebot_adapter_walleq)   | adapters.onebot12                    |\n| [Discord](https://github.com/nonebot/adapter-discord)               | adapters.discord                     |\n| [Red 协议](https://github.com/nonebot/adapter-red)                  | adapters.red                         |\n| [Satori](https://github.com/nonebot/adapter-satori)                 | adapters.satori                      |\n| [Dodo IM](https://github.com/nonebot/adapter-dodo)                  | adapters.dodo                        |\n| [Kritor](https://github.com/nonebot/adapter-kritor)                 | adapters.kritor                      |\n| [Tailchat](https://github.com/eya46/nonebot-adapter-tailchat)       | adapters.tailchat                    |\n| [Mail](https://github.com/mobyw/nonebot-adapter-mail)               | adapters.mail                        |\n| [微信公众号](https://github.com/YangRucheng/nonebot-adapter-wxmp)   | adapters.wxmp                        |\n| [黑盒语音](https://github.com/lclbm/adapter-heybox)                 | adapters.heybox                      |\n| [Milky](https://github.com/nonebot/adapter-milky)                   | adapters.milky                       |\n| [EFChat](https://github.com/molanp/nonebot_adapter_efchat)          | adapters.efchat                      |\n\n## 安装插件\n\n在使用前请先安装 `nonebot-plugin-alconna` 插件至项目环境中，可参考[获取商店插件](../../tutorial/store.mdx#安装插件)来了解并选择安装插件的方式。如：\n\n在**项目目录**下执行以下命令：\n\n<Tabs groupId=\"install\">\n<TabItem value=\"cli\" label=\"使用 nb-cli\">\n\n```shell\nnb plugin install nonebot-plugin-alconna\n```\n\n</TabItem>\n<TabItem value=\"pip\" label=\"使用 pip\">\n\n```shell\npip install nonebot-plugin-alconna\n```\n\n</TabItem>\n\n<TabItem value=\"pdm\" label=\"使用 pdm\">\n\n```shell\npdm add nonebot-plugin-alconna\n```\n\n</TabItem>\n</Tabs>\n\n## 导入插件\n\n由于 `nonebot-plugin-alconna` 作为插件，因此需要在使用前对其进行**加载**。使用 `require` 方法可轻松完成这一过程，可参考 [跨插件访问](../../advanced/requiring.md) 一节进行了解。\n\n```python\nfrom nonebot import require\n\nrequire(\"nonebot_plugin_alconna\")\n\nfrom nonebot_plugin_alconna import ...\n```\n\n## 使用插件\n\n在前面的[深入指南](../../appendices/session-control.mdx)中，我们已经得到了一个天气插件。\n现在我们将使用 `Alconna` 来改写这个插件。\n\n<details>\n  <summary>插件示例</summary>\n\n```python title=weather/__init__.py\nfrom nonebot import on_command\nfrom nonebot.rule import to_me\nfrom nonebot.matcher import Matcher\nfrom nonebot.adapters import Message\nfrom nonebot.params import CommandArg, ArgPlainText\n\nweather = on_command(\"天气\", rule=to_me(), aliases={\"weather\", \"天气预报\"})\n\n@weather.handle()\nasync def handle_function(matcher: Matcher, args: Message = CommandArg()):\n    if args.extract_plain_text():\n        matcher.set_arg(\"location\", args)\n\n@weather.got(\"location\", prompt=\"请输入地名\")\nasync def got_location(location: str = ArgPlainText()):\n    if location not in [\"北京\", \"上海\", \"广州\", \"深圳\"]:\n        await weather.reject(f\"你想查询的城市 {location} 暂不支持，请重新输入！\")\n    await weather.finish(f\"今天{location}的天气是...\")\n```\n\n</details>\n\n```python {5-9,13-15,17-18}\nfrom nonebot.rule import to_me\nfrom arclet.alconna import Alconna, Args\nfrom nonebot_plugin_alconna import Match, on_alconna\n\nweather = on_alconna(\n    Alconna(\"天气\", Args[\"location?\", str]),\n    aliases={\"weather\", \"天气预报\"},\n    rule=to_me(),\n)\n\n\n@weather.handle()\nasync def handle_function(location: Match[str]):\n    if location.available:\n        weather.set_path_arg(\"location\", location.result)\n\n@weather.got_path(\"location\", prompt=\"请输入地名\")\nasync def got_location(location: str):\n    if location not in [\"北京\", \"上海\", \"广州\", \"深圳\"]:\n        await weather.reject(f\"你想查询的城市 {location} 暂不支持，请重新输入！\")\n    await weather.finish(f\"今天{location}的天气是...\")\n```\n\n在上面的代码中，我们使用 `Alconna` 来解析命令，`on_alconna` 用来创建响应器，使用 `Match` 来获取解析结果。\n\n关于更多 `Alconna` 的使用方法，可参考 [Alconna 文档](https://arclet.top/tutorial/alconna)，\n或阅读 [Alconna 基本介绍](./command.md) 一节。\n\n关于更多 `on_alconna` 的使用方法，可参考 [插件文档](https://github.com/nonebot/plugin-alconna/blob/master/docs.md)，\n或阅读 [响应规则的使用](./matcher.mdx) 一节。\n\n## 交流与反馈\n\nQQ 交流群: [🔗 链接](https://jq.qq.com/?_wv=1027&k=PUPOnCSH)\n\n友链: [📚 文档](https://graiax.cn/guide/message_parser/alconna.html)\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/best-practice/alconna/_category_.json",
    "content": "{\n  \"label\": \"命令解析拓展\",\n  \"position\": 6\n}\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/best-practice/alconna/builtins.mdx",
    "content": "---\nsidebar_position: 7\ndescription: 内置组件\n---\n\nimport Messenger from \"@site/src/components/Messenger\";\n\n# 内置组件\n\n`nonebot_plugin_alconna` 插件提供了一系列内置组件以提升开发者和用户体验。\n\n## 内置插件\n\n类似于 Nonebot 本身提供的内置插件，`nonebot_plugin_alconna` 提供了多个内置插件。\n\n### 加载\n\n你可以用本插件的 `load_builtin_plugin(s)` 来加载它们：\n\n```python\nfrom nonebot_plugin_alconna import load_builtin_plugin, load_builtin_plugins\n\nload_builtin_plugins(\"echo\")\nload_builtin_plugins(\"help\", \"with\")\n```\n\n### 使用\n\n#### echo\n\n`echo` 插件能将用户发送的消息原样返回。\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/echo hello world!\" },\n    { position: \"left\", msg: \"hello world!\" },\n    { position: \"right\", msg: \"/echo [图片]\" },\n    { position: \"left\", msg: \"[图片]\" },\n  ]}\n/>\n\n#### help\n\n`help` 插件能列出所有 Alconna 指令。同时还能查询某个指令对应的插件信息。\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/帮助\" },\n    {\n      position: \"left\",\n      msg: \"# 当前可用的命令有:\\n 【0】/echo : echo 指令\\n 【1】/help : 显示所有命令帮助\\n# 输入'命令名 -h|--help' 查看特定命令的语法\",\n    },\n    { position: \"right\", msg: \"/help --plugin-info echo\" },\n    {\n      position: \"left\",\n      msg: \"插件名称: echo\\n插件标识: nonebot_plugin_alconna:echo\\n插件模块: nonebot-plugin-alconna\\n插件版本: 0.57.2\\n插件路径: nonebot_plugin_alconna.builtins.plugins.echo\",\n    },\n  ]}\n/>\n\nhelp 插件的帮助信息如下：\n\n```\n/help <query: str = -1>\n## 注释\n  query: 选择某条命令的id或者名称查看具体帮助\n显示所有命令帮助\n用法:\n可以使用 --hide 参数来显示隐藏命令，使用 -P 参数来显示命令所属插件名称\n\n可用的子命令有:\n* 是否列出命令所属命名空间\n  -N│--namespace│命名空间 [target: str]\n## 注释\n  target: 指定的命名空间\n  该子命令内可用的选项有:\n  * 列出所有命名空间\n    --list\n可用的选项有:\n* 查看指定页数的命令帮助\n  --page <index: int>\n* 查看命令所属插件的信息\n  -P│插件信息│--plugin-info\n* 是否列出隐藏命令\n  隐藏│-H│--hide\n```\n\n#### lang\n\n`lang` 插件能切换 i18n 的语言设置。\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/lang list\" },\n    {\n      position: \"left\",\n      msg: \"支持的语言列表:\\n * en-US\\n * zh-CN\",\n    },\n    { position: \"right\", msg: \"/lang switch en-US\" },\n    { position: \"left\", msg: \"Switch to 'en-US' successfully.\" },\n  ]}\n/>\n\nlang 插件的帮助信息如下：\n\n```\n/lang\ni18n配置相关功能\n\n可用的选项有:\n* 查看支持的语言列表\n  list [name: str]\n* 切换语言\n  switch [locale: str]\n```\n\n其中 `list` 选项可以查找某一插件下的语言支持情况 (例如 `/lang list nonebot_plugin_alconna`)。\n\n#### switch\n\n`switch` 插件能用来启用/禁用某个命令，其使用方法与 `help` 类似。\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/disable\" },\n    {\n      position: \"left\",\n      msg: \"【0】/echo : echo 指令\\n【1】/help : 显示所有命令帮助\\n【2】/lang : i18n配置相关功能\",\n    },\n    { position: \"right\", msg: \"/disable 0\" },\n    { position: \"left\", msg: \"已禁用 /echo\" },\n    { position: \"right\", msg: \"/echo 1234\" },\n    { position: \"right\", msg: \"/enable echo\" },\n    { position: \"left\", msg: \"已启用 /echo\" },\n    { position: \"right\", msg: \"/echo 1234\" },\n    { position: \"left\", msg: \"1234\" },\n  ]}\n/>\n\n#### with\n\n`with` 插件能在当前会话中设置一个局部命令前缀，以便于有多个子命令的指令使用。\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/with\" },\n    {\n      position: \"left\",\n      msg: \"当前群组未设置前缀\",\n    },\n    { position: \"right\", msg: \"/with lang\" },\n    { position: \"left\", msg: \"设置前缀成功\" },\n    { position: \"right\", msg: \"list\" },\n    {\n      position: \"left\",\n      msg: \"支持的语言列表:\\n * en-US\\n * zh-CN\",\n    },\n  ]}\n/>\n\nwith 插件的帮助信息如下：\n\n```\n.with [name: str]\nwith 指令\n用法:\n设置局部命令前缀\n\n可用的选项有:\n* 设置可能的生效时间\n  --expire│expire <time: datetime>\n* 取消当前前缀\n  unset│--unset\n\n快捷命令:\n'[.]局部前缀' => [.]with\n```\n\n### 配置\n\n内置插件也有其配置项，并且均以 `NBP_ALC` 开头。\n\n- `nbp_alc_echo_tome`: 是否让 `echo` 插件的消息经过 `to_me` 处理\n- `nbp_alc_page_size`: `help` 与 `switch` 插件的共同配置项，表示每页显示的命令数量\n- `nbp_alc_help_text`: `help` 指令的指令名，默认为 \"help\"\n- `nbp_alc_help_alias`: `help` 指令的别名，默认为 \"帮助\", \"命令帮助\"\n- `nbp_alc_help_all_alias`: `help` 指令显示隐藏指令时的别名，默认为 \"所有帮助\", \"所有命令帮助\"\n- `nbp_alc_switch_enable`: `switch` 插件的 `enable` 指令的指令名，默认为 \"enable\"\n- `nbp_alc_switch_enable_alias`: `switch` 插件的 `enable` 指令的别名，默认为 \"启用\", \"启用指令\"\n- `nbp_alc_switch_disable`: `switch` 插件的 `disable` 指令的指令名，默认为 \"disable\"\n- `nbp_alc_switch_disable_alias`: `switch` 插件的 `disable` 指令的别名，默认为 \"disable\", \"禁用\", \"禁用指令\"\n- `nbp_alc_with_text`: `with` 插件的指令名，默认为 \"with\"\n- `nbp_alc_with_alias`: `with` 插件的别名，默认为 \"局部前缀\"\n\n## 内置匹配拓展\n\n目前插件提供了 5 个内置的 `Extension`，它们在 `nonebot_plugin_alconna.builtins.extensions` 下：\n\n### ReplyRecordExtension\n\n`ReplyRecordExtension` 可将消息事件中的回复暂存在 extension 中，使得解析用的消息不带回复信息，同时可以在后续的处理中获取回复信息：\n\n```python\nfrom nonebot_plugin_alconna import MsgId, on_alconna\nfrom nonebot_plugin_alconna.builtins.extensions import ReplyRecordExtension\n\nmatcher = on_alconna(\"...\", extensions=[ReplyRecordExtension()])\n\n@matcher.handle()\nasync def handle(msg_id: MsgId, ext: ReplyRecordExtension):\n    if reply := ext.get_reply(msg_id):\n        ...\n    else:\n        ...\n```\n\n### ReplyMergeExtension\n\n`ReplyMergeExtension` 可将消息事件中的回复指向的原消息合并到当前消息中作为一部分参数：\n\n```python\nfrom nonebot_plugin_alconna import Match, on_alconna\nfrom nonebot_plugin_alconna.builtins.extensions.reply import ReplyMergeExtension\n\nmatcher = on_alconna(\"...\", extensions=[ReplyMergeExtension()])\n\n@matcher.handle()\nasync def handle(content: Match[str]):\n    ...\n```\n\n其构造时可传入两个参数:\n\n- `add_left`: 否在当前消息的左侧合并回复消息，默认为 False\n- `sep`: 合并时的分隔符，默认为空格\n\n### DiscordSlashExtension\n\n`DiscordSlashExtension` 可自动将 Alconna 对象翻译成 Discord 的 slash 指令并注册，且将收到的指令交互事件转为指令供命令解析：\n\n```python\nfrom nonebot_plugin_alconna import Match, on_alconna\nfrom nonebot_plugin_alconna.builtins.extensions.discord import DiscordSlashExtension\n\n\nalc = Alconna(\n    [\"/\"],\n    \"permission\",\n    Subcommand(\"add\", Args[\"plugin\", str][\"priority?\", int]),\n    Option(\"remove\", Args[\"plugin\", str][\"time?\", int]),\n    meta=CommandMeta(description=\"权限管理\"),\n)\n\nmatcher = on_alconna(alc, extensions=[DiscordSlashExtension()])\n\n@matcher.assign(\"add\")\nasync def add(plugin: Match[str], priority: Match[int], ext: DiscordSlashExtension):\n    await ext.send_followup_msg(f\"added {plugin.result} with {priority.result if priority.available else 0}\")\n\n@matcher.assign(\"remove\")\nasync def remove(plugin: Match[str], time: Match[int]):\n    await matcher.finish(f\"removed {plugin.result} with {time.result if time.available else -1}\")\n```\n\n### MarkdownOutputExtension\n\n`MarkdownOutputExtension` 可将 Alconna 的自动输出转换为 Markdown 格式\n\n其构造时可传入两个参数:\n\n- `escape_dot`: 是否转义句中的点号（用来避免被识别为 url）\n- `text_to_image` 将文本转换为图片的函数，可不传入。一般用来设置渲染 markdown 为图片的函数\n\n### TelegramSlashExtension\n\n`TelegramSlashExtension` 可将 Alconna 的命令注册在 Telegram 上以获得提示，类似于 `DiscordSlashExtension`。\n\n```python\nfrom nonebot_plugin_alconna import on_alconna\nfrom nonebot.adapters.telegram.model import BotCommandScopeChat\nfrom nonebot_plugin_alconna.builtins.extensions.telegram import TelegramSlashExtension\n\nTelegramSlashExtension.set_scope(BotCommandScopeChat())\n\nmatcher = on_alconna(\"...\", extensions=[TelegramSlashExtension()])\n```\n\n## 内置自定义消息段\n\n目前插件提供了 3 个内置的 `Segment`，它们在 `nonebot_plugin_alconna.builtins.segments` 下：\n\n- `Markdown`: 可以传入 **markdown模板** 的元素\n- `MarketFace`: 特指 QQ 的商城表情\n- `MusicShare`: 特指 QQ 的音乐分享卡片\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/best-practice/alconna/command.md",
    "content": "---\nsidebar_position: 2\ndescription: Alconna 基本介绍\n---\n\n# Alconna 本体\n\n[`Alconna`](https://github.com/ArcletProject/Alconna) 隶属于 `ArcletProject`，是一个简单、灵活、高效的命令参数解析器, 并且不局限于解析命令式字符串。\n\n我们先通过一个例子来讲解 **Alconna** 的核心 —— `Args`, `Subcommand`, `Option`：\n\n```python\nfrom arclet.alconna import Alconna, Args, Subcommand, Option\n\n\nalc = Alconna(\n    \"pip\",\n    Subcommand(\n        \"install\",\n        Args[\"package\", str],\n        Option(\"-r|--requirement\", Args[\"file\", str]),\n        Option(\"-i|--index-url\", Args[\"url\", str]),\n    )\n)\n\nres = alc.parse(\"pip install nonebot2 -i URL\")\n\nprint(res)\n# 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'}\n\nprint(res.all_matched_args)\n# {'package': 'nonebot2', 'url': 'URL'}\n```\n\n这段代码通过`Alconna`创捷了一个接受主命令名为`pip`, 子命令为`install`且子命令接受一个 **Args** 参数`package`和二个 **Option** 参数`-r`和`-i`的命令参数解析器, 通过`parse`方法返回解析结果 **Arparma** 的实例。\n\n## 命令头\n\n命令头是指命令的前缀 (Prefix) 与命令名 (Command) 的组合，例如 !help 中的 ! 与 help。\n\n命令构造时, `Alconna([prefix], command)` 与 `Alconna(command, [prefix])` 是等价的。\n\n|             前缀             |   命令名   |                          匹配内容                           |       说明       |\n| :--------------------------: | :--------: | :---------------------------------------------------------: | :--------------: |\n|            不传入            |   \"foo\"    |                           `\"foo\"`                           | 无前缀的纯文字头 |\n|            不传入            |    123     |                            `123`                            |  无前缀的元素头  |\n|            不传入            | \"re:\\d{2}\" |                           `\"32\"`                            |  无前缀的正则头  |\n|            不传入            |    int     |                      `123` 或 `\"456\"`                       |  无前缀的类型头  |\n|         [int, bool]          |   不传入   |                       `True` 或 `123`                       |  无名的元素类头  |\n|        [\"foo\", \"bar\"]        |   不传入   |                     `\"foo\"` 或 `\"bar\"`                      |  无名的纯文字头  |\n|        [\"foo\", \"bar\"]        |   \"baz\"    |                  `\"foobaz\"` 或 `\"barbaz\"`                   |     纯文字头     |\n|         [int, bool]          |   \"foo\"    |             `[123, \"foo\"]` 或 `[False, \"foo\"]`              |      类型头      |\n|         [123, 4567]          |   \"foo\"    |              `[123, \"foo\"]` 或 `[4567, \"foo\"]`              |      元素头      |\n|      [nepattern.NUMBER]      |   \"bar\"    |            `[123, \"bar\"]` 或 `[123.456, \"bar\"]`             |     表达式头     |\n|         [123, \"foo\"]         |   \"bar\"    |      `[123, \"bar\"]` 或 `\"foobar\"` 或 `[\"foo\", \"bar\"]`       |      混合头      |\n| [(int, \"foo\"), (456, \"bar\")] |   \"baz\"    | `[123, \"foobaz\"]` 或 `[456, \"foobaz\"]` 或 `[456, \"barbaz\"]` |       对头       |\n\n对于无前缀的类型头，此时会将传入的值尝试转为 BasePattern，例如 `int` 会转为 `nepattern.INTEGER`。如此该命令头会匹配对应的类型， 例如 `int` 会匹配 `123` 或 `\"456\"`，但不会匹配 `\"foo\"`。解析后，Alconna 会将命令头匹配到的值转为对应的类型，例如 `int` 会将 `\"123\"` 转为 `123`。\n\n:::tip\n\n**正则内容只在命令名上生效，前缀中的正则会被转义**\n\n:::\n\n除了通过传入 `re:xxx` 来使用正则表达式外，Alconna 还提供了一种更加简洁的方式来使用正则表达式，称为 Bracket Header：\n\n```python\nalc = Alconna(\".rd{roll:int}\")\nassert alc.parse(\".rd123\").header[\"roll\"] == 123\n```\n\nBracket Header 类似 python 里的 f-string 写法，通过 `\"{}\"` 声明匹配类型\n\n`\"{}\"` 中的内容为 \"name:type or pat\"：\n\n- `\"{}\"`, `\"{:}\"` ⇔ `\"(.+)\"`, 占位符\n- `\"{foo}\"` ⇔ `\"(?P&lt;foo&gt;.+)\"`\n- `\"{:\\d+}\"` ⇔ `\"(\\d+)\"`\n- `\"{foo:int}\"` ⇔ `\"(?P&lt;foo&gt;\\d+)\"`，其中 `\"int\"` 部分若能转为 `BasePattern` 则读取里面的表达式\n\n## 参数声明(Args)\n\n`Args` 是用于声明命令参数的组件， 可以通过以下几种方式构造 **Args** ：\n\n- `Args[key, var, default][key1, var1, default1][...]`\n- `Args[(key, var, default)]`\n- `Args.key[var, default]`\n\n其中，key **一定**是字符串，而 var 一般为参数的类型，default 为具体的值或者 **arclet.alconna.args.Field**\n\n其与函数签名类似，但是允许含有默认值的参数在前；同时支持 keyword-only 参数不依照构造顺序传入 （但是仍需要在非 keyword-only 参数之后）。\n\n### key\n\n`key` 的作用是用以标记解析出来的参数并存放于 **Arparma** 中，以方便用户调用。\n\n其有三种为 Args 注解的标识符: `?`、`/`、 `!`, 标识符与 key 之间建议以 `;` 分隔：\n\n- `!` 标识符表示该处传入的参数应**不是**规定的类型，或**不在**指定的值中。\n- `?` 标识符表示该参数为**可选**参数，会在无参数匹配时跳过。\n- `/` 标识符表示该参数的类型注解需要隐藏。\n\n另外，对于参数的注释也可以标记在 `key` 中，其与 key 或者标识符 以 `#` 分割：  \n`foo#这是注释;?` 或 `foo?#这是注释`\n\n:::tip\n\n`Args` 中的 `key` 在实际命令中并不需要传入（keyword 参数除外）：\n\n```python\nfrom arclet.alconna import Alconna, Args\n\n\nalc = Alconna(\"test\", Args[\"foo\", str])\nalc.parse(\"test --foo abc\") # 错误\nalc.parse(\"test abc\") # 正确\n```\n\n若需要 `test --foo abc`，你应该使用 `Option`：\n\n```python\nfrom arclet.alconna import Alconna, Args, Option\n\n\nalc = Alconna(\"test\", Option(\"--foo\", Args[\"foo\", str]))\n```\n\n:::\n\n### var\n\nvar 负责命令参数的**类型检查**与**类型转化**\n\n`Args` 的`var`表面上看需要传入一个 `type`，但实际上它需要的是一个 `nepattern.BasePattern` 的实例：\n\n```python\nfrom arclet.alconna import Args\nfrom nepattern import BasePattern\n\n\n# 表示 foo 参数需要匹配一个 @number 样式的字符串\nargs = Args[\"foo\", BasePattern(\"@\\d+\")]\n```\n\n`pip` 示例中可以传入 `str` 是因为 `str` 已经注册在了 `nepattern.global_patterns` 中，因此会替换为 `nepattern.global_patterns[str]`\n\n`nepattern.global_patterns`默认支持的类型有：\n\n- `str`: 匹配任意字符串\n- `int`: 匹配整数\n- `float`: 匹配浮点数\n- `bool`: 匹配 `True` 与 `False` 以及他们小写形式\n- `hex`: 匹配 `0x` 开头的十六进制字符串\n- `url`: 匹配网址\n- `email`: 匹配 `xxxx@xxx` 的字符串\n- `ipv4`: 匹配 `xxx.xxx.xxx.xxx` 的字符串\n- `list`: 匹配类似 `[\"foo\",\"bar\",\"baz\"]` 的字符串\n- `dict`: 匹配类似 `{\"foo\":\"bar\",\"baz\":\"qux\"}` 的字符串\n- `datetime`: 传入一个 `datetime` 支持的格式字符串，或时间戳\n- `Any`: 匹配任意类型\n- `AnyString`: 匹配任意类型，转为 `str`\n- `Number`: 匹配 `int` 与 `float`，转为 `int`\n\n同时可以使用 typing 中的类型：\n\n- `Literal[X]`: 匹配其中的任意一个值\n- `Union[X, Y]`: 匹配其中的任意一个类型\n- `Optional[xxx]`: 会自动将默认值设为 `None`，并在解析失败时使用默认值\n- `List[X]`: 匹配一个列表，其中的元素为 `X` 类型\n- `Dict[X, Y]`: 匹配一个字典，其中的 key 为 `X` 类型，value 为 `Y` 类型\n- ...\n\n:::tip\n\n几类特殊的传入标记：\n\n- `\"foo\"`: 匹配字符串 \"foo\" (若没有某个 `BasePattern` 与之关联)\n- `RawStr(\"foo\")`: 匹配字符串 \"foo\" (即使有 `BasePattern` 与之关联也不会被替换)\n- `\"foo|bar|baz\"`: 匹配 \"foo\" 或 \"bar\" 或 \"baz\"\n- `[foo, bar, Baz, ...]`: 匹配其中的任意一个值或类型\n- `Callable[[X], Y]`: 匹配一个参数为 `X` 类型的值，并返回通过该函数调用得到的 `Y` 类型的值\n- `\"re:xxx\"`: 匹配一个正则表达式 `xxx`，会返回 Match[0]\n- `\"rep:xxx\"`: 匹配一个正则表达式 `xxx`，会返回 `re.Match` 对象\n- `{foo: bar, baz: qux}`: 匹配字典中的任意一个键, 并返回对应的值 (特殊的键 ... 会匹配任意的值)\n- ...\n\n**特别的**，你可以不传入 `var`，此时会使用 `key` 作为 `var`, 匹配 `key` 字符串。\n\n:::\n\n#### MultiVar 与 KeyWordVar\n\n`MultiVar` 是一个特殊的标注，用于告知解析器该参数可以接受多个值，类似于函数中的 `*args`，其构造方法形如 `MultiVar(str)`。\n\n同样的还有 `KeyWordVar`，类似于函数中的 `*, name: type`，其构造方法形如 `KeyWordVar(str)`，用于告知解析器该参数为一个 keyword-only 参数。\n\n:::tip\n\n`MultiVar` 与 `KeyWordVar` 组合时，代表该参数为一个可接受多个 key-value 的参数，类似于函数中的 `**kwargs`，其构造方法形如 `MultiVar(KeyWordVar(str))`\n\n`MultiVar` 与 `KeyWordVar` 也可以传入 `default` 参数，用于指定默认值\n\n`MultiVar` 不能在 `KeyWordVar` 之后传入\n\n:::\n\n#### AllParam\n\n`AllParam` 是一个特殊的标注，用于告知解析器该参数接收命令中在此位置之后的所有参数并**结束解析**，可以认为是**泛匹配参数**。\n\n`AllParam` 可直接使用 (`Args[\"xxx\", AllParam]`), 也可以传入指定的接收类型 (`Args[\"xxx\", AllParam(str)]`)。\n\n:::tip\n\n在 `nonebot_plugin_alconna` 下，`AllParam` 的返回值为 [`UniMessage`](./uniseg/message.mdx)\n\n:::\n\n### default\n\n`default` 传入的是该参数的默认值或者 `Field`，以携带对于该参数的更多信息。\n\n默认情况下 (即不声明) `default` 的值为特殊值 `Empty`。这也意味着你可以将默认值设置为 `None` 表示默认值为空值。\n\n`Field` 构造需要的参数说明如下：\n\n- default: 参数单元的默认值\n- alias: 参数单元默认值的别名\n- completion: 参数单元的补全说明生成函数\n- unmatch_tips: 参数单元的错误提示生成函数，其接收一个表示匹配失败的元素的参数\n- missing_tips: 参数单元的缺失提示生成函数\n\n## 选项与子命令(Option & Subcommand)\n\n`Option` 和 `Subcommand` 可以传入一组 `alias`，如 `Option(\"--foo|-F|--FOO|-f\")`，`Subcommand(\"foo\", alias=[\"F\"])`\n\n传入别名后，选项与子命令会选择其中长度最长的作为其名称。若传入为 \"--foo|-f\"，则命令名称为 \"--foo\"\n\n:::tip 特别提醒!!!\n\nOption 的名字或别名**没有要求**必须在前面写上 `-`\n\nOption 与 Subcommand 的唯一区别在于 Subcommand 可以传入自己的 **Option** 与 **Subcommand**\n\n:::\n\n他们拥有如下共同参数：\n\n- `help_text`: 传入该组件的帮助信息\n- `dest`: 被指定为解析完成时标注匹配结果的标识符，不传入时默认为选项或子命令的名称 (name)\n- `requires`: 一段指定顺序的字符串列表，作为唯一的前置序列与命令嵌套替换\n  对于命令 `test foo bar baz qux <a:int>` 来讲，因为`foo bar baz` 仅需要判断是否相等, 所以可以这么编写：\n\n```python\nAlconna(\"test\", Option(\"qux\", Args.a[int], requires=[\"foo\", \"bar\", \"baz\"]))\n```\n\n- `default`: 默认值，在该组件未被解析时使用使用该值替换。\n  特别的，使用 `OptionResult` 或 `SubcomanndResult` 可以设置包括参数字典在内的默认值：\n\n```python\nfrom arclet.alconna import Option, OptionResult\n\nopt1 = Option(\"--foo\", default=False)\nopt2 = Option(\"--foo\", default=OptionResult(value=False, args={\"bar\": 1}))\n```\n\n### Action\n\n`Option` 可以特别设置传入一类 `Action`，作为解析操作\n\n`Action` 分为三类：\n\n- `store`: 无 Args 时， 仅存储一个值， 默认为 Ellipsis； 有 Args 时， 后续的解析结果会覆盖之前的值\n- `append`: 无 Args 时， 将多个值存为列表， 默认为 Ellipsis； 有 Args 时， 每个解析结果会追加到列表中, 当存在默认值并且不为列表时， 会自动将默认值变成列表， 以保证追加的正确性\n- `count`: 无 Args 时， 计数器加一； 有 Args 时， 表现与 STORE 相同, 当存在默认值并且不为数字时， 会自动将默认值变成 1， 以保证计数器的正确性。\n\n`Alconna` 提供了预制的几类 `Action`：\n\n- `store`(默认)，`store_value`，`store_true`，`store_false`\n- `append`，`append_value`\n- `count`\n\n## 解析结果\n\n`Alconna.parse` 会返回由 **Arparma** 承载的解析结果\n\n`Arparma` 有如下属性：\n\n- 调试类\n  - matched: 是否匹配成功\n  - error_data: 解析失败时剩余的数据\n  - error_info: 解析失败时的异常内容\n  - origin: 原始命令，可以类型标注\n\n- 分析类\n  - header_match: 命令头部的解析结果，包括原始头部、解析后头部、解析结果与可能的正则匹配组\n  - main_args: 命令的主参数的解析结果\n  - options: 命令所有选项的解析结果\n  - subcommands: 命令所有子命令的解析结果\n  - other_args: 除主参数外的其他解析结果\n  - all_matched_args: 所有 Args 的解析结果\n\n### 路径查询\n\n`Arparma` 同时提供了便捷的查询方法 `query[type]()`，会根据传入的 `path` 查找参数并返回\n\n`path` 支持如下:\n\n- `main_args`, `options`, ...: 返回对应的属性\n- `args`: 返回 all_matched_args\n- `args.<key>`: 返回 all_matched_args 中 `key` 键对应的值\n- `main_args.<key>`: 返回主命令的解析参数字典中 `key` 键对应的值\n- `<node>`: 返回选项/子命令 `node` 的解析结果 (OptionResult | SubcommandResult)\n- `<node>.value`: 返回选项/子命令 `node` 的解析值\n- `<node>.args`: 返回选项/子命令 `node` 的解析参数字典\n- `<node>.<key>`, `<node>.args.<key>`: 返回选项/子命令 `node` 的参数字典中 `key` 键对应的值\n\n以及:\n\n- `options.<opt>`: 返回选项 `opt` 的解析结果 (OptionResult)\n- `options.<opt>.value`: 返回选项 `opt` 的解析值\n- `options.<opt>.args`: 返回选项 `opt` 的解析参数字典\n- `options.<opt>.<key>`, `options.<node>.args.<key>`: 返回选项 `opt` 的参数字典中 `key` 键对应的值\n- `subcommands.<subcmd>`: 返回子命令 `subcmd` 的解析结果 (SubcommandResult)\n- `subcommands.<subcmd>.value`: 返回子命令 `subcmd` 的解析值\n- `subcommands.<subcmd>.args`: 返回子命令 `subcmd` 的解析参数字典\n- `subcommands.<subcmd>.<key>`, `subcommands.<node>.args.<key>`: 返回子命令 `subcmd` 的参数字典中 `key` 键对应的值\n\n## 元数据(CommandMeta)\n\n`Alconna` 的元数据相当于其配置，拥有以下条目：\n\n- `description`: 命令的描述\n- `usage`: 命令的用法\n- `example`: 命令的使用样例\n- `author`: 命令的作者\n- `fuzzy_match`: 命令是否开启模糊匹配\n- `fuzzy_threshold`: 模糊匹配阈值\n- `raise_exception`: 命令是否抛出异常\n- `hide`: 命令是否对 manager 隐藏\n- `hide_shortcut`: 命令的快捷指令是否在 help 信息中隐藏\n- `keep_crlf`: 命令解析时是否保留换行字符\n- `compact`: 命令是否允许第一个参数紧随头部\n- `strict`: 命令是否严格匹配，若为 False 则未知参数将作为名为 $extra 的参数\n- `context_style`: 命令上下文插值的风格，None 为关闭，bracket 为 `{...}`，parentheses 为 `$(...)`\n- `extra`: 命令的自定义额外信息\n\n元数据一定使用 `meta=...` 形式传入：\n\n```python\nfrom arclet.alconna import Alconna, CommandMeta\n\nalc = Alconna(..., meta=CommandMeta(\"foo\", example=\"bar\"))\n```\n\n## 命名空间配置\n\n命名空间配置 （以下简称命名空间） 相当于 `Alconna` 的默认配置，其优先度低于 `CommandMeta`。\n\n`Alconna` 默认使用 \"Alconna\" 命名空间。\n\n命名空间有以下几个属性：\n\n- name: 命名空间名称\n- prefixes: 默认前缀配置\n- separators: 默认分隔符配置\n- formatter_type: 默认格式化器类型\n- fuzzy_match: 默认是否开启模糊匹配\n- raise_exception: 默认是否抛出异常\n- builtin_option_name: 默认的内置选项名称(--help, --shortcut, --comp)\n- disable_builtin_options: 默认禁用的内置选项(--help, --shortcut, --comp)\n- enable_message_cache: 默认是否启用消息缓存\n- compact: 默认是否开启紧凑模式\n- strict: 命令是否严格匹配\n- context_style: 命令上下文插值的风格\n- ...\n\n### 新建命名空间并替换\n\n```python\nfrom arclet.alconna import Alconna, namespace, Namespace, Subcommand, Args, config\n\n\nns = Namespace(\"foo\", prefixes=[\"/\"])  # 创建 \"foo\"命名空间配置, 它要求创建的Alconna的主命令前缀必须是/\n\nalc = Alconna(\"pip\", Subcommand(\"install\", Args[\"package\", str]), namespace=ns) # 在创建Alconna时候传入命名空间以替换默认命名空间\n\n# 可以通过with方式创建命名空间\nwith namespace(\"bar\") as np1:\n    np1.prefixes = [\"!\"]    # 以上下文管理器方式配置命名空间，此时配置会自动注入上下文内创建的命令\n    np1.formatter_type = ShellTextFormatter  # 设置此命名空间下的命令的 formatter 默认为 ShellTextFormatter\n    np1.builtin_option_name[\"help\"] = {\"帮助\", \"-h\"}  # 设置此命名空间下的命令的帮助选项名称\n\n# 你还可以使用config来管理所有命名空间并切换至任意命名空间\nconfig.namespaces[\"foo\"] = ns  # 将命名空间挂载到 config 上\n\nalc = Alconna(\"pip\", Subcommand(\"install\", Args[\"package\", str]), namespace=config.namespaces[\"foo\"]) # 也是同样可以切换到\"foo\"命名空间\n```\n\n### 修改默认的命名空间\n\n```python\nfrom arclet.alconna import config, namespace, Namespace\n\n\nconfig.default_namespace.prefixes = [...]  # 直接修改默认配置\n\nnp = Namespace(\"xxx\", prefixes=[...])\nconfig.default_namespace = np  # 更换默认的命名空间\n\nwith namespace(config.default_namespace.name) as np:\n    np.prefixes = [...]\n```\n\n## 快捷指令\n\n快捷命令可以做到标识一段命令, 并且传递参数给原命令\n\n一般情况下你可以通过 `Alconna.shortcut` 进行快捷指令操作 (创建，删除)\n\n`shortcut` 的第一个参数为快捷指令名称，第二个参数为 `ShortcutArgs`，作为快捷指令的配置：\n\n```python\nclass ShortcutArgs(TypedDict):\n    \"\"\"快捷指令参数\"\"\"\n\n    command: NotRequired[str]\n    \"\"\"快捷指令的命令\"\"\"\n    args: NotRequired[list[Any]]\n    \"\"\"快捷指令的附带参数\"\"\"\n    fuzzy: NotRequired[bool]\n    \"\"\"是否允许命令后随参数\"\"\"\n    prefix: NotRequired[bool]\n    \"\"\"是否调用时保留指令前缀\"\"\"\n    wrapper: NotRequired[ShortcutRegWrapper]\n    \"\"\"快捷指令的正则匹配结果的额外处理函数\"\"\"\n    humanized: NotRequired[str]\n    \"\"\"快捷指令的人类可读描述\"\"\"\n```\n\n### args的使用\n\n```python\nfrom arclet.alconna import Alconna, Args\n\n\nalc = Alconna(\"setu\", Args[\"count\", int])\n\nalc.shortcut(\"涩图(\\d+)张\", {\"args\": [\"{0}\"]})\n# 'Alconna::setu 的快捷指令: \"涩图(\\\\d+)张\" 添加成功'\n\nalc.parse(\"涩图3张\").query(\"count\")\n# 3\n```\n\n### command的使用\n\n```python\nfrom arclet.alconna import Alconna, Args\n\n\nalc = Alconna(\"eval\", Args[\"content\", str])\n\nalc.shortcut(\"echo\", {\"command\": \"eval print(\\\\'{*}\\\\')\"})\n# 'Alconna::eval 的快捷指令: \"echo\" 添加成功'\n\nalc.shortcut(\"echo\", delete=True) # 删除快捷指令\n# 'Alconna::eval 的快捷指令: \"echo\" 删除成功'\n\n@alc.bind() # 绑定一个命令执行器, 若匹配成功则会传入参数, 自动执行命令执行器\ndef cb(content: str):\n    eval(content, {}, {})\n\nalc.parse('eval print(\\\\\"hello world\\\\\")')\n# hello world\n\nalc.parse(\"echo hello world!\")\n# hello world!\n```\n\n当 `fuzzy` 为 False 时，第一个例子中传入 `\"涩图1张 abc\"` 之类的快捷指令将视为解析失败\n\n快捷指令允许三类特殊的 placeholder：\n\n- `{%X}`: 如 `setu {%0}`，表示此处填入快捷指令后随的第 X 个参数。\n\n例如，若快捷指令为 `涩图`, 配置为 `{\"command\": \"setu {%0}\"}`, 则指令 `涩图 1` 相当于 `setu 1`\n\n- `{*}`: 表示此处填入所有后随参数，并且可以通过 `{*X}` 的方式指定组合参数之间的分隔符。\n\n- `{X}`: 表示此处填入可能的正则匹配的组：\n\n- 若 `command` 中存在匹配组 `(xxx)`，则 `{X}` 表示第 X 个匹配组的内容\n- 若 `command` 中存储匹配组 `(?P<xxx>...)`, 则 `{X}` 表示 **名字** 为 X 的匹配结果\n\n除此之外, 通过 **Alconna** 内置选项 `--shortcut` 可以动态操作快捷指令\n\n例如：\n\n- `cmd --shortcut <key> <cmd>` 来增加一个快捷指令\n- `cmd --shortcut list` 来列出当前指令的所有快捷指令\n- `cmd --shortcut delete key` 来删除一个快捷指令\n\n```python\nfrom arclet.alconna import Alconna, Args\n\n\nalc = Alconna(\"eval\", Args[\"content\", str])\n\nalc.shortcut(\"echo\", {\"command\": \"eval print(\\\\'{*}\\\\')\"})\n\nalc.parse(\"eval --shortcut list\")\n# 'echo'\n```\n\n## 紧凑命令\n\n`Alconna`, `Option` 与 `Subcommand` 可以设置 `compact=True` 使得解析命令时允许名称与后随参数之间没有分隔：\n\n```python\nfrom arclet.alconna import Alconna, Option, CommandMeta, Args\n\n\nalc = Alconna(\"test\", Args[\"foo\", int], Option(\"BAR\", Args[\"baz\", str], compact=True), meta=CommandMeta(compact=True))\n\nassert alc.parse(\"test123 BARabc\").matched\n```\n\n这使得我们可以实现如下命令：\n\n```python\nfrom arclet.alconna import Alconna, Option, Args, append\n\n\nalc = Alconna(\"gcc\", Option(\"--flag|-F\", Args[\"content\", str], action=append, compact=True))\nprint(alc.parse(\"gcc -Fabc -Fdef -Fxyz\").query[list](\"flag.content\"))\n# ['abc', 'def', 'xyz']\n```\n\n当 `Option` 的 `action` 为 `count` 时，其自动支持 `compact` 特性：\n\n```python\nfrom arclet.alconna import Alconna, Option, count\n\n\nalc = Alconna(\"pp\", Option(\"--verbose|-v\", action=count, default=0))\nprint(alc.parse(\"pp -vvv\").query[int](\"verbose.value\"))\n# 3\n```\n\n## 模糊匹配\n\n模糊匹配会应用在任意需要进行名称判断的地方，如 **命令名称**，**选项名称** 和 **参数名称** (如指定需要传入参数名称)。\n\n```python\nfrom arclet.alconna import Alconna, CommandMeta\n\n\nalc = Alconna(\"test_fuzzy\", meta=CommandMeta(fuzzy_match=True))\n\nalc.parse(\"test_fuzy\")\n# test_fuzy is not matched. Do you mean \"test_fuzzy\"?\n```\n\n## 半自动补全\n\n半自动补全为用户提供了推荐后续输入的功能\n\n补全默认通过 `--comp` 或 `-cp` 或 `?` 触发：（命名空间配置可修改名称）\n\n```python\nfrom arclet.alconna import Alconna, Args, Option\n\n\nalc = Alconna(\"test\", Args[\"abc\", int]) + Option(\"foo\") + Option(\"bar\")\nalc.parse(\"test --comp\")\n\n'''\noutput\n\n以下是建议的输入：\n* <abc: int>\n* --help\n* -h\n* -sct\n* --shortcut\n* foo\n* bar\n'''\n```\n\n## Duplication\n\n**Duplication** 用来提供更好的自动补全，类似于 **ArgParse** 的 **Namespace**\n\n普通情况下使用，需要利用到 **ArgsStub**、**OptionStub** 和 **SubcommandStub** 三个部分\n\n以pip为例，其对应的 Duplication 应如下构造:\n\n```python\nfrom arclet.alconna import Alconna, Args, Option, OptionResult, Duplication, SubcommandStub, Subcommand, count\n\n\nclass MyDup(Duplication):\n    verbose: OptionResult\n    install: SubcommandStub\n\n\nalc = Alconna(\n    \"pip\",\n    Subcommand(\n        \"install\",\n        Args[\"package\", str],\n        Option(\"-r|--requirement\", Args[\"file\", str]),\n        Option(\"-i|--index-url\", Args[\"url\", str]),\n    ),\n    Option(\"-v|--version\"),\n    Option(\"-v|--verbose\", action=count),\n)\n\nres = alc.parse(\"pip -v install ...\") # 不使用duplication获得的提示较少\nprint(res.query(\"install\"))\n# (value=Ellipsis args={'package': '...'} options={} subcommands={})\n\nresult = alc.parse(\"pip -v install ...\", duplication=MyDup)\nprint(result.install)\n# 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')\n```\n\n**Duplication** 也可以如 **Namespace** 一样直接标明参数名称和类型：\n\n```python\nfrom typing import Optional\nfrom arclet.alconna import Duplication\n\n\nclass MyDup(Duplication):\n    package: str\n    file: Optional[str] = None\n    url: Optional[str] = None\n```\n\n## 上下文插值\n\n当 `context_style` 条目被设置后，传入的命令中符合上下文插值的字段会被自动替换成当前上下文中的信息。\n\n上下文可以在 `parse` 中传入：\n\n```python\nfrom arclet.alconna import Alconna, Args, CommandMeta\n\nalc = Alconna(\"test\", Args[\"foo\", int], meta=CommandMeta(context_style=\"parentheses\"))\n\nalc.parse(\"test $(bar)\", {\"bar\": 123})\n# {\"foo\": 123}\n```\n\ncontext_style 的值分两种：\n\n- `\"bracket\"`: 插值格式为 `{...}`，例如 `{foo}`\n- `\"parentheses\"`: 插值格式为 `$(...)`，例如 `$(bar)`\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/best-practice/alconna/config.md",
    "content": "---\nsidebar_position: 4\ndescription: 配置项\n---\n\n# 配置项\n\n## alconna_auto_send_output\n\n- **类型**: `bool | None`\n- **默认值**: `None`\n\n是否全局启用输出信息自动发送，不启用则会在触发特殊内置选项后仍然将解析结果传递至响应器。\n\n## alconna_use_command_start\n\n- **类型**: `bool`\n- **默认值**: `False`\n\n是否读取 Nonebot 的配置项 `COMMAND_START` 来作为全局的 Alconna 命令前缀\n\n## alconna_global_completion\n\n- **类型**: [`CompConfig | None`](./matcher.mdx#补全会话)\n- **默认值**: `None`\n\n全局的补全会话配置 (不代表全局启用补全会话)。\n\n## alconna_use_origin\n\n- **类型**: `bool`\n- **默认值**: `False`\n\n是否全局使用原始消息 (即未经过 to_me 等处理的)，该选项会影响到 Alconna 的匹配行为。\n\n## alconna_use_command_sep\n\n- **类型**: `bool`\n- **默认值**: `False`\n\n是否读取 Nonebot 的配置项 `COMMAND_SEP` 来作为全局的 Alconna 命令分隔符。\n\n## alconna_global_extensions\n\n- **类型**: `list[str]`\n- **默认值**: `[]`\n\n全局加载的扩展，其读取路径以 . 分隔，如 `foo.bar.baz:DemoExtension`。\n\n对于内置扩展，路径为 `nonebot_plugin_alconna.builtins.extensions` 下的模块名，如 `ReplyMergeExtension`，可以使用 `@` 来缩写路径，\n如 `@reply:ReplyMergeExtension`。\n\n## alconna_context_style\n\n- **类型**: `Optional[Literal[\"bracket\", \"parentheses\"]]`\n- **默认值**: `None`\n\n全局命令上下文插值的风格，None 为关闭，bracket 为 `{...}`，parentheses 为 `$(...)`。\n\n## alconna_enable_saa_patch\n\n- **类型**: `bool`\n- **默认值**: `False`\n\n是否启用 SAA 补丁。\n\n## alconna_apply_filehost\n\n- **类型**: `bool`\n- **默认值**: `False`\n\n是否启用文件托管。\n\n## alconna_apply_fetch_targets\n\n- **类型**: `bool`\n- **默认值**: `False`\n\n是否启动时拉取一次[发送对象](./uniseg/utils.mdx#发送对象)列表。\n\n## alconna_builtin_plugins\n\n- **类型**: `set[str]`\n- **默认值**: `set()`\n\n需要加载的内置插件列表。\n\n## alconna_conflict_resolver\n\n- **类型**: `Literal[\"raise\", \"default\", \"ignore\", \"replace\"]`\n- **默认值**: `\"default\"`\n\n命令冲突解决策略，决定当不同插件之间或者同一插件之间存在两个以上相同的命令时的处理方式：\n\n- `default`: 默认处理方式，保留两个命令\n- `raise`: 抛出异常\n- `ignore`: 忽略较新的命令\n- `replace`: 替换较旧的命令\n\n## alconna_response_self\n\n- **类型**: `bool`\n- **默认值**: `False`\n\n是否让响应器处理由 bot 自身发送的消息。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/best-practice/alconna/matcher.mdx",
    "content": "---\nsidebar_position: 3\ndescription: 响应规则的使用\n---\n\nimport Messenger from \"@site/src/components/Messenger\";\nimport Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\n\n# `on_alconna` 响应器\n\n`nonebot_plugin_alconna` 插件本体的大部分功能都围绕着 `on_alconna` 响应器展开。\n\n该响应器类似于 `on_command`，基于 `Alconna` 解析器来解析命令。\n\n以下是一个简单的 `on_alconna` 响应器的例子：\n\n```python\nfrom nonebot_plugin_alconna import At, Image, Match, on_alconna\nfrom arclet.alconna import Args, Option, Alconna, MultiVar, Subcommand\n\n\nalc = Alconna(\n    \"role-group\",\n    Subcommand(\n        \"add|添加\",\n        Args[\"name\", str],\n        Option(\"member\", Args[\"target\", MultiVar(At)]),\n        dest=\"add\",\n        compact=True,\n    ),\n    Option(\"list\"),\n    Option(\"icon\", Args[\"icon\", Image])\n)\nrg = on_alconna(alc, use_command_start=True, aliases={\"角色组\"})\n\n\n@rg.assign(\"list\")\nasync def list_role_group():\n    img: bytes = await gen_role_group_list_image()\n    await rg.finish(Image(raw=img))\n\n@rg.assign(\"add\")\nasync def _(name: str, target: Match[tuple[At, ...]]):\n    group = await create_role_group(name)\n    if target.available:\n        ats: tuple[At, ...] = target.result\n        group.extend(member.target for member in ats)\n    await rg.finish(\"添加成功\")\n```\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/role-group list\" },\n    {\n      position: \"left\",\n      msg: \"[图片]\",\n    },\n    { position: \"right\", msg: \"/角色组 添加foo @bar @baz\" },\n    { position: \"left\", msg: \"添加成功\" },\n  ]}\n/>\n\n## 声明\n\n`on_alconna` 的参数如下:\n\n```python\ndef on_alconna(\n    command: Alconna | str,\n    rule: Rule | T_RuleChecker | None = None,\n    skip_for_unmatch: bool = True,\n    auto_send_output: bool | None = None,\n    aliases: set[str] | tuple[str, ...] | None = None,\n    comp_config: CompConfig | None = None,\n    extensions: list[type[Extension] | Extension] | None = None,\n    exclude_ext: list[type[Extension] | str] | None = None,\n    use_origin: bool | None = None,\n    use_cmd_start: bool | None = None,\n    use_cmd_sep: bool | None = None,\n    response_self: bool | None = None,\n    **kwargs: Any,\n) -> type[AlconnaMatcher]:\n    ...\n```\n\n- `command`: Alconna 命令或字符串，字符串将通过 `AlconnaFormat` 转换为 Alconna 命令\n- `rule`: 事件响应规则， 详见 [响应器规则](../../advanced/matcher.md#事件响应规则)\n- `skip_for_unmatch`: 是否在命令不匹配时跳过该响应, 默认为 `True`\n- `auto_send_output`: 是否自动发送输出信息并跳过该响应。\n  - `True`：自动发送输出信息并跳过该响应\n  - `False`：不自动发送输出信息，而是传递进行处理\n  - `None`：跟随全局配置项 `alconna_auto_send_output`，默认值为 `True`\n- `aliases`: 命令别名， 作用类似于 `on_command` 中的 aliases\n- `comp_config`: 补全会话配置， 不传入则不启用补全会话\n- `extensions`: 需要加载的匹配扩展, 可以是扩展类或扩展实例\n- `exclude_ext`: 需要排除的匹配扩展, 可以是扩展类或扩展的id\n- `use_origin`: 是否使用未经 to_me 等处理过的消息。`None` 时跟随全局配置项 `alconna_use_origin`，默认值为 `False`\n- `use_cmd_start`: 是否使用 COMMAND_START 作为命令前缀。`None` 时跟随全局配置项 `alconna_use_command_start`，默认值为 `False`\n- `use_cmd_sep`: 是否使用 COMMAND_SEP 作为命令分隔符。`None` 时跟随全局配置项 `alconna_use_command_sep`，默认值为 `False`\n- `response_self`: 是否响应自身消息。`None` 时跟随全局配置项 `alconna_response_self`，默认值为 `False`\n\n`on_alconna` 返回的是 `Matcher` 的子类 `AlconnaMatcher` ，其拓展了如下方法：\n\n- `.assign(path, value, or_not)`: 用于对包含多个选项/子命令的命令的分派处理\n- `.dispatch`: 同样的分派处理，但是是类似 `CommandGroup` 一样返回新的 `AlconnaMatcher`\n- `.got_path(path, prompt, middleware)`: 在 `got` 方法的基础上，会以 path 对应的参数为准，读取传入 message 的最后一个消息段并验证转换\n- `.got`, `send`, `reject`, ... : 拓展了 prompt 类型，即支持使用 `UniMessage` 作为 prompt\n- ...\n\n除了标准的创建方式，本插件也提供了 `funcommand` 和 `Command` 两种快捷方式来创建 `AlconnaMatcher`， 详见 [快捷方式](./shortcut.md)。\n\n## 依赖注入\n\n`AlconnaMatcher` 的特性之一是拓展了依赖注入的功能。\n\n### 注入模型\n\n插件提供了几种用来处理解析结果的模型：\n\n- `CommandResult`: 用于快捷访问命令解析结果\n  - `result (Arparma)`: 解析结果\n  - `source (Alconna)`: 源命令\n  - `matched (bool)`: 是否匹配\n  - `context (dict)`: 命令的上下文\n  - `output (str | None)`: 命令的输出\n- `Match`: 匹配项，表示参数是否存在于 `Arparma.all_matched_args` 内，可用 `Match.available` 判断是否匹配，`Match.result` 获取匹配的值\n  - `Match` 只能查找到 `Arparma.all_matched_args` 中的参数。对于特定选项/子命令的参数，需要使用 `Query` 来查询\n- `Query`: 查询项，表示参数是否可由 `Arparma.query` 查询并获得结果，可用 `Query.available` 判断是否查询成功，`Query.result` 获取查询结果\n  - `Query` 除了查询参数，也可以查询某个选项/子命令是否存在\n\n### 编写\n\n```python\nasync def handle(\n    result: CommandResult,\n    arp: Arparma,\n    dup: Duplication,\n    source: Alconna,\n    ext: Extension,\n    exts: SelectedExtensions,\n    abc: str,\n    foo: Match[str],\n    bar: Query[int] = Query(\"ttt.bar\", 0)\n):\n    ...\n```\n\n`AlconnaMatcher` 的依赖注入拓展支持以下情况：\n\n- `xxx: CommandResult`\n- `xxx: Arparma`：命令的[解析结果](./command.md#解析结果)\n- `xxx: Duplication`：命令的解析结果的 [`Duplication`](./command.md#duplication)\n- `xxx: Alconna`：命令的源命令\n- `<key>: Match[<type>]`：上述的匹配项，使用 `key` 作为查询路径\n- `xxx: Query[<type>] = Query(<path>, default)`：上述的查询项，必需声明默认值以设置查询路径 `path`\n  - 当用来查询选项/子命令是否存在时，可不写 `Query[<type>]`\n- `xxx: Extension`：当前 `AlconnaMatcher` 使用的指定类型的匹配扩展\n- `xxx: SelectedExtensions`：当前 `AlconnaMatcher` 使用的所有可用的匹配扩展\n- `<key>: <type>`: 其他情况\n  - 当 `key` 的名称是 \"ctx\" 或 \"context\" 并且类型为 `dict` 时，会注入命令的上下文\n  - 当 `key` 存在于命令的上下文中时，会注入对应的值\n  - 当 `key` 存在于 `Arparma` 的 `all_matched_args` 中时，会注入对应的值, 类似于 `Match` 的用法，但当该值不存在时将跳过响应器。\n  - 当 `key` 属于 `got_path` 的参数时，会注入对应的值\n  - 当 `key` 被某个 `Extension.before_catch` 确认为需要注入的参数时，会调用 `Extension.catch` 来注入对应的值\n\n:::note\n\n如果你更喜欢 Depends 式的依赖注入，`nonebot_plugin_alconna` 同时提供了一系列的依赖注入函数，他们包括：\n\n- `AlconnaResult`: `CommandResult` 类型的依赖注入函数\n- `AlconnaMatches`: `Arparma` 类型的依赖注入函数\n- `AlconnaDuplication`: `Duplication` 类型的依赖注入函数\n- `AlconnaMatch`: `Match` 类型的依赖注入函数，其能够额外传入一个 middleware 函数来处理得到的参数\n- `AlconnaQuery`: `Query` 类型的依赖注入函数，其能够额外传入一个 middleware 函数来处理得到的参数\n- `AlconnaExecResult`: 提供挂载在命令上的 callback 的返回结果 (`Dict[str, Any]`) 的依赖注入函数\n- `AlconnaExtension`: 提供指定类型的 `Extension` 的依赖注入函数\n\n:::\n\n示例：\n\n```python\nfrom nonebot import require\nrequire(\"nonebot_plugin_alconna\")\n\nfrom nonebot_plugin_alconna import AlconnaQuery, AlcResult, Match, Query, on_alconna\nfrom arclet.alconna import Alconna, Args, Option, Arparma\n\n\ntest = on_alconna(\n    Alconna(\n        \"test\",\n        Option(\"foo\", Args[\"bar\", int]),\n        Option(\"baz\", Args[\"qux\", bool, False])\n    )\n)\n\n@test.handle()\nasync def handle_test1(result: AlcResult):\n    await test.send(f\"matched: {result.matched}\")\n    await test.send(f\"maybe output: {result.output}\")\n\n@test.handle()\nasync def handle_test2(result: Arparma):\n    await test.send(f\"head result: {result.header_result}\")\n    await test.send(f\"args: {result.all_matched_args}\")\n\n@test.handle()\nasync def handle_test3(bar: Match[int]):\n    if bar.available:\n        await test.send(f\"foo={bar.result}\")\n\n@test.handle()\nasync def handle_test4(qux: Query[bool] = AlconnaQuery(\"baz.qux\", False)):\n    if qux.available:\n        await test.send(f\"baz.qux={qux.result}\")\n```\n\n## 条件控制\n\n### `assign` 方法\n\n`AlconnaMatcher` 的 `assign` 方法与 `handle` 类似，但是可以控制响应函数是否在不满足条件时跳过响应。\n\n`assign` 方法的参数如下：\n\n```python\ndef assign(\n    cls,\n    path: str,\n    value: Any = _seminal,\n    or_not: bool = False,\n    additional: CHECK | None = None,\n    parameterless: Iterable[Any] | None = None,\n):\n    ...\n```\n\n- `path`: 指定的[查询路径](./command.md#路径查询)\n  - \"$main\" 表示没有任何选项/子命令匹配的时候\n  - \"\\~XX\" 时会把 \"\\~\" 替换为父级路径\n- `value`: 可能的指定查询值\n- `or_not`: 是否同时处理没有查询成功的情况\n- `additional`: 额外的条件检查函数\n\n例如：\n\n```python\n# 处理没有任何选项/子命令匹配的情况\n@rg.assign(\"$main\")\nasync def handle_main(): ...\n\n# 处理 list 选项\n@rg.assign(\"list\")\nasync def handle_list(): ...\n\n# 处理 add 选项，且 name 为 admin\n@rg.assign(\"add.name\", \"admin\")\nasync def handle_add_admin(): ...\n```\n\n### `dispatch` 方法\n\n此外，使用 `.dispatch` 还能像 `CommandGroup` 一样为每个分发设置独立的 matcher：\n\n```python\nrg_list_cmd = rg.dispatch(\"list\")\n\n@rg_list_cmd.handle()\nasync def handle_list(): ...\n```\n\n`dispatch` 的参数与 `assign` 相同。\n\n当使用 `dispatch` 时，父级路径表示为传入 `dispatch` 的 `path`:\n\n```python\nrg_add_cmd = rg.dispatch(\"add\")\n\n# 此时 ~name 表示 add.name\n@rg_add_cmd.assign(\"~name\", \"admin\")\nasync def handle_add_admin(): ...\n```\n\n:::tip\n\n在 `dispatch` 下, `Query` 的 `path` 也同样支持 `~` 前缀来表示父级路径\n\n```python\n@rg_add_cmd.assign(\"~name\", \"admin\")\nasync def handle_add_admin(target: Query[tuple[At, ...]] = Query(\"~target\")):\n    if target.available:\n        await rg.send(f\"添加成功: {target.result}\")\n```\n\n:::\n\n### `got_path` 方法\n\n另外，`AlconnaMatcher` 有类似于 [`got`](../../appendices/session-control.mdx#got) 的 `got_path` 与配套的 `get_path_arg`, `set_path_arg`：\n\n```python\nfrom nonebot_plugin_alconna import At, Match, UniMessage, on_alconna\n\n\ntest_cmd = on_alconna(Alconna(\"test\", Args[\"target?\", Union[str, At]]))\n\n@test_cmd.handle()\nasync def tt_h(target: Match[Union[str, At]]):\n    if target.available:\n        test_cmd.set_path_arg(\"target\", target.result)\n\n@test_cmd.got_path(\"target\", prompt=\"请输入目标\")\nasync def tt(target: Union[str, At]):\n    await test_cmd.send(UniMessage([\"ok\\n\", target]))\n```\n\n`got_path` 与 `assign`，`Match`，`Query` 等地方一样，都需要指明 `path` 参数 (即对应 Arg 验证的路径)\n\n`got_path` 会获取消息的最后一个消息段并转为 path 对应的类型，例如示例中 `target` 对应的 Arg 里要求 str 或 At，则 got 后用户输入的消息只有为 text 或 at 才能进入处理函数。\n\n`got_path` 中可以使用依赖注入函数 `AlconnaArg`, 类似于 [`Arg`](../../advanced/dependency.mdx#arg).\n\n### `prompt` 方法\n\n基于 [`Waiter`](https://github.com/RF-Tar-Railt/nonebot-plugin-waiter) 插件，`AlconnaMatcher` 提供了 `prompt` 方法来实现更灵活的交互式提示。\n\n```python\nfrom nonebot_plugin_alconna import At, Match, UniMessage, on_alconna\n\n\ntest_cmd = on_alconna(Alconna(\"test\", Args[\"target?\", Union[str, At]]))\n\n@test_cmd.handle()\nasync def tt_h(target: Match[Union[str, At]]):\n    if target.available:\n        await test_cmd.finish(UniMessage([\"ok\\n\", target]))\n    resp = await test_cmd.prompt(\"请输入目标\", timeout=30)  # 等待 30 秒\n    if resp is None:\n        await test_cmd.finish(\"超时\")\n    await test_cmd.finish(UniMessage([\"ok\\n\", resp[-1]]))\n```\n\n## 返回值中间件\n\n在 `AlconnaMatch`，`AlconnaQuery` 或 `got_path` 中，你可以使用 `middleware` 参数来传入一个对返回值进行处理的函数：\n\n```python\nfrom nonebot_plugin_alconna import image_fetch\n\n\nmask_cmd = on_alconna(Alconna(\"search\", Args[\"img?\", Image]))\n\n\n@mask_cmd.handle()\nasync def mask_h(matcher: AlconnaMatcher, img: Match[bytes] = AlconnaMatch(\"img\", image_fetch)):\n    result = await search_img(img.result)\n    await matcher.send(result.content)\n```\n\n其中，`image_fetch` 是一个中间件，其接受一个 `Image` 对象，并提取图片的二进制数据返回。\n\n## i18n\n\n本插件基于 `tarina.lang` 模块提供了 i18n 的支持，参见 [Lang 用法](https://github.com/nonebot/plugin-alconna/discussions/50)。\n\n当你编写完语言文件后，你便可以通过 `AlconnaMatcher.i18n` 来快速地将语言文件中的内容转为 UniMessage.\n\n<Tabs groupId=\"i18n\">\n<TabItem value=\"zh\" label=\"中文\">\n\n```yaml title=\"zh-CN.yml\"\n# 中文语言文件\ndemo:\n  command:\n    role-group:\n      add: 添加 {name} 成功!\n```\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/角色组 添加 foo\" },\n    { position: \"left\", msg: \"添加 foo 成功!\" },\n  ]}\n/>\n\n</TabItem>\n<TabItem value=\"en\" label=\"英文\">\n\n```yaml title=\"en-US.yml\"\n# 英文语言文件\ndemo:\n  command:\n    role-group:\n      add: Add {name} successfully!\n```\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/role-group add foo\" },\n    { position: \"left\", msg: \"Add foo successfully!\" },\n  ]}\n/>\n</TabItem>\n</Tabs>\n\n```python title=\"使用 i18n\"\n@rg.assign(\"add\")\nasync def handle_add(name: str):\n    await rg.i18n(\"demo\", \"command.role-group.add\", name=name).finish()\n```\n\n## 匹配测试\n\n`AlconnaMatcher.test` 方法允许你在 NoneBot 启动时对命令进行测试。\n\n```python\ndef test(\n    cls,\n    message: str | UniMessage,\n    expected: dict[str, Any] | None = None,\n    prefix: bool = True\n): ...\n```\n\n- `message`: 测试的消息\n- `expected`: 预期的解析结果，若为 None 则表示只测试是否匹配\n- `prefix`: 是否使用命令前缀，默认为 True\n\n## 匹配拓展\n\n本插件提供了一个 `Extension` 类，其用于自定义 AlconnaMatcher 的部分行为\n\n目前 `Extension` 的功能有：\n\n- `validate`: 对于事件的来源适配器或 bot 选择是否接受响应\n- `output_converter`: 输出信息的自定义转换方法\n- `message_provider`: 从传入事件中自定义提取消息的方法\n- `receive_provider`: 对传入的消息 (UniMessage) 的额外处理\n- `context_provider`: 对命令上下文的额外处理\n- `permission_check`: 命令对消息解析并确认头部匹配（即确认选择响应）时对发送者的权限判断\n- `parse_wrapper`: 对命令解析结果的额外处理\n- `send_wrapper`: 对发送的消息 (Message 或 UniMessage) 的额外处理\n- `before_catch`: 自定义依赖注入的绑定确认函数\n- `catch`: 自定义依赖注入处理函数\n- `post_init`: 响应器创建后对命令对象的额外处理\n\n:::tip\n\nExtension 可以通过 `add_global_extension` 方法来全局添加。\n\n```python\nfrom nonebot_plugin_alconna import add_global_extension\nfrom nonebot_plugin_alconna.builtins.extensions.telegram import TelegramSlashExtension\n\nadd_global_extension(TelegramSlashExtension)\n```\n\n全局的 Extension 可延迟加载 (即若有全局拓展加载于部分 AlconnaMatcher 之后，这部分响应器会被追加拓展)\n\n:::\n\n例如一个 `LLMExtension` 可以如下实现 (仅举例)：\n\n```python\nfrom nonebot_plugin_alconna import Extension, Alconna, on_alconna, Interface\n\n\nclass LLMExtension(Extension):\n    @property\n    def priority(self) -> int:\n        return 10\n\n    @property\n    def id(self) -> str:\n        return \"LLMExtension\"\n\n    def __init__(self, llm):\n      self.llm = llm\n\n    def post_init(self, alc: Alconna) -> None:\n        self.llm.add_context(alc.command, alc.meta.description)\n\n    async def receive_wrapper(self, bot, event, receive):\n        resp = await self.llm.input(str(receive))\n        return receive.__class__(resp.content)\n\n    def before_catch(self, name, annotation, default):\n        return name == \"llm\"\n\n    def catch(self, interface: Interface):\n        if interface.name == \"llm\":\n            return self.llm\n\nmatcher = on_alconna(\n    Alconna(...),\n    extensions=[LLMExtension(LLM)]\n)\n...\n```\n\n那么添加了 `LLMExtension` 的响应器便能接受任何能通过 llm 翻译为具体命令的自然语言消息，同时可以在响应器中为所有 `llm` 参数注入模型变量。\n\n### validate\n\n```python\ndef validate(self, bot: Bot, event: Event) -> bool: ...\n```\n\n默认情况下，`validate` 方法会筛选 `event.get_type()` 为 `message` 的情况，表示接受消息事件。\n\n### output_converter\n\n```python\nasync def output_converter(self, output_type: OutputType, content: str) -> UniMessage: ...\n```\n\n依据输出信息的类型，将字符串转换为消息对象以便发送。\n\n其中 `OutputType` 为 \"help\", \"shortcut\", \"completion\", \"error\" 其中之一。\n\n该方法只会调用一次，即对于多个 Extension，选择优先级靠前且实现了该方法的 Extension。\n\n### message_provider\n\n```python\nasync def message_provider(\n    self, event: Event, state: T_State, bot: Bot, use_origin: bool = False\n) -> UniMessage | None:...\n```\n\n该方法用于从事件中提取消息，默认情况下会使用 `event.get_message()` 来获取消息。\n\n该方法可能会调用多次，即对于多个 Extension，选择优先级靠前且实现了该方法的 Extension，若调用的返回值不为 `None` 则作为结果。\n\n:::caution\n\n该方法的默认实现对结果 (UniMessage) 会进行缓存。`Extension` 的实现也应尽量实现缓存机制。\n\n:::\n\n### receive_provider\n\n```python\nasync def receive_provider(self, bot: Bot, event: Event, command: Alconna, receive: UniMessage) -> UniMessage: ...\n```\n\n该方法用于对传入的消息 (UniMessage) 进行额外处理，默认情况下会返回原始消息。\n\n该方法会调用多次，即对于多个 Extension，前一个 Extension 的返回值会作为下一个 Extension 的输入。\n\n### context_provider\n\n```python\nasync def context_provider(self, ctx: dict[str, Any], bot: Bot, event: Event, state: T_State) -> dict[str, Any]:\n```\n\n该方法用于提取命令上下文，默认情况下会返回 `ctx` 本身。\n\n该方法会调用多次，即对于多个 Extension，前一个 Extension 的返回值会作为下一个 Extension 的输入。\n\n### permission_check\n\n```python\nasync def permission_check(self, bot: Bot, event: Event, command: Alconna) -> bool: ...\n```\n\n该方法用于对发送者的权限进行检查，默认情况下会返回 `True`。\n\n该方法可能会调用多次，即对于多个 Extension，若调用的返回值不为 `True` 则结束判断。\n\n### parse_wrapper\n\n```python\nasync def parse_wrapper(self, bot: Bot, state: T_State, event: Event, res: Arparma) -> None: ...\n```\n\n该方法用于对命令解析结果进行额外处理。\n\n该方法会调用多次，即对于多个 Extension，会并发地调用该方法。\n\n### send_wrapper\n\n```python\nasync def send_wrapper(self, bot: Bot, event: Event, send: TMessage) -> TMessage: ...\n```\n\n该方法用于对 `AlconnaMatcher.send` 或 `UniMessage.send` 发送的消息 (str 或 Message 或 UniMessage) 进行额外处理，默认情况下会返回原始消息。\n\n该方法会调用多次，即对于多个 Extension，前一个 Extension 的返回值会作为下一个 Extension 的输入。\n\n由于需要保证输入与输出的类型一致，该方法内需要自行判断类型。\n\n### before_catch\n\n```python\ndef before_catch(self, name: str, annotation: type, default: Any) -> bool: ...\n```\n\n该方法用于响应函数中某个参数是否需要绑定到该 Extension 上。\n\n### catch\n\n```python\nasync def catch(self, interface: Interface) -> Any: ...\n```\n\n该方法用于注入经过 `before_catch` 确认的参数。其中 `Interface` 的定义为\n\n```python\nclass Interface(Generic[TE]):\n    event: TE\n    state: T_State\n    name: str\n    annotation: Any\n    default: Any\n```\n\n## 补全会话\n\n补全会话基于 [`半自动补全`](./command.md#半自动补全)，用于指令参数缺失或参数错误时给予交互式提示，类似于 `got-reject`：\n\n```python\nfrom nonebot_plugin_alconna import Alconna, Args, Field, At, on_alconna\n\nalc = Alconna(\n    \"添加教师\",\n    Args[\"name\", str, Field(completion=lambda: \"请输入姓名\")],\n    Args[\"phone\", int, Field(completion=lambda: \"请输入手机号\")],\n    Args[\"at\", [str, At], Field(completion=lambda: \"请输入教师号\")],\n)\n\ncmd = on_alconna(alc, comp_config={\"lite\": True}, skip_for_unmatch=False)\n\n@cmd.handle()\nasync def handle(result: Arparma):\n    cmd.finish(\"添加成功\")\n```\n\n此时，当用户输入 `添加教师` 时，会自动提示用户输入姓名，手机号和教师号，用户输入后会自动进入下一个提示：\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"添加教师\" },\n    { position: \"left\", msg: \"以下是建议的输入： \\n- name: 请输入姓名\" },\n    { position: \"right\", msg: \"foo\" },\n    { position: \"left\", msg: \"以下是建议的输入： \\n- phone: 请输入手机号\" },\n    { position: \"right\", msg: \"12345\" },\n    { position: \"left\", msg: \"以下是建议的输入： \\n- at: 请输入教师号\" },\n    { position: \"right\", msg: \"@me\" },\n    { position: \"left\", msg: \"添加成功\" },\n  ]}\n/>\n\n补全会话配置如下：\n\n```python\nclass CompConfig(TypedDict):\n    tab: NotRequired[str]\n    \"\"\"用于切换提示的指令的名称\"\"\"\n    enter: NotRequired[str]\n    \"\"\"用于输入提示的指令的名称\"\"\"\n    exit: NotRequired[str]\n    \"\"\"用于退出会话的指令的名称\"\"\"\n    timeout: NotRequired[int]\n    \"\"\"超时时间\"\"\"\n    hide_tabs: NotRequired[bool]\n    \"\"\"是否隐藏所有提示\"\"\"\n    hides: NotRequired[Set[Literal[\"tab\", \"enter\", \"exit\"]]]\n    \"\"\"隐藏的指令\"\"\"\n    disables: NotRequired[Set[Literal[\"tab\", \"enter\", \"exit\"]]]\n    \"\"\"禁用的指令\"\"\"\n    lite: NotRequired[bool]\n    \"\"\"是否使用简洁版本的补全会话（相当于同时配置 disables、hides、hide_tabs）\"\"\"\n    block: NotRequired[bool]\n    \"\"\"进行补全会话时是否阻塞响应器\"\"\"\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/best-practice/alconna/shortcut.md",
    "content": "---\nsidebar_position: 6\ndescription: 快捷方式\n---\n\n# 快捷方式声明\n\n针对 `Alconna` 编写对于入门开发者来说较为复杂的问题，本插件提供了一些快捷方式来简化开发者的工作。\n\n## 装饰器构造器\n\n本插件提供了一个 `funcommand` 装饰器, 其用于将一个接受任意参数， 返回 `str` 或 `Message` 或 `MessageSegment` 的函数转换为命令响应器：\n\n```python\nfrom nonebot_plugin_alconna import funcommand\n\n\n@funcommand()\nasync def echo(msg: str):\n    return msg\n```\n\n其等同于：\n\n```python\nfrom arclet.alconna import Alconna, Args\nfrom nonebot_plugin_alconna import on_alconna, AlconnaMatch, Match\n\n\necho = on_alconna(Alconna(\"echo\", Args[\"msg\", str]))\n\n@echo.handle()\nasync def echo_exit(msg: Match[str] = AlconnaMatch(\"msg\")):\n    await echo.finish(msg.result)\n\n```\n\n相比于 `on_alconna`， `funcommand` 增加了三个参数 `name`, `prefixes` 和 `description`。\n\n## 类 Koishi 构造器\n\n本插件提供了一个 `Command` 构造器，其基于 `arclet.alconna.tools` 中的 `AlconnaString`， 以类似 `Koishi` 中[注册命令](https://koishi.chat/zh-CN/guide/basic/command.html)的方式来构建一个 **AlconnaMatcher** ：\n\n```python\nfrom nonebot_plugin_alconna import Command, Arparma\n\n\nbook = (\n    Command(\"book\", \"测试\")\n    .option(\"writer\", \"-w <id:int>\")\n    .option(\"writer\", \"--anonymous\", {\"id\": 0})\n    .usage(\"book [-w <id:int> | --anonymous]\")\n    .shortcut(\"测试\", {\"args\": [\"--anonymous\"]})\n    .build()\n)\n\n@book.handle()\nasync def _(arp: Arparma):\n    await book.send(str(arp.options))\n```\n\n甚至，你可以设置 `action` 来设定响应行为：\n\n```python\nbook = (\n    Command(\"book\", \"测试\")\n    .option(\"writer\", \"-w <id:int>\")\n    .option(\"writer\", \"--anonymous\", {\"id\": 0})\n    .usage(\"book [-w <id:int> | --anonymous]\")\n    .shortcut(\"测试\", {\"args\": [\"--anonymous\"]})\n    .action(lambda options: str(options))  # 会自动通过 bot.send 发送\n    .build()\n)\n```\n\n### 参数类型\n\n`Command` 的参数类型也如 `koishi` 一样，**必选参数** 用尖括号包裹，**可选参数** 用方括号包裹:\n\n- `foo` 表示参数 `foo`, 类型为 Any\n- `foo:int` 表示参数 `foo`, 类型为 int\n- `foo:int=1` 表示参数 `foo`, 类型为 int, 默认值为 1\n- `...foo` 表示[泛匹配参数](command.md#allparam)\n- `foo:str+`, `foo:str*` 表示[变长参数](command.md#multivar-与-keywordvar) `foo`, 类型为 str\n- `foo:+str`, `foo:text` 表示参数 `foo`, 类型为 str, 并且将包含空格 (即将变长参数的结果用空格合并)\n\n特别的，针对类型部分，本插件拓展了如下内容:\n\n- `foo:At`, `foo:Image`, ... 表示类型为[通用消息段](./uniseg/segment.md)\n- `foo:select(Image).first` 表示获取子元素类型\n- `foo:Dot(Image, 'url')` 表示类型为 `Image`，并且只获取 `url` 属性\n\n### 从文件加载\n\n`Command` 支持读取 `json` 或 `yaml` 文件来加载命令：\n\n```yml title=\"book.yml\"\ncommand: book\nhelp: 测试\noptions:\n  - name: writer\n    opt: \"-w <id:int>\"\n  - name: writer\n    opt: \"--anonymous\"\n    default:\n      id: 1\nusage: book [-w <id:int> | --anonymous]\nshortcuts:\n  - key: 测试\n    args: [\"--anonymous\"]\nactions:\n  - params: [\"options\"]\n    code: |\n      return str(options)\n```\n\n```python title=\"加载\"\nfrom nonebot_plugin_alconna import command_from_yaml\n\nbook = command_from_yaml(\"book.yml\")\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/best-practice/alconna/uniseg/README.md",
    "content": "# 通用消息组件\n\n`uniseg` 模块属于 `nonebot-plugin-alconna` 的子插件。\n\n通用消息组件内容较多，故分为了一个示例以及数个专题。\n\n## 示例\n\n### 导入\n\n一般情况下，你只需要从 `nonebot_plugin_alconna.uniseg` 中导入 `UniMessage` 即可:\n\n```python\nfrom nonebot_plugin_alconna.uniseg import UniMessage\n```\n\n### 构建\n\n你可以通过 `UniMessage` 上的快捷方法来链式构造消息:\n\n```python\nmessage = (\n    UniMessage.text(\"hello world\")\n    .at(\"1234567890\")\n    .image(url=\"https://example.com/image.png\")\n)\n```\n\n也可以通过导入通用消息段来构建消息:\n\n```python\nfrom nonebot_plugin_alconna import Text, At, Image, UniMessage\n\nmessage = UniMessage(\n    [\n        Text(\"hello world\"),\n        At(\"user\", \"1234567890\"),\n        Image(url=\"https://example.com/image.png\"),\n    ]\n)\n```\n\n更深入一点，比如你想要发送一条包含多个按钮的消息，你可以这样做:\n\n```python\nfrom nonebot_plugin_alconna import Button, UniMessage\n\nmessage = (\n    UniMessage.text(\"hello world\")\n    .keyboard(\n        Button(\"link1\", url=\"https://example.com/1\"),\n        Button(\"link2\", url=\"https://example.com/2\"),\n        Button(\"link3\", url=\"https://example.com/3\"),\n        row=3,\n    )\n)\n```\n\n### 发送\n\n你可以通过 `.send` 方法来发送消息:\n\n```python\n@matcher.handle()\nasync def _():\n    message = UniMessage.text(\"hello world\").image(url=\"https://example.com/image.png\")\n    await message.send()\n    # 类似于 `matcher.finish`\n    await message.finish()\n```\n\n你可以通过参数来让消息 @ 发送者:\n\n```python\n@matcher.handle()\nasync def _():\n    message = UniMessage.text(\"hello world\").image(url=\"https://example.com/image.png\")\n    await message.send(at_sender=True)\n```\n\n或者回复消息:\n\n```python\n@matcher.handle()\nasync def _():\n    message = UniMessage.text(\"hello world\").image(url=\"https://example.com/image.png\")\n    await message.send(reply_to=True)\n```\n\n### 撤回，编辑，表态\n\n你可以通过 `message_recall`, `message_edit` 和 `message_reaction` 方法来撤回，编辑和表态消息事件。\n\n```python\nfrom nonebot_plugin_alconna import message_recall, message_edit, message_reaction\n\n@matcher.handle()\nasync def _():\n    await message_edit(UniMessage.text(\"hello world\"))\n    await message_reaction(\"👍\")\n    await message_recall()\n```\n\n你也可以对你自己发送的消息进行撤回，编辑和表态:\n\n```python\n@matcher.handle()\nasync def _():\n    message = UniMessage.text(\"hello world\").image(url=\"https://example.com/image.png\")\n    receipt = await message.send()\n    await receipt.edit(UniMessage.text(\"hello world!\"))\n    await receipt.reaction(\"👍\")\n    await receipt.recall(delay=5)  # 5秒后撤回\n```\n\n### 处理消息\n\n通过依赖注入，你可以在事件处理器中获取通用消息:\n\n```python\nfrom nonebot_plugin_alconna import UniMsg\n\n@matcher.handle()\nasync def _(msg: UniMsg):\n    ...\n```\n\n然后你可以通过 `UniMessage` 的方法来处理消息.\n\n例如，你想知道消息中是否包含图片，你可以这样做:\n\n```python\nans1 = Image in message\nans2 = message.has(Image)\nans3 = message.only(Image)\n```\n\n或者，提取所有的图片：\n\n```python\nimgs_1 = message[Image]\nimgs_2 = message.get(Image)\nimgs_3 = message.include(Image)\nimgs_4 = message.select(Image)\nimgs_5 = message.filter(lambda x: x.type == \"image\")\nimgs_6 = message.tranform({\"image\": True})\n```\n\n而后，如果你想提取出所有的图片链接，你可以这样做:\n\n```python\nurls = imgs.map(lambda x: x.url)\n```\n\n如果你想知道消息是否符合某个前缀，你可以这样做:\n\n```python\n@matcher.handle()\nasync def _(msg: UniMsg):\n    if msg.startswith(\"hello\"):\n        await matcher.finish(\"hello world\")\n    else:\n        await matcher.finish(\"not hello world\")\n```\n\n或者你想接着去除掉前缀:\n\n```python\n@matcher.handle()\nasync def _(msg: UniMsg):\n    if msg.startswith(\"hello\"):\n        msg = msg.removeprefix(\"hello\")\n        await matcher.finish(msg)\n    else:\n        await matcher.finish(\"not hello world\")\n```\n\n### 持久化\n\n假设你在编写一个词库查询插件，你可以通过 `UniMessage.dump` 方法来将消息序列化为 JSON 格式:\n\n```python\nfrom nonebot_plugin_alconna import UniMsg\n\n@matcher.handle()\nasync def _(msg: UniMsg):\n    data: list[dict] = msg.dump()\n    # 你可以将 data 存储到数据库或者 JSON 文件中\n```\n\n而后你可以通过 `UniMessage.load` 方法来将 JSON 格式的消息反序列化为 `UniMessage` 对象:\n\n```python\nfrom nonebot_plugin_alconna import UniMessage\n\n@matcher.handle()\nasync def _():\n    data = [\n        {\"type\": \"text\", \"text\": \"hello world\"},\n        {\"type\": \"image\", \"url\": \"https://example.com/image.png\"},\n    ]\n    message = UniMessage.load(data)\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/best-practice/alconna/uniseg/_category_.json",
    "content": "{\n  \"label\": \"通用消息组件\",\n  \"position\": 5\n}\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/best-practice/alconna/uniseg/message.mdx",
    "content": "---\nsidebar_position: 3\ndescription: 消息序列\n---\n\nimport Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\n\n# 通用消息序列\n\n`uniseg` 提供了一个类似于 `Message` 的 `UniMessage` 类型，其元素为[通用消息段](./segment.md)。\n\n你可以用如下方式获取 `UniMessage`：\n\n<Tabs groupId=\"get_unimsg\">\n<TabItem value=\"depend\" label=\"使用依赖注入\">\n\n通过提供的 `UniversalMessage` 或基于 [`Annotated` 支持](https://github.com/nonebot/nonebot2/pull/1832)的 `UniMsg` 依赖注入器来获取 `UniMessage`。\n\n```python\nfrom nonebot_plugin_alconna.uniseg import UniMsg, At, Text\n\n\nmatcher = on_xxx(...)\n\n@matcher.handle()\nasync def _(msg: UniMsg):\n    text = msg[Text, 0]\n    print(text.text)\n    if msg.has(At):\n        ats = msg.get(At)\n        print(ats)\n    ...\n```\n\n</TabItem>\n<TabItem value=\"method\" label=\"使用 UniMessage.generate\">\n\n注意，`generate` 方法在响应器以外的地方如果不传入 `event` 与 `bot` 则无法处理 reply。\n\n```python\nfrom nonebot import Message, EventMessage\nfrom nonebot_plugin_alconna.uniseg import UniMessage\n\n\nmatcher = on_xxx(...)\n\n@matcher.handle()\nasync def _(message: Message = EventMessage()):\n    msg = await UniMessage.generate(message=message)\n    msg1 = UniMessage.generate_without_reply(message=message)\n```\n\n</TabItem>\n</Tabs>\n\n## 发送消息\n\n你还可以通过 `UniMessage` 的 `export` 与 `send` 方法来**跨平台发送消息**。\n\n`UniMessage.export` 会通过传入的 `bot: Bot` 参数，或上下文中的 `Bot` 对象读取适配器信息，并使用对应的生成方法把通用消息转为适配器对应的消息序列：\n\n```python\nfrom nonebot import Bot, on_command\nfrom nonebot_plugin_alconna.uniseg import Image, UniMessage\n\n\ntest = on_command(\"test\")\n\n@test.handle()\nasync def handle_test():\n    await test.send(await UniMessage(Image(path=\"path/to/img\")).export())\n```\n\n除此之外 `UniMessage.send`, `.finish` 方法基于 `UniMessage.export` 并调用各适配器下的发送消息方法，返回一个 `Receipt` 对象，用于修改/撤回/表态消息：\n\n```python\nfrom nonebot import Bot, on_command\nfrom nonebot_plugin_alconna.uniseg import UniMessage\n\n\ntest = on_command(\"test\")\n\n@test.handle()\nasync def handle():\n    receipt = await UniMessage.text(\"hello!\").send(at_sender=True, reply_to=True)\n    await receipt.recall(delay=1)\n```\n\n`UniMessage.send` 的定义如下：\n\n```python\nasync def send(\n    self,\n    target: Event | Target | None = None,\n    bot: Bot | None = None,\n    fallback: bool | FallbackStrategy = FallbackStrategy.rollback,\n    at_sender: str | bool = False,\n    reply_to: str | bool | Reply | None = False,\n    **kwargs: Any,\n) -> Receipt:\n    ...\n```\n\n- `target`: 发送目标，支持事件和[发送对象](./utils.mdx#发送对象)，不传入时会尝试从响应器上下文中获取。\n- `bot`: 发送消息使用的 Bot 对象，若不传入则会尝试从响应器上下文中获取。\n- `fallback`: [回退策略](#回退策略)。\n- `at_sender`: 是否提醒发送者，默认为 `False`。当类型为 `str` 时，表示指定用户的 id。\n- `reply_to`: 是否回复消息，默认为 `False`。\n  - `str` 表示消息 id。\n  - `bool` 表示是否回复当前消息。此时 `target` 不能是[发送对象](./utils.mdx#发送对象)。\n  - `Reply` 表示直接使用回复元素。\n- `**kwargs`: 各 `Bot.send` 的特定参数。\n\n而在 `AlconnaMatcher` 下，`got`, `send`, `reject` 等可以发送消息的方法皆支持使用 `UniMessage`，不需要手动调用 export 方法：\n\n```python\nfrom arclet.alconna import Alconna, Args\nfrom nonebot_plugin_alconna import Match, AlconnaMatcher, on_alconna\nfrom nonebot_plugin_alconna.uniseg import At,  UniMessage\n\n\ntest_cmd = on_alconna(Alconna(\"test\", Args[\"target?\", At]))\n\n@test_cmd.handle()\nasync def tt_h(matcher: AlconnaMatcher, target: Match[At]):\n    if target.available:\n        matcher.set_path_arg(\"target\", target.result)\n\n@test_cmd.got_path(\"target\", prompt=\"请输入目标\")\nasync def tt(target: At):\n    await test_cmd.send(UniMessage([target, \"\\ndone.\"]))\n```\n\n### 回退策略\n\n`send` 方法的 `fallback` 参数用于指定回退策略（即当前适配器不支持的消息段如何处理）：\n\n- `FallbackStrategy.ignore`: 忽略未转换的消息段\n- `FallbackStrategy.to_text`: 将未转换的消息段转为文本元素\n- `FallbackStrategy.rollback`: 从未转换消息段的子元素中提取可能的可发送消息段\n- `FallbackStrategy.forbid`: 抛出异常\n- `FallbackStrategy.auto`: 插件自动选择策略\n\n另外 `fallback` 传入 `bool` 时，`True` 等价于 `FallbackStrategy.auto`，`False` 等价于 `FallbackStrategy.forbid`。\n\n### 主动发送消息\n\n`UniMessage.send` 也可以用于主动发送消息：\n\n```python\nfrom nonebot_plugin_alconna.uniseg import UniMessage, Target, SupportScope\nfrom nonebot import get_driver\n\n\ndriver = get_driver()\n\n@driver.on_startup\nasync def on_startup():\n    target = Target(\"xxxx\", scope=SupportScope.qq_client)\n    await UniMessage(\"Hello!\").send(target=target)\n```\n\n:::caution\n\n在响应器以外的地方，除非启用了 `alconna_apply_fetch_targets` 配置项，否则 `bot` 参数必须手动传入。\n\n:::\n\n### Receipt 对象\n\n`send` 方法返回的 `Receipt` 对象可以用于修改/撤回/表态消息：\n\n```python\nasync def handle():\n    receipt = await UniMessage.text(\"hello!\").send(at_sender=True, reply_to=True)\n    await receipt.recall(delay=1)\n    recept1 = await UniMessage.text(\"hello!\").send(at_sender=True, reply_to=True)\n    await recept1.edit(\"world!\")\n```\n\n`Receipt` 对象拥有以下方法：\n\n- `recallable`: 表明是否可以撤回\n- `recall`: 撤回消息\n- `editable`: 表明是否可以修改\n- `edit`: 修改消息\n- `reactionable`: 表明是否可以表态\n- `reaction`: 表态消息\n- `get_reply`: 生成对已经发送的消息的回复元素\n- `send`, `finish`: 发送消息\n- `reply`: 回复已经发送的消息\n\n## 构造\n\n如同 `Message`, `UniMessage` 可以传入单个字符串/消息段，或可迭代的字符串/消息段：\n\n```python\nfrom nonebot_plugin_alconna.uniseg import UniMessage, At\n\n\nmsg = UniMessage(\"Hello\")\nmsg1 = UniMessage(At(\"user\", \"124\"))\nmsg2 = UniMessage([\"Hello\", At(\"user\", \"124\")])\n```\n\n`UniMessage` 上同时存在便捷方法，令其可以链式地添加消息段：\n\n```python\nfrom nonebot_plugin_alconna.uniseg import UniMessage, At, Image\n\n\nmsg = UniMessage.text(\"Hello\").at(\"124\").image(path=\"/path/to/img\")\nassert msg == UniMessage(\n    [\"Hello\", At(\"user\", \"124\"), Image(path=\"/path/to/img\")]\n)\n```\n\n### 使用消息模板\n\n`UniMessage.template` 同样类似于 `Message.template`，可以用于格式化消息，大体用法参考 [消息模板](../../../tutorial/message#使用消息模板)。\n\n这里额外说明 `UniMessage.template` 的拓展控制符\n\n相比 `Message`，UniMessage 对于 `{:XXX}` 做了另一类拓展。其能够识别例如 At(xxx, yyy) 或 Emoji(aaa, bbb)的字符串并执行\n\n以 At(...) 为例：\n\n```python title=使用通用消息段的拓展控制符\n>>> from nonebot_plugin_alconna.uniseg import UniMessage\n>>>  UniMessage.template(\"{:At(user, target)}\").format(target=\"123\")\nUniMessage(At(\"user\", \"123\"))\n>>> UniMessage.template(\"{:At(type=user, target=id)}\").format(id=\"123\")\nUniMessage(At(\"user\", \"123\"))\n>>> UniMessage.template(\"{:At(type=user, target=123)}\").format()\nUniMessage(At(\"user\", \"123\"))\n```\n\n而在 `AlconnaMatcher` 中，`{:XXX}` 更进一步地提供了获取 `event` 和 `bot` 中的属性的功能：\n\n```python title=在AlconnaMatcher中使用通用消息段的拓展控制符\nfrom arclet.alconna import Alconna, Args\nfrom nonebot_plugin_alconna import At, Match, UniMessage, AlconnaMatcher, on_alconna\n\n\ntest_cmd = on_alconna(Alconna(\"test\", Args[\"target?\", At]))\n\n@test_cmd.handle()\nasync def tt_h(matcher: AlconnaMatcher, target: Match[At]):\n    if target.available:\n        matcher.set_path_arg(\"target\", target.result)\n\n@test_cmd.got_path(\n    \"target\",\n    prompt=UniMessage.template(\"{:At(user, $event.get_user_id())} 请确认目标\")\n)\nasync def tt():\n    await test_cmd.send(\n      UniMessage.template(\"{:At(user, $event.get_user_id())} 已确认目标为 {target}\")\n    )\n```\n\n另外也有 `$message_id` 与 `$target` 两个特殊值。\n\n:::tip\n\n注意到上述代码中的 `{target}` 了吗？\n\n在 `AlconnaMatcher` 中，`UniMessage.template` 的格式化方法会自动将 `Arparma.all_matched_args`、 `state` 中的变量传入到 `format` 方法中，因此你可以直接使用上述变量。\n\n:::\n\n### 拼接消息\n\n`str`、`UniMessage`、`Segment` 对象之间可以直接相加，相加均会返回一个新的 `UniMessage` 对象：\n\n```python\n# 消息序列与消息段相加\nUniMessage(\"text\") + Text(\"text\")\n# 消息序列与字符串相加\nUniMessage([Text(\"text\")]) + \"text\"\n# 消息序列与消息序列相加\nUniMessage(\"text\") + UniMessage([Text(\"text\")])\n# 字符串与消息序列相加\n\"text\" + UniMessage([Text(\"text\")])\n# 消息段与消息段相加\nText(\"text\") + Text(\"text\")\n# 消息段与字符串相加\nText(\"text\") + \"text\"\n# 消息段与消息序列相加\nText(\"text\") + UniMessage([Text(\"text\")])\n# 字符串与消息段相加\n\"text\" + Text(\"text\")\n```\n\n如果需要在当前消息序列后直接拼接新的消息段，可以使用 `Message.append`、`Message.extend` 方法，或者使用自加：\n\n```python\nmsg = UniMessage([Text(\"text\")])\n# 自加\nmsg += \"text\"\nmsg += Text(\"text\")\nmsg += UniMessage([Text(\"text\")])\n# 附加\nmsg.append(Text(\"text\"))\n# 扩展\nmsg.extend([Text(\"text\")])\n```\n\n## 操作\n\n### 检查消息段\n\n我们可以通过 `in` 运算符或消息序列的 `has` 方法来：\n\n```python\n# 是否存在消息段\nAt(\"user\", \"1234\") in message\n# 是否存在指定类型的消息段\nAt in message\n```\n\n我们还可以使用 `only` 方法来检查消息中是否仅包含指定的消息段：\n\n```python\n# 是否都为 \"test\"\nmessage.only(\"test\")\n# 是否仅包含指定类型的消息段\nmessage.only(Text)\n```\n\n### 获取消息纯文本\n\n类似于 `Message.extract_plain_text()`，用于获取通用消息的纯文本：\n\n```python\n# 提取消息纯文本字符串\nassert UniMessage(\n    [At(\"user\", \"1234\"), \"text\"]\n).extract_plain_text() == \"text\"\n```\n\n### 遍历\n\n通用消息序列继承自 `List[Segment]` ，因此可以使用 `for` 循环遍历消息段：\n\n```python\nfor segment in message:  # type: Segment\n    ...\n```\n\n### 过滤、索引与切片\n\n消息序列对列表的索引与切片进行了增强，在原有列表 `int` 索引与 `slice` 切片的基础上，支持 `type` 过滤索引与切片：\n\n```python\nmessage = UniMessage(\n    [\n        Reply(...),\n        \"text1\",\n        At(\"user\", \"1234\"),\n        \"text2\"\n    ]\n)\n# 索引\nmessage[0] == Reply(...)\n# 切片\nmessage[0:2] == UniMessage([Reply(...), Text(\"text1\")])\n# 类型过滤\nmessage[At] == Message([At(\"user\", \"1234\")])\n# 类型索引\nmessage[At, 0] == At(\"user\", \"1234\")\n# 类型切片\nmessage[Text, 0:2] == UniMessage([Text(\"text1\"), Text(\"text2\")])\n```\n\n我们也可以通过消息序列的 `include`、`exclude` 方法进行类型过滤：\n\n```python\nmessage.include(Text, At)\nmessage.exclude(Reply)\n```\n\n或者使用 `filter` 方法：\n\n```python\nmessage.filter(lambda x: isinstance(x, At) and x.flag == \"user\")  # 仅保留 At(\"user\", xxx) 的消息段\n```\n\n同样的，消息序列对列表的 `index`、`count` 方法也进行了增强，可以用于索引指定类型的消息段：\n\n```python\n# 指定类型首个消息段索引\nmessage.index(Text) == 1\n# 指定类型消息段数量\nmessage.count(Text) == 2\n```\n\n此外，消息序列添加了一个 `get` 方法，可以用于获取指定类型指定个数的消息段：\n\n```python\n# 获取指定类型指定个数的消息段\nmessage.get(Text, 1) == UniMessage([Text(\"test1\")])\n```\n\n### 嵌套提取\n\n消息序列的 `select` 方法可以递归地从消息中选择指定类型的消息段：\n\n```python\nmessage = UniMessage(\n    [\n        Text(\"text1\"),\n        Image(url=\"url1\")(\n            Text(\"text2\"),\n        )\n    ]\n)\nassert message.select(Text) == UniMessage(\n    [\n        Text(\"text1\"),\n        Text(\"text2\")\n    ]\n)\n```\n\n### 转换\n\n消息序列的 `map` 方法可以简单地将消息段转换为指定类型的数据：\n\n```python\n# 转换消息段为另一类型的消息段，此时返回结果仍是 UniMessage\nmessage.map(lambda x: Text(x.target))  # 转换为 Text 消息段\n# 转换消息段为另一类型的数据，此时返回结果为 list[T]\nmessage.map(lambda x: x.target)  # 转换为 list[str]\n```\n\n在此之上，消息序列还提供了 `transform` 和 `transform_async` 方法，允许你传入转换规则，将消息段转换为另一类型的消息段，并返回一个新的消息序列：\n\n```python\nrule = {\n    \"text\": True,\n    \"at\": lambda attrs, children: Text(attrs[\"target\"])\n}\nmessage.transform(rule)\n```\n\n转换规则的类型一般为 `dict[str, Transformer]`，以消息元素类型的名称为键，定义方式如下：\n\n```typescript\ntype Fragment = Segment | Segment[];\ntype Render<T> = (attrs: dict, children: Segment[]) => T;\ntype Transformer = boolean | Fragment | Render<boolean | Fragment>;\n```\n\n### 字符串操作\n\n类似于 `str`，消息序列可以通过如下方法来操作消息内的文本部分：\n\n- `split`,\n- `replace`,\n- `startwith`, `endswith`,\n- `removeprefix`, `removesuffix`,\n- `strip`, `lstrip`, `rstrip`,\n\n```python\nmsg = UniMessage.text(\"foo bar\").at(\"1234\").text(\"baz qux\")\n# 分割，返回分割结果，类型为 list[UniMessage]\nparts = msg.split(\" \")\n# 替换，返回替换结果，类型为 UniMessage。新文本可以用 str 或 Text 来替换\nnew_msg = msg.replace(\"ba\", \"baaa\")\n# 前缀/后缀检查\nmsg.startswith(\"foo\")  # True\nmsg.endswith(\"qux\")  # True\n# 去除前缀/后缀\nmsg1 = msg.removeprefix(\"foo\")  # UniMessage([Text(\" bar\"), At(\"user\", \"1234\"), Text(\"baz qux\")])\nmsg2 = msg.removesuffix(\"qux\")  # UniMessage([Text(\"foo bar\"), At(\"user\", \"1234\"), Text(\"baz \")])\n# 去除空格\nmsg1 = msg1.lstrip()  # UniMessage([Text(\"bar\"), At(\"user\", \"1234\"), Text(\"baz qux\")])\nmsg2 = msg2.rstrip()  # UniMessage([Text(\"foo bar\"), At(\"user\", \"1234\"), Text(\"baz\")])\n```\n\n## 持久化\n\n特别的，`UniMessage` 还支持消息持久化，具体来说为 `dump` 与 `load` 方法：\n\n```python\nmsg = UniMessage.text(\"Hello\").image(url=\"url\")\ndata = msg.dump()  # [{\"type\": \"text\", \"text\": \"Hello\"}, {\"type\": \"image\", \"url\": \"url\"}]\n\nassert UniMessage.load(data) == msg\n```\n\n### dump\n\n`dump` 方法的定义如下：\n\n```python\ndef dump(self, media_save_dir: str | Path | bool | None = None, json: bool = False) -> str | list[dict[str, Any]]: ...\n```\n\n其中，`media_save_dir` 用于指定持久化的媒体文件存储目录:\n\n- 若 `media_save_dir` 为 str 或 Path，则会将媒体文件保存到指定目录下。\n- 若 `media_save_dir` 为 False，则不会保存媒体文件。\n- 若 `media_save_dir` 为 True，则会将文件数据转为 base64 编码。\n- 若不指定 `media_save_dir`，则会尝试导入 [`nonebot_plugin_localstore`](../../data-storing.md) 并使用其提供的路径。否则 (即 `localstore` 未安装)，将会尝试使用当前工作目录。\n\n### load\n\n`load` 方法的定义如下：\n\n```python\n@classmethod\ndef load(cls, data: str | list[dict[str, Any]]) -> UniMessage: ...\n```\n\n其中 `data` 应符合 JSON 格式。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/best-practice/alconna/uniseg/segment.md",
    "content": "---\nsidebar_position: 2\ndescription: 消息段\n---\n\n# 通用消息段\n\n通用消息段是对各适配器中的消息段的抽象总结。其可用于 Alconna 命令的参数定义，也可用于消息的构建和解析。\n\n```python\nfrom nonebot_plugin_alconna import Alconna, Args, Image, on_alconna\n\nmeme = on_alconna(Alconna(\"make_meme\", Args[\"name\", str][\"img\", Image]))\n\n@meme.handle()\nasync def _(img: Image):\n    ...\n```\n\n## 模型定义\n\n> **注意**: 本节的内容经过简化。实际情况以源码为准。\n\n```python\nclass Segment:\n    \"\"\"基类标注\"\"\"\n    @property\n    def type(self) -> str: ...\n    @property\n    def data(self) -> [str, Any]: ...\n    @property\n    def children(self) -> list[\"Segment\"]: ...\n\nclass Text(Segment):\n    \"\"\"Text对象, 表示一类文本元素\"\"\"\n    text: str\n    styles: dict[tuple[int, int], list[str]]\n\n    def cover(self, text: str): ...\n    def mark(self, start: Optional[int] = None, end: Optional[int] = None, *styles: str): ...\n\nclass At(Segment):\n    \"\"\"At对象, 表示一类提醒某用户的元素\"\"\"\n    flag: Literal[\"user\", \"role\", \"channel\"]\n    target: str\n    display: Optional[str]\n\nclass AtAll(Segment):\n    \"\"\"AtAll对象, 表示一类提醒所有人的元素\"\"\"\n    here: bool\n\nclass Emoji(Segment):\n    \"\"\"Emoji对象, 表示一类表情元素\"\"\"\n    id: str\n    name: Optional[str]\n\nclass Media(Segment):\n    id: Optional[str]\n    url: Optional[str]\n    path: Optional[Union[str, Path]]\n    raw: Optional[Union[bytes, BytesIO]]\n    mimetype: Optional[str]\n    name: str\n\n    to_url: ClassVar[Optional[MediaToUrl]]\n\nclass Image(Media):\n    \"\"\"Image对象, 表示一类图片元素\"\"\"\n    width: Optional[int]\n    height: Optional[int]\n\nclass Audio(Media):\n    \"\"\"Audio对象, 表示一类音频元素\"\"\"\n    duration: Optional[float]\n\nclass Voice(Media):\n    \"\"\"Voice对象, 表示一类语音元素\"\"\"\n    duration: Optional[float]\n\nclass Video(Media):\n    \"\"\"Video对象, 表示一类视频元素\"\"\"\n    thumbnail: Optional[Image]\n    duration: Optional[float]\n\nclass File(Media):\n    \"\"\"File对象, 表示一类文件元素\"\"\"\n\nclass Reply(Segment):\n    \"\"\"Reply对象，表示一类回复消息\"\"\"\n    id: str\n    \"\"\"此处不一定是消息ID，可能是其他ID，如消息序号等\"\"\"\n    msg: Optional[Union[Message, str]]\n    origin: Optional[Any]\n\nclass Reference(Segment):\n    \"\"\"Reference对象，表示一类引用消息。转发消息 (Forward) 也属于此类\"\"\"\n    id: Optional[str]\n    \"\"\"此处不一定是消息ID，可能是其他ID，如消息序号等\"\"\"\n    children: List[Union[RefNode, CustomNode]]\n\nclass Hyper(Segment):\n    \"\"\"Hyper对象，表示一类超级消息。如卡片消息、ark消息、小程序等\"\"\"\n    format: Literal[\"xml\", \"json\"]\n    raw: Optional[str]\n    content: Optional[Union[dict, list]]\n\nclass Reference(Segment):\n    \"\"\"Reference对象，表示一类引用消息。转发消息 (Forward) 也属于此类\"\"\"\n    id: Optional[str]\n    nodes: Sequence[Union[RefNode, CustomNode]]\n\nclass Button(Segment):\n    \"\"\"Button对象，表示一类按钮消息\"\"\"\n    flag: Literal[\"action\", \"link\", \"input\", \"enter\"]\n    \"\"\"\n    - 点击 action 类型的按钮时会触发一个关于 按钮回调 事件，该事件的 button 资源会包含上述 id\n    - 点击 link 类型的按钮时会打开一个链接或者小程序，该链接的地址为 `url`\n    - 点击 input 类型的按钮时会在用户的输入框中填充 `text`\n    - 点击 enter 类型的按钮时会直接发送 `text`\n    \"\"\"\n    label: Union[str, Text]\n    \"\"\"按钮上的文字\"\"\"\n    clicked_label: Optional[str]\n    \"\"\"点击后按钮上的文字\"\"\"\n    id: Optional[str]\n    url: Optional[str]\n    text: Optional[str]\n    style: Optional[str]\n    \"\"\"\n    仅建议使用下列值：primary, secondary, success, warning, danger, info, link, grey, blue\n\n    此处规定 `grey` 与 `secondary` 等同, `blue` 与 `primary` 等同\n    \"\"\"\n    permission: Union[Literal[\"admin\", \"all\"], list[At]] = \"all\"\n    \"\"\"\n    - admin: 仅管理者可操作\n    - all: 所有人可操作\n    - list[At]: 指定用户/身份组可操作\n    \"\"\"\n\nclass Keyboard(Segment):\n    \"\"\"Keyboard对象，表示一行按钮元素\"\"\"\n    id: Optional[str]\n    \"\"\"此处一般用来表示模板id，特殊情况下可能表示例如 bot_appid 等\"\"\"\n    buttons: Optional[list[Button]]\n    row: Optional[int]\n    \"\"\"当消息中只写有一个 Keyboard 时可根据此参数约定按钮组的列数\"\"\"\n\nclass Other(Segment):\n    \"\"\"其他 Segment\"\"\"\n    origin: MessageSegment\n\nclass I18n(Segment):\n    \"\"\"特殊的 Segment，用于 i18n 消息\"\"\"\n    item_or_scope: Union[LangItem, str]\n    type_: Optional[str] = None\n\n    def tp(self) -> UniMessageTemplate: ...\n```\n\n:::tip\n\n或许你注意到了 `Segment` 上有一个 `children` 属性。\n\n这是因为在 [`Satori`](https://satori.js.org/zh-CN/) 协议的规定下，一类元素可以用其子元素来代表一类兼容性消息\n（例如，qq 的商场表情在某些平台上可以用图片代替）。\n\n为此，本插件提供了 `select` 方法来表达 \"命令中获取子元素\" 的方法：\n\n```python\nfrom nonebot_plugin_alconna import Args, Image, Alconna, select\nfrom nonebot_plugin_alconna.builtins.uniseg.market_face import MarketFace\n\n# 表示这个指令需要的图片会在目标元素下进行搜索，将所有符合 Image 的元素选出来并将第一个作为结果\nalc1 = Alconna(\"make_meme\", Args[\"name\", str][\"img\", select(Image).first])  # 也可以使用 select(Image).nth(0)\n\n# 表示这个指令需要的图片要么直接是 Image 要么是在 MarketFace 元素内的 Image\nalc2 = Alconna(\"make_meme\", Args[\"name\", str][\"img\", [Image, select(Image).from_(MarketFace)]])\n```\n\n也可以参考通用消息的 [`嵌套提取`](./message.mdx#嵌套提取)\n\n:::\n\n## 自定义消息段\n\n`uniseg` 提供了部分方法来允许用户自定义 Segment 的序列化和反序列化：\n\n```python\nfrom dataclasses import dataclass\n\nfrom nonebot.adapters import Bot\nfrom nonebot.adapters import MessageSegment as BaseMessageSegment\nfrom nonebot.adapters.satori import Custom, Message, MessageSegment\n\nfrom nonebot_plugin_alconna.uniseg.builder import MessageBuilder\nfrom nonebot_plugin_alconna.uniseg.exporter import MessageExporter\nfrom nonebot_plugin_alconna.uniseg import Segment, custom_handler, custom_register\n\n\n@dataclass\nclass MarketFace(Segment):\n    tabId: str\n    faceId: str\n    key: str\n\n\n@custom_register(MarketFace, \"chronocat:marketface\")\ndef mfbuild(builder: MessageBuilder, seg: BaseMessageSegment):\n    if not isinstance(seg, Custom):\n        raise ValueError(\"MarketFace can only be built from Satori Message\")\n    return MarketFace(**seg.data)(*builder.generate(seg.children))\n\n\n@custom_handler(MarketFace)\nasync def mfexport(exporter: MessageExporter, seg: MarketFace, bot: Bot, fallback: bool):\n    if exporter.get_message_type() is Message:\n        return MessageSegment(\"chronocat:marketface\", seg.data)(await exporter.export(seg.children, bot, fallback))\n\n```\n\n具体而言，你可以使用 `custom_register` 来增加一个从 MessageSegment 到 Segment 的处理方法；使用 `custom_handler` 来增加一个从 Segment 到 MessageSegment 的处理方法。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/best-practice/alconna/uniseg/utils.mdx",
    "content": "---\nsidebar_position: 4\ndescription: 辅助模型\n---\n\nimport Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\n\n# 辅助功能\n\n`uniseg` 模块同时提供了多种方法以通用消息操作。\n\n:::note\n\n这些方法中与 `event`, `bot` 相关的参数都会尝试从上下文中获取对象。\n\n:::\n\n## 消息事件 ID\n\n消息事件 ID 是用来标识当前消息事件的唯一 ID，通常用于回复/撤回/编辑/表态当前消息。\n\n<Tabs groupId=\"get_msgid\">\n<TabItem value=\"depend\" label=\"使用依赖注入\">\n\n通过提供的 `MessageId` 或 `MsgId` 依赖注入器来获取消息事件 id。\n\n```python\nfrom nonebot_plugin_alconna.uniseg import MsgId\n\n\nmatcher = on_xxx(...)\n\n@matcher.handle()\nasycn def _(msg_id: MsgId):\n    ...\n```\n\n</TabItem>\n<TabItem value=\"method\" label=\"使用获取函数\">\n\n```python\nfrom nonebot import Event, Bot\nfrom nonebot_plugin_alconna.uniseg import get_message_id\n\n\nmatcher = on_xxx(...)\n\n@matcher.handle()\nasycn def _(bot: Bot, event: Event):\n    msg_id: str = get_message_id(event, bot)\n\n```\n\n</TabItem>\n</Tabs>\n\n:::caution\n\n该方法获取的消息事件 ID 不推荐直接用于各适配器的 API 调用中，可能会操作失败。\n\n:::\n\n## 发送对象\n\n消息发送对象是用来描述当前消息事件的可发送对象或者主动发送消息时的目标对象，它包含了以下属性：\n\n```python\nclass Target:\n    id: str\n    \"\"\"目标id；若为群聊则为 group_id 或者 channel_id，若为私聊则为 user_id\"\"\"\n    parent_id: str\n    \"\"\"父级id；若为频道则为 guild_id，其他情况下可能为空字符串（例如 Feishu 下可作为部门 id）\"\"\"\n    channel: bool\n    \"\"\"是否为频道，仅当目标平台符合频道概念时\"\"\"\n    private: bool\n    \"\"\"是否为私聊\"\"\"\n    source: str\n    \"\"\"可能的事件id\"\"\"\n    self_id: str | None\n    \"\"\"机器人id，若为 None 则 Bot 对象会随机选择\"\"\"\n    selector: Callable[[Bot], Awaitable[bool]] | None\n    \"\"\"选择器，用于在多个 Bot 对象中选择特定 Bot\"\"\"\n    extra: dict[str, Any]\n    \"\"\"额外信息，用于适配器扩展\"\"\"\n```\n\n<Tabs groupId=\"get_target\">\n<TabItem value=\"depend\" label=\"使用依赖注入\">\n\n通过提供的 `MessageTarget` 或 `MsgTarget` 依赖注入器来获取消息发送对象。\n\n```python\nfrom nonebot_plugin_alconna.uniseg import MsgTarget\n\n\nmatcher = on_xxx(...)\n\n@matcher.handle()\nasycn def _(target: MsgTarget):\n    ...\n```\n\n</TabItem>\n<TabItem value=\"method\" label=\"使用获取函数\">\n\n```python\nfrom nonebot import Event, Bot\nfrom nonebot_plugin_alconna.uniseg import Target, get_target\n\n\nmatcher = on_xxx(...)\n\n@matcher.handle()\nasycn def _(bot: Bot, event: Event):\n    target: Target = get_target(event, bot)\n\n```\n\n</TabItem>\n</Tabs>\n\n主动构造一个发送对象时，则需要如下参数：\n\n- `id`: 目标ID；若为群聊则为 `group_id` 或者 `channel_id`，若为私聊则为 `user_id`\n- `parent_id`: 父级ID；若为频道则为 `guild_id`，其他情况下可能为空字符串（例如 Feishu 下可作为部门 id）\n- `channel`: 是否为频道，仅当目标平台符合频道概念时\n- `private`: 是否为私聊\n- `source`: 可能的事件ID\n- `self_id`: 机器人id，若为 None 则 Bot 对象会随机选择\n- `selector`: 选择器，用于在多个 Bot 对象中选择特定 Bot\n- `scope`: 平台范围，表示当前发送对象的平台类别\n- `adapter`: 适配器名称，若为 None 则需要明确指定 Bot 对象\n- `platform`: 平台名称，仅当目标适配器存在多个平台时使用\n- `extra`: 额外信息，用于适配器扩展\n\n通过 `Target` 对象，我们可以在 `UniMessage.send` 中指定发送对象：\n\n```python\nfrom nonebot_plugin_alconna.uniseg import UniMessage, MsgTarget, Target, SupportScope\n\n\nmatcher = on_xxx(...)\n\n@matcher.handle()\nasync def _(target: MsgTarget):\n    # 将消息发送给当前事件的发送者\n    await UniMessage(\"Hello!\").send(target=target)\n    # 主动发送消息给群号为 12345 的 QQ 群聊\n    target1 = Target(\"12345\", scope=SupportScope.qq_client)\n    await UniMessage(\"Hello!\").send(target=target1)\n```\n\n### 选择器\n\n一般来说，主动发送消息时，`UniMessage.send` 或 `Target.self_id` 应指定一个 `Bot` 对象。但是这样会加重开发者的负担。\n\n因此，我们提供了选择器来帮助开发者选择一个 `Bot` 对象。当然，这并非说明一定需要传入 `selector` 参数。\n\n事实上，构造 `Target` 对象时，`self_id`, `scope`, `adapter` 和 `platform` 都会参与到 `selector` 的构造中。\n\n:::tip\n\n你其实可以使用 `Target` 来帮你筛选 `Bot` 对象：\n\n```python\nasync def _():\n    target = Target(\"12345\", scope=SupportScope.qq_client)\n    bot = await target.select()\n```\n\n:::\n\n若配置了 [`alconna_apply_fetch_targets`](../config.md#alconna_apply_fetch_targets) 选项，则在启动时会主动拉取一次发送对象列表。即对于\n某一主动构造的 `Target` 对象，插件将其与拉取下来的众多发送对象进行匹配，并选择第一个符合条件的发送对象，以选择对应的 Bot 对象。\n\n## 撤回消息\n\n通过 `message_recall` 方法来撤回消息事件。\n\n```python\nfrom nonebot_plugin_alconna.uniseg import message_recall\n\nmatcher = on_xxx(...)\n\n@matcher.handle()\nasync def _(msg_id: MsgId):\n    await message_recall(msg_id)\n```\n\n`message_recall` 方法的参数如下：\n\n```python\nasync def message_recall(\n    message_id: str | None = None,\n    event: Event | None = None,\n    bot: Bot | None = None,\n    adapter: str | None = None\n): ...\n```\n\n当 `message_id` 为 `None` 时，插件会尝试从 `event` 中获取消息事件 ID。\n\n## 编辑消息\n\n通过 `message_edit` 方法来编辑消息事件。\n\n```python\nfrom nonebot_plugin_alconna.uniseg import UniMessage, message_edit\n\nmatcher = on_xxx(...)\n\n@matcher.handle()\nasync def _():\n    await message_edit(UniMessage.text(\"1234\"))\n```\n\n`message_edit` 方法的参数如下：\n\n```python\nasync def message_edit(\n    msg: UniMessage,\n    message_id: str | None = None,\n    event: Event | None = None,\n    bot: Bot | None = None,\n    adapter: str | None = None,\n): ...\n```\n\n当 `message_id` 为 `None` 时，插件会尝试从 `event` 中获取消息事件 ID。\n\n## 表态消息\n\n:::caution\n\n该方法属于实验性功能。其接口可能会在未来的版本中发生变化。\n\n:::\n\n通过 `message_reaction` 方法来表态消息事件。\n\n```python\nfrom nonebot_plugin_alconna.uniseg import message_reaction\n\nmatcher = on_xxx(...)\n\n@matcher.handle()\nasync def _():\n    await message_reaction(\"👍\")\n```\n\n`message_reaction` 方法的参数如下：\n\n```python\nasync def message_reaction(\n    reaction: str | Emoji,\n    message_id: str | None = None,\n    event: Event | None = None,\n    bot: Bot | None = None,\n    adapter: str | None = None,\n    delete: bool = False,\n): ...\n```\n\n当 `message_id` 为 `None` 时，插件会尝试从 `event` 中获取消息事件 ID。\n\n`delete` 参数表示是否删除**自己的**表态消息，默认为 `False`。\n\n## 响应规则\n\n`uniseg` 模块提供了两个响应规则：\n\n- `at_in`: 是否在消息中 @ 了指定的用户\n- `at_me`: 是否在消息中 @ 了机器人\n\n相较于 NoneBot 内置的 `to_me` 规则，`at_me` 规则只会在消息中 @ 机器人时触发。\n\n```python\nfrom nonebot_plugin_alconna.uniseg import at_me\n\nmatcher = on_xxx(..., rule=at_me())\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/best-practice/data-storing.md",
    "content": "---\nsidebar_position: 1\ndescription: 存储数据文件到本地\n---\n\n# 数据存储\n\n在使用插件的过程中，难免会需要存储一些持久化数据，例如用户的个人信息、群组的信息等。除了使用数据库等第三方存储之外，还可以使用本地文件来自行管理数据。NoneBot 提供了 `nonebot-plugin-localstore` 插件，可用于获取正确的数据存储路径并写入数据。\n\n## 安装插件\n\n在使用前请先安装 `nonebot-plugin-localstore` 插件至项目环境中，可参考[获取商店插件](../tutorial/store.mdx#安装插件)来了解并选择安装插件的方式。如：\n\n在**项目目录**下执行以下命令：\n\n```bash\nnb plugin install nonebot-plugin-localstore\n```\n\n## 使用插件\n\n`nonebot-plugin-localstore` 插件兼容 Windows、Linux 和 macOS 等操作系统，使用时无需关心操作系统的差异。同时插件提供 `nb-cli` 脚本，可以使用 `nb localstore` 命令来检查数据存储路径。\n\n在使用本插件前同样需要使用 `require` 方法进行**加载**并**导入**需要使用的方法，可参考 [跨插件访问](../advanced/requiring.md) 一节进行了解，如：\n\n```python\nfrom nonebot import require\n\nrequire(\"nonebot_plugin_localstore\")\n\nimport nonebot_plugin_localstore as store\n\n# 获取插件缓存目录\ncache_dir = store.get_plugin_cache_dir()\n# 获取插件缓存文件\ncache_file = store.get_plugin_cache_file(\"file_name\")\n# 获取插件数据目录\ndata_dir = store.get_plugin_data_dir()\n# 获取插件数据文件\ndata_file = store.get_plugin_data_file(\"file_name\")\n# 获取插件配置目录\nconfig_dir = store.get_plugin_config_dir()\n# 获取插件配置文件\nconfig_file = store.get_plugin_config_file(\"file_name\")\n```\n\n:::danger 警告\n在 Windows 和 macOS 系统下，插件的数据目录和配置目录是同一个目录，因此在使用时需要注意避免文件名冲突。\n:::\n\n插件提供的方法均返回一个 `pathlib.Path` 路径，可以参考 [pathlib 文档](https://docs.python.org/zh-cn/3/library/pathlib.html)来了解如何使用。常用的方法有：\n\n```python\nfrom pathlib import Path\n\ndata_file = store.get_plugin_data_file(\"file_name\")\n# 写入文件内容\ndata_file.write_text(\"Hello World!\")\n# 读取文件内容\ndata = data_file.read_text()\n```\n\n:::note 提示\n\n对于嵌套插件，子插件的存储目录将位于父插件存储目录下。\n\n:::\n\n## 配置项\n\n### localstore_use_cwd\n\n使用当前工作目录作为数据存储目录，以下数据目录配置项默认值将会对应变更\n\n默认值：`False`\n\n```dotenv\nLOCALSTORE_USE_CWD=true\n```\n\n### localstore_cache_dir\n\n自定义缓存目录\n\n默认值：\n\n当 `localstore_use_cwd` 为 `True` 时，缓存目录为 `<current_working_directory>/cache`，否则：\n\n- macOS: `~/Library/Caches/nonebot2`\n- Unix: `~/.cache/nonebot2` (XDG default)\n- Windows: `C:\\Users\\<username>\\AppData\\Local\\nonebot2\\Cache`\n\n```dotenv\nLOCALSTORE_CACHE_DIR=/tmp/cache\n```\n\n### localstore_data_dir\n\n自定义数据目录\n\n默认值：\n\n当 `localstore_use_cwd` 为 `True` 时，数据目录为 `<current_working_directory>/data`，否则：\n\n- macOS: `~/Library/Application Support/nonebot2`\n- Unix: `~/.local/share/nonebot2` or in $XDG_DATA_HOME, if defined\n- Win XP (not roaming): `C:\\Documents and Settings\\<username>\\Application Data\\nonebot2`\n- Win 7 (not roaming): `C:\\Users\\<username>\\AppData\\Local\\nonebot2`\n\n```dotenv\nLOCALSTORE_DATA_DIR=/tmp/data\n```\n\n### localstore_config_dir\n\n自定义配置目录\n\n默认值：\n\n当 `localstore_use_cwd` 为 `True` 时，配置目录为 `<current_working_directory>/config`，否则：\n\n- macOS: same as user_data_dir\n- Unix: `~/.config/nonebot2`\n- Win XP (roaming): `C:\\Documents and Settings\\<username>\\Local Settings\\Application Data\\nonebot2`\n- Win 7 (roaming): `C:\\Users\\<username>\\AppData\\Roaming\\nonebot2`\n\n```dotenv\nLOCALSTORE_CONFIG_DIR=/tmp/config\n```\n\n### localstore_plugin_cache_dir\n\n自定义插件缓存目录\n\n默认值：`{}`\n\n```dotenv\nLOCALSTORE_PLUGIN_CACHE_DIR='\n{\n  \"plugin_id\": \"/tmp/plugin_cache\"\n}\n'\n```\n\n### localstore_plugin_data_dir\n\n自定义插件数据目录\n\n默认值：`{}`\n\n```dotenv\nLOCALSTORE_PLUGIN_DATA_DIR='\n{\n  \"plugin_id\": \"/tmp/plugin_data\"\n}\n'\n```\n\n### localstore_plugin_config_dir\n\n自定义插件配置目录\n\n默认值：`{}`\n\n```dotenv\nLOCALSTORE_PLUGIN_CONFIG_DIR='\n{\n  \"plugin_id\": \"/tmp/plugin_config\"\n}\n'\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/best-practice/database/README.mdx",
    "content": "import TabItem from \"@theme/TabItem\";\nimport Tabs from \"@theme/Tabs\";\n\n# 数据库\n\n[`nonebot-plugin-orm`](https://github.com/nonebot/plugin-orm) 是 NoneBot 的数据库支持插件。\n本插件基于 [SQLAlchemy](https://www.sqlalchemy.org/) 和 [Alembic](https://alembic.sqlalchemy.org/)，提供了许多与 NoneBot 紧密集成的功能：\n\n- 多 Engine / Connection 支持\n- Session 管理\n- 关系模型管理、依赖注入支持\n- 数据库迁移\n\n## 安装\n\n<Tabs groupId=\"install\">\n<TabItem value=\"cli\" label=\"使用 nb-cli\">\n\n```shell\nnb plugin install nonebot-plugin-orm\n```\n\n</TabItem>\n<TabItem value=\"pip\" label=\"使用 pip\">\n\n```shell\npip install nonebot-plugin-orm\n```\n\n</TabItem>\n\n<TabItem value=\"pdm\" label=\"使用 pdm\">\n\n```shell\npdm add nonebot-plugin-orm\n```\n\n</TabItem>\n</Tabs>\n\n## 数据库驱动和后端\n\n本插件只提供了 ORM 功能，没有数据库后端，也没有直接连接数据库后端的能力。\n所以你需要另行安装数据库驱动和数据库后端，并且配置数据库连接信息。\n\n### SQLite\n\n[SQLite](https://www.sqlite.org/) 是一个轻量级的嵌入式数据库，它的数据以单文件的形式存储在本地，不需要单独的数据库后端。\nSQLite 非常适合用于开发环境和小型应用，但是不适合用于大型应用的生产环境。\n\n虽然不需要另行安装数据库后端，但你仍然需要安装数据库驱动：\n\n<Tabs groupId=\"install\">\n<TabItem value=\"pip\" label=\"使用 pip\">\n\n```shell\npip install \"nonebot-plugin-orm[sqlite]\"\n```\n\n</TabItem>\n\n<TabItem value=\"pdm\" label=\"使用 pdm\">\n\n```shell\npdm add \"nonebot-plugin-orm[sqlite]\"\n```\n\n</TabItem>\n</Tabs>\n\n默认情况下，数据库文件为 `<data path>/nonebot-plugin-orm/db.sqlite3`（数据目录由 [nonebot-plugin-localstore](../data-storing) 提供）。\n或者，你可以通过配置 `SQLALCHEMY_DATABASE_URL` 来指定数据库文件路径：\n\n```shell\nSQLALCHEMY_DATABASE_URL=sqlite+aiosqlite:///file_path\n```\n\n### PostgreSQL\n\n[PostgreSQL](https://www.postgresql.org/) 是世界上最先进的开源关系数据库之一，对各种高级且广泛应用的功能有最好的支持，是中小型应用的首选数据库。\n\n<Tabs groupId=\"install\">\n<TabItem value=\"pip\" label=\"使用 pip\">\n\n```shell\npip install nonebot-plugin-orm[postgresql]\n```\n\n</TabItem>\n\n<TabItem value=\"pdm\" label=\"使用 pdm\">\n\n```shell\npdm add nonebot-plugin-orm[postgresql]\n```\n\n</TabItem>\n</Tabs>\n\n```shell\nSQLALCHEMY_DATABASE_URL=postgresql+psycopg://user:password@host:port/dbname[?key=value&key=value...]\n```\n\n### MySQL / MariaDB\n\n[MySQL](https://www.mysql.com/) 和 [MariaDB](https://mariadb.com/) 是经典的开源关系数据库，适合用于中小型应用。\n\n<Tabs groupId=\"install\">\n<TabItem value=\"pip\" label=\"使用 pip\">\n\n```shell\npip install nonebot-plugin-orm[mysql]\n```\n\n</TabItem>\n\n<TabItem value=\"pdm\" label=\"使用 pdm\">\n\n```shell\npdm add nonebot-plugin-orm[mysql]\n```\n\n</TabItem>\n</Tabs>\n\n```shell\nSQLALCHEMY_DATABASE_URL=mysql+aiomysql://user:password@host:port/dbname[?key=value&key=value...]\n```\n\n## 使用\n\n本插件提供了数据库迁移功能（此功能依赖于 [nb-cli 脚手架](../../quick-start#安装脚手架)）。\n在安装了新的插件或机器人之后，你需要执行一次数据库迁移操作，将数据库同步至与机器人一致的状态：\n\n```shell\nnb orm upgrade\n```\n\n运行完毕后，可以检查一下：\n\n```shell\nnb orm check\n```\n\n如果输出是 `没有检测到新的升级操作`，那么恭喜你，数据库已经迁移完成了，你可以启动机器人了。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/best-practice/database/_category_.json",
    "content": "{\n  \"label\": \"数据库\",\n  \"position\": 7\n}\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/best-practice/database/developer/README.md",
    "content": "# 开发者指南\n\n开发者指南内容较多，故分为了一个示例以及数个专题。\n阅读（并且最好跟随实践）示例后，你将会对使用 `nonebot-plugin-orm` 开发插件有一个基本的认识。\n如果想要更深入地学习关于 [SQLAlchemy](https://www.sqlalchemy.org/) 和 [Alembic](https://alembic.sqlalchemy.org/) 的知识，或者在使用过程中遇到了问题，可以查阅专题以及其官方文档。\n\n## 示例\n\n### 模型定义\n\n首先，我们需要设计存储的数据的结构。\n例如天气插件，需要存储**什么地方 (`location`)** 的**天气是什么 (`weather`)**。\n其中，一个地方只会有一种天气，而不同地方可能有相同的天气。\n所以，我们可以设计出如下的模型：\n\n```python title=weather/__init__.py showLineNumbers\nfrom nonebot_plugin_orm import Model\nfrom sqlalchemy.orm import Mapped, mapped_column\n\n\nclass Weather(Model):\n    location: Mapped[str] = mapped_column(primary_key=True)\n    weather: Mapped[str]\n```\n\n其中，`primary_key=True` 意味着此列 (`location`) 是主键，即内容是唯一的且非空的。\n每一个模型必须有至少一个主键。\n\n我们可以用以下代码检查模型生成的数据库模式是否正确：\n\n```python\nfrom sqlalchemy.schema import CreateTable\n\nprint(CreateTable(Weather.__table__))\n```\n\n```sql\nCREATE TABLE weather_weather (\n        location VARCHAR NOT NULL,\n        weather VARCHAR NOT NULL,\n        CONSTRAINT pk_weather_weather PRIMARY KEY (location)\n)\n```\n\n可以注意到表名是 `weather_weather` 而不是 `Weather` 或者 `weather`。\n这是因为 `nonebot-plugin-orm` 会自动为模型生成一个表名，规则是：`<插件模块名>_<类名小写>`。\n\n你也可以通过指定 `__tablename__` 属性来自定义表名：\n\n```python {2}\nclass Weather(Model):\n    __tablename__ = \"weather\"\n    ...\n```\n\n```sql {1}\nCREATE TABLE weather (\n    ...\n)\n```\n\n但是，并不推荐你这么做，因为这可能会导致不同插件间的表名重复，引发冲突。\n特别是当你会发布插件时，你并不知道其他插件会不会使用相同的表名。\n\n### 首次迁移\n\n我们成功定义了模型，现在启动机器人试试吧：\n\n```shell\n$ nb run\n01-02 15:04:05 [SUCCESS] nonebot | NoneBot is initializing...\n01-02 15:04:05 [ERROR] nonebot_plugin_orm | 启动检查失败\n01-02 15:04:05 [ERROR] nonebot | Application startup failed. Exiting.\nTraceback (most recent call last):\n  ...\nclick.exceptions.UsageError: 检测到新的升级操作:\n[('add_table',\n  Table('weather', MetaData(), Column('location', String(), table=<weather>, primary_key=True, nullable=False), Column('weather', String(), table=<weather>, nullable=False), schema=None))]\n```\n\n咦，发生了什么？\n`nonebot-plugin-orm` 试图阻止我们启动机器人。\n原来是我们定义了模型，但是数据库中并没有对应的表，这会导致插件不能正常运行。\n所以，我们需要迁移数据库。\n\n首先，我们需要创建一个迁移脚本：\n\n```shell\nnb orm revision -m \"first revision\" --branch-label weather\n```\n\n其中，`-m` 参数是迁移脚本的描述，`--branch-label` 参数是迁移脚本的分支，一般为插件模块名。\n执行命令过后，出现了一个 `weather/migrations` 目录，其中有一个 `xxxxxxxxxxxx_first_revision.py` 文件：\n\n```shell {4,5}\nweather\n├── __init__.py\n├── config.py\n└── migrations\n    └── xxxxxxxxxxxx_first_revision.py\n```\n\n这就是我们创建的迁移脚本，它记录了数据库模式的变化。\n我们可以查看一下它的内容：\n\n```python title=weather/migrations/xxxxxxxxxxxx_first_revision.py {25-33,39-41} showLineNumbers\n\"\"\"first revision\n\n迁移 ID: xxxxxxxxxxxx\n父迁移:\n创建时间: 2006-01-02 15:04:05.999999\n\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom collections.abc import Sequence\n\nimport sqlalchemy as sa\nfrom alembic import op\n\nrevision: str = \"xxxxxxxxxxxx\"\ndown_revision: str | Sequence[str] | None = None\nbranch_labels: str | Sequence[str] | None = (\"weather\",)\ndepends_on: str | Sequence[str] | None = None\n\n\ndef upgrade(name: str = \"\") -> None:\n    if name:\n        return\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.create_table(\n        \"weather_weather\",\n        sa.Column(\"location\", sa.String(), nullable=False),\n        sa.Column(\"weather\", sa.String(), nullable=False),\n        sa.PrimaryKeyConstraint(\"location\", name=op.f(\"pk_weather_weather\")),\n        info={\"bind_key\": \"weather\"},\n    )\n    # ### end Alembic commands ###\n\n\ndef downgrade(name: str = \"\") -> None:\n    if name:\n        return\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_table(\"weather_weather\")\n    # ### end Alembic commands ###\n```\n\n可以注意到脚本的主体部分（其余是模版代码，请勿修改）是：\n\n```python\n# ### commands auto generated by Alembic - please adjust! ###\nop.create_table(  # CREATE TABLE\n    \"weather_weather\",  # weather_weather\n    sa.Column(\"location\", sa.String(), nullable=False),  # location VARCHAR NOT NULL,\n    sa.Column(\"weather\", sa.String(), nullable=False),  # weather VARCHAR NOT NULL,\n    sa.PrimaryKeyConstraint(\"location\", name=op.f(\"pk_weather_weather\")),  # CONSTRAINT pk_weather_weather PRIMARY KEY (location)\n    info={\"bind_key\": \"weather\"},\n)\n# ### end Alembic commands ###\n```\n\n```python\n# ### commands auto generated by Alembic - please adjust! ###\nop.drop_table(\"weather_weather\")  # DROP TABLE weather_weather;\n# ### end Alembic commands ###\n```\n\n虽然我们不是很懂这些代码的意思，但是可以注意到它们几乎与 SQL 语句 (DDL) 一一对应。\n显然，它们是用来创建和删除表的。\n\n我们还可以注意到，`upgrade()` 和 `downgrade()` 函数中的代码是**互逆**的。\n也就是说，执行一次 `upgrade()` 函数，再执行一次 `downgrade()` 函数后，数据库的模式就会回到原来的状态。\n\n这就是迁移脚本的作用：记录数据库模式的变化，以便我们在不同的环境中（例如开发环境和生产环境）**可复现地**、**可逆地**同步数据库模式，正如 git 对我们的代码做的事情那样。\n\n对了，不要忘记还有一段注释：`commands auto generated by Alembic - please adjust!`。\n它在提醒我们，这些代码是由 Alembic 自动生成的，我们应该检查它们，并且根据需要进行调整。\n\n:::caution 注意\n迁移脚本冗长且繁琐，我们一般不会手写它们，而是由 Alembic 自动生成。\n一般情况下，Alembic 足够智能，可以正确地生成迁移脚本。\n但是，在复杂或有歧义的情况下，我们可能需要手动调整迁移脚本。\n所以，**永远要检查迁移脚本，并且在开发环境中测试！**\n\n**迁移脚本中任何一处错误都足以使数据付之东流！**\n:::\n\n确定迁移脚本正确后，我们就可以执行迁移脚本，将数据库模式同步到数据库中：\n\n```shell\nnb orm upgrade\n```\n\n现在，我们可以正常启动机器人了。\n\n开发过程中，我们可能会频繁地修改模型，这意味着我们需要频繁地创建并执行迁移脚本，非常繁琐。\n实际上，此时我们不在乎数据安全，只需要数据库模式与模型定义一致即可。\n所以，我们可以关闭 `nonebot-plugin-orm` 的启动检查：\n\n```shell title=.env.dev\nALEMBIC_STARTUP_CHECK=false\n```\n\n现在，每次启动机器人时，数据库模式会自动与模型定义同步，无需手动迁移。\n\n### 会话管理\n\n我们已经成功定义了模型，并且迁移了数据库，现在可以开始使用数据库了……吗？\n并不能，因为模型只是数据结构的定义，并不能通过它操作数据（如果你曾经使用过 [Tortoise ORM](https://tortoise.github.io/)，可能会知道 `await Weather.get(location=\"上海\")` 这样的面向对象编程。\n但是 SQLAlchemy 不同，选择了命令式编程）。\n我们需要使用**会话**操作数据：\n\n```python title=weather/__init__.py {10,13} showLineNumbers\nfrom nonebot import on_command\nfrom nonebot.adapters import Message\nfrom nonebot.params import CommandArg\nfrom nonebot_plugin_orm import async_scoped_session\n\nweather = on_command(\"天气\")\n\n\n@weather.handle()\nasync def _(session: async_scoped_session, args: Message = CommandArg()):\n    location = args.extract_plain_text()\n\n    if wea := await session.get(Weather, location):\n        await weather.finish(f\"今天{location}的天气是{wea.weather}\")\n\n    await weather.finish(f\"未查询到{location}的天气\")\n```\n\n我们通过 `session: async_scoped_session` 依赖注入获得了一个会话，然后使用 `await session.get(Weather, location)` 查询数据库。\n`async_scoped_session` 是一个有作用域限制的会话，作用域为当前事件、当前事件响应器。\n会话产生的模型实例（例如此处的 `wea := await session.get(Weather, location)`）作用域与会话相同。\n\n:::caution 注意\n此处提到的“会话”指的是 ORM 会话，而非 [NoneBot 会话](../../../appendices/session-control)，两者的生命周期也是不同的（NoneBot 会话的生命周期中可能包含多个事件，不同的事件也会有不同的事件响应器）。\n具体而言，就是不要将 ORM 会话和模型实例存储在 NoneBot 会话状态中：\n\n```python {12}\nfrom nonebot.params import ArgPlainText\nfrom nonebot.typing import T_State\n\n\n@weather.got(\"location\", prompt=\"请输入地名\")\nasync def _(state: T_State, session: async_scoped_session, location: str = ArgPlainText()):\n    wea = await session.get(Weather, location)\n\n    if not wea:\n        await weather.finish(f\"未查询到{location}的天气\")\n\n    state[\"weather\"] = wea  # 不要这么做，除非你知道自己在做什么\n```\n\n当然非要这么做也不是不可以：\n\n```python {6}\n@weather.handle()\nasync def _(state: T_State, session: async_scoped_session):\n    # 通过 await session.merge(state[\"weather\"]) 获得了此 ORM 会话中的相应模型实例，\n    # 而非直接使用会话状态中的模型实例，\n    # 因为先前的 ORM 会话已经关闭了。\n    wea = await session.merge(state[\"weather\"])\n    await weather.finish(f\"今天{state['location']}的天气是{wea.weather}\")\n```\n\n:::\n\n当有数据更改时，我们需要提交事务，也要注意会话作用域问题：\n\n```python title=weather/__init__.py {12,20} showLineNumbers\nfrom nonebot.params import Depends\n\n\nasync def get_weather(\n    session: async_scoped_session, args: Message = CommandArg()\n) -> Weather:\n    location = args.extract_plain_text()\n\n    if not (wea := await session.get(Weather, location)):\n        wea = Weather(location=location, weather=\"未知\")\n        session.add(wea)\n        # await session.commit()  # 不应该在其他地方提交事务\n\n    return wea\n\n\n@weather.handle()\nasync def _(session: async_scoped_session, wea: Weather = Depends(get_weather)):\n    await weather.send(f\"今天的天气是{wea.weather}\")\n    await session.commit()  # 而应该在事件响应器结束前提交事务\n```\n\n当然我们也可以获得一个新的会话，不过此时就要手动管理会话了：\n\n```python title=weather/__init__.py {5-6} showLineNumbers\nfrom nonebot_plugin_orm import get_session\n\n\nasync def get_weather(location: str) -> str:\n    session = get_session()\n    async with session.begin():\n        wea = await session.get(Weather, location)\n\n        if not wea:\n            wea = Weather(location=location, weather=\"未知\")\n            session.add(wea)\n\n        return wea.weather\n\n\n@weather.handle()\nasync def _(args: Message = CommandArg()):\n    wea = await get_weather(args.extract_plain_text())\n    await weather.send(f\"今天的天气是{wea}\")\n```\n\n### 依赖注入\n\n在上面的示例中，我们都是通过会话获得数据的。\n不过，我们也可以通过依赖注入获得数据：\n\n```python title=weather/__init__.py {12-14} showLineNumbers\nfrom sqlalchemy import select\nfrom nonebot.params import Depends\nfrom nonebot_plugin_orm import SQLDepends\n\n\ndef extract_arg_plain_text(args: Message = CommandArg()) -> str:\n    return args.extract_plain_text()\n\n\n@weather.handle()\nasync def _(\n    wea: Weather = SQLDepends(\n        select(Weather).where(Weather.location == Depends(extract_arg_plain_text))\n    ),\n):\n    await weather.send(f\"今天的天气是{wea.weather}\")\n```\n\n其中，`SQLDepends` 是一个特殊的依赖注入，它会根据类型标注和 SQL 语句提供数据，SQL 语句中也可以有子依赖。\n\n不同的类型标注也会获得不同形式的数据：\n\n```python title=weather/__init__.py {5} showLineNumbers\nfrom collections.abc import Sequence\n\n@weather.handle()\nasync def _(\n    weas: Sequence[Weather] = SQLDepends(\n        select(Weather).where(Weather.weather == Depends(extract_arg_plain_text))\n    ),\n):\n    await weather.send(f\"今天的天气是{weas[0].weather}的城市有{'，'.join(wea.location for wea in weas)}\")\n```\n\n支持的类型标注请参见 [依赖注入](dependency)。\n\n我们也可以像 [类作为依赖](../../../advanced/dependency#类作为依赖) 那样，在类属性中声明子依赖：\n\n```python title=weather/__init__.py {5-6,10} showLineNumbers\nfrom collections.abc import Sequence\n\nclass Weather(Model):\n    location: Mapped[str] = mapped_column(primary_key=True)\n    weather: Mapped[str] = Depends(extract_arg_plain_text)\n    # weather: Annotated[Mapped[str], Depends(extract_arg_plain_text)]  # Annotated 支持\n\n\n@weather.handle()\nasync def _(weas: Sequence[Weather]):\n    await weather.send(\n        f\"今天的天气是{weas[0].weather}的城市有{'，'.join(wea.location for wea in weas)}\"\n    )\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/best-practice/database/developer/_category_.json",
    "content": "{\n  \"label\": \"开发者指南\",\n  \"position\": 3\n}\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/best-practice/database/developer/dependency.md",
    "content": "---\nsidebar_position: 3\ndescription: 依赖注入\n---\n\n# 依赖注入\n\n`nonebot-plugin-orm` 提供了强大且灵活的依赖注入，可以方便地帮助你获取数据库会话和查询数据。\n\n## 数据库会话\n\n### AsyncSession\n\n新数据库会话，常用于有独立的数据库操作逻辑的插件。\n\n```python {13,26}\nfrom nonebot import on_message\nfrom nonebot.params import Depends\nfrom nonebot_plugin_orm import AsyncSession, Model, async_scoped_session\nfrom sqlalchemy.orm import Mapped, mapped_column\n\nmessage = on_message()\n\n\nclass Message(Model):\n    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)\n\n\nasync def get_message(session: AsyncSession) -> Message:\n    # 等价于 session = get_session()\n    async with session:\n        msg = Message()\n\n        session.add(msg)\n        await session.commit()\n        await session.refresh(msg)\n\n        return msg\n\n\n@message.handle()\nasync def _(session: async_scoped_session, msg: Message = Depends(get_message)):\n    await session.rollback()  # 无法回退 get_message() 中的更改\n    await message.send(str(msg.id))  # msg 被存储，msg.id 递增\n```\n\n### async_scoped_session\n\n数据库作用域会话，常用于事件响应器和有与响应逻辑相关的数据库操作逻辑的插件。\n\n```python {13，26}\nfrom nonebot import on_message\nfrom nonebot.params import Depends\nfrom nonebot_plugin_orm import Model, async_scoped_session\nfrom sqlalchemy.orm import Mapped, mapped_column\n\nmessage = on_message()\n\n\nclass Message(Model):\n    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)\n\n\nasync def get_message(session: async_scoped_session) -> Message:\n    # 等价于 session = get_scoped_session()\n    msg = Message()\n\n    session.add(msg)\n    await session.flush()\n    await session.refresh(msg)\n\n    return msg\n\n\n@message.handle()\nasync def _(session: async_scoped_session, msg: Message = Depends(get_message)):\n    await session.rollback()  # 可以回退 get_message() 中的更改\n    await message.send(str(msg.id))  # msg 没有被存储，msg.id 不变\n```\n\n## 查询数据\n\n### Model\n\n支持类作为依赖。\n\n```python\nfrom typing import Annotated\n\nfrom nonebot.params import Depends\nfrom nonebot_plugin_orm import Model\nfrom sqlalchemy.orm import Mapped, mapped_column\n\n\ndef get_id() -> int: ...\n\n\nclass Message(Model):\n    id: Annotated[Mapped[int], Depends(get_id)] = mapped_column(\n        primary_key=True, autoincrement=True\n    )\n\n\nasync def _(msg: Message):\n    # 等价于 msg = (\n    #     await (await session.stream(select(Message).where(Message.id == get_id())))\n    #     .scalars()\n    #     .one_or_none()\n    # )\n    ...\n```\n\n### SQLDepends\n\n参数为一个 SQL 语句，决定依赖注入的内容，SQL 语句中可以使用子依赖。\n\n```python {11-13}\nfrom nonebot.params import Depends\nfrom nonebot_plugin_orm import Model, SQLDepends\nfrom sqlalchemy import select\n\n\ndef get_id() -> int: ...\n\n\nasync def _(\n    model: Model = SQLDepends(select(Model).where(Model.id == Depends(get_id))),\n): ...\n```\n\n参数可以是任意 SQL 语句，但不建议使用 `select` 以外的语句，因为语句可能没有返回值（`returning` 除外），而且代码不清晰。\n\n### 类型标注\n\n类型标注决定依赖注入的数据结构，主要影响以下几个层面：\n\n- 迭代器（`session.execute()`）或异步迭代器（`session.stream()`）\n- 标量（`session.execute().scalars()`）或元组（`session.execute()`）\n- 一个（`session.execute().one_or_none()`，注意 `None` 时可能触发 [重载](../../../appendices/overload#重载)）或全部（`session.execute()` / `session.execute().all()`）\n- 连续（`session().execute()`）或分块（`session.execute().partitions()`）\n\n具体如下（可以使用父类型作为类型标注）：\n\n- ```python\n  async def _(rows_partitions: AsyncIterator[Sequence[Tuple[Model, ...]]]):\n      # 等价于 rows_partitions = await (await session.stream(sql).partitions())\n\n      async for partition in rows_partitions:\n          for row in partition:\n              print(row[0], row[1], ...)\n  ```\n\n- ```python\n  async def _(model_partitions: AsyncIterator[Sequence[Model]]):\n      # 等价于 model_partitions = await (await session.stream(sql).scalars().partitions())\n\n      async for partition in model_partitions:\n          for model in partition:\n              print(model)\n  ```\n\n- ```python\n  async def _(row_partitions: Iterator[Sequence[Tuple[Model, ...]]]):\n      # 等价于 row_partitions = await session.execute(sql).partitions()\n\n      for partition in rows_partitions:\n          for row in partition:\n              print(row[0], row[1], ...)\n  ```\n\n- ```python\n  async def _(model_partitions: Iterator[Sequence[Model]]):\n      # 等价于 model_partitions = await (await session.execute(sql).scalars().partitions())\n\n      for partition in model_partitions:\n          for model in partition:\n              print(model)\n  ```\n\n- ```python\n  async def _(rows: sa_async.AsyncResult[Tuple[Model, ...]]):\n      # 等价于 rows = await session.stream(sql)\n\n      async for row in rows:\n          print(row[0], row[1], ...)\n  ```\n\n- ```python\n  async def _(models: sa_async.AsyncScalarResult[Model]):\n      # 等价于 models = await session.stream(sql).scalars()\n\n      async for model in models:\n          print(model)\n  ```\n\n- ```python\n  async def _(rows: sa.Result[Tuple[Model, ...]]):\n      # 等价于 rows = await session.execute(sql)\n\n      for row in rows:\n          print(row[0], row[1], ...)\n  ```\n\n- ```python\n  async def _(models: sa.ScalarResult[Model]):\n      # 等价于 models = await session.execute(sql).scalars()\n\n      for model in models:\n          print(model)\n  ```\n\n- ```python\n  async def _(rows: Sequence[Tuple[Model, ...]]):\n      # 等价于 rows = await (await session.stream(sql).all())\n\n      for row in rows:\n            print(row[0], row[1], ...)\n  ```\n\n- ```python\n  async def _(models: Sequence[Model]):\n      # 等价于 models = await (await session.stream(sql).scalars().all())\n\n      for model in models:\n          print(model)\n  ```\n\n- ```python\n  async def _(row: Tuple[Model, ...]):\n      # 等价于 row = await (await session.stream(sql).one_or_none())\n\n      print(row[0], row[1], ...)\n  ```\n\n- ```python\n  async def _(model: Model):\n      # 等价于 model = await (await session.stream(sql).scalars().one_or_none())\n\n      print(model)\n  ```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/best-practice/database/developer/test.md",
    "content": "---\nsidebar_position: 2\ndescription: 测试\n---\n\n# 测试\n\n百思不如一试，测试是发现问题的最佳方式。\n\n不同的用户会有不同的配置，为了提高项目的兼容性，我们需要在不同数据库后端上测试。\n手动进行大量的、重复的测试不可靠，也不现实，因此我们推荐使用 [GitHub Actions](https://github.com/features/actions) 进行自动化测试：\n\n```yaml title=.github/workflows/test.yml {12-42,52-53} showLineNumbers\nname: Test\n\non:\n  push:\n    branches:\n      - main\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n\n    strategy:\n      matrix:\n        db:\n          - sqlite+aiosqlite:///db.sqlite3\n          - postgresql+psycopg://postgres:postgres@localhost:5432/postgres\n          - mysql+aiomysql://mysql:mysql@localhost:3306/mymysql\n\n      fail-fast: false\n\n    env:\n      SQLALCHEMY_DATABASE_URL: ${{ matrix.db }}\n\n    services:\n      postgresql:\n        image: ${{ startsWith(matrix.db, 'postgresql') && 'postgres' || '' }}\n        env:\n          POSTGRES_USER: postgres\n          POSTGRES_PASSWORD: postgres\n          POSTGRES_DB: postgres\n        ports:\n          - 5432:5432\n\n      mysql:\n        image: ${{ startsWith(matrix.db, 'mysql') && 'mysql' || '' }}\n        env:\n          MYSQL_ROOT_PASSWORD: mysql\n          MYSQL_USER: mysql\n          MYSQL_PASSWORD: mysql\n          MYSQL_DATABASE: mymysql\n        ports:\n          - 3306:3306\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - uses: actions/setup-python@v5\n\n      - name: Install dependencies\n        run: pip install -r requirements.txt\n\n      - name: Run migrations\n        run: pipx run nb-cli orm upgrade\n\n      - name: Run tests\n        run: pytest\n```\n\n如果项目还需要考虑跨平台和跨 Python 版本兼容，测试矩阵中还需要增加这两个维度。\n但是，我们没必要在所有平台和 Python 版本上运行所有数据库的测试，因为很显然，PostgreSQL 和 MySQL 这类独立的数据库后端不会受平台和 Python 影响，而且 Github Actions 的非 Linux 平台不支持运行独立服务：\n\n|             | Python 3.9 | Python 3.10 | Python 3.11 | Python 3.12                 |\n| ----------- | ---------- | ----------- | ----------- | --------------------------- |\n| **Linux**   | SQLite     | SQLite      | SQLite      | SQLite / PostgreSQL / MySQL |\n| **Windows** | SQLite     | SQLite      | SQLite      | SQLite                      |\n| **macOS**   | SQLite     | SQLite      | SQLite      | SQLite                      |\n\n```yaml title=.github/workflows/test.yml {12-24} showLineNumbers\nname: Test\n\non:\n  push:\n    branches:\n      - main\n\njobs:\n  test:\n    runs-on: ${{ matrix.os }}\n\n    strategy:\n      matrix:\n        os: [ubuntu-latest, windows-latest, macos-latest]\n        python-version: [\"3.9\", \"3.10\", \"3.11\", \"3.12\"]\n        db: [\"sqlite+aiosqlite:///db.sqlite3\"]\n\n        include:\n          - os: ubuntu-latest\n            python-version: \"3.12\"\n            db: postgresql+psycopg://postgres:postgres@localhost:5432/postgres\n          - os: ubuntu-latest\n            python-version: \"3.12\"\n            db: mysql+aiomysql://mysql:mysql@localhost:3306/mymysql\n\n      fail-fast: false\n\n    env:\n      SQLALCHEMY_DATABASE_URL: ${{ matrix.db }}\n\n    services:\n      postgresql:\n        image: ${{ startsWith(matrix.db, 'postgresql') && 'postgres' || '' }}\n        env:\n          POSTGRES_USER: postgres\n          POSTGRES_PASSWORD: postgres\n          POSTGRES_DB: postgres\n        ports:\n          - 5432:5432\n\n      mysql:\n        image: ${{ startsWith(matrix.db, 'mysql') && 'mysql' || '' }}\n        env:\n          MYSQL_ROOT_PASSWORD: mysql\n          MYSQL_USER: mysql\n          MYSQL_PASSWORD: mysql\n          MYSQL_DATABASE: mymysql\n        ports:\n          - 3306:3306\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - uses: actions/setup-python@v5\n        with:\n          python-version: ${{ matrix.python-version }}\n\n      - name: Install dependencies\n        run: pip install -r requirements.txt\n\n      - name: Run migrations\n        run: pipx run nb-cli orm upgrade\n\n      - name: Run tests\n        run: pytest\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/best-practice/database/user.md",
    "content": "---\nsidebar_position: 2\ndescription: 用户指南\n---\n\n# 用户指南\n\n`nonebot-plugin-orm` 功能强大且复杂，使用上有一定难度。\n不过，对于用户而言，只需要掌握部分功能即可。\n\n:::caution 注意\n请注意区分插件的项目名（如：`nonebot-plugin-wordcloud`）和模块名（如：`nonebot_plugin_wordcloud`）。`nonebot-plugin-orm` 中统一使用插件模块名。参见 [插件命名规范](../../developer/plugin-publishing#插件命名规范)。\n:::\n\n## 示例\n\n### 创建新机器人\n\n我们想要创建一个机器人，并安装 `nonebot-plugin-wordcloud` 插件，只需要执行以下命令：\n\n```shell\nnb init  # 初始化项目文件夹\n\npip install nonebot-plugin-orm[sqlite]  # 安装 nonebot-plugin-orm，并附带 SQLite 支持\n\nnb plugin install nonebot-plugin-wordcloud  # 安装插件\n\n# nb orm heads  # 查看有什么插件使用到了数据库（可选）\n\nnb orm upgrade  # 升级数据库\n\n# nb orm check  # 检查一下数据库模式是否与模型定义一致（可选）\n\nnb run  # 启动机器人\n```\n\n### 卸载插件\n\n我们已经安装了 `nonebot-plugin-wordcloud` 插件，但是现在想要卸载它，并且**删除它的数据**，只需要执行以下命令：\n\n```shell\nnb plugin uninstall nonebot-plugin-wordcloud  # 卸载插件\n\n# nb orm heads  # 查看有什么插件使用到了数据库。（可选）\n\nnb orm downgrade nonebot_plugin_wordcloud@base  # 降级数据库，删除数据\n\n# nb orm check  # 检查一下数据库模式是否与模型定义一致（可选）\n```\n\n## CLI\n\n接下来，让我们了解下示例中出现的 CLI 命令的含义：\n\n### heads\n\n显示所有的分支头。一般一个分支对应一个插件。\n\n```shell\nnb orm heads\n```\n\n输出格式为 `<迁移 ID> (<插件模块名>) (<头部类型>)`：\n\n```\n46327b837dd8 (nonebot_plugin_chatrecorder) (head)\n9492159f98f7 (nonebot_plugin_user) (head)\n71a72119935f (nonebot_plugin_session_orm) (effective head)\nade8cdca5470 (nonebot_plugin_wordcloud) (head)\n```\n\n### upgrade\n\n升级数据库。每次安装新的插件或更新插件版本后，都需要执行此命令。\n\n```shell\nnb orm upgrade <插件模块名>@<迁移 ID>\n```\n\n其中，`<插件模块名>@<迁移 ID>` 是可选参数。如果不指定，则会将所有分支升级到最新版本，这也是最常见的用法：\n\n```shell\nnb orm upgrade\n```\n\n### downgrade\n\n降级数据库。当需要回滚插件版本或删除插件时，可以执行此命令。\n\n```shell\nnb orm downgrade <插件模块名>@<迁移 ID>\n```\n\n其中，`<迁移 ID>` 也可以是 `base`，即回滚到初始状态。常用于卸载插件后删除其数据：\n\n```shell\nnb orm downgrade <插件模块名>@base\n```\n\n### check\n\n检查数据库模式是否与模型定义一致。机器人启动前会自动运行此命令（`ALEMBIC_STARTUP_CHECK=true` 时），并在检查失败时阻止启动。\n\n```shell\nnb orm check\n```\n\n## 配置\n\n### sqlalchemy_database_url\n\n默认数据库连接 URL。参见 [数据库驱动和后端](.#数据库驱动和后端) 和 [引擎配置 — SQLAlchemy 2.0 文档](https://docs.sqlalchemy.org/en/20/core/engines.html#database-urls)。\n\n```shell\nSQLALCHEMY_DATABASE_URL=dialect+driver://username:password@host:port/database\n```\n\n### sqlalchemy_bind\n\nbind 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) 实例的字典。\n例如，我们想要让 `nonebot-plugin-wordcloud` 插件使用一个 SQLite 数据库，并开启 [Echo 选项](https://docs.sqlalchemy.org/en/20/core/engines.html#sqlalchemy.create_engine.params.echo) 便于 debug，而其他插件使用默认的 PostgreSQL 数据库，可以这样配置：\n\n```shell\nSQLALCHEMY_BINDS='{\n    \"\": \"postgresql+psycopg://scott:tiger@localhost/mydatabase\",\n    \"nonebot_plugin_wordcloud\": {\n        \"url\": \"sqlite+aiosqlite://\",\n        \"echo\": true\n    }\n}'\n```\n\n### sqlalchemy_engine_options\n\n[`create_async_engine()`](https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html#sqlalchemy.ext.asyncio.create_async_engine) 默认参数字典。\n\n```shell\nSQLALCHEMY_ENGINE_OPTIONS='{\n    \"pool_size\": 5,\n    \"max_overflow\": 10,\n    \"pool_timeout\": 30,\n    \"pool_recycle\": 3600,\n    \"echo\": true\n}'\n```\n\n### sqlalchemy_echo\n\n开启 [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。\n\n```shell\nSQLALCHEMY_ECHO=true\n```\n\n:::caution 注意\n以上配置之间有覆盖关系，遵循特殊优先于一般的原则，具体为 [`sqlalchemy_database_url`](#sqlalchemy_database_url) > [`sqlalchemy_bind`](#sqlalchemy_bind) > [`sqlalchemy_echo`](#sqlalchemy_echo) > [`sqlalchemy_engine_options`](#sqlalchemy_engine_options)。\n但覆盖顺序并非显而易见，出于清晰考虑，请只配置必要的选项。\n:::\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/best-practice/deployment.mdx",
    "content": "---\nsidebar_position: 3\ndescription: 部署你的机器人\n---\n\n# 部署\n\nimport Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\n\n在编写完成各类插件后，我们需要长期运行机器人来使得用户能够正常使用。通常，我们会使用云服务器来部署机器人。\n\n我们在开发插件时，机器人运行的环境称为开发环境；而在部署后，机器人运行的环境称为生产环境。与开发环境不同的是，在生产环境中，开发者通常不能随意地修改/添加/删除代码，开启或停止服务。\n\n## 部署前准备\n\n### 项目依赖管理\n\n由于部署后的机器人运行在生产环境中，因此，为确保机器人能够正常运行，我们需要保证机器人的运行环境与开发环境一致。我们可以通过以下几种方式来进行依赖管理：\n\n<Tabs groupId=\"tool\">\n  <TabItem value=\"poetry\" label=\"Poetry\" default>\n\n[Poetry](https://python-poetry.org/) 是一个 Python 项目的依赖管理工具。它可以通过声明项目所依赖的库，为你管理（安装/更新）它们。Poetry 提供了一个 `poetry.lock` 文件，以确保可重复安装，并可以构建用于分发的项目。\n\nPoetry 会在安装依赖时自动生成 `poetry.lock` 文件，在**项目目录**下执行以下命令：\n\n```bash\n# 初始化 poetry 配置\npoetry init\n# 添加项目依赖，这里以 nonebot2[fastapi] 为例\npoetry add nonebot2[fastapi]\n```\n\n  </TabItem>\n  <TabItem value=\"pdm\" label=\"PDM\">\n\n[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` 文件，以确保可重复安装，并可以构建用于分发的项目。\n\nPDM 会在安装依赖时自动生成 `pdm.lock` 文件，在**项目目录**下执行以下命令：\n\n```bash\n# 初始化 pdm 配置\npdm init\n# 添加项目依赖，这里以 nonebot2[fastapi] 为例\npdm add nonebot2[fastapi]\n```\n\n  </TabItem>\n  <TabItem value=\"pip\" label=\"pip\">\n\n[pip](https://pip.pypa.io/) 是 Python 包管理工具。他并不是一个依赖管理工具，为了尽可能保证环境的一致性，我们可以使用 `requirements.txt` 文件来声明依赖。\n\n```bash\npip freeze > requirements.txt\n```\n\n  </TabItem>\n</Tabs>\n\n### 安装 Docker\n\n[Docker](https://www.docker.com/) 是一个应用容器引擎，可以让开发者打包应用以及依赖包到一个可移植的镜像中，然后发布到服务器上。\n\n我们可以参考 [Docker 官方文档](https://docs.docker.com/get-docker/) 来安装 Docker 。\n\n在 Linux 上，我们可以使用以下一键脚本来安装 Docker 以及 Docker Compose Plugin：\n\n```bash\ncurl -fsSL https://get.docker.com | sh -s -- --mirror Aliyun\n```\n\n在 Windows/macOS 上，我们可以使用 [Docker Desktop](https://docs.docker.com/desktop/) 来安装 Docker 以及 Docker Compose Plugin。\n\n### 安装脚手架 Docker 插件\n\n我们可以使用 [nb-cli-plugin-docker](https://github.com/nonebot/cli-plugin-docker) 来快速部署机器人。\n\n插件可以帮助我们生成配置文件并构建 Docker 镜像，以及启动/停止/重启机器人。使用以下命令安装脚手架 Docker 插件：\n\n```bash\nnb self install nb-cli-plugin-docker\n```\n\n## Docker 部署\n\n### 快速部署\n\n使用脚手架命令即可一键生成配置并部署：\n\n```bash\nnb docker up\n```\n\n当看到 `Running` 字样时，说明机器人已经启动成功。我们可以通过以下命令来查看机器人的运行日志：\n\n<Tabs groupId=\"deploy-tool\">\n  <TabItem value=\"nb-cli\" label=\"NB CLI\" default>\n\n```bash\nnb docker logs\n```\n\n  </TabItem>\n  <TabItem value=\"docker-compose\" label=\"Docker Compose\">\n\n```bash\ndocker compose logs\n```\n\n  </TabItem>\n</Tabs>\n\n如果需要停止机器人，我们可以使用以下命令：\n\n<Tabs groupId=\"deploy-tool\">\n  <TabItem value=\"nb-cli\" label=\"NB CLI\" default>\n\n```bash\nnb docker down\n```\n\n  </TabItem>\n  <TabItem value=\"docker-compose\" label=\"Docker Compose\">\n\n```bash\ndocker compose down\n```\n\n  </TabItem>\n</Tabs>\n\n### 自定义部署\n\n在部分情况下，我们需要事先生成 Docker 配置文件，再到生产环境进行部署；或者自动生成的配置文件并不能满足复杂场景，需要根据实际需求手动修改配置文件。我们可以使用以下命令来生成基础配置文件：\n\n```bash\nnb docker generate\n```\n\nnb-cli 将会在项目目录下生成 `docker-compose.yml` 和 `Dockerfile` 等配置文件。在 nb-cli 完成配置文件的生成后，我们可以根据部署环境的实际情况使用 nb-cli 或者 Docker Compose 来启动机器人。\n\n我们可以参考 [Dockerfile 文件规范](https://docs.docker.com/engine/reference/builder/)和 [Compose 文件规范](https://docs.docker.com/compose/compose-file/)修改这两个文件。\n\n修改完成后我们可以直接启动或者手动构建镜像：\n\n<Tabs groupId=\"deploy-tool\">\n  <TabItem value=\"nb-cli\" label=\"NB CLI\" default>\n\n```bash\n# 启动机器人\nnb docker up\n# 手动构建镜像\nnb docker build\n```\n\n  </TabItem>\n  <TabItem value=\"docker-compose\" label=\"Docker Compose\">\n\n```bash\n# 启动机器人\ndocker compose up -d\n# 手动构建镜像\ndocker compose build\n```\n\n  </TabItem>\n</Tabs>\n\n### 持续集成\n\n我们可以使用 GitHub Actions 来实现持续集成（CI），我们只需要在 GitHub 上发布 Release 即可自动构建镜像并推送至镜像仓库。\n\n首先，我们需要在 [Docker Hub](https://hub.docker.com/) （或者其他平台，如：[GitHub Packages](https://github.com/features/packages)、[阿里云容器镜像服务](https://www.alibabacloud.com/zh/product/container-registry)等）上创建镜像仓库，用于存放镜像。\n\n前往项目仓库的 `Settings` > `Secrets` > `actions` 栏目 `New Repository Secret` 添加构建所需的密钥：\n\n- `DOCKERHUB_USERNAME`: 你的 Docker Hub 用户名\n- `DOCKERHUB_TOKEN`: 你的 Docker Hub PAT（[创建方法](https://docs.docker.com/docker-hub/access-tokens/)）\n\n将以下文件添加至**项目目录**下的 `.github/workflows/` 目录下，并将文件中高亮行中的仓库名称替换为你的仓库名称：\n\n```yaml title=.github/workflows/build.yml\nname: Docker Hub Release\n\non:\n  push:\n    tags:\n      - \"v*\"\n\njobs:\n  docker:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v3\n\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v2\n\n      - name: Setup Docker\n        uses: docker/setup-buildx-action@v2\n\n      - name: Login to DockerHub\n        uses: docker/login-action@v2\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n      - name: Generate Tags\n        uses: docker/metadata-action@v4\n        id: metadata\n        with:\n          images: |\n            # highlight-next-line\n            {organization}/{repository}\n          tags: |\n            type=semver,pattern={{version}}\n            type=semver,pattern={{major}}.{{minor}}\n            type=sha\n            type=raw,value=latest\n\n      - name: Build and Publish\n        uses: docker/build-push-action@v4\n        with:\n          context: .\n          push: true\n          tags: ${{ steps.metadata.outputs.tags }}\n          labels: ${{ steps.metadata.outputs.labels }}\n          cache-from: type=gha\n          cache-to: type=gha,mode=max\n```\n\n### 持续部署\n\n在完成发布并构建镜像后，我们可以自动将镜像部署到服务器上。\n\n前往项目仓库的 `Settings` > `Secrets` > `actions` 栏目 `New Repository Secret` 添加部署所需的密钥：\n\n- `DEPLOY_HOST`: 部署服务器的 SSH 地址\n- `DEPLOY_USER`: 部署服务器用户名\n- `DEPLOY_KEY`: 部署服务器私钥（[创建方法](https://github.com/appleboy/ssh-action#setting-up-a-ssh-key)）\n- `DEPLOY_PATH`: 部署服务器上的项目路径\n\n将以下文件添加至**项目目录**下的 `.github/workflows/` 目录下，在构建成功后触发部署：\n\n```yaml title=.github/workflows/deploy.yml\nname: Deploy\n\non:\n  workflow_run:\n    workflows:\n      - Docker Hub Release\n    types:\n      - completed\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    if: ${{ github.event.workflow_run.conclusion == 'success' }}\n    steps:\n      - name: Start Deployment\n        uses: bobheadxi/deployments@v1\n        id: deployment\n        with:\n          step: start\n          token: ${{ secrets.GITHUB_TOKEN }}\n          env: bot\n\n      - name: Run Remote SSH Command\n        uses: appleboy/ssh-action@master\n        env:\n          DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}\n        with:\n          host: ${{ secrets.DEPLOY_HOST }}\n          username: ${{ secrets.DEPLOY_USER }}\n          key: ${{ secrets.DEPLOY_KEY }}\n          envs: DEPLOY_PATH\n          script: |\n            cd $DEPLOY_PATH\n            docker compose up -d --pull always\n\n      - name: update deployment status\n        uses: bobheadxi/deployments@v0.6.2\n        if: always()\n        with:\n          step: finish\n          token: ${{ secrets.GITHUB_TOKEN }}\n          status: ${{ job.status }}\n          env: ${{ steps.deployment.outputs.env }}\n          deployment_id: ${{ steps.deployment.outputs.deployment_id }}\n```\n\n将上一部分的 `docker-compose.yml` 文件以及 `.env.prod` 配置文件添加至 `DEPLOY_PATH` 目录下，并修改 `docker-compose.yml` 文件中的镜像配置，替换为 Docker Hub 的仓库名称：\n\n```diff\n- build: .\n+ image: {organization}/{repository}:latest\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/best-practice/error-tracking.md",
    "content": "---\nsidebar_position: 2\ndescription: 使用 sentry 进行错误跟踪\n---\n\n# 错误跟踪\n\n在应用实际运行过程中，可能会出现各种各样的错误。可能是由于代码逻辑错误，也可能是由于用户输入错误，甚至是由于第三方服务的错误。这些错误都会导致应用的运行出现问题，这时候就需要对错误进行跟踪，以便及时发现问题并进行修复。NoneBot 提供了 `nonebot-plugin-sentry` 插件，支持 [sentry](https://sentry.io/) 平台，可以方便地进行错误跟踪。\n\n## 安装插件\n\n在使用前请先安装 `nonebot-plugin-sentry` 插件至项目环境中，可参考[获取商店插件](../tutorial/store.mdx#安装插件)来了解并选择安装插件的方式。如：\n\n在**项目目录**下执行以下命令：\n\n```bash\nnb plugin install nonebot-plugin-sentry\n```\n\n## 使用插件\n\n在安装完成之后，仅需要对插件进行简单的配置即可使用。\n\n### 获取 sentry DSN\n\n前往 [sentry](https://sentry.io/) 平台，注册并创建一个新的项目，然后在项目设置中找到 `Client Keys (DSN)`，复制其中的 `DSN` 值。\n\n### 配置插件\n\n:::caution 注意\n错误跟踪通常在生产环境中使用，因此开发环境中 `sentry_dsn` 留空即会停用插件。\n:::\n\n在项目 dotenv 配置文件中添加以下配置即可使用：\n\n```dotenv\nSENTRY_DSN=<your_sentry_dsn>\n```\n\n## 配置项\n\n配置项具体含义参考 [Sentry Docs](https://docs.sentry.io/platforms/python/configuration/options/)。\n\n- `sentry_dsn: str`\n- `sentry_debug: bool = False`\n- `sentry_release: str | None = None`\n- `sentry_release: str | None = None`\n- `sentry_environment: str | None = nonebot env`\n- `sentry_server_name: str | None = None`\n- `sentry_sample_rate: float = 1.`\n- `sentry_max_breadcrumbs: int = 100`\n- `sentry_attach_stacktrace: bool = False`\n- `sentry_send_default_pii: bool = False`\n- `sentry_in_app_include: List[str] = Field(default_factory=list)`\n- `sentry_in_app_exclude: List[str] = Field(default_factory=list)`\n- `sentry_request_bodies: str = \"medium\"`\n- `sentry_with_locals: bool = True`\n- `sentry_ca_certs: str | None = None`\n- `sentry_before_send: Callable[[Any, Any], Any | None] | None = None`\n- `sentry_before_breadcrumb: Callable[[Any, Any], Any | None] | None = None`\n- `sentry_transport: Any | None = None`\n- `sentry_http_proxy: str | None = None`\n- `sentry_https_proxy: str | None = None`\n- `sentry_shutdown_timeout: int = 2`\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/best-practice/htmlkit-render.md",
    "content": "---\nsidebar_position: 8\ndescription: 轻量化 HTML 绘图\n---\n\n# 轻量化 HTML 绘图\n\n图片是机器人交互中不可或缺的一部分，对于信息展示的直观性、美观性有很大的作用。\n基于 PIL 直接绘制图片具有良好的性能和存储开销，但是难以调试、维护过程式的绘图代码。\n使用浏览器渲染类插件可以方便地绘制网页，且能够直接通过 JS 对网页效果进行编程，但是它占用的存储和内存空间相对可观。\n\nNoneBot 提供的 `nonebot-plugin-htmlkit` 提供了另一种基于 HTML 和 CSS 语法的轻量化绘图选择：它基于 `litehtml` 解析库，无须安装额外的依赖即可使用，没有进程间通信带来的额外开销，且在支持 `webp` `avif` 等丰富图片格式的前提下，安装用的 wheel 文件大小仅有约 10 MB。\n\n作为粗略的性能参考，在一台 Ryzen 7 9700X 的 Windows 电脑上，渲染 [PEP 7](https://peps.python.org/pep-0007/) 的 HTML 页面（分辨率为 800x5788，大小约 1.4MB，从本地文件系统读取 CSS）大约需要 100ms，每个渲染任务内存最高占用约为 40MB.\n\n## 安装插件\n\n在使用前请先安装 `nonebot-plugin-htmlkit` 插件至项目环境中，可参考[获取商店插件](../tutorial/store.mdx#安装插件)来了解并选择安装插件的方式。如：\n\n在**项目目录**下执行以下命令：\n\n```bash\nnb plugin install nonebot-plugin-htmlkit\n```\n\n`nonebot-plugin-htmlkit` 插件目前兼容以下系统架构：\n\n- Windows x64\n- macOS arm64（M-系列芯片）\n- Linux x64 （非 Alpine 等 musl 系发行版）\n- Linux arm64 （非 Alpine 等 musl 系发行版）\n\n:::caution 访问网络内容\n\n如果需要访问网络资源（如 http(s) 网页内容），NoneBot 需要客户端型驱动器（Forward）。内置的驱动器有 `~httpx` 与 `~aiohttp`。\n\n详见[选择驱动器](../advanced/driver.md)。\n\n:::\n\n## 使用插件\n\n### 加载插件\n\n在使用本插件前同样需要使用 `require` 方法进行**加载**并**导入**需要使用的方法，可参考 [跨插件访问](../advanced/requiring.md) 一节进行了解，如：\n\n```python\nfrom nonebot import require\n\nrequire(\"nonebot_plugin_htmlkit\")\n\nfrom nonebot_plugin_htmlkit import html_to_pic, md_to_pic, template_to_pic, text_to_pic\n```\n\n插件会自动使用[配置中的参数](#配置-fontconfig)初始化 `fontconfig` 以提供字体查找功能。\n\n### 渲染 API\n\n`nonebot-plugin-htmlkit` 主要提供以下**异步**渲染函数：\n\n#### html_to_pic\n\n```python\nasync def html_to_pic(\n    html: str,\n    *,\n    base_url: str = \"\",\n    dpi: float = 144.0,\n    max_width: float = 800.0,\n    device_height: float = 600.0,\n    default_font_size: float = 12.0,\n    font_name: str = \"sans-serif\",\n    allow_refit: bool = True,\n    image_format: Literal[\"png\", \"jpeg\"] = \"png\",\n    jpeg_quality: int = 100,\n    lang: str = \"zh\",\n    culture: str = \"CN\",\n    img_fetch_fn: ImgFetchFn = combined_img_fetcher,\n    css_fetch_fn: CSSFetchFn = combined_css_fetcher,\n    urljoin_fn: Callable[[str, str], str] = urllib3.parse.urljoin,\n) -> bytes:\n    ...\n```\n\n最核心的渲染函数。\n\n`base_url` 和 `urljoin_fn` 控制着传入 `image_fetch_fn` 和 `css_fetch_fn` 回调的 url 内容。\n\n`allow_refit` 如果为真，渲染时会自动缩小产出图片的宽度到最适合的宽度，否则必定产出 `max_width` 宽度的图片。\n\n`max_width` 与 `device_height` 会在 `@media` 判断中被使用。\n\n`img_fetch_fn` 预期为一个异步可调用对象（函数），接收图片 url 并返回对应 url 的 jpeg 或 png 二进制数据（`bytes`），可在拒绝加载时返回 `None`.\n\n`css_fetch_fn` 预期为一个异步可调用对象（函数），接收目标 CSS url 并返回对应 url 的 CSS 文本（`str`），可在拒绝加载时返回 `None`.\n\n以下为辅助的封装函数，关键字参数若未特殊说明均与 `html_to_pic` 含义相同。\n\n#### text_to_pic\n\n```python\nasync def text_to_pic(\n    text: str,\n    css_path: str = \"\",\n    *,\n    max_width: int = 500,\n    allow_refit: bool = True,\n    image_format: Literal[\"png\", \"jpeg\"] = \"png\",\n    jpeg_quality: int = 100,\n) -> bytes:\n    ...\n```\n\n可用于渲染多行文本。\n\n`text` 会被放置于 `<div id=\"main\" class=\"main-box\"> <div class=\"text\">` 中，可据此编写 CSS 来改变文本表现。\n\n#### md_to_pic\n\n```python\nasync def md_to_pic(\n    md: str = \"\",\n    md_path: str = \"\",\n    css_path: str = \"\",\n    *,\n    max_width: int = 500,\n    img_fetch_fn: ImgFetchFn = combined_img_fetcher,\n    allow_refit: bool = True,\n    image_format: Literal[\"png\", \"jpeg\"] = \"png\",\n    jpeg_quality: int = 100,\n) -> bytes:\n    ...\n```\n\n可用于渲染 Markdown 文本。默认为 GitHub Markdown Light 风格，支持基于 `pygments` 的代码高亮。\n\n`md` 和 `md_path` 二选一，前者设置时应为 Markdown 的文本，后者设置时应为指向 Markdown 文本文件的路径。\n\n#### template_to_pic\n\n```python\nasync def template_to_pic(\n    template_path: str | PathLike[str] | Sequence[str | PathLike[str]],\n    template_name: str,\n    templates: Mapping[Any, Any],\n    filters: None | Mapping[str, Any] = None,\n    *,\n    max_width: int = 500,\n    device_height: int = 600,\n    base_url: str | None = None,\n    img_fetch_fn: ImgFetchFn = combined_img_fetcher,\n    css_fetch_fn: CSSFetchFn = combined_css_fetcher,\n    allow_refit: bool = True,\n    image_format: Literal[\"png\", \"jpeg\"] = \"png\",\n    jpeg_quality: int = 100,\n) -> bytes:\n    ...\n```\n\n渲染 jinja2 模板。\n\n`template_path` 为 jinja2 环境的路径，`template_name` 是环境中要加载模板的名字，`templates` 为传入模板的参数，`filters` 为过滤器名 -> 自定义过滤器的映射。\n\n### 控制外部资源获取\n\n通过传入 `img_fetch_fn` 与 `css_fetch_fn`，我们可以在实际访问资源前进行审查，修改资源的来源，或是对 IO 操作进行缓存。\n\n`img_fetch_fn` 预期为一个异步可调用对象（函数），接收图片 url 并返回对应 url 的 jpeg 或 png 二进制数据（`bytes`），可在拒绝加载时返回 `None`.\n\n`css_fetch_fn` 预期为一个异步可调用对象（函数），接收目标 CSS url 并返回对应 url 的 CSS 文本（`str`），可在拒绝加载时返回 `None`.\n\n如果你想要禁用外部资源加载/只从文件系统加载/只从网络加载，可以使用 `none_fetcher` `filesystem_***_fetcher` `network_***_fetcher`。\n\n默认的 fetcher 行为（对于 `file://` 从文件系统加载，其余从网络加载）位于 `combined_***_fetcher`，可以通过对其封装实现缓存等操作。\n\n## 配置项\n\n### 配置 fontconfig\n\n`htmlkit` 使用 `fontconfig` 查找字体，请参阅 [`fontconfig 用户手册`](https://fontconfig.pages.freedesktop.org/fontconfig/fontconfig-user) 了解环境变量的具体含义、如何通过编写配置文件修改字体配置等。\n\n#### fontconfig_file\n\n- **类型**: `str | None`\n- **默认值**: `None`\n\n覆盖默认的配置文件路径。\n\n#### fontconfig_path\n\n- **类型**: `str | None`\n- **默认值**: `None`\n\n覆盖默认的配置目录。\n\n#### fontconfig_sysroot\n\n- **类型**: `str | None`\n- **默认值**: `None`\n\n覆盖默认的 sysroot。\n\n#### fc_debug\n\n- **类型**: `str | None`\n- **默认值**: `None`\n\n设置 Fontconfig 的 debug 级别。\n\n#### fc_dbg_match_filter\n\n- **类型**: `str | None`\n- **默认值**: `None`\n\n当 `FC_DEBUG` 设置为 `MATCH2` 时，过滤 debug 输出。\n\n#### fc_lang\n\n- **类型**: `str | None`\n- **默认值**: `None`\n\n设置默认语言，否则从 `LOCALE` 环境变量获取。\n\n#### fontconfig_use_mmap\n\n- **类型**: `str | None`\n- **默认值**: `None`\n\n是否使用 `mmap(2)` 读取字体缓存。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/best-practice/multi-adapter.mdx",
    "content": "---\nsidebar_position: 4\ndescription: 插件跨平台支持\n---\n\n# 插件跨平台支持\n\nimport Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\n\n## 使用 NoneBot 本身\n\n由于不同平台的事件与接口之间，存在着极大的差异性，NoneBot 通过[重载](../appendices/overload.md)的方式，使得插件可以在不同平台上正确响应。但为了减少跨平台的兼容性问题，我们应该尽可能的使用基类方法实现原生跨平台，而不是使用特定平台的方法。当基类方法无法满足需求时，我们可以使用依赖注入的方式，将特定平台的事件或机器人注入到事件处理函数中，实现针对特定平台的处理。\n\n:::tip 提示\n如果需要在多平台上**使用**跨平台插件，首先应该根据[注册适配器](../advanced/adapter.md#注册适配器)一节，为机器人注册各平台对应的适配器。\n:::\n\n### 基于基类的跨平台\n\n在[事件通用信息](../advanced/adapter.md#获取事件通用信息)中，我们了解了事件基类能够提供的通用信息。同时，[事件响应器操作](../appendices/session-control.mdx#更多事件响应器操作)也为我们提供了基本的用户交互方式。使用这些方法，可以让我们的插件运行在任何平台上。例如，一个简单的命令处理插件：\n\n```python {5,11}\nfrom nonebot import on_command\nfrom nonebot.adapters import Event\n\nasync def is_blacklisted(event: Event) -> bool:\n    return event.get_user_id() not in BLACKLIST\n\nweather = on_command(\"天气\", rule=is_blacklisted, priority=10, block=True)\n\n@weather.handle()\nasync def handle_function():\n    await weather.finish(\"今天的天气是...\")\n```\n\n由于此插件仅使用了事件通用信息和事件响应器操作的纯文本交互方式，这些方法不使用特定平台的信息或接口，因此是原生跨平台的，并不需要额外处理。但在一些较为复杂的需求下，例如发送图片消息时，并非所有平台都具有统一的接口，因此基类便无能为力，我们需要引入特定平台的适配器了。\n\n### 基于重载的跨平台\n\n重载是 NoneBot 跨平台操作的核心，在[事件类型与重载](../appendices/overload.md#重载)一节中，我们初步了解了如何通过类型注解来实现针对不同平台事件的处理方式。在[依赖注入](../advanced/dependency.mdx)一节中，我们又对依赖注入的使用方法进行了详细的介绍。结合这两节内容，我们可以实现更复杂的跨平台操作。\n\n#### 处理近似事件\n\n对于一系列**差异不大**的事件，我们往往具有相同的处理逻辑。这时，我们不希望将相同的逻辑编写两遍，而应该复用代码，以实现在同一个事件处理函数中处理多个近似事件。我们可以使用[事件重载](../advanced/dependency.mdx#event)的特性来实现这一功能。例如：\n\n<Tabs groupId=\"python\">\n  <TabItem value=\"3.10\" label=\"Python 3.10+\" default>\n\n```python\nfrom nonebot import on_command\nfrom nonebot.adapters import Message\nfrom nonebot.params import CommandArg\nfrom nonebot.adapters.onebot.v11 import MessageEvent as OnebotV11MessageEvent\nfrom nonebot.adapters.onebot.v12 import MessageEvent as OnebotV12MessageEvent\n\necho = on_command(\"echo\", priority=10, block=True)\n\n@echo.handle()\nasync def handle_function(event: OnebotV11MessageEvent | OnebotV12MessageEvent, args: Message = CommandArg()):\n    await echo.finish(args)\n```\n\n  </TabItem>\n  <TabItem value=\"3.9\" label=\"Python 3.9\">\n\n```python\nfrom typing import Union\n\nfrom nonebot import on_command\nfrom nonebot.adapters import Message\nfrom nonebot.params import CommandArg\nfrom nonebot.adapters.onebot.v11 import MessageEvent as OnebotV11MessageEvent\nfrom nonebot.adapters.onebot.v12 import MessageEvent as OnebotV12MessageEvent\n\necho = on_command(\"echo\", priority=10, block=True)\n\n@echo.handle()\nasync def handle_function(event: Union[OnebotV11MessageEvent, OnebotV12MessageEvent], args: Message = CommandArg()):\n    await echo.finish(args)\n```\n\n  </TabItem>\n</Tabs>\n\n#### 在依赖注入中使用重载\n\nNoneBot 依赖注入系统提供了自定义子依赖的方法，子依赖的类型同样会影响到事件处理函数的重载行为。例如：\n\n```python\nfrom datetime import datetime\n\nfrom nonebot import on_command\nfrom nonebot.adapters.console import MessageEvent\n\necho = on_command(\"echo\", priority=10, block=True)\n\ndef get_event_time(event: MessageEvent):\n    return event.time\n\n# 处理控制台消息事件\n@echo.handle()\nasync def handle_function(time: datetime = Depends(get_event_time)):\n    await echo.finish(time.strftime(\"%Y-%m-%d %H:%M:%S\"))\n```\n\n示例中 ，我们为 `handle_function` 事件处理函数注入了自定义的 `get_event_time` 子依赖，而此子依赖注入参数为 Console 适配器的 `MessageEvent`。因此 `handle_function` 仅会响应 Console 适配器的 `MessageEvent` ，而不能响应其他事件。\n\n#### 处理多平台事件\n\n不同平台的事件之间，往往存在着极大的差异性。为了满足我们插件的跨平台运行，通常我们需要抽离业务逻辑，以保证代码的复用性。一个合理的做法是，在事件响应器的处理流程中，首先先针对不同平台的事件分别进行处理，提取出核心业务逻辑所需要的信息；然后再将这些信息传递给业务逻辑处理函数；最后将业务逻辑的输出以各平台合适的方式返回给用户。也就是说，与平台绑定的处理部分应该与平台无关部分尽量分离。例如：\n\n```python\nimport inspect\n\nfrom nonebot import on_command\nfrom nonebot.typing import T_State\nfrom nonebot.matcher import Matcher\nfrom nonebot.adapters import Message\nfrom nonebot.params import CommandArg, ArgPlainText\nfrom nonebot.adapters.console import Bot as ConsoleBot\nfrom nonebot.adapters.onebot.v11 import Bot as OnebotBot\nfrom nonebot.adapters.console import MessageSegment as ConsoleMessageSegment\n\nweather = on_command(\"天气\", priority=10, block=True)\n\n@weather.handle()\nasync def handle_function(matcher: Matcher, args: Message = CommandArg()):\n    if args.extract_plain_text():\n        matcher.set_arg(\"location\", args)\n\n\nasync def get_weather(state: T_State, location: str = ArgPlainText()):\n    if location not in [\"北京\", \"上海\", \"广州\", \"深圳\"]:\n        await weather.reject(f\"你想查询的城市 {location} 暂不支持，请重新输入！\")\n\n    state[\"weather\"] = \"⛅ 多云 20℃~24℃\"\n\n\n# 处理控制台询问\n@weather.got(\n    \"location\",\n    prompt=ConsoleMessageSegment.emoji(\"question\") + \"请输入地名\",\n    parameterless=[Depends(get_weather)],\n)\nasync def handle_console(bot: ConsoleBot):\n    pass\n\n# 处理 OneBot 询问\n@weather.got(\n    \"location\",\n    prompt=\"请输入地名\",\n    parameterless=[Depends(get_weather)],\n)\nasync def handle_onebot(bot: OnebotBot):\n    pass\n\n# 通过依赖注入或事件处理函数来进行业务逻辑处理\n\n# 处理控制台回复\n@weather.handle()\nasync def handle_console_reply(bot: ConsoleBot, state: T_State, location: str = ArgPlainText()):\n    await weather.send(\n        ConsoleMessageSegment.markdown(\n            inspect.cleandoc(\n                f\"\"\"\n                # {location}\n\n                - 今天\n\n                   {state['weather']}\n                \"\"\"\n            )\n        )\n    )\n\n# 处理 OneBot 回复\n@weather.handle()\nasync def handle_onebot_reply(bot: OnebotBot, state: T_State, location: str = ArgPlainText()):\n    await weather.send(f\"今天{location}的天气是{state['weather']}\")\n```\n\n## 使用插件\n\n得益于众多开发者为 NoneBot 社区做出的贡献，我们可以通过一系列插件来完成跨平台插件的开发。\n\n这些插件可以分为三类：\n\n### 事件处理\n\n- [all4one](https://github.com/nonepkg/nonebot-plugin-all4one): 将不同平台的事件转为符合 OneBot V12 协议的插件\n  - 支持的适配器: OneBot V11/V12, Discord, QQ, Telegram\n\n### 消息处理\n\n- [alconna](https://github.com/nonebot/plugin-alconna): 对几乎所有适配器中消息的收发、撤回、编辑、表态的统一插件\n  - 支持的适配器: OneBot V11/V12, Telegram, Feishu, Github, QQ, Ding, Console, Kaiheila, Mirai, NtChat, Minecraft, Discord, Satori, Red, Dodo, Kritor, Tailchat, Mail, WXMP, Heybox, Gewechat\n- [send-anything-anywhere](https://github.com/felinae98/nonebot-plugin-send-anything-anywhere): 帮助处理不同适配器消息的适配和发送的插件\n  - 支持的适配器: OneBot V11/V12, Kaiheila, Telegram, Feishu, Red, DoDo, Satori, QQ, Discord\n\n### 会话信息提取\n\n- [uninfo](https://github.com/RF-Tar-Railt/nonebot-plugin-uninfo): 多平台的会话信息(用户、群组、频道)获取插件\n  - 支持的适配器: OneBot V11/V12, Telegram, Feishu, QQ, Console, Kaiheila, Mirai, Minecraft, Discord, Satori, Dodo, Kritor, Mail, WXMP, Gewechat\n- [session](https://github.com/noneplugin/nonebot-plugin-session): 会话信息提取与会话 id 定义插件\n  - 支持的适配器: OneBot V11/V12, Console, Kaiheila, Telegram, Feishu, Red, DoDo, Satori, QQ, Discord\n- [userinfo](https://github.com/noneplugin/nonebot-plugin-userinfo): 用户信息获取插件\n  - 支持的适配器: OneBot V11/V12, Console, Kaiheila, Telegram, Feishu, Red, DoDo, Satori, QQ, Discord\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/best-practice/scheduler.md",
    "content": "---\nsidebar_position: 0\ndescription: 定时执行任务\n---\n\n# 定时任务\n\n[APScheduler](https://apscheduler.readthedocs.io/en/3.x/) (Advanced Python Scheduler) 是一个 Python 第三方库，其强大的定时任务功能被广泛应用于各个场景。在 NoneBot 中，定时任务作为一个额外功能，依赖于基于 APScheduler 开发的 [`nonebot-plugin-apscheduler`](https://github.com/nonebot/plugin-apscheduler) 插件进行支持。\n\n## 安装插件\n\n在使用前请先安装 `nonebot-plugin-apscheduler` 插件至项目环境中，可参考[获取商店插件](../tutorial/store.mdx#安装插件)来了解并选择安装插件的方式。如：\n\n在**项目目录**下执行以下命令：\n\n```bash\nnb plugin install nonebot-plugin-apscheduler\n```\n\n## 使用插件\n\n`nonebot-plugin-apscheduler` 本质上是对 [APScheduler](https://apscheduler.readthedocs.io/en/3.x/) 进行了封装以适用于 NoneBot 开发，因此其使用方式与 APScheduler 本身并无显著区别。在此我们会简要介绍其调用方法，更多的使用方面的功能请参考[APScheduler 官方文档](https://apscheduler.readthedocs.io/en/3.x/userguide.html)。\n\n### 导入调度器\n\n由于 `nonebot_plugin_apscheduler` 作为插件，因此需要在使用前对其进行**加载**并**导入**其中的 `scheduler` 调度器来创建定时任务。使用 `require` 方法可轻松完成这一过程，可参考 [跨插件访问](../advanced/requiring.md) 一节进行了解。\n\n```python\nfrom nonebot import require\n\nrequire(\"nonebot_plugin_apscheduler\")\n\nfrom nonebot_plugin_apscheduler import scheduler\n```\n\n### 添加定时任务\n\n在 [APScheduler 官方文档](https://apscheduler.readthedocs.io/en/3.x/userguide.html#adding-jobs) 中提供了以下两种直接添加任务的方式：\n\n```python\nfrom nonebot import require\n\nrequire(\"nonebot_plugin_apscheduler\")\n\nfrom nonebot_plugin_apscheduler import scheduler\n\n# 基于装饰器的方式\n@scheduler.scheduled_job(\"cron\", hour=\"*/2\", id=\"job_0\", args=[1], kwargs={arg2: 2})\nasync def run_every_2_hour(arg1: int, arg2: int):\n    pass\n\n# 基于 add_job 方法的方式\ndef run_every_day(arg1: int, arg2: int):\n    pass\n\nscheduler.add_job(\n    run_every_day, \"interval\", days=1, id=\"job_1\", args=[1], kwargs={arg2: 2}\n)\n```\n\n:::caution 注意\n由于 APScheduler 的定时任务并不是**由事件响应器所触发的事件**，因此其任务函数无法同[事件处理函数](../tutorial/handler.mdx#事件处理函数)一样通过[依赖注入](../tutorial/event-data.mdx#认识依赖注入)获取上下文信息，也无法通过事件响应器对象的方法进行任何操作，因此我们需要使用[调用平台 API](../appendices/api-calling.mdx#调用平台-api)的方式来获取信息或收发消息。\n\n相对于事件处理依赖而言，编写定时任务更像是编写普通的函数，需要我们自行获取信息以及发送信息，请**不要**将事件处理依赖的特殊语法用于定时任务！\n:::\n\n关于 APScheduler 的更多使用方法，可以参考 [APScheduler 官方文档](https://apscheduler.readthedocs.io/en/3.x/index.html) 进行了解。\n\n### 配置项\n\n#### apscheduler_autostart\n\n- **类型**: `bool`\n- **默认值**: `True`\n\n是否自动启动 `scheduler` ，若不启动需要自行调用 `scheduler.start()`。\n\n#### apscheduler_log_level\n\n- **类型**: `int`\n- **默认值**: `30`\n\napscheduler 输出的日志等级\n\n- `WARNING` = `30` (默认)\n- `INFO` = `20`\n- `DEBUG` = `10` (只有在开启 nonebot 的 debug 模式才会显示 debug 日志)\n\n#### apscheduler_config\n\n- **类型**: `dict`\n- **默认值**: `{ \"apscheduler.timezone\": \"Asia/Shanghai\" }`\n\n`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)\n\n配置需要包含 `apscheduler.` 作为前缀，例如 `apscheduler.timezone`。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/best-practice/testing/README.mdx",
    "content": "---\nsidebar_position: 1\ndescription: 使用 NoneBug 进行单元测试\n\nslug: /best-practice/testing/\n---\n\n# 配置与测试事件响应器\n\nimport Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\n\n> 在计算机编程中，单元测试（Unit Testing）又称为模块测试，是针对程序模块（软件设计的最小单位）来进行正确性检验的测试工作。\n\n为了保证代码的正确运行，我们不仅需要对错误进行跟踪，还需要对代码进行正确性检验，也就是测试。NoneBot 提供了一个测试工具——NoneBug，它是一个 [pytest](https://docs.pytest.org/en/stable/) 插件，可以帮助我们便捷地进行单元测试。\n\n:::tip 提示\n建议在阅读本文档前先阅读 [pytest 官方文档](https://docs.pytest.org/en/stable/)来了解 pytest 的相关术语和基本用法。\n:::\n\n## 安装 NoneBug\n\n在**项目目录**下激活虚拟环境后运行以下命令安装 NoneBug：\n\n<Tabs groupId=\"tool\">\n  <TabItem value=\"poetry\" label=\"Poetry\" default>\n\n```bash\npoetry add nonebug -G test\n```\n\n  </TabItem>\n  <TabItem value=\"pdm\" label=\"PDM\">\n\n```bash\npdm add nonebug -dG test\n```\n\n  </TabItem>\n  <TabItem value=\"pip\" label=\"pip\">\n\n```bash\npip install nonebug\n```\n\n  </TabItem>\n</Tabs>\n\n要运行 NoneBug 测试，还需要额外安装 pytest 异步插件 `pytest-asyncio` 或 `anyio` 以支持异步测试。文档中，我们以 `pytest-asyncio` 为例：\n\n<Tabs groupId=\"tool\">\n  <TabItem value=\"poetry\" label=\"Poetry\" default>\n\n```bash\npoetry add pytest-asyncio -G test\n```\n\n  </TabItem>\n  <TabItem value=\"pdm\" label=\"PDM\">\n\n```bash\npdm add pytest-asyncio -dG test\n```\n\n  </TabItem>\n  <TabItem value=\"pip\" label=\"pip\">\n\n```bash\npip install pytest-asyncio\n```\n\n  </TabItem>\n</Tabs>\n\n## 配置测试\n\n在开始测试之前，我们需要对测试进行一些配置，以正确启动我们的机器人。\n\n首先我们需要配置 pytest-asyncio，在 `pyproject.toml` 的 pytest 配置部分添加：\n\n```toml\n[tool.pytest.ini_options]\nasyncio_mode = \"auto\"\nasyncio_default_fixture_loop_scope = \"session\"\n```\n\n然后，我们在 `tests` 目录下新建 `conftest.py` 文件，添加以下内容：\n\n```python title=tests/conftest.py\nimport pytest\nimport nonebot\nfrom pytest_asyncio import is_async_test\n# 导入适配器\nfrom nonebot.adapters.console import Adapter as ConsoleAdapter\n\ndef pytest_collection_modifyitems(items: list[pytest.Item]):\n    pytest_asyncio_tests = (item for item in items if is_async_test(item))\n    session_scope_marker = pytest.mark.asyncio(loop_scope=\"session\")\n    for async_test in pytest_asyncio_tests:\n        async_test.add_marker(session_scope_marker, append=False)\n\n@pytest.fixture(scope=\"session\", autouse=True)\nasync def after_nonebot_init(after_nonebot_init: None):\n    # 加载适配器\n    driver = nonebot.get_driver()\n    driver.register_adapter(ConsoleAdapter)\n\n    # 加载插件\n    nonebot.load_from_toml(\"pyproject.toml\")\n```\n\n这样，我们就可以在测试中使用机器人的插件了。通常，我们不需要自行初始化 NoneBot，NoneBug 已经为我们运行了 `nonebot.init()`。如果需要自定义 NoneBot 初始化的参数，我们可以在 `conftest.py` 中添加 `pytest_configure` 钩子函数。例如，我们可以修改 NoneBot 配置环境为 `test` 并从环境变量中输入配置：\n\n```python {4,6,8-10} title=tests/conftest.py\nimport os\n\nimport pytest\nfrom nonebug import NONEBOT_INIT_KWARGS\n\nos.environ[\"ENVIRONMENT\"] = \"test\"\n\ndef pytest_configure(config: pytest.Config):\n    config.stash[NONEBOT_INIT_KWARGS] = {\"secret\": os.getenv(\"INPUT_SECRET\")}\n```\n\nNoneBug 默认也会为我们管理 lifespan 的 startup 与 shutdown。如果不希望 NoneBug 管理 lifespan，你可以在 `pytest_configure` 里添加以下配置：\n\n```python\nimport pytest\nfrom nonebug import NONEBOT_START_LIFESPAN\n\ndef pytest_configure(config: pytest.Config):\n    config.stash[NONEBOT_START_LIFESPAN] = False\n```\n\n## 编写插件测试\n\n在配置完成插件加载后，我们就可以在测试中使用插件了。NoneBug 通过 pytest fixture `app` 提供各种测试方法，我们可以在测试中使用它来测试插件。现在，我们创建一个测试脚本来测试[深入指南](../../appendices/session-control.mdx)中编写的天气插件。首先，我们先要导入我们需要的模块：\n\n<details>\n  <summary>插件示例</summary>\n\n```python title=weather/__init__.py\nfrom nonebot import on_command\nfrom nonebot.rule import to_me\nfrom nonebot.matcher import Matcher\nfrom nonebot.adapters import Message\nfrom nonebot.params import CommandArg, ArgPlainText\n\nweather = on_command(\"天气\", rule=to_me(), aliases={\"weather\", \"天气预报\"})\n\n@weather.handle()\nasync def handle_function(matcher: Matcher, args: Message = CommandArg()):\n    if args.extract_plain_text():\n        matcher.set_arg(\"location\", args)\n\n@weather.got(\"location\", prompt=\"请输入地名\")\nasync def got_location(location: str = ArgPlainText()):\n    if location not in [\"北京\", \"上海\", \"广州\", \"深圳\"]:\n        await weather.reject(f\"你想查询的城市 {location} 暂不支持，请重新输入！\")\n    await weather.finish(f\"今天{location}的天气是...\")\n```\n\n</details>\n\n```python {4,5,9,11-16} title=tests/test_weather.py\nfrom datetime import datetime\n\nimport pytest\nfrom nonebug import App\nfrom nonebot.adapters.console import User, Message, MessageEvent\n\n@pytest.mark.asyncio\nasync def test_weather(app: App):\n    from awesome_bot.plugins.weather import weather\n\n    event = MessageEvent(\n        time=datetime.now(),\n        self_id=\"test\",\n        message=Message(\"/天气 北京\"),\n        user=User(id=\"user\"),\n    )\n```\n\n在上面的代码中，我们引入了 NoneBug 的测试 `App` 对象，以及必要的适配器消息与事件定义等。在测试函数 `test_weather` 中，我们导入了要进行测试的事件响应器 `weather`。请注意，由于需要等待 NoneBot 初始化并加载插件完毕，插件内容必须在**测试函数内部**进行导入。然后，我们创建了一个 `MessageEvent` 事件对象，它模拟了一个用户发送了 `/天气 北京` 的消息。接下来，我们使用 `app.test_matcher` 方法来测试 `weather` 事件响应器：\n\n```python {11-15} title=tests/test_weather.py\n@pytest.mark.asyncio\nasync def test_weather(app: App):\n    from awesome_bot.plugins.weather import weather\n\n    event = MessageEvent(\n        time=datetime.now(),\n        self_id=\"test\",\n        message=Message(\"/天气 北京\"),\n        user=User(id=\"user\"),\n    )\n    async with app.test_matcher(weather) as ctx:\n        bot = ctx.create_bot()\n        ctx.receive_event(bot, event)\n        ctx.should_call_send(event, \"今天北京的天气是...\", result=None)\n        ctx.should_finished(weather)\n```\n\n这里我们使用 `async with` 语句并通过参数指定要测试的事件响应器 `weather` 来进入测试上下文。在测试上下文中，我们可以使用 `ctx.create_bot` 方法创建一个虚拟的机器人实例，并使用 `ctx.receive_event` 方法来模拟机器人接收到消息事件。然后，我们就可以定义预期行为来测试机器人是否正确运行。在上面的代码中，我们使用 `ctx.should_call_send` 方法来断言机器人应该发送 `今天北京的天气是...` 这条消息，并且将发送函数的调用结果作为第三个参数返回给事件处理函数。如果断言失败，测试将会不通过。我们也可以使用 `ctx.should_finished` 方法来断言机器人应该结束会话。\n\n为了测试更复杂的情况，我们可以为添加更多的测试用例。例如，我们可以测试用户输入了一个不支持的地名时机器人的反应：\n\n```python {17-21,23-26} title=tests/test_weather.py\ndef make_event(message: str = \"\") -> MessageEvent:\n    return MessageEvent(\n        time=datetime.now(),\n        self_id=\"test\",\n        message=Message(message),\n        user=User(id=\"user\"),\n    )\n\n@pytest.mark.asyncio\nasync def test_weather(app: App):\n    from awesome_bot.plugins.weather import weather\n\n    async with app.test_matcher(weather) as ctx:\n        ...  # 省略前面的测试用例\n\n    async with app.test_matcher(weather) as ctx:\n        bot = ctx.create_bot()\n        event = make_event(\"/天气 南京\")\n        ctx.receive_event(bot, event)\n        ctx.should_call_send(event, \"你想查询的城市 南京 暂不支持，请重新输入！\", result=None)\n        ctx.should_rejected(weather)\n\n        event = make_event(\"北京\")\n        ctx.receive_event(bot, event)\n        ctx.should_call_send(event, \"今天北京的天气是...\", result=None)\n        ctx.should_finished(weather)\n```\n\n在上面的代码中，我们使用 `ctx.should_rejected` 来断言机器人应该请求用户重新输入。然后，我们再次使用 `ctx.receive_event` 方法来模拟用户回复了 `北京`，并使用 `ctx.should_finished` 来断言机器人应该结束会话。\n\n更多的 NoneBug 用法将在后续章节中介绍。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/best-practice/testing/_category_.json",
    "content": "{\n  \"label\": \"单元测试\",\n  \"position\": 5\n}\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/best-practice/testing/behavior.mdx",
    "content": "---\nsidebar_position: 2\ndescription: 测试事件响应、平台接口调用和会话控制\n---\n\n# 测试事件响应与会话操作\n\nimport Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\n\n在 NoneBot 接收到事件时，事件响应器根据优先级依次通过权限、响应规则来判断当前事件是否应该触发。事件响应流程中，机器人可能会通过 `send` 发送消息或者调用平台接口来执行预期的操作。因此，我们需要对这两种操作进行单元测试。\n\n在上一节中，我们对单个事件响应器进行了简单测试。但是在实际场景中，机器人可能定义了多个事件响应器，由于优先级和响应规则的存在，预期的事件响应器可能并不会被触发。NoneBug 支持同时测试多个事件响应器，以此来测试机器人的整体行为。\n\n## 测试事件响应\n\nNoneBug 提供了六种定义 `Rule` 和 `Permission` 预期行为的方法：\n\n- `should_pass_rule`\n- `should_not_pass_rule`\n- `should_ignore_rule`\n- `should_pass_permission`\n- `should_not_pass_permission`\n- `should_ignore_permission`\n\n:::tip 提示\n事件响应器类型的检查属于 `Permission` 的一部分，因此可以通过 `should_pass_permission` 和 `should_not_pass_permission` 方法来断言事件响应器类型的检查。\n:::\n\n下面我们根据插件示例来测试事件响应行为，我们首先定义两个事件响应器作为测试的对象：\n\n```python title=example.py\nfrom nonebot import on_command\n\ndef never_pass():\n    return False\n\nfoo = on_command(\"foo\")\nbar = on_command(\"bar\", permission=never_pass)\n```\n\n在这两个事件响应器中，`foo` 当收到 `/foo` 消息时会执行，而 `bar` 则不会执行。我们使用 NoneBug 来测试它们：\n\n<Tabs groupId=\"testScope\">\n  <TabItem value=\"separate\" label=\"独立测试\" default>\n\n```python {21,22,28,29} title=tests/test_example.py\nfrom datetime import datetime\n\nimport pytest\nfrom nonebug import App\nfrom nonebot.adapters.console import User, Message, MessageEvent\n\ndef make_event(message: str = \"\") -> MessageEvent:\n    return MessageEvent(\n        time=datetime.now(),\n        self_id=\"test\",\n        message=Message(message),\n        user=User(id=\"user\"),\n    )\n\n@pytest.mark.asyncio\nasync def test_example(app: App):\n    from awesome_bot.plugins.example import foo, bar\n\n    async with app.test_matcher(foo) as ctx:\n        bot = ctx.create_bot()\n        event = make_event(\"/foo\")\n        ctx.receive_event(bot, event)\n        ctx.should_pass_rule()\n        ctx.should_pass_permission()\n\n    async with app.test_matcher(bar) as ctx:\n        bot = ctx.create_bot()\n        event = make_event(\"/foo\")\n        ctx.receive_event(bot, event)\n        ctx.should_not_pass_rule()\n        ctx.should_not_pass_permission()\n```\n\n在上面的代码中，我们分别对 `foo` 和 `bar` 事件响应器进行响应测试。我们使用 `ctx.should_pass_rule` 和 `ctx.should_pass_permission` 断言 `foo` 事件响应器应该被触发，使用 `ctx.should_not_pass_rule` 和 `ctx.should_not_pass_permission` 断言 `bar` 事件响应器应该被忽略。\n\n  </TabItem>\n  <TabItem value=\"global\" label=\"集成测试\">\n\n```python title=tests/test_example.py\nfrom datetime import datetime\n\nimport pytest\nfrom nonebug import App\nfrom nonebot.adapters.console import User, Message, MessageEvent\n\ndef make_event(message: str = \"\") -> MessageEvent:\n    return MessageEvent(\n        time=datetime.now(),\n        self_id=\"test\",\n        message=Message(message),\n        user=User(id=\"user\"),\n    )\n\n@pytest.mark.asyncio\nasync def test_example(app: App):\n    from awesome_bot.plugins.example import foo, bar\n\n    async with app.test_matcher() as ctx:\n        bot = ctx.create_bot()\n        event = make_event(\"/foo\")\n        ctx.receive_event(bot, event)\n        ctx.should_pass_rule(foo)\n        ctx.should_pass_permission(foo)\n        ctx.should_not_pass_rule(bar)\n        ctx.should_not_pass_permission(bar)\n```\n\n在上面的代码中，我们对 `foo` 和 `bar` 事件响应器一起进行响应测试。我们使用 `ctx.should_pass_rule` 和 `ctx.should_pass_permission` 断言 `foo` 事件响应器应该被触发，使用 `ctx.should_not_pass_rule` 和 `ctx.should_not_pass_permission` 断言 `bar` 事件响应器应该被忽略。通过参数，我们可以指定断言的事件响应器。\n\n  </TabItem>\n</Tabs>\n\n当然，如果需要忽略某个事件响应器的响应规则和权限检查，强行进入响应流程，我们可以使用 `should_ignore_rule` 和 `should_ignore_permission` 方法：\n\n```python {21,22} title=tests/test_example.py\nfrom datetime import datetime\n\nimport pytest\nfrom nonebug import App\nfrom nonebot.adapters.console import User, Message, MessageEvent\n\ndef make_event(message: str = \"\") -> MessageEvent:\n    return MessageEvent(\n        time=datetime.now(),\n        self_id=\"test\",\n        message=Message(message),\n        user=User(id=\"user\"),\n    )\n\n@pytest.mark.asyncio\nasync def test_example(app: App):\n    from awesome_bot.plugins.example import foo, bar\n\n    async with app.test_matcher(bar) as ctx:\n        bot = ctx.create_bot()\n        event = make_event(\"/foo\")\n        ctx.receive_event(bot, event)\n        ctx.should_ignore_rule(bar)\n        ctx.should_ignore_permission(bar)\n```\n\n在忽略了响应规则和权限检查之后，就会进入 `bar` 事件响应器的响应流程。\n\n## 测试平台接口使用\n\n上一节的示例插件测试中，我们已经尝试了测试插件对事件的消息回复。通常情况下，事件处理流程中对平台接口的使用会通过事件响应器操作或者调用平台 API 两种途径进行。针对这两种途径，NoneBug 分别提供了 `ctx.should_call_send` 和 `ctx.should_call_api` 方法来测试平台接口的使用情况。\n\n1. `should_call_send`\n\n   定义事件响应器预期发送的消息，即通过[事件响应器操作 send](../../appendices/session-control.mdx#send)进行的操作。`should_call_send` 有四个参数：\n   - `event`：回复的目标事件。\n   - `message`：预期的消息对象，可以是 `str`、`Message` 或 `MessageSegment`。\n   - `result`：send 的返回值，将会返回给插件。\n   - `bot`（可选）：发送消息的 bot 对象。\n   - `**kwargs`：send 方法的额外参数。\n\n2. `should_call_api`\n   定义事件响应器预期调用的平台 API 接口，即通过[调用平台 API](../../appendices/api-calling.mdx#调用平台-api)进行的操作。`should_call_api` 有四个参数：\n   - `api`：API 名称。\n   - `data`：预期的请求数据。\n   - `result`：call_api 的返回值，将会返回给插件。\n   - `adapter`（可选）：调用 API 的平台适配器对象。\n   - `**kwargs`：call_api 方法的额外参数。\n\n下面是一个使用 `should_call_send` 和 `should_call_api` 方法的示例：\n\n我们先定义一个测试插件，在响应流程中向用户发送一条消息并调用 `Console` 适配器的 `bell` API。\n\n```python {8,9} title=example.py\nfrom nonebot import on_command\nfrom nonebot.adapters.console import Bot\n\nfoo = on_command(\"foo\")\n\n@foo.handle()\nasync def _(bot: Bot):\n    await foo.send(\"message\")\n    await bot.bell()\n```\n\n然后我们对该插件进行测试：\n\n```python title=tests/test_example.py\nfrom datetime import datetime\n\nimport pytest\nimport nonebot\nfrom nonebug import App\nfrom nonebot.adapters.console import Bot, User, Adapter, Message, MessageEvent\n\ndef make_event(message: str = \"\") -> MessageEvent:\n    return MessageEvent(\n        time=datetime.now(),\n        self_id=\"test\",\n        message=Message(message),\n        user=User(id=\"user\"),\n    )\n\n@pytest.mark.asyncio\nasync def test_example(app: App):\n    from awesome_bot.plugins.example import foo\n\n    async with app.test_matcher(foo) as ctx:\n        # highlight-start\n        adapter = nonebot.get_adapter(Adapter)\n        bot = ctx.create_bot(base=Bot, adapter=adapter)\n        # highlight-end\n        event = make_event(\"/foo\")\n        ctx.receive_event(bot, event)\n        # highlight-start\n        ctx.should_call_send(event, \"message\", result=None, bot=bot)\n        ctx.should_call_api(\"bell\", {}, result=None, adapter=adapter)\n        # highlight-end\n```\n\n请注意，对于在依赖注入中使用了非基类对象的情况，我们需要在 `create_bot` 方法中指定 `base` 和 `adapter` 参数，确保不会因为重载功能而出现非预期情况。\n\n## 测试会话控制\n\n在[会话控制](../../appendices/session-control.mdx)一节中，我们介绍了如何使用事件响应器操作来实现对用户的交互式会话。在上一节的示例插件测试中，我们其实已经使用了 `ctx.should_finished` 来断言会话结束。NoneBug 针对各种流程控制操作分别提供了相应的方法来定义预期的会话处理行为。它们分别是：\n\n- `should_finished`：断言会话结束，对应 `matcher.finish` 操作。\n- `should_rejected`：断言会话等待用户输入并重新执行当前事件处理函数，对应 `matcher.reject` 系列操作。\n- `should_paused`: 断言会话等待用户输入并执行下一个事件处理函数，对应 `matcher.pause` 操作。\n\n我们仅需在测试用例中的正确位置调用这些方法，就可以断言会话的预期行为。例如：\n\n```python title=example.py\nfrom nonebot import on_command\nfrom nonebot.typing import T_State\n\nfoo = on_command(\"foo\")\n\n@foo.got(\"key\", prompt=\"请输入密码\")\nasync def _(state: T_State, key: str = ArgPlainText()):\n    if key != \"some password\":\n        try_count = state.get(\"try_count\", 1)\n        if try_count >= 3:\n            await foo.finish(\"密码错误次数过多\")\n        else:\n            state[\"try_count\"] = try_count + 1\n            await foo.reject(\"密码错误，请重新输入\")\n    await foo.finish(\"密码正确\")\n```\n\n```python title=tests/test_example.py\nfrom datetime import datetime\n\nimport pytest\nfrom nonebug import App\nfrom nonebot.adapters.console import User, Message, MessageEvent\n\ndef make_event(message: str = \"\") -> MessageEvent:\n    return MessageEvent(\n        time=datetime.now(),\n        self_id=\"test\",\n        message=Message(message),\n        user=User(id=\"user\"),\n    )\n\n@pytest.mark.asyncio\nasync def test_example(app: App):\n    from awesome_bot.plugins.example import foo\n\n    async with app.test_matcher(foo) as ctx:\n        bot = ctx.create_bot()\n        event = make_event(\"/foo\")\n        ctx.receive_event(bot, event)\n        ctx.should_call_send(event, \"请输入密码\", result=None)\n        ctx.should_rejected(foo)\n        event = make_event(\"wrong password\")\n        ctx.receive_event(bot, event)\n        ctx.should_call_send(event, \"密码错误，请重新输入\", result=None)\n        ctx.should_rejected(foo)\n        event = make_event(\"wrong password\")\n        ctx.receive_event(bot, event)\n        ctx.should_call_send(event, \"密码错误，请重新输入\", result=None)\n        ctx.should_rejected(foo)\n        event = make_event(\"wrong password\")\n        ctx.receive_event(bot, event)\n        ctx.should_call_send(event, \"密码错误次数过多\", result=None)\n        ctx.should_finished(foo)\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/best-practice/testing/mock-network.md",
    "content": "---\nsidebar_position: 3\ndescription: 模拟网络通信以进行测试\n---\n\n# 模拟网络通信\n\nNoneBot 驱动器提供了多种方法来帮助适配器进行网络通信，主要包括客户端和服务端两种类型。模拟网络通信可以帮助我们更加接近实际机器人应用场景，进行更加真实的集成测试。同时，通过这种途径，我们还可以完成对适配器的测试。\n\nNoneBot 中的网络通信主要包括以下几种：\n\n- HTTP 服务端（WebHook）\n- WebSocket 服务端\n- HTTP 客户端\n- WebSocket 客户端\n\n下面我们将分别介绍如何使用 NoneBug 来模拟这几种通信方式。\n\n## 测试 HTTP 服务端\n\n当 NoneBot 作为 ASGI 服务端应用时，我们可以定义一系列的路由来处理 HTTP 请求，适配器同样也可以通过定义路由来响应机器人相关的网络通信。下面假设我们使用了一个适配器 `fake` ，它定义了一个路由 `/fake/http` ，用于接收平台 WebHook 并处理。实际应用测试时，应将该路由地址替换为**真实适配器注册的路由地址**。\n\n我们首先需要获取测试用模拟客户端：\n\n```python {5,6} title=tests/test_http_server.py\nfrom nonebug import App\n\n@pytest.mark.asyncio\nasync def test_http_server(app: App):\n    async with app.test_server() as ctx:\n        client = ctx.get_client()\n```\n\n默认情况下，`app.test_server()` 会通过 `nonebot.get_asgi` 获取测试对象，我们也可以通过参数指定 ASGI 应用：\n\n```python\nasync with app.test_server(asgi=asgi_app) as ctx:\n    ...\n```\n\n获取到模拟客户端后，即可像 `requests`、`httpx` 等库类似的方法进行使用：\n\n```python {3,11-14,16} title=tests/test_http_server.py\nimport nonebot\nfrom nonebug import App\nfrom nonebot.adapters.fake import Adapter\n\n@pytest.mark.asyncio\nasync def test_http_server(app: App):\n    adapter = nonebot.get_adapter(Adapter)\n\n    async with app.test_server() as ctx:\n        client = ctx.get_client()\n        response = await client.post(\"/fake/http\", json={\"bot_id\": \"fake\"})\n        assert response.status_code == 200\n        assert response.json() == {\"status\": \"success\"}\n        assert \"fake\" in nonebot.get_bots()\n\n    adapter.bot_disconnect(nonebot.get_bot(\"fake\"))\n```\n\n在上面的测试中，我们向 `/fake/http` 发送了一个模拟 POST 请求，适配器将会对该请求进行处理，我们可以通过检查请求返回是否正确、Bot 对象是否创建等途径来验证机器人是否正确运行。在完成测试后，我们通常需要对 Bot 对象进行清理，以避免对其他测试产生影响。\n\n## 测试 WebSocket 服务端\n\n当 NoneBot 作为 ASGI 服务端应用时，我们还可以定义一系列的路由来处理 WebSocket 通信。下面假设我们使用了一个适配器 `fake` ，它定义了一个路由 `/fake/ws` ，用于处理平台 WebSocket 连接信息。实际应用测试时，应将该路由地址替换为**真实适配器注册的路由地址**。\n\n我们同样需要通过 `app.test_server()` 获取测试用模拟客户端，这里就不再赘述。在获取到模拟客户端后，我们可以通过 `client.websocket_connect` 方法来模拟 WebSocket 连接：\n\n```python {3,11-15} title=tests/test_ws_server.py\nimport nonebot\nfrom nonebug import App\nfrom nonebot.adapters.fake import Adapter\n\n@pytest.mark.asyncio\nasync def test_ws_server(app: App):\n    adapter = nonebot.get_adapter(Adapter)\n\n    async with app.test_server() as ctx:\n        client = ctx.get_client()\n        async with client.websocket_connect(\"/fake/ws\") as ws:\n            await ws.send_json({\"bot_id\": \"fake\"})\n            response = await ws.receive_json()\n            assert response == {\"status\": \"success\"}\n            assert \"fake\" in nonebot.get_bots()\n```\n\n在上面的测试中，我们向 `/fake/ws` 进行了 WebSocket 模拟通信，通过发送消息与机器人进行交互，然后检查机器人发送的信息是否正确。\n\n## 测试 HTTP 客户端\n\n~~暂不支持~~\n\n## 测试 WebSocket 客户端\n\n~~暂不支持~~\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/community/contact.md",
    "content": "---\nsidebar-position: 0\ndescription: 遇到问题如何获取帮助\n---\n\n# 参与讨论\n\n如果在安装或者开发 NoneBot 过程中遇到了任何问题，或者有新奇的点子，欢迎参与我们的社区讨论：\n\n1. 点击下方链接前往 GitHub，前往 Issues 页面，在 `New Issue` Template 中选择 `Question`\n\n   NoneBot：[![NoneBot project link](https://img.shields.io/github/stars/nonebot/nonebot2?style=social)](https://github.com/nonebot/nonebot2)\n\n2. 通过 QQ 群（点击下方链接直达）\n\n   [![QQ Chat Group](https://img.shields.io/badge/QQ%E7%BE%A4-768887710-orange?style=social)](https://jq.qq.com/?_wv=1027&k=5OFifDh)\n\n3. 通过 QQ 频道\n\n   [![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)\n\n4. 通过 Discord 服务器（点击下方链接直达）\n\n   [![Discord Server](https://discordapp.com/api/guilds/847819937858584596/widget.png?style=shield)](https://discord.gg/VKtE6Gdc4h)\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/community/contributing.md",
    "content": "---\nsidebar-position: 1\ndescription: 如何为 NoneBot 贡献代码\n---\n\n# 贡献指南\n\n## Code of Conduct\n\n请参阅 [Code of Conduct](https://github.com/nonebot/nonebot2/blob/master/CODE_OF_CONDUCT.md)。\n\n## 参与开发\n\n请参阅 [Contributing](https://github.com/nonebot/nonebot2/blob/master/CONTRIBUTING.md)。\n\n## 鸣谢\n\n感谢以下开发者对 NoneBot2 作出的贡献：\n\n<a href=\"https://github.com/nonebot/nonebot2/graphs/contributors\">\n  <img src=\"https://contrib.rocks/image?repo=nonebot/nonebot2&max=1000\" />\n</a>\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/developer/adapter-writing.md",
    "content": "---\nsidebar_position: 1\ndescription: 编写适配器对接新的平台\n---\n\n# 编写适配器\n\n在编写适配器之前，我们需要先了解[适配器的功能与组成](../advanced/adapter#适配器功能与组成)，适配器通常由 `Adapter`、`Bot`、`Event` 和 `Message` 四个部分组成，在编写适配器时，我们需要继承 NoneBot 中的基类，并根据实际平台来编写每个部分功能。\n\n## 组织结构\n\nNoneBot 适配器项目通常以 `nonebot-adapter-{adapter-name}` 作为项目名，并以**命名空间包**的形式编写，即在 `nonebot/adapters/{adapter-name}` 目录中编写实际代码，例如：\n\n```tree\n📦 nonebot-adapter-{adapter-name}\n├── 📂 nonebot\n│   ├── 📂 adapters\n│   │   ├── 📂 {adapter-name}\n│   │   │   ├── 📜 __init__.py\n│   │   │   ├── 📜 adapter.py\n│   │   │   ├── 📜 bot.py\n│   │   │   ├── 📜 config.py\n│   │   │   ├── 📜 event.py\n│   │   │   └── 📜 message.py\n├── 📜 pyproject.toml\n└── 📜 README.md\n```\n\n:::tip 提示\n\n上述的项目结构仅作推荐，不做强制要求，保证实际可用性即可。\n\n:::\n\n### 使用 NB-CLI 创建项目\n\n我们可以使用脚手架快速创建项目：\n\n```shell\nnb adapter create\n```\n\n按照指引，输入适配器名称以及存储位置，即可创建一个带有基本结构的适配器项目。\n\n## 组成部分\n\n:::tip 提示\n\n本章节的代码中提到的 `Adapter`、`Bot`、`Event` 和 `Message` 等，均为下文中适配器所编写的类，而非 NoneBot 中的基类。\n\n:::\n\n### Log\n\n适配器在处理时通常需要打印日志，但直接使用 NoneBot 的默认 `logger` 不方便区分适配器输出和其它日志。因此我们可以使用 NoneBot 提供的 `logger_wrapper` 方法，自定义一个 `log` 函数用于快捷打印适配器日志：\n\n```python {3} title=log.py\nfrom nonebot.utils import logger_wrapper\n\nlog = logger_wrapper(\"your_adapter_name\")\n```\n\n这个 `log` 函数会在默认 `logger` 中添加适配器名称前缀，它接收三个参数：日志等级、日志内容以及可选的异常，具体用法如下：\n\n```python\nfrom .log import log\n\nlog(\"DEBUG\", \"A DEBUG log.\")\nlog(\"INFO\", \"A INFO log.\")\n\ntry:\n    ...\nexcept Exception as e:\n    log(\"ERROR\", \"something error.\", e)\n```\n\n### Config\n\n通常适配器需要一些配置项，例如平台连接密钥等。适配器的配置方法与[插件配置](../appendices/config#%E6%8F%92%E4%BB%B6%E9%85%8D%E7%BD%AE)类似，例如：\n\n```python title=config.py\nfrom pydantic import BaseModel\n\nclass Config(BaseModel):\n    xxx_id: str\n    xxx_token: str\n```\n\n配置项的读取将在下方 [Adapter](#adapter) 中介绍。\n\n### Adapter\n\nAdapter 负责转换事件、调用接口，以及正确创建 Bot 对象并注册到 NoneBot 中。在编写平台相关内容之前，我们需要继承基类，并实现适配器的基本信息：\n\n```python {9,11,14,18} title=adapter.py\nfrom typing import Any\nfrom typing_extensions import override\n\nfrom nonebot.drivers import Driver\nfrom nonebot import get_plugin_config\nfrom nonebot.adapters import Adapter as BaseAdapter\n\nfrom .config import Config\n\nclass Adapter(BaseAdapter):\n    @override\n    def __init__(self, driver: Driver, **kwargs: Any):\n        super().__init__(driver, **kwargs)\n        # 读取适配器所需的配置项\n        self.adapter_config: Config = get_plugin_config(Config)\n\n    @classmethod\n    @override\n    def get_name(cls) -> str:\n        \"\"\"适配器名称\"\"\"\n        return \"your_adapter_name\"\n```\n\n#### 与平台交互\n\nNoneBot 提供了多种 [Driver](../advanced/driver) 来帮助适配器进行网络通信，主要分为客户端和服务端两种类型。我们需要**根据平台文档和特性**选择合适的通信方式，并编写相关方法用于初始化适配器，与平台建立连接和进行交互：\n\n##### 客户端通信方式\n\n```python {12,23,24} title=adapter.py\nimport asyncio\nfrom typing_extensions import override\n\nfrom nonebot import get_plugin_config\nfrom nonebot.exception import WebSocketClosed\nfrom nonebot.drivers import Request, WebSocketClientMixin\n\nclass Adapter(BaseAdapter):\n    @override\n    def __init__(self, driver: Driver, **kwargs: Any):\n        super().__init__(driver, **kwargs)\n        self.adapter_config: Config = get_plugin_config(Config)\n        self.task: Optional[asyncio.Task] = None  # 存储 ws 任务\n        self.setup()\n\n    def setup(self) -> None:\n        if not isinstance(self.driver, WebSocketClientMixin):\n            # 判断用户配置的Driver类型是否符合适配器要求，不符合时应抛出异常\n            raise RuntimeError(\n                f\"Current driver {self.config.driver} doesn't support websocket client connections!\"\n                f\"{self.get_name()} Adapter need a WebSocket Client Driver to work.\"\n            )\n        # 在 NoneBot 启动和关闭时进行相关操作\n        self.driver.on_startup(self.startup)\n        self.driver.on_shutdown(self.shutdown)\n\n    async def startup(self) -> None:\n        \"\"\"定义启动时的操作，例如和平台建立连接\"\"\"\n        self.task = asyncio.create_task(self._forward_ws())  # 建立 ws 连接\n\n    async def _forward_ws(self):\n        request = Request(\n            method=\"GET\",\n            url=\"your_platform_websocket_url\",\n            headers={\"token\": \"...\"},  # 鉴权请求头\n        )\n        while True:\n            try:\n                async with self.websocket(request) as ws:\n                    try:\n                        # 处理 websocket\n                        ...\n                    except WebSocketClosed as e:\n                        log(\n                            \"ERROR\",\n                            \"<r><bg #f8bbd0>WebSocket Closed</bg #f8bbd0></r>\",\n                            e,\n                        )\n                    except Exception as e:\n                        log(\n                            \"ERROR\",\n                            \"<r><bg #f8bbd0>Error while process data from \"\n                            \"websocket platform_websocket_url. \"\n                            \"Trying to reconnect...</bg #f8bbd0></r>\",\n                            e,\n                        )\n                    finally:\n                        # 这里要断开 Bot 连接\n            except Exception as e:\n                # 尝试重连\n                log(\n                    \"ERROR\",\n                    \"<r><bg #f8bbd0>Error while setup websocket to \"\n                    \"platform_websocket_url. Trying to reconnect...</bg #f8bbd0></r>\",\n                    e,\n                )\n                await asyncio.sleep(3)  # 重连间隔\n\n    async def shutdown(self) -> None:\n        \"\"\"定义关闭时的操作，例如停止任务、断开连接\"\"\"\n\n        # 断开 ws 连接\n        if self.task is not None and not self.task.done():\n            self.task.cancel()\n```\n\n##### 服务端通信方式\n\n```python {30,38} title=adapter.py\nfrom nonebot import get_plugin_config\nfrom nonebot.drivers import (\n    Request,\n    ASGIMixin,\n    WebSocket,\n    HTTPServerSetup,\n    WebSocketServerSetup\n)\n\nclass Adapter(BaseAdapter):\n    @override\n    def __init__(self, driver: Driver, **kwargs: Any):\n        super().__init__(driver, **kwargs)\n        self.adapter_config: Config = get_plugin_config(Config)\n        self.setup()\n\n    def setup(self) -> None:\n        if not isinstance(self.driver, ASGIMixin):\n            raise RuntimeError(\n                f\"Current driver {self.config.driver} doesn't support asgi server!\"\n                f\"{self.get_name()} Adapter need a asgi server driver to work.\"\n            )\n        # 建立服务端路由\n        # HTTP Webhook 路由\n        http_setup = HTTPServerSetup(\n            URL(\"your_webhook_url\"),  # 路由地址\n            \"POST\",  # 接收的方法\n            \"WEBHOOK name\",  # 路由名称\n            self._handle_http,  # 处理函数\n        )\n        self.setup_http_server(http_setup)\n\n        # 反向 Websocket 路由\n        ws_setup = WebSocketServerSetup(\n            URL(\"your_websocket_url\"),  # 路由地址\n            \"WebSocket name\",  # 路由名称\n            self._handle_ws,  # 处理函数\n        )\n        self.setup_websocket_server(ws_setup)\n\n\n    async def _handle_http(self, request: Request) -> Response:\n        \"\"\"HTTP 路由处理函数，只有一个类型为 Request 的参数，且返回值类型为 Response\"\"\"\n        ...\n        return Response(\n            status_code=200,  # 状态码\n            headers={\"something\": \"something\"},  # 响应头\n            content=\"xxx\",  # 响应内容\n        )\n\n    async def _handle_ws(self, websocket: WebSocket) -> Any:\n        \"\"\"WebSocket 路由处理函数，只有一个类型为 WebSocket 的参数\"\"\"\n        ...\n```\n\n更多通信交互方式可以参考以下适配器：\n\n- [OneBot](https://github.com/nonebot/adapter-onebot/blob/master/nonebot/adapters/onebot/v11/adapter.py) - `WebSocket 客户端`、`WebSocket 服务端`、`HTTP WEBHOOK`、`HTTP POST`\n- [QQ](https://github.com/nonebot/adapter-qq/blob/master/nonebot/adapters/qq/adapter.py) - `WebSocket 服务端`、`HTTP WEBHOOK`\n- [Telegram](https://github.com/nonebot/adapter-telegram/blob/beta/nonebot/adapters/telegram/adapter.py) - `HTTP WEBHOOK`\n\n#### 建立 Bot 连接\n\n在与平台建立连接后，我们需要将 [Bot](#bot) 实例化，并调用适配器提供的的 `bot_connect` 方法告知 NoneBot 建立了 Bot 连接。在与平台断开连接或出现某些异常进行重连时，我们需要调用 `bot_disconnect` 方法告知 NoneBot 断开了 Bot 连接。\n\n```python {7,8,11} title=adapter.py\nfrom .bot import Bot\n\nclass Adapter(BaseAdapter):\n\n    def _handle_connect(self):\n        bot_id = ...  # 通过配置或者平台 API 等方式，获取到 Bot 的 ID\n        bot = Bot(self, self_id=bot_id)  # 实例化 Bot\n        self.bot_connect(bot)  # 建立 Bot 连接\n\n    def _handle_disconnect(self):\n        self.bot_disconnect(bot)  # 断开 Bot 连接\n```\n\n#### 转换 Event 事件\n\n在接收到来自平台的事件数据后，我们需要将其转为适配器的 [Event](#event)，并调用 Bot 的 `handle_event` 方法来让 Bot 对事件进行处理：\n\n```python title=adapter.py\nimport asyncio\nfrom typing import Any, Dict\n\nfrom nonebot.compat import type_validate_python\n\nfrom .bot import Bot\nfrom .event import Event\nfrom .log import log\n\nclass Adapter(BaseAdapter):\n\n    @classmethod\n    def payload_to_event(cls, payload: Dict[str, Any]) -> Event:\n        \"\"\"根据平台事件的特性，转换平台 payload 为具体 Event\n\n        Event 模型继承自 pydantic.BaseModel，具体请参考 pydantic 文档\n        \"\"\"\n\n        # 做一层异常处理，以应对平台事件数据的变更\n        try:\n            return type_validate_python(your_event_class, payload)\n        except Exception as e:\n            # 无法正常解析为具体 Event 时，给出日志提示\n            log(\n                \"WARNING\",\n                f\"Parse event error: {str(payload)}\",\n            )\n            # 也可以尝试转为基础 Event 进行处理\n            return type_validate_python(Event, payload)\n\n\n    async def _forward(self, bot: Bot):\n\n        payload: Dict[str, Any]  # 接收到的事件数据\n        event = self.payload_to_event(payload)\n        # 让 bot 对事件进行处理\n        asyncio.create_task(bot.handle_event(event))\n```\n\n#### 调用平台 API\n\n我们需要实现 `Adapter` 的 `_call_api` 方法，使开发者能够调用平台提供的 API。如果通过 WebSocket 通信可以通过 `send` 方法来发送数据，如果采用 HTTP 请求，则需要通过 NoneBot 提供的 `Request` 对象，调用 `driver` 的 `request` 方法来发送请求。\n\n```python {11} title=adapter.py\nfrom typing import Any\nfrom typing_extensions import override\n\nfrom nonebot.drivers import Request, WebSocket\n\nfrom .bot import Bot\n\nclass Adapter(BaseAdapter):\n\n    @override\n    async def _call_api(self, bot: Bot, api: str, **data: Any) -> Any:\n        log(\"DEBUG\", f\"Calling API <y>{api}</y>\")  # 给予日志提示\n        platform_data = your_handle_data_method(data)  # 自行将数据转为平台所需要的格式\n\n        # 采用 HTTP 请求的方式，需要构造一个 Request 对象\n        request = Request(\n            method=\"GET\",  # 请求方法\n            url=api,  # 接口地址\n            headers=...,  # 请求头，通常需要包含鉴权信息\n            params=platform_data,  # 自行处理数据的传输形式\n            # json=platform_data,\n            # data=platform_data,\n        )\n        # 发送请求，返回结果\n        return await self.driver.request(request)\n\n\n        # 采用 WebSocket 通信的方式，可以直接调用 send 方法发送数据\n        # 通过某种方式获取到 bot 对应的 websocket 对象\n        ws: WebSocket = your_get_websocket_method(bot.self_id)\n\n        await ws.send_text(platform_data)  # 发送 str 类型的数据\n        await ws.send_bytes(platform_data)  # 发送 bytes 类型的数据\n        await ws.send(platform_data)  # 是以上两种方式的合体\n\n        # 接收并返回结果，同样的，也有 str 和 bytes 的区别\n        return await ws.receive_text()\n        return await ws.receive_bytes()\n        return await ws.receive()\n```\n\n`调用平台 API` 实现方式具体可以参考以下适配器：\n\nWebsocket:\n\n- [OneBot V11](https://github.com/nonebot/adapter-onebot/blob/54270edbbdb2a71332d744f90b1a3d7f4bf6463a/nonebot/adapters/onebot/v11/adapter.py#L167-L177)\n- [OneBot V12](https://github.com/nonebot/adapter-onebot/blob/54270edbbdb2a71332d744f90b1a3d7f4bf6463a/nonebot/adapters/onebot/v12/adapter.py#L204-L218)\n\nHTTP:\n\n- [OneBot V11](https://github.com/nonebot/adapter-onebot/blob/54270edbbdb2a71332d744f90b1a3d7f4bf6463a/nonebot/adapters/onebot/v11/adapter.py#L179-L215)\n- [OneBot V12](https://github.com/nonebot/adapter-onebot/blob/54270edbbdb2a71332d744f90b1a3d7f4bf6463a/nonebot/adapters/onebot/v12/adapter.py#L220-L266)\n- [QQ](https://github.com/nonebot/adapter-qq/blob/dc5d437e101f0e3db542de3300758a035ed7036e/nonebot/adapters/qq/adapter.py#L599-L605)\n- [Telegram](https://github.com/nonebot/adapter-telegram/blob/4a8633627e619245516767f5503dec2f58fe2193/nonebot/adapters/telegram/adapter.py#L148-L253)\n- [飞书](https://github.com/nonebot/adapter-feishu/blob/f8ab05e6d57a5e9013b944b0d019ca777725dfb0/nonebot/adapters/feishu/adapter.py#L201-L218)\n\n### Bot\n\nBot 是机器人开发者能够直接获取并使用的核心对象，负责存储平台机器人相关信息，并提供回复事件、调用 API 的上层方法。我们需要继承基类 `Bot`，并实现相关方法：\n\n```python {20,25,34} title=bot.py\nfrom typing import TYPE_CHECKING, Any, Union\nfrom typing_extensions import override\n\nfrom nonebot.message import handle_event\nfrom nonebot.adapters import Bot as BaseBot\n\nfrom .event import Event\nfrom .message import Message, MessageSegment\n\nif TYPE_CHECKING:\n    from .adapter import Adapter\n\n\nclass Bot(BaseBot):\n    \"\"\"\n    your_adapter_name 协议 Bot 适配。\n    \"\"\"\n\n    @override\n    def __init__(self, adapter: Adapter, self_id: str, **kwargs: Any):\n        super().__init__(adapter, self_id)\n        self.adapter: Adapter = adapter\n        # 一些有关 Bot 的信息也可以在此定义和存储\n\n    async def handle_event(self, event: Event):\n        # 根据需要，对事件进行某些预处理，例如：\n        # 检查事件是否和机器人有关操作，去除事件消息首尾的 @bot\n        # 检查事件是否有回复消息，调用平台 API 获取原始消息的消息内容\n        ...\n        # 调用 handle_event 让 NoneBot 对事件进行处理\n        await handle_event(self, event)\n\n    @override\n    async def send(\n        self,\n        event: Event,\n        message: Union[str, Message, MessageSegment],\n        **kwargs: Any,\n    ) -> Any:\n        # 根据平台实现 Bot 回复事件的方法\n\n        # 将消息处理为平台所需的格式后，调用发送消息接口进行发送，例如：\n        data = message_to_platform_data(message)\n        await self.send_message(\n            data=data,\n            ...\n        )\n```\n\n### Event\n\nEvent 是 NoneBot 中的事件主体对象，所有平台消息在进入处理流程前需要转换为 NoneBot 事件。我们需要继承基类 `Event`，并实现相关方法：\n\n```python {5,8,13,18,23,28,33} title=event.py\nfrom typing_extensions import override\n\nfrom nonebot.compat import model_dump\nfrom nonebot.adapters import Event as BaseEvent\n\nclass Event(BaseEvent):\n\n    @override\n    def get_event_name(self) -> str:\n        # 返回事件的名称，用于日志打印\n        return \"event name\"\n\n    @override\n    def get_event_description(self) -> str:\n        # 返回事件的描述，用于日志打印，请注意转义 loguru tag\n        return escape_tag(repr(model_dump(self)))\n\n    @override\n    def get_message(self):\n        # 获取事件消息的方法，根据事件具体实现，如果事件非消息类型事件，则抛出异常\n        raise ValueError(\"Event has no message!\")\n\n    @override\n    def get_user_id(self) -> str:\n        # 获取用户 ID 的方法，根据事件具体实现，如果事件没有用户 ID，则抛出异常\n        raise ValueError(\"Event has no context!\")\n\n    @override\n    def get_session_id(self) -> str:\n        # 获取事件会话 ID 的方法，根据事件具体实现，如果事件没有相关 ID，则抛出异常\n        raise ValueError(\"Event has no context!\")\n\n    @override\n    def is_tome(self) -> bool:\n        # 判断事件是否和机器人有关\n        return False\n```\n\n然后根据平台消息的类型，编写各种不同的事件，并且注意要根据事件类型实现 `get_type` 方法，具体请参考[事件类型](../advanced/adapter#事件类型)。消息类型事件还应重写 `get_message` 和 `get_user_id` 等方法，例如：\n\n```python {7,16,20,25,34,42} title=event.py\nfrom .message import Message\n\nclass HeartbeatEvent(Event):\n    \"\"\"心跳时间，通常为元事件\"\"\"\n\n    @override\n    def get_type(self) -> str:\n        return \"meta_event\"\n\nclass MessageEvent(Event):\n    \"\"\"消息事件\"\"\"\n    message_id: str\n    user_id: str\n\n    @override\n    def get_type(self) -> str:\n        return \"message\"\n\n    @override\n    def get_message(self) -> Message:\n        # 返回事件消息对应的 NoneBot Message 对象\n        return self.message\n\n    @override\n    def get_user_id(self) -> str:\n        return self.user_id\n\nclass JoinRoomEvent(Event):\n    \"\"\"加入房间事件，通常为通知事件\"\"\"\n    user_id: str\n    room_id: str\n\n    @override\n    def get_type(self) -> str:\n        return \"notice\"\n\nclass ApplyAddFriendEvent(Event):\n    \"\"\"申请添加好友事件，通常为请求事件\"\"\"\n    user_id: str\n\n    @override\n    def get_type(self) -> str:\n        return \"request\"\n```\n\n### Message\n\nMessage 负责正确序列化消息，以便机器人插件处理。我们需要继承 `MessageSegment` 和 `Message` 两个类，并实现相关方法：\n\n```python {9,12,17,22,27,30,36} title=message.py\nfrom typing import Type, Iterable\nfrom typing_extensions import override\n\nfrom nonebot.utils import escape_tag\n\nfrom nonebot.adapters import Message as BaseMessage\nfrom nonebot.adapters import MessageSegment as BaseMessageSegment\n\nclass MessageSegment(BaseMessageSegment[\"Message\"]):\n    @classmethod\n    @override\n    def get_message_class(cls) -> Type[\"Message\"]:\n        # 返回适配器的 Message 类型本身\n        return Message\n\n    @override\n    def __str__(self) -> str:\n        # 返回该消息段的纯文本表现形式，通常在日志中展示\n        return \"text of MessageSegment\"\n\n    @override\n    def is_text(self) -> bool:\n        # 判断该消息段是否为纯文本\n        return self.type == \"text\"\n\n\nclass Message(BaseMessage[MessageSegment]):\n    @classmethod\n    @override\n    def get_segment_class(cls) -> Type[MessageSegment]:\n        # 返回适配器的 MessageSegment 类型本身\n        return MessageSegment\n\n    @staticmethod\n    @override\n    def _construct(msg: str) -> Iterable[MessageSegment]:\n        # 实现从字符串中构造消息数组，如无字符串嵌入格式可直接返回文本类型 MessageSegment\n        ...\n```\n\n然后根据平台具体的消息类型，来实现各种 `MessageSegment` 消息段，具体可以参考以下适配器：\n\n- [OneBot V11](https://github.com/nonebot/adapter-onebot/blob/54270edbbdb2a71332d744f90b1a3d7f4bf6463a/nonebot/adapters/onebot/v11/message.py#L25-L259)\n- [QQ](https://github.com/nonebot/adapter-qq/blob/dc5d437e101f0e3db542de3300758a035ed7036e/nonebot/adapters/qq/message.py#L30-L520)\n- [Telegram](https://github.com/nonebot/adapter-telegram/blob/4a8633627e619245516767f5503dec2f58fe2193/nonebot/adapters/telegram/message.py#L13-L414)\n\n## 适配器测试\n\n关于适配器测试相关内容在这里不再展开，开发者可以根据需要进行合适的测试。这里为开发者提供几个常见问题的解决方法：\n\n1. 在测试中无法导入 editable 模式安装的适配器代码。在 pytest 的 `conftest.py` 内添加如下代码：\n\n   ```python title=tests/conftest.py\n   from pathlib import Path\n   import nonebot.adapters\n   nonebot.adapters.__path__.append(  # type: ignore\n       str((Path(__file__).parent.parent / \"nonebot\" / \"adapters\").resolve())\n   )\n   ```\n\n2. 需要计算适配器测试覆盖率，请在 `pyproject.toml` 中添加 pytest 配置：\n\n   ```toml title=pyproject.toml\n   [tool.pytest.ini_options]\n   addopts = \"--cov nonebot/adapters/{adapter-name} --cov-report term-missing\"\n   ```\n\n## 后续工作\n\n在完成适配器代码的编写后，如果想要将适配器发布到 NoneBot 商店，我们需要将适配器发布到 PyPI 中，然后前往[商店](/store/adapters)页面，切换到适配器页签，点击**发布适配器**按钮，填写适配器相关信息并提交。\n\n另外建议编写适配器文档或者一些插件开发示例，以便其他开发者使用我们的适配器。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/developer/plugin-publishing.mdx",
    "content": "---\nsidebar_position: 0\ndescription: 在商店发布自己的插件\n---\n\n# 发布插件\n\nimport Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\n\nNoneBot 为开发者提供了分享插件的官方商店。本指南囊括**从创建项目到发布到 PyPI，最终提交商店审核**的全过程。\n\n:::warning 警告\n如果你的插件只是满足自用需求，则完全可以选择**不发布插件**。发布插件**不是**使用插件的必要条件。\n\nNoneBot 社区对于插件有一定质量要求，对于不符合要求的插件，社区成员将会要求修改，直至符合要求后才能通过审核；如果长期未更新修改，社区将会关闭当前请求，之后如需发布请重新提交发布插件请求。相应的要求会在本章节以下部分介绍。\n:::\n\n:::tip 提示\n本章节仅包含插件发布流程指导，插件开发请查阅前述章节。\n:::\n\n## 准备工作\n\n### 插件命名规范\n\nNoneBot 插件使用下述命名规范：\n\n- 对于**项目名**，建议统一以 `nonebot-plugin-` 开头，之后为拟定的插件名字，词间用横杠 `-` 分隔；\n  - **项目名**用于代码仓库名称、PyPI 包的发布名称等；\n  - 本文使用 `nonebot-plugin-{your-plugin-name}` 为例。\n- 对于**模块名**，建议与**项目名**一致，但词间用下划线 `_` 分隔，即统一以 `nonebot_plugin_` 开头，之后为拟定的插件名字；\n  - **模块名**用于程序导入使用，应为插件文件（夹）的名称；\n  - 本文使用 `nonebot_plugin_{your_plugin_name}` 为例。\n\n### 项目结构\n\n:::tip 提示\n本段所述的项目结构仅作推荐，不做强制要求。\n:::\n\n插件程序本身结构可参考[插件结构](../tutorial/create-plugin.md#插件结构)一节，唯一区别在于，插件包可以直接处于项目顶层。\n\n插件项目的一种组织结构如下：\n\n```tree\n📦 nonebot-plugin-{your-plugin-name}\n├── 📂 nonebot_plugin_{your_plugin_name}\n│   ├── 📜 __init__.py\n│   └── 📜 config.py\n├── 📜 pyproject.toml\n└── 📜 README.md\n```\n\n功能开发可以在 `__init__.py` 中进行或在包内部创建其他模块并在 `__init__.py` 中导入。\n\n### 从项目模板开始\n\n为降低新手门槛，我们提供三条清晰、完整、可复制的发布路径。\n\n:::tip 提示\n你只需选择一条与你习惯一致的路径，**完整跟随即可成功发布**。无需在不同工具间切换或猜测配置。\n:::\n\nNoneBot 生态目前有如下插件项目模板：\n\n- [RF-Tar-Railt/nonebot-plugin-template](https://github.com/RF-Tar-Railt/nonebot-plugin-template)\n\n  此路径使用 **PDM** 项目管理器，符合 PEP 621 标准，自动化程度高。\n\n- [fllesser/nonebot-plugin-template](https://github.com/fllesser/nonebot-plugin-template)\n\n  此路径使用 **uv** 项目管理器和 **PoeThePoet** 任务运行器，构建速度快，适合追求效率的开发者。\n\n- [A-kirami/nonebot-plugin-template](https://github.com/A-kirami/nonebot-plugin-template)\n\n  此路径使用 **Poetry** 项目管理器，适合熟悉传统 Python 生态的开发者。\n\n#### 1. 创建项目\n\n1. 访问上述三个模板之一。\n2. 点击 **“Use this template”** → **“Create a new repository”**。\n3. 仓库名称填写：`nonebot-plugin-{your-plugin-name}`（此部分以 `nonebot-plugin-weather` 为例）。\n4. 点击 **“Create repository from template”**。\n\n#### 2. 配置发布权限\n\n1. 进入新仓库 → **Settings** → **Actions** → **General**。\n2. 在 **Workflow permissions** 下，勾选 **“Read and write permissions”** → 点击 **Save**。\n\n#### 3. 全局替换项目信息\n\n在仓库中点击 **“Add file”** → **“Create new file”**，创建一个空文件 `LICENSE`，选择开源协议并提交（此操作会触发工作流）。\n\n然后在本地克隆仓库，使用编辑器对以下内容进行**全局替换**：\n\n:::tip 提示\n此部分以“天气插件”为例，实际的替换内容应该根据你所创建的插件名称等相应调整。\n:::\n\n| 原内容                         | 替换为                             |\n| ------------------------------ | ---------------------------------- |\n| `nonebot-plugin-template`      | `nonebot-plugin-weather`           |\n| `nonebot_plugin_template`      | `nonebot_plugin_weather`           |\n| `<your_plugin_humanized_name>` | `天气查询`                         |\n| `<your_plugin_description>`    | `查询指定城市的实时天气与未来预报` |\n| `<your_github>`                | `你的GitHub用户名`                 |\n| `<your_email>`                 | `你的邮箱`                         |\n\n#### 4. 安装依赖与开发\n\n<Tabs groupId=\"publish-path\" defaultValue=\"pdm\" values={[\n  {label: 'PDM + RF-Tar-Railt 模板', value: 'pdm'},\n  {label: 'uv + fllesser 模板', value: 'uv'},\n  {label: 'Poetry + A-kirami 模板', value: 'poetry'},\n]}>\n  <TabItem value=\"pdm\" label=\"PDM + RF-Tar-Railt 模板\">\n\n```bash\n# 安装 PDM（若未安装）\ncurl -sSL https://pdm-project.org/install-pdm.py | python3 -\n\n# 安装项目依赖（自动创建虚拟环境）\npdm sync\n\n# 添加新依赖（如 httpx）\npdm add httpx\n```\n\n</TabItem>\n\n  <TabItem value=\"uv\" label=\"uv + fllesser 模板\">\n\n```bash\n# 安装 uv（Windows）\npowershell -c \"irm https://astral.sh/uv/install.ps1 | iex\"\n\n# 安装 uv（macOS/Linux）\ncurl -LsSf https://astral.sh/uv/install.sh | sh\n\n# 安装所有依赖（含 dev）\nuv sync --all-groups -p 3.12\n\n# 添加新依赖\nuv add httpx\n```\n\n</TabItem>\n\n  <TabItem value=\"poetry\" label=\"Poetry + A-kirami 模板\">\n\n```bash\n# 安装 Poetry（推荐方式）\ncurl -sSL https://install.python-poetry.org | python3 -\n\n# 安装项目依赖\npoetry install\n\n# 添加新依赖\npoetry add httpx\n```\n\n</TabItem>\n</Tabs>\n\n#### 5. 更新版本并发布\n\n<Tabs\n  groupId=\"publish-path-bump\"\n  defaultValue=\"bump-my-version\"\n  values={[\n    { label: \"使用 bump-my-version\", value: \"bump-my-version\" },\n    { label: \"使用项目管理器\", value: \"bump-manager\" },\n    { label: \"手动更新版本\", value: \"bump-manual\" },\n  ]}\n>\n  <TabItem value=\"bump-my-version\" label=\"使用 bump-my-version\">\n\n[bump-my-version](https://github.com/callowayproject/bump-my-version) 是一个功能强大、可配置的 Python 项目版本更新工具，支持自动提交到 Git 等 VCS。\n\n<Tabs groupId=\"publish-path\" defaultValue=\"pdm\" values={[\n  {label: 'PDM + RF-Tar-Railt 模板', value: 'pdm'},\n  {label: 'uv + fllesser 模板', value: 'uv'},\n  {label: 'Poetry + A-kirami 模板', value: 'poetry'},\n]}>\n  <TabItem value=\"pdm\" label=\"PDM + RF-Tar-Railt 模板\">\n\n```bash\n# 安装 bump-my-version\npdm add --dev bump-my-version\n\n# 更新 patch 版本\npdm run bump patch\n\n# 推送 tag 触发发布\ngit push origin --tags\n```\n\n</TabItem>\n\n  <TabItem value=\"uv\" label=\"uv + fllesser 模板\">\n\n```bash\n# 更新 patch 版本\nuv run poe bump patch\n\n# 推送 tag 触发发布\ngit push origin --tags\n```\n\n</TabItem>\n\n  <TabItem value=\"poetry\" label=\"Poetry + A-kirami 模板\">\n\n```bash\n# 安装 bump-my-version\npoetry add --dev bump-my-version\n\n# 更新 patch 版本\npoetry run bump patch\n\n# 推送 tag 触发发布\ngit push origin --tags\n```\n\n</TabItem>\n</Tabs>\n\n</TabItem>\n  <TabItem value=\"bump-manager\" label=\"使用项目管理器\">\n\n<Tabs groupId=\"publish-path\" defaultValue=\"pdm\" values={[\n  {label: 'PDM + RF-Tar-Railt 模板', value: 'pdm'},\n  {label: 'uv + fllesser 模板', value: 'uv'},\n  {label: 'Poetry + A-kirami 模板', value: 'poetry'},\n]}>\n  <TabItem value=\"pdm\" label=\"PDM + RF-Tar-Railt 模板\">\n\n需要安装 PDM 插件 [pdm-bump](https://github.com/carstencodes/pdm-bump)。\n\n```bash\n# 安装 pdm-bump\npdm self add pdm-bump\n\n# 更新 patch 版本\npdm bump patch\n\n# 推送 tag 触发发布\ngit push origin --tags\n```\n\n</TabItem>\n\n  <TabItem value=\"uv\" label=\"uv + fllesser 模板\">\n\n```bash\n# 更新 patch 版本\nuv version --bump patch\n\n# 创建相应提交与标签\ngit add pyproject.toml\ngit commit -m \"chore: release v0.1.1\"  # 替换为实际的版本号\ngit tag v0.1.1  # 替换为实际的版本号\n\n# 推送 tag 触发发布\ngit push origin --tags\n```\n\n</TabItem>\n\n  <TabItem value=\"poetry\" label=\"Poetry + A-kirami 模板\">\n\n```bash\n# 更新版本（自动提交并打标签）\npoetry version patch\n\n# 推送 tag 触发发布\ngit push origin --tags\n```\n\n</TabItem>\n</Tabs>\n\n</TabItem>\n  <TabItem value=\"bump-manual\" label=\"手动更新版本\">\n\n手动更新 `pyproject.toml` 中的 `version` 字段，然后推送 tag 触发发布工作流\n\n```bash\ngit add pyproject.toml\ngit commit -m \"chore: release v0.1.1\"  # 替换为实际的版本号\ngit tag v0.1.1  # 替换为实际的版本号\ngit push origin --tags\n```\n\n</TabItem>\n</Tabs>\n\n推送 `v*` 标签后，模板提供的 GitHub Actions 工作流将自动构建并发布到 PyPI。\n\n#### 6. 发布到 [PyPI](https://pypi.org)\n\n<Tabs groupId=\"publish-method\" defaultValue=\"template\" values={[\n  {label: '使用模板的自动发布工作流', value: 'template'},\n  {label: '手动发布', value: 'manual'},\n]}>\n  <TabItem value=\"template\" label=\"使用模板的自动发布工作流\">\n不同模板使用的发布方式可能不同，具体配置流程参考对应模板的详细使用指南。\n</TabItem>\n\n  <TabItem value=\"manual\" label=\"手动发布\">\n根据选用的构建系统，在项目的 `pyproject.toml` 中填入必要信息后进行构建与发布。\n\n:::tip 提示\n不同构建工具的使用可能存在差别。本文仅以 [`pdm`](https://pdm-project.org/zh/latest/),\n[`poetry`](https://python-poetry.org/docs/), [`setuptools`](https://setuptools.pypa.io/en/latest/)\n构建系统**本地构建与发布**为示例讲解，其余构建/管理工具等和自动化构建的用法请读者自行探索。\n:::\n\n<Tabs groupId=\"publishMethod\">\n  <TabItem value=\"poetry\" label=\"Poetry\" default>\n\n```bash\npoetry publish --build  # 构建并发布\n\n# 等效于以下两个命令\npoetry build            # 只构建\npoetry publish          # 只发布先前的构建\n```\n\n  </TabItem>\n\n  <TabItem value=\"pdm\" label=\"PDM\" default>\n\n```bash\npdm publish             # 构建并发布\n\n# 等效于以下两个命令\npdm build               # 只构建\npdm publish --no-build  # 只发布先前的构建\n```\n\n  </TabItem>\n\n  <TabItem value=\"setuptools\" label=\"Setuptools (PEP 517)\" default>\n\n```bash\npip install build twine             # 安装通用构建与发布工具\n\npython -m build --sdist --wheel .   # 只构建\ntwine upload dist/*                 # 只发布先前的构建\n```\n\n  </TabItem>\n</Tabs>\n\n</TabItem>\n</Tabs>\n\n:::tip 提示\n发布前建议自行测试构建包是否可用，避免遗漏代码文件或资源文件等问题。\n:::\n\n## 基本要求\n\n无论你选择哪条路径，以下内容**必须**完成，否则无法通过商店自动检查：\n\n### 能够正确加载\n\n插件包必须能够被 NoneBot 正确加载，在商店审核中会通过 **NoneFlow** 自动化加载测试进行。\n\n#### 依赖其他插件\n\n如果插件依赖其他插件提供的功能，则**必须**在代码中使用 `require()` 来引入该插件，然后才能 `import` 该插件提供的功能。具体细节参阅[跨插件访问](../advanced/requiring.md)。\n\n使用示例如下：\n\n```python title=nonebot_plugin_weather/__init__.py\nfrom nonebot import require\n\nrequire(\"nonebot_plugin_apscheduler\")\n\nfrom nonebot_plugin_apscheduler import scheduler\n```\n\n#### 不能零配置加载的插件\n\n如果插件需要必要配置项才能正常导入，则**必须**在商店提交表单中填写必要的配置项内容。\n\n但一种更好的做法是，**将插件设计为零配置即可加载**（允许缺少必要配置项时插件仍能正常导入，但不执行需要相应配置项的功能），尤其是对于一些必要配置含有敏感信息（如密钥、Token、API Key 等）的插件。这样可以避免在商店提交表单时填写敏感信息的风险。\n\n### 插件元数据\n\n插件包**必须**填写元数据才能通过 **NoneFlow** 自动化检查。\n\n下面是一个示例：\n\n```python title=nonebot_plugin_weather/__init__.py\nfrom nonebot.plugin import PluginMetadata\n\nfrom .config import Config\n\n__plugin_meta__ = PluginMetadata(\n    # 基本信息（必填）\n    name=\"天气查询\",  # 插件名称\n    description=\"查询指定城市的实时天气与未来预报\",  # 插件介绍\n    usage=\"发送【天气 城市名】获取天气信息\",  # 插件用法\n\n    # 发布额外信息\n    type=\"application\",  # 插件分类\n    # 发布必填，当前有效类型有：`library`（为其他插件编写提供功能），`application`（向机器人用户提供功能）。\n\n    homepage=\"https://github.com/你的用户名/nonebot-plugin-weather\",\n    # 发布必填。\n\n    config=Config,\n    # 插件配置项类，如果有配置类则必须填写。\n\n    supported_adapters={\"~onebot.v11\"},\n    # 支持的适配器集合，其中 `~` 在此处代表前缀 `nonebot.adapters.`，其余适配器亦按此格式填写。\n    # 若插件只使用了 NoneBot 基本抽象，应显式填写 None，否则应该列出插件支持的适配器。\n)\n```\n\n:::caution 注意\n`__plugin_meta__` 变量**必须**处于插件最外层（如 `__init__.py` 中），否则无法正常识别。\n\n一般做法是在 `__init__.py` 中定义 `__plugin_meta__`。\n:::\n\n#### 继承其他插件支持的适配器\n\n如果你的插件依赖于其他插件提供的支持功能，而其他插件可能支持更少的适配器，这时就应该使用\n[inherit_supported_adapters()](../api/plugin/load#inherit-supported-adapters) 函数来继承其他插件支持的适配器。\n\n示例用法如下：\n\n```python title=nonebot_plugin_weather/__init__.py\nfrom nonebot import require\nfrom nonebot.plugin import PluginMetadata, inherit_supported_adapters\n\nfrom .config import Config\n\nrequire(\"nonebot_plugin_alconna\")  # 必须先 require 才能被 inherit_supported_adapters 处理\n\n__plugin_meta__ = PluginMetadata(\n    name=\"天气查询\",\n    description=\"查询指定城市的实时天气与未来预报\",\n    usage=\"发送【天气 城市名】获取天气信息\",\n    type=\"application\",\n    homepage=\"https://github.com/你的用户名/nonebot-plugin-weather\",\n    config=Config,\n\n    supported_adapters=inherit_supported_adapters(\"nonebot_plugin_alconna\"),\n    # 继承 nonebot_plugin_alconna 插件的适配器支持列表\n)\n```\n\n### 准备项目主页\n\n通常可以使用 GitHub 项目页面作为项目主页，在 `README.md` 文件中编写插件介绍等内容。\n\n内容大致包括：\n\n- 插件功能介绍；\n- 安装方法\n  - **必须**有 NB-CLI 方式安装\n  - 可选依赖可以给出其他安装方式\n  - **不得**使用旧式的 `bot.py` 配置\n- 插件配置项（如 `Config` 类字段，若无可跳过）\n- 插件设置的触发规则（若无可跳过）\n- 插件的其它用法（按需编写）\n- 效果图、权限说明（按需编写）\n\n## 质量要求\n\n以下内容**强烈建议**完成，否则社区成员将会要求修改：\n\n### 依赖管理原则\n\n- **必须**包含 `nonebot2`。\n- **必须**将插件直接使用的适配器加入依赖列表，如：使用 OneBot 适配器的插件应添加 `nonebot-adapter-onebot` 依赖；\n- **禁止**使用 `==` 锁定单一版本，使用 `>=` 或 `~=`。\n- **禁止**添加 `nonebot`（V1）作为依赖。\n- 所有在代码中 `import` 的第三方库，必须在 `pyproject.toml` 的 `dependencies` 中列出。\n\n### 避免误用同步操作\n\nNoneBot 是一个异步框架，插件中**禁止**使用任何可能阻塞事件循环的同步操作，例如：\n\n- 同步 HTTP 请求（如 `requests` 库）；\n\n  **推荐**操作（以 `httpx` 为例）：\n\n  ```python\n  import httpx\n\n  async with httpx.AsyncClient() as client:\n      response = await client.get(\"https://api.example.com/data\")  # 异步操作，不阻塞机器人\n  ```\n\n  **禁止**操作：\n\n  ```python\n  import requests\n\n  requests.get(\"https://api.example.com/data\")  # 同步操作，会阻塞机器人\n  ```\n\n- 其他可能长时间运行阻塞事件循环的操作。\n\n### 本地文件存储\n\n如果插件需要在本地存储数据、配置或缓存文件，**必须**使用 [`nonebot-plugin-localstore`](https://github.com/nonebot/plugin-localstore) 管理，具体细节参阅[本地存储](../best-practice/data-storing.md)章节。\n\n参考示例：\n\n```python title=nonebot_plugin_weather/__init__.py\nfrom pathlib import Path\nfrom nonebot import require\nrequire(\"nonebot_plugin_localstore\")\n\nimport nonebot_plugin_localstore as store\n\n# 获取插件缓存文件（夹）路径\nweather_cache_dir: Path = store.get_plugin_cache_dir()\nweather_cache_file: Path = store.get_plugin_cache_file(\"cache.json\")\n\n# 获取插件配置文件（夹）路径\nweather_config_dir: Path = store.get_plugin_config_dir()\nweather_config_file: Path = store.get_plugin_config_file(\"config.toml\")\n\n# 获取插件数据文件（夹）路径\nweather_data_dir: Path = store.get_plugin_data_dir()\nweather_data_file: Path = store.get_plugin_data_file(\"resource-index.json\")\n```\n\n## 商店审核\n\n### 提交申请\n\n完成在 PyPI 的插件发布流程后，前往[商店](/store/plugins)页面，切换到插件页签，点击 **发布插件** 按钮。\n\n在弹出的插件信息提交表单内，填入您所要发布的相应插件信息。请注意，如果插件需要必要配置项才能正常导入，请在“插件配置项”中填写必要的内容（请勿填写密钥等敏感信息）。\n\n完成填写后，点击 **发布** 按钮，这将自动跳转 NoneBot 仓库内的“发布插件”页面。确认信息无误后点击页面下方的 `Submit new issue` 按钮进行最终提交即可。\n\n### 等待插件审核\n\n插件发布 Issue 创建后，将会经过 **NoneFlow Bot** 的自动前置检查，以确保插件信息正确无误、插件能被正确加载。\n\n:::tip 提示\n若插件检查未通过或信息有误，**不必**关闭当前 Issue。只需更新插件并上传到 PyPI/修改信息后勾选插件测试勾选框即可重新触发插件检查。\n:::\n\n之后，NoneBot 的维护者和一些志愿者会初步检查插件代码，帮助减少该插件的问题。\n\n完成这些步骤后，您的插件将会被自动合并到[商店](/store/plugins)，而您也将成为 [**NoneBot 贡献者**](https://github.com/nonebot/nonebot2/graphs/contributors)的一员。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/editor-support.md",
    "content": "---\nsidebar_position: 2\ndescription: 配置编辑器以获得最佳体验\n---\n\n# 编辑器支持\n\n框架基于 [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）工具进行类型检查，确保代码可以被编辑器正确解析。\n\n## 编辑器推荐配置\n\n### Visual Studio Code\n\n在 Visual Studio Code 中，可以使用 Pylance Language Server 并启用 `Type Checking` 配置以达到最佳开发体验。\n\n1. 在 VSCode 插件视图搜索并安装 `Python (ms-python.python)` 和 `Pylance (ms-python.vscode-pylance)` 插件。\n2. 修改 VSCode 配置\n   在 VSCode 设置视图搜索配置项 `Python: Language Server` 并将其值设置为 `Pylance`，搜索配置项 `Python > Analysis: Type Checking Mode` 并将其值设置为 `basic`。\n\n   或者向项目 `.vscode` 文件夹中配置文件添加以下内容：\n\n   ```json title=settings.json\n   {\n     \"python.languageServer\": \"Pylance\",\n     \"python.analysis.typeCheckingMode\": \"basic\"\n   }\n   ```\n\n### 其他\n\n欢迎提交 Pull Request 添加其他编辑器配置推荐。点击左下角 `Edit this page` 前往编辑。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/ospp/2021.md",
    "content": "---\nsidebar_position: 0\ndescription: 开源软件供应链点亮计划 - 暑期 2021\nmdx:\n  format: md\n---\n\n# 暑期 2021\n\n**开源软件供应链点亮计划 - 暑期 2021** 是**中国科学院软件研究所**与 **openEuler 社区**共同举办的一项面向高校学生的暑期活动，旨在鼓励在校学生积极参与开源软件的开发维护，促进优秀开源软件社区的蓬勃发展。关于具体的活动规划、报名方式，请查看该活动的 [官网](https://summer.iscas.ac.cn/) 和 [帮助文档](https://summer.iscas.ac.cn/help/)。\n\nNoneBot 社区有幸作为开源社区参与了本次活动，下面列出了目前我们已经发布的项目，欢迎感兴趣的同学在上面给出的活动官网报名，或通过 [contact@nonebot.dev](mailto:contact@nonebot.dev) 联系我们。\n\n## NoneBot v1\n\n### 更新 NoneBot v1 文档中的“指南”部分\n\n由于 NoneBot v1 和 aiocqhttp 最初基于的 QQ 机器人平台不再提供服务，CQHTTP 接口也转型且改名为 OneBot 标准，目前 NoneBot v1 文档的“指南”部分和 aiocqhttp 文档有部分过时内容需要更新。我们希望将其中与旧的机器人平台相关的内容改为基于 go-cqhttp 或通用的 OneBot 表述，同时对 NoneBot v1 的 awesome-bot 示例做一次全面检查，修改其中可能已经不可用的部分。\n\n**难度**：低\n\n**导师**：[@cleoold](https://github.com/cleoold)\n\n**产出要求**\n\n- 修改“指南”文档和 aiocqhttp 文档中与旧的 QQ 机器人平台相关的部分\n- 检查 awesome-bot 示例是否有已经过时/不可用的地方，并更新/修复\n- 修改“图灵机器人”案例，使用其它 AI 聊天 API 提供商（需先做简单调研）\n\n**技术要求**\n\n- 熟悉 Python 编程语言及 asyncio 机制\n- 了解 Git 基本用法\n- 了解聊天机器人基本开发过程\n- 了解 VuePress 更佳\n\n### NoneBot v1 API 文档自动生成\n\n目前 NoneBot v1 的文档中“API”部分是手动编写的，在更新代码接口的同时需要手动更新文档，可能造成文档与代码不匹配，形成额外的维护成本。我们希望将 API 文档改为直接编写在 Python docstring 中，通过工具自动生成 API 文档。\n\n**难度**：中\n\n**导师**：[@cleoold](https://github.com/cleoold)\n\n**产出要求**\n\n- 调研市面上常见的 Python API 文档生成工具\n- 在代码中补充 API 文档\n- 编写或应用开源工具自动生成 API 文档\n- 配置 GitHub Actions 或其它 CI 自动化构建和部署 API 文档\n\n**技术要求**\n\n- 熟悉 Python 编程语言及 asyncio 和 Type Hints\n- 了解 Git 基本用法\n- 了解 Sphinx 等文档生成工具更佳\n- 了解 GitHub Actions 等 CI 工具更佳\n\n## NoneBot v2\n\n### NoneBot v2 自动化测试框架“NoneBug”\n\n在聊天机器人的开发过程中，一套自动化的测试机制是非常重要的，特别是对于 NoneBot 2 这类为大型机器人开发而设计的项目来说，需要手动测试每一个边际条件是非常痛苦的。我们希望能够开发一款基于 NoneBot 2 插件机制的自动化测试框架，为 NoneBot 2 用户提供一套易用便捷、高度灵活的自动化测试框架。\n\n**难度**：高\n\n**导师**：[@yanyongyu](https://github.com/yanyongyu)\n\n**产出要求**\n\n- 调研现有的 Python 和其它语言集成测试框架\n- 设计 NoneBug 的用户 API 和实现方式\n- 实现 NoneBug 自动化测试框架\n- 编写详细的使用文档\n\n**技术要求**\n\n- 熟悉 Python 编程语言及 asyncio 和 Type Hints\n- 了解 Git 基本用法\n- 了解 NoneBot v2 的基本原理和使用方式\n- 了解主流的 Python 自动化测试框架\n\n### NoneBot v2 Telegram 适配器\n\n目前 NoneBot v2 已支持 OneBot、Mirai HTTP API、钉钉协议，社区反馈有更多的平台需求，希望能在 NoneBot v2 获得更多的跨平台支持，提高机器人的便携性。同时，我们也希望随着新平台加入，提升现有 NoneBot v2 核心代码的平台通用性。Telegram 是一款较为广泛使用的安全即时聊天软件，同时其官方提供了丰富的聊天机器人 API，因此我们希望为 NoneBot v2 编写一个 Telegram 适配器来支持 Telegram 机器人的开发。\n\n**难度**：中\n\n**导师**：[@yanyongyu](https://github.com/yanyongyu)\n\n**产出要求**\n\n- 调研 Telegram Bot API 以及 WebHook 等官方接口\n- 编写 Telegram 适配器并能够使用\n- 代码遵守项目 Contributing 规范\n\n**技术要求**\n\n- 熟悉 Python 编程语言及 asyncio 和 Type Hints\n- 了解 Git 基本用法\n- 了解 Web 开发相关知识\n- 了解 Sphinx 等文档生成工具更佳\n\n### NoneBot v2 飞书适配器\n\n目前 NoneBot v2 已支持 OneBot、Mirai HTTP API、钉钉协议，社区反馈有更多的平台需求，希望能在 NoneBot v2 获得更多的跨平台支持，提高机器人的便携性。同时，我们也希望随着新平台加入，提升现有 NoneBot v2 核心代码的平台通用性。飞书是目前企业用户广泛使用的即时聊天和协作软件，其官方提供了丰富的聊天机器人 API，因此我们希望为 NoneBot v2 编写一个飞书适配器来支持飞书机器人的开发。\n\n**难度**：中\n\n**导师**：[@yanyongyu](https://github.com/yanyongyu)\n\n**产出要求**\n\n- 调研飞书机器人 API 以及 WebHook 等官方接口\n- 编写飞书适配器并能够使用\n- 代码遵守项目 Contributing 规范\n\n**技术要求**\n\n- 熟悉 Python 编程语言及 asyncio 和 Type Hints\n- 了解 Git 基本用法\n- 了解 Web 开发相关知识\n- 了解 Sphinx 等文档生成工具更佳\n\n## OneBot\n\n### 设计 OneBot v12 接口标准\n\n目前的 OneBot 标准的 v11 版本仍然与 QQ 平台有较多耦合，我们希望在 v12 去掉与 QQ 耦合的历史包袱，形成一个通用的、可扩展的、易于使用的同时易于实现的聊天机器人接口标准。\n\n**难度**：中\n\n**导师**：[@richardchien](https://github.com/richardchien)\n\n**产出要求**\n\n- 调研各聊天机器人平台的官方/非官方接口特点\n- 通用化 OneBot 核心 API，分离 QQ 特定的 API，去掉无用 API\n- 优化现有的通信、消息表示机制\n- 补充 QQ 特定的缺失 API\n- 文档需符合风格指南\n\n**技术要求**\n\n- 熟悉至少两个聊天平台的聊天机器人开发\n- 了解 Git 基本用法\n- 了解使用不同语言编写聊天机器人时的常用实践\n- 对文档的优雅性与美观性有追求更佳\n\n### 实现 Rust 版 libonebot\n\n目前最常用的 OneBot 实现包括 go-cqhttp、onebot-kotlin、node-onebot 等，这些实现都各自重复实现了 Web 通信、消息解析、配置读写等功能，当社区中的开发者想针对一个新的聊天平台实现 OneBot 时，他们往往同样需要再次实现类似逻辑。我们希望使用 Rust 编写一个 libonebot 模块，该模块实现所有 OneBot 实现所共享的功能，从而方便其他开发者们使用 Rust 快速编写具体的 OneBot 实现。同时，我们希望借此项目在聊天机器人社区中推广 Rust 编程语言。\n\n> 注：这里的逻辑是 libonebot + 针对某聊天平台的对接代码 = 某聊天平台的 OneBot 实现，libonebot 要做的是让 OneBot 实现的开发者只需编写针对特定聊天平台的对接代码，而无需关心 OneBot 标准定义的通信方式、消息格式等。\n\n**难度**：高\n\n**导师**：[@richardchien](https://github.com/richardchien)\n\n**产出要求**\n\n- 实现所有 OneBot 实现所共享的功能，包括 Web 通信、消息解析、配置读写等\n- 充分考虑同时兼容 OneBot v11 和 v12 接口\n- 能够根据用户（OneBot 实现的开发者）所实现的接口自动实现类似 get_available_apis 等接口\n- 编写详细的使用文档\n- 如果可能，与 v12 设计项目联动，实现第一手 v12 支持\n\n**技术要求**\n\n- 熟悉聊天机器人开发\n- 熟悉 Rust Web 开发\n\n### 实现自选语言版 libonebot\n\n目前最常用的 OneBot 实现包括 go-cqhttp、onebot-kotlin、node-onebot 等，这些实现都各自重复实现了 Web 通信、消息解析、配置读写等功能，当社区中的开发者想针对一个新的聊天平台实现 OneBot 时，他们往往同样需要再次实现类似逻辑。我们希望使用 Python、Go、Kotlin、Node、PHP、C#.NET 等主流语言（任选一个）编写 libonebot 模块，该模块实现所有 OneBot 实现所共享的功能，从而方便其他开发者们使用对应语言快速编写具体的 OneBot 实现。\n\n> 注：这里的逻辑是 libonebot + 针对某聊天平台的对接代码 = 某聊天平台的 OneBot 实现，libonebot 要做的是让 OneBot 实现的开发者只需编写针对特定聊天平台的对接代码，而无需关心 OneBot 标准定义的通信方式、消息格式等。\n\n**难度**：中\n\n**导师**：[@richardchien](https://github.com/richardchien)\n\n**产出要求**\n\n- 实现所有 OneBot 实现所共享的功能，包括 Web 通信、消息解析、配置读写等\n- 充分考虑同时兼容 OneBot v11 和 v12 接口\n- 编写详细的使用文档\n- 如果可能，实现更多附加特性，如根据用户（OneBot 实现的开发者）所实现的接口自动实现类似 get_available_apis 等接口、实现第一手 v12 支持等\n\n**技术要求**\n\n- 熟悉聊天机器人开发\n- 熟悉所选语言的 Web 开发\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/ospp/2022.md",
    "content": "---\nsidebar_position: 1\ndescription: 开源之夏 - 暑期 2022\nmdx:\n  format: md\n---\n\n# 暑期 2022\n\n**开源之夏 - 暑期 2022** 是由**开源软件供应链点亮计划**发起、由**中国科学院软件研究所**与 **openEuler 社区**主办的一项面向高校学生的暑期活动，类似 Google Summer of Code（GSoC），旨在鼓励在校学生积极参与开源软件的开发维护，促进优秀开源软件社区的蓬勃发展。关于具体的活动规划、报名方式，请查看该活动的 [官网](https://summer-ospp.ac.cn/) 和 [帮助文档](https://summer-ospp.ac.cn/help/)。\n\nNoneBot 社区有幸作为开源社区 [参与](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) 联系我们。\n\n## NoneBot2 命令行 CLI 交互体验升级\n\nNoneBot2 为用户提供了命令行脚手架 ──`nb-cli`，辅助用户更好地上手项目以及进行开发。nb-cli 主要包括：创建项目、运行项目、安装与卸载插件、部署项目等功能。随着 NoneBot2 Beta 版本的发布，脚手架功能存在一定的定位不明确、功能体验不佳。本项目旨在重新设计 nb-cli 功能框架，完善功能，优化用户体验。\n\n**难度**：进阶\n\n**导师**：[@yanyongyu](https://github.com/yanyongyu)\n\n**产出要求**\n\n- 设计 nb-cli 功能框架\n  - 明确各功能模块\n  - 设计用户交互模式\n- 完成 nb-cli 主要功能代码\n  - 项目管理\n  - 插件管理\n  - 其它\n- 同步更新使用文档\n\n**技术要求**\n\n- 熟悉 Python 命令行交互代码编写\n- 熟悉 NoneBot2 框架功能\n- 熟悉 NoneBot2 项目组织方式\n\n**成果仓库**\n\n- <https://github.com/nonebot/nb-cli>\n- <https://github.com/nonebot/nonebot2>\n\n## NoneBot2 命令行即时交互通信设计与实现\n\nNoneBot2 在早期提供了基于网页的 nonebot-plugin-test 插件，无需平台适配接入即可对机器人进行测试，方便了开发者直观的感受机器人文本交互功能。我们希望提供一款基于命令行的适配器/驱动器，用于无平台适配接入、可以运行机器人的场景进行功能体验或测试。\n\n**难度**：进阶\n\n**导师**：[@mnixry](https://github.com/mnixry)\n\n**产出要求**\n\n- 设计命令行与 NoneBot2 通信模式\n  - 直接调用/HTTP/WebSocket\n- 设计命令行交互界面\n- 实现相应适配器/驱动器\n- 同步更新使用说明文档\n\n**技术要求**\n\n- 熟悉 Python 命令行交互代码编写\n- 熟悉 NoneBot2 框架功能\n- 熟悉 NoneBot2 项目组织方式\n\n**成果仓库**\n\n- <https://github.com/nonebot/adapter-console>\n\n## NoneBot2 用户上手与深入教程设计\n\nNoneBot2 为用户提供了详细的文档介绍，辅助用户更好的上手项目以及进行开发。文档分为基础与进阶两个部分。基础部分帮助新用户快速上手开发，主要包括：安装 NoneBot2、使用脚手架、创建配置项目、使用适配器、加载插件、定义消息事件、处理消息事件、调用平台 API 等。进阶部分向已经熟悉开发流程的用户介绍更多高级技巧，主要包括：NoneBot2 工作原理、定时任务、权限控制、钩子函数、跨插件访问、单元测试、发布插件等。目前文档对于用户而言过于费解，导致用户难以理解 NoneBot2 开发。本项目旨在优化文档内容，使其更加通俗易懂，不让文档成为用户上手的阻碍，同时完善进阶内容，让有更复杂需求的用户，同样能从文档中受益。\n\n相关 issue：\n\n- <https://github.com/nonebot/nonebot2/issues/793>\n- <https://github.com/nonebot/nonebot2/issues/295>\n\n**难度**：进阶\n\n**导师**：[@SK-415](https://github.com/SK-415)\n\n**产出要求**\n\n- 文档通俗易懂\n  - 附有适当的图片指引（如 asciinema）\n- 内容完整，由浅入深\n- 适当的界面美化，合理分配布局\n\n**技术要求**\n\n- 熟悉文档结构组织与语言表达\n- 熟悉 NoneBot2 框架功能\n- 熟悉 NoneBot2 项目组织方式\n\n**成果仓库**\n\n- <https://github.com/nonebot/nonebot2>\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/ospp/2023.md",
    "content": "---\nsidebar_position: 2\ndescription: 开源之夏 - 暑期 2023\nmdx:\n  format: md\n---\n\n# 暑期 2023\n\n**开源之夏 - 暑期 2023** 是由**开源软件供应链点亮计划**发起、由**中国科学院软件研究所**与 **openEuler 社区**主办的一项面向高校学生的暑期活动，类似 Google Summer of Code（GSoC），旨在鼓励在校学生积极参与开源软件的开发维护，促进优秀开源软件社区的蓬勃发展。关于具体的活动规划、报名方式，请查看该活动的 [官网](https://summer-ospp.ac.cn/) 和 [帮助文档](https://summer-ospp.ac.cn/help/)。\n\nNoneBot 社区有幸作为开源社区 [参与](https://summer-ospp.ac.cn/org/orgdetail/e1fb5b8d-125a-4138-b756-25bd32c0a31a?lang=zh) 了本次活动，下面列出了目前我们已经发布的项目，欢迎感兴趣的同学通过 [contact@nonebot.dev](mailto:contact@nonebot.dev) 联系我们。\n\n## NoneBot 项目管理图形化面板\n\nNoneBot 目前提供了开箱即用的命令行脚手架来帮助初次使用的用户更快的上手编写应用。但是，对于未有一定开发经验的用户，命令行的使用仍具有一定的困难。此外，其他项目如 koishi、vue 等，均可通过图形化界面的形式为用户提供更便捷的项目开发。因此，我们希望借助现有命令行脚手架的可扩展特性，提供一个项目管理面板服务，以网页的形式帮助用户开发 NoneBot 应用。\n\n**难度**：进阶\n\n**导师**：[@mnixry](https://github.com/mnixry)\n\n**产出要求**\n\n- 设计并实现项目管理面板相关功能\n  - 创建与管理项目\n  - 配置与运行项目\n  - NoneBot 插件管理\n- 实现相应 nb-cli 插件提供面板服务\n- 代码符合 NoneBot Contributing 规范\n\n**技术要求**\n\n- 熟悉 nb-cli 相关功能\n- 熟悉 NoneBot 框架功能\n- 熟悉前后端相关实现方式\n\n**成果仓库**\n\n- <https://github.com/nonebot/cli-plugin-webui>\n\n## NoneBot Discord 适配器\n\nNoneBot 作为一个跨平台聊天机器人框架，目前已有 OneBot、飞书、Telegram、QQ 频道等诸多平台的适配支持。作为众多用户期待的平台适配之一，我们希望借此机会接入 Discord 聊天机器人。\n\n**难度**：进阶\n\n**导师**：[@iyume](https://github.com/iyume)\n\n**产出要求**\n\n- 调研 Discord Bot 相关功能与接口\n- 设计与编写 NoneBot Discord 适配器\n- 代码符合 NoneBot Contributing 规范\n\n**技术要求**\n\n- 熟悉 NoneBot 框架功能\n- 熟悉 NoneBot 各模块职责与适配器编写\n\n**成果仓库**\n\n- <https://github.com/nonebot/adapter-discord>\n\n## NoneBot 数据库支持插件\n\nNoneBot 的插件系统为用户实现应用提供了极高的便捷性，但因此也增加了插件统一管理的难度。目前，我们发现许多用户发布的插件中存在文件存储结构化数据、数据存放散乱等现象，同时插件间也可能产生冲突。因此，我们希望提供一个统一的数据存储与管理方式，便于用户读写应用数据。\n\n**难度**：进阶\n\n**导师**：[@yanyongyu](https://github.com/yanyongyu)\n\n**产出要求**\n\n- 设计并实现 ORM 插件\n  - 提供关系模型定义功能\n  - 提供模型迁移与管理功能\n  - 能较好的支持 Python 类型检查与推导\n- 编写相应的用户使用文档\n- 代码符合 NoneBot Contributing 规范\n\n**技术要求**\n\n- 熟悉 NoneBot 框架功能与插件编写\n- 熟悉 SQLAlchemy 等 ORM 框架\n  - 熟悉 SQLAlchemy ORM\n  - 熟悉 alembic 等迁移工具\n- 熟悉 nb-cli 插件编写\n\n**成果仓库**\n\n- <https://github.com/nonebot/plugin-orm>\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/ospp/2024.md",
    "content": "---\nsidebar_position: 3\ndescription: 开源之夏 - 暑期 2024\nmdx:\n  format: md\n---\n\n# 暑期 2024\n\n**开源之夏 - 暑期 2024** 是**中国科学院软件研究所**发起的**开源软件供应链点亮计划**系列暑期活动，旨在鼓励高校学生积极参与开源软件的开发维护，促进优秀开源软件社区的蓬勃发展。活动联合各大开源社区，针对重要开源软件的开发与维护提供项目开发任务，并向全球高校学生开放报名。关于具体的活动规划、报名方式，请查看该活动的 [官网](https://summer-ospp.ac.cn/) 和 [帮助文档](https://summer-ospp.ac.cn/help/)。\n\nNoneBot 社区有幸作为开源社区 [参与](https://summer-ospp.ac.cn/org/orgdetail/e1fb5b8d-125a-4138-b756-25bd32c0a31a?lang=zh) 了本次活动，下面列出了目前我们已经发布的项目，欢迎感兴趣的同学通过 [contact@nonebot.dev](mailto:contact@nonebot.dev) 联系我们。\n\n## NonePress 官网组件库更新与优化\n\nNoneBot 官网目前采用基于 TailwindCSS 自研的 NonePress 组件库及 Docusaurus 框架进行构建。由于相关依赖版本迭代迅速，目前官网组件库已产生了较大的版本落后。本项目希望在跟进框架新版本的基础上，对文档整体视觉体验进行重新设计，提升页面的无障碍访问性，基于 React Hydrate 特性实现完整的静态网站生成（SSG）以提升搜索引擎优化（SEO）水平。在解决以上问题的基础上，可对网页的开发以及生产构建性能做相应的优化提升，例如在生产构建使用自有的 webpack loader、替换现有的热重载逻辑以减少开发环境启动耗时等。\n\n**难度**：进阶\n\n**导师**：[@yanyongyu](https://github.com/yanyongyu)\n\n**产出要求**\n\n- 基于 Docusaurus v3 重构 NonePress 组件库及相关插件\n  - 升级相关依赖并重新打造 Docusaurus theme（布局与组件）\n  - 根据需求实现/修改 Docusaurus 插件使得官网内容构建正常\n  - 能够提升页面渲染性能与 MDX 相关能力\n- 升级官网采用新版组件库\n  - Algolia 索引与 SEO 正常\n  - 桌面端与移动端显示正常\n  - 优化官网开发与生产构建体验\n- （可选）优化官网部分页面\n  - 优化官网过长的 changelog\n  - 优化官网插件商店的展示细节\n\n**技术要求**\n\n- 熟练掌握 TS、PostCSS、TSX、MDX等相关技术\n- 掌握 React、Docusaurus、tailwind css 等框架\n- 熟悉静态网站生成 SSG、SEO 优化与 Algolia 索引原理等\n\n**成果仓库**\n\n- <https://github.com/nonebot/docusaurus-theme-nonepress>\n\n## NoneFlow 社区自动化工作流管理优化\n\nNoneFlow 在 NoneBot 社区中承担着重要的角色，它由 NoneBot 框架基于 GitHub APP 编写而成，能够自动化的完成许多复杂流程的处理，如：用户请求提交插件到商店时进行自动化检测，并在人工审核通过后自动存储至 registry；定时自动更新 registry 内插件信息，跟进插件新版本情况等。但是，在长期的使用中发现了一些问题和不足的地方，例如：项目本身结构复杂耦合，添加新自动化流程与维护现有流程困难；目前采用了 GitHub 用户名作为插件作者名，但已有不少插件作者改名；插件存储至 registry 并定时更新，缺少统计相关信息以帮助商店更好的展示当前插件状态；插件作者想要修改插件信息时无法便捷的找到操作方式等。本项目希望针对以上问题与不足的地方进行修复与优化，提升用户体验。\n\n**难度**：进阶\n\n**导师**：[@uy/sun](https://github.com/he0119)\n\n**产出要求**\n\n- 重构现有工作流处理结构\n  - 整合现有 Issue、Pull Request、Git 相关操作\n  - 提供用户修改信息的处理方式\n  - 正确处理 PR 的 Open、Close、Draft 状态\n- 修复流程中存在的问题\n  - 插件作者名正确展示\n  - registry 定时更新中需要插件测试环境隔离\n- 在 registry 定时更新的同时提供统计数据\n\n**技术要求**\n\n- 掌握 GitHub APP 开发\n  - 熟悉 GitHub REST API、GraphQL 等\n  - 熟悉 GitHub APP 权限限制\n- 熟悉 NoneBot 框架与 Python 相关技术\n- 熟悉 Git、GitHub Action、GitHub 工作流\n\n**成果仓库**\n\n- <https://github.com/nonebot/noneflow>\n\n## NoneBlockly 低代码框架开发\n\n经过深入分析社区反馈，我们发现部分新手因不熟悉编程概念或框架本身而遇到问题。为了解决初学者在使用面向开发者的聊天机器人框架 NoneBot 时遇到的挑战，我们计划引入 Blockly 提供低代码编程支持。通过减少常见的编码错误和降低入门门槛，使框架对初学者更加友好，从而提升用户体验并有助于 NoneBot 生态的成长。本项目将基于 Blockly 实现 NoneBot 插件的低代码编写，使得用户能够快速搭建聊天机器人。\n\n**难度**：进阶\n\n**导师**：[@mnixry](https://github.com/mnixry)\n\n**产出要求**\n\n- 实现 NoneBlockly 低代码开发框架\n  - 能够基于 Alconna 编写跨平台插件\n  - 确保插件对 Python 和 NoneBot 版本的兼容性\n  - 支持对多种类型 NoneBot 事件的响应\n  - 支持对 NoneBot 消息对象的便捷操作\n  - 集成 localstore 文件存储、apscheduler 定时任务、网络请求等常用功能\n- 对接 NB-CLI 脚手架，通过脚手架扩展使用低代码框架\n\n**技术要求**\n\n- 掌握 Python 与 NoneBot 框架的使用\n  - 熟悉 NoneBot 插件的开发，包括事件响应与消息处理等\n  - 熟悉 NoneBot 生态组件（Alconna、localstore、apscheduler等）的使用\n  - 了解 NB-CLI 脚手架的扩展开发\n- 熟悉 Blockly 低代码框架的使用和开发\n\n**成果仓库**\n\n- <https://github.com/nonebot/noneblockly>\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/ospp/2025.md",
    "content": "---\nsidebar_position: 4\ndescription: 开源之夏 - 暑期 2025\nmdx:\n  format: md\n---\n\n# 暑期 2025\n\n**开源之夏 - 暑期 2025** 是**中国科学院软件研究所**发起的**开源软件供应链点亮计划**系列暑期活动，旨在鼓励高校学生积极参与开源软件的开发维护，培养和发掘更多优秀的开发者，促进优秀开源软件社区的蓬勃发展，助力开源软件供应链建设。活动联合各大开源社区，针对重要开源软件的开发与维护提供项目开发任务，并向全球高校学生开放报名。关于具体的活动规划、报名方式，请查看该活动的 [官网](https://summer-ospp.ac.cn/) 和 [帮助文档](https://summer-ospp.ac.cn/help/)。\n\nNoneBot 社区有幸作为开源社区 [参与](https://summer-ospp.ac.cn/org/orgdetail/e1fb5b8d-125a-4138-b756-25bd32c0a31a?lang=zh) 了本次活动，下面列出了目前我们已经发布的项目，欢迎感兴趣的同学通过 [contact@nonebot.dev](mailto:contact@nonebot.dev) 联系我们。\n\n## NoneBot HTML 图片渲染插件\n\n文字与图片一直是聊天机器人的两大主流交互方式，而图片的渲染一直是用户开发应用的一大痛点。常见的方式包括 PIL 图片编辑、浏览器渲染 HTML 截图等。PIL 图片编辑依赖人工构建图片布局，容易出现自适应问题，且提升图片特效、美观程度需要极大的开发成本。浏览器渲染方案通过 HTML 与 CSS 能够轻松完成美观自适应能力强的布局，但其部署门槛较高，难以支撑较大规模调用量。而其他轻量化渲染引擎通常不具有完整 HTML/CSS 现代化标准实现，且未提供 Python Binding 直接使用。\n\n本项目希望调研并实现一种高效、便捷的图片渲染方案。该方案需要在保障跨平台一致性、最大程度保证 HTML 与 CSS 现代化标准的前提下，低成本（资源消耗与吞吐量）将 HTML 渲染为对应图片。\n\n**难度**：进阶\n\n**导师**：[@MelodyKnit](https://github.com/MelodyKnit)\n\n**产出要求**\n\n- 调研 HTML/CSS 渲染引擎\n  - 调研 litehtml 等渲染引擎 标准支持能力与兼容性\n- 基于渲染引擎实现 HTML 图片渲染插件\n  - 将渲染引擎通过 binding 等方式集成为 Python 模块\n  - 基于集成模块实现 HTML 图片渲染能力\n  - 编写插件使用文档\n\n**技术要求**\n\n- 掌握 Python 及其异步编程\n- 熟悉 NoneBot 框架及其插件编写\n- 了解浏览器与 HTML 渲染原理\n\n**成果仓库**\n\n- <https://github.com/nonebot/plugin-htmlkit>\n\n## NB-CLI 命令行工具交互优化\n\nNB-CLI 作为 NoneBot 生态的核心入门与管理工具，主要负责新手引导项目创建、项目运行以及插件管理几大功能。目前该脚手架工具仍存在几点缺陷：\n\n- 作为插件管理工具，由于存储数据的局限性，无法很好地展示用户项目当前安装插件状态，并进行卸载等操作；\n- 当前插件管理高度依赖云端 registry 提供插件信息，在离线情况下完全无法使用；\n- 由于插件信息繁多，工具未能向用户展示充分的信息，交互复杂 体验较差。\n\n以上问题对用户使用 NB-CLI 管理项目插件造成了极大的阻碍。\n本项目希望重点针对插件管理部分，重构工具插件管理模块，完善框架缺陷，并通过缓存等方式确保可用性。其次，调研同类工具方案与 TUI 等相关技术，优化信息展示能力、用户交互方式，提升工具整体交互体验。\n\n**相关链接**\n\n- https://github.com/nonebot/nb-cli/issues/138\n- https://github.com/nonebot/nb-cli/issues/140\n\n**难度**：基础\n\n**导师**：[@yanyongyu](https://github.com/yanyongyu)\n\n**产出要求**\n\n- 重构 NB-CLI 插件管理模块\n  - 优化项目插件信息存储方式，支持列出、卸载插件等操作\n  - 通过缓存 registry 数据等方式确保离线场景的可用性\n- 提升 NB-CLI 交互体验\n  - 调研同类工具方案与 TUI 等相关技术\n  - 优化 registry 多字段信息展示能力\n  - 基于 TUI 等技术优化用户交互方式，提升整体交互体验\n\n**技术要求**\n\n- 熟练掌握 Python 及其异步编程\n- 熟悉 NoneBot 框架与 NB-CLI 使用方法\n- 了解 TUI 等终端交互技术\n\n**成果仓库**\n\n- <https://github.com/nonebot/nb-cli>\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/quick-start.mdx",
    "content": "---\nsidebar_position: 1\ndescription: 尝试使用 NoneBot\n\noptions:\n  menu:\n    - category: tutorial\n      weight: 10\n---\n\nimport Asciinema from \"@site/src/components/Asciinema\";\nimport Messenger from \"@site/src/components/Messenger\";\n\n# 快速上手\n\n:::caution 前提条件\n\n- 请确保你的 Python 版本 >= 3.9\n- **我们强烈建议使用虚拟环境进行开发**，如果没有使用虚拟环境，请确保已经卸载可能存在的 NoneBot v1！！！\n  ```bash\n  pip uninstall nonebot\n  ```\n\n:::\n\n在本章节中，我们将介绍如何使用脚手架来创建一个 NoneBot 简易项目。项目将基于 nb-cli 脚手架运行，并允许我们从商店安装插件。\n\n<Asciinema\n  url=\"https://asciinema.org/a/569440.cast\"\n  options={{ theme: \"monokai\", poster: \"npt:21.5\" }}\n/>\n\n## 安装脚手架\n\n确保你已经安装了 Python 3.9 及以上版本，然后在命令行中执行以下命令：\n\n1. 安装 [pipx](https://pypa.github.io/pipx/)\n\n   ```bash\n   python -m pip install --user pipx\n   python -m pipx ensurepath\n   ```\n\n   如果在此步骤的输出中出现了“open a new terminal”或者“re-login”字样，那么请关闭当前终端并重新打开一个新的终端。\n\n2. 安装脚手架\n\n   ```bash\n   pipx install nb-cli\n   ```\n\n安装完成后，你可以在命令行使用 `nb` 命令来使用脚手架。如果出现无法找到命令的情况（例如出现“Command not found”字样），请参考 [pipx 文档](https://pypa.github.io/pipx/) 检查你的环境变量。\n\n## 创建项目\n\n使用脚手架来创建一个项目：\n\n```bash\nnb create\n```\n\n这一指令将会执行创建项目的流程，你将会看到一些询问：\n\n1. 项目模板\n\n   ```bash\n   [?] 选择一个要使用的模板: bootstrap (初学者或用户)\n   ```\n\n   这里我们选择 `bootstrap` 模板，它是一个简单的项目模板，能够安装商店插件。如果你需要**自行编写插件**，这里请选择 `simple` 模板。\n\n2. 项目名称\n\n   ```bash\n   [?] 项目名称: awesome-bot\n   ```\n\n   这里我们以 `awesome-bot` 为例，作为项目名称。你可以根据自己的需要来命名。\n\n3. 其他选项\n   请注意，多选项使用**空格**选中或取消，**回车**确认。\n\n   ```bash\n   [?] 要使用哪些驱动器? FastAPI (FastAPI 驱动器)\n   [?] 要使用哪些适配器? Console (基于终端的交互式适配器)\n   [?] 立即安装依赖? (Y/n) Yes\n   [?] 创建虚拟环境? (Y/n) Yes\n   ```\n\n   这里我们选择了创建虚拟环境，nb-cli 在之后的操作中将会自动使用这个虚拟环境。如果你不需要自动创建虚拟环境或者已经创建了其他虚拟环境，nb-cli 将会安装依赖至当前激活的 Python 虚拟环境。\n\n4. 选择内置插件\n\n   ```bash\n   [?] 要使用哪些内置插件? echo\n   ```\n\n   这里我们选择 `echo` 插件作为示例。这是一个简单的复读回显插件，可以用于测试你的机器人是否正常运行。\n\n## 运行项目\n\n在项目创建完成后，你可以在**项目目录**中使用以下命令来运行项目：\n\n```bash\nnb run\n```\n\n你现在应该已经运行起来了你的第一个 NoneBot 项目了！请注意，生成的项目中使用了 `FastAPI` 驱动器和 `Console` 适配器，你之后可以自行修改配置或安装其他适配器。\n\n## 尝试使用\n\n在项目运行起来后，`Console` 适配器会在你的终端启动交互模式，你可以直接在输入框中输入 `/echo hello world` 来测试你的机器人是否正常运行。\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/echo hello world\" },\n    { position: \"left\", msg: \"hello world\" },\n  ]}\n/>\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/tutorial/application.md",
    "content": "---\nsidebar_position: 0\ndescription: 创建一个 NoneBot 项目\n\noptions:\n  menu:\n    - category: tutorial\n      weight: 20\n---\n\n# 手动创建项目\n\n在[快速上手](../quick-start.mdx)中，我们已经介绍了如何安装和使用 `nb-cli` 创建一个项目。在本章节中，我们将简要介绍如何在不使用 `nb-cli` 的方式创建一个机器人项目的**最小实例**并启动。如果你想要了解 NoneBot 的启动流程，也可以阅读本章节。\n\n:::caution 警告\n我们十分不推荐直接创建机器人项目，请优先考虑使用 nb-cli 进行项目创建。\n:::\n\n一个机器人项目的**最小实例**中**至少**需要包含以下内容:\n\n- 入口文件：初始化并运行机器人的 Python 文件\n- 配置文件：存储机器人启动所需的配置\n- 插件：为机器人提供具体的功能\n\n下面我们创建一个项目文件夹，来存放项目所需文件，以下步骤均在该文件夹中进行。\n\n## 安装依赖\n\n在创建项目前，我们首先需要将项目所需依赖安装至环境中。\n\n1. （可选）创建虚拟环境，以 venv 为例\n\n   ```bash\n   python -m venv .venv --prompt nonebot2\n   # windows\n   .venv\\Scripts\\activate\n   # linux/macOS\n   source .venv/bin/activate\n   ```\n\n2. 安装 nonebot2 以及驱动器\n\n   ```bash\n   pip install 'nonebot2[fastapi]'\n   ```\n\n   驱动器包名可以在 [驱动器商店](/store/drivers) 中找到。\n\n3. 安装适配器\n\n   ```bash\n   pip install nonebot-adapter-console\n   ```\n\n   适配器包名可以在 [适配器商店](/store/adapters) 中找到。\n\n## 创建配置文件\n\n配置文件用于存放 NoneBot 运行所需要的配置项，使用 [`pydantic`](https://docs.pydantic.dev/) 以及 [`python-dotenv`](https://saurabh-kumar.com/python-dotenv/) 来读取配置。配置项需符合 dotenv 格式，复杂类型数据需使用 JSON 格式填写。具体可选配置方式以及配置项详情参考[配置](../appendices/config.mdx)。\n\n在**项目文件夹**中创建一个 `.env` 文本文件，并写入以下内容:\n\n```bash title=.env\nHOST=0.0.0.0  # 配置 NoneBot 监听的 IP / 主机名\nPORT=8080  # 配置 NoneBot 监听的端口\nCOMMAND_START=[\"/\"]  # 配置命令起始字符\nCOMMAND_SEP=[\".\"]  # 配置命令分割字符\n```\n\n## 创建入口文件\n\n入口文件（ Entrypoint ）顾名思义，是用来初始化并运行机器人的 Python 文件。入口文件需要完成框架的初始化、注册适配器、加载插件等工作。\n\n:::tip 提示\n如果你使用 `nb-cli` 创建项目，入口文件不会被创建，该文件功能会被 `nb run` 命令代替。\n:::\n\n在**项目文件夹**中创建一个 `bot.py` 文件，并写入以下内容:\n\n```python title=bot.py\nimport nonebot\nfrom nonebot.adapters.console import Adapter as ConsoleAdapter  # 避免重复命名\n\n# 初始化 NoneBot\nnonebot.init()\n\n# 注册适配器\ndriver = nonebot.get_driver()\ndriver.register_adapter(ConsoleAdapter)\n\n# 在这里加载插件\nnonebot.load_builtin_plugins(\"echo\")  # 内置插件\n# nonebot.load_plugin(\"thirdparty_plugin\")  # 第三方插件\n# nonebot.load_plugins(\"awesome_bot/plugins\")  # 本地插件\n\nif __name__ == \"__main__\":\n    nonebot.run()\n```\n\n我们暂时不需要了解其中内容的含义，这些将会在稍后的章节中逐一介绍。在创建完成以上文件并确认已安装所需适配器和插件后，即可运行机器人。\n\n## 运行机器人\n\n在**项目文件夹**中，使用配置好环境的 Python 解释器运行入口文件（如果使用虚拟环境，请先激活虚拟环境）:\n\n```bash\npython bot.py\n```\n\n如果你后续使用了 `nb-cli` ，你仍可以使用 `nb run` 命令来运行机器人，`nb-cli` 会自动检测入口文件 `bot.py` 是否存在并运行。同时，你也可以使用 `nb run --reload` 来自动检测代码的更改并自动重新运行入口文件。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/tutorial/create-plugin.md",
    "content": "---\nsidebar_position: 3\ndescription: 创建并加载自定义插件\n\noptions:\n  menu:\n    - category: tutorial\n      weight: 50\n---\n\n# 插件编写准备\n\n在正式编写插件之前，我们需要先了解一下插件的概念。\n\n## 插件结构\n\n在 NoneBot 中，插件即是 Python 的一个[模块（module）](https://docs.python.org/zh-cn/3/glossary.html#term-module)。NoneBot 会在导入时对这些模块做一些特殊的处理使得他们成为一个插件。插件间应尽量减少耦合，可以进行有限制的相互调用，NoneBot 能够正确解析插件间的依赖关系。\n\n### 单文件插件\n\n一个普通的 `.py` 文件即可以作为一个插件，例如创建一个 `foo.py` 文件：\n\n```tree title=Project\n📂 plugins\n└── 📜 foo.py\n```\n\n这个时候模块 `foo` 已经可以被称为一个插件了，尽管它还什么都没做。\n\n### 包插件\n\n一个包含 `__init__.py` 的文件夹即是一个常规 Python [包 `package`](https://docs.python.org/zh-cn/3/glossary.html#term-regular-package)，例如创建一个 `foo` 文件夹：\n\n```tree title=Project\n📂 plugins\n└── 📂 foo\n    └── 📜 __init__.py\n```\n\n这个时候包 `foo` 同样是一个合法的插件，插件内容可以在 `__init__.py` 文件中编写。\n\n## 创建插件\n\n:::caution 注意\n如果在之前的[快速上手](../quick-start.mdx)章节中已经使用 `bootstrap` 模板创建了项目，那么你需要做出如下修改：\n\n1. 在项目目录中创建一个两层文件夹 `awesome_bot/plugins`\n\n   ```tree title=Project\n   📦 awesome-bot\n   ├── 📂 .venv\n   ├── 📂 awesome_bot\n   │   └── 📂 plugins\n   ├── 📜 .env.prod\n   ├── 📜 pyproject.toml\n   └── 📜 README.md\n   ```\n\n2. 修改 `pyproject.toml` 文件中的 `nonebot` 配置项，在 `plugin_dirs` 中添加 `awesome_bot/plugins`\n\n   ```toml title=pyproject.toml\n   [tool.nonebot]\n   plugin_dirs = [\"awesome_bot/plugins\"]\n   ```\n\n:::\n\n:::caution 注意\n如果在之前的[创建项目](./application.md)章节中手动创建了相关文件，那么你需要做出如下修改：\n\n1. 在项目目录中创建一个两层文件夹 `awesome_bot/plugins`\n\n   ```tree title=Project\n   📦 awesome-bot\n   ├── 📂 awesome_bot\n   │   └── 📂 plugins\n   └── 📜 bot.py\n   ```\n\n2. 修改 `bot.py` 文件中的加载插件部分，取消注释或者添加如下代码\n\n   ```python title=bot.py\n   # 在这里加载插件\n   nonebot.load_builtin_plugins(\"echo\")  # 内置插件\n   nonebot.load_plugins(\"awesome_bot/plugins\")  # 本地插件\n   ```\n\n:::\n\n创建插件可以通过 `nb-cli` 命令从完整模板创建，也可以手动新建空白文件。通过以下命令创建一个名为 `weather` 的插件：\n\n```bash\n$ nb plugin create\n[?] 插件名称: weather\n[?] 使用嵌套插件? (y/N) N\n[?] 请输入插件存储位置: awesome_bot/plugins\n```\n\n`nb-cli` 会在 `awesome_bot/plugins` 目录下创建一个名为 `weather` 的文件夹，其中包含的文件将在稍后章节中用到。\n\n```tree title=Project\n📦 awesome-bot\n├── 📂 .venv\n├── 📂 awesome_bot\n│   └── 📂 plugins\n|       └── 📂 weather\n|           ├── 📜 __init__.py\n|           └── 📜 config.py\n├── 📜 .env.prod\n├── 📜 pyproject.toml\n└── 📜 README.md\n```\n\n## 加载插件\n\n:::danger 警告\n请勿在插件被加载前 `import` 插件模块，这会导致 NoneBot 无法将其转换为插件而出现意料之外的情况。\n:::\n\n加载插件是在机器人入口文件中完成的，需要在框架初始化之后，运行之前进行。\n\n请注意，加载的插件模块名称（插件文件名或文件夹名）**不能相同**，且每一个插件**只能被加载一次**，重复加载将会导致异常。\n\n如果你使用 `nb-cli` 管理插件，那么你可以跳过这一节，`nb-cli` 将会自动处理加载。\n\n如果你**使用自定义的入口文件** `bot.py`，那么你需要在 `bot.py` 中加载插件。\n\n```python {5} title=bot.py\nimport nonebot\n\nnonebot.init()\n\n# 加载插件\n\nnonebot.run()\n```\n\n加载插件的方式有多种，但在底层的加载逻辑是一致的。以下是为加载插件提供的几种方式：\n\n### `load_plugin`\n\n通过点分割模块名称或使用 [`pathlib`](https://docs.python.org/zh-cn/3/library/pathlib.html) 的 `Path` 对象来加载插件。通常用于加载第三方插件或者项目插件。例如：\n\n```python\nfrom pathlib import Path\n\nnonebot.load_plugin(\"path.to.your.plugin\")  # 加载第三方插件\nnonebot.load_plugin(Path(\"./path/to/your/plugin.py\"))  # 加载项目插件\n```\n\n:::caution 注意\n请注意，本地插件的路径应该为相对机器人 **入口文件（通常为 bot.py）** 可导入的，例如在项目 `plugins` 目录下。\n:::\n\n### `load_plugins`\n\n加载传入插件目录中的所有插件，通常用于加载一系列本地编写的项目插件。例如：\n\n```python\nnonebot.load_plugins(\"src/plugins\", \"path/to/your/plugins\")\n```\n\n:::caution 注意\n请注意，插件目录应该为相对机器人 **入口文件（通常为 bot.py）** 可导入的，例如在项目 `plugins` 目录下。\n:::\n\n### `load_all_plugins`\n\n这种加载方式是以上两种方式的混合，加载所有传入的插件模块名称，以及所有给定目录下的插件。例如：\n\n```python\nnonebot.load_all_plugins([\"path.to.your.plugin\"], [\"path/to/your/plugins\"])\n```\n\n### `load_from_json`\n\n通过 JSON 文件加载插件，是 [`load_all_plugins`](#load_all_plugins) 的 JSON 变种。通过读取 JSON 文件中的 `plugins` 字段和 `plugin_dirs` 字段进行加载。例如：\n\n```json title=plugin_config.json\n{\n  \"plugins\": [\"path.to.your.plugin\"],\n  \"plugin_dirs\": [\"path/to/your/plugins\"]\n}\n```\n\n```python\nnonebot.load_from_json(\"plugin_config.json\", encoding=\"utf-8\")\n```\n\n:::tip 提示\n如果 JSON 配置文件中的字段无法满足你的需求，可以使用 [`load_all_plugins`](#load_all_plugins) 方法自行读取配置来加载插件。\n:::\n\n### `load_from_toml`\n\n通过 TOML 文件加载插件，是 [`load_all_plugins`](#load_all_plugins) 的 TOML 变种。通过读取 TOML 文件中的 `[tool.nonebot]` Table 中的 `plugin_dirs` Array 与\n`[tool.nonebot.plugins]` Table 中的多个 Array 进行加载。例如：\n\n```toml title=plugin_config.toml\n[tool.nonebot]\nplugin_dirs = [\"path/to/your/plugins\"]\n\n[tool.nonebot.plugins]\n\"@local\" = [\"path.to.your.plugin\"]  # 本地插件等非插件商店来源的插件\n\"nonebot-plugin-someplugin\" = [\"nonebot_plugin_someplugin\"]  # 插件商店来源的插件\n```\n\n```python\nnonebot.load_from_toml(\"plugin_config.toml\", encoding=\"utf-8\")\n```\n\n:::tip 提示\n如果 TOML 配置文件中的字段无法满足你的需求，可以使用 [`load_all_plugins`](#load_all_plugins) 方法自行读取配置来加载插件。\n:::\n\n### `load_builtin_plugin`\n\n加载一个内置插件，传入的插件名必须为 NoneBot 内置插件。该方法是 [`load_plugin`](#load_plugin) 的封装。例如：\n\n```python\nnonebot.load_builtin_plugin(\"echo\")\n```\n\n### `load_builtin_plugins`\n\n加载传入插件列表中的所有内置插件。例如：\n\n```python\nnonebot.load_builtin_plugins(\"echo\", \"single_session\")\n```\n\n### 其他加载方式\n\n有关其他插件加载的方式，可参考[跨插件访问](../advanced/requiring.md)和[嵌套插件](../advanced/plugin-nesting.md)。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/tutorial/event-data.mdx",
    "content": "---\nsidebar_position: 6\ndescription: 通过依赖注入获取所需事件信息\n\noptions:\n  menu:\n    - category: tutorial\n      weight: 80\n---\n\n# 获取事件信息\n\nimport Messenger from \"@site/src/components/Messenger\";\n\n在 NoneBot 事件处理流程中，获取事件信息并做出对应的操作是非常常见的场景。本章节中我们将介绍如何通过**依赖注入**获取事件信息。\n\n## 认识依赖注入\n\n在事件处理流程中，事件响应器具有自己独立的上下文，例如：当前响应的事件、收到事件的机器人或者其他处理流程中新增的信息等。这些数据可以根据我们的需求，通过依赖注入的方式，在执行事件处理流程中注入到事件处理函数中。\n\n相对于传统的信息获取方法，通过依赖注入获取信息的最大特色在于**按需获取**。如果该事件处理函数不需要任何额外信息即可运行，那么可以不进行依赖注入。如果事件处理函数需要额外的数据，可以通过依赖注入的方式灵活的标注出需要的依赖，在函数运行时便会被按需注入。\n\n## 使用依赖注入\n\n使用依赖注入获取上下文信息的方法十分简单，我们仅需要在函数的参数中声明所需的依赖，并正确的将函数添加为事件处理依赖即可。在 NoneBot 中，我们可以直接使用 `nonebot.params` 模块中定义的参数类型来声明依赖。\n\n例如，我们可以继续改进上一章节中的 `weather` 插件，使其可以获取到 `天气` 命令的地名参数，并根据地名返回天气信息。\n\n```python {9,11} title=weather/__init__.py\nfrom nonebot import on_command\nfrom nonebot.rule import to_me\nfrom nonebot.adapters import Message\nfrom nonebot.params import CommandArg\n\nweather = on_command(\"天气\", rule=to_me(), aliases={\"weather\", \"查天气\"}, priority=10, block=True)\n\n@weather.handle()\nasync def handle_function(args: Message = CommandArg()):\n    # 提取参数纯文本作为地名，并判断是否有效\n    if location := args.extract_plain_text():\n        await weather.finish(f\"今天{location}的天气是...\")\n    else:\n        await weather.finish(\"请输入地名\")\n```\n\n如上方示例所示，我们使用了 `args` 作为注入参数名，注入的内容为 `CommandArg()`，也就是**消息命令后跟随的内容**。在这个示例中，我们获得的参数会被检查是否有效，对无效参数则会结束事件。\n\n:::tip 提示\n命令与参数之间可以不需要空格，`CommandArg()` 获取的信息为命令后跟随的内容并去除了头部空白符。例如：`/天气 上海` 消息的参数为 `上海`。\n:::\n\n:::tip 提示\n`:=` 是 Python 3.8 引入的新语法 [Assignment Expressions](https://docs.python.org/zh-cn/3/reference/expressions.html#assignment-expressions)，也称为海象表达式，可以在表达式中直接赋值。\n:::\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/天气\" },\n    { position: \"left\", msg: \"请输入地名\" },\n    { position: \"right\", msg: \"/天气 上海\" },\n    { position: \"left\", msg: \"今天上海的天气是...\" },\n  ]}\n/>\n\nNoneBot 提供了多种依赖注入类型，可以获取不同的信息，具体内容可参考[依赖注入](../advanced/dependency.mdx)。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/tutorial/fundamentals.md",
    "content": "---\nsidebar_position: 1\ndescription: NoneBot 机器人构成及基本使用\n\noptions:\n  menu:\n    - category: tutorial\n      weight: 30\n---\n\n# 机器人的构成\n\n了解机器人的基本构成有助于你更好地使用 NoneBot，本章节将介绍 NoneBot 中的基本组成部分，稍后的文档中将会使用到这些概念。\n\n使用 NoneBot 框架搭建的机器人具有以下几个基本组成部分：\n\n1. NoneBot 机器人框架主体：负责连接各个组成部分，提供基本的机器人功能\n2. 驱动器 `Driver`：客户端/服务端的功能实现，负责接收和发送消息（通常为 HTTP 通信）\n3. 适配器 `Adapter`：驱动器的上层，负责将**平台消息**与 NoneBot 事件/操作系统的消息格式相互转换\n4. 插件 `Plugin`：机器人的功能实现，通常为负责处理事件并进行一系列的操作\n\n除 NoneBot 机器人框架主体外，其他部分均可按需选择、互相搭配，但由于平台的兼容性问题，部分插件可能仅在某些特定平台上可用（这由插件编写者决定）。\n\n在接下来的章节中，我们将重点介绍机器人功能实现，即插件 `Plugin` 部分。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/tutorial/handler.mdx",
    "content": "---\nsidebar_position: 5\ndescription: 处理接收到的特定事件\n\noptions:\n  menu:\n    - category: tutorial\n      weight: 70\n---\n\n# 事件处理\n\nimport Messenger from \"@site/src/components/Messenger\";\n\n在我们收到事件，并被某个事件响应器正确响应后，便正式开启了对于这个事件的**处理流程**。\n\n## 认识事件处理流程\n\n就像我们在解决问题时需要遵循流程一样，处理一个事件也需要一套流程。在事件响应器对一个事件进行响应之后，会依次执行一系列的**事件处理依赖**（通常是函数）。简单来说，事件处理流程并不是一个函数、一个对象或一个方法，而是一整套由开发者设计的流程。\n\n在这个流程中，我们**目前**只需要了解两个概念：函数形式的“事件处理依赖”（下称“事件处理函数”）和“事件响应器操作”。\n\n## 事件处理函数\n\n在事件响应器中，事件处理流程可以由一个或多个“事件处理函数”组成，这些事件处理函数将会按照顺序依次对事件进行处理，直到全部执行完成或被中断。我们可以采用事件响应器的“事件处理函数装饰器”来添加这些“事件处理函数”。\n\n顾名思义，“事件处理函数装饰器”是一个[装饰器（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)中所展示的包装用法相同。例如：\n\n```python {6-8} title=weather/__init__.py\nfrom nonebot import on_command\nfrom nonebot.rule import to_me\n\nweather = on_command(\"天气\", rule=to_me(), aliases={\"weather\", \"查天气\"}, priority=10, block=True)\n\n@weather.handle()\nasync def handle_function():\n    pass  # do something here\n```\n\n如上方示例所示，我们使用 `weather` 响应器的 `handle` 装饰器装饰了一个函数 `handle_function`。`handle_function` 函数会被添加到 `weather` 的事件处理流程中。在 `weather` 响应器被触发之后，将会依次调用 `weather` 响应器的事件处理函数，即 `handle_function` 来对事件进行处理。\n\n## 事件响应器操作\n\n在事件处理流程中，我们可以使用事件响应器操作来进行一些交互或改变事件处理流程，例如向机器人用户发送消息或提前结束事件处理流程等。\n\n事件响应器操作与事件处理函数装饰器类似，通常作为事件响应器 `Matcher` 的[类方法](https://docs.python.org/zh-cn/3/library/functions.html#classmethod)存在，因此事件响应器操作的调用方法也是 `Matcher.func()` 的形式。不过不同的是，事件响应器操作并不是装饰器，因此并不需要@进行标注。\n\n```python {8,9} title=weather/__init__.py\nfrom nonebot import on_command\nfrom nonebot.rule import to_me\n\nweather = on_command(\"天气\", rule=to_me(), aliases={\"weather\", \"查天气\"}, priority=10, block=True)\n\n@weather.handle()\nasync def handle_function():\n    # await weather.send(\"天气是...\")\n    await weather.finish(\"天气是...\")\n```\n\n如上方示例所示，我们使用 `weather` 响应器的 `finish` 操作方法向机器人用户回复了 `天气是...` 并结束了事件处理流程。效果如下：\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/天气\" },\n    { position: \"left\", msg: \"天气是...\" },\n  ]}\n/>\n\n值得注意的是，在执行 `finish` 方法时，NoneBot 会在向机器人用户发送消息内容后抛出 `FinishedException` 异常来结束事件响应流程。也就是说，在 `finish` 被执行后，后续的程序是不会被执行的。如果你需要回复机器人用户消息但不想事件处理流程结束，可以使用注释的部分中展示的 `send` 方法。\n\n:::danger 警告\n由于 `finish` 是通过抛出 `FinishedException` 异常来结束事件的，因此异常可能会被未加限制的 `try-except` 捕获，影响事件处理流程正确处理，导致无法正常结束此事件。请务必在异常捕获中指定错误类型或排除所有 [MatcherException](../api/exception.md#MatcherException) 类型的异常（如下所示），或将 `finish` 移出捕获范围进行使用。\n\n```python\nfrom nonebot.exception import MatcherException\n\ntry:\n    await weather.finish(\"天气是...\")\nexcept MatcherException:\n    raise\nexcept Exception as e:\n    pass # do something here\n```\n\n:::\n\n目前 NoneBot 提供了多种事件响应器操作，其中包括用于机器人用户交互与流程控制两大类，进阶使用方法可以查看[会话控制](../appendices/session-control.mdx)。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/tutorial/matcher.md",
    "content": "---\nsidebar_position: 4\ndescription: 响应接收到的特定事件\n\noptions:\n  menu:\n    - category: tutorial\n      weight: 60\n---\n\n# 事件响应器\n\n事件响应器（Matcher）是对接收到的事件进行响应的基本单元，所有的事件响应器都继承自 `Matcher` 基类。\n\n在 NoneBot 中，事件响应器可以通过一系列特定的规则**筛选**出**具有某种特征的事件**，并按照**特定的流程**交由**预定义的事件处理依赖**进行处理。例如，在[快速上手](../quick-start.mdx)中，我们使用了内置插件 `echo` ，它定义的事件响应器能响应机器人用户发送的“/echo hello world”消息，提取“hello world”信息并作为回复消息发送。\n\n## 事件响应器辅助函数\n\nNoneBot 中所有事件响应器均继承自 `Matcher` 基类，但直接使用 `Matcher.new()` 方法创建事件响应器过于繁琐且不能记录插件信息。因此，NoneBot 中提供了一系列“事件响应器辅助函数”（下称“辅助函数”）来辅助我们用**最简的方式**创建**带有不同规则预设**的事件响应器，提高代码可读性和书写效率。通常情况下，我们只需要使用辅助函数即可完成事件响应器的创建。\n\n在 NoneBot 中，辅助函数以 `on()` 或 `on_<type/rule>()` 形式出现（例如 `on_command()`），调用后根据不同的参数返回一个 `Type[Matcher]` 类型的新事件响应器。\n\n目前 NoneBot 提供了多种功能各异的辅助函数、具有共同命令名称前缀的命令组以及具有共同参数的响应器组，均可以从 `nonebot` 模块直接导入使用，具体内容参考[事件响应器进阶](../advanced/matcher.md)。\n\n## 创建事件响应器\n\n在上一节[创建插件](./create-plugin.md#创建插件)中，我们创建了一个 `weather` 插件，现在我们来实现他的功能。\n\n我们直接使用 `on_command()` 辅助函数来创建一个事件响应器：\n\n```python {3} title=weather/__init__.py\nfrom nonebot import on_command\n\nweather = on_command(\"天气\")\n```\n\n这样，我们就获得一个名为 `weather` 的事件响应器了，这个事件响应器会对 `/天气` 开头的消息进行响应。\n\n:::tip 提示\n如果一条消息中包含“@机器人”或以“机器人的昵称”开始，例如 `@bot /天气` 时，协议适配器会将 `event.is_tome()` 判断为 `True` ，同时也会自动去除 `@bot`，即事件响应器收到的信息内容为 `/天气`，方便进行命令匹配。\n:::\n\n### 为事件响应器添加参数\n\n在辅助函数中，我们可以添加一些参数来对事件响应器进行更加精细的调整，例如事件响应器的优先级、匹配规则等。例如：\n\n```python {4} title=weather/__init__.py\nfrom nonebot import on_command\nfrom nonebot.rule import to_me\n\nweather = on_command(\"天气\", rule=to_me(), aliases={\"weather\", \"查天气\"}, priority=10, block=True)\n```\n\n这样，我们就获得了一个可以响应 `天气`、`weather`、`查天气` 三个命令的响应规则，需要私聊或 `@bot` 时才会响应，优先级为 10（越小越优先），阻断事件向后续优先级传播的事件响应器了。这些内容的意义和使用方法将会在后续的章节中一一介绍。\n\n:::tip 提示\n需要注意的是，不同的辅助函数有不同的可选参数，在使用之前可以参考[事件响应器进阶 - 基本辅助函数](../advanced/matcher.md#基本辅助函数)或 [API 文档](../api/plugin/on.md#on)。\n:::\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/tutorial/message.md",
    "content": "---\nsidebar_position: 7\ndescription: 处理消息序列与消息段\n\noptions:\n  menu:\n    - category: tutorial\n      weight: 90\n---\n\n# 处理消息\n\n在不同平台中，一条消息可能会有承载有各种不同的表现形式，它可能是一段纯文本、一张图片、一段语音、一篇富文本文章，也有可能是多种类型的组合等等。\n\n在 NoneBot 中，为确保消息的正常处理与跨平台兼容性，采用了扁平化的消息序列形式，即 `Message` 对象。消息序列是 NoneBot 中的消息载体，无论是接收还是发送的消息，都采用消息序列的形式进行处理。\n\n## 认识消息类型\n\n### 消息序列 `Message`\n\n在 NoneBot 中，消息序列 `Message` 的主要作用是用于表达“一串消息”。由于消息序列继承自 `List[MessageSegment]`，所以 `Message` 的本质是由若干消息段所组成的序列。因此，消息序列的使用方法与 `List` 有很多相似之处，例如切片、索引、拼接等。\n\n在上一节的[使用依赖注入](./event-data.mdx#使用依赖注入)中，我们已经通过依赖注入 `CommandArg()` 获取了命令的参数，它的类型即是消息序列。我们使用了消息序列的 `extract_plain_text()` 方法来获取消息序列中的纯文本内容。\n\n### 消息段 `MessageSegment`\n\n顾名思义，消息段 `MessageSegment` 是一段消息。由于消息序列的本质是由若干消息段所组成的序列，消息段可以被认为是构成消息序列的最小单位。简单来说，消息序列类似于一个自然段，而消息段则是组成自然段的一句话。同时，作为特殊消息载体的存在，绝大多数的平台都有着**独特的消息类型**，这些独特的内容均需要由对应的**协议适配器**所提供，以适应不同平台中的消息模式。**这也意味着，你需要导入对应的协议适配器中的消息序列和消息段后才能使用其特殊的工厂方法。**\n\n:::caution 注意\n消息段的类型是由协议适配器提供的，因此你需要参考协议适配器的文档并导入对应的消息段后才能使用其特殊的消息类型。\n\n在上一节的[使用依赖注入](./event-data.mdx#使用依赖注入)中，我们导入的为 `nonebot.adapters.Message` 抽象基类，因此我们无法使用平台特有的消息类型。仅能使用 `str` 作为纯文本消息回复。\n:::\n\n## 使用消息序列\n\n:::caution 注意\n在以下的示例中，为了更好的理解多种类型的消息组成方式，我们将使用 `Console` 协议适配器来演示消息序列的使用方法。在实际使用中，你需要确保你使用的**消息序列类型**与你所要发送的**平台类型**一致。\n:::\n\n通常情况下，适配器在接收到消息时，会将消息转换为消息序列，可以通过依赖注入 [`EventMessage`](../advanced/dependency.mdx#eventmessage)，或者使用 `event.get_message()` 获取。\n\n由于消息序列是 `List[MessageSegment]` 的子类，所以你总是可以用和操作 `List` 类似的方式来处理消息序列。例如：\n\n```python\n>>> from nonebot.adapters.console import Message, MessageSegment\n>>> message = Message([\n    MessageSegment(type=\"text\", data={\"text\":\"hello\"}),\n    MessageSegment(type=\"markdown\", data={\"markup\":\"**world**\"}),\n])\n>>> for segment in message:\n...     print(segment.type, segment.data)\n...\ntext {'text': 'hello'}\nmarkdown {'markup': '**world**'}\n>>> len(message)\n2\n```\n\n### 构造消息序列\n\n在使用事件响应器操作发送消息时，既可以使用 `str` 作为消息，也可以使用 `Message`、`MessageSegment` 或者 `MessageTemplate`。那么，我们就需要先构造一个消息序列。消息序列可以通过多种方式构造：\n\n#### 直接构造\n\n`Message` 类可以直接实例化，支持 `str`、`MessageSegment`、`Iterable[MessageSegment]` 或适配器自定义类型的参数。\n\n```python\nfrom nonebot.adapters.console import Message, MessageSegment\n\n# str\nMessage(\"Hello, world!\")\n# MessageSegment\nMessage(MessageSegment.text(\"Hello, world!\"))\n# List[MessageSegment]\nMessage([MessageSegment.text(\"Hello, world!\")])\n```\n\n#### 运算构造\n\n`Message` 对象可以通过 `str`、`MessageSegment` 相加构造，详情请参考[拼接消息](#拼接消息)。\n\n#### 从字典数组构造\n\n`Message` 对象支持 Pydantic 自定义类型构造，可以使用 Pydantic 的 `TypeAdapter` 方法进行构造。\n\n```python\nfrom pydantic import TypeAdapter\nfrom nonebot.adapters.console import Message, MessageSegment\n\n# 由字典构造消息段\nTypeAdapter(MessageSegment).validate_python(\n    {\"type\": \"text\", \"data\": {\"text\": \"text\"}}\n) == MessageSegment.text(\"text\")\n\n# 由字典数组构造消息序列\nTypeAdapter(Message).validate_python(\n    [MessageSegment.text(\"text\"), {\"type\": \"text\", \"data\": {\"text\": \"text\"}}],\n) == Message([MessageSegment.text(\"text\"), MessageSegment.text(\"text\")])\n```\n\n### 获取消息纯文本\n\n由于消息中存在各种类型的消息段，因此 `str(message)` 通常**不能得到消息的纯文本**，而是一个消息序列的字符串表示。\n\nNoneBot 为消息段定义了一个方法 `is_text()` ，可以用于判断消息段是否为纯文本；也可以使用 `message.extract_plain_text()` 方法获取消息纯文本。\n\n```python\nfrom nonebot.adapters.console import Message, MessageSegment\n\n# 判断消息段是否为纯文本\nMessageSegment.text(\"text\").is_text() == True\n\n# 提取消息纯文本字符串\nMessage(\n    [MessageSegment.text(\"text\"), MessageSegment.markdown(\"**markup**\")]\n).extract_plain_text() == \"text\"\n```\n\n### 遍历\n\n消息序列继承自 `List[MessageSegment]` ，因此可以使用 `for` 循环遍历消息段。\n\n```python\nfor segment in message:\n    ...\n```\n\n### 比较\n\n消息和消息段都可以使用 `==` 或 `!=` 运算符比较是否相同。\n\n```python\nMessageSegment.text(\"text\") != MessageSegment.text(\"foo\")\n\nsome_message == Message([MessageSegment.text(\"text\")])\n```\n\n### 检查消息段\n\n我们可以通过 `in` 运算符或消息序列的 `has` 方法来：\n\n```python\n# 是否存在消息段\nMessageSegment.text(\"text\") in message\n# 是否存在指定类型的消息段\n\"text\" in message\n```\n\n我们还可以使用消息序列的 `only` 方法来检查消息中是否仅包含指定的消息段。\n\n```python\n# 是否都为指定消息段\nmessage.only(MessageSegment.text(\"test\"))\n# 是否仅包含指定类型的消息段\nmessage.only(\"text\")\n```\n\n### 过滤、索引与切片\n\n消息序列对列表的索引与切片进行了增强，在原有列表 `int` 索引与 `slice` 切片的基础上，支持 `type` 过滤索引与切片。\n\n```python\nfrom nonebot.adapters.console import Message, MessageSegment\n\nmessage = Message(\n    [\n        MessageSegment.text(\"test\"),\n        MessageSegment.markdown(\"test2\"),\n        MessageSegment.markdown(\"test3\"),\n        MessageSegment.text(\"test4\"),\n    ]\n)\n# 索引\nmessage[0] == MessageSegment.text(\"test\")\n# 切片\nmessage[0:2] == Message(\n    [MessageSegment.text(\"test\"), MessageSegment.markdown(\"test2\")]\n)\n# 类型过滤\nmessage[\"markdown\"] == Message(\n    [MessageSegment.markdown(\"test2\"), MessageSegment.markdown(\"test3\")]\n)\n# 类型索引\nmessage[\"markdown\", 0] == MessageSegment.markdown(\"test2\")\n# 类型切片\nmessage[\"markdown\", 0:2] == Message(\n    [MessageSegment.markdown(\"test2\"), MessageSegment.markdown(\"test3\")]\n)\n```\n\n我们也可以通过消息序列的 `include`、`exclude` 方法进行类型过滤。\n\n```python\nmessage.include(\"text\", \"markdown\")\nmessage.exclude(\"text\")\n```\n\n同样的，消息序列对列表的 `index`、`count` 方法也进行了增强，可以用于索引指定类型的消息段。\n\n```python\n# 指定类型首个消息段索引\nmessage.index(\"markdown\") == 1\n# 指定类型消息段数量\nmessage.count(\"markdown\") == 2\n```\n\n此外，消息序列添加了一个 `get` 方法，可以用于获取指定类型指定个数的消息段。\n\n```python\n# 获取指定类型指定个数的消息段\nmessage.get(\"markdown\", 1) == Message([MessageSegment.markdown(\"test2\")])\n```\n\n### 拼接消息\n\n`str`、`Message`、`MessageSegment` 对象之间可以直接相加，相加均会返回一个新的 `Message` 对象。\n\n```python\n# 消息序列与消息段相加\nMessage([MessageSegment.text(\"text\")]) + MessageSegment.text(\"text\")\n# 消息序列与字符串相加\nMessage([MessageSegment.text(\"text\")]) + \"text\"\n# 消息序列与消息序列相加\nMessage([MessageSegment.text(\"text\")]) + Message([MessageSegment.text(\"text\")])\n# 字符串与消息序列相加\n\"text\" + Message([MessageSegment.text(\"text\")])\n# 消息段与消息段相加\nMessageSegment.text(\"text\") + MessageSegment.text(\"text\")\n# 消息段与字符串相加\nMessageSegment.text(\"text\") + \"text\"\n# 消息段与消息序列相加\nMessageSegment.text(\"text\") + Message([MessageSegment.text(\"text\")])\n# 字符串与消息段相加\n\"text\" + MessageSegment.text(\"text\")\n```\n\n如果需要在当前消息序列后直接拼接新的消息段，可以使用 `Message.append`、`Message.extend` 方法，或者使用自加。\n\n```python\nmsg = Message([MessageSegment.text(\"text\")])\n# 自加\nmsg += \"text\"\nmsg += MessageSegment.text(\"text\")\nmsg += Message([MessageSegment.text(\"text\")])\n# 附加\nmsg.append(\"text\")\nmsg.append(MessageSegment.text(\"text\"))\n# 扩展\nmsg.extend([MessageSegment.text(\"text\")])\n```\n\n我们也可以通过消息段或消息序列的 `join` 方法来拼接一串消息：\n\n```python\nseg = MessageSegment.text(\"text\")\nmsg = seg.join(\n    [\n        MessageSegment.text(\"first\"),\n        Message(\n            [\n                MessageSegment.text(\"second\"),\n                MessageSegment.text(\"third\"),\n            ]\n        )\n    ]\n)\nmsg == Message(\n    [\n        MessageSegment.text(\"first\"),\n        MessageSegment.text(\"text\"),\n        MessageSegment.text(\"second\"),\n        MessageSegment.text(\"third\"),\n    ]\n)\n```\n\n### 使用消息模板\n\n为了提供安全可靠的跨平台模板字符，我们提供了一个消息模板功能来构建消息序列\n\n它在以下常见场景中尤其有用：\n\n- 多行富文本编排（包含图片，文字以及表情等）\n- 客制化（由 Bot 最终用户提供消息模板时）\n\n在事实上，它的用法和 `str.format` 极为相近，所以你在使用的时候，总是可以参考[Python 文档](https://docs.python.org/zh-cn/3/library/stdtypes.html#str.format)来达到你想要的效果，这里给出几个简单的例子。\n\n默认情况下，消息模板采用 `str` 纯文本形式的格式化：\n\n```python title=基础格式化用法\n>>> from nonebot.adapters import MessageTemplate\n>>> MessageTemplate(\"{} {}\").format(\"hello\", \"world\")\n'hello world'\n```\n\n如果 `Message.template` 构建消息模板，那么消息模板将采用消息序列形式的格式化，此时的消息将会是平台特定的：\n\n:::caution 注意\n使用 `Message.template` 构建消息模板时，应注意消息序列为平台适配器提供的类型，不能使用 `nonebot.adapters.Message` 基类作为模板构建。使用基类构建模板与使用 `str` 构建模板的效果是一样的，因此请使用上述的 `MessageTemplate` 类直接构建模板。：\n:::\n\n```python title=平台格式化用法\n>>> from nonebot.adapters.console import Message, MessageSegment\n>>> Message.template(\"{} {}\").format(\"hello\", \"world\")\nMessage(\n    MessageSegment.text(\"hello\"),\n    MessageSegment.text(\" \"),\n    MessageSegment.text(\"world\")\n)\n```\n\n消息模板支持使用消息段进行格式化：\n\n```python title=对消息段进行安全的拼接\n>>> from nonebot.adapters.console import Message, MessageSegment\n>>> Message.template(\"{}{}\").format(MessageSegment.markdown(\"**markup**\"), \"world\")\nMessage(\n    MessageSegment(type='markdown', data={'markup': '**markup**'}),\n    MessageSegment(type='text', data={'text': 'world'})\n)\n```\n\n消息模板同样支持使用消息序列作为模板：\n\n```python title=以消息对象作为模板\n>>> from nonebot.adapters.console import Message, MessageSegment\n>>> Message.template(\n...     MessageSegment.text(\"{user_id}\") + MessageSegment.emoji(\"tada\") +\n...     MessageSegment.text(\"{message}\")\n... ).format_map({\"user_id\": 123456, \"message\": \"hello world\"})\nMessage(\n    MessageSegment(type='text', data={'text': '123456'}),\n    MessageSegment(type='emoji', data={'emoji': 'tada'}),\n    MessageSegment(type='text', data={'text': 'hello world'})\n)\n```\n\n:::caution 注意\n只有消息序列中的文本类型消息段才能被格式化，其他类型的消息段将会原样添加。\n:::\n\n消息模板支持使用拓展控制符来控制消息段类型：\n\n```python title=使用消息段的拓展控制符\n>>> from nonebot.adapters.console import Message, MessageSegment\n>>> Message.template(\"{name:emoji}\").format(name='tada')\nMessage(MessageSegment(type='emoji', data={'name': 'tada'}))\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.3/tutorial/store.mdx",
    "content": "---\nsidebar_position: 2\ndescription: 从商店安装适配器和插件\n\noptions:\n  menu:\n    - category: tutorial\n      weight: 40\n---\n\n# 获取商店内容\n\nimport Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Asciinema from \"@site/src/components/Asciinema\";\n\n:::tip 提示\n\n如果你暂时没有获取商店内容的需求，可以跳过本章节。\n\n:::\n\nNoneBot 提供了一个[商店](/store/plugins)，商店内容均由社区开发者贡献。你可以在商店中查找你需要的适配器和插件等，进行安装或者参考其文档等。\n\n商店中每个内容的卡片都包含了其名称和简介等信息，点击**卡片右上角**链接图标即可跳转到其主页。\n\n## 安装插件\n\n<Asciinema\n  url=\"https://asciinema.org/a/569650.cast\"\n  options={{ theme: \"monokai\", poster: \"npt:16.8\" }}\n/>\n\n在商店插件页面中，点击你需要安装的插件下方的 `点击复制安装命令` 按钮，即可复制 `nb-cli` 命令。\n\n请在你的**项目目录**下执行该命令。`nb-cli` 会自动安装插件并将其添加到加载列表中。\n\n<Tabs groupId=\"cli-install\">\n  <TabItem value=\"cli\" label=\"使用命令\" default>\n\n```bash\nnb plugin install <插件名称>\n```\n\n  </TabItem>\n  <TabItem value=\"interactive\" label=\"交互式安装\">\n\n```bash\n$ nb plugin install\n[?] 想要安装的插件名称: <插件名称>\n```\n\n  </TabItem>\n  <TabItem value=\"pip\" label=\"使用 pip\">\n\n```bash\npip install <插件包名>\n```\n\n插件包名可以在商店插件卡片中找到，或者使用 `nb-cli` 搜索插件显示的详情中找到。安装完成后，需要参考[加载插件章节](./create-plugin.md#加载插件)自行加载。\n\n  </TabItem>\n</Tabs>\n\n如果想要查看插件列表，可以使用以下命令\n\n```bash\n# 列出商店所有插件\nnb plugin list\n# 搜索商店插件\nnb plugin search [可选关键词]\n```\n\n升级和卸载插件可以使用以下命令\n\n<Tabs groupId=\"cli-install\">\n  <TabItem value=\"cli\" label=\"使用命令\" default>\n\n```bash\nnb plugin update <插件名称>\nnb plugin uninstall <插件名称>\n```\n\n  </TabItem>\n  <TabItem value=\"interactive\" label=\"交互式安装\">\n\n```bash\n$ nb plugin update\n[?] 想要安装的插件名称: <插件名称>\n$ nb plugin uninstall\n[?] 想要卸载的插件名称: <插件名称>\n```\n\n  </TabItem>\n  <TabItem value=\"pip\" label=\"使用 pip\">\n\n```bash\npip install --upgrade <插件包名>\npip uninstall <插件包名>\n```\n\n插件包名可以在商店插件卡片中找到，或者使用 `nb-cli` 搜索插件显示的详情中找到。卸载完成后，需要自行移除插件加载。\n\n  </TabItem>\n</Tabs>\n\n## 安装适配器\n\n<Asciinema\n  url=\"https://asciinema.org/a/569664.cast\"\n  options={{ theme: \"monokai\", poster: \"npt:12.0\" }}\n/>\n\n安装适配器与安装插件类似，只是将命令换为 `nb adapter`，这里就不再赘述。\n\n请在你的**项目目录**下执行该命令。`nb-cli` 会自动安装适配器并将其添加到注册列表中。\n\n<Tabs groupId=\"cli-install\">\n  <TabItem value=\"cli\" label=\"使用命令\" default>\n\n```bash\nnb adapter install <适配器名称>\n```\n\n  </TabItem>\n  <TabItem value=\"interactive\" label=\"交互式安装\">\n\n```bash\n$ nb adapter install\n[?] 想要安装的适配器名称: <适配器名称>\n```\n\n  </TabItem>\n  <TabItem value=\"pip\" label=\"使用 pip\">\n\n```bash\npip install <适配器包名>\n```\n\n适配器包名可以在商店适配器卡片中找到，或者使用 `nb-cli` 搜索适配器显示的详情中找到。安装完成后，需要参考[注册适配器章节](../advanced/adapter.md#注册适配器)自行注册。\n\n  </TabItem>\n</Tabs>\n\n如果想要查看适配器列表，可以使用以下命令\n\n```bash\n# 列出商店所有适配器\nnb adapter list\n# 搜索商店适配器\nnb adapter search [可选关键词]\n```\n\n升级和卸载适配器可以使用以下命令\n\n<Tabs groupId=\"cli-install\">\n  <TabItem value=\"cli\" label=\"使用命令\" default>\n\n```bash\nnb adapter update <适配器名称>\nnb adapter uninstall <适配器名称>\n```\n\n  </TabItem>\n  <TabItem value=\"interactive\" label=\"交互式安装\">\n\n```bash\n$ nb adapter update\n[?] 想要安装的适配器名称: <适配器名称>\n$ nb adapter uninstall\n[?] 想要卸载的适配器名称: <适配器名称>\n```\n\n  </TabItem>\n  <TabItem value=\"pip\" label=\"使用 pip\">\n\n```bash\npip install --upgrade <适配器包名>\npip uninstall <适配器包名>\n```\n\n适配器包名可以在商店适配器卡片中找到，或者使用 `nb-cli` 搜索适配器显示的详情中找到。卸载完成后，需要自行移除适配器加载。\n\n  </TabItem>\n</Tabs>\n\n## 安装驱动器\n\n<Asciinema\n  url=\"https://asciinema.org/a/569665.cast\"\n  options={{ theme: \"monokai\", poster: \"npt:14.0\" }}\n/>\n\n安装驱动器与安装插件同样类似，只是将命令换为 `nb driver`，这里就不再赘述。\n\n如果你使用了虚拟环境，请在你的**项目目录**下执行该命令，`nb-cli` 会自动安装驱动器到虚拟环境中。\n\n请注意 `nb-cli` 并不会在安装驱动器后修改项目所使用的驱动器，请自行参考[配置方法](../appendices/config.mdx)章节以及 [`DRIVER` 配置项](../appendices/config.mdx#driver)修改驱动器。\n\n<Tabs groupId=\"cli-install\">\n  <TabItem value=\"cli\" label=\"使用命令\" default>\n\n```bash\nnb driver install <驱动器名称>\n```\n\n  </TabItem>\n  <TabItem value=\"interactive\" label=\"交互式安装\">\n\n```bash\n$ nb driver install\n[?] 想要安装的驱动器名称: <驱动器名称>\n```\n\n  </TabItem>\n  <TabItem value=\"pip\" label=\"使用 pip\">\n\n```bash\npip install <驱动器包名>\n```\n\n驱动器包名可以在商店驱动器卡片中找到，或者使用 `nb-cli` 搜索驱动器显示的详情中找到。\n\n  </TabItem>\n</Tabs>\n\n如果想要查看驱动器列表，可以使用以下命令\n\n```bash\n# 列出商店所有驱动器\nnb driver list\n# 搜索商店驱动器\nnb driver search [可选关键词]\n```\n\n升级和卸载驱动器可以使用以下命令\n\n<Tabs groupId=\"cli-install\">\n  <TabItem value=\"cli\" label=\"使用命令\" default>\n\n```bash\nnb driver update <驱动器名称>\nnb driver uninstall <驱动器名称>\n```\n\n  </TabItem>\n  <TabItem value=\"interactive\" label=\"交互式安装\">\n\n```bash\n$ nb driver update\n[?] 想要安装的驱动器名称: <驱动器名称>\n$ nb driver uninstall\n[?] 想要卸载的驱动器名称: <驱动器名称>\n```\n\n  </TabItem>\n  <TabItem value=\"pip\" label=\"使用 pip\">\n\n```bash\npip install --upgrade <驱动器包名>\npip uninstall <驱动器包名>\n```\n\n驱动器包名可以在商店驱动器卡片中找到，或者使用 `nb-cli` 搜索驱动器显示的详情中找到。卸载完成后，需要自行移除适配器加载。\n\n  </TabItem>\n</Tabs>\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/README.md",
    "content": "---\nsidebar_position: 0\nid: index\nslug: /\n---\n\n# 概览\n\nNoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架（下称 NoneBot），它基于 Python 的类型注解和异步优先特性（兼容同步），能够为你的需求实现提供便捷灵活的支持。同时，NoneBot 拥有大量的开发者为其开发插件，用户无需编写任何代码，仅需完成环境配置及插件安装，就可以正常使用 NoneBot。\n\n需要注意的是，NoneBot 仅支持 **Python 3.9 以上版本**\n\n## 特色\n\n### 异步优先\n\nNoneBot 基于 Python [asyncio](https://docs.python.org/zh-cn/3/library/asyncio.html) / [trio](https://trio.readthedocs.io/en/stable/) 编写，并在异步机制的基础上进行了一定程度的同步函数兼容。\n\n### 完整的类型注解\n\nNoneBot 参考 [PEP 484](https://www.python.org/dev/peps/pep-0484/) 等 PEP 完整实现了类型注解，通过 Pyright（Pylance） 检查。配合编辑器的类型推导功能，能将绝大多数的 Bug 杜绝在编辑器中（[编辑器支持](./editor-support)）。\n\n### 开箱即用\n\nNoneBot 提供了使用便捷、具有交互式功能的命令行工具--`nb-cli`，使得用户初次接触 NoneBot 时更容易上手。使用方法请阅读本文档[指南](./quick-start.mdx)以及 [CLI 文档](https://cli.nonebot.dev/)。\n\n### 插件系统\n\n插件系统是 NoneBot 的核心，通过它可以实现机器人的模块化以及功能扩展，便于维护和管理。\n\n### 依赖注入系统\n\nNoneBot 采用了一套自行定义的依赖注入系统，可以让事件的处理过程更加的简洁、清晰，增加代码的可读性，减少代码冗余。\n\n#### 什么是依赖注入\n\n[**『依赖注入』**](https://zh.wikipedia.org/wiki/%E6%8E%A7%E5%88%B6%E5%8F%8D%E8%BD%AC)意思是，在编程中，有一种方法可以让你的代码声明它工作和使用所需要的东西，即**『依赖』**。\n\n系统（在这里是指 NoneBot）将负责做任何需要的事情，为你的代码提供这些必要依赖（即**『注入』**依赖性）\n\n这在你有以下情形的需求时非常有用：\n\n- 这部分代码拥有共享的逻辑（同样的代码逻辑多次重复）\n- 共享数据库以及网络请求连接会话\n  - 比如 `httpx.AsyncClient`、`aiohttp.ClientSession` 和 `sqlalchemy.Session`\n- 机器人用户权限检查以及认证\n- 还有更多...\n\n它在完成上述工作的同时，还能尽量减少代码的耦合和重复\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/advanced/adapter.md",
    "content": "---\nsidebar_position: 1\ndescription: 注册适配器与指定平台交互\n\noptions:\n  menu:\n    - category: advanced\n      weight: 20\n---\n\n# 使用适配器\n\n适配器 (Adapter) 是机器人与平台交互的核心桥梁，它负责在驱动器和机器人插件之间转换与传递消息。\n\n## 适配器功能与组成\n\n适配器通常有两种功能，分别是**接收事件**和**调用平台接口**。其中，接收事件是指将驱动器收到的事件消息转换为 NoneBot 定义的事件模型，然后交由机器人插件处理；调用平台接口是指将机器人插件调用平台接口的数据转换为平台指定的格式，然后交由驱动器发送，并接收接口返回数据。\n\n为了实现这两种功能，适配器通常由四个部分组成：\n\n- **Adapter**：负责转换事件和调用接口，正确创建 Bot 对象并注册到 NoneBot 中。\n- **Bot**：负责存储平台机器人相关信息，并提供回复事件的方法。\n- **Event**：负责定义事件内容，以及事件主体对象。\n- **Message**：负责正确序列化消息，以便机器人插件处理。\n\n## 注册适配器\n\n在使用适配器之前，我们需要先将适配器注册到驱动器中，这样适配器就可以通过驱动器接收事件和调用接口了。我们以 Console 适配器为例，来看看如何注册适配器：\n\n```python {2,5} title=bot.py\nimport nonebot\nfrom nonebot.adapters.console import Adapter\n\ndriver = nonebot.get_driver()\ndriver.register_adapter(Adapter)\n```\n\n我们首先需要从适配器模块中导入所需要的适配器类，然后通过驱动器的 `register_adapter` 方法将适配器注册到驱动器中即可。如果我们需要多平台支持，可以多次调用 `register_adapter` 方法来注册多个适配器。\n\n## 获取已注册的适配器\n\nNoneBot 提供了 `get_adapter` 方法来获取已注册的适配器，我们可以通过适配器的名称或类型来获取指定的适配器实例：\n\n```python\nimport nonebot\nfrom nonebot.adapters.console import Adapter\n\nadapters = nonebot.get_adapters()\nconsole_adapter = nonebot.get_adapter(Adapter)\nconsole_adapter = nonebot.get_adapter(Adapter.get_name())\n```\n\n## 获取 Bot 对象\n\n当前所有适配器已连接的 Bot 对象可以通过 `get_bots` 方法获取，这是一个以机器人 ID 为键的字典：\n\n```python\nimport nonebot\n\nbots = nonebot.get_bots()\n```\n\n我们也可以通过 `get_bot` 方法获取指定 ID 的 Bot 对象。如果省略 ID 参数，将会返回所有 Bot 中的第一个：\n\n```python\nimport nonebot\n\nbot = nonebot.get_bot(\"bot_id\")\n```\n\n如果需要获取指定适配器连接的 Bot 对象，我们可以通过适配器的 `bots` 属性获取，这也是一个以机器人 ID 为键的字典：\n\n```python\nimport nonebot\nfrom nonebot.adapters.console import Adapter\n\nconsole_adapter = nonebot.get_adapter(Adapter)\nbots = console_adapter.bots\n```\n\nBot 对象都具有一个 `self_id` 属性，它是机器人的唯一 ID，由适配器填写，通常为机器人的帐号 ID 或者 APP ID。\n\n## 获取事件通用信息\n\n适配器的所有事件模型均继承自 `Event` 基类，在[事件类型与重载](../appendices/overload.md)一节中，我们也提到了如何使用基类抽象方法来获取事件通用信息。基类能提供如下信息：\n\n### 事件类型\n\n事件类型通常为 `meta_event`、`message`、`notice`、`request`。\n\n```python\ntype: str = event.get_type()\n```\n\n### 事件名称\n\n事件名称由适配器定义，通常用于日志记录。\n\n```python\nname: str = event.get_event_name()\n```\n\n### 事件描述\n\n事件描述由适配器定义，通常用于日志记录。\n\n```python\ndescription: str = event.get_event_description()\n```\n\n### 事件日志字符串\n\n事件日志字符串由事件名称和事件描述组成，用于日志记录。\n\n```python\nlog: str = event.get_log_string()\n```\n\n### 事件主体 ID\n\n事件主体 ID 通常为机器人用户 ID。\n\n```python\nuser_id: str = event.get_user_id()\n```\n\n### 事件会话 ID\n\n事件会话 ID 通常为机器人用户 ID 与群聊/频道 ID 组合而成。\n\n```python\nsession_id: str = event.get_session_id()\n```\n\n### 事件消息\n\n如果事件包含消息，则可以通过该方法获取，否则会产生异常。\n\n```python\nmessage: Message = event.get_message()\n```\n\n### 事件纯文本消息\n\n通常为事件消息的纯文本内容，如果事件不包含消息，则会产生异常。\n\n```python\ntext: str = event.get_plaintext()\n```\n\n### 事件是否与机器人有关\n\n由适配器实现的判断，通常将事件目标主体为机器人、消息中包含“@机器人”或以“机器人的昵称”开始视为与机器人有关。\n\n```python\nis_tome: bool = event.is_tome()\n```\n\n## 更多\n\n官方支持的适配器和社区贡献的适配器均可在[商店](/store/adapters)中查看。如果你想要开发自己的适配器，可以参考[开发文档](../developer/adapter-writing.md)。欢迎通过商店发布你的适配器。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/advanced/dependency.mdx",
    "content": "---\nsidebar_position: 6\ndescription: 通过依赖注入获取上下文信息\n\noptions:\n  menu:\n    - category: advanced\n      weight: 70\n---\n\n# 依赖注入\n\nimport Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\n\n在事件处理流程中，事件响应器具有自己独立的上下文，例如：当前的事件、机器人等信息。在 NoneBot 中，这些信息通过依赖注入的方式提供给事件处理函数，可以让代码更加整洁可读、提升复用能力。\n\n在了解如何使用依赖注入获取上下文信息之前，我们需要先了解两个概念：\n\n- `Dependent`：使用依赖注入的函数或其他任意可调用对象。如：事件处理函数、自定义的依赖函数等。\n- `Dependency`：依赖注入的对象。如：当前事件、机器人等。\n\n在之前的文档中，我们已经多次使用了依赖注入来获取事件信息。通过对函数参数依照一定规则填写类型注解，即可获得想要的上下文信息。任何一个事件处理函数在添加到事件处理流程时，都会根据一定规则提前将其解析成一个 `Dependent` 对象，方便运行时进行注入。如果遇到无法解析的参数，将会抛出 `ValueError(\"Unknown parameter\")` 的异常。整个依赖注入系统可以分为两部分：\n\n- 参数解析\n  - 依据一定规则解析函数参数，识别 `Dependency` 依赖。\n  - 生成 `Dependent` 对象。\n- 执行\n  - 根据已经解析的 `Dependency` 依赖，执行调用。\n  - 将所有 `Dependency` 的返回值根据参数名传入并调用 `Dependent` 。\n\n:::danger 警告\n在依赖注入中，类型注解是非常重要的，因为它不仅可以决定依赖注入的对象，还可以触发[重载机制](../appendices/overload.md#重载)。如果类型注解与实际获得数据类型不一致，将会跳过当前 `Dependent` 对象（即事件处理函数）。\n:::\n\n:::tip 提示\n如果对于依赖注入的解析流程有疑问，可以调整[日志等级配置项](../appendices/config.mdx#log-level)为 `TRACE`，查看依赖解析日志。\n:::\n\n## 同步支持\n\n对于依赖注入系统中的 `Dependent` 或者 `Dependency` 对象，均支持同步类型的函数或可调用对象。例如：\n\n```python {6,10}\nfrom nonebot import on_command\nfrom nonebot.params import Depends\n\nmatcher = on_command(\"foo\")\n\ndef dependency() -> str:\n    return \"something\"\n\n@matcher.handle()\ndef _(result: str = Depends(dependency)):\n    ...\n```\n\n## 非依赖参数\n\n在依赖注入解析中，任何无法解析的参数如果带有默认值，将会被视为非依赖参数。这些参数在依赖运行时将不会被注入而使用函数默认值。例如：\n\n```python\nasync def _(foo: str = \"bar\"): ...\n```\n\n## 类型依赖注入\n\n这一类的依赖注入仅需要在函数参数中添加对应的类型注解即可。\n\n### Bot\n\n获取当前事件的 Bot 对象。\n\n通过标注参数为 `Bot` 类型，或者一系列 `Bot` 类型，即可获取到当前事件的 Bot 对象。为兼容性考虑，如果参数名为 `bot` 且无类型注解，也会视为 Bot 依赖注入。\n\nBot 依赖注入支持重载（即：可以标注参数为子类型）且具有[重载优先检查权](../appendices/overload.md#重载)。\n\n<Tabs groupId=\"python\">\n  <TabItem value=\"3.10\" label=\"Python 3.10+\" default>\n\n```python\nfrom nonebot.adapters import Bot\nfrom nonebot.adapters.console import Bot as ConsoleBot\nfrom nonebot.adapters.onebot.v11 import Bot as OneBotV11Bot\n\nasync def _(foo: Bot): ...\nasync def _(foo: ConsoleBot | OneBotV11Bot): ...\nasync def _(bot): ...  # 兼容性处理\n```\n\n  </TabItem>\n  <TabItem value=\"3.9\" label=\"Python 3.9\">\n\n```python\nfrom typing import Union\n\nfrom nonebot.adapters import Bot\nfrom nonebot.adapters.console import Bot as ConsoleBot\nfrom nonebot.adapters.onebot.v11 import Bot as OneBotV11Bot\n\nasync def _(foo: Bot): ...\nasync def _(foo: Union[ConsoleBot, OneBotV11Bot]): ...\nasync def _(bot): ...  # 兼容性处理\n```\n\n  </TabItem>\n</Tabs>\n\n### Event\n\n获取当前事件。\n\n通过标注参数为 `Event` 类型，或者一系列 `Event` 类型，即可获取到当前事件。为兼容性考虑，如果参数名为 `event` 且无类型注解，也会视为 Event 依赖注入。\n\nEvent 依赖注入支持重载（即：可以标注参数为子类型）且具有[重载优先检查权](../appendices/overload.md#重载)。\n\n<Tabs groupId=\"python\">\n  <TabItem value=\"3.10\" label=\"Python 3.10+\" default>\n\n```python\nfrom nonebot.adapters import Event\nfrom nonebot.adapters.onebot.v11 import PrivateMessageEvent, GroupMessageEvent\n\nasync def _(foo: Event): ...\nasync def _(foo: PrivateMessageEvent | GroupMessageEvent): ...\nasync def _(event): ...  # 兼容性处理\n```\n\n  </TabItem>\n  <TabItem value=\"3.9\" label=\"Python 3.9\">\n\n```python\nfrom typing import Union\n\nfrom nonebot.adapters import Event\nfrom nonebot.adapters.onebot.v11 import PrivateMessageEvent, GroupMessageEvent\n\nasync def _(foo: Event): ...\nasync def _(foo: Union[PrivateMessageEvent, GroupMessageEvent]): ...\nasync def _(event): ...  # 兼容性处理\n```\n\n  </TabItem>\n</Tabs>\n\n### State\n\n获取当前[会话状态](../appendices/session-state.md)。\n\n通过标注参数为 `T_State` 类型，即可获取到当前会话状态。为兼容性考虑，如果参数名为 `state` 且无类型注解，也会视为 State 依赖注入。\n\n```python\nfrom nonebot.typing import T_State\n\nasync def _(foo: T_State): ...\n```\n\n### Matcher\n\n获取当前事件响应器实例。常用于使用[事件响应器操作](../appendices/session-control.mdx)。\n\n通过标注参数为 `Matcher` 类型，或者一系列 `Matcher` 类型，即可获取到当前事件。为兼容性考虑，如果参数名为 `matcher` 且无类型注解，也会视为 Matcher 依赖注入。\n\nMatcher 依赖注入支持重载（即：可以标注参数为子类型）且具有[重载优先检查权](../appendices/overload.md#重载)。\n\n```python\nfrom nonebot.matcher import Matcher\n\nasync def _(foo: Matcher): ...\nasync def _(matcher): ...  # 兼容性处理\n```\n\n### Exception\n\n获取事件响应器运行中抛出的异常。该依赖注入目前仅在事件响应器运行后处理 Hook 中可用。\n\n通过标注参数为异常类型，或者一系列异常类型，即可获取到事件响应器运行中抛出的异常。\n\n<Tabs groupId=\"python\">\n  <TabItem value=\"3.10\" label=\"Python 3.10+\" default>\n\n```python {5,8}\nfrom nonebot.message import run_postprocessor\nfrom nonebot.exception import ActionFailed, NetworkError\n\n@run_postprocessor\nasync def _(e: Exception): ...\n\n@run_postprocessor\nasync def _(e: ActionFailed | NetworkError): ...\n```\n\n  </TabItem>\n  <TabItem value=\"3.9\" label=\"Python 3.9\">\n\n```python {6,9}\nfrom typing import Union\nfrom nonebot.message import run_postprocessor\nfrom nonebot.exception import ActionFailed, NetworkError\n\n@run_postprocessor\nasync def _(e: Exception): ...\n\n@run_postprocessor\nasync def _(e: Union[ActionFailed, NetworkError]): ...\n```\n\n  </TabItem>\n</Tabs>\n\n## 子依赖\n\n在依赖注入系统中，我们可以定义一个子依赖，来执行自定义的操作，提高代码复用性以及处理性能。\n\n### 定义子依赖\n\n子依赖使用 `Depends` 标记进行定义，其参数即依赖的函数或可调用对象，同样会被解析为 `Dependent` 对象，将会在依赖注入期间执行。我们来看一个例子：\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {5,15}\nfrom typing import Annotated\n\nfrom nonebot import on_command\nfrom nonebot.adapters import Event\nfrom nonebot.params import Depends\n\ntest = on_command(\"test\")\n\nasync def check(event: Event) -> Event:\n    if event.get_user_id() in BLACKLIST:\n        await test.finish()\n    return event\n\n@test.handle()\nasync def _(event: Annotated[Event, Depends(check)]):\n    ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {3,13}\nfrom nonebot import on_command\nfrom nonebot.adapters import Event\nfrom nonebot.params import Depends\n\ntest = on_command(\"test\")\n\nasync def check(event: Event) -> Event:\n    if event.get_user_id() in BLACKLIST:\n        await test.finish()\n    return event\n\n@test.handle()\nasync def _(event: Event = Depends(check)):\n    ...\n```\n\n  </TabItem>\n</Tabs>\n\n在上面的代码中，我们使用 `Depends` 标记定义了一个子依赖 `check`。它判断事件主体用户是否在黑名单中，如果在，则直接结束事件处理流程。如果不在，则返回事件对象，以便事件处理函数可以继续执行。\n\n通过将 `Depends` 包裹的子依赖作为参数的默认值，我们就可以在执行事件处理函数之前执行子依赖，并将其返回值作为参数传入事件处理函数。子依赖和普通的事件处理函数并没有区别，同样可以使用依赖注入，并且可以返回任何类型的值。但需要注意的是，如果事件处理函数参数的类型注解与子依赖返回值的类型**不一致**，将会触发[重载](../appendices/overload.md)而跳过当前事件处理函数。\n\n特别的，我们可以为 `Dependent` 对象定义一系列前置子依赖，它们会在参数执行前被顺序执行，且返回值将会被忽略，例如：\n\n```python {11}\nfrom nonebot import on_command\nfrom nonebot.adapters import Event\nfrom nonebot.params import Depends\n\ntest = on_command(\"test\")\n\nasync def check(event: Event):\n    if event.get_user_id() in BLACKLIST:\n        await test.finish()\n\n@test.handle(parameterless=[Depends(check)])\nasync def _():\n    ...\n```\n\n### 依赖缓存\n\nNoneBot 在执行子依赖时，会将其返回值缓存起来。当我们在使用子依赖时，`Depends` 具有一个参数 `use_cache`，默认为 `True`。此时在事件处理流程中，多次使用同一个子依赖时，将会使用缓存中的结果而不会重复执行。这在很多情景中非常有用，例如：\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {7}\nimport random\nfrom typing import Annotated\n\nasync def random_result() -> int:\n    return random.randint(1, 100)\n\nasync def _(x: Annotated[int, Depends(random_result)]):\n    print(x)\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {6}\nimport random\n\nasync def random_result() -> int:\n    return random.randint(1, 100)\n\nasync def _(x: int = Depends(random_result)):\n    print(x)\n```\n\n  </TabItem>\n</Tabs>\n\n此时，在同一事件处理流程中，这个随机函数的返回值将会保持一致。如果我们希望每次都重新执行子依赖，可以将 `use_cache` 设置为 `False`。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {7}\nimport random\nfrom typing import Annotated\n\nasync def random_result() -> int:\n    return random.randint(1, 100)\n\nasync def _(x: Annotated[int, Depends(random_result, use_cache=False)]):\n    print(x)\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {6}\nimport random\n\nasync def random_result() -> int:\n    return random.randint(1, 100)\n\nasync def _(x: int = Depends(random_result, use_cache=False)):\n    print(x)\n```\n\n  </TabItem>\n</Tabs>\n\n:::tip 提示\n缓存的生命周期与当前接收到的事件相同。接收到事件后，子依赖在首次执行时缓存，在该事件处理完成后，缓存就会被清除。\n:::\n\n### 类型转换与校验\n\n在依赖注入系统中，我们可以对子依赖的返回值进行自动类型转换与校验。这个功能由 Pydantic 支持，因此我们通过参数类型注解自动使用 Pydantic 支持的类型转换。例如：\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {6,9}\nfrom typing import Annotated\n\nfrom nonebot.params import Depends\nfrom nonebot.adapters import Event\n\ndef get_user_id(event: Event) -> str:\n    return event.get_user_id()\n\nasync def _(user_id: Annotated[int, Depends(get_user_id, validate=True)]):\n    print(user_id)\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {4,7}\nfrom nonebot.params import Depends\nfrom nonebot.adapters import Event\n\ndef get_user_id(event: Event) -> str:\n    return event.get_user_id()\n\nasync def _(user_id: int = Depends(get_user_id, validate=True)):\n    print(user_id)\n```\n\n  </TabItem>\n</Tabs>\n\n在进行类型自动转换的同时，Pydantic 还支持对数据进行更多的限制，如：大于、小于、长度等。使用方法如下：\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {7,10}\nfrom typing import Annotated\n\nfrom pydantic import Field\nfrom nonebot.params import Depends\nfrom nonebot.adapters import Event\n\ndef get_user_id(event: Event) -> str:\n    return event.get_user_id()\n\nasync def _(user_id: Annotated[int, Depends(get_user_id, validate=Field(gt=100))]):\n    print(user_id)\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {5,8}\nfrom pydantic import Field\nfrom nonebot.params import Depends\nfrom nonebot.adapters import Event\n\ndef get_user_id(event: Event) -> str:\n    return event.get_user_id()\n\nasync def _(user_id: int = Depends(get_user_id, validate=Field(gt=100))):\n    print(user_id)\n```\n\n  </TabItem>\n</Tabs>\n\n### 类作为依赖\n\n在前面的事例中，我们使用了函数作为子依赖。实际上，我们还可以使用类作为依赖。当我们在实例化一个类的时候，其实我们就在调用它，类本身也是一个可调用对象。例如：\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {16}\nfrom typing import Annotated\nfrom dataclasses import dataclass\n\nfrom nonebot.params import Depends\nfrom nonebot.adapters import Event\nfrom nonebot.typing import T_State\n\ndef get_context(state: T_State) -> dict:\n    return state.setdefault(\"context\", {})\n\n@dataclass\nclass ClassDependency:\n    event: Event\n    context: dict = Depends(get_context)\n\nasync def _(data: Annotated[ClassDependency, Depends(ClassDependency)]):\n    print(data.event, data.context)\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {15}\nfrom dataclasses import dataclass\n\nfrom nonebot.params import Depends\nfrom nonebot.adapters import Event\nfrom nonebot.typing import T_State\n\ndef get_context(state: T_State) -> dict:\n    return state.setdefault(\"context\", {})\n\n@dataclass\nclass ClassDependency:\n    event: Event\n    context: dict = Depends(get_context)\n\nasync def _(data: ClassDependency = Depends(ClassDependency)):\n    print(data.event, data.context)\n```\n\n  </TabItem>\n</Tabs>\n\n可以看到，我们使用 `dataclass` 定义了一个类。由于这个类的 `__init__` 方法可以被依赖注入系统解析，因此，我们可以将其作为子依赖进行声明。特别地，对于类依赖，`Depends` 的参数可以为空，NoneBot 将会使用参数的类型注解进行解析与推断：\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python\nfrom typing import Annotated\n\nasync def _(data: Annotated[ClassDependency, Depends()]):\n    print(data.event, data.context)\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python\nasync def _(data: ClassDependency = Depends()):\n    print(data.event, data.context)\n```\n\n  </TabItem>\n</Tabs>\n\n### 生成器作为依赖\n\nNoneBot 的依赖注入支持依赖项在事件处理流程结束后进行一些额外的工作，比如数据库 session 或者网络 IO 的关闭，互斥锁的解锁等等。同时，由于[依赖缓存](#依赖缓存)的存在，我们可以通过这种方式来实现共享一个 session 等功能。\n\n要实现上述功能，我们可以用生成器函数作为依赖项，我们用 `yield` 关键字取代 `return` 关键字，并在 `yield` 之后进行额外的工作。\n\n我们可以看下述代码段, 使用 `httpx.AsyncClient` 异步网络 IO，并在事件处理流程中共用一个 client：\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {15}\nfrom typing import Annotated\nfrom collections.abc import AsyncGenerator\n\nimport httpx\nfrom nonebot.params import Depends\n\nasync def get_client() -> AsyncGenerator[httpx.AsyncClient, None]:\n    try:\n        async with httpx.AsyncClient() as client:\n            yield client\n    finally:\n        # 在这里进行额外的工作\n\n\n@test.handle()\nasync def _(x: Annotated[httpx.AsyncClient, Depends(get_client)]):\n    resp = await x.get(\"https://nonebot.dev\")\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {15}\nfrom collections.abc import AsyncGenerator\n\nimport httpx\nfrom nonebot.params import Depends\n\nasync def get_client() -> AsyncGenerator[httpx.AsyncClient, None]:\n    try:\n        async with httpx.AsyncClient() as client:\n            yield client\n    finally:\n        # 在这里进行额外的工作\n\n\n@test.handle()\nasync def _(x: httpx.AsyncClient = Depends(get_client)):\n    resp = await x.get(\"https://nonebot.dev\")\n```\n\n  </TabItem>\n</Tabs>\n\n:::caution 注意\n生成器作为依赖时，其中只能进行一次 `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 内部就使用了这两个装饰器。\n:::\n\n### 可调用对象作为依赖\n\n在 Python 里，为类定义 `__call__` 方法就可以使得这个类的实例成为一个可调用对象。因此，我们也可以将定义了 `__call__` 方法的类的实例作为依赖。事实上，NoneBot 的[内置响应规则](./matcher.md#内置响应规则)就广泛使用了这种方式，以 `is_type` 规则为例：\n\n```python\nfrom nonebot.adapters import Event\n\nclass IsTypeRule:\n    def __init__(self, *types: type[Event]):\n        self.types = types\n\n    async def __call__(self, event: Event) -> bool:\n        return isinstance(event, self.types)\n```\n\n我们在使用 `is_type` 时，即实例化了 `IsTypeRule` 类，然后将实例作为响应规则依赖项传入。\n\n## 其他依赖注入\n\n这一类的依赖注入通常基于子依赖编写，为我们开发者提供更方便的途径获取上下文信息。\n\n### EventType\n\n获取当前事件的类型。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot.params import EventType\n\nasync def _(foo: Annotated[str, EventType()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {3}\nfrom nonebot.params import EventType\n\nasync def _(foo: str = EventType()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### EventMessage\n\n获取当前事件的消息。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {5}\nfrom typing import Annotated\nfrom nonebot.adapters import Message\nfrom nonebot.params import EventMessage\n\nasync def _(foo: Annotated[Message, EventMessage()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {4}\nfrom nonebot.adapters import Message\nfrom nonebot.params import EventMessage\n\nasync def _(foo: Message = EventMessage()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### EventPlainText\n\n获取当前事件的消息纯文本部分。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot.params import EventPlainText\n\nasync def _(foo: Annotated[str, EventPlainText()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {3}\nfrom nonebot.params import EventPlainText\n\nasync def _(foo: str = EventPlainText()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### EventToMe\n\n获取当前事件是否与机器人相关。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot.params import EventToMe\n\nasync def _(foo: Annotated[bool, EventToMe()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {3}\nfrom nonebot.params import EventToMe\n\nasync def _(foo: bool = EventToMe()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### Command\n\n获取当前命令型消息的元组形式命令名。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot.params import Command\n\nasync def _(foo: Annotated[tuple[str, ...], Command()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {4}\nfrom nonebot.params import Command\n\nasync def _(foo: tuple[str, ...] = Command()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n:::tip 提示\n命令详情只能在**触发命令型事件响应器时**获取。如果在事件处理后续流程中获取，则会获取到不同的值。\n:::\n\n### RawCommand\n\n获取当前命令型消息的文本形式命令名。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot.params import RawCommand\n\nasync def _(foo: Annotated[str, RawCommand()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {3}\nfrom nonebot.params import RawCommand\n\nasync def _(foo: str = RawCommand()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n:::tip 提示\n命令详情只能在**触发命令型事件响应器时**获取。如果在事件处理后续流程中获取，则会获取到不同的值。\n:::\n\n### CommandArg\n\n获取命令型消息命令后跟随的参数。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {5}\nfrom typing import Annotated\nfrom nonebot.adapters import Message\nfrom nonebot.params import CommandArg\n\nasync def _(foo: Annotated[Message, CommandArg()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {4}\nfrom nonebot.adapters import Message\nfrom nonebot.params import CommandArg\n\nasync def _(foo: Message = CommandArg()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n:::tip 提示\n命令详情只能在**触发命令型事件响应器时**获取。如果在事件处理后续流程中获取，则会获取到不同的值。\n:::\n\n### CommandStart\n\n获取命令型消息命令前缀。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot.params import CommandStart\n\nasync def _(foo: Annotated[str, CommandStart()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {3}\nfrom nonebot.params import CommandStart\n\nasync def _(foo: str = CommandStart()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n:::tip 提示\n命令详情只能在**触发命令型事件响应器时**获取。如果在事件处理后续流程中获取，则会获取到不同的值。\n:::\n\n### CommandWhitespace\n\n获取命令型消息命令与参数间空白符。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot.params import CommandWhitespace\n\nasync def _(foo: Annotated[str, CommandWhitespace()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {3}\nfrom nonebot.params import CommandWhitespace\n\nasync def _(foo: str = CommandWhitespace()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n:::tip 提示\n命令详情只能在**触发命令型事件响应器时**获取。如果在事件处理后续流程中获取，则会获取到不同的值。\n:::\n\n### ShellCommandArgv\n\n获取 shell 命令解析前的参数列表，列表中可能包含文本字符串和富文本消息段（如：图片）。当词法解析出错的时候，返回值将为 `None`。通过重载机制即可处理两种不同的情况。\n\n<Tabs groupId=\"python\">\n  <TabItem value=\"3.10\" label=\"Python 3.10+\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot import on_shell_command\nfrom nonebot.params import ShellCommandArgv\n\nmatcher = on_shell_command(\"cmd\")\n\n# 解析失败\n@matcher.handle()\nasync def _(foo: Annotated[None, ShellCommandArgv()]): ...\n\n# 解析成功\n@matcher.handle()\nasync def _(foo: Annotated[list[str | MessageSegment], ShellCommandArgv()]): ...\n```\n\n```python {4}\nfrom nonebot import on_shell_command\nfrom nonebot.params import ShellCommandArgv\n\nmatcher = on_shell_command(\"cmd\")\n\n# 解析失败\n@matcher.handle()\nasync def _(foo: None = ShellCommandArgv()): ...\n\n# 解析成功\n@matcher.handle()\nasync def _(foo: list[str | MessageSegment] = ShellCommandArgv()): ...\n```\n\n  </TabItem>\n  <TabItem value=\"3.9\" label=\"Python 3.9\">\n\n```python {4}\nfrom typing import Union, Annotated\nfrom nonebot import on_shell_command\nfrom nonebot.params import ShellCommandArgv\n\nmatcher = on_shell_command(\"cmd\")\n\n# 解析失败\n@matcher.handle()\nasync def _(foo: Annotated[None, ShellCommandArgv()]): ...\n\n# 解析成功\n@matcher.handle()\nasync def _(foo: Annotated[list[Union[str, MessageSegment]], ShellCommandArgv()]): ...\n```\n\n```python {4}\nfrom typing import Union\nfrom nonebot import on_shell_command\nfrom nonebot.params import ShellCommandArgv\n\nmatcher = on_shell_command(\"cmd\")\n\n# 解析失败\n@matcher.handle()\nasync def _(foo: None = ShellCommandArgv()): ...\n\n# 解析成功\n@matcher.handle()\nasync def _(foo: list[Union[str, MessageSegment]] = ShellCommandArgv()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### ShellCommandArgs\n\n获取 shell 命令解析后的参数 Namespace，支持 MessageSegment 富文本（如：图片）。\n\n:::tip 提示\n如果参数解析成功，则为 parser 返回的 Namespace；如果参数解析失败，则为 [`ParserExit`](../api/exception.md#ParserExit) 异常，并携带错误码与错误信息。在前置词法解析失败时，返回值也为 [`ParserExit`](../api/exception.md#ParserExit) 异常。通过重载机制即可处理两种不同的情况。\n\n由于 `ArgumentParser` 在解析到 `--help` 参数时也会抛出异常，这种情况下错误码为 `0` 且错误信息即为帮助信息。\n:::\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {14,22}\nfrom typing import Annotated\n\nfrom nonebot import on_shell_command\nfrom nonebot.exception import ParserExit\nfrom nonebot.params import ShellCommandArgs\nfrom nonebot.rule import Namespace, ArgumentParser\n\nparser = ArgumentParser(\"demo\")\n# parser.add_argument ...\nmatcher = on_shell_command(\"cmd\", parser=parser)\n\n# 解析失败\n@matcher.handle()\nasync def _(foo: Annotated[ParserExit, ShellCommandArgs()]):\n    if foo.status == 0:\n        foo.message  # help message\n    else:\n        foo.message  # error message\n\n# 解析成功\n@matcher.handle()\nasync def _(foo: Annotated[Namespace, ShellCommandArgs()]):\n    arg_dict = vars(foo)\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {12,20}\nfrom nonebot import on_shell_command\nfrom nonebot.exception import ParserExit\nfrom nonebot.params import ShellCommandArgs\nfrom nonebot.rule import Namespace, ArgumentParser\n\nparser = ArgumentParser(\"demo\")\n# parser.add_argument ...\nmatcher = on_shell_command(\"cmd\", parser=parser)\n\n# 解析失败\n@matcher.handle()\nasync def _(foo: ParserExit = ShellCommandArgs()):\n    if foo.status == 0:\n        foo.message  # help message\n    else:\n        foo.message  # error message\n\n# 解析成功\n@matcher.handle()\nasync def _(foo: Namespace = ShellCommandArgs()):\n    arg_dict = vars(foo)\n```\n\n  </TabItem>\n</Tabs>\n\n### RegexMatched\n\n获取正则匹配结果的对象。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {5}\nfrom re import Match\nfrom typing import Annotated\nfrom nonebot.params import RegexMatched\n\nasync def _(foo: Annotated[Match[str], RegexMatched()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {4}\nfrom re import Match\nfrom nonebot.params import RegexMatched\n\nasync def _(foo: Match[str] = RegexMatched()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### RegexStr\n\n获取正则匹配结果的文本。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot.params import RegexStr\n\nasync def _(foo: Annotated[str, RegexStr()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {3}\nfrom nonebot.params import RegexStr\n\nasync def _(foo: str = RegexStr()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### RegexGroup\n\n获取正则匹配结果的 group 元组。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Any, Annotated\nfrom nonebot.params import RegexGroup\n\nasync def _(foo: Annotated[tuple[Any, ...], RegexGroup()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {4}\nfrom typing import Any\nfrom nonebot.params import RegexGroup\n\nasync def _(foo: tuple[Any, ...] = RegexGroup()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### RegexDict\n\n获取正则匹配结果的 group 字典。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Any, Annotated\nfrom nonebot.params import RegexDict\n\nasync def _(foo: Annotated[dict[str, Any], RegexDict()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {4}\nfrom typing import Any\nfrom nonebot.params import RegexDict\n\nasync def _(foo: dict[str, Any] = RegexDict()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### Startswith\n\n获取触发响应器的消息前缀字符串。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot.params import Startswith\n\nasync def _(foo: Annotated[str, Startswith()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {3}\nfrom nonebot.params import Startswith\n\nasync def _(foo: str = Startswith()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### Endswith\n\n获取触发响应器的消息后缀字符串。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot.params import Endswith\n\nasync def _(foo: Annotated[str, Endswith()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {3}\nfrom nonebot.params import Endswith\n\nasync def _(foo: str = Endswith()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### Fullmatch\n\n获取触发响应器的消息字符串。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot.params import Fullmatch\n\nasync def _(foo: Annotated[str, Fullmatch()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {3}\nfrom nonebot.params import Fullmatch\n\nasync def _(foo: str = Fullmatch()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### Keyword\n\n获取触发响应器的关键字字符串。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {4}\nfrom typing import Annotated\nfrom nonebot.params import Keyword\n\nasync def _(foo: Annotated[str, Keyword()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {3}\nfrom nonebot.params import Keyword\n\nasync def _(foo: str = Keyword()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### Received\n\n获取某次 `receive` 接收的事件。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {7}\nfrom typing import Annotated\n\nfrom nonebot.adapters import Event\nfrom nonebot.params import Received\n\n@matcher.receive(\"id\")\nasync def _(foo: Annotated[Event, Received(\"id\")]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {5}\nfrom nonebot.adapters import Event\nfrom nonebot.params import Received\n\n@matcher.receive(\"id\")\nasync def _(foo: Event = Received(\"id\")): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### LastReceived\n\n获取最近一次 `receive` 接收的事件。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {7}\nfrom typing import Annotated\n\nfrom nonebot.adapters import Event\nfrom nonebot.params import LastReceived\n\n@matcher.receive(\"any\")\nasync def _(foo: Annotated[Event, LastReceived()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {5}\nfrom nonebot.adapters import Event\nfrom nonebot.params import LastReceived\n\n@matcher.receive(\"any\")\nasync def _(foo: Event = LastReceived()): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### ReceivePromptResult\n\n获取某次 `receive` 发送提示消息的结果。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {6}\nfrom typing import Any, Annotated\n\nfrom nonebot.params import ReceivePromptResult\n\n@matcher.receive(\"id\", prompt=\"prompt\")\nasync def _(result: Annotated[Any, ReceivePromptResult(\"id\")]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {6}\nfrom typing import Any\n\nfrom nonebot.params import ReceivePromptResult\n\n@matcher.receive(\"id\", prompt=\"prompt\")\nasync def _(result: Any = ReceivePromptResult(\"id\")): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### Arg\n\n获取某次 `got` 接收的参数。如果 `Arg` 参数留空，则使用函数的参数名作为要获取的参数。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {7,8}\nfrom typing import Annotated\n\nfrom nonebot.params import Arg\nfrom nonebot.adapters import Message\n\n@matcher.got(\"key\")\nasync def _(key: Annotated[Message, Arg()]): ...\nasync def _(foo: Annotated[Message, Arg(\"key\")]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {5,6}\nfrom nonebot.params import Arg\nfrom nonebot.adapters import Message\n\n@matcher.got(\"key\")\nasync def _(key: Message = Arg()): ...\nasync def _(foo: Message = Arg(\"key\")): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### ArgStr\n\n获取某次 `got` 接收的参数，并转换为字符串。如果 `Arg` 参数留空，则使用函数的参数名作为要获取的参数。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {6,7}\nfrom typing import Annotated\n\nfrom nonebot.params import ArgStr\n\n@matcher.got(\"key\")\nasync def _(key: Annotated[str, ArgStr()]): ...\nasync def _(foo: Annotated[str, ArgStr(\"key\")]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {4,5}\nfrom nonebot.params import ArgStr\n\n@matcher.got(\"key\")\nasync def _(key: str = ArgStr()): ...\nasync def _(foo: str = ArgStr(\"key\")): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### ArgPlainText\n\n获取某次 `got` 接收的参数的纯文本部分。如果 `Arg` 参数留空，则使用函数的参数名作为要获取的参数。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {6,7}\nfrom typing import Annotated\n\nfrom nonebot.params import ArgPlainText\n\n@matcher.got(\"key\")\nasync def _(key: Annotated[str, ArgPlainText()]): ...\nasync def _(foo: Annotated[str, ArgPlainText(\"key\")]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {4,5}\nfrom nonebot.params import ArgPlainText\n\n@matcher.got(\"key\")\nasync def _(key: str = ArgPlainText()): ...\nasync def _(foo: str = ArgPlainText(\"key\")): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### ArgPromptResult\n\n获取某次 `got` 发送提示消息的结果。如果 `Arg` 参数留空，则使用函数的参数名作为要获取的参数。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {6,7}\nfrom typing import Any, Annotated\n\nfrom nonebot.params import ArgPromptResult\n\n@matcher.got(\"key\", prompt=\"prompt\")\nasync def _(result: Annotated[Any, ArgPromptResult()]): ...\nasync def _(result: Annotated[Any, ArgPromptResult(\"key\")]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {6,7}\nfrom typing import Any\n\nfrom nonebot.params import ArgPromptResult\n\n@matcher.got(\"key\", prompt=\"prompt\")\nasync def _(result: Any = ArgPromptResult()): ...\nasync def _(result: Any = ArgPromptResult(\"key\")): ...\n```\n\n  </TabItem>\n</Tabs>\n\n### PausePromptResult\n\n获取最近一次 `pause` 发送提示消息的结果。\n\n<Tabs groupId=\"annotated\">\n  <TabItem value=\"annotated\" label=\"Use Annotated\" default>\n\n```python {6}\nfrom typing import Any, Annotated\n\nfrom nonebot.params import PausePromptResult\n\n@matcher.handle()\nasync def _():\n    await matcher.pause(prompt=\"prompt\")\n\n@matcher.handle()\nasync def _(result: Annotated[Any, PausePromptResult()]): ...\n```\n\n  </TabItem>\n  <TabItem value=\"no-annotated\" label=\"Without Annotated\">\n\n```python {6}\nfrom typing import Any\n\nfrom nonebot.params import PausePromptResult\n\n@matcher.handle()\nasync def _():\n    await matcher.pause(prompt=\"prompt\")\n\n@matcher.handle()\nasync def _(result: Any = PausePromptResult()): ...\n```\n\n  </TabItem>\n</Tabs>\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/advanced/driver.md",
    "content": "---\nsidebar_position: 0\ndescription: 选择合适的驱动器运行机器人\n\noptions:\n  menu:\n    - category: advanced\n      weight: 10\n---\n\n# 选择驱动器\n\n驱动器 (Driver) 是机器人运行的基石，它是机器人初始化的第一步，主要负责数据收发。\n\n:::important 提示\n驱动器的选择通常与机器人所使用的协议适配器相关，如果不知道该选择哪个驱动器，可以先阅读相关协议适配器文档说明。\n:::\n\n:::tip 提示\n如何**安装**驱动器请参考[安装驱动器](../tutorial/store.mdx#安装驱动器)。\n:::\n\n## 驱动器类型\n\n驱动器类型大体上可以分为两种：\n\n- `Forward`：即客户端型驱动器，多用于使用 HTTP 轮询，连接 WebSocket 服务器等情形。\n- `Reverse`：即服务端型驱动器，多用于使用 WebHook，接收 WebSocket 客户端连接等情形。\n\n客户端型驱动器可以分为以下两种：\n\n1. 异步发送 HTTP 请求，自定义 `HTTP Method`、`URL`、`Header`、`Body`、`Cookie`、`Proxy`、`Timeout` 等。\n2. 异步建立 WebSocket 连接上下文，自定义 `WebSocket URL`、`Header`、`Cookie`、`Proxy`、`Timeout` 等。\n\n服务端型驱动器目前有：\n\n1. ASGI 应用框架，具有以下功能：\n   - 协议适配器自定义 HTTP 上报地址以及对上报数据处理的回调函数。\n   - 协议适配器自定义 WebSocket 连接请求地址以及对 WebSocket 请求处理的回调函数。\n   - 用户可以向 ASGI 应用添加任何服务端相关功能，如：[添加自定义路由](./routing.md)。\n\n## 配置驱动器\n\n驱动器的配置方法已经在[配置](../appendices/config.mdx)章节中简单进行了介绍，这里将详细介绍驱动器配置的格式。\n\nNoneBot 中的客户端和服务端型驱动器可以相互配合使用，但服务端型驱动器**仅能选择一个**。所有驱动器模块都会包含一个 `Driver` 子类，即驱动器类，他可以作为驱动器单独运行。同时，客户端驱动器模块中还会提供一个 `Mixin` 子类，用于在与其他驱动器配合使用时加载。因此，驱动器配置格式采用特殊语法：`<module>[:<Driver>][+<module>[:<Mixin>]]*`。\n\n其中，`<module>` 代表**驱动器模块路径**；`<Driver>` 代表**驱动器类名**，默认为 `Driver`；`<Mixin>` 代表**驱动器混入类名**，默认为 `Mixin`。即，我们需要选择一个主要驱动器，然后在其基础上配合使用其他驱动器的功能。主要驱动器可以为客户端或服务端类型，但混入类驱动器只能为客户端类型。\n\n特别的，为了简化内置驱动器模块路径，我们可以使用 `~` 符号作为内置驱动器模块路径的前缀，如 `~fastapi` 代表使用内置驱动器 `fastapi`。NoneBot 内置了多个驱动器适配，但需要安装额外依赖才能使用，具体请参考[安装驱动器](../tutorial/store.mdx#安装驱动器)。常见的驱动器配置如下：\n\n```dotenv\nDRIVER=~fastapi\nDRIVER=~aiohttp\nDRIVER=~httpx+~websockets\nDRIVER=~fastapi+~httpx+~websockets\n```\n\n## 获取驱动器\n\n在 NoneBot 框架初始化完成后，我们就可以通过 `get_driver()` 方法获取全局驱动器实例：\n\n```python\nfrom nonebot import get_driver\n\ndriver = get_driver()\n```\n\n## 内置驱动器\n\n### None\n\n**类型：**服务端驱动器\n\nNoneBot 内置的空驱动器，不提供任何收发数据功能，可以在不需要外部网络连接时使用。\n\n```env\nDRIVER=~none\n```\n\n### FastAPI（默认）\n\n**类型：**ASGI 服务端驱动器\n\n> FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints.\n\n[FastAPI](https://fastapi.tiangolo.com/) 是一个易上手、高性能的异步 Web 框架，具有极佳的编写体验。 FastAPI 可以通过类型注解、依赖注入等方式实现输入参数校验、自动生成 API 文档等功能，也可以挂载其他 ASGI、WSGI 应用。\n\n```env\nDRIVER=~fastapi\n```\n\n#### FastAPI 配置项\n\n##### `fastapi_openapi_url`\n\n类型：`str | None`  \n默认值：`None`  \n说明：`FastAPI` 提供的 `OpenAPI` JSON 定义地址，如果为 `None`，则不提供 `OpenAPI` JSON 定义。\n\n##### `fastapi_docs_url`\n\n类型：`str | None`  \n默认值：`None`  \n说明：`FastAPI` 提供的 `Swagger` 文档地址，如果为 `None`，则不提供 `Swagger` 文档。\n\n##### `fastapi_redoc_url`\n\n类型：`str | None`  \n默认值：`None`  \n说明：`FastAPI` 提供的 `ReDoc` 文档地址，如果为 `None`，则不提供 `ReDoc` 文档。\n\n##### `fastapi_include_adapter_schema`\n\n类型：`bool`  \n默认值：`True`  \n说明：`FastAPI` 提供的 `OpenAPI` JSON 定义中是否包含适配器路由的 `Schema`。\n\n##### `fastapi_reload`\n\n:::caution 警告\n不推荐开启该配置项，在 Windows 平台上开启该功能有可能会造成预料之外的影响！替代方案：使用 `nb-cli` 命令行工具以及参数 `--reload` 启动 NoneBot。\n\n```bash\nnb run --reload\n```\n\n开启该功能后，在 uvicorn 运行时（FastAPI 提供的 ASGI 底层，即 reload 功能的实际来源），asyncio 使用的事件循环会被 uvicorn 从默认的 `ProactorEventLoop` 强制切换到 `SelectorEventLoop`。\n\n> 相关信息参考 [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)\n\n后者（`SelectorEventLoop`）在 Windows 平台的可使用性不如前者（`ProactorEventLoop`），包括但不限于\n\n1. 不支持创建子进程\n2. 最多只支持 512 个套接字\n3. ...\n\n> 具体信息参考 [Python 文档](https://docs.python.org/zh-cn/3/library/asyncio-platforms.html#windows)\n\n所以，一些使用了 asyncio 的库因此可能无法正常工作，如：\n\n1. [playwright](https://playwright.dev/python/docs/library#incompatible-with-selectoreventloop-of-asyncio-on-windows)\n\n如果在开启该功能后，原本**正常运行**的代码报错，且打印的异常堆栈信息和 asyncio 有关（异常一般为 `NotImplementedError`），\n你可能就需要考虑相关库对事件循环的支持，以及是否启用该功能。\n:::\n\n类型：`bool`  \n默认值：`False`  \n说明：是否开启 `uvicorn` 的 `reload` 功能，需要在机器人入口文件提供 ASGI 应用路径。\n\n```python title=bot.py\napp = nonebot.get_asgi()\nnonebot.run(app=\"bot:app\")\n```\n\n##### `fastapi_reload_dirs`\n\n类型：`List[str] | None`  \n默认值：`None`  \n说明：重载监控文件夹列表，默认为 uvicorn 默认值\n\n##### `fastapi_reload_delay`\n\n类型：`float | None`  \n默认值：`None`  \n说明：重载延迟，默认为 uvicorn 默认值\n\n##### `fastapi_reload_includes`\n\n类型：`List[str] | None`  \n默认值：`None`  \n说明：要监听的文件列表，支持 glob pattern，默认为 uvicorn 默认值\n\n##### `fastapi_reload_excludes`\n\n类型：`List[str] | None`  \n默认值：`None`  \n说明：不要监听的文件列表，支持 glob pattern，默认为 uvicorn 默认值\n\n##### `fastapi_extra`\n\n类型：`Dist[str, Any]`  \n默认值：`{}`  \n说明：传递给 `FastAPI` 的其他参数\n\n### Quart\n\n**类型：**ASGI 服务端驱动器\n\n> Quart is an asyncio reimplementation of the popular Flask microframework API.\n\n[Quart](https://quart.palletsprojects.com/) 是一个类 Flask 的异步版本，拥有与 Flask 非常相似的接口和使用方法。\n\n```env\nDRIVER=~quart\n```\n\n#### Quart 配置项\n\n##### `quart_reload`\n\n:::caution 警告\n不推荐开启该配置项，在 Windows 平台上开启该功能有可能会造成预料之外的影响！替代方案：使用 `nb-cli` 命令行工具以及参数 `--reload` 启动 NoneBot。\n\n```bash\nnb run --reload\n```\n\n:::\n\n类型：`bool`  \n默认值：`False`  \n说明：是否开启 `uvicorn` 的 `reload` 功能，需要在机器人入口文件提供 ASGI 应用路径。\n\n```python title=bot.py\napp = nonebot.get_asgi()\nnonebot.run(app=\"bot:app\")\n```\n\n##### `quart_reload_dirs`\n\n类型：`List[str] | None`  \n默认值：`None`  \n说明：重载监控文件夹列表，默认为 uvicorn 默认值\n\n##### `quart_reload_delay`\n\n类型：`float | None`  \n默认值：`None`  \n说明：重载延迟，默认为 uvicorn 默认值\n\n##### `quart_reload_includes`\n\n类型：`List[str] | None`  \n默认值：`None`  \n说明：要监听的文件列表，支持 glob pattern，默认为 uvicorn 默认值\n\n##### `quart_reload_excludes`\n\n类型：`List[str] | None`  \n默认值：`None`  \n说明：不要监听的文件列表，支持 glob pattern，默认为 uvicorn 默认值\n\n##### `quart_extra`\n\n类型：`Dist[str, Any]`  \n默认值：`{}`  \n说明：传递给 `Quart` 的其他参数\n\n### HTTPX\n\n**类型：**HTTP 客户端驱动器\n\n:::caution 注意\n本驱动器仅支持 HTTP 请求，不支持 WebSocket 连接请求。\n:::\n\n> [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.\n\n```env\nDRIVER=~httpx\n```\n\n### websockets\n\n**类型：**WebSocket 客户端驱动器\n\n:::caution 注意\n本驱动器仅支持 WebSocket 连接请求，不支持 HTTP 请求。\n:::\n\n> [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.\n\n```env\nDRIVER=~websockets\n```\n\n### AIOHTTP\n\n**类型：**HTTP/WebSocket 客户端驱动器\n\n> [AIOHTTP](https://docs.aiohttp.org/): Asynchronous HTTP Client/Server for asyncio and Python.\n\n```env\nDRIVER=~aiohttp\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/advanced/matcher-provider.md",
    "content": "---\nsidebar_position: 10\ndescription: 自定义事件响应器存储\n\noptions:\n  menu:\n    - category: advanced\n      weight: 110\n---\n\n# 事件响应器存储\n\n事件响应器是 NoneBot 处理事件的核心，它们默认存储在一个字典中。在进入会话状态后，事件响应器将会转为临时响应器，作为最高优先级同样存储于该字典中。因此，事件响应器的存储类似于会话存储，它决定了整个 NoneBot 对事件的处理行为。\n\nNoneBot 默认使用 Python 的字典将事件响应器存储于内存中，但是我们也可以自定义事件响应器存储，将事件响应器存储于其他地方，例如 Redis 等。这样我们就可以实现持久化、在多实例间共享会话状态等功能。\n\n## 编写存储提供者\n\n事件响应器的存储提供者 `MatcherProvider` 抽象类继承自 `MutableMapping[int, list[type[Matcher]]]`，即以优先级为键，以事件响应器列表为值的映射。我们可以方便地进行逐优先级事件传播。\n\n编写一个自定义的存储提供者，只需要继承并实现 `MatcherProvider` 抽象类：\n\n```python\nfrom nonebot.matcher import MatcherProvider\n\nclass CustomProvider(MatcherProvider):\n    ...\n```\n\n## 设置存储提供者\n\n我们可以通过 `matchers.set_provider` 方法设置存储提供者：\n\n```python {3}\nfrom nonebot.matcher import matchers\n\nmatchers.set_provider(CustomProvider)\n\nassert isinstance(matchers.provider, CustomProvider)\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/advanced/matcher.md",
    "content": "---\nsidebar_position: 5\ndescription: 事件响应器组成与内置响应规则\n\noptions:\n  menu:\n    - category: advanced\n      weight: 60\n---\n\n# 事件响应器进阶\n\n在[指南](../tutorial/matcher.md)与[深入](../appendices/rule.md)中，我们已经介绍了事件响应器的基本用法以及响应规则、权限控制等功能。在这一节中，我们将介绍事件响应器的组成，内置的响应规则，与第三方响应规则拓展。\n\n:::tip 提示\n事件响应器允许继承，你可以通过直接继承 `Matcher` 类来创建一个新的事件响应器。\n:::\n\n## 事件响应器组成\n\n### 事件响应器类型\n\n事件响应器类型 `type` 即是该响应器所要响应的事件类型，只有在接收到的事件类型与该响应器的类型相同时，才会触发该响应器。如果类型为空字符串 `\"\"`，则响应器将会响应所有类型的事件。事件响应器类型的检查在所有其他检查（权限控制、响应规则）之前进行。\n\nNoneBot 内置了四种常用事件类型：`meta_event`、`message`、`notice`、`request`，分别对应元事件、消息、通知、请求。通常情况下，协议适配器会将事件合理地分类至这四种类型中。如果有其他类型的事件需要响应，可以自行定义新的类型。\n\n### 事件触发权限\n\n事件触发权限 `permission` 是一个 `Permission` 对象，这在[权限控制](../appendices/permission.mdx)一节中已经介绍过。事件触发权限会在事件响应器的类型检查通过后进行检查，如果权限检查通过，则执行响应规则检查。\n\n### 事件响应规则\n\n事件响应规则 `rule` 是一个 `Rule` 对象，这在[响应规则](../appendices/rule.md)一节中已经介绍过。事件响应器的响应规则会在事件响应器的权限检查通过后进行匹配，如果响应规则检查通过，则触发该响应器。\n\n### 响应优先级\n\n响应优先级 `priority` 是一个正整数，用于指定响应器的优先级。响应器的优先级越小，越先被触发。如果响应器的优先级相同，则按照响应器的注册顺序进行触发。\n\n### 阻断\n\n阻断 `block` 是一个布尔值，用于指定响应器是否阻断事件的传播。如果阻断为 `True`，则在该响应器被触发后，事件将不会再传播给其他下一优先级的响应器。\n\nNoneBot 内置的事件响应器中，所有非 `command` 规则的 `message` 类型的事件响应器都会阻断事件传递，其他则不会。\n\n在部分情况中，可以使用 [`stop_propagation`](../appendices/session-control.mdx#stop_propagation) 方法动态阻止事件传播，该方法需要 handler 在参数中获取 matcher 实例后调用方法。\n\n### 有效期\n\n事件响应器的有效期分为 `temp` 和 `expire_time` 。`temp` 是一个布尔值，用于指定响应器是否为临时响应器。如果为 `True`，则该响应器在被触发后会被自动销毁。`expire_time` 是一个 `datetime` 对象，用于指定响应器的过期时间。如果 `expire_time` 不为 `None`，则在该时间点后，该响应器会被自动销毁。\n\n### 默认状态\n\n事件响应器的默认状态 `default_state` 是一个 `dict` 对象，用于指定响应器的默认状态。在响应器被触发时，响应器将会初始化默认状态然后开始执行事件处理流程。\n\n## 基本辅助函数\n\nNoneBot 为四种类型的事件响应器提供了五个基本的辅助函数：\n\n- `on`：创建任何类型的事件响应器。\n- `on_metaevent`：创建元事件响应器。\n- `on_message`：创建消息事件响应器。\n- `on_request`：创建请求事件响应器。\n- `on_notice`：创建通知事件响应器。\n\n除了 `on` 函数具有一个 `type` 参数外，其余参数均相同：\n\n- `rule`：响应规则，可以是 `Rule` 对象或者 `RuleChecker` 函数。\n- `permission`：事件触发权限，可以是 `Permission` 对象或者 `PermissionChecker` 函数。\n- `handlers`：事件处理函数列表。\n- `temp`：是否为临时响应器。\n- `expire_time`：响应器的过期时间。\n- `priority`：响应器的优先级。\n- `block`：是否阻断事件传播。\n- `state`：响应器的默认状态。\n\n在消息类型的事件响应器的基础上，NoneBot 还内置了一些常用的响应规则，并结合为辅助函数来方便我们快速创建指定功能的响应器。下面我们逐个介绍。\n\n## 内置响应规则\n\n:::tip\n响应规则的使用方法可以参考 [深入 - 响应规则](../appendices/rule.md)。\n:::\n\n### `startswith`\n\n`startswith` 响应规则用于匹配消息纯文本部分的开头是否与指定字符串（或一系列字符串）相同。可选参数 `ignorecase` 用于指定是否忽略大小写，默认为 `False`。\n\n例如，我们可以创建一个匹配消息开头为 `!` 或者 `/` 的规则：\n\n```python\nfrom nonebot.rule import startswith\n\nrule = startswith((\"!\", \"/\"), ignorecase=False)\n```\n\n也可以直接使用辅助函数新建一个响应器：\n\n```python\nfrom nonebot import on_startswith\n\nmatcher = on_startswith((\"!\", \"/\"), ignorecase=False)\n```\n\n### `endswith`\n\n`endswith` 响应规则用于匹配消息纯文本部分的结尾是否与指定字符串（或一系列字符串）相同。可选参数 `ignorecase` 用于指定是否忽略大小写，默认为 `False`。\n\n例如，我们可以创建一个匹配消息结尾为 `.` 或者 `。` 的规则：\n\n```python\nfrom nonebot.rule import endswith\n\nrule = endswith((\".\", \"。\"), ignorecase=False)\n```\n\n也可以直接使用辅助函数新建一个响应器：\n\n```python\nfrom nonebot import on_endswith\n\nmatcher = on_endswith((\".\", \"。\"), ignorecase=False)\n```\n\n### `fullmatch`\n\n`fullmatch` 响应规则用于匹配消息纯文本部分是否与指定字符串（或一系列字符串）完全相同。可选参数 `ignorecase` 用于指定是否忽略大小写，默认为 `False`。\n\n例如，我们可以创建一个匹配消息为 `ping` 或者 `pong` 的规则：\n\n```python\nfrom nonebot.rule import fullmatch\n\nrule = fullmatch((\"ping\", \"pong\"), ignorecase=False)\n```\n\n也可以直接使用辅助函数新建一个响应器：\n\n```python\nfrom nonebot import on_fullmatch\n\nmatcher = on_fullmatch((\"ping\", \"pong\"), ignorecase=False)\n```\n\n### `keyword`\n\n`keyword` 响应规则用于匹配消息纯文本部分是否包含指定字符串（或一系列字符串）。\n\n例如，我们可以创建一个匹配消息中包含 `hello` 或者 `hi` 的规则：\n\n```python\nfrom nonebot.rule import keyword\n\nrule = keyword(\"hello\", \"hi\")\n```\n\n也可以直接使用辅助函数新建一个响应器：\n\n```python\nfrom nonebot import on_keyword\n\nmatcher = on_keyword({\"hello\", \"hi\"})\n```\n\n### `command`\n\n`command` 是最常用的响应规则，它用于匹配消息是否为命令。它会根据配置中的 [Command Start 和 Command Separator](../appendices/config.mdx#command-start-和-command-separator) 来判断消息是否为命令。\n\n例如，当我们配置了 `Command Start` 为 `/`，`Command Separator` 为 `.` 时：\n\n```python\nfrom nonebot.rule import command\n\n# 匹配 \"/help\" 或者 \"/帮助\" 开头的消息\nrule = command(\"help\", \"帮助\")\n# 匹配 \"/help.cmd\" 开头的消息\nrule = command((\"help\", \"cmd\"))\n```\n\n也可以直接使用辅助函数新建一个响应器：\n\n```python\nfrom nonebot import on_command\n\nmatcher = on_command(\"help\", aliases={\"帮助\"})\n```\n\n此外，`command` 响应规则默认允许消息命令与参数间不加空格，如果需要严格匹配命令与参数间的空白符，可以使用 `command` 函数的 `force_whitespace` 参数。`force_whitespace` 参数可以是 bool 类型或者具体的字符串，默认为 `False`。如果为 `True`，则命令与参数间必须有任意个数的空白符；如果为字符串，则命令与参数间必须有且与给定字符串一致的空白符。\n\n```python\nrule = command(\"help\", force_whitespace=True)\nrule = command(\"help\", force_whitespace=\" \")\n```\n\n命令解析后的结果可以通过 [`Command`](./dependency.mdx#command)、[`RawCommand`](./dependency.mdx#rawcommand)、[`CommandArg`](./dependency.mdx#commandarg)、[`CommandStart`](./dependency.mdx#commandstart)、[`CommandWhitespace`](./dependency.mdx#commandwhitespace) 依赖注入获取。\n\n### `shell_command`\n\n`shell_command` 响应规则用于匹配类 shell 命令形式的消息。它首先与 [`command`](#command) 响应规则一样进行命令匹配，如果匹配成功，则会进行进一步的参数解析。参数解析采用 `argparse` 标准库进行，在此基础上添加了消息序列 `Message` 支持。\n\n例如，我们可以创建一个匹配 `/cmd` 命令并且带有 `-v` 选项与默认 `-h` 帮助选项的规则：\n\n```python\nfrom nonebot.rule import shell_command, ArgumentParser\n\nparser = ArgumentParser()\nparser.add_argument(\"-v\", \"--verbose\", action=\"store_true\")\n\nrule = shell_command(\"cmd\", parser=parser)\n```\n\n更多关于 `argparse` 的使用方法请参考 [argparse 文档](https://docs.python.org/zh-cn/3/library/argparse.html)。我们也可以选择不提供 `parser` 参数，这样 `shell_command` 将不会解析参数，但会提供参数列表 `argv`。\n\n直接使用辅助函数新建一个响应器：\n\n```python\nfrom nonebot import on_shell_command\nfrom nonebot.rule import ArgumentParser\n\nparser = ArgumentParser()\nparser.add_argument(\"-v\", \"--verbose\", action=\"store_true\")\n\nmatcher = on_shell_command(\"cmd\", parser=parser)\n```\n\n参数解析后的结果可以通过 [`ShellCommandArgv`](./dependency.mdx#shellcommandargv)、[`ShellCommandArgs`](./dependency.mdx#shellcommandargs) 依赖注入获取。\n\n### `regex`\n\n`regex` 响应规则用于匹配消息是否与指定正则表达式匹配。\n\n:::tip 提示\n正则表达式匹配使用 search 而非 match，如需从头匹配请使用 `r\"^xxx\"` 模式来确保匹配开头。\n:::\n\n例如，我们可以创建一个匹配消息中包含字母并且忽略大小写的规则：\n\n```python\nfrom nonebot.rule import regex\n\nrule = regex(r\"[a-z]+\", flags=re.IGNORECASE)\n```\n\n也可以直接使用辅助函数新建一个响应器：\n\n```python\nfrom nonebot import on_regex\n\nmatcher = on_regex(r\"[a-z]+\", flags=re.IGNORECASE)\n```\n\n正则匹配后的结果可以通过 [`RegexStr`](./dependency.mdx#regexstr)、[`RegexGroup`](./dependency.mdx#regexgroup)、[`RegexDict`](./dependency.mdx#regexdict) 依赖注入获取。\n\n### `to_me`\n\n`to_me` 响应规则用于匹配事件是否与机器人相关。\n\n例如：\n\n```python\nfrom nonebot.rule import to_me\n\nrule = to_me()\n```\n\n### `is_type`\n\n`is_type` 响应规则用于匹配事件类型是否为指定类型（或者一系列类型）。\n\n例如，我们可以创建一个匹配 OneBot v11 私聊和群聊消息事件的规则：\n\n```python\nfrom nonebot.rule import is_type\nfrom nonebot.adapters.onebot.v11 import PrivateMessageEvent, GroupMessageEvent\n\nrule = is_type(PrivateMessageEvent, GroupMessageEvent)\n```\n\n## 响应器组\n\n为了更方便的管理一系列功能相近的响应器，NoneBot 提供了两种响应器组，它们可以帮助我们进行响应器的统一管理。\n\n### `CommandGroup`\n\n`CommandGroup` 可以用于管理一系列具有相同前置命令的子命令响应器。\n\n例如，我们创建 `/cmd`、`/cmd.sub`、`/cmd.help` 三个命令，他们具有相同的优先级：\n\n```python\nfrom nonebot import CommandGroup\n\ngroup = CommandGroup(\"cmd\", priority=10)\n\ncmd = group.command(tuple())\nsub_cmd = group.command(\"sub\")\nhelp_cmd = group.command(\"help\")\n```\n\n命令别名 aliases 默认不会添加 `CommandGroup` 设定的前缀，如果需要为 aliases 添加前缀，可以添加 `prefix_aliases=True` 参数:\n\n```python\nfrom nonebot import CommandGroup\n\ngroup = CommandGroup(\"cmd\", prefix_aliases=True)\n\ncmd = group.command(tuple())\nhelp_cmd = group.command(\"help\", aliases={\"帮助\"})\n```\n\n这样就能成功匹配 `/cmd`、`/cmd.help`、`/cmd.帮助` 命令。如果未设置，将默认匹配 `/cmd`、`/cmd.help`、`/帮助` 命令。\n\n### `MatcherGroup`\n\n`MatcherGroup` 可以用于管理一系列具有相同属性的响应器。\n\n例如，我们创建一个具有相同响应规则的响应器组：\n\n```python\nfrom nonebot.rule import to_me\nfrom nonebot import MatcherGroup\n\ngroup = MatcherGroup(rule=to_me())\n\nmatcher1 = group.on_message()\nmatcher2 = group.on_message()\n```\n\n## 第三方响应规则\n\n### Alconna\n\n[`nonebot-plugin-alconna`](https://github.com/nonebot/plugin-alconna) 是一类提供了拓展响应规则的插件。\n该插件使用 [Alconna](https://github.com/ArcletProject/Alconna) 作为命令解析器，\n是一个简单、灵活、高效的命令参数解析器, 并且不局限于解析命令式字符串。\n\n该插件提供了一类新的事件响应器辅助函数 `on_alconna`，以及 `AlconnaResult` 等依赖注入函数。\n\n基于 `Alconna` 的特性，该插件同时提供了一系列便捷的消息段标注。\n标注可用于在 `Alconna` 中匹配消息中除 text 外的其他消息段，也可用于快速创建各适配器下的消息段。所有标注位于 `nonebot_plugin_alconna.adapters` 中。\n\n该插件同时通过提供 `UniMessage` (通用消息模型) 实现了**跨平台接收和发送消息**的功能。\n\n详情请阅读最佳实践中的 [命令解析拓展](../best-practice/alconna/README.mdx) 章节。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/advanced/plugin-info.md",
    "content": "---\nsidebar_position: 2\ndescription: 填写与获取插件相关的信息\n\noptions:\n  menu:\n    - category: advanced\n      weight: 30\n---\n\n# 插件信息\n\nNoneBot 是一个插件化的框架，可以通过加载插件来扩展功能。同时，我们也可以通过 NoneBot 的插件系统来获取相关信息，例如插件的名称、使用方法，用于收集帮助信息等。下面我们将介绍如何为插件添加元数据，以及如何获取插件信息。\n\n## 插件元数据\n\n在 NoneBot 中，插件 [`Plugin`](../api/plugin/model.md#Plugin) 对象中存储了插件系统所需要的一系列信息。包括插件的索引名称、插件模块、插件中的事件响应器、插件父子关系等。通常，只有插件开发者才需要关心这些信息，而插件使用者或者机器人用户想要看到的是插件使用方法等帮助信息。因此，我们可以为插件添加插件元数据 `PluginMetadata`，它允许插件开发者为插件添加一些额外的信息。这些信息编写于插件模块的顶层，可以直接通过源码查看，或者通过 NoneBot 插件系统获取收集到的信息，通过其他方式发送给机器人用户等。\n\n现在，假设我们有一个插件 `example`, 它的模块结构如下：\n\n```tree {4-6} title=Project\n📦 awesome-bot\n├── 📂 awesome_bot\n│   └── 📂 plugins\n|       └── 📂 example\n|           ├── 📜 __init__.py\n|           └── 📜 config.py\n├── 📜 pyproject.toml\n└── 📜 README.md\n```\n\n我们需要在插件顶层模块 `example/__init__.py` 中添加插件元数据，如下所示：\n\n```python {1,5-12} title=example/__init__.py\nfrom nonebot.plugin import PluginMetadata\n\nfrom .config import Config\n\n__plugin_meta__ = PluginMetadata(\n    name=\"示例插件\",\n    description=\"这是一个示例插件\",\n    usage=\"没什么用\",\n    type=\"application\",\n    config=Config,\n    extra={},\n)\n```\n\n我们可以看到，插件元数据 `PluginMetadata` 有三个基本属性：插件名称、插件描述、插件使用方法。除此之外，还有几个可选的属性（具体填写见[发布插件](../developer/plugin-publishing.mdx#插件元数据)章节）：\n\n- `type`：插件类别，发布插件必填。当前有效类别有：`library`（为其他插件编写提供功能），`application`（向机器人用户提供功能）；\n- `homepage`：插件项目主页，发布插件必填；\n- `config`：插件的[配置类](../appendices/config.mdx#插件配置)，发布插件时如有配置类则必须填写；\n- `supported_adapters`：支持的适配器模块名集合，若插件只使用了 NoneBot 基本抽象，应显式填写 `None`；\n- `extra`：一个字典，可以用于存储任意信息。其他插件可以通过约定 `extra` 字典的键名来达成收集某些特殊信息的目的。\n\n请注意，这里的**插件名称**是供使用者或机器人用户查看的人类可读名称，与插件索引名称无关。**插件索引名称（插件模块名称）**仅用于 NoneBot 插件系统**内部索引**。\n\n## 获取插件信息\n\nNoneBot 提供了多种获取插件对象的方法，例如获取当前所有已导入的插件：\n\n```python\nimport nonebot\n\nplugins: set[Plugin] = nonebot.get_loaded_plugins()\n```\n\n也可以通过插件索引名称获取插件对象：\n\n```python\nimport nonebot\n\nplugin: Plugin | None = nonebot.get_plugin(\"example\")\n```\n\n或者通过模块路径获取插件对象：\n\n```python\nimport nonebot\n\nplugin: Plugin | None = nonebot.get_plugin_by_module_name(\"awesome_bot.plugins.example\")\n```\n\n如果需要获取所有当前声明的插件名称（可能还未加载），可以使用 `get_available_plugin_names` 函数：\n\n```python\nimport nonebot\n\nplugin_names: set[str] = nonebot.get_available_plugin_names()\n```\n\n插件对象 `Plugin` 中包含了多个属性：\n\n- `name`：插件索引名称\n- `module`：插件模块\n- `module_name`：插件模块路径\n- `manager`：插件管理器\n- `matcher`：插件中定义的事件响应器\n- `parent_plugin`：插件的父插件\n- `sub_plugins`：插件的子插件集合\n- `metadata`：插件元数据\n\n通过这些属性以及插件元数据，我们就可以收集所需要的插件信息了。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/advanced/plugin-nesting.md",
    "content": "---\nsidebar_position: 3\ndescription: 编写与加载嵌套插件\n\noptions:\n  menu:\n    - category: advanced\n      weight: 40\n---\n\n# 嵌套插件\n\nNoneBot 支持嵌套插件，即一个插件可以包含其他插件。通过这种方式，我们可以将一个大型插件拆分成多个功能子插件，使得插件更加清晰、易于维护。我们可以直接在插件中使用 NoneBot 加载插件的方法来加载子插件。\n\n## 创建嵌套插件\n\n我们可以在使用 `nb-cli` 命令[创建插件](../tutorial/create-plugin.md#创建插件)时，选择直接通过模板创建一个嵌套插件：\n\n```bash\n$ nb plugin create\n[?] 插件名称: parent\n[?] 使用嵌套插件? (y/N) Y\n[?] 输出目录: awesome_bot/plugins\n```\n\n或者使用 `nb plugin create --sub-plugin` 选项直接创建一个嵌套插件。\n\n## 已有插件\n\n如果你已经有一个插件，想要在其中嵌套加载子插件，可以在插件的 `__init__.py` 中添加如下代码：\n\n```python title=parent/__init__.py\nimport nonebot\nfrom pathlib import Path\n\nsub_plugins = nonebot.load_plugins(\n    str(Path(__file__).parent.joinpath(\"plugins\").resolve())\n)\n```\n\n这样，`parent` 插件就会加载 `parent/plugins` 目录下的所有插件。NoneBot 会正确识别这些插件的父子关系，你可以在 `parent` 的插件信息中看到这些子插件的信息，也可以在子插件信息中看到它们的父插件信息。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/advanced/requiring.md",
    "content": "---\nsidebar_position: 4\ndescription: 使用其他插件提供的功能\n\noptions:\n  menu:\n    - category: advanced\n      weight: 50\n---\n\n# 跨插件访问\n\nNoneBot 插件化系统的设计使得插件之间可以功能独立、各司其职，我们可以更好地维护和扩展插件。但是，有时候我们可能需要在不同插件之间调用功能。NoneBot 生态中就有一类插件，它们专为其他插件提供功能支持，如：[定时任务插件](../best-practice/scheduler.md)、[数据存储插件](../best-practice/data-storing.md)等。这时候我们就需要在插件之间进行跨插件访问。\n\n## 插件跟踪\n\n由于 NoneBot 插件系统通过 [Import Hooks](https://docs.python.org/3/reference/import.html#import-hooks) 的方式实现插件加载与跟踪管理，因此我们**不能**在 NoneBot 跟踪插件前进行模块 import，这会导致插件加载失败。即，我们不能在使用 NoneBot 提供的加载插件方法前，直接使用 `import` 语句导入插件。\n\n对于在项目目录下的插件，我们通常直接使用 `load_from_toml` 等方法一次性加载所有插件。由于这些插件已经被声明，即便插件导入顺序不同，NoneBot 也能正确跟踪插件。此时，我们不需要对跨插件访问进行特殊处理。但当我们使用了外部插件，如果没有事先声明或加载插件，NoneBot 并不会将其当作插件进行跟踪，可能会出现意料之外的错误出现。\n\n简单来说，我们必须在 `import` 外部插件之前，确保依赖的外部插件已经被声明或加载。\n\n## 插件依赖声明\n\nNoneBot 提供了一种方法来确保我们依赖的插件已经被正确加载，即使用 `require` 函数。通过 `require` 函数，我们可以在当前插件中声明依赖的插件，NoneBot 会在加载当前插件时，检查依赖的插件是否已经被加载，如果没有，会尝试优先加载依赖的插件。\n\n假设我们有一个插件 `a` 依赖于插件 `b`，我们可以在插件 `a` 中使用 `require` 函数声明其依赖于插件 `b`：\n\n```python {3} title=a/__init__.py\nfrom nonebot import require\n\nrequire(\"b\")\n\nfrom b import some_function\n```\n\n其中，`require` 函数的参数为插件索引名称或者外部插件的模块名称。在完成依赖声明后，我们可以在插件 `a` 中直接导入插件 `b` 所提供的功能。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/advanced/routing.md",
    "content": "---\nsidebar_position: 9\ndescription: 添加服务端路由规则\n\noptions:\n  menu:\n    - category: advanced\n      weight: 100\n---\n\n# 添加路由\n\n在[驱动器](./driver.md)一节中，我们了解了驱动器的两种类型。既然驱动器可以作为服务端运行，那么我们就可以向驱动器添加路由规则，从而实现自定义的 API 接口等功能。在添加路由规则时，我们需要注意驱动器的类型，详情可以参考[选择驱动器](./driver.md#配置驱动器)。\n\nNoneBot 中，我们可以通过两种途径向 ASGI 驱动器添加路由规则：\n\n1. 通过 NoneBot 的兼容层建立路由规则。\n2. 直接向 ASGI 应用添加路由规则。\n\n这两种途径各有优劣，前者可以在各种服务端型驱动器下运行，但并不能直接使用 ASGI 应用框架提供的特性与功能；后者直接使用 ASGI 应用，更自由、功能完整，但只能在特定类型驱动器下运行。\n\n在向驱动器添加路由规则时，我们需要注意驱动器是否为服务端类型，我们可以通过以下方式判断：\n\n```python\nfrom nonebot import get_driver\nfrom nonebot.drivers import ASGIMixin\n\n# highlight-next-line\ncan_use = isinstance(get_driver(), ASGIMixin)\n```\n\n## 通过兼容层添加路由\n\nNoneBot 兼容层定义了两个数据类 `HTTPServerSetup` 和 `WebSocketServerSetup`，分别用于定义 HTTP 服务端和 WebSocket 服务端的路由规则。\n\n### HTTP 路由\n\n`HTTPServerSetup` 具有四个属性：\n\n- `path`：路由路径，不支持特殊占位表达式。类型为 `URL`。\n- `method`：请求方法。类型为 `str`。\n- `name`：路由名称，不可重复。类型为 `str`。\n- `handle_func`：路由处理函数。类型为 `Callable[[Request], Awaitable[Response]]`。\n\n例如，我们添加一个 `/hello` 的路由，当请求方法为 `GET` 时，返回 `200 OK` 以及返回体信息：\n\n```python\nfrom nonebot import get_driver\nfrom nonebot.drivers import URL, Request, Response, ASGIMixin, HTTPServerSetup\n\nasync def hello(request: Request) -> Response:\n    return Response(200, content=\"Hello, world!\")\n\nif isinstance((driver := get_driver()), ASGIMixin):\n    driver.setup_http_server(\n        HTTPServerSetup(\n            path=URL(\"/hello\"),\n            method=\"GET\",\n            name=\"hello\",\n            handle_func=hello,\n        )\n    )\n```\n\n对于 `Request` 和 `Response` 的详细信息，可以参考 [API 文档](../api/drivers/index.md)。\n\n### WebSocket 路由\n\n`WebSocketServerSetup` 具有三个属性：\n\n- `path`：路由路径，不支持特殊占位表达式。类型为 `URL`。\n- `name`：路由名称，不可重复。类型为 `str`。\n- `handle_func`：路由处理函数。类型为 `Callable[[WebSocket], Awaitable[Any]]`。\n\n例如，我们添加一个 `/ws` 的路由，发送所有接收到的数据：\n\n```python\nfrom nonebot import get_driver\nfrom nonebot.drivers import URL, ASGIMixin, WebSocket, WebSocketServerSetup\n\nasync def ws_handler(ws: WebSocket):\n    await ws.accept()\n    try:\n      while True:\n          data = await ws.receive()\n          await ws.send(data)\n    except WebSocketClosed as e:\n        # handle closed\n        ...\n    finally:\n        with contextlib.suppress(Exception):\n            await websocket.close()\n        # do some cleanup\n\nif isinstance((driver := get_driver()), ASGIMixin):\n    driver.setup_websocket_server(\n        WebSocketServerSetup(\n            path=URL(\"/ws\"),\n            name=\"ws\",\n            handle_func=ws_handler,\n        )\n    )\n```\n\n对于 `WebSocket` 的详细信息，可以参考 [API 文档](../api/drivers/index.md)。\n\n## 使用 ASGI 应用添加路由\n\n### 获取 ASGI 应用\n\nNoneBot 服务端类型的驱动器具有两个属性 `server_app` 和 `asgi`，分别对应驱动框架应用和 ASGI 应用。通常情况下，这两个应用是同一个对象。我们可以通过 `get_app()` 方法快速获取：\n\n```python\nimport nonebot\n\napp = nonebot.get_app()\nasgi = nonebot.get_asgi()\n```\n\n### 添加路由规则\n\n在获取到了 ASGI 应用后，我们就可以直接使用 ASGI 应用框架提供的功能来添加路由规则了。这里我们以 [FastAPI](./driver.md#fastapi默认) 为例，演示如何添加路由规则。\n\n在下面的代码中，我们添加了一个 `GET` 类型的 `/api` 路由，具体方法参考 [FastAPI 文档](https://fastapi.tiangolo.com/)。\n\n```python\nimport nonebot\nfrom fastapi import FastAPI\n\napp: FastAPI = nonebot.get_app()\n\n@app.get(\"/api\")\nasync def custom_api():\n    return {\"message\": \"Hello, world!\"}\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/advanced/runtime-hook.md",
    "content": "---\nsidebar_position: 8\ndescription: 在特定的生命周期中执行代码\n\noptions:\n  menu:\n    - category: advanced\n      weight: 90\n---\n\n# 钩子函数\n\n> [钩子编程](https://zh.wikipedia.org/wiki/%E9%92%A9%E5%AD%90%E7%BC%96%E7%A8%8B)（hooking），也称作“挂钩”，是计算机程序设计术语，指通过拦截软件模块间的函数调用、消息传递、事件传递来修改或扩展操作系统、应用程序或其他软件组件的行为的各种技术。处理被拦截的函数调用、事件、消息的代码，被称为钩子（hook）。\n\n在 NoneBot 中有一系列预定义的钩子函数，可以分为两类：**全局钩子函数**和**事件处理钩子函数**，这些钩子函数可以用装饰器的形式来使用。\n\n## 全局钩子函数\n\n全局钩子函数是指 NoneBot 针对其本身运行过程的钩子函数。\n\n这些钩子函数是由驱动器来运行的，故需要先[获得全局驱动器](./driver.md#获取驱动器)。\n\n### 启动准备\n\n这个钩子函数会在 NoneBot 启动时运行。很多时候，我们并不希望在模块被导入时就执行一些耗时操作，如：连接数据库，这时候我们可以在这个钩子函数中进行这些操作。\n\n```python\nfrom nonebot import get_driver\n\ndriver = get_driver()\n\n@driver.on_startup\nasync def do_something():\n    pass\n```\n\n### 终止处理\n\n这个钩子函数会在 NoneBot 终止时运行。我们可以在这个钩子函数中进行一些清理工作，如：关闭数据库连接。\n\n```python\nfrom nonebot import get_driver\n\ndriver = get_driver()\n\n@driver.on_shutdown\nasync def do_something():\n    pass\n```\n\n### Bot 连接处理\n\n这个钩子函数会在任何协议适配器连接 `Bot` 对象至 NoneBot 时运行。支持依赖注入，可以直接注入 `Bot` 对象。\n\n```python\nfrom nonebot import get_driver\n\ndriver = get_driver()\n\n@driver.on_bot_connect\nasync def do_something(bot: Bot):\n    pass\n```\n\n### Bot 断开处理\n\n这个钩子函数会在 `Bot` 断开与 NoneBot 的连接时运行。支持依赖注入，可以直接注入 `Bot` 对象。\n\n```python\nfrom nonebot import get_driver\n\ndriver = get_driver()\n\n@driver.on_bot_disconnect\nasync def do_something(bot: Bot):\n    pass\n```\n\n## 事件处理钩子函数\n\n这些钩子函数指的是影响 NoneBot 进行**事件处理**的函数, 这些函数可以跟普通的事件处理函数一样接受相应的参数。\n\n### 事件预处理\n\n这个钩子函数会在 NoneBot 接收到新的事件时运行。支持依赖注入，可以注入 `Bot` 对象、事件、会话状态。在这个钩子函数内抛出 `nonebot.exception.IgnoredException` 会使 NoneBot 忽略该事件。\n\n```python\nfrom nonebot.exception import IgnoredException\nfrom nonebot.message import event_preprocessor\n\n@event_preprocessor\nasync def do_something(event: Event):\n    if not event.is_tome():\n        raise IgnoredException(\"some reason\")\n```\n\n### 事件后处理\n\n这个钩子函数会在 NoneBot 处理事件完成后运行。支持依赖注入，可以注入 `Bot` 对象、事件、会话状态。\n\n```python\nfrom nonebot.message import event_postprocessor\n\n@event_postprocessor\nasync def do_something(event: Event):\n    pass\n```\n\n### 运行预处理\n\n这个钩子函数会在 NoneBot 运行事件响应器前运行。支持依赖注入，可以注入 `Bot` 对象、事件、事件响应器、会话状态。在这个钩子函数内抛出 `nonebot.exception.IgnoredException` 也会使 NoneBot 忽略本次运行。\n\n```python\nfrom nonebot.message import run_preprocessor\nfrom nonebot.exception import IgnoredException\n\n@run_preprocessor\nasync def do_something(event: Event, matcher: Matcher):\n    if not event.is_tome():\n        raise IgnoredException(\"some reason\")\n```\n\n### 运行后处理\n\n这个钩子函数会在 NoneBot 运行事件响应器后运行。支持依赖注入，可以注入 `Bot` 对象、事件、事件响应器、会话状态、运行中产生的异常。\n\n```python\nfrom nonebot.message import run_postprocessor\n\n@run_postprocessor\nasync def do_something(event: Event, matcher: Matcher, exception: Optional[Exception]):\n    pass\n```\n\n### 平台接口调用钩子\n\n这个钩子函数会在 `Bot` 对象调用平台接口时运行。在这个钩子函数中，我们可以通过引起 `MockApiException` 异常来阻止 `Bot` 对象调用平台接口并返回指定的结果。\n\n```python\nfrom nonebot.adapters import Bot\nfrom nonebot.exception import MockApiException\n\n@Bot.on_calling_api\nasync def handle_api_call(bot: Bot, api: str, data: Dict[str, Any]):\n    if api == \"send_msg\":\n        raise MockApiException(result={\"message_id\": 123})\n```\n\n### 平台接口调用后钩子\n\n这个钩子函数会在 `Bot` 对象调用平台接口后运行。在这个钩子函数中，我们可以通过引起 `MockApiException` 异常来忽略平台接口返回的结果并返回指定的结果。\n\n```python\nfrom nonebot.adapters import Bot\nfrom nonebot.exception import MockApiException\n\n@Bot.on_called_api\nasync def handle_api_result(\n    bot: Bot, exception: Optional[Exception], api: str, data: Dict[str, Any], result: Any\n):\n    if not exception and api == \"send_msg\":\n        raise MockApiException(result={**result, \"message_id\": 123})\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/advanced/session-updating.md",
    "content": "---\nsidebar_position: 7\ndescription: 控制会话响应对象\n\noptions:\n  menu:\n    - category: advanced\n      weight: 80\n---\n\n# 会话更新\n\n在 NoneBot 中，在某个事件响应器对事件响应后，即是进入了会话状态，会话状态会持续到整个事件响应流程结束。会话过程中，机器人可以与用户进行多次交互。每次需要等待用户事件时，NoneBot 将会复制一个新的临时事件响应器，并更新该事件响应器使其响应当前会话主体的消息，这个过程称为会话更新。\n\n会话更新分为两部分：**更新[事件响应器类型](./matcher.md#事件响应器类型)**和**更新[事件触发权限](./matcher.md#事件触发权限)**。\n\n## 更新事件响应器类型\n\n通常情况下，与机器人用户进行的会话都是通过消息事件进行的，因此会话更新后的默认响应事件类型为 `message`。如果希望接收一个特定类型的消息，比如 `notice` 等，我们需要自定义响应事件类型更新函数。响应事件类型更新函数是一个 `Dependent`，可以使用依赖注入。\n\n```python {3-5}\nfoo = on_message()\n\n@foo.type_updater\nasync def _() -> str:\n    return \"notice\"\n```\n\n在注册了上述响应事件类型更新函数后，当我们需要等待用户事件时，将只会响应 `notice` 类型的事件。如果希望在会话过程中的不同阶段响应不同类型的事件，我们就需要使用更复杂的逻辑来更新响应事件类型（如：根据会话状态），这里将不再展示。\n\n## 更新事件触发权限\n\n会话通常是由机器人与用户进行的一对一交互，因此会话更新后的默认触发权限为当前事件的会话 ID。这个会话 ID 由协议适配器生成，通常由用户 ID 和群 ID 等组成。如果希望实现更复杂的会话功能（如：多用户同时参与的会话），我们需要自定义触发权限更新函数。触发权限更新函数是一个 `Dependent`，可以使用依赖注入。\n\n```python {5-7}\nfrom nonebot.permission import User\n\nfoo = on_message()\n\n@foo.permission_updater\nasync def _(event: Event, matcher: Matcher) -> Permission:\n    return Permission(User.from_event(event, perm=matcher.permission))\n```\n\n上述权限更新函数是默认的权限更新函数，它将会话的触发权限更新为当前事件的会话 ID。如果我们希望响应多个用户的消息，我们可以如下修改：\n\n```python {5-7}\nfrom nonebot.permission import USER\n\nfoo = on_message()\n\n@foo.permission_updater\nasync def _(matcher: Matcher) -> Permission:\n    return USER(\"session1\", \"session2\", perm=matcher.permission)\n```\n\n请注意，此处为全大写字母的 `USER` 权限，它可以匹配多个会话 ID。通过这种方式，我们可以实现多用户同时参与的会话。\n\n我们已经了解了如何控制会话的更新，相信你已经能够实现更复杂的会话功能了，例如多人小游戏等等。欢迎将你的作品分享到[插件商店](/store/plugins)。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/api/.gitkeep",
    "content": ""
  },
  {
    "path": "website/versioned_docs/version-2.4.4/api/adapters/_category_.json",
    "content": "{\n  \"position\": 15\n}\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/api/adapters/index.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 0\ndescription: nonebot.adapters 模块\n---\n\n# nonebot.adapters\n\n本模块定义了协议适配基类，各协议请继承以下基类。\n\n使用 [Driver.register_adapter](../drivers/index.md#Driver-register-adapter) 注册适配器。\n\n## _abstract class_ `Adapter(driver, **kwargs)` {#Adapter}\n\n- **说明**\n\n  协议适配器基类。\n\n  通常，在 Adapter 中编写协议通信相关代码，如: 建立通信连接、处理接收与发送 data 等。\n\n- **参数**\n  - `driver` ([Driver](../drivers/index.md#Driver)): [Driver](../drivers/index.md#Driver) 实例\n\n  - `**kwargs` (Any): 其他由 [Driver.register_adapter](../drivers/index.md#Driver-register-adapter) 传入的额外参数\n\n### _instance-var_ `driver` {#Adapter-driver}\n\n- **类型:** [Driver](../drivers/index.md#Driver)\n\n- **说明:** 实例\n\n### _instance-var_ `bots` {#Adapter-bots}\n\n- **类型:** dict[str, [Bot](#Bot)]\n\n- **说明:** 本协议适配器已建立连接的 [Bot](#Bot) 实例\n\n### _abstract classmethod_ `get_name()` {#Adapter-get-name}\n\n- **说明:** 当前协议适配器的名称\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n### _property_ `config` {#Adapter-config}\n\n- **类型:** [Config](../config.md#Config)\n\n- **说明:** 全局 NoneBot 配置\n\n### _method_ `bot_connect(bot)` {#Adapter-bot-connect}\n\n- **说明**\n\n  告知 NoneBot 建立了一个新的 [Bot](#Bot) 连接。\n\n  当有新的 [Bot](#Bot) 实例连接建立成功时调用。\n\n- **参数**\n  - `bot` ([Bot](#Bot)): [Bot](#Bot) 实例\n\n- **返回**\n  - None\n\n### _method_ `bot_disconnect(bot)` {#Adapter-bot-disconnect}\n\n- **说明**\n\n  告知 NoneBot [Bot](#Bot) 连接已断开。\n\n  当有 [Bot](#Bot) 实例连接断开时调用。\n\n- **参数**\n  - `bot` ([Bot](#Bot)): [Bot](#Bot) 实例\n\n- **返回**\n  - None\n\n### _method_ `setup_http_server(setup)` {#Adapter-setup-http-server}\n\n- **说明:** 设置一个 HTTP 服务器路由配置\n\n- **参数**\n  - `setup` ([HTTPServerSetup](../drivers/index.md#HTTPServerSetup))\n\n- **返回**\n  - untyped\n\n### _method_ `setup_websocket_server(setup)` {#Adapter-setup-websocket-server}\n\n- **说明:** 设置一个 WebSocket 服务器路由配置\n\n- **参数**\n  - `setup` ([WebSocketServerSetup](../drivers/index.md#WebSocketServerSetup))\n\n- **返回**\n  - untyped\n\n### _async method_ `request(setup)` {#Adapter-request}\n\n- **说明:** 进行一个 HTTP 客户端请求\n\n- **参数**\n  - `setup` ([Request](../drivers/index.md#Request))\n\n- **返回**\n  - [Response](../drivers/index.md#Response)\n\n### _method_ `websocket(setup)` {#Adapter-websocket}\n\n- **说明:** 建立一个 WebSocket 客户端连接请求\n\n- **参数**\n  - `setup` ([Request](../drivers/index.md#Request))\n\n- **返回**\n  - AsyncGenerator[[WebSocket](../drivers/index.md#WebSocket), None]\n\n### _method_ `on_ready(func)` {#Adapter-on-ready}\n\n- **参数**\n  - `func` (LIFESPAN_FUNC)\n\n- **返回**\n  - LIFESPAN_FUNC\n\n## _abstract class_ `Bot(adapter, self_id)` {#Bot}\n\n- **说明**\n\n  Bot 基类。\n\n  用于处理上报消息，并提供 API 调用接口。\n\n- **参数**\n  - `adapter` ([Adapter](#Adapter)): 协议适配器实例\n\n  - `self_id` (str): 机器人 ID\n\n### _instance-var_ `adapter` {#Bot-adapter}\n\n- **类型:** [Adapter](#Adapter)\n\n- **说明:** 协议适配器实例\n\n### _instance-var_ `self_id` {#Bot-self-id}\n\n- **类型:** str\n\n- **说明:** 机器人 ID\n\n### _property_ `type` {#Bot-type}\n\n- **类型:** str\n\n- **说明:** 协议适配器名称\n\n### _property_ `config` {#Bot-config}\n\n- **类型:** [Config](../config.md#Config)\n\n- **说明:** 全局 NoneBot 配置\n\n### _async method_ `call_api(api, **data)` {#Bot-call-api}\n\n- **说明:** 调用机器人 API 接口，可以通过该函数或直接通过 bot 属性进行调用\n\n- **参数**\n  - `api` (str): API 名称\n\n  - `**data` (Any): API 数据\n\n- **返回**\n  - Any\n\n- **用法**\n\n  ```python\n  await bot.call_api(\"send_msg\", message=\"hello world\")\n  await bot.send_msg(message=\"hello world\")\n  ```\n\n### _abstract async method_ `send(event, message, **kwargs)` {#Bot-send}\n\n- **说明:** 调用机器人基础发送消息接口\n\n- **参数**\n  - `event` ([Event](#Event)): 上报事件\n\n  - `message` (str | [Message](#Message) | [MessageSegment](#MessageSegment)): 要发送的消息\n\n  - `**kwargs` (Any): 任意额外参数\n\n- **返回**\n  - Any\n\n### _classmethod_ `on_calling_api(func)` {#Bot-on-calling-api}\n\n- **说明**\n\n  调用 api 预处理。\n\n  钩子函数参数:\n  - bot: 当前 bot 对象\n  - api: 调用的 api 名称\n  - data: api 调用的参数字典\n\n- **参数**\n  - `func` ([T_CallingAPIHook](../typing.md#T-CallingAPIHook))\n\n- **返回**\n  - [T_CallingAPIHook](../typing.md#T-CallingAPIHook)\n\n### _classmethod_ `on_called_api(func)` {#Bot-on-called-api}\n\n- **说明**\n\n  调用 api 后处理。\n\n  钩子函数参数:\n  - bot: 当前 bot 对象\n  - exception: 调用 api 时发生的错误\n  - api: 调用的 api 名称\n  - data: api 调用的参数字典\n  - result: api 调用的返回\n\n- **参数**\n  - `func` ([T_CalledAPIHook](../typing.md#T-CalledAPIHook))\n\n- **返回**\n  - [T_CalledAPIHook](../typing.md#T-CalledAPIHook)\n\n## _abstract class_ `Event(<auto>)` {#Event}\n\n- **说明:** Event 基类。提供获取关键信息的方法，其余信息可直接获取。\n\n- **参数**\n\n  auto\n\n### _abstract method_ `get_type()` {#Event-get-type}\n\n- **说明:** 获取事件类型的方法，类型通常为 NoneBot 内置的四种类型。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n### _abstract method_ `get_event_name()` {#Event-get-event-name}\n\n- **说明:** 获取事件名称的方法。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n### _abstract method_ `get_event_description()` {#Event-get-event-description}\n\n- **说明:** 获取事件描述的方法，通常为事件具体内容。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n### _method_ `get_log_string()` {#Event-get-log-string}\n\n- **说明**\n\n  获取事件日志信息的方法。\n\n  通常你不需要修改这个方法，只有当希望 NoneBot 隐藏该事件日志时，\n  可以抛出 `NoLogException` 异常。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n- **异常**\n  - NoLogException: 希望 NoneBot 隐藏该事件日志\n\n### _abstract method_ `get_user_id()` {#Event-get-user-id}\n\n- **说明:** 获取事件主体 id 的方法，通常是用户 id 。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n### _abstract method_ `get_session_id()` {#Event-get-session-id}\n\n- **说明:** 获取会话 id 的方法，用于判断当前事件属于哪一个会话， 通常是用户 id、群组 id 组合。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n### _abstract method_ `get_message()` {#Event-get-message}\n\n- **说明:** 获取事件消息内容的方法。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - [Message](#Message)\n\n### _method_ `get_plaintext()` {#Event-get-plaintext}\n\n- **说明**\n\n  获取消息纯文本的方法。\n\n  通常不需要修改，默认通过 `get_message().extract_plain_text` 获取。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n### _abstract method_ `is_tome()` {#Event-is-tome}\n\n- **说明:** 获取事件是否与机器人有关的方法。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - bool\n\n## _abstract class_ `Message(<auto>)` {#Message}\n\n- **说明:** 消息序列\n\n- **参数**\n  - `message`: 消息内容\n\n### _classmethod_ `template(format_string)` {#Message-template}\n\n- **说明**\n\n  创建消息模板。\n\n  用法和 `str.format` 大致相同，支持以 `Message` 对象作为消息模板并输出消息对象。\n  并且提供了拓展的格式化控制符，\n  可以通过该消息类型的 `MessageSegment` 工厂方法创建消息。\n\n- **参数**\n  - `format_string` (str | TM): 格式化模板\n\n- **返回**\n  - [MessageTemplate](#MessageTemplate)[Self]: 消息格式化器\n\n### _abstract classmethod_ `get_segment_class()` {#Message-get-segment-class}\n\n- **说明:** 获取消息段类型\n\n- **参数**\n\n  empty\n\n- **返回**\n  - type[TMS]\n\n### _abstract staticmethod_ `_construct(msg)` {#Message--construct}\n\n- **说明:** 构造消息数组\n\n- **参数**\n  - `msg` (str)\n\n- **返回**\n  - Iterable[TMS]\n\n### _method_ `__getitem__(args)` {#Message---getitem--}\n\n- **重载**\n\n  **1.** `(args) -> Self`\n  - **参数**\n    - `args` (str): 消息段类型\n\n  - **返回**\n    - Self: 所有类型为 `args` 的消息段\n\n  **2.** `(args) -> TMS`\n  - **参数**\n    - `args` (tuple[str, int]): 消息段类型和索引\n\n  - **返回**\n    - TMS: 类型为 `args[0]` 的消息段第 `args[1]` 个\n\n  **3.** `(args) -> Self`\n  - **参数**\n    - `args` (tuple[str, slice]): 消息段类型和切片\n\n  - **返回**\n    - Self: 类型为 `args[0]` 的消息段切片 `args[1]`\n\n  **4.** `(args) -> TMS`\n  - **参数**\n    - `args` (int): 索引\n\n  - **返回**\n    - TMS: 第 `args` 个消息段\n\n  **5.** `(args) -> Self`\n  - **参数**\n    - `args` (slice): 切片\n\n  - **返回**\n    - Self: 消息切片 `args`\n\n### _method_ `__contains__(value)` {#Message---contains--}\n\n- **说明:** 检查消息段是否存在\n\n- **参数**\n  - `value` (TMS | str): 消息段或消息段类型\n\n- **返回**\n  - bool: 消息内是否存在给定消息段或给定类型的消息段\n\n### _method_ `has(value)` {#Message-has}\n\n- **说明:** 与 [`__contains__`](#Message---contains--) 相同\n\n- **参数**\n  - `value` (TMS | str)\n\n- **返回**\n  - bool\n\n### _method_ `index(value, *args)` {#Message-index}\n\n- **说明:** 索引消息段\n\n- **参数**\n  - `value` (TMS | str): 消息段或者消息段类型\n\n  - `*args` (SupportsIndex)\n\n  - `arg`: start 与 end\n\n- **返回**\n  - int: 索引 index\n\n- **异常**\n  - ValueError: 消息段不存在\n\n### _method_ `get(type_, count=None)` {#Message-get}\n\n- **说明:** 获取指定类型的消息段\n\n- **参数**\n  - `type_` (str): 消息段类型\n\n  - `count` (int | None): 获取个数\n\n- **返回**\n  - Self: 构建的新消息\n\n### _method_ `count(value)` {#Message-count}\n\n- **说明:** 计算指定消息段的个数\n\n- **参数**\n  - `value` (TMS | str): 消息段或消息段类型\n\n- **返回**\n  - int: 个数\n\n### _method_ `only(value)` {#Message-only}\n\n- **说明:** 检查消息中是否仅包含指定消息段\n\n- **参数**\n  - `value` (TMS | str): 指定消息段或消息段类型\n\n- **返回**\n  - bool: 是否仅包含指定消息段\n\n### _method_ `append(obj)` {#Message-append}\n\n- **说明:** 添加一个消息段到消息数组末尾。\n\n- **参数**\n  - `obj` (str | TMS): 要添加的消息段\n\n- **返回**\n  - Self\n\n### _method_ `extend(obj)` {#Message-extend}\n\n- **说明:** 拼接一个消息数组或多个消息段到消息数组末尾。\n\n- **参数**\n  - `obj` (Self | Iterable[TMS]): 要添加的消息数组\n\n- **返回**\n  - Self\n\n### _method_ `join(iterable)` {#Message-join}\n\n- **说明:** 将多个消息连接并将自身作为分割\n\n- **参数**\n  - `iterable` (Iterable[TMS | Self]): 要连接的消息\n\n- **返回**\n  - Self: 连接后的消息\n\n### _method_ `copy()` {#Message-copy}\n\n- **说明:** 深拷贝消息\n\n- **参数**\n\n  empty\n\n- **返回**\n  - Self\n\n### _method_ `include(*types)` {#Message-include}\n\n- **说明:** 过滤消息\n\n- **参数**\n  - `*types` (str): 包含的消息段类型\n\n- **返回**\n  - Self: 新构造的消息\n\n### _method_ `exclude(*types)` {#Message-exclude}\n\n- **说明:** 过滤消息\n\n- **参数**\n  - `*types` (str): 不包含的消息段类型\n\n- **返回**\n  - Self: 新构造的消息\n\n### _method_ `extract_plain_text()` {#Message-extract-plain-text}\n\n- **说明:** 提取消息内纯文本消息\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n## _abstract class_ `MessageSegment(<auto>)` {#MessageSegment}\n\n- **说明:** 消息段基类\n\n- **参数**\n\n  auto\n\n### _instance-var_ `type` {#MessageSegment-type}\n\n- **类型:** str\n\n- **说明:** 消息段类型\n\n### _class-var_ `data` {#MessageSegment-data}\n\n- **类型:** dict[str, Any]\n\n- **说明:** 消息段数据\n\n### _abstract classmethod_ `get_message_class()` {#MessageSegment-get-message-class}\n\n- **说明:** 获取消息数组类型\n\n- **参数**\n\n  empty\n\n- **返回**\n  - type[TM]\n\n### _abstract method_ `__str__()` {#MessageSegment---str--}\n\n- **说明:** 该消息段所代表的 str，在命令匹配部分使用\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n### _method_ `__add__(other)` {#MessageSegment---add--}\n\n- **参数**\n  - `other` (str | Self | Iterable[Self])\n\n- **返回**\n  - TM\n\n### _method_ `get(key, default=None)` {#MessageSegment-get}\n\n- **参数**\n  - `key` (str)\n\n  - `default` (Any)\n\n- **返回**\n  - untyped\n\n### _method_ `keys()` {#MessageSegment-keys}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - untyped\n\n### _method_ `values()` {#MessageSegment-values}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - untyped\n\n### _method_ `items()` {#MessageSegment-items}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - untyped\n\n### _method_ `join(iterable)` {#MessageSegment-join}\n\n- **参数**\n  - `iterable` (Iterable[Self | TM])\n\n- **返回**\n  - TM\n\n### _method_ `copy()` {#MessageSegment-copy}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - Self\n\n### _abstract method_ `is_text()` {#MessageSegment-is-text}\n\n- **说明:** 当前消息段是否为纯文本\n\n- **参数**\n\n  empty\n\n- **返回**\n  - bool\n\n## _class_ `MessageTemplate(template, factory=str, private_getattr=False)` {#MessageTemplate}\n\n- **说明:** 消息模板格式化实现类。\n\n- **参数**\n  - `template` (str | TM): 模板\n\n  - `factory` (type[str] | type[TM]): 消息类型工厂，默认为 `str`\n\n  - `private_getattr` (bool): 是否允许在模板中访问私有属性，默认为 `False`\n\n### _method_ `add_format_spec(spec, name=None)` {#MessageTemplate-add-format-spec}\n\n- **参数**\n  - `spec` (FormatSpecFunc_T)\n\n  - `name` (str | None)\n\n- **返回**\n  - FormatSpecFunc_T\n\n### _method_ `format(*args, **kwargs)` {#MessageTemplate-format}\n\n- **说明:** 根据传入参数和模板生成消息对象\n\n- **参数**\n  - `*args`\n\n  - `**kwargs`\n\n- **返回**\n  - TF\n\n### _method_ `format_map(mapping)` {#MessageTemplate-format-map}\n\n- **说明:** 根据传入字典和模板生成消息对象, 在传入字段名不是有效标识符时有用\n\n- **参数**\n  - `mapping` (Mapping[str, Any])\n\n- **返回**\n  - TF\n\n### _method_ `vformat(format_string, args, kwargs)` {#MessageTemplate-vformat}\n\n- **参数**\n  - `format_string` (str)\n\n  - `args` (Sequence[Any])\n\n  - `kwargs` (Mapping[str, Any])\n\n- **返回**\n  - TF\n\n### _method_ `get_field(field_name, args, kwargs)` {#MessageTemplate-get-field}\n\n- **参数**\n  - `field_name` (str)\n\n  - `args` (Sequence[Any])\n\n  - `kwargs` (Mapping[str, Any])\n\n- **返回**\n  - tuple[Any, int | str]\n\n### _method_ `format_field(value, format_spec)` {#MessageTemplate-format-field}\n\n- **参数**\n  - `value` (Any)\n\n  - `format_spec` (str)\n\n- **返回**\n  - Any\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/api/compat.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 16\ndescription: nonebot.compat 模块\n---\n\n# nonebot.compat\n\n本模块为 Pydantic 版本兼容层模块\n\n为兼容 Pydantic V1 与 V2 版本，定义了一系列兼容函数与类供使用。\n\n## _var_ `Required` {#Required}\n\n- **类型:** untyped\n\n- **说明:** Alias of Ellipsis for compatibility with pydantic v1\n\n## _library-attr_ `PydanticUndefined` {#PydanticUndefined}\n\n- **说明:** Pydantic Undefined object\n\n## _library-attr_ `PydanticUndefinedType` {#PydanticUndefinedType}\n\n- **说明:** Pydantic Undefined type\n\n## _var_ `DEFAULT_CONFIG` {#DEFAULT-CONFIG}\n\n- **类型:** untyped\n\n- **说明:** Default config for validations\n\n## _def_ `LegacyUnionField(<auto>)` {#LegacyUnionField}\n\n- **说明:** Mark field to use legacy left to right union mode\n\n- **参数**\n\n  auto\n\n- **返回**\n  - untyped\n\n## _class_ `FieldInfo(default=PydanticUndefined, **kwargs)` {#FieldInfo}\n\n- **说明:** FieldInfo class with extra property for compatibility with pydantic v1\n\n- **参数**\n  - `default` (Any)\n\n  - `**kwargs` (Any)\n\n### _property_ `extra` {#FieldInfo-extra}\n\n- **类型:** dict[str, Any]\n\n- **说明**\n\n  Extra data that is not part of the standard pydantic fields.\n\n  For compatibility with pydantic v1.\n\n## _class_ `ModelField(<auto>)` {#ModelField}\n\n- **说明:** ModelField class for compatibility with pydantic v1\n\n- **参数**\n\n  auto\n\n### _instance-var_ `name` {#ModelField-name}\n\n- **类型:** str\n\n- **说明:** The name of the field.\n\n### _instance-var_ `annotation` {#ModelField-annotation}\n\n- **类型:** Any\n\n- **说明:** The annotation of the field.\n\n### _instance-var_ `field_info` {#ModelField-field-info}\n\n- **类型:** FieldInfo\n\n- **说明:** The FieldInfo of the field.\n\n### _classmethod_ `construct(name, annotation, field_info=None)` {#ModelField-construct}\n\n- **说明:** Construct a ModelField from given infos.\n\n- **参数**\n  - `name` (str)\n\n  - `annotation` (Any)\n\n  - `field_info` (FieldInfo | None)\n\n- **返回**\n  - Self\n\n### _method_ `get_default()` {#ModelField-get-default}\n\n- **说明:** Get the default value of the field.\n\n- **参数**\n\n  empty\n\n- **返回**\n  - Any\n\n### _method_ `validate_value(value)` {#ModelField-validate-value}\n\n- **说明:** Validate the value pass to the field.\n\n- **参数**\n  - `value` (Any)\n\n- **返回**\n  - Any\n\n## _def_ `model_fields(model)` {#model-fields}\n\n- **说明:** Get field list of a model.\n\n- **参数**\n  - `model` (type[BaseModel])\n\n- **返回**\n  - list[ModelField]\n\n## _def_ `model_config(model)` {#model-config}\n\n- **说明:** Get config of a model.\n\n- **参数**\n  - `model` (type[BaseModel])\n\n- **返回**\n  - Any\n\n## _def_ `model_dump(model, include=None, exclude=None, by_alias=False, exclude_unset=False, exclude_defaults=False, exclude_none=False)` {#model-dump}\n\n- **参数**\n  - `model` (BaseModel)\n\n  - `include` (set[str] | None)\n\n  - `exclude` (set[str] | None)\n\n  - `by_alias` (bool)\n\n  - `exclude_unset` (bool)\n\n  - `exclude_defaults` (bool)\n\n  - `exclude_none` (bool)\n\n- **返回**\n  - dict[str, Any]\n\n## _def_ `type_validate_python(type_, data)` {#type-validate-python}\n\n- **说明:** Validate data with given type.\n\n- **参数**\n  - `type_` (type[T])\n\n  - `data` (Any)\n\n- **返回**\n  - T\n\n## _def_ `type_validate_json(type_, data)` {#type-validate-json}\n\n- **说明:** Validate JSON with given type.\n\n- **参数**\n  - `type_` (type[T])\n\n  - `data` (str | bytes)\n\n- **返回**\n  - T\n\n## _def_ `custom_validation(class_)` {#custom-validation}\n\n- **说明:** Use pydantic v1 like validator generator in pydantic v2\n\n- **参数**\n  - `class_` (type[CVC])\n\n- **返回**\n  - type[CVC]\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/api/config.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 1\ndescription: nonebot.config 模块\n---\n\n# nonebot.config\n\n本模块定义了 NoneBot 本身运行所需的配置项。\n\nNoneBot 使用 [`pydantic`](https://pydantic-docs.helpmanual.io/) 以及\n[`python-dotenv`](https://saurabh-kumar.com/python-dotenv/) 来读取配置。\n\n配置项需符合特殊格式或 json 序列化格式\n详情见 [`pydantic Field Type`](https://pydantic-docs.helpmanual.io/usage/types/) 文档。\n\n## _class_ `Env(_env_file=ENV_FILE_SENTINEL, _env_file_encoding=None, _env_nested_delimiter=None, **values)` {#Env}\n\n- **说明**\n\n  运行环境配置。大小写不敏感。\n\n  将会从 **环境变量** > **dotenv 配置文件** 的优先级读取环境信息。\n\n- **参数**\n  - `_env_file` (DOTENV_TYPE | None)\n\n  - `_env_file_encoding` (str | None)\n\n  - `_env_nested_delimiter` (str | None)\n\n  - `**values` (Any)\n\n### _class-var_ `environment` {#Env-environment}\n\n- **类型:** str\n\n- **说明**\n\n  当前环境名。\n\n  NoneBot 将从 `.env.{environment}` 文件中加载配置。\n\n## _class_ `Config(_env_file=ENV_FILE_SENTINEL, _env_file_encoding=None, _env_nested_delimiter=None, **values)` {#Config}\n\n- **说明**\n\n  NoneBot 主要配置。大小写不敏感。\n\n  除了 NoneBot 的配置项外，还可以自行添加配置项到 `.env.{environment}` 文件中。\n  这些配置将会在 json 反序列化后一起带入 `Config` 类中。\n\n  配置方法参考: [配置](https://nonebot.dev/docs/appendices/config)\n\n- **参数**\n  - `_env_file` (DOTENV_TYPE | None)\n\n  - `_env_file_encoding` (str | None)\n\n  - `_env_nested_delimiter` (str | None)\n\n  - `**values` (Any)\n\n### _class-var_ `driver` {#Config-driver}\n\n- **类型:** str\n\n- **说明**\n\n  NoneBot 运行所使用的 `Driver` 。继承自 [Driver](drivers/index.md#Driver) 。\n\n  配置格式为 `<module>[:<Driver>][+<module>[:<Mixin>]]*`。\n\n  `~` 为 `nonebot.drivers.` 的缩写。\n\n  配置方法参考: [配置驱动器](https://nonebot.dev/docs/advanced/driver#%E9%85%8D%E7%BD%AE%E9%A9%B1%E5%8A%A8%E5%99%A8)\n\n### _class-var_ `host` {#Config-host}\n\n- **类型:** IPvAnyAddress\n\n- **说明:** NoneBot [ReverseDriver](drivers/index.md#ReverseDriver) 服务端监听的 IP/主机名。\n\n### _class-var_ `port` {#Config-port}\n\n- **类型:** int\n\n- **说明:** NoneBot [ReverseDriver](drivers/index.md#ReverseDriver) 服务端监听的端口。\n\n### _class-var_ `log_level` {#Config-log-level}\n\n- **类型:** int | str\n\n- **说明**\n\n  NoneBot 日志输出等级，可以为 `int` 类型等级或等级名称。\n\n  参考 [记录日志](https://nonebot.dev/docs/appendices/log)，[loguru 日志等级](https://loguru.readthedocs.io/en/stable/api/logger.html#levels)。\n\n  :::tip 提示\n  日志等级名称应为大写，如 `INFO`。\n  :::\n\n- **用法**\n\n  ```conf\n  LOG_LEVEL=25\n  LOG_LEVEL=INFO\n  ```\n\n### _class-var_ `api_timeout` {#Config-api-timeout}\n\n- **类型:** float | None\n\n- **说明:** API 请求超时时间，单位: 秒。\n\n### _class-var_ `superusers` {#Config-superusers}\n\n- **类型:** set[str]\n\n- **说明:** 机器人超级用户。\n\n- **用法**\n\n  ```conf\n  SUPERUSERS=[\"12345789\"]\n  ```\n\n### _class-var_ `nickname` {#Config-nickname}\n\n- **类型:** set[str]\n\n- **说明:** 机器人昵称。\n\n### _class-var_ `command_start` {#Config-command-start}\n\n- **类型:** set[str]\n\n- **说明**\n\n  命令的起始标记，用于判断一条消息是不是命令。\n\n  参考[命令响应规则](https://nonebot.dev/docs/advanced/matcher#command)。\n\n- **用法**\n\n  ```conf\n  COMMAND_START=[\"/\", \"\"]\n  ```\n\n### _class-var_ `command_sep` {#Config-command-sep}\n\n- **类型:** set[str]\n\n- **说明**\n\n  命令的分隔标记，用于将文本形式的命令切分为元组（实际的命令名）。\n\n  参考[命令响应规则](https://nonebot.dev/docs/advanced/matcher#command)。\n\n- **用法**\n\n  ```conf\n  COMMAND_SEP=[\".\"]\n  ```\n\n### _class-var_ `session_expire_timeout` {#Config-session-expire-timeout}\n\n- **类型:** timedelta\n\n- **说明:** 等待用户回复的超时时间。\n\n- **用法**\n\n  ```conf\n  SESSION_EXPIRE_TIMEOUT=[-][DD]D[,][HH:MM:]SS[.ffffff]\n  SESSION_EXPIRE_TIMEOUT=[±]P[DD]DT[HH]H[MM]M[SS]S  # ISO 8601\n  ```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/api/consts.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 9\ndescription: nonebot.consts 模块\n---\n\n# nonebot.consts\n\n本模块包含了 NoneBot 事件处理过程中使用到的常量。\n\n## _var_ `RECEIVE_KEY` {#RECEIVE-KEY}\n\n- **类型:** Literal['\\_receive\\_{id}']\n\n- **说明:** `receive` 存储 key\n\n## _var_ `LAST_RECEIVE_KEY` {#LAST-RECEIVE-KEY}\n\n- **类型:** Literal['\\_last\\_receive']\n\n- **说明:** `last_receive` 存储 key\n\n## _var_ `ARG_KEY` {#ARG-KEY}\n\n- **类型:** Literal['{key}']\n\n- **说明:** `arg` 存储 key\n\n## _var_ `REJECT_TARGET` {#REJECT-TARGET}\n\n- **类型:** Literal['\\_current\\_target']\n\n- **说明:** 当前 `reject` 目标存储 key\n\n## _var_ `REJECT_CACHE_TARGET` {#REJECT-CACHE-TARGET}\n\n- **类型:** Literal['\\_next\\_target']\n\n- **说明:** 下一个 `reject` 目标存储 key\n\n## _var_ `PAUSE_PROMPT_RESULT_KEY` {#PAUSE-PROMPT-RESULT-KEY}\n\n- **类型:** Literal['\\_pause\\_result']\n\n- **说明:** `pause` prompt 发送结果存储 key\n\n## _var_ `REJECT_PROMPT_RESULT_KEY` {#REJECT-PROMPT-RESULT-KEY}\n\n- **类型:** Literal['\\_reject\\_{key}\\_result']\n\n- **说明:** `reject` prompt 发送结果存储 key\n\n## _var_ `PREFIX_KEY` {#PREFIX-KEY}\n\n- **类型:** Literal['\\_prefix']\n\n- **说明:** 命令前缀存储 key\n\n## _var_ `CMD_KEY` {#CMD-KEY}\n\n- **类型:** Literal['command']\n\n- **说明:** 命令元组存储 key\n\n## _var_ `RAW_CMD_KEY` {#RAW-CMD-KEY}\n\n- **类型:** Literal['raw\\_command']\n\n- **说明:** 命令文本存储 key\n\n## _var_ `CMD_ARG_KEY` {#CMD-ARG-KEY}\n\n- **类型:** Literal['command\\_arg']\n\n- **说明:** 命令参数存储 key\n\n## _var_ `CMD_START_KEY` {#CMD-START-KEY}\n\n- **类型:** Literal['command\\_start']\n\n- **说明:** 命令开头存储 key\n\n## _var_ `CMD_WHITESPACE_KEY` {#CMD-WHITESPACE-KEY}\n\n- **类型:** Literal['command\\_whitespace']\n\n- **说明:** 命令与参数间空白符存储 key\n\n## _var_ `SHELL_ARGS` {#SHELL-ARGS}\n\n- **类型:** Literal['\\_args']\n\n- **说明:** shell 命令 parse 后参数字典存储 key\n\n## _var_ `SHELL_ARGV` {#SHELL-ARGV}\n\n- **类型:** Literal['\\_argv']\n\n- **说明:** shell 命令原始参数列表存储 key\n\n## _var_ `REGEX_MATCHED` {#REGEX-MATCHED}\n\n- **类型:** Literal['\\_matched']\n\n- **说明:** 正则匹配结果存储 key\n\n## _var_ `STARTSWITH_KEY` {#STARTSWITH-KEY}\n\n- **类型:** Literal['\\_startswith']\n\n- **说明:** 响应触发前缀 key\n\n## _var_ `ENDSWITH_KEY` {#ENDSWITH-KEY}\n\n- **类型:** Literal['\\_endswith']\n\n- **说明:** 响应触发后缀 key\n\n## _var_ `FULLMATCH_KEY` {#FULLMATCH-KEY}\n\n- **类型:** Literal['\\_fullmatch']\n\n- **说明:** 响应触发完整消息 key\n\n## _var_ `KEYWORD_KEY` {#KEYWORD-KEY}\n\n- **类型:** Literal['\\_keyword']\n\n- **说明:** 响应触发关键字 key\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/api/dependencies/_category_.json",
    "content": "{\n  \"position\": 13\n}\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/api/dependencies/index.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 0\ndescription: nonebot.dependencies 模块\n---\n\n# nonebot.dependencies\n\n本模块模块实现了依赖注入的定义与处理。\n\n## _abstract class_ `Param(*args, validate=False, **kwargs)` {#Param}\n\n- **说明**\n\n  依赖注入的基本单元 —— 参数。\n\n  继承自 `pydantic.fields.FieldInfo`，用于描述参数信息（不包括参数名）。\n\n- **参数**\n  - `*args`\n\n  - `validate` (bool)\n\n  - `**kwargs` (Any)\n\n## _class_ `Dependent(<auto>)` {#Dependent}\n\n- **说明:** 依赖注入容器\n\n- **参数**\n  - `call`: 依赖注入的可调用对象，可以是任何 Callable 对象\n\n  - `pre_checkers`: 依赖注入解析前的参数检查\n\n  - `params`: 具名参数列表\n\n  - `parameterless`: 匿名参数列表\n\n  - `allow_types`: 允许的参数类型\n\n### _staticmethod_ `parse_params(call, allow_types)` {#Dependent-parse-params}\n\n- **参数**\n  - `call` (\\_DependentCallable[R])\n\n  - `allow_types` (tuple[type[Param], ...])\n\n- **返回**\n  - tuple[[ModelField](../compat.md#ModelField), ...]\n\n### _staticmethod_ `parse_parameterless(parameterless, allow_types)` {#Dependent-parse-parameterless}\n\n- **参数**\n  - `parameterless` (tuple[Any, ...])\n\n  - `allow_types` (tuple[type[Param], ...])\n\n- **返回**\n  - tuple[Param, ...]\n\n### _classmethod_ `parse(*, call, parameterless=None, allow_types)` {#Dependent-parse}\n\n- **参数**\n  - `call` (\\_DependentCallable[R])\n\n  - `parameterless` (Iterable[Any] | None)\n\n  - `allow_types` (Iterable[type[Param]])\n\n- **返回**\n  - Dependent[R]\n\n### _async method_ `check(**params)` {#Dependent-check}\n\n- **参数**\n  - `**params` (Any)\n\n- **返回**\n  - None\n\n### _async method_ `solve(**params)` {#Dependent-solve}\n\n- **参数**\n  - `**params` (Any)\n\n- **返回**\n  - dict[str, Any]\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/api/dependencies/utils.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 1\ndescription: nonebot.dependencies.utils 模块\n---\n\n# nonebot.dependencies.utils\n\n## _def_ `get_typed_signature(call)` {#get-typed-signature}\n\n- **说明:** 获取可调用对象签名\n\n- **参数**\n  - `call` ((...) -> Any)\n\n- **返回**\n  - inspect.Signature\n\n## _def_ `get_typed_annotation(param, globalns)` {#get-typed-annotation}\n\n- **说明:** 获取参数的类型注解\n\n- **参数**\n  - `param` (inspect.Parameter)\n\n  - `globalns` (dict[str, Any])\n\n- **返回**\n  - Any\n\n## _def_ `check_field_type(field, value)` {#check-field-type}\n\n- **说明:** 检查字段类型是否匹配\n\n- **参数**\n  - `field` ([ModelField](../compat.md#ModelField))\n\n  - `value` (Any)\n\n- **返回**\n  - Any\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/api/drivers/_category_.json",
    "content": "{\n  \"position\": 14\n}\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/api/drivers/aiohttp.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 2\ndescription: nonebot.drivers.aiohttp 模块\n---\n\n# nonebot.drivers.aiohttp\n\n[AIOHTTP](https://aiohttp.readthedocs.io/en/stable/) 驱动适配器。\n\n```bash\nnb driver install aiohttp\n# 或者\npip install nonebot2[aiohttp]\n```\n\n:::tip 提示\n本驱动仅支持客户端连接\n:::\n\n## _class_ `Session(params=None, headers=None, cookies=None, version=HTTPVersion.H11, timeout=None, proxy=None)` {#Session}\n\n- **参数**\n  - `params` (QueryTypes)\n\n  - `headers` (HeaderTypes)\n\n  - `cookies` (CookieTypes)\n\n  - `version` (str | [HTTPVersion](index.md#HTTPVersion))\n\n  - `timeout` (TimeoutTypes)\n\n  - `proxy` (str | None)\n\n### _async method_ `request(setup)` {#Session-request}\n\n- **参数**\n  - `setup` ([Request](index.md#Request))\n\n- **返回**\n  - [Response](index.md#Response)\n\n### _method_ `stream_request(setup, *, chunk_size=1024)` {#Session-stream-request}\n\n- **参数**\n  - `setup` ([Request](index.md#Request))\n\n  - `chunk_size` (int)\n\n- **返回**\n  - AsyncGenerator[[Response](index.md#Response), None]\n\n### _async method_ `setup()` {#Session-setup}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - None\n\n### _async method_ `close()` {#Session-close}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - None\n\n## _class_ `Mixin(<auto>)` {#Mixin}\n\n- **说明:** AIOHTTP Mixin\n\n- **参数**\n\n  auto\n\n### _async method_ `request(setup)` {#Mixin-request}\n\n- **参数**\n  - `setup` ([Request](index.md#Request))\n\n- **返回**\n  - [Response](index.md#Response)\n\n### _method_ `stream_request(setup, *, chunk_size=1024)` {#Mixin-stream-request}\n\n- **参数**\n  - `setup` ([Request](index.md#Request))\n\n  - `chunk_size` (int)\n\n- **返回**\n  - AsyncGenerator[[Response](index.md#Response), None]\n\n### _method_ `websocket(setup)` {#Mixin-websocket}\n\n- **参数**\n  - `setup` ([Request](index.md#Request))\n\n- **返回**\n  - AsyncGenerator[[WebSocket](index.md#WebSocket), None]\n\n### _method_ `get_session(params=None, headers=None, cookies=None, version=HTTPVersion.H11, timeout=None, proxy=None)` {#Mixin-get-session}\n\n- **参数**\n  - `params` (QueryTypes)\n\n  - `headers` (HeaderTypes)\n\n  - `cookies` (CookieTypes)\n\n  - `version` (str | [HTTPVersion](index.md#HTTPVersion))\n\n  - `timeout` (TimeoutTypes)\n\n  - `proxy` (str | None)\n\n- **返回**\n  - Session\n\n## _class_ `WebSocket(*, request, session, websocket)` {#WebSocket}\n\n- **说明:** AIOHTTP Websocket Wrapper\n\n- **参数**\n  - `request` ([Request](index.md#Request))\n\n  - `session` (aiohttp.ClientSession)\n\n  - `websocket` (aiohttp.ClientWebSocketResponse)\n\n### _async method_ `accept()` {#WebSocket-accept}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - untyped\n\n### _async method_ `close(code=1000, reason=\"\")` {#WebSocket-close}\n\n- **参数**\n  - `code` (int)\n\n  - `reason` (str)\n\n- **返回**\n  - untyped\n\n### _async method_ `receive()` {#WebSocket-receive}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n### _async method_ `receive_text()` {#WebSocket-receive-text}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n### _async method_ `receive_bytes()` {#WebSocket-receive-bytes}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - bytes\n\n### _async method_ `send_text(data)` {#WebSocket-send-text}\n\n- **参数**\n  - `data` (str)\n\n- **返回**\n  - None\n\n### _async method_ `send_bytes(data)` {#WebSocket-send-bytes}\n\n- **参数**\n  - `data` (bytes)\n\n- **返回**\n  - None\n\n## _class_ `Driver(env, config)` {#Driver}\n\n- **参数**\n  - `env` ([Env](../config.md#Env))\n\n  - `config` ([Config](../config.md#Config))\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/api/drivers/fastapi.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 1\ndescription: nonebot.drivers.fastapi 模块\n---\n\n# nonebot.drivers.fastapi\n\n[FastAPI](https://fastapi.tiangolo.com/) 驱动适配\n\n```bash\nnb driver install fastapi\n# 或者\npip install nonebot2[fastapi]\n```\n\n:::tip 提示\n本驱动仅支持服务端连接\n:::\n\n## _class_ `Config(<auto>)` {#Config}\n\n- **说明:** FastAPI 驱动框架设置，详情参考 FastAPI 文档\n\n- **参数**\n\n  auto\n\n### _class-var_ `fastapi_openapi_url` {#Config-fastapi-openapi-url}\n\n- **类型:** str | None\n\n- **说明:** `openapi.json` 地址，默认为 `None` 即关闭\n\n### _class-var_ `fastapi_docs_url` {#Config-fastapi-docs-url}\n\n- **类型:** str | None\n\n- **说明:** `swagger` 地址，默认为 `None` 即关闭\n\n### _class-var_ `fastapi_redoc_url` {#Config-fastapi-redoc-url}\n\n- **类型:** str | None\n\n- **说明:** `redoc` 地址，默认为 `None` 即关闭\n\n### _class-var_ `fastapi_include_adapter_schema` {#Config-fastapi-include-adapter-schema}\n\n- **类型:** bool\n\n- **说明:** 是否包含适配器路由的 schema，默认为 `True`\n\n### _class-var_ `fastapi_reload` {#Config-fastapi-reload}\n\n- **类型:** bool\n\n- **说明:** 开启/关闭冷重载\n\n### _class-var_ `fastapi_reload_dirs` {#Config-fastapi-reload-dirs}\n\n- **类型:** list[str] | None\n\n- **说明:** 重载监控文件夹列表，默认为 uvicorn 默认值\n\n### _class-var_ `fastapi_reload_delay` {#Config-fastapi-reload-delay}\n\n- **类型:** float\n\n- **说明:** 重载延迟，默认为 uvicorn 默认值\n\n### _class-var_ `fastapi_reload_includes` {#Config-fastapi-reload-includes}\n\n- **类型:** list[str] | None\n\n- **说明:** 要监听的文件列表，支持 glob pattern，默认为 uvicorn 默认值\n\n### _class-var_ `fastapi_reload_excludes` {#Config-fastapi-reload-excludes}\n\n- **类型:** list[str] | None\n\n- **说明:** 不要监听的文件列表，支持 glob pattern，默认为 uvicorn 默认值\n\n### _class-var_ `fastapi_extra` {#Config-fastapi-extra}\n\n- **类型:** dict[str, Any]\n\n- **说明:** 传递给 `FastAPI` 的其他参数。\n\n## _class_ `Driver(env, config)` {#Driver}\n\n- **说明:** FastAPI 驱动框架。\n\n- **参数**\n  - `env` ([Env](../config.md#Env))\n\n  - `config` (NoneBotConfig)\n\n### _property_ `type` {#Driver-type}\n\n- **类型:** str\n\n- **说明:** 驱动名称: `fastapi`\n\n### _property_ `server_app` {#Driver-server-app}\n\n- **类型:** FastAPI\n\n- **说明:** `FastAPI APP` 对象\n\n### _property_ `asgi` {#Driver-asgi}\n\n- **类型:** FastAPI\n\n- **说明:** `FastAPI APP` 对象\n\n### _property_ `logger` {#Driver-logger}\n\n- **类型:** logging.Logger\n\n- **说明:** fastapi 使用的 logger\n\n### _method_ `setup_http_server(setup)` {#Driver-setup-http-server}\n\n- **参数**\n  - `setup` ([HTTPServerSetup](index.md#HTTPServerSetup))\n\n- **返回**\n  - untyped\n\n### _method_ `setup_websocket_server(setup)` {#Driver-setup-websocket-server}\n\n- **参数**\n  - `setup` ([WebSocketServerSetup](index.md#WebSocketServerSetup))\n\n- **返回**\n  - None\n\n### _method_ `run(host=None, port=None, *args, app=None, **kwargs)` {#Driver-run}\n\n- **说明:** 使用 `uvicorn` 启动 FastAPI\n\n- **参数**\n  - `host` (str | None)\n\n  - `port` (int | None)\n\n  - `*args`\n\n  - `app` (str | None)\n\n  - `**kwargs`\n\n- **返回**\n  - untyped\n\n## _class_ `FastAPIWebSocket(*, request, websocket)` {#FastAPIWebSocket}\n\n- **说明:** FastAPI WebSocket Wrapper\n\n- **参数**\n  - `request` (BaseRequest)\n\n  - `websocket` ([WebSocket](index.md#WebSocket))\n\n### _async method_ `accept()` {#FastAPIWebSocket-accept}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - None\n\n### _async method_ `close(code=status.WS_1000_NORMAL_CLOSURE, reason=\"\")` {#FastAPIWebSocket-close}\n\n- **参数**\n  - `code` (int)\n\n  - `reason` (str)\n\n- **返回**\n  - None\n\n### _async method_ `receive()` {#FastAPIWebSocket-receive}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str | bytes\n\n### _async method_ `receive_text()` {#FastAPIWebSocket-receive-text}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n### _async method_ `receive_bytes()` {#FastAPIWebSocket-receive-bytes}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - bytes\n\n### _async method_ `send_text(data)` {#FastAPIWebSocket-send-text}\n\n- **参数**\n  - `data` (str)\n\n- **返回**\n  - None\n\n### _async method_ `send_bytes(data)` {#FastAPIWebSocket-send-bytes}\n\n- **参数**\n  - `data` (bytes)\n\n- **返回**\n  - None\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/api/drivers/httpx.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 3\ndescription: nonebot.drivers.httpx 模块\n---\n\n# nonebot.drivers.httpx\n\n[HTTPX](https://www.python-httpx.org/) 驱动适配\n\n```bash\nnb driver install httpx\n# 或者\npip install nonebot2[httpx]\n```\n\n:::tip 提示\n本驱动仅支持客户端 HTTP 连接\n:::\n\n## _class_ `Session(params=None, headers=None, cookies=None, version=HTTPVersion.H11, timeout=None, proxy=None)` {#Session}\n\n- **参数**\n  - `params` (QueryTypes)\n\n  - `headers` (HeaderTypes)\n\n  - `cookies` (CookieTypes)\n\n  - `version` (str | [HTTPVersion](index.md#HTTPVersion))\n\n  - `timeout` (TimeoutTypes)\n\n  - `proxy` (str | None)\n\n### _async method_ `request(setup)` {#Session-request}\n\n- **参数**\n  - `setup` ([Request](index.md#Request))\n\n- **返回**\n  - [Response](index.md#Response)\n\n### _method_ `stream_request(setup, *, chunk_size=1024)` {#Session-stream-request}\n\n- **参数**\n  - `setup` ([Request](index.md#Request))\n\n  - `chunk_size` (int)\n\n- **返回**\n  - AsyncGenerator[[Response](index.md#Response), None]\n\n### _async method_ `setup()` {#Session-setup}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - None\n\n### _async method_ `close()` {#Session-close}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - None\n\n## _class_ `Mixin(<auto>)` {#Mixin}\n\n- **说明:** HTTPX Mixin\n\n- **参数**\n\n  auto\n\n### _async method_ `request(setup)` {#Mixin-request}\n\n- **参数**\n  - `setup` ([Request](index.md#Request))\n\n- **返回**\n  - [Response](index.md#Response)\n\n### _method_ `stream_request(setup, *, chunk_size=1024)` {#Mixin-stream-request}\n\n- **参数**\n  - `setup` ([Request](index.md#Request))\n\n  - `chunk_size` (int)\n\n- **返回**\n  - AsyncGenerator[[Response](index.md#Response), None]\n\n### _method_ `get_session(params=None, headers=None, cookies=None, version=HTTPVersion.H11, timeout=None, proxy=None)` {#Mixin-get-session}\n\n- **参数**\n  - `params` (QueryTypes)\n\n  - `headers` (HeaderTypes)\n\n  - `cookies` (CookieTypes)\n\n  - `version` (str | [HTTPVersion](index.md#HTTPVersion))\n\n  - `timeout` (TimeoutTypes)\n\n  - `proxy` (str | None)\n\n- **返回**\n  - Session\n\n## _class_ `Driver(env, config)` {#Driver}\n\n- **参数**\n  - `env` ([Env](../config.md#Env))\n\n  - `config` ([Config](../config.md#Config))\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/api/drivers/index.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 0\ndescription: nonebot.drivers 模块\n---\n\n# nonebot.drivers\n\n本模块定义了驱动适配器基类。\n\n各驱动请继承以下基类。\n\n## _abstract class_ `ASGIMixin(<auto>)` {#ASGIMixin}\n\n- **说明**\n\n  ASGI 服务端基类。\n\n  将后端框架封装，以满足适配器使用。\n\n- **参数**\n\n  auto\n\n### _abstract property_ `server_app` {#ASGIMixin-server-app}\n\n- **类型:** Any\n\n- **说明:** 驱动 APP 对象\n\n### _abstract property_ `asgi` {#ASGIMixin-asgi}\n\n- **类型:** Any\n\n- **说明:** 驱动 ASGI 对象\n\n### _abstract method_ `setup_http_server(setup)` {#ASGIMixin-setup-http-server}\n\n- **说明:** 设置一个 HTTP 服务器路由配置\n\n- **参数**\n  - `setup` ([HTTPServerSetup](#HTTPServerSetup))\n\n- **返回**\n  - None\n\n### _abstract method_ `setup_websocket_server(setup)` {#ASGIMixin-setup-websocket-server}\n\n- **说明:** 设置一个 WebSocket 服务器路由配置\n\n- **参数**\n  - `setup` ([WebSocketServerSetup](#WebSocketServerSetup))\n\n- **返回**\n  - None\n\n## _class_ `Cookies(cookies=None)` {#Cookies}\n\n- **参数**\n  - `cookies` (CookieTypes)\n\n### _method_ `set(name, value, domain=\"\", path=\"/\")` {#Cookies-set}\n\n- **参数**\n  - `name` (str)\n\n  - `value` (str)\n\n  - `domain` (str)\n\n  - `path` (str)\n\n- **返回**\n  - None\n\n### _method_ `get(name, default=None, domain=None, path=None)` {#Cookies-get}\n\n- **参数**\n  - `name` (str)\n\n  - `default` (str | None)\n\n  - `domain` (str | None)\n\n  - `path` (str | None)\n\n- **返回**\n  - str | None\n\n### _method_ `delete(name, domain=None, path=None)` {#Cookies-delete}\n\n- **参数**\n  - `name` (str)\n\n  - `domain` (str | None)\n\n  - `path` (str | None)\n\n- **返回**\n  - None\n\n### _method_ `clear(domain=None, path=None)` {#Cookies-clear}\n\n- **参数**\n  - `domain` (str | None)\n\n  - `path` (str | None)\n\n- **返回**\n  - None\n\n### _method_ `update(cookies=None)` {#Cookies-update}\n\n- **参数**\n  - `cookies` (CookieTypes)\n\n- **返回**\n  - None\n\n### _method_ `as_header(request)` {#Cookies-as-header}\n\n- **参数**\n  - `request` (Request)\n\n- **返回**\n  - dict[str, str]\n\n## _abstract class_ `Driver(env, config)` {#Driver}\n\n- **说明**\n\n  驱动器基类。\n\n  驱动器控制框架的启动和停止，适配器的注册，以及机器人生命周期管理。\n\n- **参数**\n  - `env` ([Env](../config.md#Env)): 包含环境信息的 Env 对象\n\n  - `config` ([Config](../config.md#Config)): 包含配置信息的 Config 对象\n\n### _instance-var_ `env` {#Driver-env}\n\n- **类型:** str\n\n- **说明:** 环境名称\n\n### _instance-var_ `config` {#Driver-config}\n\n- **类型:** [Config](../config.md#Config)\n\n- **说明:** 全局配置对象\n\n### _property_ `bots` {#Driver-bots}\n\n- **类型:** dict[str, [Bot](../adapters/index.md#Bot)]\n\n- **说明:** 获取当前所有已连接的 Bot\n\n### _method_ `register_adapter(adapter, **kwargs)` {#Driver-register-adapter}\n\n- **说明:** 注册一个协议适配器\n\n- **参数**\n  - `adapter` (type[[Adapter](../adapters/index.md#Adapter)]): 适配器类\n\n  - `**kwargs`: 其他传递给适配器的参数\n\n- **返回**\n  - None\n\n### _abstract property_ `type` {#Driver-type}\n\n- **类型:** str\n\n- **说明:** 驱动类型名称\n\n### _abstract property_ `logger` {#Driver-logger}\n\n- **类型:** untyped\n\n- **说明:** 驱动专属 logger 日志记录器\n\n### _abstract method_ `run(*args, **kwargs)` {#Driver-run}\n\n- **说明:** 启动驱动框架\n\n- **参数**\n  - `*args`\n\n  - `**kwargs`\n\n- **返回**\n  - untyped\n\n### _method_ `on_startup(func)` {#Driver-on-startup}\n\n- **说明:** 注册一个启动时执行的函数\n\n- **参数**\n  - `func` (LIFESPAN_FUNC)\n\n- **返回**\n  - LIFESPAN_FUNC\n\n### _method_ `on_shutdown(func)` {#Driver-on-shutdown}\n\n- **说明:** 注册一个停止时执行的函数\n\n- **参数**\n  - `func` (LIFESPAN_FUNC)\n\n- **返回**\n  - LIFESPAN_FUNC\n\n### _classmethod_ `on_bot_connect(func)` {#Driver-on-bot-connect}\n\n- **说明**\n\n  装饰一个函数使他在 bot 连接成功时执行。\n\n  钩子函数参数:\n  - bot: 当前连接上的 Bot 对象\n\n- **参数**\n  - `func` ([T_BotConnectionHook](../typing.md#T-BotConnectionHook))\n\n- **返回**\n  - [T_BotConnectionHook](../typing.md#T-BotConnectionHook)\n\n### _classmethod_ `on_bot_disconnect(func)` {#Driver-on-bot-disconnect}\n\n- **说明**\n\n  装饰一个函数使他在 bot 连接断开时执行。\n\n  钩子函数参数:\n  - bot: 当前连接上的 Bot 对象\n\n- **参数**\n  - `func` ([T_BotDisconnectionHook](../typing.md#T-BotDisconnectionHook))\n\n- **返回**\n  - [T_BotDisconnectionHook](../typing.md#T-BotDisconnectionHook)\n\n## _var_ `ForwardDriver` {#ForwardDriver}\n\n- **类型:** ForwardMixin\n\n- **说明**\n\n  支持客户端请求的驱动器。\n\n  **Deprecated**，请使用 [ForwardMixin](#ForwardMixin) 或其子类代替。\n\n## _abstract class_ `ForwardMixin(<auto>)` {#ForwardMixin}\n\n- **说明:** 客户端混入基类。\n\n- **参数**\n\n  auto\n\n## _abstract class_ `HTTPClientMixin(<auto>)` {#HTTPClientMixin}\n\n- **说明:** HTTP 客户端混入基类。\n\n- **参数**\n\n  auto\n\n### _abstract async method_ `request(setup)` {#HTTPClientMixin-request}\n\n- **说明:** 发送一个 HTTP 请求\n\n- **参数**\n  - `setup` ([Request](#Request))\n\n- **返回**\n  - [Response](#Response)\n\n### _abstract method_ `stream_request(setup, *, chunk_size=1024)` {#HTTPClientMixin-stream-request}\n\n- **说明:** 发送一个 HTTP 流式请求\n\n- **参数**\n  - `setup` ([Request](#Request))\n\n  - `chunk_size` (int)\n\n- **返回**\n  - AsyncGenerator[[Response](#Response), None]\n\n### _abstract method_ `get_session(params=None, headers=None, cookies=None, version=HTTPVersion.H11, timeout=None, proxy=None)` {#HTTPClientMixin-get-session}\n\n- **说明:** 获取一个 HTTP 会话\n\n- **参数**\n  - `params` (QueryTypes)\n\n  - `headers` (HeaderTypes)\n\n  - `cookies` (CookieTypes)\n\n  - `version` (str | [HTTPVersion](#HTTPVersion))\n\n  - `timeout` (TimeoutTypes)\n\n  - `proxy` (str | None)\n\n- **返回**\n  - HTTPClientSession\n\n## _class_ `HTTPServerSetup(<auto>)` {#HTTPServerSetup}\n\n- **说明:** HTTP 服务器路由配置。\n\n- **参数**\n\n  auto\n\n## _enum_ `HTTPVersion` {#HTTPVersion}\n\n- **参数**\n\n  auto\n  - `H10: '1.0'`\n\n  - `H11: '1.1'`\n\n  - `H2: '2'`\n\n## _abstract class_ `Mixin(<auto>)` {#Mixin}\n\n- **说明:** 可与其他驱动器共用的混入基类。\n\n- **参数**\n\n  auto\n\n### _abstract property_ `type` {#Mixin-type}\n\n- **类型:** str\n\n- **说明:** 混入驱动类型名称\n\n## _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}\n\n- **参数**\n  - `method` (str | bytes)\n\n  - `url` (URL | str | RawURL)\n\n  - `params` (QueryTypes)\n\n  - `headers` (HeaderTypes)\n\n  - `cookies` (CookieTypes)\n\n  - `content` (ContentTypes)\n\n  - `data` (DataTypes)\n\n  - `json` (Any)\n\n  - `files` (FilesTypes)\n\n  - `version` (str | HTTPVersion)\n\n  - `timeout` (TimeoutTypes)\n\n  - `proxy` (str | None)\n\n## _class_ `Response(status_code, *, headers=None, content=None, request=None)` {#Response}\n\n- **参数**\n  - `status_code` (int)\n\n  - `headers` (HeaderTypes)\n\n  - `content` (ContentTypes)\n\n  - `request` (Request | None)\n\n## _var_ `ReverseDriver` {#ReverseDriver}\n\n- **类型:** ReverseMixin\n\n- **说明**\n\n  支持服务端请求的驱动器。\n\n  **Deprecated**，请使用 [ReverseMixin](#ReverseMixin) 或其子类代替。\n\n## _abstract class_ `ReverseMixin(<auto>)` {#ReverseMixin}\n\n- **说明:** 服务端混入基类。\n\n- **参数**\n\n  auto\n\n## _class_ `Timeout(<auto>)` {#Timeout}\n\n- **说明:** Request 超时配置。\n\n- **参数**\n\n  auto\n\n## _abstract class_ `WebSocket(*, request)` {#WebSocket}\n\n- **参数**\n  - `request` (Request)\n\n### _abstract property_ `closed` {#WebSocket-closed}\n\n- **类型:** bool\n\n- **说明:** 连接是否已经关闭\n\n### _abstract async method_ `accept()` {#WebSocket-accept}\n\n- **说明:** 接受 WebSocket 连接请求\n\n- **参数**\n\n  empty\n\n- **返回**\n  - None\n\n### _abstract async method_ `close(code=1000, reason=\"\")` {#WebSocket-close}\n\n- **说明:** 关闭 WebSocket 连接请求\n\n- **参数**\n  - `code` (int)\n\n  - `reason` (str)\n\n- **返回**\n  - None\n\n### _abstract async method_ `receive()` {#WebSocket-receive}\n\n- **说明:** 接收一条 WebSocket text/bytes 信息\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str | bytes\n\n### _abstract async method_ `receive_text()` {#WebSocket-receive-text}\n\n- **说明:** 接收一条 WebSocket text 信息\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n### _abstract async method_ `receive_bytes()` {#WebSocket-receive-bytes}\n\n- **说明:** 接收一条 WebSocket binary 信息\n\n- **参数**\n\n  empty\n\n- **返回**\n  - bytes\n\n### _async method_ `send(data)` {#WebSocket-send}\n\n- **说明:** 发送一条 WebSocket text/bytes 信息\n\n- **参数**\n  - `data` (str | bytes)\n\n- **返回**\n  - None\n\n### _abstract async method_ `send_text(data)` {#WebSocket-send-text}\n\n- **说明:** 发送一条 WebSocket text 信息\n\n- **参数**\n  - `data` (str)\n\n- **返回**\n  - None\n\n### _abstract async method_ `send_bytes(data)` {#WebSocket-send-bytes}\n\n- **说明:** 发送一条 WebSocket binary 信息\n\n- **参数**\n  - `data` (bytes)\n\n- **返回**\n  - None\n\n## _abstract class_ `WebSocketClientMixin(<auto>)` {#WebSocketClientMixin}\n\n- **说明:** WebSocket 客户端混入基类。\n\n- **参数**\n\n  auto\n\n### _abstract method_ `websocket(setup)` {#WebSocketClientMixin-websocket}\n\n- **说明:** 发起一个 WebSocket 连接\n\n- **参数**\n  - `setup` ([Request](#Request))\n\n- **返回**\n  - AsyncGenerator[[WebSocket](#WebSocket), None]\n\n## _class_ `WebSocketServerSetup(<auto>)` {#WebSocketServerSetup}\n\n- **说明:** WebSocket 服务器路由配置。\n\n- **参数**\n\n  auto\n\n## _def_ `combine_driver(driver, *mixins)` {#combine-driver}\n\n- **说明:** 将一个驱动器和多个混入类合并。\n\n- **重载**\n\n  **1.** `(driver) -> type[D]`\n  - **参数**\n    - `driver` (type[D])\n\n  - **返回**\n    - type[D]\n\n  **2.** `(driver, __m, /, *mixins) -> type[CombinedDriver]`\n  - **参数**\n    - `driver` (type[D])\n\n    - `__m` (type[[Mixin](#Mixin)])\n\n    - `*mixins` (type[[Mixin](#Mixin)])\n\n  - **返回**\n    - type[CombinedDriver]\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/api/drivers/none.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 6\ndescription: nonebot.drivers.none 模块\n---\n\n# nonebot.drivers.none\n\nNone 驱动适配\n\n:::tip 提示\n本驱动不支持任何服务器或客户端连接\n:::\n\n## _class_ `Driver(env, config)` {#Driver}\n\n- **说明:** None 驱动框架\n\n- **参数**\n  - `env` ([Env](../config.md#Env))\n\n  - `config` ([Config](../config.md#Config))\n\n### _property_ `type` {#Driver-type}\n\n- **类型:** str\n\n- **说明:** 驱动名称: `none`\n\n### _property_ `logger` {#Driver-logger}\n\n- **类型:** untyped\n\n- **说明:** none driver 使用的 logger\n\n### _method_ `run(*args, **kwargs)` {#Driver-run}\n\n- **说明:** 启动 none driver\n\n- **参数**\n  - `*args`\n\n  - `**kwargs`\n\n- **返回**\n  - untyped\n\n### _method_ `exit(force=False)` {#Driver-exit}\n\n- **说明:** 退出 none driver\n\n- **参数**\n  - `force` (bool): 强制退出\n\n- **返回**\n  - untyped\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/api/drivers/quart.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 5\ndescription: nonebot.drivers.quart 模块\n---\n\n# nonebot.drivers.quart\n\n[Quart](https://pgjones.gitlab.io/quart/index.html) 驱动适配\n\n```bash\nnb driver install quart\n# 或者\npip install nonebot2[quart]\n```\n\n:::tip 提示\n本驱动仅支持服务端连接\n:::\n\n## _class_ `Config(<auto>)` {#Config}\n\n- **说明:** Quart 驱动框架设置\n\n- **参数**\n\n  auto\n\n### _class-var_ `quart_reload` {#Config-quart-reload}\n\n- **类型:** bool\n\n- **说明:** 开启/关闭冷重载\n\n### _class-var_ `quart_reload_dirs` {#Config-quart-reload-dirs}\n\n- **类型:** list[str] | None\n\n- **说明:** 重载监控文件夹列表，默认为 uvicorn 默认值\n\n### _class-var_ `quart_reload_delay` {#Config-quart-reload-delay}\n\n- **类型:** float\n\n- **说明:** 重载延迟，默认为 uvicorn 默认值\n\n### _class-var_ `quart_reload_includes` {#Config-quart-reload-includes}\n\n- **类型:** list[str] | None\n\n- **说明:** 要监听的文件列表，支持 glob pattern，默认为 uvicorn 默认值\n\n### _class-var_ `quart_reload_excludes` {#Config-quart-reload-excludes}\n\n- **类型:** list[str] | None\n\n- **说明:** 不要监听的文件列表，支持 glob pattern，默认为 uvicorn 默认值\n\n### _class-var_ `quart_extra` {#Config-quart-extra}\n\n- **类型:** dict[str, Any]\n\n- **说明:** 传递给 `Quart` 的其他参数。\n\n## _class_ `Driver(env, config)` {#Driver}\n\n- **说明:** Quart 驱动框架\n\n- **参数**\n  - `env` ([Env](../config.md#Env))\n\n  - `config` (NoneBotConfig)\n\n### _property_ `type` {#Driver-type}\n\n- **类型:** str\n\n- **说明:** 驱动名称: `quart`\n\n### _property_ `server_app` {#Driver-server-app}\n\n- **类型:** Quart\n\n- **说明:** `Quart` 对象\n\n### _property_ `asgi` {#Driver-asgi}\n\n- **类型:** untyped\n\n- **说明:** `Quart` 对象\n\n### _property_ `logger` {#Driver-logger}\n\n- **类型:** untyped\n\n- **说明:** Quart 使用的 logger\n\n### _method_ `setup_http_server(setup)` {#Driver-setup-http-server}\n\n- **参数**\n  - `setup` ([HTTPServerSetup](index.md#HTTPServerSetup))\n\n- **返回**\n  - untyped\n\n### _method_ `setup_websocket_server(setup)` {#Driver-setup-websocket-server}\n\n- **参数**\n  - `setup` ([WebSocketServerSetup](index.md#WebSocketServerSetup))\n\n- **返回**\n  - None\n\n### _method_ `run(host=None, port=None, *args, app=None, **kwargs)` {#Driver-run}\n\n- **说明:** 使用 `uvicorn` 启动 Quart\n\n- **参数**\n  - `host` (str | None)\n\n  - `port` (int | None)\n\n  - `*args`\n\n  - `app` (str | None)\n\n  - `**kwargs`\n\n- **返回**\n  - untyped\n\n## _class_ `WebSocket(*, request, websocket_ctx)` {#WebSocket}\n\n- **说明:** Quart WebSocket Wrapper\n\n- **参数**\n  - `request` (BaseRequest)\n\n  - `websocket_ctx` (WebsocketContext)\n\n### _async method_ `accept()` {#WebSocket-accept}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - untyped\n\n### _async method_ `close(code=1000, reason=\"\")` {#WebSocket-close}\n\n- **参数**\n  - `code` (int)\n\n  - `reason` (str)\n\n- **返回**\n  - untyped\n\n### _async method_ `receive()` {#WebSocket-receive}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str | bytes\n\n### _async method_ `receive_text()` {#WebSocket-receive-text}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n### _async method_ `receive_bytes()` {#WebSocket-receive-bytes}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - bytes\n\n### _async method_ `send_text(data)` {#WebSocket-send-text}\n\n- **参数**\n  - `data` (str)\n\n- **返回**\n  - untyped\n\n### _async method_ `send_bytes(data)` {#WebSocket-send-bytes}\n\n- **参数**\n  - `data` (bytes)\n\n- **返回**\n  - untyped\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/api/drivers/websockets.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 4\ndescription: nonebot.drivers.websockets 模块\n---\n\n# nonebot.drivers.websockets\n\n[websockets](https://websockets.readthedocs.io/) 驱动适配\n\n```bash\nnb driver install websockets\n# 或者\npip install nonebot2[websockets]\n```\n\n:::tip 提示\n本驱动仅支持客户端 WebSocket 连接\n:::\n\n## _def_ `catch_closed(func)` {#catch-closed}\n\n- **参数**\n  - `func` ((P) -> CoroutineType[Any, Any, T])\n\n- **返回**\n  - (P) -> CoroutineType[Any, Any, T]\n\n## _class_ `Mixin(<auto>)` {#Mixin}\n\n- **说明:** Websockets Mixin\n\n- **参数**\n\n  auto\n\n### _method_ `websocket(setup)` {#Mixin-websocket}\n\n- **参数**\n  - `setup` ([Request](index.md#Request))\n\n- **返回**\n  - AsyncGenerator[[WebSocket](index.md#WebSocket), None]\n\n## _class_ `WebSocket(*, request, websocket)` {#WebSocket}\n\n- **说明:** Websockets WebSocket Wrapper\n\n- **参数**\n  - `request` ([Request](index.md#Request))\n\n  - `websocket` (ClientConnection)\n\n### _async method_ `accept()` {#WebSocket-accept}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - untyped\n\n### _async method_ `close(code=1000, reason=\"\")` {#WebSocket-close}\n\n- **参数**\n  - `code` (int)\n\n  - `reason` (str)\n\n- **返回**\n  - untyped\n\n### _async method_ `receive()` {#WebSocket-receive}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str | bytes\n\n### _async method_ `receive_text()` {#WebSocket-receive-text}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n### _async method_ `receive_bytes()` {#WebSocket-receive-bytes}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - bytes\n\n### _async method_ `send_text(data)` {#WebSocket-send-text}\n\n- **参数**\n  - `data` (str)\n\n- **返回**\n  - None\n\n### _async method_ `send_bytes(data)` {#WebSocket-send-bytes}\n\n- **参数**\n  - `data` (bytes)\n\n- **返回**\n  - None\n\n## _class_ `Driver(env, config)` {#Driver}\n\n- **参数**\n  - `env` ([Env](../config.md#Env))\n\n  - `config` ([Config](../config.md#Config))\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/api/exception.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 10\ndescription: nonebot.exception 模块\n---\n\n# nonebot.exception\n\n本模块包含了所有 NoneBot 运行时可能会抛出的异常。\n\n这些异常并非所有需要用户处理，在 NoneBot 内部运行时被捕获，并进行对应操作。\n\n```bash\nNoneBotException\n├── ParserExit\n├── ProcessException\n|   ├── IgnoredException\n|   ├── SkippedException\n|   |   └── TypeMisMatch\n|   ├── MockApiException\n|   └── StopPropagation\n├── MatcherException\n|   ├── PausedException\n|   ├── RejectedException\n|   └── FinishedException\n├── AdapterException\n|   ├── NoLogException\n|   ├── ApiNotAvailable\n|   ├── NetworkError\n|   └── ActionFailed\n└── DriverException\n    └── WebSocketClosed\n```\n\n## _class_ `NoneBotException(<auto>)` {#NoneBotException}\n\n- **说明:** 所有 NoneBot 发生的异常基类。\n\n- **参数**\n\n  auto\n\n## _class_ `ParserExit(<auto>)` {#ParserExit}\n\n- **说明:** 处理消息失败时返回的异常。\n\n- **参数**\n\n  auto\n\n## _class_ `ProcessException(<auto>)` {#ProcessException}\n\n- **说明:** 事件处理过程中发生的异常基类。\n\n- **参数**\n\n  auto\n\n## _class_ `IgnoredException(<auto>)` {#IgnoredException}\n\n- **说明:** 指示 NoneBot 应该忽略该事件。可由 PreProcessor 抛出。\n\n- **参数**\n  - `reason`: 忽略事件的原因\n\n## _class_ `SkippedException(<auto>)` {#SkippedException}\n\n- **说明**\n\n  指示 NoneBot 立即结束当前 `Dependent` 的运行。\n\n  例如，可以在 `Handler` 中通过 [Matcher.skip](matcher.md#Matcher-skip) 抛出。\n\n- **参数**\n\n  auto\n\n- **用法**\n\n  ```python\n  def always_skip():\n      Matcher.skip()\n\n  @matcher.handle()\n  async def handler(dependency = Depends(always_skip)):\n      # never run\n  ```\n\n## _class_ `TypeMisMatch(<auto>)` {#TypeMisMatch}\n\n- **说明:** 当前 `Handler` 的参数类型不匹配。\n\n- **参数**\n\n  auto\n\n## _class_ `MockApiException(<auto>)` {#MockApiException}\n\n- **说明:** 指示 NoneBot 阻止本次 API 调用或修改本次调用返回值，并返回自定义内容。 可由 api hook 抛出。\n\n- **参数**\n  - `result`: 返回的内容\n\n## _class_ `StopPropagation(<auto>)` {#StopPropagation}\n\n- **说明**\n\n  指示 NoneBot 终止事件向下层传播。\n\n  在 [Matcher.block](matcher.md#Matcher-block) 为 `True`\n  或使用 [Matcher.stop_propagation](matcher.md#Matcher-stop-propagation) 方法时抛出。\n\n- **参数**\n\n  auto\n\n- **用法**\n\n  ```python\n  matcher = on_notice(block=True)\n  # 或者\n  @matcher.handle()\n  async def handler(matcher: Matcher):\n      matcher.stop_propagation()\n  ```\n\n## _class_ `MatcherException(<auto>)` {#MatcherException}\n\n- **说明:** 所有 Matcher 发生的异常基类。\n\n- **参数**\n\n  auto\n\n## _class_ `PausedException(<auto>)` {#PausedException}\n\n- **说明**\n\n  指示 NoneBot 结束当前 `Handler` 并等待下一条消息后继续下一个 `Handler`。 可用于用户输入新信息。\n\n  可以在 `Handler` 中通过 [Matcher.pause](matcher.md#Matcher-pause) 抛出。\n\n- **参数**\n\n  auto\n\n- **用法**\n\n  ```python\n  @matcher.handle()\n  async def handler():\n      await matcher.pause(\"some message\")\n  ```\n\n## _class_ `RejectedException(<auto>)` {#RejectedException}\n\n- **说明**\n\n  指示 NoneBot 结束当前 `Handler` 并等待下一条消息后重新运行当前 `Handler`。 可用于用户重新输入。\n\n  可以在 `Handler` 中通过 [Matcher.reject](matcher.md#Matcher-reject) 抛出。\n\n- **参数**\n\n  auto\n\n- **用法**\n\n  ```python\n  @matcher.handle()\n  async def handler():\n      await matcher.reject(\"some message\")\n  ```\n\n## _class_ `FinishedException(<auto>)` {#FinishedException}\n\n- **说明**\n\n  指示 NoneBot 结束当前 `Handler` 且后续 `Handler` 不再被运行。可用于结束用户会话。\n\n  可以在 `Handler` 中通过 [Matcher.finish](matcher.md#Matcher-finish) 抛出。\n\n- **参数**\n\n  auto\n\n- **用法**\n\n  ```python\n  @matcher.handle()\n  async def handler():\n      await matcher.finish(\"some message\")\n  ```\n\n## _class_ `AdapterException(<auto>)` {#AdapterException}\n\n- **说明:** 代表 `Adapter` 抛出的异常，所有的 `Adapter` 都要在内部继承自这个 `Exception`。\n\n- **参数**\n  - `adapter_name`: 标识 adapter\n\n## _class_ `NoLogException(<auto>)` {#NoLogException}\n\n- **说明**\n\n  指示 NoneBot 对当前 `Event` 进行处理但不显示 Log 信息。\n\n  可在 [Event.get_log_string](adapters/index.md#Event-get-log-string) 时抛出\n\n- **参数**\n\n  auto\n\n## _class_ `ApiNotAvailable(<auto>)` {#ApiNotAvailable}\n\n- **说明:** 在 API 连接不可用时抛出。\n\n- **参数**\n\n  auto\n\n## _class_ `NetworkError(<auto>)` {#NetworkError}\n\n- **说明:** 在网络出现问题时抛出， 如: API 请求地址不正确, API 请求无返回或返回状态非正常等。\n\n- **参数**\n\n  auto\n\n## _class_ `ActionFailed(<auto>)` {#ActionFailed}\n\n- **说明:** API 请求成功返回数据，但 API 操作失败。\n\n- **参数**\n\n  auto\n\n## _class_ `DriverException(<auto>)` {#DriverException}\n\n- **说明:** `Driver` 抛出的异常基类。\n\n- **参数**\n\n  auto\n\n## _class_ `WebSocketClosed(<auto>)` {#WebSocketClosed}\n\n- **说明:** WebSocket 连接已关闭。\n\n- **参数**\n\n  auto\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/api/index.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 0\ndescription: nonebot 模块\n---\n\n# nonebot\n\n本模块主要定义了 NoneBot 启动所需函数，供 bot 入口文件调用。\n\n## 快捷导入\n\n为方便使用，本模块从子模块导入了部分内容，以下内容可以直接通过本模块导入:\n\n- `on` => [`on`](plugin/on.md#on)\n- `on_metaevent` => [`on_metaevent`](plugin/on.md#on-metaevent)\n- `on_message` => [`on_message`](plugin/on.md#on-message)\n- `on_notice` => [`on_notice`](plugin/on.md#on-notice)\n- `on_request` => [`on_request`](plugin/on.md#on-request)\n- `on_startswith` => [`on_startswith`](plugin/on.md#on-startswith)\n- `on_endswith` => [`on_endswith`](plugin/on.md#on-endswith)\n- `on_fullmatch` => [`on_fullmatch`](plugin/on.md#on-fullmatch)\n- `on_keyword` => [`on_keyword`](plugin/on.md#on-keyword)\n- `on_command` => [`on_command`](plugin/on.md#on-command)\n- `on_shell_command` => [`on_shell_command`](plugin/on.md#on-shell-command)\n- `on_regex` => [`on_regex`](plugin/on.md#on-regex)\n- `on_type` => [`on_type`](plugin/on.md#on-type)\n- `CommandGroup` => [`CommandGroup`](plugin/on.md#CommandGroup)\n- `Matchergroup` => [`MatcherGroup`](plugin/on.md#MatcherGroup)\n- `load_plugin` => [`load_plugin`](plugin/load.md#load-plugin)\n- `load_plugins` => [`load_plugins`](plugin/load.md#load-plugins)\n- `load_all_plugins` => [`load_all_plugins`](plugin/load.md#load-all-plugins)\n- `load_from_json` => [`load_from_json`](plugin/load.md#load-from-json)\n- `load_from_toml` => [`load_from_toml`](plugin/load.md#load-from-toml)\n- `load_builtin_plugin` =>\n  [`load_builtin_plugin`](plugin/load.md#load-builtin-plugin)\n- `load_builtin_plugins` =>\n  [`load_builtin_plugins`](plugin/load.md#load-builtin-plugins)\n- `get_plugin` => [`get_plugin`](plugin/index.md#get-plugin)\n- `get_plugin_by_module_name` =>\n  [`get_plugin_by_module_name`](plugin/index.md#get-plugin-by-module-name)\n- `get_loaded_plugins` =>\n  [`get_loaded_plugins`](plugin/index.md#get-loaded-plugins)\n- `get_available_plugin_names` =>\n  [`get_available_plugin_names`](plugin/index.md#get-available-plugin-names)\n- `get_plugin_config` => [`get_plugin_config`](plugin/index.md#get-plugin-config)\n- `require` => [`require`](plugin/load.md#require)\n\n## _def_ `get_driver()` {#get-driver}\n\n- **说明**\n\n  获取全局 [Driver](drivers/index.md#Driver) 实例。\n\n  可用于在计划任务的回调等情形中获取当前 [Driver](drivers/index.md#Driver) 实例。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - [Driver](drivers/index.md#Driver): 全局 [Driver](drivers/index.md#Driver) 对象\n\n- **异常**\n  - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用)\n\n- **用法**\n\n  ```python\n  driver = nonebot.get_driver()\n  ```\n\n## _def_ `get_adapter(name)` {#get-adapter}\n\n- **说明:** 获取已注册的 [Adapter](adapters/index.md#Adapter) 实例。\n\n- **重载**\n\n  **1.** `(name) -> Adapter`\n  - **参数**\n    - `name` (str): 适配器名称\n\n  - **返回**\n    - [Adapter](adapters/index.md#Adapter): 指定名称的 [Adapter](adapters/index.md#Adapter) 对象\n\n  **2.** `(name) -> A`\n  - **参数**\n    - `name` (type[A]): 适配器类型\n\n  - **返回**\n    - A: 指定类型的 [Adapter](adapters/index.md#Adapter) 对象\n\n- **异常**\n  - ValueError: 指定的 [Adapter](adapters/index.md#Adapter) 未注册\n\n  - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用)\n\n- **用法**\n\n  ```python\n  from nonebot.adapters.console import Adapter\n  adapter = nonebot.get_adapter(Adapter)\n  ```\n\n## _def_ `get_adapters()` {#get-adapters}\n\n- **说明:** 获取所有已注册的 [Adapter](adapters/index.md#Adapter) 实例。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - dict[str, [Adapter](adapters/index.md#Adapter)]: 所有 [Adapter](adapters/index.md#Adapter) 实例字典\n\n- **异常**\n  - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用)\n\n- **用法**\n\n  ```python\n  adapters = nonebot.get_adapters()\n  ```\n\n## _def_ `get_app()` {#get-app}\n\n- **说明:** 获取全局 [ASGIMixin](drivers/index.md#ASGIMixin) 对应的 Server App 对象。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - Any: Server App 对象\n\n- **异常**\n  - AssertionError: 全局 Driver 对象不是 [ASGIMixin](drivers/index.md#ASGIMixin) 类型\n\n  - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用)\n\n- **用法**\n\n  ```python\n  app = nonebot.get_app()\n  ```\n\n## _def_ `get_asgi()` {#get-asgi}\n\n- **说明:** 获取全局 [ASGIMixin](drivers/index.md#ASGIMixin) 对应的 [ASGI](https://asgi.readthedocs.io/) 对象。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - Any: ASGI 对象\n\n- **异常**\n  - AssertionError: 全局 Driver 对象不是 [ASGIMixin](drivers/index.md#ASGIMixin) 类型\n\n  - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用)\n\n- **用法**\n\n  ```python\n  asgi = nonebot.get_asgi()\n  ```\n\n## _def_ `get_bot(self_id=None)` {#get-bot}\n\n- **说明**\n\n  获取一个连接到 NoneBot 的 [Bot](adapters/index.md#Bot) 对象。\n\n  当提供 `self_id` 时，此函数是 `get_bots()[self_id]` 的简写；\n  当不提供时，返回一个 [Bot](adapters/index.md#Bot)。\n\n- **参数**\n  - `self_id` (str | None): 用来识别 [Bot](adapters/index.md#Bot) 的 [Bot.self_id](adapters/index.md#Bot-self-id) 属性\n\n- **返回**\n  - [Bot](adapters/index.md#Bot): [Bot](adapters/index.md#Bot) 对象\n\n- **异常**\n  - KeyError: 对应 self_id 的 Bot 不存在\n\n  - ValueError: 没有传入 self_id 且没有 Bot 可用\n\n  - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用)\n\n- **用法**\n\n  ```python\n  assert nonebot.get_bot(\"12345\") == nonebot.get_bots()[\"12345\"]\n\n  another_unspecified_bot = nonebot.get_bot()\n  ```\n\n## _def_ `get_bots()` {#get-bots}\n\n- **说明:** 获取所有连接到 NoneBot 的 [Bot](adapters/index.md#Bot) 对象。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - dict[str, [Bot](adapters/index.md#Bot)]: 一个以 [Bot.self_id](adapters/index.md#Bot-self-id) 为键\n\n    [Bot](adapters/index.md#Bot) 对象为值的字典\n\n- **异常**\n  - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用)\n\n- **用法**\n\n  ```python\n  bots = nonebot.get_bots()\n  ```\n\n## _def_ `init(*, _env_file=None, **kwargs)` {#init}\n\n- **说明**\n\n  初始化 NoneBot 以及 全局 [Driver](drivers/index.md#Driver) 对象。\n\n  NoneBot 将会从 .env 文件中读取环境信息，并使用相应的 env 文件配置。\n\n  也可以传入自定义的 `_env_file` 来指定 NoneBot 从该文件读取配置。\n\n- **参数**\n  - `_env_file` (DOTENV_TYPE | None): 配置文件名，默认从 `.env.{env_name}` 中读取配置\n\n  - `**kwargs` (Any): 任意变量，将会存储到 [Driver.config](drivers/index.md#Driver-config) 对象里\n\n- **返回**\n  - None\n\n- **用法**\n\n  ```python\n  nonebot.init(database=Database(...))\n  ```\n\n## _def_ `run(*args, **kwargs)` {#run}\n\n- **说明:** 启动 NoneBot，即运行全局 [Driver](drivers/index.md#Driver) 对象。\n\n- **参数**\n  - `*args` (Any): 传入 [Driver.run](drivers/index.md#Driver-run) 的位置参数\n\n  - `**kwargs` (Any): 传入 [Driver.run](drivers/index.md#Driver-run) 的命名参数\n\n- **返回**\n  - None\n\n- **用法**\n\n  ```python\n  nonebot.run(host=\"127.0.0.1\", port=8080)\n  ```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/api/log.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 7\ndescription: nonebot.log 模块\n---\n\n# nonebot.log\n\n本模块定义了 NoneBot 的日志记录 Logger。\n\nNoneBot 使用 [`loguru`][loguru] 来记录日志信息。\n\n自定义 logger 请参考 [自定义日志](https://nonebot.dev/docs/appendices/log)\n以及 [`loguru`][loguru] 文档。\n\n[loguru]: https://github.com/Delgan/loguru\n\n## _var_ `logger` {#logger}\n\n- **类型:** Logger\n\n- **说明**\n\n  NoneBot 日志记录器对象。\n\n  默认信息:\n  - 格式: `[%(asctime)s %(name)s] %(levelname)s: %(message)s`\n  - 等级: `INFO` ，根据 `config.log_level` 配置改变\n  - 输出: 输出至 stdout\n\n- **用法**\n\n  ```python\n  from nonebot.log import logger\n  ```\n\n## _class_ `LoguruHandler(<auto>)` {#LoguruHandler}\n\n- **说明:** logging 与 loguru 之间的桥梁，将 logging 的日志转发到 loguru。\n\n- **参数**\n\n  auto\n\n### _method_ `emit(record)` {#LoguruHandler-emit}\n\n- **参数**\n  - `record` (logging.LogRecord)\n\n- **返回**\n  - untyped\n\n## _def_ `default_filter(record)` {#default-filter}\n\n- **说明:** 默认的日志过滤器，根据 `config.log_level` 配置改变日志等级。\n\n- **参数**\n  - `record` (Record)\n\n- **返回**\n  - untyped\n\n## _var_ `default_format` {#default-format}\n\n- **类型:** str\n\n- **说明:** 默认日志格式\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/api/matcher.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 3\ndescription: nonebot.matcher 模块\n---\n\n# nonebot.matcher\n\n本模块实现事件响应器的创建与运行，并提供一些快捷方法来帮助用户更好的与机器人进行对话。\n\n## _var_ `DEFAULT_PROVIDER_CLASS` {#DEFAULT-PROVIDER-CLASS}\n\n- **类型:** untyped\n\n- **说明:** 默认存储器类型\n\n## _class_ `Matcher()` {#Matcher}\n\n- **说明:** 事件响应器类\n\n- **参数**\n\n  empty\n\n### _class-var_ `type` {#Matcher-type}\n\n- **类型:** ClassVar[str]\n\n- **说明:** 事件响应器类型\n\n### _class-var_ `rule` {#Matcher-rule}\n\n- **类型:** ClassVar[[Rule](rule.md#Rule)]\n\n- **说明:** 事件响应器匹配规则\n\n### _class-var_ `permission` {#Matcher-permission}\n\n- **类型:** ClassVar[[Permission](permission.md#Permission)]\n\n- **说明:** 事件响应器触发权限\n\n### _class-var_ `handlers` {#Matcher-handlers}\n\n- **类型:** ClassVar[list[[Dependent](dependencies/index.md#Dependent)[Any]]]\n\n- **说明:** 事件响应器拥有的事件处理函数列表\n\n### _class-var_ `priority` {#Matcher-priority}\n\n- **类型:** ClassVar[int]\n\n- **说明:** 事件响应器优先级\n\n### _class-var_ `block` {#Matcher-block}\n\n- **类型:** bool\n\n- **说明:** 事件响应器是否阻止事件传播\n\n### _class-var_ `temp` {#Matcher-temp}\n\n- **类型:** ClassVar[bool]\n\n- **说明:** 事件响应器是否为临时\n\n### _class-var_ `expire_time` {#Matcher-expire-time}\n\n- **类型:** ClassVar[datetime | None]\n\n- **说明:** 事件响应器过期时间点\n\n### _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}\n\n- **说明:** 创建一个新的事件响应器，并存储至 `matchers <#matchers>`\\_\n\n- **参数**\n  - `type_` (str): 事件响应器类型，与 `event.get_type()` 一致时触发，空字符串表示任意\n\n  - `rule` ([Rule](rule.md#Rule) | None): 匹配规则\n\n  - `permission` ([Permission](permission.md#Permission) | None): 权限\n\n  - `handlers` (list[[T\\_Handler](typing.md#T-Handler) | [Dependent](dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器，即触发一次后删除\n\n  - `priority` (int): 响应优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级的响应器传播\n\n  - `plugin` ([Plugin](plugin/model.md#Plugin) | None): **Deprecated.** 事件响应器所在插件\n\n  - `module` (ModuleType | None): **Deprecated.** 事件响应器所在模块\n\n  - `source` (MatcherSource | None): 事件响应器源代码上下文信息\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `default_state` ([T_State](typing.md#T-State) | None): 默认状态 `state`\n\n  - `default_type_updater` ([T_TypeUpdater](typing.md#T-TypeUpdater) | [Dependent](dependencies/index.md#Dependent)[str] | None): 默认事件类型更新函数\n\n  - `default_permission_updater` ([T_PermissionUpdater](typing.md#T-PermissionUpdater) | [Dependent](dependencies/index.md#Dependent)[[Permission](permission.md#Permission)] | None): 默认会话权限更新函数\n\n- **返回**\n  - type[Matcher]: 新的事件响应器类\n\n### _classmethod_ `destroy()` {#Matcher-destroy}\n\n- **说明:** 销毁当前的事件响应器\n\n- **参数**\n\n  empty\n\n- **返回**\n  - None\n\n### _classmethod_ `check_perm(bot, event, stack=None, dependency_cache=None)` {#Matcher-check-perm}\n\n- **说明:** 检查是否满足触发权限\n\n- **参数**\n  - `bot` ([Bot](adapters/index.md#Bot)): Bot 对象\n\n  - `event` ([Event](adapters/index.md#Event)): 上报事件\n\n  - `stack` (AsyncExitStack | None): 异步上下文栈\n\n  - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None): 依赖缓存\n\n- **返回**\n  - bool: 是否满足权限\n\n### _classmethod_ `check_rule(bot, event, state, stack=None, dependency_cache=None)` {#Matcher-check-rule}\n\n- **说明:** 检查是否满足匹配规则\n\n- **参数**\n  - `bot` ([Bot](adapters/index.md#Bot)): Bot 对象\n\n  - `event` ([Event](adapters/index.md#Event)): 上报事件\n\n  - `state` ([T_State](typing.md#T-State)): 当前状态\n\n  - `stack` (AsyncExitStack | None): 异步上下文栈\n\n  - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None): 依赖缓存\n\n- **返回**\n  - bool: 是否满足匹配规则\n\n### _classmethod_ `type_updater(func)` {#Matcher-type-updater}\n\n- **说明:** 装饰一个函数来更改当前事件响应器的默认响应事件类型更新函数\n\n- **参数**\n  - `func` ([T_TypeUpdater](typing.md#T-TypeUpdater)): 响应事件类型更新函数\n\n- **返回**\n  - [T_TypeUpdater](typing.md#T-TypeUpdater)\n\n### _classmethod_ `permission_updater(func)` {#Matcher-permission-updater}\n\n- **说明:** 装饰一个函数来更改当前事件响应器的默认会话权限更新函数\n\n- **参数**\n  - `func` ([T_PermissionUpdater](typing.md#T-PermissionUpdater)): 会话权限更新函数\n\n- **返回**\n  - [T_PermissionUpdater](typing.md#T-PermissionUpdater)\n\n### _classmethod_ `append_handler(handler, parameterless=None)` {#Matcher-append-handler}\n\n- **参数**\n  - `handler` ([T_Handler](typing.md#T-Handler))\n\n  - `parameterless` (Iterable[Any] | None)\n\n- **返回**\n  - [Dependent](dependencies/index.md#Dependent)[Any]\n\n### _classmethod_ `handle(parameterless=None)` {#Matcher-handle}\n\n- **说明:** 装饰一个函数来向事件响应器直接添加一个处理函数\n\n- **参数**\n  - `parameterless` (Iterable[Any] | None): 非参数类型依赖列表\n\n- **返回**\n  - ([T_Handler](typing.md#T-Handler)) -> [T_Handler](typing.md#T-Handler)\n\n### _classmethod_ `receive(id=\"\", parameterless=None)` {#Matcher-receive}\n\n- **说明:** 装饰一个函数来指示 NoneBot 在接收用户新的一条消息后继续运行该函数\n\n- **参数**\n  - `id` (str): 消息 ID\n\n  - `parameterless` (Iterable[Any] | None): 非参数类型依赖列表\n\n- **返回**\n  - ([T_Handler](typing.md#T-Handler)) -> [T_Handler](typing.md#T-Handler)\n\n### _classmethod_ `got(key, prompt=None, parameterless=None)` {#Matcher-got}\n\n- **说明**\n\n  装饰一个函数来指示 NoneBot 获取一个参数 `key`\n\n  当要获取的 `key` 不存在时接收用户新的一条消息再运行该函数，\n  如果 `key` 已存在则直接继续运行\n\n- **参数**\n  - `key` (str): 参数名\n\n  - `prompt` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate) | None): 在参数不存在时向用户发送的消息\n\n  - `parameterless` (Iterable[Any] | None): 非参数类型依赖列表\n\n- **返回**\n  - ([T_Handler](typing.md#T-Handler)) -> [T_Handler](typing.md#T-Handler)\n\n### _classmethod_ `send(message, **kwargs)` {#Matcher-send}\n\n- **说明:** 发送一条消息给当前交互用户\n\n- **参数**\n  - `message` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate)): 消息内容\n\n  - `**kwargs` (Any): [Bot.send](adapters/index.md#Bot-send) 的参数， 请参考对应 adapter 的 bot 对象 api\n\n- **返回**\n  - Any\n\n### _classmethod_ `finish(message=None, **kwargs)` {#Matcher-finish}\n\n- **说明:** 发送一条消息给当前交互用户并结束当前事件响应器\n\n- **参数**\n  - `message` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate) | None): 消息内容\n\n  - `**kwargs`: [Bot.send](adapters/index.md#Bot-send) 的参数， 请参考对应 adapter 的 bot 对象 api\n\n- **返回**\n  - NoReturn\n\n### _classmethod_ `pause(prompt=None, **kwargs)` {#Matcher-pause}\n\n- **说明:** 发送一条消息给当前交互用户并暂停事件响应器，在接收用户新的一条消息后继续下一个处理函数\n\n- **参数**\n  - `prompt` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate) | None): 消息内容\n\n  - `**kwargs`: [Bot.send](adapters/index.md#Bot-send) 的参数， 请参考对应 adapter 的 bot 对象 api\n\n- **返回**\n  - NoReturn\n\n### _classmethod_ `reject(prompt=None, **kwargs)` {#Matcher-reject}\n\n- **说明:** 最近使用 `got` / `receive` 接收的消息不符合预期， 发送一条消息给当前交互用户并将当前事件处理流程中断在当前位置，在接收用户新的一个事件后从头开始执行当前处理函数\n\n- **参数**\n  - `prompt` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate) | None): 消息内容\n\n  - `**kwargs`: [Bot.send](adapters/index.md#Bot-send) 的参数， 请参考对应 adapter 的 bot 对象 api\n\n- **返回**\n  - NoReturn\n\n### _classmethod_ `reject_arg(key, prompt=None, **kwargs)` {#Matcher-reject-arg}\n\n- **说明:** 最近使用 `got` 接收的消息不符合预期， 发送一条消息给当前交互用户并将当前事件处理流程中断在当前位置，在接收用户新的一条消息后从头开始执行当前处理函数\n\n- **参数**\n  - `key` (str): 参数名\n\n  - `prompt` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate) | None): 消息内容\n\n  - `**kwargs`: [Bot.send](adapters/index.md#Bot-send) 的参数， 请参考对应 adapter 的 bot 对象 api\n\n- **返回**\n  - NoReturn\n\n### _classmethod_ `reject_receive(id=\"\", prompt=None, **kwargs)` {#Matcher-reject-receive}\n\n- **说明:** 最近使用 `receive` 接收的消息不符合预期， 发送一条消息给当前交互用户并将当前事件处理流程中断在当前位置，在接收用户新的一个事件后从头开始执行当前处理函数\n\n- **参数**\n  - `id` (str): 消息 id\n\n  - `prompt` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate) | None): 消息内容\n\n  - `**kwargs`: [Bot.send](adapters/index.md#Bot-send) 的参数， 请参考对应 adapter 的 bot 对象 api\n\n- **返回**\n  - NoReturn\n\n### _classmethod_ `skip()` {#Matcher-skip}\n\n- **说明**\n\n  跳过当前事件处理函数，继续下一个处理函数\n\n  通常在事件处理函数的依赖中使用。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - NoReturn\n\n### _method_ `get_receive(id, default=None)` {#Matcher-get-receive}\n\n- **说明**\n\n  获取一个 `receive` 事件\n\n  如果没有找到对应的事件，返回 `default` 值\n\n- **重载**\n\n  **1.** `(id) -> Event | None`\n  - **参数**\n    - `id` (str)\n\n  - **返回**\n    - [Event](adapters/index.md#Event) | None\n\n  **2.** `(id, default) -> Event | T`\n  - **参数**\n    - `id` (str)\n\n    - `default` (T)\n\n  - **返回**\n    - [Event](adapters/index.md#Event) | T\n\n### _method_ `set_receive(id, event)` {#Matcher-set-receive}\n\n- **说明:** 设置一个 `receive` 事件\n\n- **参数**\n  - `id` (str)\n\n  - `event` ([Event](adapters/index.md#Event))\n\n- **返回**\n  - None\n\n### _method_ `get_last_receive(default=None)` {#Matcher-get-last-receive}\n\n- **说明**\n\n  获取最近一次 `receive` 事件\n\n  如果没有事件，返回 `default` 值\n\n- **重载**\n\n  **1.** `() -> Event | None`\n  - **参数**\n\n    empty\n\n  - **返回**\n    - [Event](adapters/index.md#Event) | None\n\n  **2.** `(default) -> Event | T`\n  - **参数**\n    - `default` (T)\n\n  - **返回**\n    - [Event](adapters/index.md#Event) | T\n\n### _method_ `get_arg(key, default=None)` {#Matcher-get-arg}\n\n- **说明**\n\n  获取一个 `got` 消息\n\n  如果没有找到对应的消息，返回 `default` 值\n\n- **重载**\n\n  **1.** `(key) -> Message | None`\n  - **参数**\n    - `key` (str)\n\n  - **返回**\n    - [Message](adapters/index.md#Message) | None\n\n  **2.** `(key, default) -> Message | T`\n  - **参数**\n    - `key` (str)\n\n    - `default` (T)\n\n  - **返回**\n    - [Message](adapters/index.md#Message) | T\n\n### _method_ `set_arg(key, message)` {#Matcher-set-arg}\n\n- **说明:** 设置一个 `got` 消息\n\n- **参数**\n  - `key` (str)\n\n  - `message` ([Message](adapters/index.md#Message))\n\n- **返回**\n  - None\n\n### _method_ `set_target(target, cache=True)` {#Matcher-set-target}\n\n- **参数**\n  - `target` (str)\n\n  - `cache` (bool)\n\n- **返回**\n  - None\n\n### _method_ `get_target(default=None)` {#Matcher-get-target}\n\n- **重载**\n\n  **1.** `() -> str | None`\n  - **参数**\n\n    empty\n\n  - **返回**\n    - str | None\n\n  **2.** `(default) -> str | T`\n  - **参数**\n    - `default` (T)\n\n  - **返回**\n    - str | T\n\n### _method_ `stop_propagation()` {#Matcher-stop-propagation}\n\n- **说明:** 阻止事件传播\n\n- **参数**\n\n  empty\n\n- **返回**\n  - untyped\n\n### _async method_ `update_type(bot, event, stack=None, dependency_cache=None)` {#Matcher-update-type}\n\n- **参数**\n  - `bot` ([Bot](adapters/index.md#Bot))\n\n  - `event` ([Event](adapters/index.md#Event))\n\n  - `stack` (AsyncExitStack | None)\n\n  - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None)\n\n- **返回**\n  - str\n\n### _async method_ `update_permission(bot, event, stack=None, dependency_cache=None)` {#Matcher-update-permission}\n\n- **参数**\n  - `bot` ([Bot](adapters/index.md#Bot))\n\n  - `event` ([Event](adapters/index.md#Event))\n\n  - `stack` (AsyncExitStack | None)\n\n  - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None)\n\n- **返回**\n  - [Permission](permission.md#Permission)\n\n### _async method_ `resolve_reject()` {#Matcher-resolve-reject}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - untyped\n\n### _method_ `ensure_context(bot, event)` {#Matcher-ensure-context}\n\n- **参数**\n  - `bot` ([Bot](adapters/index.md#Bot))\n\n  - `event` ([Event](adapters/index.md#Event))\n\n- **返回**\n  - untyped\n\n### _async method_ `simple_run(bot, event, state, stack=None, dependency_cache=None)` {#Matcher-simple-run}\n\n- **参数**\n  - `bot` ([Bot](adapters/index.md#Bot))\n\n  - `event` ([Event](adapters/index.md#Event))\n\n  - `state` ([T_State](typing.md#T-State))\n\n  - `stack` (AsyncExitStack | None)\n\n  - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None)\n\n- **返回**\n  - untyped\n\n### _async method_ `run(bot, event, state, stack=None, dependency_cache=None)` {#Matcher-run}\n\n- **参数**\n  - `bot` ([Bot](adapters/index.md#Bot))\n\n  - `event` ([Event](adapters/index.md#Event))\n\n  - `state` ([T_State](typing.md#T-State))\n\n  - `stack` (AsyncExitStack | None)\n\n  - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None)\n\n- **返回**\n  - untyped\n\n## _class_ `MatcherManager()` {#MatcherManager}\n\n- **说明**\n\n  事件响应器管理器\n\n  实现了常用字典操作，用于管理事件响应器。\n\n- **参数**\n\n  empty\n\n### _method_ `keys()` {#MatcherManager-keys}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - KeysView[int]\n\n### _method_ `values()` {#MatcherManager-values}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - ValuesView[list[type[[Matcher](#Matcher)]]]\n\n### _method_ `items()` {#MatcherManager-items}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - ItemsView[int, list[type[[Matcher](#Matcher)]]]\n\n### _method_ `get(key, default=None)` {#MatcherManager-get}\n\n- **重载**\n\n  **1.** `(key) -> list[type[Matcher]] | None`\n  - **参数**\n    - `key` (int)\n\n  - **返回**\n    - list[type[[Matcher](#Matcher)]] | None\n\n  **2.** `(key, default) -> list[type[Matcher]]`\n  - **参数**\n    - `key` (int)\n\n    - `default` (list[type[[Matcher](#Matcher)]])\n\n  - **返回**\n    - list[type[[Matcher](#Matcher)]]\n\n  **3.** `(key, default) -> list[type[Matcher]] | T`\n  - **参数**\n    - `key` (int)\n\n    - `default` (T)\n\n  - **返回**\n    - list[type[[Matcher](#Matcher)]] | T\n\n### _method_ `pop(key)` {#MatcherManager-pop}\n\n- **参数**\n  - `key` (int)\n\n- **返回**\n  - list[type[[Matcher](#Matcher)]]\n\n### _method_ `popitem()` {#MatcherManager-popitem}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - tuple[int, list[type[[Matcher](#Matcher)]]]\n\n### _method_ `clear()` {#MatcherManager-clear}\n\n- **参数**\n\n  empty\n\n- **返回**\n  - None\n\n### _method_ `update(m, /)` {#MatcherManager-update}\n\n- **参数**\n  - `m` (MutableMapping[int, list[type[[Matcher](#Matcher)]]])\n\n- **返回**\n  - None\n\n### _method_ `setdefault(key, default)` {#MatcherManager-setdefault}\n\n- **参数**\n  - `key` (int)\n\n  - `default` (list[type[[Matcher](#Matcher)]])\n\n- **返回**\n  - list[type[[Matcher](#Matcher)]]\n\n### _method_ `set_provider(provider_class)` {#MatcherManager-set-provider}\n\n- **说明:** 设置事件响应器存储器\n\n- **参数**\n  - `provider_class` (type[[MatcherProvider](#MatcherProvider)]): 事件响应器存储器类\n\n- **返回**\n  - None\n\n## _abstract class_ `MatcherProvider(matchers)` {#MatcherProvider}\n\n- **说明:** 事件响应器存储器基类\n\n- **参数**\n  - `matchers` (Mapping[int, list[type[[Matcher](#Matcher)]]]): 当前存储器中已有的事件响应器\n\n## _var_ `matchers` {#matchers}\n\n- **类型:** untyped\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/api/message.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 2\ndescription: nonebot.message 模块\n---\n\n# nonebot.message\n\n本模块定义了事件处理主要流程。\n\nNoneBot 内部处理并按优先级分发事件给所有事件响应器，提供了多个插槽以进行事件的预处理等。\n\n## _def_ `event_preprocessor(func)` {#event-preprocessor}\n\n- **说明**\n\n  事件预处理。\n\n  装饰一个函数，使它在每次接收到事件并分发给各响应器之前执行。\n\n- **参数**\n  - `func` ([T_EventPreProcessor](typing.md#T-EventPreProcessor))\n\n- **返回**\n  - [T_EventPreProcessor](typing.md#T-EventPreProcessor)\n\n## _def_ `event_postprocessor(func)` {#event-postprocessor}\n\n- **说明**\n\n  事件后处理。\n\n  装饰一个函数，使它在每次接收到事件并分发给各响应器之后执行。\n\n- **参数**\n  - `func` ([T_EventPostProcessor](typing.md#T-EventPostProcessor))\n\n- **返回**\n  - [T_EventPostProcessor](typing.md#T-EventPostProcessor)\n\n## _def_ `run_preprocessor(func)` {#run-preprocessor}\n\n- **说明**\n\n  运行预处理。\n\n  装饰一个函数，使它在每次事件响应器运行前执行。\n\n- **参数**\n  - `func` ([T_RunPreProcessor](typing.md#T-RunPreProcessor))\n\n- **返回**\n  - [T_RunPreProcessor](typing.md#T-RunPreProcessor)\n\n## _def_ `run_postprocessor(func)` {#run-postprocessor}\n\n- **说明**\n\n  运行后处理。\n\n  装饰一个函数，使它在每次事件响应器运行后执行。\n\n- **参数**\n  - `func` ([T_RunPostProcessor](typing.md#T-RunPostProcessor))\n\n- **返回**\n  - [T_RunPostProcessor](typing.md#T-RunPostProcessor)\n\n## _async def_ `check_and_run_matcher(Matcher, bot, event, state, stack=None, dependency_cache=None)` {#check-and-run-matcher}\n\n- **说明:** 检查并运行事件响应器。\n\n- **参数**\n  - `Matcher` (type[[Matcher](matcher.md#Matcher)]): 事件响应器\n\n  - `bot` ([Bot](adapters/index.md#Bot)): Bot 对象\n\n  - `event` ([Event](adapters/index.md#Event)): Event 对象\n\n  - `state` ([T_State](typing.md#T-State)): 会话状态\n\n  - `stack` (AsyncExitStack | None): 异步上下文栈\n\n  - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None): 依赖缓存\n\n- **返回**\n  - None\n\n## _async def_ `handle_event(bot, event)` {#handle-event}\n\n- **说明:** 处理一个事件。调用该函数以实现分发事件。\n\n- **参数**\n  - `bot` ([Bot](adapters/index.md#Bot)): Bot 对象\n\n  - `event` ([Event](adapters/index.md#Event)): Event 对象\n\n- **返回**\n  - None\n\n- **用法**\n\n  ```python\n  driver.task_group.start_soon(handle_event, bot, event)\n  ```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/api/params.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 4\ndescription: nonebot.params 模块\n---\n\n# nonebot.params\n\n本模块定义了依赖注入的各类参数。\n\n## _def_ `Arg(key=None)` {#Arg}\n\n- **说明:** Arg 参数消息\n\n- **参数**\n  - `key` (str | None)\n\n- **返回**\n  - Any\n\n## _class_ `ArgParam(*args, key, type, **kwargs)` {#ArgParam}\n\n- **说明**\n\n  Arg 注入参数\n\n  本注入解析事件响应器操作 `got` 所获取的参数。\n\n  可以通过 `Arg`、`ArgStr`、`ArgPlainText` 等函数参数 `key` 指定获取的参数，\n  留空则会根据参数名称获取。\n\n- **参数**\n  - `*args`\n\n  - `key` (str)\n\n  - `type` (Literal['message', 'str', 'plaintext', 'prompt'])\n\n  - `**kwargs` (Any)\n\n## _def_ `ArgPlainText(key=None)` {#ArgPlainText}\n\n- **说明:** Arg 参数消息纯文本\n\n- **参数**\n  - `key` (str | None)\n\n- **返回**\n  - str\n\n## _def_ `ArgPromptResult(key=None)` {#ArgPromptResult}\n\n- **说明:** `arg` prompt 发送结果\n\n- **参数**\n  - `key` (str | None)\n\n- **返回**\n  - Any\n\n## _def_ `ArgStr(key=None)` {#ArgStr}\n\n- **说明:** Arg 参数消息文本\n\n- **参数**\n  - `key` (str | None)\n\n- **返回**\n  - str\n\n## _class_ `BotParam(*args, checker=None, **kwargs)` {#BotParam}\n\n- **说明**\n\n  注入参数。\n\n  本注入解析所有类型为且仅为 [Bot](adapters/index.md#Bot) 及其子类或 `None` 的参数。\n\n  为保证兼容性，本注入还会解析名为 `bot` 且没有类型注解的参数。\n\n- **参数**\n  - `*args`\n\n  - `checker` ([ModelField](compat.md#ModelField) | None)\n\n  - `**kwargs` (Any)\n\n## _class_ `DefaultParam(*args, validate=False, **kwargs)` {#DefaultParam}\n\n- **说明**\n\n  默认值注入参数\n\n  本注入解析所有剩余未能解析且具有默认值的参数。\n\n  本注入参数应该具有最低优先级，因此应该在所有其他注入参数之后使用。\n\n- **参数**\n  - `*args`\n\n  - `validate` (bool)\n\n  - `**kwargs` (Any)\n\n## _class_ `DependParam(*args, dependent, use_cache, **kwargs)` {#DependParam}\n\n- **说明**\n\n  子依赖注入参数。\n\n  本注入解析所有子依赖注入，然后将它们的返回值作为参数值传递给父依赖。\n\n  本注入应该具有最高优先级，因此应该在其他参数之前检查。\n\n- **参数**\n  - `*args`\n\n  - `dependent` ([Dependent](dependencies/index.md#Dependent)[Any])\n\n  - `use_cache` (bool)\n\n  - `**kwargs` (Any)\n\n## _def_ `Depends(dependency=None, *, use_cache=True, validate=False)` {#Depends}\n\n- **说明:** 子依赖装饰器\n\n- **参数**\n  - `dependency` ([T_Handler](typing.md#T-Handler) | None): 依赖函数。默认为参数的类型注释。\n\n  - `use_cache` (bool): 是否使用缓存。默认为 `True`。\n\n  - `validate` (bool | PydanticFieldInfo): 是否使用 Pydantic 类型校验。默认为 `False`。\n\n- **返回**\n  - Any\n\n- **用法**\n\n  ```python\n  def depend_func() -> Any:\n      return ...\n\n  def depend_gen_func():\n      try:\n          yield ...\n      finally:\n          ...\n\n  async def handler(\n      param_name: Any = Depends(depend_func),\n      gen: Any = Depends(depend_gen_func),\n  ):\n      ...\n  ```\n\n## _class_ `EventParam(*args, checker=None, **kwargs)` {#EventParam}\n\n- **说明**\n\n  注入参数\n\n  本注入解析所有类型为且仅为 [Event](adapters/index.md#Event) 及其子类或 `None` 的参数。\n\n  为保证兼容性，本注入还会解析名为 `event` 且没有类型注解的参数。\n\n- **参数**\n  - `*args`\n\n  - `checker` ([ModelField](compat.md#ModelField) | None)\n\n  - `**kwargs` (Any)\n\n## _class_ `ExceptionParam(*args, validate=False, **kwargs)` {#ExceptionParam}\n\n- **说明**\n\n  的异常注入参数\n\n  本注入解析所有类型为 `Exception` 或 `None` 的参数。\n\n  为保证兼容性，本注入还会解析名为 `exception` 且没有类型注解的参数。\n\n- **参数**\n  - `*args`\n\n  - `validate` (bool)\n\n  - `**kwargs` (Any)\n\n## _class_ `MatcherParam(*args, checker=None, **kwargs)` {#MatcherParam}\n\n- **说明**\n\n  事件响应器实例注入参数\n\n  本注入解析所有类型为且仅为 [Matcher](matcher.md#Matcher) 及其子类或 `None` 的参数。\n\n  为保证兼容性，本注入还会解析名为 `matcher` 且没有类型注解的参数。\n\n- **参数**\n  - `*args`\n\n  - `checker` ([ModelField](compat.md#ModelField) | None)\n\n  - `**kwargs` (Any)\n\n## _class_ `StateParam(*args, validate=False, **kwargs)` {#StateParam}\n\n- **说明**\n\n  事件处理状态注入参数\n\n  本注入解析所有类型为 `T_State` 的参数。\n\n  为保证兼容性，本注入还会解析名为 `state` 且没有类型注解的参数。\n\n- **参数**\n  - `*args`\n\n  - `validate` (bool)\n\n  - `**kwargs` (Any)\n\n## _def_ `EventType()` {#EventType}\n\n- **说明:** 类型参数\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n## _def_ `EventMessage()` {#EventMessage}\n\n- **说明:** 消息参数\n\n- **参数**\n\n  empty\n\n- **返回**\n  - Any\n\n## _def_ `EventPlainText()` {#EventPlainText}\n\n- **说明:** 纯文本消息参数\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n## _def_ `EventToMe()` {#EventToMe}\n\n- **说明:** `to_me` 参数\n\n- **参数**\n\n  empty\n\n- **返回**\n  - bool\n\n## _def_ `Command()` {#Command}\n\n- **说明:** 消息命令元组\n\n- **参数**\n\n  empty\n\n- **返回**\n  - tuple[str, ...]\n\n## _def_ `RawCommand()` {#RawCommand}\n\n- **说明:** 消息命令文本\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n## _def_ `CommandArg()` {#CommandArg}\n\n- **说明:** 消息命令参数\n\n- **参数**\n\n  empty\n\n- **返回**\n  - Any\n\n## _def_ `CommandStart()` {#CommandStart}\n\n- **说明:** 消息命令开头\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n## _def_ `CommandWhitespace()` {#CommandWhitespace}\n\n- **说明:** 消息命令与参数之间的空白\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n## _def_ `ShellCommandArgs()` {#ShellCommandArgs}\n\n- **说明:** shell 命令解析后的参数字典\n\n- **参数**\n\n  empty\n\n- **返回**\n  - Any\n\n## _def_ `ShellCommandArgv()` {#ShellCommandArgv}\n\n- **说明:** shell 命令原始参数列表\n\n- **参数**\n\n  empty\n\n- **返回**\n  - Any\n\n## _def_ `RegexMatched()` {#RegexMatched}\n\n- **说明:** 正则匹配结果\n\n- **参数**\n\n  empty\n\n- **返回**\n  - Match[str]\n\n## _def_ `RegexStr(*groups)` {#RegexStr}\n\n- **说明:** 正则匹配结果文本\n\n- **重载**\n\n  **1.** `(group, /) -> str`\n  - **参数**\n    - `group` (Literal[0])\n\n  - **返回**\n    - str\n\n  **2.** `(group, /) -> str | Any`\n  - **参数**\n    - `group` (str | int)\n\n  - **返回**\n    - str | Any\n\n  **3.** `(group1, group2, /, *groups) -> tuple[str | Any, ...]`\n  - **参数**\n    - `group1` (str | int)\n\n    - `group2` (str | int)\n\n    - `*groups` (str | int)\n\n  - **返回**\n    - tuple[str | Any, ...]\n\n## _def_ `RegexGroup()` {#RegexGroup}\n\n- **说明:** 正则匹配结果 group 元组\n\n- **参数**\n\n  empty\n\n- **返回**\n  - tuple[Any, ...]\n\n## _def_ `RegexDict()` {#RegexDict}\n\n- **说明:** 正则匹配结果 group 字典\n\n- **参数**\n\n  empty\n\n- **返回**\n  - dict[str, Any]\n\n## _def_ `Startswith()` {#Startswith}\n\n- **说明:** 响应触发前缀\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n## _def_ `Endswith()` {#Endswith}\n\n- **说明:** 响应触发后缀\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n## _def_ `Fullmatch()` {#Fullmatch}\n\n- **说明:** 响应触发完整消息\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n## _def_ `Keyword()` {#Keyword}\n\n- **说明:** 响应触发关键字\n\n- **参数**\n\n  empty\n\n- **返回**\n  - str\n\n## _def_ `Received(id=None, default=None)` {#Received}\n\n- **说明:** `receive` 事件参数\n\n- **参数**\n  - `id` (str | None)\n\n  - `default` (Any)\n\n- **返回**\n  - Any\n\n## _def_ `LastReceived(default=None)` {#LastReceived}\n\n- **说明:** `last_receive` 事件参数\n\n- **参数**\n  - `default` (Any)\n\n- **返回**\n  - Any\n\n## _def_ `ReceivePromptResult(id=None)` {#ReceivePromptResult}\n\n- **说明:** `receive` prompt 发送结果\n\n- **参数**\n  - `id` (str | None)\n\n- **返回**\n  - Any\n\n## _def_ `PausePromptResult()` {#PausePromptResult}\n\n- **说明:** `pause` prompt 发送结果\n\n- **参数**\n\n  empty\n\n- **返回**\n  - Any\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/api/permission.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 6\ndescription: nonebot.permission 模块\n---\n\n# nonebot.permission\n\n本模块是 [Matcher.permission](matcher.md#Matcher-permission) 的类型定义。\n\n每个[事件响应器](matcher.md#Matcher)\n拥有一个 [Permission](#Permission)，其中是 `PermissionChecker` 的集合。\n只要有一个 `PermissionChecker` 检查结果为 `True` 时就会继续运行。\n\n## _def_ `USER(*users, perm=None)` {#USER}\n\n- **说明**\n\n  匹配当前事件属于指定会话。\n\n  如果 `perm` 中仅有 `User` 类型的权限检查函数，则会去除原有检查函数的会话 ID 限制。\n\n- **参数**\n  - `*users` (str)\n\n  - `perm` (Permission | None): 需要同时满足的权限\n\n  - `user`: 会话白名单\n\n- **返回**\n  - untyped\n\n## _class_ `Permission(*checkers)` {#Permission}\n\n- **说明**\n\n  权限类。\n\n  当事件传递时，在 [Matcher](matcher.md#Matcher) 运行前进行检查。\n\n- **参数**\n  - `*checkers` ([T_PermissionChecker](typing.md#T-PermissionChecker) | [Dependent](dependencies/index.md#Dependent)[bool]): PermissionChecker\n\n- **用法**\n\n  ```python\n  Permission(async_function) | sync_function\n  # 等价于\n  Permission(async_function, sync_function)\n  ```\n\n### _instance-var_ `checkers` {#Permission-checkers}\n\n- **类型:** set[[Dependent](dependencies/index.md#Dependent)[bool]]\n\n- **说明:** 存储 `PermissionChecker`\n\n### _async method_ `__call__(bot, event, stack=None, dependency_cache=None)` {#Permission---call--}\n\n- **说明:** 检查是否满足某个权限。\n\n- **参数**\n  - `bot` ([Bot](adapters/index.md#Bot)): Bot 对象\n\n  - `event` ([Event](adapters/index.md#Event)): Event 对象\n\n  - `stack` (AsyncExitStack | None): 异步上下文栈\n\n  - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None): 依赖缓存\n\n- **返回**\n  - bool\n\n## _class_ `User(users, perm=None)` {#User}\n\n- **说明:** 检查当前事件是否属于指定会话。\n\n- **参数**\n  - `users` (tuple[str, ...]): 会话 ID 元组\n\n  - `perm` (Permission | None): 需同时满足的权限\n\n### _classmethod_ `from_event(event, perm=None)` {#User-from-event}\n\n- **说明**\n\n  从事件中获取会话 ID。\n\n  如果 `perm` 中仅有 `User` 类型的权限检查函数，则会去除原有的会话 ID 限制。\n\n- **参数**\n  - `event` ([Event](adapters/index.md#Event)): Event 对象\n\n  - `perm` (Permission | None): 需同时满足的权限\n\n- **返回**\n  - Self\n\n### _classmethod_ `from_permission(*users, perm=None)` {#User-from-permission}\n\n- **说明**\n\n  指定会话与权限。\n\n  如果 `perm` 中仅有 `User` 类型的权限检查函数，则会去除原有的会话 ID 限制。\n\n- **参数**\n  - `*users` (str): 会话白名单\n\n  - `perm` (Permission | None): 需同时满足的权限\n\n- **返回**\n  - Self\n\n## _class_ `Message(<auto>)` {#Message}\n\n- **说明:** 检查是否为消息事件\n\n- **参数**\n\n  auto\n\n## _class_ `Notice(<auto>)` {#Notice}\n\n- **说明:** 检查是否为通知事件\n\n- **参数**\n\n  auto\n\n## _class_ `Request(<auto>)` {#Request}\n\n- **说明:** 检查是否为请求事件\n\n- **参数**\n\n  auto\n\n## _class_ `MetaEvent(<auto>)` {#MetaEvent}\n\n- **说明:** 检查是否为元事件\n\n- **参数**\n\n  auto\n\n## _var_ `MESSAGE` {#MESSAGE}\n\n- **类型:** [Permission](#Permission)\n\n- **说明**\n\n  匹配任意 `message` 类型事件\n\n  仅在需要同时捕获不同类型事件时使用，优先使用 message type 的 Matcher。\n\n## _var_ `NOTICE` {#NOTICE}\n\n- **类型:** [Permission](#Permission)\n\n- **说明**\n\n  匹配任意 `notice` 类型事件\n\n  仅在需要同时捕获不同类型事件时使用，优先使用 notice type 的 Matcher。\n\n## _var_ `REQUEST` {#REQUEST}\n\n- **类型:** [Permission](#Permission)\n\n- **说明**\n\n  匹配任意 `request` 类型事件\n\n  仅在需要同时捕获不同类型事件时使用，优先使用 request type 的 Matcher。\n\n## _var_ `METAEVENT` {#METAEVENT}\n\n- **类型:** [Permission](#Permission)\n\n- **说明**\n\n  匹配任意 `meta_event` 类型事件\n\n  仅在需要同时捕获不同类型事件时使用，优先使用 meta_event type 的 Matcher。\n\n## _class_ `SuperUser(<auto>)` {#SuperUser}\n\n- **说明:** 检查当前事件是否是消息事件且属于超级管理员\n\n- **参数**\n\n  auto\n\n## _var_ `SUPERUSER` {#SUPERUSER}\n\n- **类型:** [Permission](#Permission)\n\n- **说明:** 匹配任意超级用户事件\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/api/plugin/_category_.json",
    "content": "{\n  \"position\": 12\n}\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/api/plugin/index.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 0\ndescription: nonebot.plugin 模块\n---\n\n# nonebot.plugin\n\n本模块为 NoneBot 插件开发提供便携的定义函数。\n\n## 快捷导入\n\n为方便使用，本模块从子模块导入了部分内容，以下内容可以直接通过本模块导入:\n\n- `on` => [`on`](on.md#on)\n- `on_metaevent` => [`on_metaevent`](on.md#on-metaevent)\n- `on_message` => [`on_message`](on.md#on-message)\n- `on_notice` => [`on_notice`](on.md#on-notice)\n- `on_request` => [`on_request`](on.md#on-request)\n- `on_startswith` => [`on_startswith`](on.md#on-startswith)\n- `on_endswith` => [`on_endswith`](on.md#on-endswith)\n- `on_fullmatch` => [`on_fullmatch`](on.md#on-fullmatch)\n- `on_keyword` => [`on_keyword`](on.md#on-keyword)\n- `on_command` => [`on_command`](on.md#on-command)\n- `on_shell_command` => [`on_shell_command`](on.md#on-shell-command)\n- `on_regex` => [`on_regex`](on.md#on-regex)\n- `on_type` => [`on_type`](on.md#on-type)\n- `CommandGroup` => [`CommandGroup`](on.md#CommandGroup)\n- `Matchergroup` => [`MatcherGroup`](on.md#MatcherGroup)\n- `load_plugin` => [`load_plugin`](load.md#load-plugin)\n- `load_plugins` => [`load_plugins`](load.md#load-plugins)\n- `load_all_plugins` => [`load_all_plugins`](load.md#load-all-plugins)\n- `load_from_json` => [`load_from_json`](load.md#load-from-json)\n- `load_from_toml` => [`load_from_toml`](load.md#load-from-toml)\n- `load_builtin_plugin` =>\n  [`load_builtin_plugin`](load.md#load-builtin-plugin)\n- `load_builtin_plugins` =>\n  [`load_builtin_plugins`](load.md#load-builtin-plugins)\n- `require` => [`require`](load.md#require)\n- `PluginMetadata` => [`PluginMetadata`](model.md#PluginMetadata)\n\n## _def_ `get_plugin(plugin_id)` {#get-plugin}\n\n- **说明**\n\n  获取已经导入的某个插件。\n\n  如果为 `load_plugins` 文件夹导入的插件，则为文件(夹)名。\n\n  如果为嵌套的子插件，标识符为 `父插件标识符:子插件文件(夹)名`。\n\n- **参数**\n  - `plugin_id` (str): 插件标识符，即 [Plugin.id\\_](model.md#Plugin-id-)。\n\n- **返回**\n  - [Plugin](model.md#Plugin) | None\n\n## _def_ `get_plugin_by_module_name(module_name)` {#get-plugin-by-module-name}\n\n- **说明**\n\n  通过模块名获取已经导入的某个插件。\n\n  如果提供的模块名为某个插件的子模块，同样会返回该插件。\n\n- **参数**\n  - `module_name` (str): 模块名，即 [Plugin.module_name](model.md#Plugin-module-name)。\n\n- **返回**\n  - [Plugin](model.md#Plugin) | None\n\n## _def_ `get_loaded_plugins()` {#get-loaded-plugins}\n\n- **说明:** 获取当前已导入的所有插件。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - set[[Plugin](model.md#Plugin)]\n\n## _def_ `get_available_plugin_names()` {#get-available-plugin-names}\n\n- **说明:** 获取当前所有可用的插件标识符（包含尚未加载的插件）。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - set[str]\n\n## _def_ `get_plugin_config(config)` {#get-plugin-config}\n\n- **说明:** 从全局配置获取当前插件需要的配置项。\n\n- **参数**\n  - `config` (type[C])\n\n- **返回**\n  - C\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/api/plugin/load.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 1\ndescription: nonebot.plugin.load 模块\n---\n\n# nonebot.plugin.load\n\n本模块定义插件加载接口。\n\n## _def_ `load_plugin(module_path)` {#load-plugin}\n\n- **说明:** 加载单个插件，可以是本地插件或是通过 `pip` 安装的插件。\n\n- **参数**\n  - `module_path` (str | Path): 插件名称 `path.to.your.plugin` 或插件路径 `pathlib.Path(path/to/your/plugin)`\n\n- **返回**\n  - [Plugin](model.md#Plugin) | None\n\n## _def_ `load_plugins(*plugin_dir)` {#load-plugins}\n\n- **说明:** 导入文件夹下多个插件，以 `_` 开头的插件不会被导入!\n\n- **参数**\n  - `*plugin_dir` (str): 文件夹路径\n\n- **返回**\n  - set[[Plugin](model.md#Plugin)]\n\n## _def_ `load_all_plugins(module_path, plugin_dir)` {#load-all-plugins}\n\n- **说明:** 导入指定列表中的插件以及指定目录下多个插件，以 `_` 开头的插件不会被导入!\n\n- **参数**\n  - `module_path` (Iterable[str]): 指定插件集合\n\n  - `plugin_dir` (Iterable[str]): 指定文件夹路径集合\n\n- **返回**\n  - set[[Plugin](model.md#Plugin)]\n\n## _def_ `load_from_json(file_path, encoding=\"utf-8\")` {#load-from-json}\n\n- **说明:** 导入指定 json 文件中的 `plugins` 以及 `plugin_dirs` 下多个插件。 以 `_` 开头的插件不会被导入!\n\n- **参数**\n  - `file_path` (str): 指定 json 文件路径\n\n  - `encoding` (str): 指定 json 文件编码\n\n- **返回**\n  - set[[Plugin](model.md#Plugin)]\n\n- **用法**\n\n  ```json title=plugins.json\n  {\n    \"plugins\": [\"some_plugin\"],\n    \"plugin_dirs\": [\"some_dir\"]\n  }\n  ```\n\n  ```python\n  nonebot.load_from_json(\"plugins.json\")\n  ```\n\n## _def_ `load_from_toml(file_path, encoding=\"utf-8\")` {#load-from-toml}\n\n- **说明:** 导入指定 toml 文件 `[tool.nonebot]` 中的 `plugins` 以及 `plugin_dirs` 下多个插件。 以 `_` 开头的插件不会被导入!\n\n- **参数**\n  - `file_path` (str): 指定 toml 文件路径\n\n  - `encoding` (str): 指定 toml 文件编码\n\n- **返回**\n  - set[[Plugin](model.md#Plugin)]\n\n- **用法**\n\n  新格式:\n\n  ```toml title=pyproject.toml\n  [tool.nonebot]\n  plugin_dirs = [\"some_dir\"]\n\n  [tool.nonebot.plugins]\n  some-store-plugin = [\"some_store_plugin\"]\n  \"@local\" = [\"some_local_plugin\"]\n  ```\n\n  旧格式:\n\n  ```toml title=pyproject.toml\n  [tool.nonebot]\n  plugins = [\"some_plugin\"]\n  plugin_dirs = [\"some_dir\"]\n  ```\n\n  ```python\n  nonebot.load_from_toml(\"pyproject.toml\")\n  ```\n\n## _def_ `load_builtin_plugin(name)` {#load-builtin-plugin}\n\n- **说明:** 导入 NoneBot 内置插件。\n\n- **参数**\n  - `name` (str): 插件名称\n\n- **返回**\n  - [Plugin](model.md#Plugin) | None\n\n## _def_ `load_builtin_plugins(*plugins)` {#load-builtin-plugins}\n\n- **说明:** 导入多个 NoneBot 内置插件。\n\n- **参数**\n  - `*plugins` (str): 插件名称列表\n\n- **返回**\n  - set[[Plugin](model.md#Plugin)]\n\n## _def_ `require(name)` {#require}\n\n- **说明:** 声明依赖插件。\n\n- **参数**\n  - `name` (str): 插件模块名或插件标识符，仅在已声明插件的情况下可使用标识符。\n\n- **返回**\n  - ModuleType\n\n- **异常**\n  - RuntimeError: 插件无法加载\n\n## _def_ `inherit_supported_adapters(*names)` {#inherit-supported-adapters}\n\n- **说明**\n\n  获取已加载插件的适配器支持状态集合。\n\n  如果传入了多个插件名称，返回值会自动取交集。\n\n- **参数**\n  - `*names` (str): 插件名称列表。\n\n- **返回**\n  - set[str] | None\n\n- **异常**\n  - RuntimeError: 插件未加载\n\n  - ValueError: 插件缺少元数据\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/api/plugin/manager.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 5\ndescription: nonebot.plugin.manager 模块\n---\n\n# nonebot.plugin.manager\n\n本模块实现插件加载流程。\n\n参考: [import hooks](https://docs.python.org/3/reference/import.html#import-hooks), [PEP302](https://www.python.org/dev/peps/pep-0302/)\n\n## _class_ `PluginManager(plugins=None, search_path=None)` {#PluginManager}\n\n- **说明:** 插件管理器。\n\n- **参数**\n  - `plugins` (Iterable[str] | None): 独立插件模块名集合。\n\n  - `search_path` (Iterable[str] | None): 插件搜索路径（文件夹），相对于当前工作目录。\n\n### _property_ `third_party_plugins` {#PluginManager-third-party-plugins}\n\n- **类型:** set[str]\n\n- **说明:** 返回所有独立插件标识符。\n\n### _property_ `searched_plugins` {#PluginManager-searched-plugins}\n\n- **类型:** set[str]\n\n- **说明:** 返回已搜索到的插件标识符。\n\n### _property_ `available_plugins` {#PluginManager-available-plugins}\n\n- **类型:** set[str]\n\n- **说明:** 返回当前插件管理器中可用的插件标识符。\n\n### _property_ `controlled_modules` {#PluginManager-controlled-modules}\n\n- **类型:** dict[str, str]\n\n- **说明:** 返回当前插件管理器中控制的插件标识符与模块路径映射字典。\n\n### _method_ `load_plugin(name)` {#PluginManager-load-plugin}\n\n- **说明**\n\n  加载指定插件。\n\n  可以使用完整插件模块名或者插件标识符加载。\n\n- **参数**\n  - `name` (str): 插件名称或插件标识符。\n\n- **返回**\n  - [Plugin](model.md#Plugin) | None\n\n### _method_ `load_all_plugins()` {#PluginManager-load-all-plugins}\n\n- **说明:** 加载所有可用插件。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - set[[Plugin](model.md#Plugin)]\n\n## _class_ `PluginFinder(<auto>)` {#PluginFinder}\n\n- **参数**\n\n  auto\n\n### _method_ `find_spec(fullname, path, target=None)` {#PluginFinder-find-spec}\n\n- **参数**\n  - `fullname` (str)\n\n  - `path` (Sequence[str] | None)\n\n  - `target` (ModuleType | None)\n\n- **返回**\n  - untyped\n\n## _class_ `PluginLoader(manager, fullname, path)` {#PluginLoader}\n\n- **参数**\n  - `manager` (PluginManager)\n\n  - `fullname` (str)\n\n  - `path` (str)\n\n### _method_ `create_module(spec)` {#PluginLoader-create-module}\n\n- **参数**\n  - `spec`\n\n- **返回**\n  - ModuleType | None\n\n### _method_ `exec_module(module)` {#PluginLoader-exec-module}\n\n- **参数**\n  - `module` (ModuleType)\n\n- **返回**\n  - None\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/api/plugin/model.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 3\ndescription: nonebot.plugin.model 模块\n---\n\n# nonebot.plugin.model\n\n本模块定义插件相关信息。\n\n## _class_ `PluginMetadata(<auto>)` {#PluginMetadata}\n\n- **说明:** 插件元信息，由插件编写者提供\n\n- **参数**\n\n  auto\n\n### _instance-var_ `name` {#PluginMetadata-name}\n\n- **类型:** str\n\n- **说明:** 插件名称\n\n### _instance-var_ `description` {#PluginMetadata-description}\n\n- **类型:** str\n\n- **说明:** 插件功能介绍\n\n### _instance-var_ `usage` {#PluginMetadata-usage}\n\n- **类型:** str\n\n- **说明:** 插件使用方法\n\n### _class-var_ `type` {#PluginMetadata-type}\n\n- **类型:** str | None\n\n- **说明:** 插件类型，用于商店分类\n\n### _class-var_ `homepage` {#PluginMetadata-homepage}\n\n- **类型:** str | None\n\n- **说明:** 插件主页\n\n### _class-var_ `config` {#PluginMetadata-config}\n\n- **类型:** type[BaseModel] | None\n\n- **说明:** 插件配置项\n\n### _class-var_ `supported_adapters` {#PluginMetadata-supported-adapters}\n\n- **类型:** set[str] | None\n\n- **说明**\n\n  插件支持的适配器模块路径\n\n  格式为 `<module>[:<Adapter>]`，`~` 为 `nonebot.adapters.` 的缩写。\n\n  `None` 表示支持**所有适配器**。\n\n### _class-var_ `extra` {#PluginMetadata-extra}\n\n- **类型:** dict[Any, Any]\n\n- **说明:** 插件额外信息，可由插件编写者自由扩展定义\n\n### _method_ `get_supported_adapters()` {#PluginMetadata-get-supported-adapters}\n\n- **说明:** 获取当前已安装的插件支持适配器类列表\n\n- **参数**\n\n  empty\n\n- **返回**\n  - set[type[[Adapter](../adapters/index.md#Adapter)]] | None\n\n## _class_ `Plugin(<auto>)` {#Plugin}\n\n- **说明:** 存储插件信息\n\n- **参数**\n\n  auto\n\n### _instance-var_ `name` {#Plugin-name}\n\n- **类型:** str\n\n- **说明:** 插件名称，NoneBot 使用 文件/文件夹 名称作为插件名称\n\n### _instance-var_ `module` {#Plugin-module}\n\n- **类型:** ModuleType\n\n- **说明:** 插件模块对象\n\n### _instance-var_ `module_name` {#Plugin-module-name}\n\n- **类型:** str\n\n- **说明:** 点分割模块路径\n\n### _instance-var_ `manager` {#Plugin-manager}\n\n- **类型:** [PluginManager](manager.md#PluginManager)\n\n- **说明:** 导入该插件的插件管理器\n\n### _class-var_ `matcher` {#Plugin-matcher}\n\n- **类型:** set[type[[Matcher](../matcher.md#Matcher)]]\n\n- **说明:** 插件加载时定义的 `Matcher`\n\n### _class-var_ `parent_plugin` {#Plugin-parent-plugin}\n\n- **类型:** Plugin | None\n\n- **说明:** 父插件\n\n### _class-var_ `sub_plugins` {#Plugin-sub-plugins}\n\n- **类型:** set[Plugin]\n\n- **说明:** 子插件集合\n\n### _class-var_ `metadata` {#Plugin-metadata}\n\n- **类型:** PluginMetadata | None\n\n- **说明:** 插件元信息\n\n### _property_ `id_` {#Plugin-id-}\n\n- **类型:** str\n\n- **说明:** 插件索引标识\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/api/plugin/on.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 2\ndescription: nonebot.plugin.on 模块\n---\n\n# nonebot.plugin.on\n\n本模块定义事件响应器便携定义函数。\n\n## _def_ `store_matcher(matcher)` {#store-matcher}\n\n- **说明:** 存储一个事件响应器到插件。\n\n- **参数**\n  - `matcher` (type[[Matcher](../matcher.md#Matcher)]): 事件响应器\n\n- **返回**\n  - None\n\n## _def_ `get_matcher_plugin(depth=...)` {#get-matcher-plugin}\n\n- **说明**\n\n  获取事件响应器定义所在插件。\n\n  **Deprecated**, 请使用 [get_matcher_source](#get-matcher-source) 获取信息。\n\n- **参数**\n  - `depth` (int): 调用栈深度\n\n- **返回**\n  - [Plugin](model.md#Plugin) | None\n\n## _def_ `get_matcher_module(depth=...)` {#get-matcher-module}\n\n- **说明**\n\n  获取事件响应器定义所在模块。\n\n  **Deprecated**, 请使用 [get_matcher_source](#get-matcher-source) 获取信息。\n\n- **参数**\n  - `depth` (int): 调用栈深度\n\n- **返回**\n  - ModuleType | None\n\n## _def_ `get_matcher_source(depth=...)` {#get-matcher-source}\n\n- **说明:** 获取事件响应器定义所在源码信息。\n\n- **参数**\n  - `depth` (int): 调用栈深度\n\n- **返回**\n  - MatcherSource | None\n\n## _def_ `on(type=\"\", rule=..., permission=..., *, handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on}\n\n- **说明:** 注册一个基础事件响应器，可自定义类型。\n\n- **参数**\n  - `type` (str): 事件响应器类型\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n## _def_ `on_metaevent(rule=..., permission=..., *, handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-metaevent}\n\n- **说明:** 注册一个元事件响应器。\n\n- **参数**\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n## _def_ `on_message(rule=..., permission=..., *, handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-message}\n\n- **说明:** 注册一个消息事件响应器。\n\n- **参数**\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n## _def_ `on_notice(rule=..., permission=..., *, handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-notice}\n\n- **说明:** 注册一个通知事件响应器。\n\n- **参数**\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n## _def_ `on_request(rule=..., permission=..., *, handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-request}\n\n- **说明:** 注册一个请求事件响应器。\n\n- **参数**\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n## _def_ `on_startswith(msg, rule=..., ignorecase=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-startswith}\n\n- **说明:** 注册一个消息事件响应器，并且当消息的**文本部分**以指定内容开头时响应。\n\n- **参数**\n  - `msg` (str | tuple[str, ...]): 指定消息开头内容\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `ignorecase` (bool): 是否忽略大小写\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n## _def_ `on_endswith(msg, rule=..., ignorecase=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-endswith}\n\n- **说明:** 注册一个消息事件响应器，并且当消息的**文本部分**以指定内容结尾时响应。\n\n- **参数**\n  - `msg` (str | tuple[str, ...]): 指定消息结尾内容\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `ignorecase` (bool): 是否忽略大小写\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n## _def_ `on_fullmatch(msg, rule=..., ignorecase=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-fullmatch}\n\n- **说明:** 注册一个消息事件响应器，并且当消息的**文本部分**与指定内容完全一致时响应。\n\n- **参数**\n  - `msg` (str | tuple[str, ...]): 指定消息全匹配内容\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `ignorecase` (bool): 是否忽略大小写\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n## _def_ `on_keyword(keywords, rule=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-keyword}\n\n- **说明:** 注册一个消息事件响应器，并且当消息纯文本部分包含关键词时响应。\n\n- **参数**\n  - `keywords` (set[str]): 关键词列表\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n## _def_ `on_command(cmd, rule=..., aliases=..., force_whitespace=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-command}\n\n- **说明**\n\n  注册一个消息事件响应器，并且当消息以指定命令开头时响应。\n\n  命令匹配规则参考: `命令形式匹配 <rule.md#command-command>`\\_\n\n- **参数**\n  - `cmd` (str | tuple[str, ...]): 指定命令内容\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `aliases` (set[str | tuple[str, ...]] | None): 命令别名\n\n  - `force_whitespace` (str | bool | None): 是否强制命令后必须有指定空白符\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n## _def_ `on_shell_command(cmd, rule=..., aliases=..., parser=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-shell-command}\n\n- **说明**\n\n  注册一个支持 `shell_like` 解析参数的命令消息事件响应器。\n\n  与普通的 `on_command` 不同的是，在添加 `parser` 参数时, 响应器会自动处理消息。\n\n  可以通过 [ShellCommandArgv](../params.md#ShellCommandArgv) 获取原始参数列表，\n  通过 [ShellCommandArgs](../params.md#ShellCommandArgs) 获取解析后的参数字典。\n\n- **参数**\n  - `cmd` (str | tuple[str, ...]): 指定命令内容\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `aliases` (set[str | tuple[str, ...]] | None): 命令别名\n\n  - `parser` ([ArgumentParser](../rule.md#ArgumentParser) | None): `nonebot.rule.ArgumentParser` 对象\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n## _def_ `on_regex(pattern, flags=..., rule=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-regex}\n\n- **说明**\n\n  注册一个消息事件响应器，并且当消息匹配正则表达式时响应。\n\n  命令匹配规则参考: `正则匹配 <rule.md#regex-regex-flags-0>`\\_\n\n- **参数**\n  - `pattern` (str): 正则表达式\n\n  - `flags` (int | re.RegexFlag): 正则匹配标志\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n## _def_ `on_type(types, rule=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-type}\n\n- **说明:** 注册一个事件响应器，并且当事件为指定类型时响应。\n\n- **参数**\n  - `types` (type[[Event](../adapters/index.md#Event)] | tuple[type[[Event](../adapters/index.md#Event)], ...]): 事件类型\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n## _class_ `CommandGroup(cmd, prefix_aliases=..., *, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#CommandGroup}\n\n- **参数**\n  - `cmd` (str | tuple[str, ...])\n\n  - `prefix_aliases` (bool)\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None)\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None)\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None)\n\n  - `temp` (bool)\n\n  - `expire_time` (datetime | timedelta | None)\n\n  - `priority` (int)\n\n  - `block` (bool)\n\n  - `state` ([T_State](../typing.md#T-State) | None)\n\n### _method_ `command(cmd, *, rule=..., aliases=..., force_whitespace=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#CommandGroup-command}\n\n- **说明:** 注册一个新的命令。新参数将会覆盖命令组默认值\n\n- **参数**\n  - `cmd` (str | tuple[str, ...]): 指定命令内容\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `aliases` (set[str | tuple[str, ...]] | None): 命令别名\n\n  - `force_whitespace` (str | bool | None): 是否强制命令后必须有指定空白符\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n### _method_ `shell_command(cmd, *, rule=..., aliases=..., parser=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#CommandGroup-shell-command}\n\n- **说明:** 注册一个新的 `shell_like` 命令。新参数将会覆盖命令组默认值\n\n- **参数**\n  - `cmd` (str | tuple[str, ...]): 指定命令内容\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `aliases` (set[str | tuple[str, ...]] | None): 命令别名\n\n  - `parser` ([ArgumentParser](../rule.md#ArgumentParser) | None): `nonebot.rule.ArgumentParser` 对象\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n## _class_ `MatcherGroup(*, type=..., rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup}\n\n- **参数**\n  - `type` (str)\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None)\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None)\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None)\n\n  - `temp` (bool)\n\n  - `expire_time` (datetime | timedelta | None)\n\n  - `priority` (int)\n\n  - `block` (bool)\n\n  - `state` ([T_State](../typing.md#T-State) | None)\n\n### _method_ `on(*, type=..., rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on}\n\n- **说明:** 注册一个基础事件响应器，可自定义类型。\n\n- **参数**\n  - `type` (str): 事件响应器类型\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n### _method_ `on_metaevent(*, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-metaevent}\n\n- **说明:** 注册一个元事件响应器。\n\n- **参数**\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n### _method_ `on_message(*, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-message}\n\n- **说明:** 注册一个消息事件响应器。\n\n- **参数**\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n### _method_ `on_notice(*, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-notice}\n\n- **说明:** 注册一个通知事件响应器。\n\n- **参数**\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n### _method_ `on_request(*, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-request}\n\n- **说明:** 注册一个请求事件响应器。\n\n- **参数**\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n### _method_ `on_startswith(msg, *, ignorecase=..., rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-startswith}\n\n- **说明:** 注册一个消息事件响应器，并且当消息的**文本部分**以指定内容开头时响应。\n\n- **参数**\n  - `msg` (str | tuple[str, ...]): 指定消息开头内容\n\n  - `ignorecase` (bool): 是否忽略大小写\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n### _method_ `on_endswith(msg, *, ignorecase=..., rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-endswith}\n\n- **说明:** 注册一个消息事件响应器，并且当消息的**文本部分**以指定内容结尾时响应。\n\n- **参数**\n  - `msg` (str | tuple[str, ...]): 指定消息结尾内容\n\n  - `ignorecase` (bool): 是否忽略大小写\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n### _method_ `on_fullmatch(msg, *, ignorecase=..., rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-fullmatch}\n\n- **说明:** 注册一个消息事件响应器，并且当消息的**文本部分**与指定内容完全一致时响应。\n\n- **参数**\n  - `msg` (str | tuple[str, ...]): 指定消息全匹配内容\n\n  - `ignorecase` (bool): 是否忽略大小写\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n### _method_ `on_keyword(keywords, *, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-keyword}\n\n- **说明:** 注册一个消息事件响应器，并且当消息纯文本部分包含关键词时响应。\n\n- **参数**\n  - `keywords` (set[str]): 关键词列表\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n### _method_ `on_command(cmd, aliases=..., force_whitespace=..., *, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-command}\n\n- **说明**\n\n  注册一个消息事件响应器，并且当消息以指定命令开头时响应。\n\n  命令匹配规则参考: `命令形式匹配 <rule.md#command-command>`\\_\n\n- **参数**\n  - `cmd` (str | tuple[str, ...]): 指定命令内容\n\n  - `aliases` (set[str | tuple[str, ...]] | None): 命令别名\n\n  - `force_whitespace` (str | bool | None): 是否强制命令后必须有指定空白符\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n### _method_ `on_shell_command(cmd, aliases=..., parser=..., *, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-shell-command}\n\n- **说明**\n\n  注册一个支持 `shell_like` 解析参数的命令消息事件响应器。\n\n  与普通的 `on_command` 不同的是，在添加 `parser` 参数时, 响应器会自动处理消息。\n\n  可以通过 [ShellCommandArgv](../params.md#ShellCommandArgv) 获取原始参数列表，\n  通过 [ShellCommandArgs](../params.md#ShellCommandArgs) 获取解析后的参数字典。\n\n- **参数**\n  - `cmd` (str | tuple[str, ...]): 指定命令内容\n\n  - `aliases` (set[str | tuple[str, ...]] | None): 命令别名\n\n  - `parser` ([ArgumentParser](../rule.md#ArgumentParser) | None): `nonebot.rule.ArgumentParser` 对象\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n### _method_ `on_regex(pattern, flags=..., *, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-regex}\n\n- **说明**\n\n  注册一个消息事件响应器，并且当消息匹配正则表达式时响应。\n\n  命令匹配规则参考: `正则匹配 <rule.md#regex-regex-flags-0>`\\_\n\n- **参数**\n  - `pattern` (str): 正则表达式\n\n  - `flags` (int | re.RegexFlag): 正则匹配标志\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n\n### _method_ `on_type(types, *, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-type}\n\n- **说明:** 注册一个事件响应器，并且当事件为指定类型时响应。\n\n- **参数**\n  - `types` (type[[Event](../adapters/index.md#Event)] | tuple[type[[Event](../adapters/index.md#Event)]]): 事件类型\n\n  - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则\n\n  - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限\n\n  - `handlers` (list[[T\\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表\n\n  - `temp` (bool): 是否为临时事件响应器（仅执行一次）\n\n  - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点，过时即被删除\n\n  - `priority` (int): 事件响应器优先级\n\n  - `block` (bool): 是否阻止事件向更低优先级传递\n\n  - `state` ([T_State](../typing.md#T-State) | None): 默认 state\n\n- **返回**\n  - type[[Matcher](../matcher.md#Matcher)]\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/api/rule.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 5\ndescription: nonebot.rule 模块\n---\n\n# nonebot.rule\n\n本模块是 [Matcher.rule](matcher.md#Matcher-rule) 的类型定义。\n\n每个[事件响应器](matcher.md#Matcher)拥有一个\n[Rule](#Rule)，其中是 `RuleChecker` 的集合。\n只有当所有 `RuleChecker` 检查结果为 `True` 时继续运行。\n\n## _class_ `Rule(*checkers)` {#Rule}\n\n- **说明**\n\n  规则类。\n\n  当事件传递时，在 [Matcher](matcher.md#Matcher) 运行前进行检查。\n\n- **参数**\n  - `*checkers` ([T_RuleChecker](typing.md#T-RuleChecker) | [Dependent](dependencies/index.md#Dependent)[bool]): RuleChecker\n\n- **用法**\n\n  ```python\n  Rule(async_function) & sync_function\n  # 等价于\n  Rule(async_function, sync_function)\n  ```\n\n### _instance-var_ `checkers` {#Rule-checkers}\n\n- **类型:** set[[Dependent](dependencies/index.md#Dependent)[bool]]\n\n- **说明:** 存储 `RuleChecker`\n\n### _async method_ `__call__(bot, event, state, stack=None, dependency_cache=None)` {#Rule---call--}\n\n- **说明:** 检查是否符合所有规则\n\n- **参数**\n  - `bot` ([Bot](adapters/index.md#Bot)): Bot 对象\n\n  - `event` ([Event](adapters/index.md#Event)): Event 对象\n\n  - `state` ([T_State](typing.md#T-State)): 当前 State\n\n  - `stack` (AsyncExitStack | None): 异步上下文栈\n\n  - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None): 依赖缓存\n\n- **返回**\n  - bool\n\n## _class_ `CMD_RESULT(<auto>)` {#CMD-RESULT}\n\n- **参数**\n\n  auto\n\n## _class_ `TRIE_VALUE(<auto>)` {#TRIE-VALUE}\n\n- **说明:** TRIE_VALUE(command_start, command)\n\n- **参数**\n\n  auto\n\n## _class_ `StartswithRule(msg, ignorecase=False)` {#StartswithRule}\n\n- **说明:** 检查消息纯文本是否以指定字符串开头。\n\n- **参数**\n  - `msg` (tuple[str, ...]): 指定消息开头字符串元组\n\n  - `ignorecase` (bool): 是否忽略大小写\n\n## _def_ `startswith(msg, ignorecase=False)` {#startswith}\n\n- **说明:** 匹配消息纯文本开头。\n\n- **参数**\n  - `msg` (str | tuple[str, ...]): 指定消息开头字符串元组\n\n  - `ignorecase` (bool): 是否忽略大小写\n\n- **返回**\n  - [Rule](#Rule)\n\n## _class_ `EndswithRule(msg, ignorecase=False)` {#EndswithRule}\n\n- **说明:** 检查消息纯文本是否以指定字符串结尾。\n\n- **参数**\n  - `msg` (tuple[str, ...]): 指定消息结尾字符串元组\n\n  - `ignorecase` (bool): 是否忽略大小写\n\n## _def_ `endswith(msg, ignorecase=False)` {#endswith}\n\n- **说明:** 匹配消息纯文本结尾。\n\n- **参数**\n  - `msg` (str | tuple[str, ...]): 指定消息开头字符串元组\n\n  - `ignorecase` (bool): 是否忽略大小写\n\n- **返回**\n  - [Rule](#Rule)\n\n## _class_ `FullmatchRule(msg, ignorecase=False)` {#FullmatchRule}\n\n- **说明:** 检查消息纯文本是否与指定字符串全匹配。\n\n- **参数**\n  - `msg` (tuple[str, ...]): 指定消息全匹配字符串元组\n\n  - `ignorecase` (bool): 是否忽略大小写\n\n## _def_ `fullmatch(msg, ignorecase=False)` {#fullmatch}\n\n- **说明:** 完全匹配消息。\n\n- **参数**\n  - `msg` (str | tuple[str, ...]): 指定消息全匹配字符串元组\n\n  - `ignorecase` (bool): 是否忽略大小写\n\n- **返回**\n  - [Rule](#Rule)\n\n## _class_ `KeywordsRule(*keywords)` {#KeywordsRule}\n\n- **说明:** 检查消息纯文本是否包含指定关键字。\n\n- **参数**\n  - `*keywords` (str): 指定关键字元组\n\n## _def_ `keyword(*keywords)` {#keyword}\n\n- **说明:** 匹配消息纯文本关键词。\n\n- **参数**\n  - `*keywords` (str): 指定关键字元组\n\n- **返回**\n  - [Rule](#Rule)\n\n## _class_ `CommandRule(cmds, force_whitespace=None)` {#CommandRule}\n\n- **说明:** 检查消息是否为指定命令。\n\n- **参数**\n  - `cmds` (list[tuple[str, ...]]): 指定命令元组列表\n\n  - `force_whitespace` (str | bool | None): 是否强制命令后必须有指定空白符\n\n## _def_ `command(*cmds, force_whitespace=None)` {#command}\n\n- **说明**\n\n  匹配消息命令。\n\n  根据配置里提供的 [`command_start`](config.md#Config-command-start),\n  [`command_sep`](config.md#Config-command-sep) 判断消息是否为命令。\n\n  可以通过 [Command](params.md#Command) 获取匹配成功的命令（例: `(\"test\",)`），\n  通过 [RawCommand](params.md#RawCommand) 获取匹配成功的原始命令文本（例: `\"/test\"`），\n  通过 [CommandArg](params.md#CommandArg) 获取匹配成功的命令参数。\n\n- **参数**\n  - `*cmds` (str | tuple[str, ...]): 命令文本或命令元组\n\n  - `force_whitespace` (str | bool | None): 是否强制命令后必须有指定空白符\n\n- **返回**\n  - [Rule](#Rule)\n\n- **用法**\n\n  使用默认 `command_start`, `command_sep` 配置情况下：\n\n  命令 `(\"test\",)` 可以匹配: `/test` 开头的消息\n  命令 `(\"test\", \"sub\")` 可以匹配: `/test.sub` 开头的消息\n\n:::tip 提示\n命令内容与后续消息间无需空格!\n:::\n\n## _class_ `ArgumentParser(<auto>)` {#ArgumentParser}\n\n- **说明**\n\n  `shell_like` 命令参数解析器，解析出错时不会退出程序。\n\n  支持 [Message](adapters/index.md#Message) 富文本解析。\n\n- **参数**\n\n  auto\n\n- **用法**\n\n  用法与 `argparse.ArgumentParser` 相同，\n  参考文档: [argparse](https://docs.python.org/3/library/argparse.html)\n\n### _method_ `parse_known_args(args=None, namespace=None)` {#ArgumentParser-parse-known-args}\n\n- **重载**\n\n  **1.** `(args=None, namespace=None) -> tuple[Namespace, list[str | MessageSegment]]`\n  - **参数**\n    - `args` (Sequence[str | [MessageSegment](adapters/index.md#MessageSegment)] | None)\n\n    - `namespace` (None)\n\n  - **返回**\n    - tuple[Namespace, list[str | [MessageSegment](adapters/index.md#MessageSegment)]]\n\n  **2.** `(args, namespace) -> tuple[T, list[str | MessageSegment]]`\n  - **参数**\n    - `args` (Sequence[str | [MessageSegment](adapters/index.md#MessageSegment)] | None)\n\n    - `namespace` (T)\n\n  - **返回**\n    - tuple[T, list[str | [MessageSegment](adapters/index.md#MessageSegment)]]\n\n  **3.** `(*, namespace) -> tuple[T, list[str | MessageSegment]]`\n  - **参数**\n    - `namespace` (T)\n\n  - **返回**\n    - tuple[T, list[str | [MessageSegment](adapters/index.md#MessageSegment)]]\n\n## _class_ `ShellCommandRule(cmds, parser)` {#ShellCommandRule}\n\n- **说明:** 检查消息是否为指定 shell 命令。\n\n- **参数**\n  - `cmds` (list[tuple[str, ...]]): 指定命令元组列表\n\n  - `parser` (ArgumentParser | None): 可选参数解析器\n\n## _def_ `shell_command(*cmds, parser=None)` {#shell-command}\n\n- **说明**\n\n  匹配 `shell_like` 形式的消息命令。\n\n  根据配置里提供的 [`command_start`](config.md#Config-command-start),\n  [`command_sep`](config.md#Config-command-sep) 判断消息是否为命令。\n\n  可以通过 [Command](params.md#Command) 获取匹配成功的命令\n  （例: `(\"test\",)`），\n  通过 [RawCommand](params.md#RawCommand) 获取匹配成功的原始命令文本\n  （例: `\"/test\"`），\n  通过 [ShellCommandArgv](params.md#ShellCommandArgv) 获取解析前的参数列表\n  （例: `[\"arg\", \"-h\"]`），\n  通过 [ShellCommandArgs](params.md#ShellCommandArgs) 获取解析后的参数字典\n  （例: `{\"arg\": \"arg\", \"h\": True}`）。\n\n  :::caution 警告\n  如果参数解析失败，则通过 [ShellCommandArgs](params.md#ShellCommandArgs)\n  获取的将是 [ParserExit](exception.md#ParserExit) 异常。\n  :::\n\n- **参数**\n  - `*cmds` (str | tuple[str, ...]): 命令文本或命令元组\n\n  - `parser` (ArgumentParser | None): [ArgumentParser](#ArgumentParser) 对象\n\n- **返回**\n  - [Rule](#Rule)\n\n- **用法**\n\n  使用默认 `command_start`, `command_sep` 配置，更多示例参考\n  [argparse](https://docs.python.org/3/library/argparse.html) 标准库文档。\n\n  ```python\n  from nonebot.rule import ArgumentParser\n\n  parser = ArgumentParser()\n  parser.add_argument(\"-a\", action=\"store_true\")\n\n  rule = shell_command(\"ls\", parser=parser)\n  ```\n\n:::tip 提示\n命令内容与后续消息间无需空格!\n:::\n\n## _class_ `RegexRule(regex, flags=0)` {#RegexRule}\n\n- **说明:** 检查消息字符串是否符合指定正则表达式。\n\n- **参数**\n  - `regex` (str): 正则表达式\n\n  - `flags` (int): 正则表达式标记\n\n## _def_ `regex(regex, flags=0)` {#regex}\n\n- **说明**\n\n  匹配符合正则表达式的消息字符串。\n\n  可以通过 [RegexStr](params.md#RegexStr) 获取匹配成功的字符串，\n  通过 [RegexGroup](params.md#RegexGroup) 获取匹配成功的 group 元组，\n  通过 [RegexDict](params.md#RegexDict) 获取匹配成功的 group 字典。\n\n- **参数**\n  - `regex` (str): 正则表达式\n\n  - `flags` (int | re.RegexFlag): 正则表达式标记\n\n- **返回**\n  - [Rule](#Rule)\n\n:::tip 提示\n正则表达式匹配使用 search 而非 match，如需从头匹配请使用 `r\"^xxx\"` 来确保匹配开头\n:::\n:::tip 提示\n正则表达式匹配使用 `EventMessage` 的 `str` 字符串，\n而非 `EventMessage` 的 `PlainText` 纯文本字符串\n:::\n\n## _class_ `ToMeRule(<auto>)` {#ToMeRule}\n\n- **说明:** 检查事件是否与机器人有关。\n\n- **参数**\n\n  auto\n\n## _def_ `to_me()` {#to-me}\n\n- **说明:** 匹配与机器人有关的事件。\n\n- **参数**\n\n  empty\n\n- **返回**\n  - [Rule](#Rule)\n\n## _class_ `IsTypeRule(*types)` {#IsTypeRule}\n\n- **说明:** 检查事件类型是否为指定类型。\n\n- **参数**\n  - `*types` (type[[Event](adapters/index.md#Event)])\n\n## _def_ `is_type(*types)` {#is-type}\n\n- **说明:** 匹配事件类型。\n\n- **参数**\n  - `*types` (type[[Event](adapters/index.md#Event)]): 事件类型\n\n- **返回**\n  - [Rule](#Rule)\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/api/typing.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 11\ndescription: nonebot.typing 模块\n---\n\n# nonebot.typing\n\n本模块定义了 NoneBot 模块中共享的一些类型。\n\n使用 Python 的 Type Hint 语法，\n参考 [`PEP 484`](https://www.python.org/dev/peps/pep-0484/),\n[`PEP 526`](https://www.python.org/dev/peps/pep-0526/) 和\n[`typing`](https://docs.python.org/3/library/typing.html)。\n\n## _def_ `overrides(InterfaceClass)` {#overrides}\n\n- **说明:** 标记一个方法为父类 interface 的 implement\n\n- **参数**\n  - `InterfaceClass` (object)\n\n- **返回**\n  - untyped\n\n## _def_ `type_has_args(type_)` {#type-has-args}\n\n- **参数**\n  - `type_` (type[Any])\n\n- **返回**\n  - bool\n\n## _def_ `origin_is_union(origin)` {#origin-is-union}\n\n- **参数**\n  - `origin` (type[Any] | None)\n\n- **返回**\n  - bool\n\n## _def_ `origin_is_literal(origin)` {#origin-is-literal}\n\n- **说明:** 判断是否是 Literal 类型\n\n- **参数**\n  - `origin` (type[Any] | None)\n\n- **返回**\n  - bool\n\n## _def_ `all_literal_values(type_)` {#all-literal-values}\n\n- **说明:** 获取 Literal 类型包含的所有值\n\n- **参数**\n  - `type_` (type[Any])\n\n- **返回**\n  - list[Any]\n\n## _def_ `origin_is_annotated(origin)` {#origin-is-annotated}\n\n- **说明:** 判断是否是 Annotated 类型\n\n- **参数**\n  - `origin` (type[Any] | None)\n\n- **返回**\n  - bool\n\n## _def_ `is_none_type(type_)` {#is-none-type}\n\n- **说明:** 判断是否是 None 类型\n\n- **参数**\n  - `type_` (type[Any])\n\n- **返回**\n  - bool\n\n## _def_ `is_type_alias_type(type_)` {#is-type-alias-type}\n\n- **参数**\n  - `type_` (type[Any])\n\n- **返回**\n  - bool\n\n## _def_ `evaluate_forwardref(ref, globalns, localns)` {#evaluate-forwardref}\n\n- **参数**\n  - `ref` (ForwardRef)\n\n  - `globalns` (dict[str, Any])\n\n  - `localns` (dict[str, Any])\n\n- **返回**\n  - Any\n\n## _class_ `StateFlag(<auto>)` {#StateFlag}\n\n- **参数**\n\n  auto\n\n## _var_ `T_State` {#T-State}\n\n- **类型:** dict[Any, Any]\n\n- **说明:** 事件处理状态 State 类型\n\n## _var_ `T_BotConnectionHook` {#T-BotConnectionHook}\n\n- **类型:** \\_DependentCallable[Any]\n\n- **说明**\n\n  Bot 连接建立时钩子函数\n\n  依赖参数:\n  - DependParam: 子依赖参数\n  - BotParam: Bot 对象\n  - DefaultParam: 带有默认值的参数\n\n## _var_ `T_BotDisconnectionHook` {#T-BotDisconnectionHook}\n\n- **类型:** \\_DependentCallable[Any]\n\n- **说明**\n\n  Bot 连接断开时钩子函数\n\n  依赖参数:\n  - DependParam: 子依赖参数\n  - BotParam: Bot 对象\n  - DefaultParam: 带有默认值的参数\n\n## _var_ `T_CallingAPIHook` {#T-CallingAPIHook}\n\n- **类型:** ([Bot](adapters/index.md#Bot), str, dict[str, Any]) -> Awaitable[Any]\n\n- **说明:** `bot.call_api` 钩子函数\n\n## _var_ `T_CalledAPIHook` {#T-CalledAPIHook}\n\n- **类型:** ([Bot](adapters/index.md#Bot), Exception | None, str, dict[str, Any], Any) -> Awaitable[Any]\n\n- **说明:** `bot.call_api` 后执行的函数，参数分别为 bot, exception, api, data, result\n\n## _var_ `T_EventPreProcessor` {#T-EventPreProcessor}\n\n- **类型:** \\_DependentCallable[Any]\n\n- **说明**\n\n  事件预处理函数 EventPreProcessor 类型\n\n  依赖参数:\n  - DependParam: 子依赖参数\n  - BotParam: Bot 对象\n  - EventParam: Event 对象\n  - StateParam: State 对象\n  - DefaultParam: 带有默认值的参数\n\n## _var_ `T_EventPostProcessor` {#T-EventPostProcessor}\n\n- **类型:** \\_DependentCallable[Any]\n\n- **说明**\n\n  事件后处理函数 EventPostProcessor 类型\n\n  依赖参数:\n  - DependParam: 子依赖参数\n  - BotParam: Bot 对象\n  - EventParam: Event 对象\n  - StateParam: State 对象\n  - DefaultParam: 带有默认值的参数\n\n## _var_ `T_RunPreProcessor` {#T-RunPreProcessor}\n\n- **类型:** \\_DependentCallable[Any]\n\n- **说明**\n\n  事件响应器运行前预处理函数 RunPreProcessor 类型\n\n  依赖参数:\n  - DependParam: 子依赖参数\n  - BotParam: Bot 对象\n  - EventParam: Event 对象\n  - StateParam: State 对象\n  - MatcherParam: Matcher 对象\n  - DefaultParam: 带有默认值的参数\n\n## _var_ `T_RunPostProcessor` {#T-RunPostProcessor}\n\n- **类型:** \\_DependentCallable[Any]\n\n- **说明**\n\n  事件响应器运行后后处理函数 RunPostProcessor 类型\n\n  依赖参数:\n  - DependParam: 子依赖参数\n  - BotParam: Bot 对象\n  - EventParam: Event 对象\n  - StateParam: State 对象\n  - MatcherParam: Matcher 对象\n  - ExceptionParam: 异常对象（可能为 None）\n  - DefaultParam: 带有默认值的参数\n\n## _var_ `T_RuleChecker` {#T-RuleChecker}\n\n- **类型:** \\_DependentCallable[bool]\n\n- **说明**\n\n  RuleChecker 即判断是否响应事件的处理函数。\n\n  依赖参数:\n  - DependParam: 子依赖参数\n  - BotParam: Bot 对象\n  - EventParam: Event 对象\n  - StateParam: State 对象\n  - DefaultParam: 带有默认值的参数\n\n## _var_ `T_PermissionChecker` {#T-PermissionChecker}\n\n- **类型:** \\_DependentCallable[bool]\n\n- **说明**\n\n  PermissionChecker 即判断事件是否满足权限的处理函数。\n\n  依赖参数:\n  - DependParam: 子依赖参数\n  - BotParam: Bot 对象\n  - EventParam: Event 对象\n  - DefaultParam: 带有默认值的参数\n\n## _var_ `T_Handler` {#T-Handler}\n\n- **类型:** \\_DependentCallable[Any]\n\n- **说明:** Handler 处理函数。\n\n## _var_ `T_TypeUpdater` {#T-TypeUpdater}\n\n- **类型:** \\_DependentCallable[str]\n\n- **说明**\n\n  TypeUpdater 在 Matcher.pause, Matcher.reject 时被运行，用于更新响应的事件类型。 默认会更新为 `message`。\n\n  依赖参数:\n  - DependParam: 子依赖参数\n  - BotParam: Bot 对象\n  - EventParam: Event 对象\n  - StateParam: State 对象\n  - MatcherParam: Matcher 对象\n  - DefaultParam: 带有默认值的参数\n\n## _var_ `T_PermissionUpdater` {#T-PermissionUpdater}\n\n- **类型:** \\_DependentCallable[[Permission](permission.md#Permission)]\n\n- **说明**\n\n  PermissionUpdater 在 Matcher.pause, Matcher.reject 时被运行，用于更新会话对象权限。 默认会更新为当前事件的触发对象。\n\n  依赖参数:\n  - DependParam: 子依赖参数\n  - BotParam: Bot 对象\n  - EventParam: Event 对象\n  - StateParam: State 对象\n  - MatcherParam: Matcher 对象\n  - DefaultParam: 带有默认值的参数\n\n## _var_ `T_DependencyCache` {#T-DependencyCache}\n\n- **类型:** dict[\\_DependentCallable[Any], DependencyCache]\n\n- **说明:** 依赖缓存, 用于存储依赖函数的返回值\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/api/utils.md",
    "content": "---\nmdx:\n  format: md\nsidebar_position: 8\ndescription: nonebot.utils 模块\n---\n\n# nonebot.utils\n\n本模块包含了 NoneBot 的一些工具函数\n\n## _def_ `escape_tag(s)` {#escape-tag}\n\n- **说明**\n\n  用于记录带颜色日志时转义 `<tag>` 类型特殊标签\n\n  参考: [loguru color 标签](https://loguru.readthedocs.io/en/stable/api/logger.html#color)\n\n- **参数**\n  - `s` (str): 需要转义的字符串\n\n- **返回**\n  - str\n\n## _def_ `deep_update(mapping, *updating_mappings)` {#deep-update}\n\n- **说明:** 深度更新合并字典\n\n- **参数**\n  - `mapping` (dict[K, Any])\n\n  - `*updating_mappings` (dict[K, Any])\n\n- **返回**\n  - dict[K, Any]\n\n## _def_ `lenient_issubclass(cls, class_or_tuple)` {#lenient-issubclass}\n\n- **说明:** 检查 cls 是否是 class_or_tuple 中的一个类型子类并忽略类型错误。\n\n- **参数**\n  - `cls` (Any)\n\n  - `class_or_tuple` (type[Any] | tuple[type[Any], ...])\n\n- **返回**\n  - bool\n\n## _def_ `generic_check_issubclass(cls, class_or_tuple)` {#generic-check-issubclass}\n\n- **说明**\n\n  检查 cls 是否是 class_or_tuple 中的一个类型子类。\n\n  特别的：\n  - 如果 cls 是 `typing.TypeVar` 类型，\n    则会检查其 `__bound__` 或 `__constraints__`\n    是否是 class_or_tuple 中一个类型的子类或 None。\n  - 如果 cls 是 `typing.Union` 或 `types.UnionType` 类型，\n    则会检查其中的所有类型是否是 class_or_tuple 中一个类型的子类或 None。\n  - 如果 cls 是 `typing.Literal` 类型，\n    则会检查其中的所有值是否是 class_or_tuple 中一个类型的实例。\n  - 如果 cls 是 `typing.List`、`typing.Dict` 等泛型类型，\n    则会检查其原始类型是否是 class_or_tuple 中一个类型的子类。\n\n- **参数**\n  - `cls` (Any)\n\n  - `class_or_tuple` (type[Any] | tuple[type[Any], ...])\n\n- **返回**\n  - bool\n\n## _def_ `type_is_complex(type_)` {#type-is-complex}\n\n- **说明:** 检查 type\\_ 是否是复杂类型\n\n- **参数**\n  - `type_` (type[Any])\n\n- **返回**\n  - bool\n\n## _def_ `is_coroutine_callable(call)` {#is-coroutine-callable}\n\n- **说明:** 检查 call 是否是一个 callable 协程函数\n\n- **参数**\n  - `call` ((...) -> Any)\n\n- **返回**\n  - bool\n\n## _def_ `is_gen_callable(call)` {#is-gen-callable}\n\n- **说明:** 检查 call 是否是一个生成器函数\n\n- **参数**\n  - `call` ((...) -> Any)\n\n- **返回**\n  - bool\n\n## _def_ `is_async_gen_callable(call)` {#is-async-gen-callable}\n\n- **说明:** 检查 call 是否是一个异步生成器函数\n\n- **参数**\n  - `call` ((...) -> Any)\n\n- **返回**\n  - bool\n\n## _def_ `run_sync(call)` {#run-sync}\n\n- **说明:** 一个用于包装 sync function 为 async function 的装饰器\n\n- **参数**\n  - `call` ((P) -> R): 被装饰的同步函数\n\n- **返回**\n  - (P) -> Coroutine[None, None, R]\n\n## _def_ `run_sync_ctx_manager(cm)` {#run-sync-ctx-manager}\n\n- **说明:** 一个用于包装 sync context manager 为 async context manager 的执行函数\n\n- **参数**\n  - `cm` (AbstractContextManager[T])\n\n- **返回**\n  - AsyncGenerator[T, None]\n\n## _async def_ `run_coro_with_catch(coro, exc, return_on_err=None)` {#run-coro-with-catch}\n\n- **说明:** 运行协程并当遇到指定异常时返回指定值。\n\n- **重载**\n\n  **1.** `(coro, exc, return_on_err=None) -> T | None`\n  - **参数**\n    - `coro` (Coroutine[Any, Any, T])\n\n    - `exc` (tuple[type[Exception], ...])\n\n    - `return_on_err` (None)\n\n  - **返回**\n    - T | None\n\n  **2.** `(coro, exc, return_on_err) -> T | R`\n  - **参数**\n    - `coro` (Coroutine[Any, Any, T])\n\n    - `exc` (tuple[type[Exception], ...])\n\n    - `return_on_err` (R)\n\n  - **返回**\n    - T | R\n\n- **参数**\n  - `coro`: 要运行的协程\n\n  - `exc`: 要捕获的异常\n\n  - `return_on_err`: 当发生异常时返回的值\n\n- **返回**\n\n  协程的返回值或发生异常时的指定值\n\n## _async def_ `run_coro_with_shield(coro)` {#run-coro-with-shield}\n\n- **说明:** 运行协程并在取消时屏蔽取消异常。\n\n- **参数**\n  - `coro` (Coroutine[Any, Any, T]): 要运行的协程\n\n- **返回**\n  - T: 协程的返回值\n\n## _def_ `flatten_exception_group(exc_group)` {#flatten-exception-group}\n\n- **参数**\n  - `exc_group` (BaseExceptionGroup[E])\n\n- **返回**\n  - Generator[E, None, None]\n\n## _def_ `get_name(obj)` {#get-name}\n\n- **说明:** 获取对象的名称\n\n- **参数**\n  - `obj` (Any)\n\n- **返回**\n  - str\n\n## _def_ `path_to_module_name(path)` {#path-to-module-name}\n\n- **说明:** 转换路径为模块名\n\n- **参数**\n  - `path` (Path)\n\n- **返回**\n  - str\n\n## _def_ `resolve_dot_notation(obj_str, default_attr, default_prefix=None)` {#resolve-dot-notation}\n\n- **说明:** 解析并导入点分表示法的对象\n\n- **参数**\n  - `obj_str` (str)\n\n  - `default_attr` (str)\n\n  - `default_prefix` (str | None)\n\n- **返回**\n  - Any\n\n## _class_ `classproperty(func)` {#classproperty}\n\n- **说明:** 类属性装饰器\n\n- **参数**\n  - `func` ((Any) -> T)\n\n## _class_ `DataclassEncoder(<auto>)` {#DataclassEncoder}\n\n- **说明:** 可以序列化 [Message](adapters/index.md#Message)(List[Dataclass]) 的 `JSONEncoder`\n\n- **参数**\n\n  auto\n\n### _method_ `default(o)` {#DataclassEncoder-default}\n\n- **参数**\n  - `o`\n\n- **返回**\n  - untyped\n\n## _def_ `logger_wrapper(logger_name)` {#logger-wrapper}\n\n- **说明:** 用于打印 adapter 的日志。\n\n- **参数**\n  - `logger_name` (str): adapter 的名称\n\n- **返回**\n  - untyped: 日志记录函数\n\n    日志记录函数的参数:\n    - level: 日志等级\n    - message: 日志信息\n    - exception: 异常信息\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/appendices/api-calling.mdx",
    "content": "---\nsidebar_position: 4\ndescription: 使用平台接口，完成更多功能\n\noptions:\n  menu:\n    - category: appendices\n      weight: 50\n---\n\n# 使用平台接口\n\nimport Messenger from \"@/components/Messenger\";\n\n在 NoneBot 中，除了使用事件响应器操作发送文本消息外，我们还可以直接通过使用协议适配器提供的方法来使用平台特定的接口，完成发送特殊消息、获取信息等其他平台提供的功能。同时，在部分无法使用事件响应器的情况中，例如[定时任务](../best-practice/scheduler.md)，我们也可以使用平台接口来完成需要的功能。\n\n## 发送平台特殊消息\n\n在之前的章节中，我们介绍了如何向用户发送文本消息以及[如何处理平台消息](../tutorial/message.md)，现在我们来向用户发送平台特殊消息。\n\n:::caution 注意\n在以下的示例中，我们将使用 `Console` 协议适配器来演示如何发送平台消息。在实际使用中，你需要确保你使用的**消息序列类型**与你所要发送的**平台类型**一致。\n:::\n\n```python {4,7-17} title=weather/__init__.py\nimport inspect\nfrom nonebot.adapters.console import MessageSegment\n\n@weather.got(\"location\", prompt=MessageSegment.emoji(\"question\") + \"请输入地名\")\nasync def got_location(location: str = ArgPlainText()):\n    result = await weather.send(\n        MessageSegment.markdown(\n            inspect.cleandoc(\n                f\"\"\"\n                # {location}\n\n                - 今天\n\n                   ⛅ 多云 20℃~24℃\n                \"\"\"\n            )\n        )\n    )\n```\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/天气\" },\n    { position: \"left\", msg: \"❓请输入地名\" },\n    { position: \"right\", msg: \"北京\" },\n    {\n      position: \"left\",\n      monospace: true,\n      msg: \"┏━━━━━━━━━━━━━━━━┓\\n┃      北京       ┃\\n┗━━━━━━━━━━━━━━━━┛\\n• 今天\\n⛅ 多云 20℃~24℃\",\n    },\n  ]}\n/>\n\n在上面的示例中，我们使用了 `Console` 协议适配器提供的 `MessageSegment` 类来发送平台特定的消息 `emoji` 和 `markdown`。这两种消息可以显示在终端中，但是无法在其他平台上使用。在事件响应器操作中，我们可以使用 `str`、消息序列、消息段、消息模板四种类型来发送消息，但其中只有 `str` 和[纯文本形式的消息模板类型](../tutorial/message.md#使用消息模板)消息可以在所有平台上使用。\n\n`send` 事件响应器操作实际上是由协议适配器通过调用平台 API 来实现的，通常会将 API 调用的结果作为返回值返回。\n\n## 调用平台 API\n\n在 NoneBot 中，我们可以通过 `Bot` 对象来调用协议适配器支持的平台 API，来完成更多的功能。\n\n### 获取 Bot\n\n在调用平台 API 之前，我们首先要获得 Bot 对象。有两种方式可以获得 Bot 对象。\n\n在事件处理流程的上下文中，我们可以直接使用依赖注入 Bot 来获取：\n\n```python {1,4} title=weather/__init__.py\nfrom nonebot.adapters import Bot\n\n@weather.got(\"location\", prompt=\"请输入地名\")\nasync def got_location(bot: Bot, location: str = ArgPlainText()):\n    ...\n```\n\n依赖注入会确保你获得的 Bot 对象与类型注解的 Bot 类型一致。也就是说，如果你使用的是 Bot 基类，将会允许任何平台的 Bot 对象；如果你使用的是平台特定的 Bot 类型，将会只允许该平台的 Bot 对象，其他类型的 Bot 将会跳过这个事件处理函数。更多详情请参考[事件处理重载](./overload.md)。\n\n在其他情况下，我们可以通过 NoneBot 提供的方法来获取 Bot 对象，这些方法将会在[使用适配器](../advanced/adapter.md#获取-bot-对象)中详细介绍：\n\n```python {4,6}\nfrom nonebot import get_bot\n\n# 获取当前所有 Bot 中的第一个\nbot = get_bot()\n# 获取指定 ID 的 Bot\nbot = get_bot(\"bot_id\")\n```\n\n### 调用 API\n\n在获得 Bot 对象后，我们可以通过 Bot 的实例方法来调用平台 API：\n\n```python {2,5}\n# 通过 bot.api_name(**kwargs) 的方法调用 API\nresult = await bot.get_user_info(user_id=12345678)\n\n# 通过 bot.call_api(api_name, **kwargs) 的方法调用 API\nresult = await bot.call_api(\"get_user_info\", user_id=12345678)\n```\n\n:::caution 注意\n实际可以使用的 API 以及参数取决于平台提供的接口以及协议适配器的实现，请参考协议适配器以及平台文档。\n:::\n\n在了解了如何调用 API 后，我们可以来改进 `weather` 插件，使得消息发送后，调用 `Console` 接口响铃提醒机器人用户：\n\n```python {4,18} title=weather/__init__.py\nfrom nonebot.adapters.console import Bot, MessageSegment\n\n@weather.got(\"location\", prompt=MessageSegment.emoji(\"question\") + \"请输入地名\")\nasync def got_location(bot: Bot, location: str = ArgPlainText()):\n    await weather.send(\n        MessageSegment.markdown(\n            inspect.cleandoc(\n                f\"\"\"\n                # {location}\n\n                - 今天\n\n                   ⛅ 多云 20℃~24℃\n                \"\"\"\n            )\n        )\n    )\n    await bot.bell()\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/appendices/config.mdx",
    "content": "---\nsidebar_position: 0\ndescription: 读取用户配置来控制插件行为\n\noptions:\n  menu:\n    - category: appendices\n      weight: 10\n---\n\n# 配置\n\nimport Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\n\n配置是项目中非常重要的一部分，为了方便我们控制机器人的行为，NoneBot 提供了一套配置系统。下面我们将会补充[指南](../quick-start.mdx)中的天气插件，使其能够读取用户配置。在这之前，我们需要先了解一下配置系统，如果你已经了解了 NoneBot 中的配置方法，可以跳转到[编写插件配置](#插件配置)。\n\nNoneBot 使用 [`pydantic`](https://docs.pydantic.dev/) 以及 [`python-dotenv`](https://saurabh-kumar.com/python-dotenv/) 来读取 dotenv 配置文件以及环境变量，从而控制机器人行为。配置文件需要符合 dotenv 格式，复杂数据类型需使用 JSON 格式或 [pydantic 支持格式](https://docs.pydantic.dev/usage/types/)填写。\n\nNoneBot 内置的配置项列表及含义可以在[内置配置项](#内置配置项)中查看。\n\n:::caution 注意\n\nNoneBot 自 2.2.0 起兼容了 Pydantic v1 与 v2 版本，以下文档中 Pydantic 相关示例均采用 v2 版本用法。\n\n如果在使用商店或其他第三方插件的过程中遇到 Pydantic 相关警告或报错，例如：\n\n```python\npydantic_core._pydantic_core.ValidationError: 1 validation error for Config\n  Input should be a valid dictionary or instance of Config [type=model_type, input_value=Config(...), input_type=Config]\n```\n\n请考虑降级 Pydantic 至 v1 版本：\n\n```bash\npip install --force-reinstall 'pydantic~=1.10'\n```\n\n:::\n\n## 配置项的加载\n\n在 NoneBot 中，我们可以把配置途径分为 **直接传入**、**系统环境变量**、**dotenv 配置文件** 三种，其加载优先级依次由高到低。\n\n### 直接传入\n\n在 NoneBot 初始化的过程中，可以通过 `nonebot.init()` 传入任意合法的 Python 变量，也可以在初始化完成后直接赋值。\n\n通常，在初始化前的传参会在机器人的入口文件（如 `bot.py`）中进行，而初始化后的赋值可以在任何地方进行。\n\n```python {4,8,9} title=bot.py\nimport nonebot\n\n# 初始化时\nnonebot.init(custom_config1=\"config on init\")\n\n# 初始化后\nconfig = nonebot.get_driver().config\nconfig.custom_config1 = \"changed after init\"\nconfig.custom_config2 = \"new config after init\"\n```\n\n### 系统环境变量\n\n在 dotenv 配置文件中定义的配置项，也会在环境变量中进行寻找。如果在环境变量中发现同名配置项（大小写不敏感），将会覆盖 dotenv 中所填值。\n\n例如，在 dotenv 配置文件中存在配置项 `custom_config`：\n\n```dotenv\nCUSTOM_CONFIG=config in dotenv\n```\n\n同时，设置环境变量：\n\n```bash\n# windows cmd\nset CUSTOM_CONFIG 'config in environment variables'\n# windows powershell\n$Env:CUSTOM_CONFIG='config in environment variables'\n# linux/macOS\nexport CUSTOM_CONFIG='config in environment variables'\n```\n\n那最终 NoneBot 所读取的内容为环境变量中的内容，即 `config in environment variables`。\n\n:::caution 注意\n如果一个环境变量既不是 NoneBot 的[**内置配置项**](#内置配置项)，也不是任何插件所定义的[**插件配置**](#插件配置)，那么 NoneBot 不会自发读取该环境变量，需要在 dotenv 配置文件中先行声明。\n:::\n\n### dotenv 配置文件\n\ndotenv 是一种便捷的跨平台配置通用模式，也是我们推荐的配置方式。\n\nNoneBot 在启动时将会从系统环境变量或者 `.env` 文件中寻找配置项 `ENVIRONMENT` （大小写不敏感），默认值为 `prod`。这将决定 NoneBot 后续进一步加载环境配置的文件路径 `.env.{ENVIRONMENT}`。\n\n#### 配置项解析\n\ndotenv 文件中的配置值使用 JSON 进行解析。如果配置项值无法被解析，将作为**字符串**处理。例如：\n\n```dotenv\nSTRING_CONFIG=some string\nLIST_CONFIG=[1, 2, 3]\nDICT_CONFIG={\"key\": \"value\"}\nMULTILINE_CONFIG='\n[\n  {\n    \"item_key\": \"item_value\"\n  }\n]\n'\nEMPTY_CONFIG=\nNULL_CONFIG\n```\n\n将被解析为：\n\n```python\ndotenv_config = {\n    \"string_config\": \"some string\",\n    \"list_config\": [1, 2, 3],\n    \"dict_config\": {\"key\": \"value\"},\n    \"multiline_config\": [{\"item_key\": \"item_value\"}],\n    \"empty_config\": \"\",\n    \"null_config\": None\n}\n```\n\n特别的，NoneBot 支持使用 `env_nested_delimiter` 配置嵌套字典，在层与层之间使用 `__` 分隔即可：\n\n```dotenv\nDICT={\"k1\": \"v1\", \"k2\": null}\nDICT__K2=v2\nDICT__K3=v3\nDICT__INNER__K4=v4\n```\n\n将被解析为：\n\n```python\ndotenv_config = {\n    \"dict\": {\n        \"k1\": \"v1\",\n        \"k2\": \"v2\",\n        \"k3\": \"v3\",\n        \"inner\": {\n            \"k4\": \"v4\"\n        }\n    }\n}\n```\n\n#### .env 文件\n\n`.env` 文件是基础配置文件，该文件中的配置项在不同环境下都会被加载，但会被 `.env.{ENVIRONMENT}` 文件中的配置所**覆盖**。\n\n我们可以在 `.env` 文件中写入当前的环境信息：\n\n```dotenv\nENVIRONMENT=dev\nCOMMON_CONFIG=common config  # 这个配置项在任何环境中都会被加载\n```\n\n这样，我们在启动 NoneBot 时就会从 `.env.dev` 文件中加载剩余配置项。\n\n:::tip 提示\n在生产环境中，可以通过设置环境变量 `ENVIRONMENT=prod` 来确保 NoneBot 读取正确的环境配置。\n:::\n\n#### .env.\\{ENVIRONMENT\\} 文件\n\n`.env.{ENVIRONMENT}` 文件类似于预设，可以让我们在多套不同的配置方案中灵活切换，默认 NoneBot 会读取 `.env.prod` 配置。如果你使用了 `nb-cli` 创建 `simple` 项目，那么将含有两套预设配置：`.env.dev` 和 `.env.prod`。\n\n在 NoneBot 初始化时，可以指定加载某个环境配置文件：\n\n```python\nnonebot.init(_env_file=\".env.dev\")\n```\n\n这将忽略在 `.env` 文件或环境变量中指定的 `ENVIRONMENT` 配置项。\n\n## 读取全局配置项\n\nNoneBot 的全局配置对象可以通过 `driver` 获取，如：\n\n```python\nimport nonebot\n\nconfig = nonebot.get_driver().config\n```\n\n如果我们需要获取某个配置项，可以直接通过 `config` 对象的属性访问：\n\n```python\nsuperusers = config.superusers\n```\n\n如果配置项不存在，将会抛出异常。\n\n## 插件配置\n\n在一个涉及大量配置项的项目中，通过直接读取全局配置项的方式显然并不高效。同时，由于额外的全局配置项没有预先定义，开发时编辑器将无法提示字段与类型，并且运行时没有对配置项直接进行合法性检查。那么就需要一种方式来规范定义插件配置项。\n\n在 NoneBot 中，我们使用强大高效的 `pydantic` 来定义配置模型，这个模型可以被用于配置的读取和类型检查等。例如在 `weather` 插件目录中新建 `config.py` 来定义一个模型：\n\n```python title=weather/config.py\nfrom pydantic import BaseModel, field_validator\n\nclass Config(BaseModel):\n    weather_api_key: str\n    weather_command_priority: int = 10\n    weather_plugin_enabled: bool = True\n\n    @field_validator(\"weather_command_priority\")\n    @classmethod\n    def check_priority(cls, v: int) -> int:\n        if v >= 1:\n            return v\n        raise ValueError(\"weather command priority must greater than 1\")\n```\n\n在 `config.py` 中，我们定义了一个 `Config` 类，它继承自 `pydantic.BaseModel`，并定义了一些配置项。在 `Config` 类中，我们还定义了一个 `check_priority` 方法，它用于检查 `weather_command_priority` 配置项的合法性。更多关于 `pydantic` 的编写方式，可以参考 [pydantic 官方文档](https://docs.pydantic.dev/)。\n\n在定义好配置模型后，我们可以在插件加载时通过配置模型获取插件配置：\n\n```python {5,11} title=weather/__init__.py\nfrom nonebot import get_plugin_config\n\nfrom .config import Config\n\nplugin_config = get_plugin_config(Config)\n\nweather = on_command(\n    \"天气\",\n    rule=to_me(),\n    aliases={\"weather\", \"查天气\"},\n    priority=plugin_config.weather_command_priority,\n    block=True,\n)\n```\n\n然后，我们便可以从 `plugin_config` 中读取配置了，例如 `plugin_config.weather_api_key`。\n\n这种方式可以简洁、高效地读取配置项，同时也可以设置默认值或者在运行时对配置项进行合法性检查，防止由于配置项导致的插件出错等情况出现。\n\n:::tip 可配置的事件响应优先级\n发布插件应该为自身的事件响应器提供可配置的优先级，以便插件使用者可以自定义多个插件间的响应顺序。\n:::\n\n:::tip 插件配置获取逻辑\n无论是否在 dotenv 文件中声明了插件配置项，使用 `get_plugin_config` 获取插件配置模型中定义的配置项时都遵循[**配置项的加载**](#配置项的加载)一节中的优先级顺序进行读取。\n:::\n\n### 避免插件配置名称冲突\n\n由于插件配置项是从全局配置和环境变量中读取的，通常我们需要在配置项名称前面添加前缀名，以防止配置项冲突。例如在上方的示例中，我们就添加了配置项前缀 `weather_`。但是这样会导致使用配置项时变量名过长，此时我们可以使用 `pydantic` 的 `alias` 或者通过配置 scope 来简化配置项名称。这里我们以 scope 配置为例：\n\n```python title=weather/config.py\nfrom pydantic import BaseModel\n\nclass ScopedConfig(BaseModel):\n    api_key: str\n    command_priority: int = 10\n    plugin_enabled: bool = True\n\nclass Config(BaseModel):\n    weather: ScopedConfig\n```\n\n```python title=weather/__init__.py\nfrom nonebot import get_plugin_config\n\nfrom .config import Config\n\nplugin_config = get_plugin_config(Config).weather\n```\n\n这样我们就可以省略插件配置项名称中的前缀 `weather_` 了。但需要注意的是，如果我们使用了 scope 配置，那么在配置文件中也需要使用 [`env_nested_delimiter` 格式](#配置项解析)，例如：\n\n```dotenv\nWEATHER__API_KEY=123456\nWEATHER__COMMAND_PRIORITY=10\n```\n\n## 内置配置项\n\n配置项 API 文档可以前往 [Config 类](../api/config.md#Config)查看。\n\n### Driver\n\n- **类型**: `str`\n- **默认值**: `\"~fastapi\"`\n\nNoneBot 运行所使用的驱动器。具体配置方法可以参考[安装驱动器](../tutorial/store.mdx#安装驱动器)和[选择驱动器](../advanced/driver.md)。\n\n<Tabs groupId=\"configMethod\">\n  <TabItem value=\"dotenv\" label=\"dotenv\" default>\n\n```dotenv\nDRIVER=~fastapi+~httpx+~websockets\n```\n\n  </TabItem>\n  <TabItem value=\"env\" label=\"系统环境变量\">\n\n```bash\n# windows cmd\nset DRIVER '~fastapi+~httpx+~websockets'\n# windows powershell\n$Env:DRIVER='~fastapi+~httpx+~websockets'\n# linux/macOS\nexport DRIVER='~fastapi+~httpx+~websockets'\n```\n\n  </TabItem>\n  <TabItem value=\"init\" label=\"直接传入\">\n\n```python title=bot.py\nimport nonebot\n\nnonebot.init(driver=\"~fastapi+~httpx+~websockets\")\n```\n\n  </TabItem>\n</Tabs>\n\n### Host\n\n- **类型**: `IPvAnyAddress`\n- **默认值**: `127.0.0.1`\n\n当 NoneBot 作为服务端时，监听的 IP / 主机名。\n\n<Tabs groupId=\"configMethod\">\n  <TabItem value=\"dotenv\" label=\"dotenv\" default>\n\n```dotenv\nHOST=127.0.0.1\n```\n\n  </TabItem>\n  <TabItem value=\"env\" label=\"系统环境变量\">\n\n```bash\n# windows cmd\nset HOST '127.0.0.1'\n# windows powershell\n$Env:HOST='127.0.0.1'\n# linux/macOS\nexport HOST='127.0.0.1'\n```\n\n  </TabItem>\n  <TabItem value=\"init\" label=\"直接传入\">\n\n```python title=bot.py\nimport nonebot\n\nnonebot.init(host=\"127.0.0.1\")\n```\n\n  </TabItem>\n</Tabs>\n\n### Port\n\n- **类型**: `int` (1 ~ 65535)\n- **默认值**: `8080`\n\n当 NoneBot 作为服务端时，监听的端口。\n\n<Tabs groupId=\"configMethod\">\n  <TabItem value=\"dotenv\" label=\"dotenv\" default>\n\n```dotenv\nPORT=8080\n```\n\n  </TabItem>\n  <TabItem value=\"env\" label=\"系统环境变量\">\n\n```bash\n# windows cmd\nset PORT '8080'\n# windows powershell\n$Env:PORT='8080'\n# linux/macOS\nexport PORT='8080'\n```\n\n  </TabItem>\n  <TabItem value=\"init\" label=\"直接传入\">\n\n```python title=bot.py\nimport nonebot\n\nnonebot.init(port=8080)\n```\n\n  </TabItem>\n</Tabs>\n\n### Log Level\n\n- **类型**: `int | str`\n- **默认值**: `INFO`\n\nNoneBot 日志输出等级，可以为 `int` 类型等级或等级名称。具体等级对照表参考 [loguru 日志等级](https://loguru.readthedocs.io/en/stable/api/logger.html#levels)。\n\n:::tip 提示\n日志等级名称应为大写，如 `INFO`。\n:::\n\n<Tabs groupId=\"configMethod\">\n  <TabItem value=\"dotenv\" label=\"dotenv\" default>\n\n```dotenv\nLOG_LEVEL=DEBUG\n```\n\n  </TabItem>\n  <TabItem value=\"env\" label=\"系统环境变量\">\n\n```bash\n# windows cmd\nset LOG_LEVEL 'DEBUG'\n# windows powershell\n$Env:LOG_LEVEL='DEBUG'\n# linux/macOS\nexport LOG_LEVEL='DEBUG'\n```\n\n  </TabItem>\n  <TabItem value=\"init\" label=\"直接传入\">\n\n```python title=bot.py\nimport nonebot\n\nnonebot.init(log_level=\"DEBUG\")\n```\n\n  </TabItem>\n</Tabs>\n\n### API Timeout\n\n- **类型**: `float | None`\n- **默认值**: `30.0`\n\n调用平台接口的超时时间，单位为秒。`None` 表示不设置超时时间。\n\n<Tabs groupId=\"configMethod\">\n  <TabItem value=\"dotenv\" label=\"dotenv\" default>\n\n```dotenv\nAPI_TIMEOUT=10.0\n```\n\n  </TabItem>\n  <TabItem value=\"env\" label=\"系统环境变量\">\n\n```bash\n# windows cmd\nset API_TIMEOUT '10.0'\n# windows powershell\n$Env:API_TIMEOUT='10.0'\n# linux/macOS\nexport API_TIMEOUT='10.0'\n```\n\n  </TabItem>\n  <TabItem value=\"init\" label=\"直接传入\">\n\n```python title=bot.py\nimport nonebot\n\nnonebot.init(api_timeout=10.0)\n```\n\n  </TabItem>\n</Tabs>\n\n### SuperUsers\n\n- **类型**: `set[str]`\n- **默认值**: `set()`\n\n机器人超级用户，可以使用权限 [`SUPERUSER`](../api/permission.md#SUPERUSER)。\n\n<Tabs groupId=\"configMethod\">\n  <TabItem value=\"dotenv\" label=\"dotenv\" default>\n\n```dotenv\nSUPERUSERS=[\"123123123\"]\n```\n\n  </TabItem>\n  <TabItem value=\"env\" label=\"系统环境变量\">\n\n```bash\n# windows cmd\nset SUPERUSERS '[\"123123123\"]'\n# windows powershell\n$Env:SUPERUSERS='[\"123123123\"]'\n# linux/macOS\nexport SUPERUSERS='[\"123123123\"]'\n```\n\n  </TabItem>\n  <TabItem value=\"init\" label=\"直接传入\">\n\n```python title=bot.py\nimport nonebot\n\nnonebot.init(superusers={\"123123123\"})\n```\n\n  </TabItem>\n</Tabs>\n\n### Nickname\n\n- **类型**: `set[str]`\n- **默认值**: `set()`\n\n机器人昵称，通常协议适配器会根据用户是否 @bot 或者是否以机器人昵称开头来判断是否是向机器人发送的消息。\n\n<Tabs groupId=\"configMethod\">\n  <TabItem value=\"dotenv\" label=\"dotenv\" default>\n\n```dotenv\nNICKNAME=[\"bot\"]\n```\n\n  </TabItem>\n  <TabItem value=\"env\" label=\"系统环境变量\">\n\n```bash\n# windows cmd\nset NICKNAME '[\"bot\"]'\n# windows powershell\n$Env:NICKNAME='[\"bot\"]'\n# linux/macOS\nexport NICKNAME='[\"bot\"]'\n```\n\n  </TabItem>\n  <TabItem value=\"init\" label=\"直接传入\">\n\n```python title=bot.py\nimport nonebot\n\nnonebot.init(nickname={\"bot\"})\n```\n\n  </TabItem>\n</Tabs>\n\n### Command Start 和 Command Separator\n\n- **类型**: `set[str]`\n- **默认值**:\n  - Command Start: `{\"/\"}`\n  - Command Separator: `{\".\"}`\n\n命令消息的起始符和分隔符。用于 [`command`](../advanced/matcher.md#command) 规则。\n\n<Tabs groupId=\"configMethod\">\n  <TabItem value=\"dotenv\" label=\"dotenv\" default>\n\n```dotenv\nCOMMAND_START=[\"/\", \"\"]\nCOMMAND_SEP=[\".\", \" \"]\n```\n\n  </TabItem>\n  <TabItem value=\"env\" label=\"系统环境变量\">\n\n```bash\n# windows cmd\nset COMMAND_START '[\"/\", \"\"]'\nset COMMAND_SEP '[\".\", \" \"]'\n# windows powershell\n$Env:COMMAND_START='[\"/\", \"\"]'\n$Env:COMMAND_SEP='[\".\", \" \"]'\n# linux/macOS\nexport COMMAND_START='[\"/\", \"\"]'\nexport COMMAND_SEP='[\".\", \" \"]'\n```\n\n  </TabItem>\n  <TabItem value=\"init\" label=\"直接传入\">\n\n```python title=bot.py\nimport nonebot\n\nnonebot.init(command_start={\"/\", \"\"}, command_sep={\".\", \" \"})\n```\n\n  </TabItem>\n</Tabs>\n\n### Session Expire Timeout\n\n- **类型**: `timedelta`\n- **默认值**: `timedelta(minutes=2)`\n\n用户会话超时时间，配置格式参考 [Datetime Types](https://docs.pydantic.dev/latest/api/standard_library_types/#datetimetimedelta)。\n\n<Tabs groupId=\"configMethod\">\n  <TabItem value=\"dotenv\" label=\"dotenv\" default>\n\n```dotenv\nSESSION_EXPIRE_TIMEOUT=00:02:00\n```\n\n  </TabItem>\n  <TabItem value=\"env\" label=\"系统环境变量\">\n\n```bash\n# windows cmd\nset SESSION_EXPIRE_TIMEOUT '00:02:00'\n# windows powershell\n$Env:SESSION_EXPIRE_TIMEOUT='00:02:00'\n# linux/macOS\nexport SESSION_EXPIRE_TIMEOUT='00:02:00'\n```\n\n  </TabItem>\n  <TabItem value=\"init\" label=\"直接传入\">\n\n```python title=bot.py\nimport nonebot\n\nnonebot.init(session_expire_timeout=120)\n```\n\n  </TabItem>\n</Tabs>\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/appendices/log.md",
    "content": "---\nsidebar_position: 6\ndescription: 记录与控制日志\n\noptions:\n  menu:\n    - category: appendices\n      weight: 70\n---\n\n# 日志\n\n无论是在开发还是在生产环境中，日志都是一个重要的功能，可以帮助我们了解运行状况、排查问题等。虽然我们可以使用 `print` 来将需要的信息输出到控制台，但是这种方式难以控制，而且不利于日志的归档、分析等。NoneBot 使用优秀的 [Loguru](https://loguru.readthedocs.io/) 库来进行日志记录。\n\n## 记录日志\n\n我们可以从 NoneBot 中导入 `logger` 对象，然后使用 `logger` 对象的方法来记录日志。\n\n```python\nfrom nonebot import logger\n\nlogger.trace(\"This is a trace message\")\nlogger.debug(\"This is a debug message\")\nlogger.info(\"This is an info message\")\nlogger.success(\"This is a success message\")\nlogger.warning(\"This is a warning message\")\nlogger.error(\"This is an error message\")\nlogger.critical(\"This is a critical message\")\n```\n\n我们仅需一行代码即可记录对应级别的日志。日志可以通过配置 [`LOG_LEVEL` 配置项](./config.mdx#log-level)来过滤输出等级，控制台中仅会输出大于等于 `LOG_LEVEL` 的日志。默认的 `LOG_LEVEL` 为 `INFO`，即只会输出 `INFO`、`SUCCESS`、`WARNING`、`ERROR`、`CRITICAL` 级别的日志。\n\n如果需要记录 `Exception traceback` 日志，可以向 `logger` 添加 `exception` 选项：\n\n```python {4}\ntry:\n    1 / 0\nexcept ZeroDivisionError:\n    logger.opt(exception=True).error(\"ZeroDivisionError\")\n```\n\n如果需要输出彩色日志，可以向 `logger` 添加 `colors` 选项：\n\n```python\nlogger.opt(colors=True).warning(\"We got a <red>BIG</red> problem\")\n```\n\n更多日志记录方法请参考 [Loguru 文档](https://loguru.readthedocs.io/)。\n\n## 自定义日志输出\n\nNoneBot 在启动时会添加一个默认的日志处理器，该处理器会将日志输出到**stdout**，并且根据 `LOG_LEVEL` 配置项过滤日志等级。\n\n默认的日志格式为：\n\n```text\n<g>{time:MM-DD HH:mm:ss}</g> [<lvl>{level}</lvl>] <c><u>{name}</u></c> | {message}\n```\n\n我们可以从 `nonebot.log` 模块导入以使用 NoneBot 的默认格式和过滤器：\n\n```python\nfrom nonebot.log import default_format, default_filter\n```\n\n如果需要自定义日志格式，我们需要移除 NoneBot 默认的日志处理器并添加新的日志处理器。例如，在机器人入口文件中 `nonebot.init` 之前添加以下内容：\n\n```python title=bot.py\nfrom nonebot.log import logger_id\n\n# 移除 NoneBot 默认的日志处理器\nlogger.remove(logger_id)\n# 添加新的日志处理器\nlogger.add(\n    sys.stdout,\n    level=0,\n    diagnose=True,\n    format=\"<g>{time:MM-DD HH:mm:ss}</g> [<lvl>{level}</lvl>] <c><u>{name}</u></c> | {message}\",\n    filter=default_filter\n)\n```\n\n如果想要输出日志到文件，我们可以使用 `logger.add` 方法添加文件处理器：\n\n```python title=bot.py\nlogger.add(\"error.log\", level=\"ERROR\", format=default_format, rotation=\"1 week\")\n```\n\n更多日志处理器的使用方法请参考 [Loguru 文档](https://loguru.readthedocs.io/)。\n\n## 重定向 logging 日志\n\n`logging` 是 Python 标准库中的日志模块，NoneBot 提供了一个 logging handler 用于将 `logging` 日志重定向到 `loguru` 处理。\n\n```python\nfrom nonebot.log import LoguruHandler\n\n# root logger 添加 LoguruHandler\nlogging.basicConfig(handlers=[LoguruHandler()])\n# 或者为其他 logging.Logger 添加 LoguruHandler\nlogger.addHandler(LoguruHandler())\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/appendices/overload.md",
    "content": "---\nsidebar_position: 7\ndescription: 根据事件类型进行不同的处理\n\noptions:\n  menu:\n    - category: appendices\n      weight: 80\n---\n\n# 事件类型与重载\n\n在之前的示例中，我们已经了解了如何[获取事件信息](../tutorial/event-data.mdx)以及[使用平台接口](./api-calling.mdx)。但是，事件信息通常不仅仅包含消息这一个内容，还有其他平台提供的信息，例如消息发送时间、消息发送者等等。同时，在使用平台接口时，我们需要确保使用的**平台接口**与所要发送的**平台类型**一致，对不同类型的事件需要做出不同的处理。在本章节中，我们将介绍如何获取事件更多的信息以及根据事件类型进行不同的处理。\n\n## 事件类型\n\n在 NoneBot 中，事件均是 `nonebot.adapters.Event` 基类的子类型，基类对一些必要的属性进行了抽象，子类型则根据不同的平台进行了实现。在[自定义权限](./permission.mdx#自定义权限)一节中，我们就使用了 `Event` 的抽象方法 `get_user_id` 来获取事件发送者 ID，这个方法由协议适配器进行了实现，返回机器人用户对应的平台 ID。更多的基类抽象方法可以在[使用适配器](../advanced/adapter.md#获取事件通用信息)中查看。\n\n既然事件是基类的子类型，我们实际可以获得的信息通常多于基类抽象方法所提供的。如果我们不满足于基类能获得的信息，我们可以小小的修改一下事件处理函数的事件参数类型注解，使其变为子类型，这样我们就可以通过协议适配器定义的子类型来获取更多的信息。我们以 `Console` 协议适配器为例：\n\n```python {4} title=weather/__init__.py\nfrom nonebot.adapters.console import MessageEvent\n\n@weather.got(\"location\", prompt=\"请输入地名\")\nasync def got_location(event: MessageEvent, location: str = ArgPlainText()):\n    await weather.finish(f\"{event.time.strftime('%Y-%m-%d')} {location} 的天气是...\")\n```\n\n在上面的代码中，我们获取了 `Console` 协议适配器的消息事件提供的发送时间 `time` 属性。\n\n:::caution 注意\n如果**基类**就能满足你的需求，那么就**不要修改**事件参数类型注解，这样可以使你的代码更加**通用**，可以在更多平台上运行。如何根据不同平台事件类型进行不同的处理，我们将在[重载](#重载)一节中介绍。\n:::\n\n## 重载\n\n我们在编写机器人时，常常会遇到这样一个问题：如何对私聊和群聊消息进行不同的处理？如何对不同平台的事件进行不同的处理？针对这些问题，NoneBot 提供了一个便捷而高效的解决方案 ── 重载。简单来说，依赖函数会根据其参数的类型注解来决定是否执行，忽略不符合其参数类型注解的情况。这样，我们就可以通过修改事件参数类型注解来实现对不同事件的处理，或者修改 `Bot` 参数类型注解来实现使用不同平台的接口。我们以 `OneBot` 协议适配器为例：\n\n```python {4,8}\nfrom nonebot.adapters.onebot.v11 import PrivateMessageEvent, GroupMessageEvent\n\n@matcher.handle()\nasync def handle_private(event: PrivateMessageEvent):\n    await matcher.finish(\"私聊消息\")\n\n@matcher.handle()\nasync def handle_group(event: GroupMessageEvent):\n    await matcher.finish(\"群聊消息\")\n```\n\n这样，机器人用户就会在私聊和群聊中分别收到不同的回复。同样的，我们也可以通过修改 `Bot` 参数类型注解来实现使用不同平台的接口：\n\n```python\nfrom nonebot.adapters.console import Bot as ConsoleBot\nfrom nonebot.adapters.onebot.v11 import Bot as OneBot\n\n@matcher.handle()\nasync def handle_console(bot: ConsoleBot):\n    await bot.bell()\n\n@matcher.handle()\nasync def handle_onebot(bot: OneBot):\n    await bot.send_group_message(group_id=123123, message=\"OneBot\")\n```\n\n:::caution 注意\n重载机制对所有的参数类型注解都有效，因此，依赖注入也可以使用这个特性来对不同的返回值进行处理。\n\n但 Bot、Event 和 Matcher 三者的参数类型注解具有最高检查优先级，如果三者任一类型注解不匹配，那么其他依赖注入将不会执行（如：`Depends`）。\n:::\n\n:::tip 提示\n如何更好地编写一个跨平台的插件，我们将在[最佳实践](../best-practice/multi-adapter.mdx)中介绍。\n:::\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/appendices/permission.mdx",
    "content": "---\nsidebar_position: 5\ndescription: 控制事件响应器的权限\n\noptions:\n  menu:\n    - category: appendices\n      weight: 60\n---\n\n# 权限控制\n\nimport Messenger from \"@site/src/components/Messenger\";\n\n**权限控制**是机器人在实际应用中需要解决的重点问题之一，NoneBot 提供了灵活的权限控制机制 —— `Permission`。\n\n类似于响应规则 `Rule`，`Permission` 是由非负整数个 `PermissionChecker` 所共同组成的**用于筛选事件**的对象。但需要特别说明的是，权限和响应规则有如下区别：\n\n1. 权限检查**先于**响应规则检查\n2. `Permission` 只需**其中一个** `PermissionChecker` 返回 `True` 时就会检查通过\n3. 权限检查进行时，上下文中并不存在会话状态 `state`\n4. `Rule` 仅在**初次触发**事件响应器时进行检查，在余下的会话中并不会限制事件；而 `Permission` 会**持续生效**，在连续对话中一直对事件主体加以限制。\n\n## 基础使用\n\n通常情况下，`Permission` 更侧重于对于**触发事件的机器人用户**的筛选，例如由 NoneBot 自身提供的 `SUPERUSER` 权限，便是筛选出会话发起者是否为超级用户。它可以对输入的用户进行鉴别，如果符合要求则会被认为通过并返回 `True`，反之则返回 `False`。\n\n简单来说，`Permission` 是一个用于筛选出符合要求的用户的机制，可以通过 `Permission` 精确的控制响应对象的覆盖范围，从而拒绝掉我们所不希望的事件。\n\n例如，我们可以在 `weather` 插件中添加一个超级用户可用的指令：\n\n```python {3,9} title=weather/__init__.py\nfrom typing import Tuple\nfrom nonebot.params import Command\nfrom nonebot.permission import SUPERUSER\n\nmanage = on_command(\n    (\"天气\", \"启用\"),\n    rule=to_me(),\n    aliases={(\"天气\", \"禁用\")},\n    permission=SUPERUSER,\n)\n\n@manage.handle()\nasync def control(cmd: Tuple[str, str] = Command()):\n    _, action = cmd\n    if action == \"启用\":\n        plugin_config.weather_plugin_enabled = True\n    elif action == \"禁用\":\n        plugin_config.weather_plugin_enabled = False\n    await manage.finish(f\"天气插件已{action}\")\n```\n\n如上方示例所示，在注册事件响应器时，我们设置了 `permission` 参数，那么这个事件处理器在触发事件前的检查阶段会对用户身份进行验证，如果不符合我们设置的条件（此处即为**超级用户**）则不会响应。此时，我们向机器人发送 `/天气.禁用` 指令，机器人不会有任何响应，因为我们还不是机器人的超级管理员。我们在 dotenv 文件中设置了 `SUPERUSERS` 配置项之后，机器人就会响应我们的指令了。\n\n```dotenv title=.env\nSUPERUSERS=[\"console_user\"]\n```\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/天气.禁用\" },\n    { position: \"left\", msg: \"天气插件已禁用\" },\n    { position: \"right\", msg: \"/天气.启用\" },\n    { position: \"left\", msg: \"天气插件已启用\" },\n  ]}\n/>\n\n## 自定义权限\n\n与事件响应规则类似，`PermissionChecker` 也是一个返回值为 `bool` 类型的依赖函数，即 `PermissionChecker` 支持依赖注入。例如，我们可以限制用户的指令调用次数：\n\n```python title=weather/__init__.py\nfrom nonebot.adapters import Event\n\nfake_db: Dict[str, int] = {}\n\nasync def limit_permission(event: Event):\n    count = fake_db.setdefault(event.get_user_id(), 100)\n    if count > 0:\n        fake_db[event.get_user_id()] -= 1\n        return True\n    return False\n\nweather = on_command(\"天气\", permission=limit_permission)\n```\n\n## 权限组合\n\n权限之间可以通过 `|` 运算符进行组合，使得任意一个权限检查返回 `True` 时通过。例如：\n\n```python {4-6}\nperm1 = Permission(foo_checker)\nperm2 = Permission(bar_checker)\n\nperm = perm1 | perm2\nperm = perm1 | bar_checker\nperm = foo_checker | perm2\n```\n\n同样的，我们也无需担心组合了一个 `None` 值，`Permission` 会自动忽略 `None` 值。\n\n```python\nassert (perm | None) is perm\n```\n\n## 主动使用权限\n\n除了在事件响应器中使用权限外，我们也可以主动使用权限来判断事件是否符合条件。例如：\n\n```python {3}\nperm = Permission(some_checker)\n\nresult: bool = await perm(bot, event)\n```\n\n我们只需要传入 `Bot` 实例、事件，`Permission` 会并发调用所有 `PermissionChecker` 进行检查，并返回结果。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/appendices/rule.md",
    "content": "---\nsidebar_position: 1\ndescription: 自定义响应规则\n\noptions:\n  menu:\n    - category: appendices\n      weight: 20\n---\n\n# 响应规则\n\n机器人在实际应用中，往往会接收到多种多样的事件类型，NoneBot 通过响应规则来控制事件的处理。\n\n在[指南](../tutorial/matcher.md#为事件响应器添加参数)中，我们为 `weather` 命令添加了一个 `rule=to_me()` 参数，这个参数就是一个响应规则，确保只有在私聊或者 `@bot` 时才会响应。\n\n响应规则是一个 `Rule` 对象，它由一系列的 `RuleChecker` 函数组成，每个 `RuleChecker` 函数都会检查事件是否符合条件，如果所有的检查都通过，则事件会被处理。\n\n## RuleChecker\n\n`RuleChecker` 是一个返回值为 `bool` 类型的依赖函数，即 `RuleChecker` 支持依赖注入。我们可以根据上一节中添加的[配置项](./config.mdx#插件配置)，在 `weather` 插件目录中编写一个响应规则：\n\n```python {7,8} title=weather/__init__.py\nfrom nonebot import get_plugin_config\n\nfrom .config import Config\n\nplugin_config = get_plugin_config(Config)\n\nasync def is_enable() -> bool:\n    return plugin_config.weather_plugin_enabled\n\nweather = on_command(\"天气\", rule=is_enable)\n```\n\n在上面的代码中，我们定义了一个函数 `is_enable`，它会检查配置项 `weather_plugin_enabled` 是否为 `True`。这个函数 `is_enable` 即为一个 `RuleChecker`。\n\n## Rule\n\n`Rule` 是若干个 `RuleChecker` 的集合，它会并发调用每个 `RuleChecker`，只有当所有 `RuleChecker` 检查通过时匹配成功。例如：我们可以组合两个 `RuleChecker`，一个用于检查插件是否启用，一个用于检查用户是否在黑名单中：\n\n```python {10}\nfrom nonebot.rule import Rule\nfrom nonebot.adapters import Event\n\nasync def is_enable() -> bool:\n    return plugin_config.weather_plugin_enabled\n\nasync def is_blacklisted(event: Event) -> bool:\n    return event.get_user_id() not in BLACKLIST\n\nrule = Rule(is_enable, is_blacklisted)\n\nweather = on_command(\"天气\", rule=rule)\n```\n\n## 合并响应规则\n\n在定义响应规则时，我们可以将规则进行细分，来更好地复用规则。而在使用时，我们需要合并多个规则。除了使用 `Rule` 对象来组合多个 `RuleChecker` 外，我们还可以对 `Rule` 对象进行合并。在原 `weather` 插件中，我们可以将 `rule=to_me()` 与 `rule=is_enable` 使用 `&` 运算符合并：\n\n```python {13} title=weather/__init__.py\nfrom nonebot.rule import to_me\nfrom nonebot import get_plugin_config\n\nfrom .config import Config\n\nplugin_config = get_plugin_config(Config)\n\nasync def is_enable() -> bool:\n    return plugin_config.weather_plugin_enabled\n\nweather = on_command(\n    \"天气\",\n    rule=to_me() & is_enable,\n    aliases={\"weather\", \"查天气\"},\n    priority=plugin_config.weather_command_priority,\n    block=True,\n)\n```\n\n这样，`weather` 命令就只会在插件启用且在私聊或者 `@bot` 时才会响应。\n\n合并响应规则可以有多种形式，例如：\n\n```python {4-6}\nrule1 = Rule(foo_checker)\nrule2 = Rule(bar_checker)\n\nrule = rule1 & rule2\nrule = rule1 & bar_checker\nrule = foo_checker & rule2\n```\n\n同时，我们也无需担心合并了一个 `None` 值，`Rule` 会忽略 `None` 值。\n\n```python\nassert (rule & None) is rule\n```\n\n## 主动使用响应规则\n\n除了在事件响应器中使用响应规则外，我们也可以主动使用响应规则来判断事件是否符合条件。例如：\n\n```python {3}\nrule = Rule(some_checker)\n\nresult: bool = await rule(bot, event, state)\n```\n\n我们只需要传入 `Bot` 对象、事件和会话状态，`Rule` 会并发调用所有 `RuleChecker` 进行检查，并返回结果。\n\n## 内置响应规则\n\nNoneBot 内置了一些常用的响应规则，可以直接通过事件响应器辅助函数或者自行合并其他规则使用。内置响应规则列表可以参考[事件响应器进阶](../advanced/matcher.md)\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/appendices/session-control.mdx",
    "content": "---\nsidebar_position: 2\ndescription: 更灵活的会话控制\n\noptions:\n  menu:\n    - category: appendices\n      weight: 30\n---\n\n# 会话控制\n\nimport Messenger from \"@site/src/components/Messenger\";\n\n在[指南](../tutorial/event-data.mdx#使用依赖注入)的 `weather` 插件中，我们使用依赖注入获取了机器人用户发送的地名参数，并根据地名参数进行相应的回复。但是，一问一答的对话模式仅仅适用于简单的对话场景，如果我们想要实现更复杂的对话模式，就需要使用会话控制。\n\n## 询问并获取用户输入\n\n在 `weather` 插件中，我们对于用户未输入地名参数的情况直接回复了 `请输入地名` 并结束了事件流程。但是，这样用户体验并不好，需要重新输入指令和地名参数才能获取天气回复。我们现在来实现询问并获取用户地名参数的功能。\n\n### 询问用户\n\n我们可以使用事件响应器操作中的 `got` 装饰器来表示当前事件处理流程需要询问并获取用户输入的消息：\n\n```python {6} title=weather/__init__.py\n@weather.handle()\nasync def handle_function(args: Message = CommandArg()):\n    if location := args.extract_plain_text():\n        await weather.finish(f\"今天{location}的天气是...\")\n\n@weather.got(\"location\", prompt=\"请输入地名\")\nasync def got_location():\n    ...\n```\n\n在上面的代码中，我们使用 `got` 事件响应器操作来向用户发送 `prompt` 消息，并等待用户的回复。用户的回复消息将会被作为 `location` 参数存储于事件响应器状态中。\n\n:::tip 提示\n事件处理函数根据定义的顺序依次执行。\n:::\n\n### 获取用户输入\n\n在询问以及用户回复之后，我们就可以获取到我们需要的 `location` 参数了。我们使用 `ArgPlainText` 依赖注入来获取参数纯文本信息：\n\n```python {9} title=weather/__init__.py\nfrom nonebot.params import ArgPlainText\n\n@weather.handle()\nasync def handle_function(args: Message = CommandArg()):\n    if location := args.extract_plain_text():\n        await weather.finish(f\"今天{location}的天气是...\")\n\n@weather.got(\"location\", prompt=\"请输入地名\")\nasync def got_location(location: str = ArgPlainText()):\n    await weather.finish(f\"今天{location}的天气是...\")\n```\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/天气\" },\n    { position: \"left\", msg: \"请输入地名\" },\n    { position: \"right\", msg: \"北京\" },\n    { position: \"left\", msg: \"今天北京的天气是...\" },\n  ]}\n/>\n\n在上面的代码中，我们在 `got_location` 函数中定义了一个依赖注入参数 `location`，他的值将会是用户回复的消息纯文本信息。获取到用户输入的地名参数后，我们就可以进行天气查询并回复了。\n\n:::tip 提示\n如果想要获取用户回复的消息对象 `Message` ，可以使用 `Arg` 依赖注入。\n:::\n\n### 跳过询问\n\n在上面的代码中，如果用户在输入天气指令时，同时提供了地名参数，我们直接回复了天气信息，这部分的逻辑是和询问用户地名参数之后的逻辑一致的。如果在复杂的业务场景下，我们希望这部分代码应该复用以减少代码冗余。我们可以使用事件响应器操作中的 `set_arg` 来主动设置一个参数：\n\n```python {4,6} title=weather/__init__.py\nfrom nonebot.matcher import Matcher\n\n@weather.handle()\nasync def handle_function(matcher: Matcher, args: Message = CommandArg()):\n    if args.extract_plain_text():\n        matcher.set_arg(\"location\", args)\n\n@weather.got(\"location\", prompt=\"请输入地名\")\nasync def got_location(location: str = ArgPlainText()):\n    await weather.finish(f\"今天{location}的天气是...\")\n```\n\n请注意，设置参数需要使用依赖注入来获取 `Matcher` 实例以确保上下文正确，且参数值应为 `Message` 对象。\n\n在 `location` 参数被设置之后，`got` 事件响应器操作将不再会询问并等待用户的回复，而是直接进入 `got_location` 函数。\n\n## 请求重新输入\n\n在实际的业务场景中，用户的输入很有可能并非是我们所期望的，而结束事件处理流程让用户重新发送指令也不是一个好的体验。这时我们可以使用 `reject` 事件响应器操作来请求用户重新输入：\n\n```python {8,9} title=weather/__init__.py\n@weather.handle()\nasync def handle_function(matcher: Matcher, args: Message = CommandArg()):\n    if args.extract_plain_text():\n        matcher.set_arg(\"location\", args)\n\n@weather.got(\"location\", prompt=\"请输入地名\")\nasync def got_location(location: str = ArgPlainText()):\n    if location not in [\"北京\", \"上海\", \"广州\", \"深圳\"]:\n        await weather.reject(f\"你想查询的城市 {location} 暂不支持，请重新输入！\")\n    await weather.finish(f\"今天{location}的天气是...\")\n```\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/天气\" },\n    { position: \"left\", msg: \"请输入地名\" },\n    { position: \"right\", msg: \"南京\" },\n    { position: \"left\", msg: \"你想查询的城市 南京 暂不支持，请重新输入！\" },\n    { position: \"right\", msg: \"北京\" },\n    { position: \"left\", msg: \"今天北京的天气是...\" },\n  ]}\n/>\n\n在上面的代码中，我们在 `got_location` 函数中判断用户输入的地名是否在支持的城市列表中，如果不在，则使用 `reject` 事件响应器操作。操作将会向用户发送 `reject` 参数中的消息，并等待用户回复后，重新执行 `got_location` 函数。通过 `got` 和 `reject` 事件响应器操作，我们实现了类似于**循环**的执行方式。\n\n`reject` 事件响应器操作与 `finish` 类似，NoneBot 会在向机器人用户发送消息内容后抛出 `RejectedException` 异常来暂停事件响应流程以等待用户输入。也就是说，在 `reject` 被执行后，后续的程序同样是不会被执行的。\n\n## 更多事件响应器操作\n\n在之前的章节中，我们已经大致了解了五个事件响应器操作：`handle`、`got`、`finish`、`send` 和 `reject`。现在我们来完整地介绍一下这些操作。\n\n事件响应器操作可以分为两大类：**交互操作**和**流程控制操作**。我们可以通过交互操作来与用户进行交互，而流程控制操作则可以用来控制事件处理流程的执行。\n\n:::tip 提示\n事件处理流程按照事件处理函数添加顺序执行，已经结束的事件处理函数不可能被恢复执行。\n:::\n\n### handle\n\n`handle` 事件响应器操作是一个装饰器，用于向事件处理流程添加一个事件处理函数。\n\n```python\n@matcher.handle()\nasync def handle_func():\n    ...\n```\n\n`handle` 装饰器支持嵌套操作，即一个事件处理函数可以被添加多次：\n\n```python\n@matcher.handle()\n@matcher.handle()\nasync def handle_func():\n    # 这个函数会被执行两次\n    ...\n```\n\n### got\n\n`got` 事件响应器操作也是一个装饰器，它会在当前装饰的事件处理函数运行之前，中断当前事件处理流程，等待接收一个新的事件。它可以通过 `prompt` 参数来向用户发送询问消息，然后等待用户的回复消息，贴近对话形式会话。\n\n`got` 装饰器接受一个参数 `key` 和一个可选参数 `prompt`。当会话状态中不存在 `key` 对应的消息时，会向用户发送 `prompt` 参数的消息，并等待用户回复。`prompt` 参数的类型和 [`send`](#send) 事件响应器操作的参数类型一致。\n\n在事件处理函数中，可以通过依赖注入的方式来获取接收到的消息，参考：[`Arg`](../advanced/dependency.mdx#arg)、[`ArgStr`](../advanced/dependency.mdx#argstr)、[`ArgPlainText`](../advanced/dependency.mdx#argplaintext)。\n\n```python\n@matcher.got(\"key\", prompt=\"请输入...\")\nasync def got_func(key: Message = Arg()):\n    ...\n```\n\n`got` 装饰器支持与 `got` 和 `receive` 装饰器嵌套操作，即一个事件处理函数可以在接收多个事件或消息后执行：\n\n```python\n@matcher.got(\"key1\", prompt=\"请输入key1...\")\n@matcher.got(\"key2\", prompt=\"请输入key2...\")\n@matcher.receive(\"key3\")\nasync def got_func(key1: Message = Arg(), key2: Message = Arg(), key3: Event = Received(\"key3\")):\n    ...\n```\n\n### receive\n\n`receive` 事件响应器操作也是一个装饰器，它会在当前装饰的事件处理函数运行之前，中断当前事件处理流程，等待接收一个新的事件。与 `got` 不同的是，`receive` 不会向用户发送询问消息，并且等待一个用户事件。可以接收的事件类型取决于[会话更新](../advanced/session-updating.md)。\n\n`receive` 装饰器接受一个可选参数 id，用于标识当前需要接收的事件，如果不指定，则默认为空 `\"\"`。\n\n在事件处理函数中，可以通过依赖注入的方式来获取接收到的事件，参考：[`Received`](../advanced/dependency.mdx#received)、[`LastReceived`](../advanced/dependency.mdx#lastreceived)。\n\n```python\n@matcher.receive(\"id\")\nasync def receive_func(event: Event = Received(\"id\")):\n    ...\n```\n\n`receive` 装饰器支持与 `got` 和 `receive` 装饰器嵌套操作，即一个事件处理函数可以在接收多个事件或消息后执行：\n\n```python\n@matcher.receive(\"key1\")\n@matcher.got(\"key2\", prompt=\"请输入key2...\")\n@matcher.got(\"key3\", prompt=\"请输入key3...\")\nasync def receive_func(key1: Event = Received(\"key1\"), key2: Message = Arg(), key3: Message = Arg()):\n    ...\n```\n\n### send\n\n`send` 事件响应器操作用于向用户回复一条消息。协议适配器会根据当前 event 选择回复的途径。\n\n`send` 操作接受一个参数 message 和其他任何协议适配器接受的参数。message 参数类型可以是字符串、消息序列、消息段或者消息模板。消息模板将会使用会话状态字典进行渲染后发送。\n\n这个操作等同于使用 `bot.send(event, message, **kwargs)`，但不需要自行传入 `event`。\n\n```python\n@matcher.handle()\nasync def _():\n    await matcher.send(\"Hello world!\")\n```\n\n### finish\n\n向用户回复一条消息（可选），并立即结束**整个处理流程**。\n\n参数与 [`send`](#send) 相同。\n\n```python\n@matcher.handle()\nasync def _():\n    await matcher.finish(\"Hello world!\")\n    # 下面的代码不会被执行\n```\n\n### pause\n\n向用户回复一条消息（可选），立即结束**当前**事件处理函数，等待接收一个新的事件后进入**下一个**事件处理函数。\n\n参数与 [`send`](#send) 相同。\n\n```python\n@matcher.handle()\nasync def _():\n    if need_confirm:\n        await matcher.pause(\"请在两分钟内确认执行\")\n\n@matcher.handle()\nasync def _():\n    ...\n```\n\n### reject\n\n向用户回复一条消息（可选），立即结束**当前**事件处理函数，等待接收一个新的事件后再次执行**当前**事件处理函数。\n\n`reject` 可以用于拒绝当前 `receive` 接收的事件或 `got` 接收的参数。通常在用户回复不符合格式或标准需要重新输入，或者用于循环进行用户交互。\n\n参数与 [`send`](#send) 相同。\n\n```python\n@matcher.got(\"arg\")\nasync def _(arg: str = ArgPlainText()):\n    if not is_valid(arg):\n        await matcher.reject(\"Invalid arg!\")\n```\n\n### reject_arg\n\n向用户回复一条消息（可选），立即结束**当前**事件处理函数，等待接收一个新的消息后再次执行**当前**事件处理函数。\n\n`reject_arg` 用于拒绝指定 `got` 接收的参数，通常在嵌套装饰器时使用。\n\n`reject_arg` 操作接受一个 key 参数以及可选的 prompt 参数。prompt 参数与 [`send`](#send) 相同。\n\n```python\n@matcher.got(\"a\")\n@matcher.got(\"b\")\nasync def _(a: str = ArgPlainText(), b: str = ArgPlainText()):\n    if a not in b:\n        await matcher.reject_arg(\"a\", \"Invalid a!\")\n```\n\n### reject_receive\n\n向用户回复一条消息（可选），立即结束**当前**事件处理函数，等待接收一个新的事件后再次执行**当前**事件处理函数。\n\n`reject_receive` 用于拒绝指定 `receive` 接收的事件，通常在嵌套装饰器时使用。\n\n`reject_receive` 操作接受一个可选的 id 参数以及可选的 prompt 参数。id 参数默认为空 `\"\"`，prompt 参数与 [`send`](#send) 相同。\n\n```python\n@matcher.receive(\"a\")\n@matcher.receive(\"b\")\nasync def _(a: Event = Received(\"a\"), b: Event = Received(\"b\")):\n    if a.get_user_id() != b.get_user_id():\n        await matcher.reject_receive(\"a\")\n```\n\n### skip\n\n立即结束当前事件处理函数，进入下一个事件处理函数。\n\n通常在依赖注入中使用，用于跳过当前事件处理函数的执行。\n\n```python\nfrom nonebot.params import Depends\n\nasync def dependency():\n    matcher.skip()\n\n@matcher.handle()\nasync def _(check=Depends(dependency)):\n    # 这个函数不会被执行\n```\n\n### stop_propagation\n\n阻止事件向更低优先级的事件响应器传播。\n\n```python\nfrom nonebot.matcher import Matcher\n\n@foo.handle()\nasync def _(matcher: Matcher):\n    matcher.stop_propagation()\n```\n\n:::caution 注意\n`stop_propagation` 操作是实例方法，需要先通过依赖注入获取事件响应器实例再进行调用。\n:::\n\n### get_arg\n\n获取一个 `got` 接收的参数。\n\n`get_arg` 操作接受一个 key 参数和一个可选的 default 参数。当参数不存在时，将返回 default 或 `None`。\n\n```python\nfrom nonebot.matcher import Matcher\n\n@matcher.handle()\nasync def _(matcher: Matcher):\n    key = matcher.get_arg(\"key\", default=None)\n```\n\n### set_arg\n\n设置 / 覆盖一个 `got` 接收的参数。\n\n`set_arg` 操作接受一个 key 参数和一个 value 参数。请注意，value 参数必须是消息序列对象，如需存储其他数据请使用[会话状态](./session-state.md)。\n\n```python\nfrom nonebot.matcher import Matcher\n\n@matcher.handle()\nasync def _(matcher: Matcher):\n    matcher.set_arg(\"key\", Message(\"value\"))\n```\n\n### get_receive\n\n获取一个 `receive` 接收的事件。\n\n`get_receive` 操作接受一个 id 参数和一个可选的 default 参数。当事件不存在时，将返回 default 或 `None`。\n\n```python\nfrom nonebot.matcher import Matcher\n\n@matcher.handle()\nasync def _(matcher: Matcher):\n    event = matcher.get_receive(\"id\", default=None)\n```\n\n### get_last_receive\n\n获取最近的一个 `receive` 接收的事件。\n\n`get_last_receive` 操作接受一个可选的 default 参数。当事件不存在时，将返回 default 或 `None`。\n\n```python\nfrom nonebot.matcher import Matcher\n\n@matcher.handle()\nasync def _(matcher: Matcher):\n    event = matcher.get_last_receive(default=None)\n```\n\n### set_receive\n\n设置 / 覆盖一个 `receive` 接收的事件。\n\n`set_receive` 操作接受一个 id 参数和一个 event 参数。请注意，event 参数必须是事件对象，如需存储其他数据请使用[会话状态](./session-state.md)。\n\n```python\nfrom nonebot.matcher import Matcher\n\n@matcher.handle()\nasync def _(matcher: Matcher):\n    matcher.set_receive(\"key\", Event())\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/appendices/session-state.md",
    "content": "---\nsidebar_position: 3\ndescription: 会话状态信息\n\noptions:\n  menu:\n    - category: appendices\n      weight: 40\n---\n\n# 会话状态\n\n在事件处理流程中，和用户交互的过程即是会话。在会话中，我们可能需要记录一些信息，例如用户的重试次数等等，以便在会话中的不同阶段进行判断和处理。这些信息都可以存储于会话状态中。\n\nNoneBot 中的会话状态是一个字典，可以通过类型 `T_State` 来获取。字典内可以存储任意类型的数据，但是要注意的是，NoneBot 本身会在会话状态中存储一些信息，因此不要使用 [NoneBot 使用的键名](../api/consts.md)。\n\n```python\nfrom nonebot.typing import T_State\n\n@matcher.got(\"key\", prompt=\"请输入密码\")\nasync def _(state: T_State, key: str = ArgPlainText()):\n    if key != \"some password\":\n        try_count = state.get(\"try_count\", 1)\n        if try_count >= 3:\n            await matcher.finish(\"密码错误次数过多\")\n        else:\n            state[\"try_count\"] = try_count + 1\n            await matcher.reject(\"密码错误，请重新输入\")\n    await matcher.finish(\"密码正确\")\n```\n\n会话状态的生命周期与事件处理流程相同，在期间的任何一个事件处理函数都可以进行读写。\n\n```python\nfrom nonebot.typing import T_State\n\n@matcher.handle()\nasync def _(state: T_State):\n    state[\"key\"] = \"value\"\n\n@matcher.handle()\nasync def _(state: T_State):\n    await matcher.finish(state[\"key\"])\n```\n\n会话状态还可以用于发送动态消息，消息模板在发送时会使用会话状态字典进行渲染。消息模板的使用方法已经在[消息处理](../tutorial/message.md#使用消息模板)中介绍过，这里不再赘述。\n\n```python\nfrom nonebot.typing import T_State\nfrom nonebot.adapters import MessageTemplate\n\n@matcher.handle()\nasync def _(state: T_State):\n    state[\"username\"] = \"user\"\n\n@matcher.got(\"password\", prompt=MessageTemplate(\"请输入 {username} 的密码\"))\nasync def _():\n    await matcher.finish(MessageTemplate(\"密码为 {password}\"))\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/appendices/whats-next.md",
    "content": "---\nsidebar_position: 99\ndescription: 下一步──进阶！\n---\n\n# 下一步\n\n至此，我们已经了解了 NoneBot 的大多数功能用法，相信你已经可以独自写出一个插件了。现在你可以选择：\n\n- 即刻开始插件编写！\n- 更深入地了解 NoneBot 的[更多功能和原理](../advanced/plugin-info.md)！\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/best-practice/alconna/README.mdx",
    "content": "import Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\n\n# Alconna 插件\n\n[`nonebot-plugin-alconna`](https://github.com/nonebot/plugin-alconna) 是一类极大地提升了 NoneBot 开发体验的插件。\n\n该插件可分为三个部分：\n\n- 增强的命令解析: 基于 [Alconna](https://github.com/ArcletProject/Alconna), 提供一类新的事件响应器辅助函数 `on_alconna`. 相比 `on_command`, `on_shell`, `on_regex` 等函数，`on_alconna` 提供了更强大的命令解析能力与诸多特性。\n- 通用消息组件: 实现了跨平台接收、发送、撤回、编辑、表态消息的功能。\n  - `UniMessage` 通用消息模型，支持各适配器下的消息转换和导出，发送。\n  - `Text`, `Image`, `At` 等通用消息段模型，既与 `UniMessage` 配合使用，又能用于 `Alconna` 的命令解析。\n  - `message_recall`, `message_edit`, `message_reaction` 等功能函数。\n  - `Target` 通用消息目标模型，并通过该模型进行主动消息发送。\n  - `UniMsg`, `MsgId`, `MsgTarget`, `at_in`, `at_me` 等提供给 nonebot 使用的依赖注入和 `Rule`。\n- 内置功能插件：基于上述部分实现的内置功能插件。\n  - `echo`: 通过 `on_alconna` 实现的 echo 插件，支持回显回复消息。\n  - `help`: 列出所有 `on_alconna` 事件响应器的帮助信息或其对应的插件信息。\n  - `lang`: 切换 `Alconna` 使用的语言\n  - `switch`: 禁用/启用某个指令\n  - `with`: 针对具有多个子命令的指令，通过 `with` 在当前会话中载入命令头以节省输入。\n\n以最新版本为例 (v0.59), 本插件已支持 NoneBot 生态中几乎所有的适配器, 包括:\n\n| 协议名称                                                            | 路径                                 |\n| ------------------------------------------------------------------- | ------------------------------------ |\n| [OneBot 协议](https://onebot.dev/)                                  | adapters.onebot11, adapters.onebot12 |\n| [Telegram](https://core.telegram.org/bots/api)                      | adapters.telegram                    |\n| [飞书](https://open.feishu.cn/document/home/index)                  | adapters.feishu                      |\n| [GitHub](https://docs.github.com/en/developers/apps)                | adapters.github                      |\n| [QQ bot](https://github.com/nonebot/adapter-qq)                     | adapters.qq                          |\n| [钉钉](https://open.dingtalk.com/document/)                         | adapters.ding                        |\n| [Console](https://github.com/nonebot/adapter-console)               | adapters.console                     |\n| [开黑啦](https://developer.kookapp.cn/)                             | adapters.kook                        |\n| [Mirai](https://docs.mirai.mamoe.net/mirai-api-http/)               | adapters.mirai                       |\n| [Ntchat](https://github.com/JustUndertaker/adapter-ntchat)          | adapters.ntchat                      |\n| [MineCraft](https://github.com/17TheWord/nonebot-adapter-minecraft) | adapters.minecraft                   |\n| [Walle-Q](https://github.com/onebot-walle/nonebot_adapter_walleq)   | adapters.onebot12                    |\n| [Discord](https://github.com/nonebot/adapter-discord)               | adapters.discord                     |\n| [Red 协议](https://github.com/nonebot/adapter-red)                  | adapters.red                         |\n| [Satori](https://github.com/nonebot/adapter-satori)                 | adapters.satori                      |\n| [Dodo IM](https://github.com/nonebot/adapter-dodo)                  | adapters.dodo                        |\n| [Kritor](https://github.com/nonebot/adapter-kritor)                 | adapters.kritor                      |\n| [Tailchat](https://github.com/eya46/nonebot-adapter-tailchat)       | adapters.tailchat                    |\n| [Mail](https://github.com/mobyw/nonebot-adapter-mail)               | adapters.mail                        |\n| [微信公众号](https://github.com/YangRucheng/nonebot-adapter-wxmp)   | adapters.wxmp                        |\n| [黑盒语音](https://github.com/lclbm/adapter-heybox)                 | adapters.heybox                      |\n| [Milky](https://github.com/nonebot/adapter-milky)                   | adapters.milky                       |\n| [EFChat](https://github.com/molanp/nonebot_adapter_efchat)          | adapters.efchat                      |\n\n## 安装插件\n\n在使用前请先安装 `nonebot-plugin-alconna` 插件至项目环境中，可参考[获取商店插件](../../tutorial/store.mdx#安装插件)来了解并选择安装插件的方式。如：\n\n在**项目目录**下执行以下命令：\n\n<Tabs groupId=\"install\">\n<TabItem value=\"cli\" label=\"使用 nb-cli\">\n\n```shell\nnb plugin install nonebot-plugin-alconna\n```\n\n</TabItem>\n<TabItem value=\"pip\" label=\"使用 pip\">\n\n```shell\npip install nonebot-plugin-alconna\n```\n\n</TabItem>\n\n<TabItem value=\"pdm\" label=\"使用 pdm\">\n\n```shell\npdm add nonebot-plugin-alconna\n```\n\n</TabItem>\n</Tabs>\n\n## 导入插件\n\n由于 `nonebot-plugin-alconna` 作为插件，因此需要在使用前对其进行**加载**。使用 `require` 方法可轻松完成这一过程，可参考 [跨插件访问](../../advanced/requiring.md) 一节进行了解。\n\n```python\nfrom nonebot import require\n\nrequire(\"nonebot_plugin_alconna\")\n\nfrom nonebot_plugin_alconna import ...\n```\n\n## 使用插件\n\n在前面的[深入指南](../../appendices/session-control.mdx)中，我们已经得到了一个天气插件。\n现在我们将使用 `Alconna` 来改写这个插件。\n\n<details>\n  <summary>插件示例</summary>\n\n```python title=weather/__init__.py\nfrom nonebot import on_command\nfrom nonebot.rule import to_me\nfrom nonebot.matcher import Matcher\nfrom nonebot.adapters import Message\nfrom nonebot.params import CommandArg, ArgPlainText\n\nweather = on_command(\"天气\", rule=to_me(), aliases={\"weather\", \"天气预报\"})\n\n@weather.handle()\nasync def handle_function(matcher: Matcher, args: Message = CommandArg()):\n    if args.extract_plain_text():\n        matcher.set_arg(\"location\", args)\n\n@weather.got(\"location\", prompt=\"请输入地名\")\nasync def got_location(location: str = ArgPlainText()):\n    if location not in [\"北京\", \"上海\", \"广州\", \"深圳\"]:\n        await weather.reject(f\"你想查询的城市 {location} 暂不支持，请重新输入！\")\n    await weather.finish(f\"今天{location}的天气是...\")\n```\n\n</details>\n\n```python {5-9,13-15,17-18}\nfrom nonebot.rule import to_me\nfrom arclet.alconna import Alconna, Args\nfrom nonebot_plugin_alconna import Match, on_alconna\n\nweather = on_alconna(\n    Alconna(\"天气\", Args[\"location?\", str]),\n    aliases={\"weather\", \"天气预报\"},\n    rule=to_me(),\n)\n\n\n@weather.handle()\nasync def handle_function(location: Match[str]):\n    if location.available:\n        weather.set_path_arg(\"location\", location.result)\n\n@weather.got_path(\"location\", prompt=\"请输入地名\")\nasync def got_location(location: str):\n    if location not in [\"北京\", \"上海\", \"广州\", \"深圳\"]:\n        await weather.reject(f\"你想查询的城市 {location} 暂不支持，请重新输入！\")\n    await weather.finish(f\"今天{location}的天气是...\")\n```\n\n在上面的代码中，我们使用 `Alconna` 来解析命令，`on_alconna` 用来创建响应器，使用 `Match` 来获取解析结果。\n\n关于更多 `Alconna` 的使用方法，可参考 [Alconna 文档](https://arclet.top/tutorial/alconna)，\n或阅读 [Alconna 基本介绍](./command.md) 一节。\n\n关于更多 `on_alconna` 的使用方法，可参考 [插件文档](https://github.com/nonebot/plugin-alconna/blob/master/docs.md)，\n或阅读 [响应规则的使用](./matcher.mdx) 一节。\n\n## 交流与反馈\n\nQQ 交流群: [🔗 链接](https://jq.qq.com/?_wv=1027&k=PUPOnCSH)\n\n友链: [📚 文档](https://graiax.cn/guide/message_parser/alconna.html)\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/best-practice/alconna/_category_.json",
    "content": "{\n  \"label\": \"命令解析拓展\",\n  \"position\": 6\n}\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/best-practice/alconna/builtins.mdx",
    "content": "---\nsidebar_position: 7\ndescription: 内置组件\n---\n\nimport Messenger from \"@site/src/components/Messenger\";\n\n# 内置组件\n\n`nonebot_plugin_alconna` 插件提供了一系列内置组件以提升开发者和用户体验。\n\n## 内置插件\n\n类似于 Nonebot 本身提供的内置插件，`nonebot_plugin_alconna` 提供了多个内置插件。\n\n### 加载\n\n你可以用本插件的 `load_builtin_plugin(s)` 来加载它们：\n\n```python\nfrom nonebot_plugin_alconna import load_builtin_plugin, load_builtin_plugins\n\nload_builtin_plugins(\"echo\")\nload_builtin_plugins(\"help\", \"with\")\n```\n\n### 使用\n\n#### echo\n\n`echo` 插件能将用户发送的消息原样返回。\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/echo hello world!\" },\n    { position: \"left\", msg: \"hello world!\" },\n    { position: \"right\", msg: \"/echo [图片]\" },\n    { position: \"left\", msg: \"[图片]\" },\n  ]}\n/>\n\n#### help\n\n`help` 插件能列出所有 Alconna 指令。同时还能查询某个指令对应的插件信息。\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/帮助\" },\n    {\n      position: \"left\",\n      msg: \"# 当前可用的命令有:\\n 【0】/echo : echo 指令\\n 【1】/help : 显示所有命令帮助\\n# 输入'命令名 -h|--help' 查看特定命令的语法\",\n    },\n    { position: \"right\", msg: \"/help --plugin-info echo\" },\n    {\n      position: \"left\",\n      msg: \"插件名称: echo\\n插件标识: nonebot_plugin_alconna:echo\\n插件模块: nonebot-plugin-alconna\\n插件版本: 0.57.2\\n插件路径: nonebot_plugin_alconna.builtins.plugins.echo\",\n    },\n  ]}\n/>\n\nhelp 插件的帮助信息如下：\n\n```\n/help <query: str = -1>\n## 注释\n  query: 选择某条命令的id或者名称查看具体帮助\n显示所有命令帮助\n用法:\n可以使用 --hide 参数来显示隐藏命令，使用 -P 参数来显示命令所属插件名称\n\n可用的子命令有:\n* 是否列出命令所属命名空间\n  -N│--namespace│命名空间 [target: str]\n## 注释\n  target: 指定的命名空间\n  该子命令内可用的选项有:\n  * 列出所有命名空间\n    --list\n可用的选项有:\n* 查看指定页数的命令帮助\n  --page <index: int>\n* 查看命令所属插件的信息\n  -P│插件信息│--plugin-info\n* 是否列出隐藏命令\n  隐藏│-H│--hide\n```\n\n#### lang\n\n`lang` 插件能切换 i18n 的语言设置。\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/lang list\" },\n    {\n      position: \"left\",\n      msg: \"支持的语言列表:\\n * en-US\\n * zh-CN\",\n    },\n    { position: \"right\", msg: \"/lang switch en-US\" },\n    { position: \"left\", msg: \"Switch to 'en-US' successfully.\" },\n  ]}\n/>\n\nlang 插件的帮助信息如下：\n\n```\n/lang\ni18n配置相关功能\n\n可用的选项有:\n* 查看支持的语言列表\n  list [name: str]\n* 切换语言\n  switch [locale: str]\n```\n\n其中 `list` 选项可以查找某一插件下的语言支持情况 (例如 `/lang list nonebot_plugin_alconna`)。\n\n#### switch\n\n`switch` 插件能用来启用/禁用某个命令，其使用方法与 `help` 类似。\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/disable\" },\n    {\n      position: \"left\",\n      msg: \"【0】/echo : echo 指令\\n【1】/help : 显示所有命令帮助\\n【2】/lang : i18n配置相关功能\",\n    },\n    { position: \"right\", msg: \"/disable 0\" },\n    { position: \"left\", msg: \"已禁用 /echo\" },\n    { position: \"right\", msg: \"/echo 1234\" },\n    { position: \"right\", msg: \"/enable echo\" },\n    { position: \"left\", msg: \"已启用 /echo\" },\n    { position: \"right\", msg: \"/echo 1234\" },\n    { position: \"left\", msg: \"1234\" },\n  ]}\n/>\n\n#### with\n\n`with` 插件能在当前会话中设置一个局部命令前缀，以便于有多个子命令的指令使用。\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/with\" },\n    {\n      position: \"left\",\n      msg: \"当前群组未设置前缀\",\n    },\n    { position: \"right\", msg: \"/with lang\" },\n    { position: \"left\", msg: \"设置前缀成功\" },\n    { position: \"right\", msg: \"list\" },\n    {\n      position: \"left\",\n      msg: \"支持的语言列表:\\n * en-US\\n * zh-CN\",\n    },\n  ]}\n/>\n\nwith 插件的帮助信息如下：\n\n```\n.with [name: str]\nwith 指令\n用法:\n设置局部命令前缀\n\n可用的选项有:\n* 设置可能的生效时间\n  --expire│expire <time: datetime>\n* 取消当前前缀\n  unset│--unset\n\n快捷命令:\n'[.]局部前缀' => [.]with\n```\n\n### 配置\n\n内置插件也有其配置项，并且均以 `NBP_ALC` 开头。\n\n- `nbp_alc_echo_tome`: 是否让 `echo` 插件的消息经过 `to_me` 处理\n- `nbp_alc_page_size`: `help` 与 `switch` 插件的共同配置项，表示每页显示的命令数量\n- `nbp_alc_help_text`: `help` 指令的指令名，默认为 \"help\"\n- `nbp_alc_help_alias`: `help` 指令的别名，默认为 \"帮助\", \"命令帮助\"\n- `nbp_alc_help_all_alias`: `help` 指令显示隐藏指令时的别名，默认为 \"所有帮助\", \"所有命令帮助\"\n- `nbp_alc_switch_enable`: `switch` 插件的 `enable` 指令的指令名，默认为 \"enable\"\n- `nbp_alc_switch_enable_alias`: `switch` 插件的 `enable` 指令的别名，默认为 \"启用\", \"启用指令\"\n- `nbp_alc_switch_disable`: `switch` 插件的 `disable` 指令的指令名，默认为 \"disable\"\n- `nbp_alc_switch_disable_alias`: `switch` 插件的 `disable` 指令的别名，默认为 \"disable\", \"禁用\", \"禁用指令\"\n- `nbp_alc_with_text`: `with` 插件的指令名，默认为 \"with\"\n- `nbp_alc_with_alias`: `with` 插件的别名，默认为 \"局部前缀\"\n\n## 内置匹配拓展\n\n目前插件提供了 5 个内置的 `Extension`，它们在 `nonebot_plugin_alconna.builtins.extensions` 下：\n\n### ReplyRecordExtension\n\n`ReplyRecordExtension` 可将消息事件中的回复暂存在 extension 中，使得解析用的消息不带回复信息，同时可以在后续的处理中获取回复信息：\n\n```python\nfrom nonebot_plugin_alconna import MsgId, on_alconna\nfrom nonebot_plugin_alconna.builtins.extensions import ReplyRecordExtension\n\nmatcher = on_alconna(\"...\", extensions=[ReplyRecordExtension()])\n\n@matcher.handle()\nasync def handle(msg_id: MsgId, ext: ReplyRecordExtension):\n    if reply := ext.get_reply(msg_id):\n        ...\n    else:\n        ...\n```\n\n### ReplyMergeExtension\n\n`ReplyMergeExtension` 可将消息事件中的回复指向的原消息合并到当前消息中作为一部分参数：\n\n```python\nfrom nonebot_plugin_alconna import Match, on_alconna\nfrom nonebot_plugin_alconna.builtins.extensions.reply import ReplyMergeExtension\n\nmatcher = on_alconna(\"...\", extensions=[ReplyMergeExtension()])\n\n@matcher.handle()\nasync def handle(content: Match[str]):\n    ...\n```\n\n其构造时可传入两个参数:\n\n- `add_left`: 否在当前消息的左侧合并回复消息，默认为 False\n- `sep`: 合并时的分隔符，默认为空格\n\n### DiscordSlashExtension\n\n`DiscordSlashExtension` 可自动将 Alconna 对象翻译成 Discord 的 slash 指令并注册，且将收到的指令交互事件转为指令供命令解析：\n\n```python\nfrom nonebot_plugin_alconna import Match, on_alconna\nfrom nonebot_plugin_alconna.builtins.extensions.discord import DiscordSlashExtension\n\n\nalc = Alconna(\n    [\"/\"],\n    \"permission\",\n    Subcommand(\"add\", Args[\"plugin\", str][\"priority?\", int]),\n    Option(\"remove\", Args[\"plugin\", str][\"time?\", int]),\n    meta=CommandMeta(description=\"权限管理\"),\n)\n\nmatcher = on_alconna(alc, extensions=[DiscordSlashExtension()])\n\n@matcher.assign(\"add\")\nasync def add(plugin: Match[str], priority: Match[int], ext: DiscordSlashExtension):\n    await ext.send_followup_msg(f\"added {plugin.result} with {priority.result if priority.available else 0}\")\n\n@matcher.assign(\"remove\")\nasync def remove(plugin: Match[str], time: Match[int]):\n    await matcher.finish(f\"removed {plugin.result} with {time.result if time.available else -1}\")\n```\n\n### MarkdownOutputExtension\n\n`MarkdownOutputExtension` 可将 Alconna 的自动输出转换为 Markdown 格式\n\n其构造时可传入两个参数:\n\n- `escape_dot`: 是否转义句中的点号（用来避免被识别为 url）\n- `text_to_image` 将文本转换为图片的函数，可不传入。一般用来设置渲染 markdown 为图片的函数\n\n### TelegramSlashExtension\n\n`TelegramSlashExtension` 可将 Alconna 的命令注册在 Telegram 上以获得提示，类似于 `DiscordSlashExtension`。\n\n```python\nfrom nonebot_plugin_alconna import on_alconna\nfrom nonebot.adapters.telegram.model import BotCommandScopeChat\nfrom nonebot_plugin_alconna.builtins.extensions.telegram import TelegramSlashExtension\n\nTelegramSlashExtension.set_scope(BotCommandScopeChat())\n\nmatcher = on_alconna(\"...\", extensions=[TelegramSlashExtension()])\n```\n\n## 内置自定义消息段\n\n目前插件提供了 3 个内置的 `Segment`，它们在 `nonebot_plugin_alconna.builtins.segments` 下：\n\n- `Markdown`: 可以传入 **markdown模板** 的元素\n- `MarketFace`: 特指 QQ 的商城表情\n- `MusicShare`: 特指 QQ 的音乐分享卡片\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/best-practice/alconna/command.md",
    "content": "---\nsidebar_position: 2\ndescription: Alconna 基本介绍\n---\n\n# Alconna 本体\n\n[`Alconna`](https://github.com/ArcletProject/Alconna) 隶属于 `ArcletProject`，是一个简单、灵活、高效的命令参数解析器, 并且不局限于解析命令式字符串。\n\n我们先通过一个例子来讲解 **Alconna** 的核心 —— `Args`, `Subcommand`, `Option`：\n\n```python\nfrom arclet.alconna import Alconna, Args, Subcommand, Option\n\n\nalc = Alconna(\n    \"pip\",\n    Subcommand(\n        \"install\",\n        Args[\"package\", str],\n        Option(\"-r|--requirement\", Args[\"file\", str]),\n        Option(\"-i|--index-url\", Args[\"url\", str]),\n    )\n)\n\nres = alc.parse(\"pip install nonebot2 -i URL\")\n\nprint(res)\n# 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'}\n\nprint(res.all_matched_args)\n# {'package': 'nonebot2', 'url': 'URL'}\n```\n\n这段代码通过`Alconna`创捷了一个接受主命令名为`pip`, 子命令为`install`且子命令接受一个 **Args** 参数`package`和二个 **Option** 参数`-r`和`-i`的命令参数解析器, 通过`parse`方法返回解析结果 **Arparma** 的实例。\n\n## 命令头\n\n命令头是指命令的前缀 (Prefix) 与命令名 (Command) 的组合，例如 !help 中的 ! 与 help。\n\n命令构造时, `Alconna([prefix], command)` 与 `Alconna(command, [prefix])` 是等价的。\n\n|             前缀             |   命令名   |                          匹配内容                           |       说明       |\n| :--------------------------: | :--------: | :---------------------------------------------------------: | :--------------: |\n|            不传入            |   \"foo\"    |                           `\"foo\"`                           | 无前缀的纯文字头 |\n|            不传入            |    123     |                            `123`                            |  无前缀的元素头  |\n|            不传入            | \"re:\\d{2}\" |                           `\"32\"`                            |  无前缀的正则头  |\n|            不传入            |    int     |                      `123` 或 `\"456\"`                       |  无前缀的类型头  |\n|         [int, bool]          |   不传入   |                       `True` 或 `123`                       |  无名的元素类头  |\n|        [\"foo\", \"bar\"]        |   不传入   |                     `\"foo\"` 或 `\"bar\"`                      |  无名的纯文字头  |\n|        [\"foo\", \"bar\"]        |   \"baz\"    |                  `\"foobaz\"` 或 `\"barbaz\"`                   |     纯文字头     |\n|         [int, bool]          |   \"foo\"    |             `[123, \"foo\"]` 或 `[False, \"foo\"]`              |      类型头      |\n|         [123, 4567]          |   \"foo\"    |              `[123, \"foo\"]` 或 `[4567, \"foo\"]`              |      元素头      |\n|      [nepattern.NUMBER]      |   \"bar\"    |            `[123, \"bar\"]` 或 `[123.456, \"bar\"]`             |     表达式头     |\n|         [123, \"foo\"]         |   \"bar\"    |      `[123, \"bar\"]` 或 `\"foobar\"` 或 `[\"foo\", \"bar\"]`       |      混合头      |\n| [(int, \"foo\"), (456, \"bar\")] |   \"baz\"    | `[123, \"foobaz\"]` 或 `[456, \"foobaz\"]` 或 `[456, \"barbaz\"]` |       对头       |\n\n对于无前缀的类型头，此时会将传入的值尝试转为 BasePattern，例如 `int` 会转为 `nepattern.INTEGER`。如此该命令头会匹配对应的类型， 例如 `int` 会匹配 `123` 或 `\"456\"`，但不会匹配 `\"foo\"`。解析后，Alconna 会将命令头匹配到的值转为对应的类型，例如 `int` 会将 `\"123\"` 转为 `123`。\n\n:::tip\n\n**正则内容只在命令名上生效，前缀中的正则会被转义**\n\n:::\n\n除了通过传入 `re:xxx` 来使用正则表达式外，Alconna 还提供了一种更加简洁的方式来使用正则表达式，称为 Bracket Header：\n\n```python\nalc = Alconna(\".rd{roll:int}\")\nassert alc.parse(\".rd123\").header[\"roll\"] == 123\n```\n\nBracket Header 类似 python 里的 f-string 写法，通过 `\"{}\"` 声明匹配类型\n\n`\"{}\"` 中的内容为 \"name:type or pat\"：\n\n- `\"{}\"`, `\"{:}\"` ⇔ `\"(.+)\"`, 占位符\n- `\"{foo}\"` ⇔ `\"(?P&lt;foo&gt;.+)\"`\n- `\"{:\\d+}\"` ⇔ `\"(\\d+)\"`\n- `\"{foo:int}\"` ⇔ `\"(?P&lt;foo&gt;\\d+)\"`，其中 `\"int\"` 部分若能转为 `BasePattern` 则读取里面的表达式\n\n## 参数声明(Args)\n\n`Args` 是用于声明命令参数的组件， 可以通过以下几种方式构造 **Args** ：\n\n- `Args[key, var, default][key1, var1, default1][...]`\n- `Args[(key, var, default)]`\n- `Args.key[var, default]`\n\n其中，key **一定**是字符串，而 var 一般为参数的类型，default 为具体的值或者 **arclet.alconna.args.Field**\n\n其与函数签名类似，但是允许含有默认值的参数在前；同时支持 keyword-only 参数不依照构造顺序传入 （但是仍需要在非 keyword-only 参数之后）。\n\n### key\n\n`key` 的作用是用以标记解析出来的参数并存放于 **Arparma** 中，以方便用户调用。\n\n其有三种为 Args 注解的标识符: `?`、`/`、 `!`, 标识符与 key 之间建议以 `;` 分隔：\n\n- `!` 标识符表示该处传入的参数应**不是**规定的类型，或**不在**指定的值中。\n- `?` 标识符表示该参数为**可选**参数，会在无参数匹配时跳过。\n- `/` 标识符表示该参数的类型注解需要隐藏。\n\n另外，对于参数的注释也可以标记在 `key` 中，其与 key 或者标识符 以 `#` 分割：  \n`foo#这是注释;?` 或 `foo?#这是注释`\n\n:::tip\n\n`Args` 中的 `key` 在实际命令中并不需要传入（keyword 参数除外）：\n\n```python\nfrom arclet.alconna import Alconna, Args\n\n\nalc = Alconna(\"test\", Args[\"foo\", str])\nalc.parse(\"test --foo abc\") # 错误\nalc.parse(\"test abc\") # 正确\n```\n\n若需要 `test --foo abc`，你应该使用 `Option`：\n\n```python\nfrom arclet.alconna import Alconna, Args, Option\n\n\nalc = Alconna(\"test\", Option(\"--foo\", Args[\"foo\", str]))\n```\n\n:::\n\n### var\n\nvar 负责命令参数的**类型检查**与**类型转化**\n\n`Args` 的`var`表面上看需要传入一个 `type`，但实际上它需要的是一个 `nepattern.BasePattern` 的实例：\n\n```python\nfrom arclet.alconna import Args\nfrom nepattern import BasePattern\n\n\n# 表示 foo 参数需要匹配一个 @number 样式的字符串\nargs = Args[\"foo\", BasePattern(\"@\\d+\")]\n```\n\n`pip` 示例中可以传入 `str` 是因为 `str` 已经注册在了 `nepattern.global_patterns` 中，因此会替换为 `nepattern.global_patterns[str]`\n\n`nepattern.global_patterns`默认支持的类型有：\n\n- `str`: 匹配任意字符串\n- `int`: 匹配整数\n- `float`: 匹配浮点数\n- `bool`: 匹配 `True` 与 `False` 以及他们小写形式\n- `hex`: 匹配 `0x` 开头的十六进制字符串\n- `url`: 匹配网址\n- `email`: 匹配 `xxxx@xxx` 的字符串\n- `ipv4`: 匹配 `xxx.xxx.xxx.xxx` 的字符串\n- `list`: 匹配类似 `[\"foo\",\"bar\",\"baz\"]` 的字符串\n- `dict`: 匹配类似 `{\"foo\":\"bar\",\"baz\":\"qux\"}` 的字符串\n- `datetime`: 传入一个 `datetime` 支持的格式字符串，或时间戳\n- `Any`: 匹配任意类型\n- `AnyString`: 匹配任意类型，转为 `str`\n- `Number`: 匹配 `int` 与 `float`，转为 `int`\n\n同时可以使用 typing 中的类型：\n\n- `Literal[X]`: 匹配其中的任意一个值\n- `Union[X, Y]`: 匹配其中的任意一个类型\n- `Optional[xxx]`: 会自动将默认值设为 `None`，并在解析失败时使用默认值\n- `List[X]`: 匹配一个列表，其中的元素为 `X` 类型\n- `Dict[X, Y]`: 匹配一个字典，其中的 key 为 `X` 类型，value 为 `Y` 类型\n- ...\n\n:::tip\n\n几类特殊的传入标记：\n\n- `\"foo\"`: 匹配字符串 \"foo\" (若没有某个 `BasePattern` 与之关联)\n- `RawStr(\"foo\")`: 匹配字符串 \"foo\" (即使有 `BasePattern` 与之关联也不会被替换)\n- `\"foo|bar|baz\"`: 匹配 \"foo\" 或 \"bar\" 或 \"baz\"\n- `[foo, bar, Baz, ...]`: 匹配其中的任意一个值或类型\n- `Callable[[X], Y]`: 匹配一个参数为 `X` 类型的值，并返回通过该函数调用得到的 `Y` 类型的值\n- `\"re:xxx\"`: 匹配一个正则表达式 `xxx`，会返回 Match[0]\n- `\"rep:xxx\"`: 匹配一个正则表达式 `xxx`，会返回 `re.Match` 对象\n- `{foo: bar, baz: qux}`: 匹配字典中的任意一个键, 并返回对应的值 (特殊的键 ... 会匹配任意的值)\n- ...\n\n**特别的**，你可以不传入 `var`，此时会使用 `key` 作为 `var`, 匹配 `key` 字符串。\n\n:::\n\n#### MultiVar 与 KeyWordVar\n\n`MultiVar` 是一个特殊的标注，用于告知解析器该参数可以接受多个值，类似于函数中的 `*args`，其构造方法形如 `MultiVar(str)`。\n\n同样的还有 `KeyWordVar`，类似于函数中的 `*, name: type`，其构造方法形如 `KeyWordVar(str)`，用于告知解析器该参数为一个 keyword-only 参数。\n\n:::tip\n\n`MultiVar` 与 `KeyWordVar` 组合时，代表该参数为一个可接受多个 key-value 的参数，类似于函数中的 `**kwargs`，其构造方法形如 `MultiVar(KeyWordVar(str))`\n\n`MultiVar` 与 `KeyWordVar` 也可以传入 `default` 参数，用于指定默认值\n\n`MultiVar` 不能在 `KeyWordVar` 之后传入\n\n:::\n\n#### AllParam\n\n`AllParam` 是一个特殊的标注，用于告知解析器该参数接收命令中在此位置之后的所有参数并**结束解析**，可以认为是**泛匹配参数**。\n\n`AllParam` 可直接使用 (`Args[\"xxx\", AllParam]`), 也可以传入指定的接收类型 (`Args[\"xxx\", AllParam(str)]`)。\n\n:::tip\n\n在 `nonebot_plugin_alconna` 下，`AllParam` 的返回值为 [`UniMessage`](./uniseg/message.mdx)\n\n:::\n\n### default\n\n`default` 传入的是该参数的默认值或者 `Field`，以携带对于该参数的更多信息。\n\n默认情况下 (即不声明) `default` 的值为特殊值 `Empty`。这也意味着你可以将默认值设置为 `None` 表示默认值为空值。\n\n`Field` 构造需要的参数说明如下：\n\n- default: 参数单元的默认值\n- alias: 参数单元默认值的别名\n- completion: 参数单元的补全说明生成函数\n- unmatch_tips: 参数单元的错误提示生成函数，其接收一个表示匹配失败的元素的参数\n- missing_tips: 参数单元的缺失提示生成函数\n\n## 选项与子命令(Option & Subcommand)\n\n`Option` 和 `Subcommand` 可以传入一组 `alias`，如 `Option(\"--foo|-F|--FOO|-f\")`，`Subcommand(\"foo\", alias=[\"F\"])`\n\n传入别名后，选项与子命令会选择其中长度最长的作为其名称。若传入为 \"--foo|-f\"，则命令名称为 \"--foo\"\n\n:::tip 特别提醒!!!\n\nOption 的名字或别名**没有要求**必须在前面写上 `-`\n\nOption 与 Subcommand 的唯一区别在于 Subcommand 可以传入自己的 **Option** 与 **Subcommand**\n\n:::\n\n他们拥有如下共同参数：\n\n- `help_text`: 传入该组件的帮助信息\n- `dest`: 被指定为解析完成时标注匹配结果的标识符，不传入时默认为选项或子命令的名称 (name)\n- `requires`: 一段指定顺序的字符串列表，作为唯一的前置序列与命令嵌套替换\n  对于命令 `test foo bar baz qux <a:int>` 来讲，因为`foo bar baz` 仅需要判断是否相等, 所以可以这么编写：\n\n```python\nAlconna(\"test\", Option(\"qux\", Args.a[int], requires=[\"foo\", \"bar\", \"baz\"]))\n```\n\n- `default`: 默认值，在该组件未被解析时使用使用该值替换。\n  特别的，使用 `OptionResult` 或 `SubcomanndResult` 可以设置包括参数字典在内的默认值：\n\n```python\nfrom arclet.alconna import Option, OptionResult\n\nopt1 = Option(\"--foo\", default=False)\nopt2 = Option(\"--foo\", default=OptionResult(value=False, args={\"bar\": 1}))\n```\n\n### Action\n\n`Option` 可以特别设置传入一类 `Action`，作为解析操作\n\n`Action` 分为三类：\n\n- `store`: 无 Args 时， 仅存储一个值， 默认为 Ellipsis； 有 Args 时， 后续的解析结果会覆盖之前的值\n- `append`: 无 Args 时， 将多个值存为列表， 默认为 Ellipsis； 有 Args 时， 每个解析结果会追加到列表中, 当存在默认值并且不为列表时， 会自动将默认值变成列表， 以保证追加的正确性\n- `count`: 无 Args 时， 计数器加一； 有 Args 时， 表现与 STORE 相同, 当存在默认值并且不为数字时， 会自动将默认值变成 1， 以保证计数器的正确性。\n\n`Alconna` 提供了预制的几类 `Action`：\n\n- `store`(默认)，`store_value`，`store_true`，`store_false`\n- `append`，`append_value`\n- `count`\n\n## 解析结果\n\n`Alconna.parse` 会返回由 **Arparma** 承载的解析结果\n\n`Arparma` 有如下属性：\n\n- 调试类\n  - matched: 是否匹配成功\n  - error_data: 解析失败时剩余的数据\n  - error_info: 解析失败时的异常内容\n  - origin: 原始命令，可以类型标注\n\n- 分析类\n  - header_match: 命令头部的解析结果，包括原始头部、解析后头部、解析结果与可能的正则匹配组\n  - main_args: 命令的主参数的解析结果\n  - options: 命令所有选项的解析结果\n  - subcommands: 命令所有子命令的解析结果\n  - other_args: 除主参数外的其他解析结果\n  - all_matched_args: 所有 Args 的解析结果\n\n### 路径查询\n\n`Arparma` 同时提供了便捷的查询方法 `query[type]()`，会根据传入的 `path` 查找参数并返回\n\n`path` 支持如下:\n\n- `main_args`, `options`, ...: 返回对应的属性\n- `args`: 返回 all_matched_args\n- `args.<key>`: 返回 all_matched_args 中 `key` 键对应的值\n- `main_args.<key>`: 返回主命令的解析参数字典中 `key` 键对应的值\n- `<node>`: 返回选项/子命令 `node` 的解析结果 (OptionResult | SubcommandResult)\n- `<node>.value`: 返回选项/子命令 `node` 的解析值\n- `<node>.args`: 返回选项/子命令 `node` 的解析参数字典\n- `<node>.<key>`, `<node>.args.<key>`: 返回选项/子命令 `node` 的参数字典中 `key` 键对应的值\n\n以及:\n\n- `options.<opt>`: 返回选项 `opt` 的解析结果 (OptionResult)\n- `options.<opt>.value`: 返回选项 `opt` 的解析值\n- `options.<opt>.args`: 返回选项 `opt` 的解析参数字典\n- `options.<opt>.<key>`, `options.<node>.args.<key>`: 返回选项 `opt` 的参数字典中 `key` 键对应的值\n- `subcommands.<subcmd>`: 返回子命令 `subcmd` 的解析结果 (SubcommandResult)\n- `subcommands.<subcmd>.value`: 返回子命令 `subcmd` 的解析值\n- `subcommands.<subcmd>.args`: 返回子命令 `subcmd` 的解析参数字典\n- `subcommands.<subcmd>.<key>`, `subcommands.<node>.args.<key>`: 返回子命令 `subcmd` 的参数字典中 `key` 键对应的值\n\n## 元数据(CommandMeta)\n\n`Alconna` 的元数据相当于其配置，拥有以下条目：\n\n- `description`: 命令的描述\n- `usage`: 命令的用法\n- `example`: 命令的使用样例\n- `author`: 命令的作者\n- `fuzzy_match`: 命令是否开启模糊匹配\n- `fuzzy_threshold`: 模糊匹配阈值\n- `raise_exception`: 命令是否抛出异常\n- `hide`: 命令是否对 manager 隐藏\n- `hide_shortcut`: 命令的快捷指令是否在 help 信息中隐藏\n- `keep_crlf`: 命令解析时是否保留换行字符\n- `compact`: 命令是否允许第一个参数紧随头部\n- `strict`: 命令是否严格匹配，若为 False 则未知参数将作为名为 $extra 的参数\n- `context_style`: 命令上下文插值的风格，None 为关闭，bracket 为 `{...}`，parentheses 为 `$(...)`\n- `extra`: 命令的自定义额外信息\n\n元数据一定使用 `meta=...` 形式传入：\n\n```python\nfrom arclet.alconna import Alconna, CommandMeta\n\nalc = Alconna(..., meta=CommandMeta(\"foo\", example=\"bar\"))\n```\n\n## 命名空间配置\n\n命名空间配置 （以下简称命名空间） 相当于 `Alconna` 的默认配置，其优先度低于 `CommandMeta`。\n\n`Alconna` 默认使用 \"Alconna\" 命名空间。\n\n命名空间有以下几个属性：\n\n- name: 命名空间名称\n- prefixes: 默认前缀配置\n- separators: 默认分隔符配置\n- formatter_type: 默认格式化器类型\n- fuzzy_match: 默认是否开启模糊匹配\n- raise_exception: 默认是否抛出异常\n- builtin_option_name: 默认的内置选项名称(--help, --shortcut, --comp)\n- disable_builtin_options: 默认禁用的内置选项(--help, --shortcut, --comp)\n- enable_message_cache: 默认是否启用消息缓存\n- compact: 默认是否开启紧凑模式\n- strict: 命令是否严格匹配\n- context_style: 命令上下文插值的风格\n- ...\n\n### 新建命名空间并替换\n\n```python\nfrom arclet.alconna import Alconna, namespace, Namespace, Subcommand, Args, config\n\n\nns = Namespace(\"foo\", prefixes=[\"/\"])  # 创建 \"foo\"命名空间配置, 它要求创建的Alconna的主命令前缀必须是/\n\nalc = Alconna(\"pip\", Subcommand(\"install\", Args[\"package\", str]), namespace=ns) # 在创建Alconna时候传入命名空间以替换默认命名空间\n\n# 可以通过with方式创建命名空间\nwith namespace(\"bar\") as np1:\n    np1.prefixes = [\"!\"]    # 以上下文管理器方式配置命名空间，此时配置会自动注入上下文内创建的命令\n    np1.formatter_type = ShellTextFormatter  # 设置此命名空间下的命令的 formatter 默认为 ShellTextFormatter\n    np1.builtin_option_name[\"help\"] = {\"帮助\", \"-h\"}  # 设置此命名空间下的命令的帮助选项名称\n\n# 你还可以使用config来管理所有命名空间并切换至任意命名空间\nconfig.namespaces[\"foo\"] = ns  # 将命名空间挂载到 config 上\n\nalc = Alconna(\"pip\", Subcommand(\"install\", Args[\"package\", str]), namespace=config.namespaces[\"foo\"]) # 也是同样可以切换到\"foo\"命名空间\n```\n\n### 修改默认的命名空间\n\n```python\nfrom arclet.alconna import config, namespace, Namespace\n\n\nconfig.default_namespace.prefixes = [...]  # 直接修改默认配置\n\nnp = Namespace(\"xxx\", prefixes=[...])\nconfig.default_namespace = np  # 更换默认的命名空间\n\nwith namespace(config.default_namespace.name) as np:\n    np.prefixes = [...]\n```\n\n## 快捷指令\n\n快捷命令可以做到标识一段命令, 并且传递参数给原命令\n\n一般情况下你可以通过 `Alconna.shortcut` 进行快捷指令操作 (创建，删除)\n\n`shortcut` 的第一个参数为快捷指令名称，第二个参数为 `ShortcutArgs`，作为快捷指令的配置：\n\n```python\nclass ShortcutArgs(TypedDict):\n    \"\"\"快捷指令参数\"\"\"\n\n    command: NotRequired[str]\n    \"\"\"快捷指令的命令\"\"\"\n    args: NotRequired[list[Any]]\n    \"\"\"快捷指令的附带参数\"\"\"\n    fuzzy: NotRequired[bool]\n    \"\"\"是否允许命令后随参数\"\"\"\n    prefix: NotRequired[bool]\n    \"\"\"是否调用时保留指令前缀\"\"\"\n    wrapper: NotRequired[ShortcutRegWrapper]\n    \"\"\"快捷指令的正则匹配结果的额外处理函数\"\"\"\n    humanized: NotRequired[str]\n    \"\"\"快捷指令的人类可读描述\"\"\"\n```\n\n### args的使用\n\n```python\nfrom arclet.alconna import Alconna, Args\n\n\nalc = Alconna(\"setu\", Args[\"count\", int])\n\nalc.shortcut(\"涩图(\\d+)张\", {\"args\": [\"{0}\"]})\n# 'Alconna::setu 的快捷指令: \"涩图(\\\\d+)张\" 添加成功'\n\nalc.parse(\"涩图3张\").query(\"count\")\n# 3\n```\n\n### command的使用\n\n```python\nfrom arclet.alconna import Alconna, Args\n\n\nalc = Alconna(\"eval\", Args[\"content\", str])\n\nalc.shortcut(\"echo\", {\"command\": \"eval print(\\\\'{*}\\\\')\"})\n# 'Alconna::eval 的快捷指令: \"echo\" 添加成功'\n\nalc.shortcut(\"echo\", delete=True) # 删除快捷指令\n# 'Alconna::eval 的快捷指令: \"echo\" 删除成功'\n\n@alc.bind() # 绑定一个命令执行器, 若匹配成功则会传入参数, 自动执行命令执行器\ndef cb(content: str):\n    eval(content, {}, {})\n\nalc.parse('eval print(\\\\\"hello world\\\\\")')\n# hello world\n\nalc.parse(\"echo hello world!\")\n# hello world!\n```\n\n当 `fuzzy` 为 False 时，第一个例子中传入 `\"涩图1张 abc\"` 之类的快捷指令将视为解析失败\n\n快捷指令允许三类特殊的 placeholder：\n\n- `{%X}`: 如 `setu {%0}`，表示此处填入快捷指令后随的第 X 个参数。\n\n例如，若快捷指令为 `涩图`, 配置为 `{\"command\": \"setu {%0}\"}`, 则指令 `涩图 1` 相当于 `setu 1`\n\n- `{*}`: 表示此处填入所有后随参数，并且可以通过 `{*X}` 的方式指定组合参数之间的分隔符。\n\n- `{X}`: 表示此处填入可能的正则匹配的组：\n\n- 若 `command` 中存在匹配组 `(xxx)`，则 `{X}` 表示第 X 个匹配组的内容\n- 若 `command` 中存储匹配组 `(?P<xxx>...)`, 则 `{X}` 表示 **名字** 为 X 的匹配结果\n\n除此之外, 通过 **Alconna** 内置选项 `--shortcut` 可以动态操作快捷指令\n\n例如：\n\n- `cmd --shortcut <key> <cmd>` 来增加一个快捷指令\n- `cmd --shortcut list` 来列出当前指令的所有快捷指令\n- `cmd --shortcut delete key` 来删除一个快捷指令\n\n```python\nfrom arclet.alconna import Alconna, Args\n\n\nalc = Alconna(\"eval\", Args[\"content\", str])\n\nalc.shortcut(\"echo\", {\"command\": \"eval print(\\\\'{*}\\\\')\"})\n\nalc.parse(\"eval --shortcut list\")\n# 'echo'\n```\n\n## 紧凑命令\n\n`Alconna`, `Option` 与 `Subcommand` 可以设置 `compact=True` 使得解析命令时允许名称与后随参数之间没有分隔：\n\n```python\nfrom arclet.alconna import Alconna, Option, CommandMeta, Args\n\n\nalc = Alconna(\"test\", Args[\"foo\", int], Option(\"BAR\", Args[\"baz\", str], compact=True), meta=CommandMeta(compact=True))\n\nassert alc.parse(\"test123 BARabc\").matched\n```\n\n这使得我们可以实现如下命令：\n\n```python\nfrom arclet.alconna import Alconna, Option, Args, append\n\n\nalc = Alconna(\"gcc\", Option(\"--flag|-F\", Args[\"content\", str], action=append, compact=True))\nprint(alc.parse(\"gcc -Fabc -Fdef -Fxyz\").query[list](\"flag.content\"))\n# ['abc', 'def', 'xyz']\n```\n\n当 `Option` 的 `action` 为 `count` 时，其自动支持 `compact` 特性：\n\n```python\nfrom arclet.alconna import Alconna, Option, count\n\n\nalc = Alconna(\"pp\", Option(\"--verbose|-v\", action=count, default=0))\nprint(alc.parse(\"pp -vvv\").query[int](\"verbose.value\"))\n# 3\n```\n\n## 模糊匹配\n\n模糊匹配会应用在任意需要进行名称判断的地方，如 **命令名称**，**选项名称** 和 **参数名称** (如指定需要传入参数名称)。\n\n```python\nfrom arclet.alconna import Alconna, CommandMeta\n\n\nalc = Alconna(\"test_fuzzy\", meta=CommandMeta(fuzzy_match=True))\n\nalc.parse(\"test_fuzy\")\n# test_fuzy is not matched. Do you mean \"test_fuzzy\"?\n```\n\n## 半自动补全\n\n半自动补全为用户提供了推荐后续输入的功能\n\n补全默认通过 `--comp` 或 `-cp` 或 `?` 触发：（命名空间配置可修改名称）\n\n```python\nfrom arclet.alconna import Alconna, Args, Option\n\n\nalc = Alconna(\"test\", Args[\"abc\", int]) + Option(\"foo\") + Option(\"bar\")\nalc.parse(\"test --comp\")\n\n'''\noutput\n\n以下是建议的输入：\n* <abc: int>\n* --help\n* -h\n* -sct\n* --shortcut\n* foo\n* bar\n'''\n```\n\n## Duplication\n\n**Duplication** 用来提供更好的自动补全，类似于 **ArgParse** 的 **Namespace**\n\n普通情况下使用，需要利用到 **ArgsStub**、**OptionStub** 和 **SubcommandStub** 三个部分\n\n以pip为例，其对应的 Duplication 应如下构造:\n\n```python\nfrom arclet.alconna import Alconna, Args, Option, OptionResult, Duplication, SubcommandStub, Subcommand, count\n\n\nclass MyDup(Duplication):\n    verbose: OptionResult\n    install: SubcommandStub\n\n\nalc = Alconna(\n    \"pip\",\n    Subcommand(\n        \"install\",\n        Args[\"package\", str],\n        Option(\"-r|--requirement\", Args[\"file\", str]),\n        Option(\"-i|--index-url\", Args[\"url\", str]),\n    ),\n    Option(\"-v|--version\"),\n    Option(\"-v|--verbose\", action=count),\n)\n\nres = alc.parse(\"pip -v install ...\") # 不使用duplication获得的提示较少\nprint(res.query(\"install\"))\n# (value=Ellipsis args={'package': '...'} options={} subcommands={})\n\nresult = alc.parse(\"pip -v install ...\", duplication=MyDup)\nprint(result.install)\n# 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')\n```\n\n**Duplication** 也可以如 **Namespace** 一样直接标明参数名称和类型：\n\n```python\nfrom typing import Optional\nfrom arclet.alconna import Duplication\n\n\nclass MyDup(Duplication):\n    package: str\n    file: Optional[str] = None\n    url: Optional[str] = None\n```\n\n## 上下文插值\n\n当 `context_style` 条目被设置后，传入的命令中符合上下文插值的字段会被自动替换成当前上下文中的信息。\n\n上下文可以在 `parse` 中传入：\n\n```python\nfrom arclet.alconna import Alconna, Args, CommandMeta\n\nalc = Alconna(\"test\", Args[\"foo\", int], meta=CommandMeta(context_style=\"parentheses\"))\n\nalc.parse(\"test $(bar)\", {\"bar\": 123})\n# {\"foo\": 123}\n```\n\ncontext_style 的值分两种：\n\n- `\"bracket\"`: 插值格式为 `{...}`，例如 `{foo}`\n- `\"parentheses\"`: 插值格式为 `$(...)`，例如 `$(bar)`\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/best-practice/alconna/config.md",
    "content": "---\nsidebar_position: 4\ndescription: 配置项\n---\n\n# 配置项\n\n## alconna_auto_send_output\n\n- **类型**: `bool | None`\n- **默认值**: `None`\n\n是否全局启用输出信息自动发送，不启用则会在触发特殊内置选项后仍然将解析结果传递至响应器。\n\n## alconna_use_command_start\n\n- **类型**: `bool`\n- **默认值**: `False`\n\n是否读取 Nonebot 的配置项 `COMMAND_START` 来作为全局的 Alconna 命令前缀\n\n## alconna_global_completion\n\n- **类型**: [`CompConfig | None`](./matcher.mdx#补全会话)\n- **默认值**: `None`\n\n全局的补全会话配置 (不代表全局启用补全会话)。\n\n## alconna_use_origin\n\n- **类型**: `bool`\n- **默认值**: `False`\n\n是否全局使用原始消息 (即未经过 to_me 等处理的)，该选项会影响到 Alconna 的匹配行为。\n\n## alconna_use_command_sep\n\n- **类型**: `bool`\n- **默认值**: `False`\n\n是否读取 Nonebot 的配置项 `COMMAND_SEP` 来作为全局的 Alconna 命令分隔符。\n\n## alconna_global_extensions\n\n- **类型**: `list[str]`\n- **默认值**: `[]`\n\n全局加载的扩展，其读取路径以 . 分隔，如 `foo.bar.baz:DemoExtension`。\n\n对于内置扩展，路径为 `nonebot_plugin_alconna.builtins.extensions` 下的模块名，如 `ReplyMergeExtension`，可以使用 `@` 来缩写路径，\n如 `@reply:ReplyMergeExtension`。\n\n## alconna_context_style\n\n- **类型**: `Optional[Literal[\"bracket\", \"parentheses\"]]`\n- **默认值**: `None`\n\n全局命令上下文插值的风格，None 为关闭，bracket 为 `{...}`，parentheses 为 `$(...)`。\n\n## alconna_enable_saa_patch\n\n- **类型**: `bool`\n- **默认值**: `False`\n\n是否启用 SAA 补丁。\n\n## alconna_apply_filehost\n\n- **类型**: `bool`\n- **默认值**: `False`\n\n是否启用文件托管。\n\n## alconna_apply_fetch_targets\n\n- **类型**: `bool`\n- **默认值**: `False`\n\n是否启动时拉取一次[发送对象](./uniseg/utils.mdx#发送对象)列表。\n\n## alconna_builtin_plugins\n\n- **类型**: `set[str]`\n- **默认值**: `set()`\n\n需要加载的内置插件列表。\n\n## alconna_conflict_resolver\n\n- **类型**: `Literal[\"raise\", \"default\", \"ignore\", \"replace\"]`\n- **默认值**: `\"default\"`\n\n命令冲突解决策略，决定当不同插件之间或者同一插件之间存在两个以上相同的命令时的处理方式：\n\n- `default`: 默认处理方式，保留两个命令\n- `raise`: 抛出异常\n- `ignore`: 忽略较新的命令\n- `replace`: 替换较旧的命令\n\n## alconna_response_self\n\n- **类型**: `bool`\n- **默认值**: `False`\n\n是否让响应器处理由 bot 自身发送的消息。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/best-practice/alconna/matcher.mdx",
    "content": "---\nsidebar_position: 3\ndescription: 响应规则的使用\n---\n\nimport Messenger from \"@site/src/components/Messenger\";\nimport Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\n\n# `on_alconna` 响应器\n\n`nonebot_plugin_alconna` 插件本体的大部分功能都围绕着 `on_alconna` 响应器展开。\n\n该响应器类似于 `on_command`，基于 `Alconna` 解析器来解析命令。\n\n以下是一个简单的 `on_alconna` 响应器的例子：\n\n```python\nfrom nonebot_plugin_alconna import At, Image, Match, on_alconna\nfrom arclet.alconna import Args, Option, Alconna, MultiVar, Subcommand\n\n\nalc = Alconna(\n    \"role-group\",\n    Subcommand(\n        \"add|添加\",\n        Args[\"name\", str],\n        Option(\"member\", Args[\"target\", MultiVar(At)]),\n        dest=\"add\",\n        compact=True,\n    ),\n    Option(\"list\"),\n    Option(\"icon\", Args[\"icon\", Image])\n)\nrg = on_alconna(alc, use_command_start=True, aliases={\"角色组\"})\n\n\n@rg.assign(\"list\")\nasync def list_role_group():\n    img: bytes = await gen_role_group_list_image()\n    await rg.finish(Image(raw=img))\n\n@rg.assign(\"add\")\nasync def _(name: str, target: Match[tuple[At, ...]]):\n    group = await create_role_group(name)\n    if target.available:\n        ats: tuple[At, ...] = target.result\n        group.extend(member.target for member in ats)\n    await rg.finish(\"添加成功\")\n```\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/role-group list\" },\n    {\n      position: \"left\",\n      msg: \"[图片]\",\n    },\n    { position: \"right\", msg: \"/角色组 添加foo @bar @baz\" },\n    { position: \"left\", msg: \"添加成功\" },\n  ]}\n/>\n\n## 声明\n\n`on_alconna` 的参数如下:\n\n```python\ndef on_alconna(\n    command: Alconna | str,\n    rule: Rule | T_RuleChecker | None = None,\n    skip_for_unmatch: bool = True,\n    auto_send_output: bool | None = None,\n    aliases: set[str] | tuple[str, ...] | None = None,\n    comp_config: CompConfig | None = None,\n    extensions: list[type[Extension] | Extension] | None = None,\n    exclude_ext: list[type[Extension] | str] | None = None,\n    use_origin: bool | None = None,\n    use_cmd_start: bool | None = None,\n    use_cmd_sep: bool | None = None,\n    response_self: bool | None = None,\n    **kwargs: Any,\n) -> type[AlconnaMatcher]:\n    ...\n```\n\n- `command`: Alconna 命令或字符串，字符串将通过 `AlconnaFormat` 转换为 Alconna 命令\n- `rule`: 事件响应规则， 详见 [响应器规则](../../advanced/matcher.md#事件响应规则)\n- `skip_for_unmatch`: 是否在命令不匹配时跳过该响应, 默认为 `True`\n- `auto_send_output`: 是否自动发送输出信息并跳过该响应。\n  - `True`：自动发送输出信息并跳过该响应\n  - `False`：不自动发送输出信息，而是传递进行处理\n  - `None`：跟随全局配置项 `alconna_auto_send_output`，默认值为 `True`\n- `aliases`: 命令别名， 作用类似于 `on_command` 中的 aliases\n- `comp_config`: 补全会话配置， 不传入则不启用补全会话\n- `extensions`: 需要加载的匹配扩展, 可以是扩展类或扩展实例\n- `exclude_ext`: 需要排除的匹配扩展, 可以是扩展类或扩展的id\n- `use_origin`: 是否使用未经 to_me 等处理过的消息。`None` 时跟随全局配置项 `alconna_use_origin`，默认值为 `False`\n- `use_cmd_start`: 是否使用 COMMAND_START 作为命令前缀。`None` 时跟随全局配置项 `alconna_use_command_start`，默认值为 `False`\n- `use_cmd_sep`: 是否使用 COMMAND_SEP 作为命令分隔符。`None` 时跟随全局配置项 `alconna_use_command_sep`，默认值为 `False`\n- `response_self`: 是否响应自身消息。`None` 时跟随全局配置项 `alconna_response_self`，默认值为 `False`\n\n`on_alconna` 返回的是 `Matcher` 的子类 `AlconnaMatcher` ，其拓展了如下方法：\n\n- `.assign(path, value, or_not)`: 用于对包含多个选项/子命令的命令的分派处理\n- `.dispatch`: 同样的分派处理，但是是类似 `CommandGroup` 一样返回新的 `AlconnaMatcher`\n- `.got_path(path, prompt, middleware)`: 在 `got` 方法的基础上，会以 path 对应的参数为准，读取传入 message 的最后一个消息段并验证转换\n- `.got`, `send`, `reject`, ... : 拓展了 prompt 类型，即支持使用 `UniMessage` 作为 prompt\n- ...\n\n除了标准的创建方式，本插件也提供了 `funcommand` 和 `Command` 两种快捷方式来创建 `AlconnaMatcher`， 详见 [快捷方式](./shortcut.md)。\n\n## 依赖注入\n\n`AlconnaMatcher` 的特性之一是拓展了依赖注入的功能。\n\n### 注入模型\n\n插件提供了几种用来处理解析结果的模型：\n\n- `CommandResult`: 用于快捷访问命令解析结果\n  - `result (Arparma)`: 解析结果\n  - `source (Alconna)`: 源命令\n  - `matched (bool)`: 是否匹配\n  - `context (dict)`: 命令的上下文\n  - `output (str | None)`: 命令的输出\n- `Match`: 匹配项，表示参数是否存在于 `Arparma.all_matched_args` 内，可用 `Match.available` 判断是否匹配，`Match.result` 获取匹配的值\n  - `Match` 只能查找到 `Arparma.all_matched_args` 中的参数。对于特定选项/子命令的参数，需要使用 `Query` 来查询\n- `Query`: 查询项，表示参数是否可由 `Arparma.query` 查询并获得结果，可用 `Query.available` 判断是否查询成功，`Query.result` 获取查询结果\n  - `Query` 除了查询参数，也可以查询某个选项/子命令是否存在\n\n### 编写\n\n```python\nasync def handle(\n    result: CommandResult,\n    arp: Arparma,\n    dup: Duplication,\n    source: Alconna,\n    ext: Extension,\n    exts: SelectedExtensions,\n    abc: str,\n    foo: Match[str],\n    bar: Query[int] = Query(\"ttt.bar\", 0)\n):\n    ...\n```\n\n`AlconnaMatcher` 的依赖注入拓展支持以下情况：\n\n- `xxx: CommandResult`\n- `xxx: Arparma`：命令的[解析结果](./command.md#解析结果)\n- `xxx: Duplication`：命令的解析结果的 [`Duplication`](./command.md#duplication)\n- `xxx: Alconna`：命令的源命令\n- `<key>: Match[<type>]`：上述的匹配项，使用 `key` 作为查询路径\n- `xxx: Query[<type>] = Query(<path>, default)`：上述的查询项，必需声明默认值以设置查询路径 `path`\n  - 当用来查询选项/子命令是否存在时，可不写 `Query[<type>]`\n- `xxx: Extension`：当前 `AlconnaMatcher` 使用的指定类型的匹配扩展\n- `xxx: SelectedExtensions`：当前 `AlconnaMatcher` 使用的所有可用的匹配扩展\n- `<key>: <type>`: 其他情况\n  - 当 `key` 的名称是 \"ctx\" 或 \"context\" 并且类型为 `dict` 时，会注入命令的上下文\n  - 当 `key` 存在于命令的上下文中时，会注入对应的值\n  - 当 `key` 存在于 `Arparma` 的 `all_matched_args` 中时，会注入对应的值, 类似于 `Match` 的用法，但当该值不存在时将跳过响应器。\n  - 当 `key` 属于 `got_path` 的参数时，会注入对应的值\n  - 当 `key` 被某个 `Extension.before_catch` 确认为需要注入的参数时，会调用 `Extension.catch` 来注入对应的值\n\n:::note\n\n如果你更喜欢 Depends 式的依赖注入，`nonebot_plugin_alconna` 同时提供了一系列的依赖注入函数，他们包括：\n\n- `AlconnaResult`: `CommandResult` 类型的依赖注入函数\n- `AlconnaMatches`: `Arparma` 类型的依赖注入函数\n- `AlconnaDuplication`: `Duplication` 类型的依赖注入函数\n- `AlconnaMatch`: `Match` 类型的依赖注入函数，其能够额外传入一个 middleware 函数来处理得到的参数\n- `AlconnaQuery`: `Query` 类型的依赖注入函数，其能够额外传入一个 middleware 函数来处理得到的参数\n- `AlconnaExecResult`: 提供挂载在命令上的 callback 的返回结果 (`Dict[str, Any]`) 的依赖注入函数\n- `AlconnaExtension`: 提供指定类型的 `Extension` 的依赖注入函数\n\n:::\n\n示例：\n\n```python\nfrom nonebot import require\nrequire(\"nonebot_plugin_alconna\")\n\nfrom nonebot_plugin_alconna import AlconnaQuery, AlcResult, Match, Query, on_alconna\nfrom arclet.alconna import Alconna, Args, Option, Arparma\n\n\ntest = on_alconna(\n    Alconna(\n        \"test\",\n        Option(\"foo\", Args[\"bar\", int]),\n        Option(\"baz\", Args[\"qux\", bool, False])\n    )\n)\n\n@test.handle()\nasync def handle_test1(result: AlcResult):\n    await test.send(f\"matched: {result.matched}\")\n    await test.send(f\"maybe output: {result.output}\")\n\n@test.handle()\nasync def handle_test2(result: Arparma):\n    await test.send(f\"head result: {result.header_result}\")\n    await test.send(f\"args: {result.all_matched_args}\")\n\n@test.handle()\nasync def handle_test3(bar: Match[int]):\n    if bar.available:\n        await test.send(f\"foo={bar.result}\")\n\n@test.handle()\nasync def handle_test4(qux: Query[bool] = AlconnaQuery(\"baz.qux\", False)):\n    if qux.available:\n        await test.send(f\"baz.qux={qux.result}\")\n```\n\n## 条件控制\n\n### `assign` 方法\n\n`AlconnaMatcher` 的 `assign` 方法与 `handle` 类似，但是可以控制响应函数是否在不满足条件时跳过响应。\n\n`assign` 方法的参数如下：\n\n```python\ndef assign(\n    cls,\n    path: str,\n    value: Any = _seminal,\n    or_not: bool = False,\n    additional: CHECK | None = None,\n    parameterless: Iterable[Any] | None = None,\n):\n    ...\n```\n\n- `path`: 指定的[查询路径](./command.md#路径查询)\n  - \"$main\" 表示没有任何选项/子命令匹配的时候\n  - \"\\~XX\" 时会把 \"\\~\" 替换为父级路径\n- `value`: 可能的指定查询值\n- `or_not`: 是否同时处理没有查询成功的情况\n- `additional`: 额外的条件检查函数\n\n例如：\n\n```python\n# 处理没有任何选项/子命令匹配的情况\n@rg.assign(\"$main\")\nasync def handle_main(): ...\n\n# 处理 list 选项\n@rg.assign(\"list\")\nasync def handle_list(): ...\n\n# 处理 add 选项，且 name 为 admin\n@rg.assign(\"add.name\", \"admin\")\nasync def handle_add_admin(): ...\n```\n\n### `dispatch` 方法\n\n此外，使用 `.dispatch` 还能像 `CommandGroup` 一样为每个分发设置独立的 matcher：\n\n```python\nrg_list_cmd = rg.dispatch(\"list\")\n\n@rg_list_cmd.handle()\nasync def handle_list(): ...\n```\n\n`dispatch` 的参数与 `assign` 相同。\n\n当使用 `dispatch` 时，父级路径表示为传入 `dispatch` 的 `path`:\n\n```python\nrg_add_cmd = rg.dispatch(\"add\")\n\n# 此时 ~name 表示 add.name\n@rg_add_cmd.assign(\"~name\", \"admin\")\nasync def handle_add_admin(): ...\n```\n\n:::tip\n\n在 `dispatch` 下, `Query` 的 `path` 也同样支持 `~` 前缀来表示父级路径\n\n```python\n@rg_add_cmd.assign(\"~name\", \"admin\")\nasync def handle_add_admin(target: Query[tuple[At, ...]] = Query(\"~target\")):\n    if target.available:\n        await rg.send(f\"添加成功: {target.result}\")\n```\n\n:::\n\n### `got_path` 方法\n\n另外，`AlconnaMatcher` 有类似于 [`got`](../../appendices/session-control.mdx#got) 的 `got_path` 与配套的 `get_path_arg`, `set_path_arg`：\n\n```python\nfrom nonebot_plugin_alconna import At, Match, UniMessage, on_alconna\n\n\ntest_cmd = on_alconna(Alconna(\"test\", Args[\"target?\", Union[str, At]]))\n\n@test_cmd.handle()\nasync def tt_h(target: Match[Union[str, At]]):\n    if target.available:\n        test_cmd.set_path_arg(\"target\", target.result)\n\n@test_cmd.got_path(\"target\", prompt=\"请输入目标\")\nasync def tt(target: Union[str, At]):\n    await test_cmd.send(UniMessage([\"ok\\n\", target]))\n```\n\n`got_path` 与 `assign`，`Match`，`Query` 等地方一样，都需要指明 `path` 参数 (即对应 Arg 验证的路径)\n\n`got_path` 会获取消息的最后一个消息段并转为 path 对应的类型，例如示例中 `target` 对应的 Arg 里要求 str 或 At，则 got 后用户输入的消息只有为 text 或 at 才能进入处理函数。\n\n`got_path` 中可以使用依赖注入函数 `AlconnaArg`, 类似于 [`Arg`](../../advanced/dependency.mdx#arg).\n\n### `prompt` 方法\n\n基于 [`Waiter`](https://github.com/RF-Tar-Railt/nonebot-plugin-waiter) 插件，`AlconnaMatcher` 提供了 `prompt` 方法来实现更灵活的交互式提示。\n\n```python\nfrom nonebot_plugin_alconna import At, Match, UniMessage, on_alconna\n\n\ntest_cmd = on_alconna(Alconna(\"test\", Args[\"target?\", Union[str, At]]))\n\n@test_cmd.handle()\nasync def tt_h(target: Match[Union[str, At]]):\n    if target.available:\n        await test_cmd.finish(UniMessage([\"ok\\n\", target]))\n    resp = await test_cmd.prompt(\"请输入目标\", timeout=30)  # 等待 30 秒\n    if resp is None:\n        await test_cmd.finish(\"超时\")\n    await test_cmd.finish(UniMessage([\"ok\\n\", resp[-1]]))\n```\n\n## 返回值中间件\n\n在 `AlconnaMatch`，`AlconnaQuery` 或 `got_path` 中，你可以使用 `middleware` 参数来传入一个对返回值进行处理的函数：\n\n```python\nfrom nonebot_plugin_alconna import image_fetch\n\n\nmask_cmd = on_alconna(Alconna(\"search\", Args[\"img?\", Image]))\n\n\n@mask_cmd.handle()\nasync def mask_h(matcher: AlconnaMatcher, img: Match[bytes] = AlconnaMatch(\"img\", image_fetch)):\n    result = await search_img(img.result)\n    await matcher.send(result.content)\n```\n\n其中，`image_fetch` 是一个中间件，其接受一个 `Image` 对象，并提取图片的二进制数据返回。\n\n## i18n\n\n本插件基于 `tarina.lang` 模块提供了 i18n 的支持，参见 [Lang 用法](https://github.com/nonebot/plugin-alconna/discussions/50)。\n\n当你编写完语言文件后，你便可以通过 `AlconnaMatcher.i18n` 来快速地将语言文件中的内容转为 UniMessage.\n\n<Tabs groupId=\"i18n\">\n<TabItem value=\"zh\" label=\"中文\">\n\n```yaml title=\"zh-CN.yml\"\n# 中文语言文件\ndemo:\n  command:\n    role-group:\n      add: 添加 {name} 成功!\n```\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/角色组 添加 foo\" },\n    { position: \"left\", msg: \"添加 foo 成功!\" },\n  ]}\n/>\n\n</TabItem>\n<TabItem value=\"en\" label=\"英文\">\n\n```yaml title=\"en-US.yml\"\n# 英文语言文件\ndemo:\n  command:\n    role-group:\n      add: Add {name} successfully!\n```\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/role-group add foo\" },\n    { position: \"left\", msg: \"Add foo successfully!\" },\n  ]}\n/>\n</TabItem>\n</Tabs>\n\n```python title=\"使用 i18n\"\n@rg.assign(\"add\")\nasync def handle_add(name: str):\n    await rg.i18n(\"demo\", \"command.role-group.add\", name=name).finish()\n```\n\n## 匹配测试\n\n`AlconnaMatcher.test` 方法允许你在 NoneBot 启动时对命令进行测试。\n\n```python\ndef test(\n    cls,\n    message: str | UniMessage,\n    expected: dict[str, Any] | None = None,\n    prefix: bool = True\n): ...\n```\n\n- `message`: 测试的消息\n- `expected`: 预期的解析结果，若为 None 则表示只测试是否匹配\n- `prefix`: 是否使用命令前缀，默认为 True\n\n## 匹配拓展\n\n本插件提供了一个 `Extension` 类，其用于自定义 AlconnaMatcher 的部分行为\n\n目前 `Extension` 的功能有：\n\n- `validate`: 对于事件的来源适配器或 bot 选择是否接受响应\n- `output_converter`: 输出信息的自定义转换方法\n- `message_provider`: 从传入事件中自定义提取消息的方法\n- `receive_provider`: 对传入的消息 (UniMessage) 的额外处理\n- `context_provider`: 对命令上下文的额外处理\n- `permission_check`: 命令对消息解析并确认头部匹配（即确认选择响应）时对发送者的权限判断\n- `parse_wrapper`: 对命令解析结果的额外处理\n- `send_wrapper`: 对发送的消息 (Message 或 UniMessage) 的额外处理\n- `before_catch`: 自定义依赖注入的绑定确认函数\n- `catch`: 自定义依赖注入处理函数\n- `post_init`: 响应器创建后对命令对象的额外处理\n\n:::tip\n\nExtension 可以通过 `add_global_extension` 方法来全局添加。\n\n```python\nfrom nonebot_plugin_alconna import add_global_extension\nfrom nonebot_plugin_alconna.builtins.extensions.telegram import TelegramSlashExtension\n\nadd_global_extension(TelegramSlashExtension)\n```\n\n全局的 Extension 可延迟加载 (即若有全局拓展加载于部分 AlconnaMatcher 之后，这部分响应器会被追加拓展)\n\n:::\n\n例如一个 `LLMExtension` 可以如下实现 (仅举例)：\n\n```python\nfrom nonebot_plugin_alconna import Extension, Alconna, on_alconna, Interface\n\n\nclass LLMExtension(Extension):\n    @property\n    def priority(self) -> int:\n        return 10\n\n    @property\n    def id(self) -> str:\n        return \"LLMExtension\"\n\n    def __init__(self, llm):\n      self.llm = llm\n\n    def post_init(self, alc: Alconna) -> None:\n        self.llm.add_context(alc.command, alc.meta.description)\n\n    async def receive_wrapper(self, bot, event, receive):\n        resp = await self.llm.input(str(receive))\n        return receive.__class__(resp.content)\n\n    def before_catch(self, name, annotation, default):\n        return name == \"llm\"\n\n    def catch(self, interface: Interface):\n        if interface.name == \"llm\":\n            return self.llm\n\nmatcher = on_alconna(\n    Alconna(...),\n    extensions=[LLMExtension(LLM)]\n)\n...\n```\n\n那么添加了 `LLMExtension` 的响应器便能接受任何能通过 llm 翻译为具体命令的自然语言消息，同时可以在响应器中为所有 `llm` 参数注入模型变量。\n\n### validate\n\n```python\ndef validate(self, bot: Bot, event: Event) -> bool: ...\n```\n\n默认情况下，`validate` 方法会筛选 `event.get_type()` 为 `message` 的情况，表示接受消息事件。\n\n### output_converter\n\n```python\nasync def output_converter(self, output_type: OutputType, content: str) -> UniMessage: ...\n```\n\n依据输出信息的类型，将字符串转换为消息对象以便发送。\n\n其中 `OutputType` 为 \"help\", \"shortcut\", \"completion\", \"error\" 其中之一。\n\n该方法只会调用一次，即对于多个 Extension，选择优先级靠前且实现了该方法的 Extension。\n\n### message_provider\n\n```python\nasync def message_provider(\n    self, event: Event, state: T_State, bot: Bot, use_origin: bool = False\n) -> UniMessage | None:...\n```\n\n该方法用于从事件中提取消息，默认情况下会使用 `event.get_message()` 来获取消息。\n\n该方法可能会调用多次，即对于多个 Extension，选择优先级靠前且实现了该方法的 Extension，若调用的返回值不为 `None` 则作为结果。\n\n:::caution\n\n该方法的默认实现对结果 (UniMessage) 会进行缓存。`Extension` 的实现也应尽量实现缓存机制。\n\n:::\n\n### receive_provider\n\n```python\nasync def receive_provider(self, bot: Bot, event: Event, command: Alconna, receive: UniMessage) -> UniMessage: ...\n```\n\n该方法用于对传入的消息 (UniMessage) 进行额外处理，默认情况下会返回原始消息。\n\n该方法会调用多次，即对于多个 Extension，前一个 Extension 的返回值会作为下一个 Extension 的输入。\n\n### context_provider\n\n```python\nasync def context_provider(self, ctx: dict[str, Any], bot: Bot, event: Event, state: T_State) -> dict[str, Any]:\n```\n\n该方法用于提取命令上下文，默认情况下会返回 `ctx` 本身。\n\n该方法会调用多次，即对于多个 Extension，前一个 Extension 的返回值会作为下一个 Extension 的输入。\n\n### permission_check\n\n```python\nasync def permission_check(self, bot: Bot, event: Event, command: Alconna) -> bool: ...\n```\n\n该方法用于对发送者的权限进行检查，默认情况下会返回 `True`。\n\n该方法可能会调用多次，即对于多个 Extension，若调用的返回值不为 `True` 则结束判断。\n\n### parse_wrapper\n\n```python\nasync def parse_wrapper(self, bot: Bot, state: T_State, event: Event, res: Arparma) -> None: ...\n```\n\n该方法用于对命令解析结果进行额外处理。\n\n该方法会调用多次，即对于多个 Extension，会并发地调用该方法。\n\n### send_wrapper\n\n```python\nasync def send_wrapper(self, bot: Bot, event: Event, send: TMessage) -> TMessage: ...\n```\n\n该方法用于对 `AlconnaMatcher.send` 或 `UniMessage.send` 发送的消息 (str 或 Message 或 UniMessage) 进行额外处理，默认情况下会返回原始消息。\n\n该方法会调用多次，即对于多个 Extension，前一个 Extension 的返回值会作为下一个 Extension 的输入。\n\n由于需要保证输入与输出的类型一致，该方法内需要自行判断类型。\n\n### before_catch\n\n```python\ndef before_catch(self, name: str, annotation: type, default: Any) -> bool: ...\n```\n\n该方法用于响应函数中某个参数是否需要绑定到该 Extension 上。\n\n### catch\n\n```python\nasync def catch(self, interface: Interface) -> Any: ...\n```\n\n该方法用于注入经过 `before_catch` 确认的参数。其中 `Interface` 的定义为\n\n```python\nclass Interface(Generic[TE]):\n    event: TE\n    state: T_State\n    name: str\n    annotation: Any\n    default: Any\n```\n\n## 补全会话\n\n补全会话基于 [`半自动补全`](./command.md#半自动补全)，用于指令参数缺失或参数错误时给予交互式提示，类似于 `got-reject`：\n\n```python\nfrom nonebot_plugin_alconna import Alconna, Args, Field, At, on_alconna\n\nalc = Alconna(\n    \"添加教师\",\n    Args[\"name\", str, Field(completion=lambda: \"请输入姓名\")],\n    Args[\"phone\", int, Field(completion=lambda: \"请输入手机号\")],\n    Args[\"at\", [str, At], Field(completion=lambda: \"请输入教师号\")],\n)\n\ncmd = on_alconna(alc, comp_config={\"lite\": True}, skip_for_unmatch=False)\n\n@cmd.handle()\nasync def handle(result: Arparma):\n    cmd.finish(\"添加成功\")\n```\n\n此时，当用户输入 `添加教师` 时，会自动提示用户输入姓名，手机号和教师号，用户输入后会自动进入下一个提示：\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"添加教师\" },\n    { position: \"left\", msg: \"以下是建议的输入： \\n- name: 请输入姓名\" },\n    { position: \"right\", msg: \"foo\" },\n    { position: \"left\", msg: \"以下是建议的输入： \\n- phone: 请输入手机号\" },\n    { position: \"right\", msg: \"12345\" },\n    { position: \"left\", msg: \"以下是建议的输入： \\n- at: 请输入教师号\" },\n    { position: \"right\", msg: \"@me\" },\n    { position: \"left\", msg: \"添加成功\" },\n  ]}\n/>\n\n补全会话配置如下：\n\n```python\nclass CompConfig(TypedDict):\n    tab: NotRequired[str]\n    \"\"\"用于切换提示的指令的名称\"\"\"\n    enter: NotRequired[str]\n    \"\"\"用于输入提示的指令的名称\"\"\"\n    exit: NotRequired[str]\n    \"\"\"用于退出会话的指令的名称\"\"\"\n    timeout: NotRequired[int]\n    \"\"\"超时时间\"\"\"\n    hide_tabs: NotRequired[bool]\n    \"\"\"是否隐藏所有提示\"\"\"\n    hides: NotRequired[Set[Literal[\"tab\", \"enter\", \"exit\"]]]\n    \"\"\"隐藏的指令\"\"\"\n    disables: NotRequired[Set[Literal[\"tab\", \"enter\", \"exit\"]]]\n    \"\"\"禁用的指令\"\"\"\n    lite: NotRequired[bool]\n    \"\"\"是否使用简洁版本的补全会话（相当于同时配置 disables、hides、hide_tabs）\"\"\"\n    block: NotRequired[bool]\n    \"\"\"进行补全会话时是否阻塞响应器\"\"\"\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/best-practice/alconna/shortcut.md",
    "content": "---\nsidebar_position: 6\ndescription: 快捷方式\n---\n\n# 快捷方式声明\n\n针对 `Alconna` 编写对于入门开发者来说较为复杂的问题，本插件提供了一些快捷方式来简化开发者的工作。\n\n## 装饰器构造器\n\n本插件提供了一个 `funcommand` 装饰器, 其用于将一个接受任意参数， 返回 `str` 或 `Message` 或 `MessageSegment` 的函数转换为命令响应器：\n\n```python\nfrom nonebot_plugin_alconna import funcommand\n\n\n@funcommand()\nasync def echo(msg: str):\n    return msg\n```\n\n其等同于：\n\n```python\nfrom arclet.alconna import Alconna, Args\nfrom nonebot_plugin_alconna import on_alconna, AlconnaMatch, Match\n\n\necho = on_alconna(Alconna(\"echo\", Args[\"msg\", str]))\n\n@echo.handle()\nasync def echo_exit(msg: Match[str] = AlconnaMatch(\"msg\")):\n    await echo.finish(msg.result)\n\n```\n\n相比于 `on_alconna`， `funcommand` 增加了三个参数 `name`, `prefixes` 和 `description`。\n\n## 类 Koishi 构造器\n\n本插件提供了一个 `Command` 构造器，其基于 `arclet.alconna.tools` 中的 `AlconnaString`， 以类似 `Koishi` 中[注册命令](https://koishi.chat/zh-CN/guide/basic/command.html)的方式来构建一个 **AlconnaMatcher** ：\n\n```python\nfrom nonebot_plugin_alconna import Command, Arparma\n\n\nbook = (\n    Command(\"book\", \"测试\")\n    .option(\"writer\", \"-w <id:int>\")\n    .option(\"writer\", \"--anonymous\", {\"id\": 0})\n    .usage(\"book [-w <id:int> | --anonymous]\")\n    .shortcut(\"测试\", {\"args\": [\"--anonymous\"]})\n    .build()\n)\n\n@book.handle()\nasync def _(arp: Arparma):\n    await book.send(str(arp.options))\n```\n\n甚至，你可以设置 `action` 来设定响应行为：\n\n```python\nbook = (\n    Command(\"book\", \"测试\")\n    .option(\"writer\", \"-w <id:int>\")\n    .option(\"writer\", \"--anonymous\", {\"id\": 0})\n    .usage(\"book [-w <id:int> | --anonymous]\")\n    .shortcut(\"测试\", {\"args\": [\"--anonymous\"]})\n    .action(lambda options: str(options))  # 会自动通过 bot.send 发送\n    .build()\n)\n```\n\n### 参数类型\n\n`Command` 的参数类型也如 `koishi` 一样，**必选参数** 用尖括号包裹，**可选参数** 用方括号包裹:\n\n- `foo` 表示参数 `foo`, 类型为 Any\n- `foo:int` 表示参数 `foo`, 类型为 int\n- `foo:int=1` 表示参数 `foo`, 类型为 int, 默认值为 1\n- `...foo` 表示[泛匹配参数](command.md#allparam)\n- `foo:str+`, `foo:str*` 表示[变长参数](command.md#multivar-与-keywordvar) `foo`, 类型为 str\n- `foo:+str`, `foo:text` 表示参数 `foo`, 类型为 str, 并且将包含空格 (即将变长参数的结果用空格合并)\n\n特别的，针对类型部分，本插件拓展了如下内容:\n\n- `foo:At`, `foo:Image`, ... 表示类型为[通用消息段](./uniseg/segment.md)\n- `foo:select(Image).first` 表示获取子元素类型\n- `foo:Dot(Image, 'url')` 表示类型为 `Image`，并且只获取 `url` 属性\n\n### 从文件加载\n\n`Command` 支持读取 `json` 或 `yaml` 文件来加载命令：\n\n```yml title=\"book.yml\"\ncommand: book\nhelp: 测试\noptions:\n  - name: writer\n    opt: \"-w <id:int>\"\n  - name: writer\n    opt: \"--anonymous\"\n    default:\n      id: 1\nusage: book [-w <id:int> | --anonymous]\nshortcuts:\n  - key: 测试\n    args: [\"--anonymous\"]\nactions:\n  - params: [\"options\"]\n    code: |\n      return str(options)\n```\n\n```python title=\"加载\"\nfrom nonebot_plugin_alconna import command_from_yaml\n\nbook = command_from_yaml(\"book.yml\")\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/best-practice/alconna/uniseg/README.md",
    "content": "# 通用消息组件\n\n`uniseg` 模块属于 `nonebot-plugin-alconna` 的子插件。\n\n通用消息组件内容较多，故分为了一个示例以及数个专题。\n\n## 示例\n\n### 导入\n\n一般情况下，你只需要从 `nonebot_plugin_alconna.uniseg` 中导入 `UniMessage` 即可:\n\n```python\nfrom nonebot_plugin_alconna.uniseg import UniMessage\n```\n\n### 构建\n\n你可以通过 `UniMessage` 上的快捷方法来链式构造消息:\n\n```python\nmessage = (\n    UniMessage.text(\"hello world\")\n    .at(\"1234567890\")\n    .image(url=\"https://example.com/image.png\")\n)\n```\n\n也可以通过导入通用消息段来构建消息:\n\n```python\nfrom nonebot_plugin_alconna import Text, At, Image, UniMessage\n\nmessage = UniMessage(\n    [\n        Text(\"hello world\"),\n        At(\"user\", \"1234567890\"),\n        Image(url=\"https://example.com/image.png\"),\n    ]\n)\n```\n\n更深入一点，比如你想要发送一条包含多个按钮的消息，你可以这样做:\n\n```python\nfrom nonebot_plugin_alconna import Button, UniMessage\n\nmessage = (\n    UniMessage.text(\"hello world\")\n    .keyboard(\n        Button(\"link1\", url=\"https://example.com/1\"),\n        Button(\"link2\", url=\"https://example.com/2\"),\n        Button(\"link3\", url=\"https://example.com/3\"),\n        row=3,\n    )\n)\n```\n\n### 发送\n\n你可以通过 `.send` 方法来发送消息:\n\n```python\n@matcher.handle()\nasync def _():\n    message = UniMessage.text(\"hello world\").image(url=\"https://example.com/image.png\")\n    await message.send()\n    # 类似于 `matcher.finish`\n    await message.finish()\n```\n\n你可以通过参数来让消息 @ 发送者:\n\n```python\n@matcher.handle()\nasync def _():\n    message = UniMessage.text(\"hello world\").image(url=\"https://example.com/image.png\")\n    await message.send(at_sender=True)\n```\n\n或者回复消息:\n\n```python\n@matcher.handle()\nasync def _():\n    message = UniMessage.text(\"hello world\").image(url=\"https://example.com/image.png\")\n    await message.send(reply_to=True)\n```\n\n### 撤回，编辑，表态\n\n你可以通过 `message_recall`, `message_edit` 和 `message_reaction` 方法来撤回，编辑和表态消息事件。\n\n```python\nfrom nonebot_plugin_alconna import message_recall, message_edit, message_reaction\n\n@matcher.handle()\nasync def _():\n    await message_edit(UniMessage.text(\"hello world\"))\n    await message_reaction(\"👍\")\n    await message_recall()\n```\n\n你也可以对你自己发送的消息进行撤回，编辑和表态:\n\n```python\n@matcher.handle()\nasync def _():\n    message = UniMessage.text(\"hello world\").image(url=\"https://example.com/image.png\")\n    receipt = await message.send()\n    await receipt.edit(UniMessage.text(\"hello world!\"))\n    await receipt.reaction(\"👍\")\n    await receipt.recall(delay=5)  # 5秒后撤回\n```\n\n### 处理消息\n\n通过依赖注入，你可以在事件处理器中获取通用消息:\n\n```python\nfrom nonebot_plugin_alconna import UniMsg\n\n@matcher.handle()\nasync def _(msg: UniMsg):\n    ...\n```\n\n然后你可以通过 `UniMessage` 的方法来处理消息.\n\n例如，你想知道消息中是否包含图片，你可以这样做:\n\n```python\nans1 = Image in message\nans2 = message.has(Image)\nans3 = message.only(Image)\n```\n\n或者，提取所有的图片：\n\n```python\nimgs_1 = message[Image]\nimgs_2 = message.get(Image)\nimgs_3 = message.include(Image)\nimgs_4 = message.select(Image)\nimgs_5 = message.filter(lambda x: x.type == \"image\")\nimgs_6 = message.tranform({\"image\": True})\n```\n\n而后，如果你想提取出所有的图片链接，你可以这样做:\n\n```python\nurls = imgs.map(lambda x: x.url)\n```\n\n如果你想知道消息是否符合某个前缀，你可以这样做:\n\n```python\n@matcher.handle()\nasync def _(msg: UniMsg):\n    if msg.startswith(\"hello\"):\n        await matcher.finish(\"hello world\")\n    else:\n        await matcher.finish(\"not hello world\")\n```\n\n或者你想接着去除掉前缀:\n\n```python\n@matcher.handle()\nasync def _(msg: UniMsg):\n    if msg.startswith(\"hello\"):\n        msg = msg.removeprefix(\"hello\")\n        await matcher.finish(msg)\n    else:\n        await matcher.finish(\"not hello world\")\n```\n\n### 持久化\n\n假设你在编写一个词库查询插件，你可以通过 `UniMessage.dump` 方法来将消息序列化为 JSON 格式:\n\n```python\nfrom nonebot_plugin_alconna import UniMsg\n\n@matcher.handle()\nasync def _(msg: UniMsg):\n    data: list[dict] = msg.dump()\n    # 你可以将 data 存储到数据库或者 JSON 文件中\n```\n\n而后你可以通过 `UniMessage.load` 方法来将 JSON 格式的消息反序列化为 `UniMessage` 对象:\n\n```python\nfrom nonebot_plugin_alconna import UniMessage\n\n@matcher.handle()\nasync def _():\n    data = [\n        {\"type\": \"text\", \"text\": \"hello world\"},\n        {\"type\": \"image\", \"url\": \"https://example.com/image.png\"},\n    ]\n    message = UniMessage.load(data)\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/best-practice/alconna/uniseg/_category_.json",
    "content": "{\n  \"label\": \"通用消息组件\",\n  \"position\": 5\n}\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/best-practice/alconna/uniseg/message.mdx",
    "content": "---\nsidebar_position: 3\ndescription: 消息序列\n---\n\nimport Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\n\n# 通用消息序列\n\n`uniseg` 提供了一个类似于 `Message` 的 `UniMessage` 类型，其元素为[通用消息段](./segment.md)。\n\n你可以用如下方式获取 `UniMessage`：\n\n<Tabs groupId=\"get_unimsg\">\n<TabItem value=\"depend\" label=\"使用依赖注入\">\n\n通过提供的 `UniversalMessage` 或基于 [`Annotated` 支持](https://github.com/nonebot/nonebot2/pull/1832)的 `UniMsg` 依赖注入器来获取 `UniMessage`。\n\n```python\nfrom nonebot_plugin_alconna.uniseg import UniMsg, At, Text\n\n\nmatcher = on_xxx(...)\n\n@matcher.handle()\nasync def _(msg: UniMsg):\n    text = msg[Text, 0]\n    print(text.text)\n    if msg.has(At):\n        ats = msg.get(At)\n        print(ats)\n    ...\n```\n\n</TabItem>\n<TabItem value=\"method\" label=\"使用 UniMessage.generate\">\n\n注意，`generate` 方法在响应器以外的地方如果不传入 `event` 与 `bot` 则无法处理 reply。\n\n```python\nfrom nonebot import Message, EventMessage\nfrom nonebot_plugin_alconna.uniseg import UniMessage\n\n\nmatcher = on_xxx(...)\n\n@matcher.handle()\nasync def _(message: Message = EventMessage()):\n    msg = await UniMessage.generate(message=message)\n    msg1 = UniMessage.generate_without_reply(message=message)\n```\n\n</TabItem>\n</Tabs>\n\n## 发送消息\n\n你还可以通过 `UniMessage` 的 `export` 与 `send` 方法来**跨平台发送消息**。\n\n`UniMessage.export` 会通过传入的 `bot: Bot` 参数，或上下文中的 `Bot` 对象读取适配器信息，并使用对应的生成方法把通用消息转为适配器对应的消息序列：\n\n```python\nfrom nonebot import Bot, on_command\nfrom nonebot_plugin_alconna.uniseg import Image, UniMessage\n\n\ntest = on_command(\"test\")\n\n@test.handle()\nasync def handle_test():\n    await test.send(await UniMessage(Image(path=\"path/to/img\")).export())\n```\n\n除此之外 `UniMessage.send`, `.finish` 方法基于 `UniMessage.export` 并调用各适配器下的发送消息方法，返回一个 `Receipt` 对象，用于修改/撤回/表态消息：\n\n```python\nfrom nonebot import Bot, on_command\nfrom nonebot_plugin_alconna.uniseg import UniMessage\n\n\ntest = on_command(\"test\")\n\n@test.handle()\nasync def handle():\n    receipt = await UniMessage.text(\"hello!\").send(at_sender=True, reply_to=True)\n    await receipt.recall(delay=1)\n```\n\n`UniMessage.send` 的定义如下：\n\n```python\nasync def send(\n    self,\n    target: Event | Target | None = None,\n    bot: Bot | None = None,\n    fallback: bool | FallbackStrategy = FallbackStrategy.rollback,\n    at_sender: str | bool = False,\n    reply_to: str | bool | Reply | None = False,\n    **kwargs: Any,\n) -> Receipt:\n    ...\n```\n\n- `target`: 发送目标，支持事件和[发送对象](./utils.mdx#发送对象)，不传入时会尝试从响应器上下文中获取。\n- `bot`: 发送消息使用的 Bot 对象，若不传入则会尝试从响应器上下文中获取。\n- `fallback`: [回退策略](#回退策略)。\n- `at_sender`: 是否提醒发送者，默认为 `False`。当类型为 `str` 时，表示指定用户的 id。\n- `reply_to`: 是否回复消息，默认为 `False`。\n  - `str` 表示消息 id。\n  - `bool` 表示是否回复当前消息。此时 `target` 不能是[发送对象](./utils.mdx#发送对象)。\n  - `Reply` 表示直接使用回复元素。\n- `**kwargs`: 各 `Bot.send` 的特定参数。\n\n而在 `AlconnaMatcher` 下，`got`, `send`, `reject` 等可以发送消息的方法皆支持使用 `UniMessage`，不需要手动调用 export 方法：\n\n```python\nfrom arclet.alconna import Alconna, Args\nfrom nonebot_plugin_alconna import Match, AlconnaMatcher, on_alconna\nfrom nonebot_plugin_alconna.uniseg import At,  UniMessage\n\n\ntest_cmd = on_alconna(Alconna(\"test\", Args[\"target?\", At]))\n\n@test_cmd.handle()\nasync def tt_h(matcher: AlconnaMatcher, target: Match[At]):\n    if target.available:\n        matcher.set_path_arg(\"target\", target.result)\n\n@test_cmd.got_path(\"target\", prompt=\"请输入目标\")\nasync def tt(target: At):\n    await test_cmd.send(UniMessage([target, \"\\ndone.\"]))\n```\n\n### 回退策略\n\n`send` 方法的 `fallback` 参数用于指定回退策略（即当前适配器不支持的消息段如何处理）：\n\n- `FallbackStrategy.ignore`: 忽略未转换的消息段\n- `FallbackStrategy.to_text`: 将未转换的消息段转为文本元素\n- `FallbackStrategy.rollback`: 从未转换消息段的子元素中提取可能的可发送消息段\n- `FallbackStrategy.forbid`: 抛出异常\n- `FallbackStrategy.auto`: 插件自动选择策略\n\n另外 `fallback` 传入 `bool` 时，`True` 等价于 `FallbackStrategy.auto`，`False` 等价于 `FallbackStrategy.forbid`。\n\n### 主动发送消息\n\n`UniMessage.send` 也可以用于主动发送消息：\n\n```python\nfrom nonebot_plugin_alconna.uniseg import UniMessage, Target, SupportScope\nfrom nonebot import get_driver\n\n\ndriver = get_driver()\n\n@driver.on_startup\nasync def on_startup():\n    target = Target(\"xxxx\", scope=SupportScope.qq_client)\n    await UniMessage(\"Hello!\").send(target=target)\n```\n\n:::caution\n\n在响应器以外的地方，除非启用了 `alconna_apply_fetch_targets` 配置项，否则 `bot` 参数必须手动传入。\n\n:::\n\n### Receipt 对象\n\n`send` 方法返回的 `Receipt` 对象可以用于修改/撤回/表态消息：\n\n```python\nasync def handle():\n    receipt = await UniMessage.text(\"hello!\").send(at_sender=True, reply_to=True)\n    await receipt.recall(delay=1)\n    recept1 = await UniMessage.text(\"hello!\").send(at_sender=True, reply_to=True)\n    await recept1.edit(\"world!\")\n```\n\n`Receipt` 对象拥有以下方法：\n\n- `recallable`: 表明是否可以撤回\n- `recall`: 撤回消息\n- `editable`: 表明是否可以修改\n- `edit`: 修改消息\n- `reactionable`: 表明是否可以表态\n- `reaction`: 表态消息\n- `get_reply`: 生成对已经发送的消息的回复元素\n- `send`, `finish`: 发送消息\n- `reply`: 回复已经发送的消息\n\n## 构造\n\n如同 `Message`, `UniMessage` 可以传入单个字符串/消息段，或可迭代的字符串/消息段：\n\n```python\nfrom nonebot_plugin_alconna.uniseg import UniMessage, At\n\n\nmsg = UniMessage(\"Hello\")\nmsg1 = UniMessage(At(\"user\", \"124\"))\nmsg2 = UniMessage([\"Hello\", At(\"user\", \"124\")])\n```\n\n`UniMessage` 上同时存在便捷方法，令其可以链式地添加消息段：\n\n```python\nfrom nonebot_plugin_alconna.uniseg import UniMessage, At, Image\n\n\nmsg = UniMessage.text(\"Hello\").at(\"124\").image(path=\"/path/to/img\")\nassert msg == UniMessage(\n    [\"Hello\", At(\"user\", \"124\"), Image(path=\"/path/to/img\")]\n)\n```\n\n### 使用消息模板\n\n`UniMessage.template` 同样类似于 `Message.template`，可以用于格式化消息，大体用法参考 [消息模板](../../../tutorial/message#使用消息模板)。\n\n这里额外说明 `UniMessage.template` 的拓展控制符\n\n相比 `Message`，UniMessage 对于 `{:XXX}` 做了另一类拓展。其能够识别例如 At(xxx, yyy) 或 Emoji(aaa, bbb)的字符串并执行\n\n以 At(...) 为例：\n\n```python title=使用通用消息段的拓展控制符\n>>> from nonebot_plugin_alconna.uniseg import UniMessage\n>>>  UniMessage.template(\"{:At(user, target)}\").format(target=\"123\")\nUniMessage(At(\"user\", \"123\"))\n>>> UniMessage.template(\"{:At(type=user, target=id)}\").format(id=\"123\")\nUniMessage(At(\"user\", \"123\"))\n>>> UniMessage.template(\"{:At(type=user, target=123)}\").format()\nUniMessage(At(\"user\", \"123\"))\n```\n\n而在 `AlconnaMatcher` 中，`{:XXX}` 更进一步地提供了获取 `event` 和 `bot` 中的属性的功能：\n\n```python title=在AlconnaMatcher中使用通用消息段的拓展控制符\nfrom arclet.alconna import Alconna, Args\nfrom nonebot_plugin_alconna import At, Match, UniMessage, AlconnaMatcher, on_alconna\n\n\ntest_cmd = on_alconna(Alconna(\"test\", Args[\"target?\", At]))\n\n@test_cmd.handle()\nasync def tt_h(matcher: AlconnaMatcher, target: Match[At]):\n    if target.available:\n        matcher.set_path_arg(\"target\", target.result)\n\n@test_cmd.got_path(\n    \"target\",\n    prompt=UniMessage.template(\"{:At(user, $event.get_user_id())} 请确认目标\")\n)\nasync def tt():\n    await test_cmd.send(\n      UniMessage.template(\"{:At(user, $event.get_user_id())} 已确认目标为 {target}\")\n    )\n```\n\n另外也有 `$message_id` 与 `$target` 两个特殊值。\n\n:::tip\n\n注意到上述代码中的 `{target}` 了吗？\n\n在 `AlconnaMatcher` 中，`UniMessage.template` 的格式化方法会自动将 `Arparma.all_matched_args`、 `state` 中的变量传入到 `format` 方法中，因此你可以直接使用上述变量。\n\n:::\n\n### 拼接消息\n\n`str`、`UniMessage`、`Segment` 对象之间可以直接相加，相加均会返回一个新的 `UniMessage` 对象：\n\n```python\n# 消息序列与消息段相加\nUniMessage(\"text\") + Text(\"text\")\n# 消息序列与字符串相加\nUniMessage([Text(\"text\")]) + \"text\"\n# 消息序列与消息序列相加\nUniMessage(\"text\") + UniMessage([Text(\"text\")])\n# 字符串与消息序列相加\n\"text\" + UniMessage([Text(\"text\")])\n# 消息段与消息段相加\nText(\"text\") + Text(\"text\")\n# 消息段与字符串相加\nText(\"text\") + \"text\"\n# 消息段与消息序列相加\nText(\"text\") + UniMessage([Text(\"text\")])\n# 字符串与消息段相加\n\"text\" + Text(\"text\")\n```\n\n如果需要在当前消息序列后直接拼接新的消息段，可以使用 `Message.append`、`Message.extend` 方法，或者使用自加：\n\n```python\nmsg = UniMessage([Text(\"text\")])\n# 自加\nmsg += \"text\"\nmsg += Text(\"text\")\nmsg += UniMessage([Text(\"text\")])\n# 附加\nmsg.append(Text(\"text\"))\n# 扩展\nmsg.extend([Text(\"text\")])\n```\n\n## 操作\n\n### 检查消息段\n\n我们可以通过 `in` 运算符或消息序列的 `has` 方法来：\n\n```python\n# 是否存在消息段\nAt(\"user\", \"1234\") in message\n# 是否存在指定类型的消息段\nAt in message\n```\n\n我们还可以使用 `only` 方法来检查消息中是否仅包含指定的消息段：\n\n```python\n# 是否都为 \"test\"\nmessage.only(\"test\")\n# 是否仅包含指定类型的消息段\nmessage.only(Text)\n```\n\n### 获取消息纯文本\n\n类似于 `Message.extract_plain_text()`，用于获取通用消息的纯文本：\n\n```python\n# 提取消息纯文本字符串\nassert UniMessage(\n    [At(\"user\", \"1234\"), \"text\"]\n).extract_plain_text() == \"text\"\n```\n\n### 遍历\n\n通用消息序列继承自 `List[Segment]` ，因此可以使用 `for` 循环遍历消息段：\n\n```python\nfor segment in message:  # type: Segment\n    ...\n```\n\n### 过滤、索引与切片\n\n消息序列对列表的索引与切片进行了增强，在原有列表 `int` 索引与 `slice` 切片的基础上，支持 `type` 过滤索引与切片：\n\n```python\nmessage = UniMessage(\n    [\n        Reply(...),\n        \"text1\",\n        At(\"user\", \"1234\"),\n        \"text2\"\n    ]\n)\n# 索引\nmessage[0] == Reply(...)\n# 切片\nmessage[0:2] == UniMessage([Reply(...), Text(\"text1\")])\n# 类型过滤\nmessage[At] == Message([At(\"user\", \"1234\")])\n# 类型索引\nmessage[At, 0] == At(\"user\", \"1234\")\n# 类型切片\nmessage[Text, 0:2] == UniMessage([Text(\"text1\"), Text(\"text2\")])\n```\n\n我们也可以通过消息序列的 `include`、`exclude` 方法进行类型过滤：\n\n```python\nmessage.include(Text, At)\nmessage.exclude(Reply)\n```\n\n或者使用 `filter` 方法：\n\n```python\nmessage.filter(lambda x: isinstance(x, At) and x.flag == \"user\")  # 仅保留 At(\"user\", xxx) 的消息段\n```\n\n同样的，消息序列对列表的 `index`、`count` 方法也进行了增强，可以用于索引指定类型的消息段：\n\n```python\n# 指定类型首个消息段索引\nmessage.index(Text) == 1\n# 指定类型消息段数量\nmessage.count(Text) == 2\n```\n\n此外，消息序列添加了一个 `get` 方法，可以用于获取指定类型指定个数的消息段：\n\n```python\n# 获取指定类型指定个数的消息段\nmessage.get(Text, 1) == UniMessage([Text(\"test1\")])\n```\n\n### 嵌套提取\n\n消息序列的 `select` 方法可以递归地从消息中选择指定类型的消息段：\n\n```python\nmessage = UniMessage(\n    [\n        Text(\"text1\"),\n        Image(url=\"url1\")(\n            Text(\"text2\"),\n        )\n    ]\n)\nassert message.select(Text) == UniMessage(\n    [\n        Text(\"text1\"),\n        Text(\"text2\")\n    ]\n)\n```\n\n### 转换\n\n消息序列的 `map` 方法可以简单地将消息段转换为指定类型的数据：\n\n```python\n# 转换消息段为另一类型的消息段，此时返回结果仍是 UniMessage\nmessage.map(lambda x: Text(x.target))  # 转换为 Text 消息段\n# 转换消息段为另一类型的数据，此时返回结果为 list[T]\nmessage.map(lambda x: x.target)  # 转换为 list[str]\n```\n\n在此之上，消息序列还提供了 `transform` 和 `transform_async` 方法，允许你传入转换规则，将消息段转换为另一类型的消息段，并返回一个新的消息序列：\n\n```python\nrule = {\n    \"text\": True,\n    \"at\": lambda attrs, children: Text(attrs[\"target\"])\n}\nmessage.transform(rule)\n```\n\n转换规则的类型一般为 `dict[str, Transformer]`，以消息元素类型的名称为键，定义方式如下：\n\n```typescript\ntype Fragment = Segment | Segment[];\ntype Render<T> = (attrs: dict, children: Segment[]) => T;\ntype Transformer = boolean | Fragment | Render<boolean | Fragment>;\n```\n\n### 字符串操作\n\n类似于 `str`，消息序列可以通过如下方法来操作消息内的文本部分：\n\n- `split`,\n- `replace`,\n- `startwith`, `endswith`,\n- `removeprefix`, `removesuffix`,\n- `strip`, `lstrip`, `rstrip`,\n\n```python\nmsg = UniMessage.text(\"foo bar\").at(\"1234\").text(\"baz qux\")\n# 分割，返回分割结果，类型为 list[UniMessage]\nparts = msg.split(\" \")\n# 替换，返回替换结果，类型为 UniMessage。新文本可以用 str 或 Text 来替换\nnew_msg = msg.replace(\"ba\", \"baaa\")\n# 前缀/后缀检查\nmsg.startswith(\"foo\")  # True\nmsg.endswith(\"qux\")  # True\n# 去除前缀/后缀\nmsg1 = msg.removeprefix(\"foo\")  # UniMessage([Text(\" bar\"), At(\"user\", \"1234\"), Text(\"baz qux\")])\nmsg2 = msg.removesuffix(\"qux\")  # UniMessage([Text(\"foo bar\"), At(\"user\", \"1234\"), Text(\"baz \")])\n# 去除空格\nmsg1 = msg1.lstrip()  # UniMessage([Text(\"bar\"), At(\"user\", \"1234\"), Text(\"baz qux\")])\nmsg2 = msg2.rstrip()  # UniMessage([Text(\"foo bar\"), At(\"user\", \"1234\"), Text(\"baz\")])\n```\n\n## 持久化\n\n特别的，`UniMessage` 还支持消息持久化，具体来说为 `dump` 与 `load` 方法：\n\n```python\nmsg = UniMessage.text(\"Hello\").image(url=\"url\")\ndata = msg.dump()  # [{\"type\": \"text\", \"text\": \"Hello\"}, {\"type\": \"image\", \"url\": \"url\"}]\n\nassert UniMessage.load(data) == msg\n```\n\n### dump\n\n`dump` 方法的定义如下：\n\n```python\ndef dump(self, media_save_dir: str | Path | bool | None = None, json: bool = False) -> str | list[dict[str, Any]]: ...\n```\n\n其中，`media_save_dir` 用于指定持久化的媒体文件存储目录:\n\n- 若 `media_save_dir` 为 str 或 Path，则会将媒体文件保存到指定目录下。\n- 若 `media_save_dir` 为 False，则不会保存媒体文件。\n- 若 `media_save_dir` 为 True，则会将文件数据转为 base64 编码。\n- 若不指定 `media_save_dir`，则会尝试导入 [`nonebot_plugin_localstore`](../../data-storing.md) 并使用其提供的路径。否则 (即 `localstore` 未安装)，将会尝试使用当前工作目录。\n\n### load\n\n`load` 方法的定义如下：\n\n```python\n@classmethod\ndef load(cls, data: str | list[dict[str, Any]]) -> UniMessage: ...\n```\n\n其中 `data` 应符合 JSON 格式。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/best-practice/alconna/uniseg/segment.md",
    "content": "---\nsidebar_position: 2\ndescription: 消息段\n---\n\n# 通用消息段\n\n通用消息段是对各适配器中的消息段的抽象总结。其可用于 Alconna 命令的参数定义，也可用于消息的构建和解析。\n\n```python\nfrom nonebot_plugin_alconna import Alconna, Args, Image, on_alconna\n\nmeme = on_alconna(Alconna(\"make_meme\", Args[\"name\", str][\"img\", Image]))\n\n@meme.handle()\nasync def _(img: Image):\n    ...\n```\n\n## 模型定义\n\n> **注意**: 本节的内容经过简化。实际情况以源码为准。\n\n```python\nclass Segment:\n    \"\"\"基类标注\"\"\"\n    @property\n    def type(self) -> str: ...\n    @property\n    def data(self) -> [str, Any]: ...\n    @property\n    def children(self) -> list[\"Segment\"]: ...\n\nclass Text(Segment):\n    \"\"\"Text对象, 表示一类文本元素\"\"\"\n    text: str\n    styles: dict[tuple[int, int], list[str]]\n\n    def cover(self, text: str): ...\n    def mark(self, start: Optional[int] = None, end: Optional[int] = None, *styles: str): ...\n\nclass At(Segment):\n    \"\"\"At对象, 表示一类提醒某用户的元素\"\"\"\n    flag: Literal[\"user\", \"role\", \"channel\"]\n    target: str\n    display: Optional[str]\n\nclass AtAll(Segment):\n    \"\"\"AtAll对象, 表示一类提醒所有人的元素\"\"\"\n    here: bool\n\nclass Emoji(Segment):\n    \"\"\"Emoji对象, 表示一类表情元素\"\"\"\n    id: str\n    name: Optional[str]\n\nclass Media(Segment):\n    id: Optional[str]\n    url: Optional[str]\n    path: Optional[Union[str, Path]]\n    raw: Optional[Union[bytes, BytesIO]]\n    mimetype: Optional[str]\n    name: str\n\n    to_url: ClassVar[Optional[MediaToUrl]]\n\nclass Image(Media):\n    \"\"\"Image对象, 表示一类图片元素\"\"\"\n    width: Optional[int]\n    height: Optional[int]\n\nclass Audio(Media):\n    \"\"\"Audio对象, 表示一类音频元素\"\"\"\n    duration: Optional[float]\n\nclass Voice(Media):\n    \"\"\"Voice对象, 表示一类语音元素\"\"\"\n    duration: Optional[float]\n\nclass Video(Media):\n    \"\"\"Video对象, 表示一类视频元素\"\"\"\n    thumbnail: Optional[Image]\n    duration: Optional[float]\n\nclass File(Media):\n    \"\"\"File对象, 表示一类文件元素\"\"\"\n\nclass Reply(Segment):\n    \"\"\"Reply对象，表示一类回复消息\"\"\"\n    id: str\n    \"\"\"此处不一定是消息ID，可能是其他ID，如消息序号等\"\"\"\n    msg: Optional[Union[Message, str]]\n    origin: Optional[Any]\n\nclass Reference(Segment):\n    \"\"\"Reference对象，表示一类引用消息。转发消息 (Forward) 也属于此类\"\"\"\n    id: Optional[str]\n    \"\"\"此处不一定是消息ID，可能是其他ID，如消息序号等\"\"\"\n    children: List[Union[RefNode, CustomNode]]\n\nclass Hyper(Segment):\n    \"\"\"Hyper对象，表示一类超级消息。如卡片消息、ark消息、小程序等\"\"\"\n    format: Literal[\"xml\", \"json\"]\n    raw: Optional[str]\n    content: Optional[Union[dict, list]]\n\nclass Reference(Segment):\n    \"\"\"Reference对象，表示一类引用消息。转发消息 (Forward) 也属于此类\"\"\"\n    id: Optional[str]\n    nodes: Sequence[Union[RefNode, CustomNode]]\n\nclass Button(Segment):\n    \"\"\"Button对象，表示一类按钮消息\"\"\"\n    flag: Literal[\"action\", \"link\", \"input\", \"enter\"]\n    \"\"\"\n    - 点击 action 类型的按钮时会触发一个关于 按钮回调 事件，该事件的 button 资源会包含上述 id\n    - 点击 link 类型的按钮时会打开一个链接或者小程序，该链接的地址为 `url`\n    - 点击 input 类型的按钮时会在用户的输入框中填充 `text`\n    - 点击 enter 类型的按钮时会直接发送 `text`\n    \"\"\"\n    label: Union[str, Text]\n    \"\"\"按钮上的文字\"\"\"\n    clicked_label: Optional[str]\n    \"\"\"点击后按钮上的文字\"\"\"\n    id: Optional[str]\n    url: Optional[str]\n    text: Optional[str]\n    style: Optional[str]\n    \"\"\"\n    仅建议使用下列值：primary, secondary, success, warning, danger, info, link, grey, blue\n\n    此处规定 `grey` 与 `secondary` 等同, `blue` 与 `primary` 等同\n    \"\"\"\n    permission: Union[Literal[\"admin\", \"all\"], list[At]] = \"all\"\n    \"\"\"\n    - admin: 仅管理者可操作\n    - all: 所有人可操作\n    - list[At]: 指定用户/身份组可操作\n    \"\"\"\n\nclass Keyboard(Segment):\n    \"\"\"Keyboard对象，表示一行按钮元素\"\"\"\n    id: Optional[str]\n    \"\"\"此处一般用来表示模板id，特殊情况下可能表示例如 bot_appid 等\"\"\"\n    buttons: Optional[list[Button]]\n    row: Optional[int]\n    \"\"\"当消息中只写有一个 Keyboard 时可根据此参数约定按钮组的列数\"\"\"\n\nclass Other(Segment):\n    \"\"\"其他 Segment\"\"\"\n    origin: MessageSegment\n\nclass I18n(Segment):\n    \"\"\"特殊的 Segment，用于 i18n 消息\"\"\"\n    item_or_scope: Union[LangItem, str]\n    type_: Optional[str] = None\n\n    def tp(self) -> UniMessageTemplate: ...\n```\n\n:::tip\n\n或许你注意到了 `Segment` 上有一个 `children` 属性。\n\n这是因为在 [`Satori`](https://satori.js.org/zh-CN/) 协议的规定下，一类元素可以用其子元素来代表一类兼容性消息\n（例如，qq 的商场表情在某些平台上可以用图片代替）。\n\n为此，本插件提供了 `select` 方法来表达 \"命令中获取子元素\" 的方法：\n\n```python\nfrom nonebot_plugin_alconna import Args, Image, Alconna, select\nfrom nonebot_plugin_alconna.builtins.uniseg.market_face import MarketFace\n\n# 表示这个指令需要的图片会在目标元素下进行搜索，将所有符合 Image 的元素选出来并将第一个作为结果\nalc1 = Alconna(\"make_meme\", Args[\"name\", str][\"img\", select(Image).first])  # 也可以使用 select(Image).nth(0)\n\n# 表示这个指令需要的图片要么直接是 Image 要么是在 MarketFace 元素内的 Image\nalc2 = Alconna(\"make_meme\", Args[\"name\", str][\"img\", [Image, select(Image).from_(MarketFace)]])\n```\n\n也可以参考通用消息的 [`嵌套提取`](./message.mdx#嵌套提取)\n\n:::\n\n## 自定义消息段\n\n`uniseg` 提供了部分方法来允许用户自定义 Segment 的序列化和反序列化：\n\n```python\nfrom dataclasses import dataclass\n\nfrom nonebot.adapters import Bot\nfrom nonebot.adapters import MessageSegment as BaseMessageSegment\nfrom nonebot.adapters.satori import Custom, Message, MessageSegment\n\nfrom nonebot_plugin_alconna.uniseg.builder import MessageBuilder\nfrom nonebot_plugin_alconna.uniseg.exporter import MessageExporter\nfrom nonebot_plugin_alconna.uniseg import Segment, custom_handler, custom_register\n\n\n@dataclass\nclass MarketFace(Segment):\n    tabId: str\n    faceId: str\n    key: str\n\n\n@custom_register(MarketFace, \"chronocat:marketface\")\ndef mfbuild(builder: MessageBuilder, seg: BaseMessageSegment):\n    if not isinstance(seg, Custom):\n        raise ValueError(\"MarketFace can only be built from Satori Message\")\n    return MarketFace(**seg.data)(*builder.generate(seg.children))\n\n\n@custom_handler(MarketFace)\nasync def mfexport(exporter: MessageExporter, seg: MarketFace, bot: Bot, fallback: bool):\n    if exporter.get_message_type() is Message:\n        return MessageSegment(\"chronocat:marketface\", seg.data)(await exporter.export(seg.children, bot, fallback))\n\n```\n\n具体而言，你可以使用 `custom_register` 来增加一个从 MessageSegment 到 Segment 的处理方法；使用 `custom_handler` 来增加一个从 Segment 到 MessageSegment 的处理方法。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/best-practice/alconna/uniseg/utils.mdx",
    "content": "---\nsidebar_position: 4\ndescription: 辅助模型\n---\n\nimport Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\n\n# 辅助功能\n\n`uniseg` 模块同时提供了多种方法以通用消息操作。\n\n:::note\n\n这些方法中与 `event`, `bot` 相关的参数都会尝试从上下文中获取对象。\n\n:::\n\n## 消息事件 ID\n\n消息事件 ID 是用来标识当前消息事件的唯一 ID，通常用于回复/撤回/编辑/表态当前消息。\n\n<Tabs groupId=\"get_msgid\">\n<TabItem value=\"depend\" label=\"使用依赖注入\">\n\n通过提供的 `MessageId` 或 `MsgId` 依赖注入器来获取消息事件 id。\n\n```python\nfrom nonebot_plugin_alconna.uniseg import MsgId\n\n\nmatcher = on_xxx(...)\n\n@matcher.handle()\nasycn def _(msg_id: MsgId):\n    ...\n```\n\n</TabItem>\n<TabItem value=\"method\" label=\"使用获取函数\">\n\n```python\nfrom nonebot import Event, Bot\nfrom nonebot_plugin_alconna.uniseg import get_message_id\n\n\nmatcher = on_xxx(...)\n\n@matcher.handle()\nasycn def _(bot: Bot, event: Event):\n    msg_id: str = get_message_id(event, bot)\n\n```\n\n</TabItem>\n</Tabs>\n\n:::caution\n\n该方法获取的消息事件 ID 不推荐直接用于各适配器的 API 调用中，可能会操作失败。\n\n:::\n\n## 发送对象\n\n消息发送对象是用来描述当前消息事件的可发送对象或者主动发送消息时的目标对象，它包含了以下属性：\n\n```python\nclass Target:\n    id: str\n    \"\"\"目标id；若为群聊则为 group_id 或者 channel_id，若为私聊则为 user_id\"\"\"\n    parent_id: str\n    \"\"\"父级id；若为频道则为 guild_id，其他情况下可能为空字符串（例如 Feishu 下可作为部门 id）\"\"\"\n    channel: bool\n    \"\"\"是否为频道，仅当目标平台符合频道概念时\"\"\"\n    private: bool\n    \"\"\"是否为私聊\"\"\"\n    source: str\n    \"\"\"可能的事件id\"\"\"\n    self_id: str | None\n    \"\"\"机器人id，若为 None 则 Bot 对象会随机选择\"\"\"\n    selector: Callable[[Bot], Awaitable[bool]] | None\n    \"\"\"选择器，用于在多个 Bot 对象中选择特定 Bot\"\"\"\n    extra: dict[str, Any]\n    \"\"\"额外信息，用于适配器扩展\"\"\"\n```\n\n<Tabs groupId=\"get_target\">\n<TabItem value=\"depend\" label=\"使用依赖注入\">\n\n通过提供的 `MessageTarget` 或 `MsgTarget` 依赖注入器来获取消息发送对象。\n\n```python\nfrom nonebot_plugin_alconna.uniseg import MsgTarget\n\n\nmatcher = on_xxx(...)\n\n@matcher.handle()\nasycn def _(target: MsgTarget):\n    ...\n```\n\n</TabItem>\n<TabItem value=\"method\" label=\"使用获取函数\">\n\n```python\nfrom nonebot import Event, Bot\nfrom nonebot_plugin_alconna.uniseg import Target, get_target\n\n\nmatcher = on_xxx(...)\n\n@matcher.handle()\nasycn def _(bot: Bot, event: Event):\n    target: Target = get_target(event, bot)\n\n```\n\n</TabItem>\n</Tabs>\n\n主动构造一个发送对象时，则需要如下参数：\n\n- `id`: 目标ID；若为群聊则为 `group_id` 或者 `channel_id`，若为私聊则为 `user_id`\n- `parent_id`: 父级ID；若为频道则为 `guild_id`，其他情况下可能为空字符串（例如 Feishu 下可作为部门 id）\n- `channel`: 是否为频道，仅当目标平台符合频道概念时\n- `private`: 是否为私聊\n- `source`: 可能的事件ID\n- `self_id`: 机器人id，若为 None 则 Bot 对象会随机选择\n- `selector`: 选择器，用于在多个 Bot 对象中选择特定 Bot\n- `scope`: 平台范围，表示当前发送对象的平台类别\n- `adapter`: 适配器名称，若为 None 则需要明确指定 Bot 对象\n- `platform`: 平台名称，仅当目标适配器存在多个平台时使用\n- `extra`: 额外信息，用于适配器扩展\n\n通过 `Target` 对象，我们可以在 `UniMessage.send` 中指定发送对象：\n\n```python\nfrom nonebot_plugin_alconna.uniseg import UniMessage, MsgTarget, Target, SupportScope\n\n\nmatcher = on_xxx(...)\n\n@matcher.handle()\nasync def _(target: MsgTarget):\n    # 将消息发送给当前事件的发送者\n    await UniMessage(\"Hello!\").send(target=target)\n    # 主动发送消息给群号为 12345 的 QQ 群聊\n    target1 = Target(\"12345\", scope=SupportScope.qq_client)\n    await UniMessage(\"Hello!\").send(target=target1)\n```\n\n### 选择器\n\n一般来说，主动发送消息时，`UniMessage.send` 或 `Target.self_id` 应指定一个 `Bot` 对象。但是这样会加重开发者的负担。\n\n因此，我们提供了选择器来帮助开发者选择一个 `Bot` 对象。当然，这并非说明一定需要传入 `selector` 参数。\n\n事实上，构造 `Target` 对象时，`self_id`, `scope`, `adapter` 和 `platform` 都会参与到 `selector` 的构造中。\n\n:::tip\n\n你其实可以使用 `Target` 来帮你筛选 `Bot` 对象：\n\n```python\nasync def _():\n    target = Target(\"12345\", scope=SupportScope.qq_client)\n    bot = await target.select()\n```\n\n:::\n\n若配置了 [`alconna_apply_fetch_targets`](../config.md#alconna_apply_fetch_targets) 选项，则在启动时会主动拉取一次发送对象列表。即对于\n某一主动构造的 `Target` 对象，插件将其与拉取下来的众多发送对象进行匹配，并选择第一个符合条件的发送对象，以选择对应的 Bot 对象。\n\n## 撤回消息\n\n通过 `message_recall` 方法来撤回消息事件。\n\n```python\nfrom nonebot_plugin_alconna.uniseg import message_recall\n\nmatcher = on_xxx(...)\n\n@matcher.handle()\nasync def _(msg_id: MsgId):\n    await message_recall(msg_id)\n```\n\n`message_recall` 方法的参数如下：\n\n```python\nasync def message_recall(\n    message_id: str | None = None,\n    event: Event | None = None,\n    bot: Bot | None = None,\n    adapter: str | None = None\n): ...\n```\n\n当 `message_id` 为 `None` 时，插件会尝试从 `event` 中获取消息事件 ID。\n\n## 编辑消息\n\n通过 `message_edit` 方法来编辑消息事件。\n\n```python\nfrom nonebot_plugin_alconna.uniseg import UniMessage, message_edit\n\nmatcher = on_xxx(...)\n\n@matcher.handle()\nasync def _():\n    await message_edit(UniMessage.text(\"1234\"))\n```\n\n`message_edit` 方法的参数如下：\n\n```python\nasync def message_edit(\n    msg: UniMessage,\n    message_id: str | None = None,\n    event: Event | None = None,\n    bot: Bot | None = None,\n    adapter: str | None = None,\n): ...\n```\n\n当 `message_id` 为 `None` 时，插件会尝试从 `event` 中获取消息事件 ID。\n\n## 表态消息\n\n:::caution\n\n该方法属于实验性功能。其接口可能会在未来的版本中发生变化。\n\n:::\n\n通过 `message_reaction` 方法来表态消息事件。\n\n```python\nfrom nonebot_plugin_alconna.uniseg import message_reaction\n\nmatcher = on_xxx(...)\n\n@matcher.handle()\nasync def _():\n    await message_reaction(\"👍\")\n```\n\n`message_reaction` 方法的参数如下：\n\n```python\nasync def message_reaction(\n    reaction: str | Emoji,\n    message_id: str | None = None,\n    event: Event | None = None,\n    bot: Bot | None = None,\n    adapter: str | None = None,\n    delete: bool = False,\n): ...\n```\n\n当 `message_id` 为 `None` 时，插件会尝试从 `event` 中获取消息事件 ID。\n\n`delete` 参数表示是否删除**自己的**表态消息，默认为 `False`。\n\n## 响应规则\n\n`uniseg` 模块提供了两个响应规则：\n\n- `at_in`: 是否在消息中 @ 了指定的用户\n- `at_me`: 是否在消息中 @ 了机器人\n\n相较于 NoneBot 内置的 `to_me` 规则，`at_me` 规则只会在消息中 @ 机器人时触发。\n\n```python\nfrom nonebot_plugin_alconna.uniseg import at_me\n\nmatcher = on_xxx(..., rule=at_me())\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/best-practice/data-storing.md",
    "content": "---\nsidebar_position: 1\ndescription: 存储数据文件到本地\n---\n\n# 数据存储\n\n在使用插件的过程中，难免会需要存储一些持久化数据，例如用户的个人信息、群组的信息等。除了使用数据库等第三方存储之外，还可以使用本地文件来自行管理数据。NoneBot 提供了 `nonebot-plugin-localstore` 插件，可用于获取正确的数据存储路径并写入数据。\n\n## 安装插件\n\n在使用前请先安装 `nonebot-plugin-localstore` 插件至项目环境中，可参考[获取商店插件](../tutorial/store.mdx#安装插件)来了解并选择安装插件的方式。如：\n\n在**项目目录**下执行以下命令：\n\n```bash\nnb plugin install nonebot-plugin-localstore\n```\n\n## 使用插件\n\n`nonebot-plugin-localstore` 插件兼容 Windows、Linux 和 macOS 等操作系统，使用时无需关心操作系统的差异。同时插件提供 `nb-cli` 脚本，可以使用 `nb localstore` 命令来检查数据存储路径。\n\n在使用本插件前同样需要使用 `require` 方法进行**加载**并**导入**需要使用的方法，可参考 [跨插件访问](../advanced/requiring.md) 一节进行了解，如：\n\n```python\nfrom nonebot import require\n\nrequire(\"nonebot_plugin_localstore\")\n\nimport nonebot_plugin_localstore as store\n\n# 获取插件缓存目录\ncache_dir = store.get_plugin_cache_dir()\n# 获取插件缓存文件\ncache_file = store.get_plugin_cache_file(\"file_name\")\n# 获取插件数据目录\ndata_dir = store.get_plugin_data_dir()\n# 获取插件数据文件\ndata_file = store.get_plugin_data_file(\"file_name\")\n# 获取插件配置目录\nconfig_dir = store.get_plugin_config_dir()\n# 获取插件配置文件\nconfig_file = store.get_plugin_config_file(\"file_name\")\n```\n\n:::danger 警告\n在 Windows 和 macOS 系统下，插件的数据目录和配置目录是同一个目录，因此在使用时需要注意避免文件名冲突。\n:::\n\n插件提供的方法均返回一个 `pathlib.Path` 路径，可以参考 [pathlib 文档](https://docs.python.org/zh-cn/3/library/pathlib.html)来了解如何使用。常用的方法有：\n\n```python\nfrom pathlib import Path\n\ndata_file = store.get_plugin_data_file(\"file_name\")\n# 写入文件内容\ndata_file.write_text(\"Hello World!\")\n# 读取文件内容\ndata = data_file.read_text()\n```\n\n:::note 提示\n\n对于嵌套插件，子插件的存储目录将位于父插件存储目录下。\n\n:::\n\n## 配置项\n\n### localstore_use_cwd\n\n使用当前工作目录作为数据存储目录，以下数据目录配置项默认值将会对应变更\n\n默认值：`False`\n\n```dotenv\nLOCALSTORE_USE_CWD=true\n```\n\n### localstore_cache_dir\n\n自定义缓存目录\n\n默认值：\n\n当 `localstore_use_cwd` 为 `True` 时，缓存目录为 `<current_working_directory>/cache`，否则：\n\n- macOS: `~/Library/Caches/nonebot2`\n- Unix: `~/.cache/nonebot2` (XDG default)\n- Windows: `C:\\Users\\<username>\\AppData\\Local\\nonebot2\\Cache`\n\n```dotenv\nLOCALSTORE_CACHE_DIR=/tmp/cache\n```\n\n### localstore_data_dir\n\n自定义数据目录\n\n默认值：\n\n当 `localstore_use_cwd` 为 `True` 时，数据目录为 `<current_working_directory>/data`，否则：\n\n- macOS: `~/Library/Application Support/nonebot2`\n- Unix: `~/.local/share/nonebot2` or in $XDG_DATA_HOME, if defined\n- Win XP (not roaming): `C:\\Documents and Settings\\<username>\\Application Data\\nonebot2`\n- Win 7 (not roaming): `C:\\Users\\<username>\\AppData\\Local\\nonebot2`\n\n```dotenv\nLOCALSTORE_DATA_DIR=/tmp/data\n```\n\n### localstore_config_dir\n\n自定义配置目录\n\n默认值：\n\n当 `localstore_use_cwd` 为 `True` 时，配置目录为 `<current_working_directory>/config`，否则：\n\n- macOS: same as user_data_dir\n- Unix: `~/.config/nonebot2`\n- Win XP (roaming): `C:\\Documents and Settings\\<username>\\Local Settings\\Application Data\\nonebot2`\n- Win 7 (roaming): `C:\\Users\\<username>\\AppData\\Roaming\\nonebot2`\n\n```dotenv\nLOCALSTORE_CONFIG_DIR=/tmp/config\n```\n\n### localstore_plugin_cache_dir\n\n自定义插件缓存目录\n\n默认值：`{}`\n\n```dotenv\nLOCALSTORE_PLUGIN_CACHE_DIR='\n{\n  \"plugin_id\": \"/tmp/plugin_cache\"\n}\n'\n```\n\n### localstore_plugin_data_dir\n\n自定义插件数据目录\n\n默认值：`{}`\n\n```dotenv\nLOCALSTORE_PLUGIN_DATA_DIR='\n{\n  \"plugin_id\": \"/tmp/plugin_data\"\n}\n'\n```\n\n### localstore_plugin_config_dir\n\n自定义插件配置目录\n\n默认值：`{}`\n\n```dotenv\nLOCALSTORE_PLUGIN_CONFIG_DIR='\n{\n  \"plugin_id\": \"/tmp/plugin_config\"\n}\n'\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/best-practice/database/README.mdx",
    "content": "import TabItem from \"@theme/TabItem\";\nimport Tabs from \"@theme/Tabs\";\n\n# 数据库\n\n[`nonebot-plugin-orm`](https://github.com/nonebot/plugin-orm) 是 NoneBot 的数据库支持插件。\n本插件基于 [SQLAlchemy](https://www.sqlalchemy.org/) 和 [Alembic](https://alembic.sqlalchemy.org/)，提供了许多与 NoneBot 紧密集成的功能：\n\n- 多 Engine / Connection 支持\n- Session 管理\n- 关系模型管理、依赖注入支持\n- 数据库迁移\n\n## 安装\n\n<Tabs groupId=\"install\">\n<TabItem value=\"cli\" label=\"使用 nb-cli\">\n\n```shell\nnb plugin install nonebot-plugin-orm\n```\n\n</TabItem>\n<TabItem value=\"pip\" label=\"使用 pip\">\n\n```shell\npip install nonebot-plugin-orm\n```\n\n</TabItem>\n\n<TabItem value=\"pdm\" label=\"使用 pdm\">\n\n```shell\npdm add nonebot-plugin-orm\n```\n\n</TabItem>\n</Tabs>\n\n## 数据库驱动和后端\n\n本插件只提供了 ORM 功能，没有数据库后端，也没有直接连接数据库后端的能力。\n所以你需要另行安装数据库驱动和数据库后端，并且配置数据库连接信息。\n\n### SQLite\n\n[SQLite](https://www.sqlite.org/) 是一个轻量级的嵌入式数据库，它的数据以单文件的形式存储在本地，不需要单独的数据库后端。\nSQLite 非常适合用于开发环境和小型应用，但是不适合用于大型应用的生产环境。\n\n虽然不需要另行安装数据库后端，但你仍然需要安装数据库驱动：\n\n<Tabs groupId=\"install\">\n<TabItem value=\"pip\" label=\"使用 pip\">\n\n```shell\npip install \"nonebot-plugin-orm[sqlite]\"\n```\n\n</TabItem>\n\n<TabItem value=\"pdm\" label=\"使用 pdm\">\n\n```shell\npdm add \"nonebot-plugin-orm[sqlite]\"\n```\n\n</TabItem>\n</Tabs>\n\n默认情况下，数据库文件为 `<data path>/nonebot-plugin-orm/db.sqlite3`（数据目录由 [nonebot-plugin-localstore](../data-storing) 提供）。\n或者，你可以通过配置 `SQLALCHEMY_DATABASE_URL` 来指定数据库文件路径：\n\n```shell\nSQLALCHEMY_DATABASE_URL=sqlite+aiosqlite:///file_path\n```\n\n### PostgreSQL\n\n[PostgreSQL](https://www.postgresql.org/) 是世界上最先进的开源关系数据库之一，对各种高级且广泛应用的功能有最好的支持，是中小型应用的首选数据库。\n\n<Tabs groupId=\"install\">\n<TabItem value=\"pip\" label=\"使用 pip\">\n\n```shell\npip install nonebot-plugin-orm[postgresql]\n```\n\n</TabItem>\n\n<TabItem value=\"pdm\" label=\"使用 pdm\">\n\n```shell\npdm add nonebot-plugin-orm[postgresql]\n```\n\n</TabItem>\n</Tabs>\n\n```shell\nSQLALCHEMY_DATABASE_URL=postgresql+psycopg://user:password@host:port/dbname[?key=value&key=value...]\n```\n\n### MySQL / MariaDB\n\n[MySQL](https://www.mysql.com/) 和 [MariaDB](https://mariadb.com/) 是经典的开源关系数据库，适合用于中小型应用。\n\n<Tabs groupId=\"install\">\n<TabItem value=\"pip\" label=\"使用 pip\">\n\n```shell\npip install nonebot-plugin-orm[mysql]\n```\n\n</TabItem>\n\n<TabItem value=\"pdm\" label=\"使用 pdm\">\n\n```shell\npdm add nonebot-plugin-orm[mysql]\n```\n\n</TabItem>\n</Tabs>\n\n```shell\nSQLALCHEMY_DATABASE_URL=mysql+aiomysql://user:password@host:port/dbname[?key=value&key=value...]\n```\n\n## 使用\n\n本插件提供了数据库迁移功能（此功能依赖于 [nb-cli 脚手架](../../quick-start#安装脚手架)）。\n在安装了新的插件或机器人之后，你需要执行一次数据库迁移操作，将数据库同步至与机器人一致的状态：\n\n```shell\nnb orm upgrade\n```\n\n运行完毕后，可以检查一下：\n\n```shell\nnb orm check\n```\n\n如果输出是 `没有检测到新的升级操作`，那么恭喜你，数据库已经迁移完成了，你可以启动机器人了。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/best-practice/database/_category_.json",
    "content": "{\n  \"label\": \"数据库\",\n  \"position\": 7\n}\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/best-practice/database/developer/README.md",
    "content": "# 开发者指南\n\n开发者指南内容较多，故分为了一个示例以及数个专题。\n阅读（并且最好跟随实践）示例后，你将会对使用 `nonebot-plugin-orm` 开发插件有一个基本的认识。\n如果想要更深入地学习关于 [SQLAlchemy](https://www.sqlalchemy.org/) 和 [Alembic](https://alembic.sqlalchemy.org/) 的知识，或者在使用过程中遇到了问题，可以查阅专题以及其官方文档。\n\n## 示例\n\n### 模型定义\n\n首先，我们需要设计存储的数据的结构。\n例如天气插件，需要存储**什么地方 (`location`)** 的**天气是什么 (`weather`)**。\n其中，一个地方只会有一种天气，而不同地方可能有相同的天气。\n所以，我们可以设计出如下的模型：\n\n```python title=weather/__init__.py showLineNumbers\nfrom nonebot_plugin_orm import Model\nfrom sqlalchemy.orm import Mapped, mapped_column\n\n\nclass Weather(Model):\n    location: Mapped[str] = mapped_column(primary_key=True)\n    weather: Mapped[str]\n```\n\n其中，`primary_key=True` 意味着此列 (`location`) 是主键，即内容是唯一的且非空的。\n每一个模型必须有至少一个主键。\n\n我们可以用以下代码检查模型生成的数据库模式是否正确：\n\n```python\nfrom sqlalchemy.schema import CreateTable\n\nprint(CreateTable(Weather.__table__))\n```\n\n```sql\nCREATE TABLE weather_weather (\n        location VARCHAR NOT NULL,\n        weather VARCHAR NOT NULL,\n        CONSTRAINT pk_weather_weather PRIMARY KEY (location)\n)\n```\n\n可以注意到表名是 `weather_weather` 而不是 `Weather` 或者 `weather`。\n这是因为 `nonebot-plugin-orm` 会自动为模型生成一个表名，规则是：`<插件模块名>_<类名小写>`。\n\n你也可以通过指定 `__tablename__` 属性来自定义表名：\n\n```python {2}\nclass Weather(Model):\n    __tablename__ = \"weather\"\n    ...\n```\n\n```sql {1}\nCREATE TABLE weather (\n    ...\n)\n```\n\n但是，并不推荐你这么做，因为这可能会导致不同插件间的表名重复，引发冲突。\n特别是当你会发布插件时，你并不知道其他插件会不会使用相同的表名。\n\n### 首次迁移\n\n我们成功定义了模型，现在启动机器人试试吧：\n\n```shell\n$ nb run\n01-02 15:04:05 [SUCCESS] nonebot | NoneBot is initializing...\n01-02 15:04:05 [ERROR] nonebot_plugin_orm | 启动检查失败\n01-02 15:04:05 [ERROR] nonebot | Application startup failed. Exiting.\nTraceback (most recent call last):\n  ...\nclick.exceptions.UsageError: 检测到新的升级操作:\n[('add_table',\n  Table('weather', MetaData(), Column('location', String(), table=<weather>, primary_key=True, nullable=False), Column('weather', String(), table=<weather>, nullable=False), schema=None))]\n```\n\n咦，发生了什么？\n`nonebot-plugin-orm` 试图阻止我们启动机器人。\n原来是我们定义了模型，但是数据库中并没有对应的表，这会导致插件不能正常运行。\n所以，我们需要迁移数据库。\n\n首先，我们需要创建一个迁移脚本：\n\n```shell\nnb orm revision -m \"first revision\" --branch-label weather\n```\n\n其中，`-m` 参数是迁移脚本的描述，`--branch-label` 参数是迁移脚本的分支，一般为插件模块名。\n执行命令过后，出现了一个 `weather/migrations` 目录，其中有一个 `xxxxxxxxxxxx_first_revision.py` 文件：\n\n```shell {4,5}\nweather\n├── __init__.py\n├── config.py\n└── migrations\n    └── xxxxxxxxxxxx_first_revision.py\n```\n\n这就是我们创建的迁移脚本，它记录了数据库模式的变化。\n我们可以查看一下它的内容：\n\n```python title=weather/migrations/xxxxxxxxxxxx_first_revision.py {25-33,39-41} showLineNumbers\n\"\"\"first revision\n\n迁移 ID: xxxxxxxxxxxx\n父迁移:\n创建时间: 2006-01-02 15:04:05.999999\n\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom collections.abc import Sequence\n\nimport sqlalchemy as sa\nfrom alembic import op\n\nrevision: str = \"xxxxxxxxxxxx\"\ndown_revision: str | Sequence[str] | None = None\nbranch_labels: str | Sequence[str] | None = (\"weather\",)\ndepends_on: str | Sequence[str] | None = None\n\n\ndef upgrade(name: str = \"\") -> None:\n    if name:\n        return\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.create_table(\n        \"weather_weather\",\n        sa.Column(\"location\", sa.String(), nullable=False),\n        sa.Column(\"weather\", sa.String(), nullable=False),\n        sa.PrimaryKeyConstraint(\"location\", name=op.f(\"pk_weather_weather\")),\n        info={\"bind_key\": \"weather\"},\n    )\n    # ### end Alembic commands ###\n\n\ndef downgrade(name: str = \"\") -> None:\n    if name:\n        return\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_table(\"weather_weather\")\n    # ### end Alembic commands ###\n```\n\n可以注意到脚本的主体部分（其余是模版代码，请勿修改）是：\n\n```python\n# ### commands auto generated by Alembic - please adjust! ###\nop.create_table(  # CREATE TABLE\n    \"weather_weather\",  # weather_weather\n    sa.Column(\"location\", sa.String(), nullable=False),  # location VARCHAR NOT NULL,\n    sa.Column(\"weather\", sa.String(), nullable=False),  # weather VARCHAR NOT NULL,\n    sa.PrimaryKeyConstraint(\"location\", name=op.f(\"pk_weather_weather\")),  # CONSTRAINT pk_weather_weather PRIMARY KEY (location)\n    info={\"bind_key\": \"weather\"},\n)\n# ### end Alembic commands ###\n```\n\n```python\n# ### commands auto generated by Alembic - please adjust! ###\nop.drop_table(\"weather_weather\")  # DROP TABLE weather_weather;\n# ### end Alembic commands ###\n```\n\n虽然我们不是很懂这些代码的意思，但是可以注意到它们几乎与 SQL 语句 (DDL) 一一对应。\n显然，它们是用来创建和删除表的。\n\n我们还可以注意到，`upgrade()` 和 `downgrade()` 函数中的代码是**互逆**的。\n也就是说，执行一次 `upgrade()` 函数，再执行一次 `downgrade()` 函数后，数据库的模式就会回到原来的状态。\n\n这就是迁移脚本的作用：记录数据库模式的变化，以便我们在不同的环境中（例如开发环境和生产环境）**可复现地**、**可逆地**同步数据库模式，正如 git 对我们的代码做的事情那样。\n\n对了，不要忘记还有一段注释：`commands auto generated by Alembic - please adjust!`。\n它在提醒我们，这些代码是由 Alembic 自动生成的，我们应该检查它们，并且根据需要进行调整。\n\n:::caution 注意\n迁移脚本冗长且繁琐，我们一般不会手写它们，而是由 Alembic 自动生成。\n一般情况下，Alembic 足够智能，可以正确地生成迁移脚本。\n但是，在复杂或有歧义的情况下，我们可能需要手动调整迁移脚本。\n所以，**永远要检查迁移脚本，并且在开发环境中测试！**\n\n**迁移脚本中任何一处错误都足以使数据付之东流！**\n:::\n\n确定迁移脚本正确后，我们就可以执行迁移脚本，将数据库模式同步到数据库中：\n\n```shell\nnb orm upgrade\n```\n\n现在，我们可以正常启动机器人了。\n\n开发过程中，我们可能会频繁地修改模型，这意味着我们需要频繁地创建并执行迁移脚本，非常繁琐。\n实际上，此时我们不在乎数据安全，只需要数据库模式与模型定义一致即可。\n所以，我们可以关闭 `nonebot-plugin-orm` 的启动检查：\n\n```shell title=.env.dev\nALEMBIC_STARTUP_CHECK=false\n```\n\n现在，每次启动机器人时，数据库模式会自动与模型定义同步，无需手动迁移。\n\n### 会话管理\n\n我们已经成功定义了模型，并且迁移了数据库，现在可以开始使用数据库了……吗？\n并不能，因为模型只是数据结构的定义，并不能通过它操作数据（如果你曾经使用过 [Tortoise ORM](https://tortoise.github.io/)，可能会知道 `await Weather.get(location=\"上海\")` 这样的面向对象编程。\n但是 SQLAlchemy 不同，选择了命令式编程）。\n我们需要使用**会话**操作数据：\n\n```python title=weather/__init__.py {10,13} showLineNumbers\nfrom nonebot import on_command\nfrom nonebot.adapters import Message\nfrom nonebot.params import CommandArg\nfrom nonebot_plugin_orm import async_scoped_session\n\nweather = on_command(\"天气\")\n\n\n@weather.handle()\nasync def _(session: async_scoped_session, args: Message = CommandArg()):\n    location = args.extract_plain_text()\n\n    if wea := await session.get(Weather, location):\n        await weather.finish(f\"今天{location}的天气是{wea.weather}\")\n\n    await weather.finish(f\"未查询到{location}的天气\")\n```\n\n我们通过 `session: async_scoped_session` 依赖注入获得了一个会话，然后使用 `await session.get(Weather, location)` 查询数据库。\n`async_scoped_session` 是一个有作用域限制的会话，作用域为当前事件、当前事件响应器。\n会话产生的模型实例（例如此处的 `wea := await session.get(Weather, location)`）作用域与会话相同。\n\n:::caution 注意\n此处提到的“会话”指的是 ORM 会话，而非 [NoneBot 会话](../../../appendices/session-control)，两者的生命周期也是不同的（NoneBot 会话的生命周期中可能包含多个事件，不同的事件也会有不同的事件响应器）。\n具体而言，就是不要将 ORM 会话和模型实例存储在 NoneBot 会话状态中：\n\n```python {12}\nfrom nonebot.params import ArgPlainText\nfrom nonebot.typing import T_State\n\n\n@weather.got(\"location\", prompt=\"请输入地名\")\nasync def _(state: T_State, session: async_scoped_session, location: str = ArgPlainText()):\n    wea = await session.get(Weather, location)\n\n    if not wea:\n        await weather.finish(f\"未查询到{location}的天气\")\n\n    state[\"weather\"] = wea  # 不要这么做，除非你知道自己在做什么\n```\n\n当然非要这么做也不是不可以：\n\n```python {6}\n@weather.handle()\nasync def _(state: T_State, session: async_scoped_session):\n    # 通过 await session.merge(state[\"weather\"]) 获得了此 ORM 会话中的相应模型实例，\n    # 而非直接使用会话状态中的模型实例，\n    # 因为先前的 ORM 会话已经关闭了。\n    wea = await session.merge(state[\"weather\"])\n    await weather.finish(f\"今天{state['location']}的天气是{wea.weather}\")\n```\n\n:::\n\n当有数据更改时，我们需要提交事务，也要注意会话作用域问题：\n\n```python title=weather/__init__.py {12,20} showLineNumbers\nfrom nonebot.params import Depends\n\n\nasync def get_weather(\n    session: async_scoped_session, args: Message = CommandArg()\n) -> Weather:\n    location = args.extract_plain_text()\n\n    if not (wea := await session.get(Weather, location)):\n        wea = Weather(location=location, weather=\"未知\")\n        session.add(wea)\n        # await session.commit()  # 不应该在其他地方提交事务\n\n    return wea\n\n\n@weather.handle()\nasync def _(session: async_scoped_session, wea: Weather = Depends(get_weather)):\n    await weather.send(f\"今天的天气是{wea.weather}\")\n    await session.commit()  # 而应该在事件响应器结束前提交事务\n```\n\n当然我们也可以获得一个新的会话，不过此时就要手动管理会话了：\n\n```python title=weather/__init__.py {5-6} showLineNumbers\nfrom nonebot_plugin_orm import get_session\n\n\nasync def get_weather(location: str) -> str:\n    session = get_session()\n    async with session.begin():\n        wea = await session.get(Weather, location)\n\n        if not wea:\n            wea = Weather(location=location, weather=\"未知\")\n            session.add(wea)\n\n        return wea.weather\n\n\n@weather.handle()\nasync def _(args: Message = CommandArg()):\n    wea = await get_weather(args.extract_plain_text())\n    await weather.send(f\"今天的天气是{wea}\")\n```\n\n### 依赖注入\n\n在上面的示例中，我们都是通过会话获得数据的。\n不过，我们也可以通过依赖注入获得数据：\n\n```python title=weather/__init__.py {12-14} showLineNumbers\nfrom sqlalchemy import select\nfrom nonebot.params import Depends\nfrom nonebot_plugin_orm import SQLDepends\n\n\ndef extract_arg_plain_text(args: Message = CommandArg()) -> str:\n    return args.extract_plain_text()\n\n\n@weather.handle()\nasync def _(\n    wea: Weather = SQLDepends(\n        select(Weather).where(Weather.location == Depends(extract_arg_plain_text))\n    ),\n):\n    await weather.send(f\"今天的天气是{wea.weather}\")\n```\n\n其中，`SQLDepends` 是一个特殊的依赖注入，它会根据类型标注和 SQL 语句提供数据，SQL 语句中也可以有子依赖。\n\n不同的类型标注也会获得不同形式的数据：\n\n```python title=weather/__init__.py {5} showLineNumbers\nfrom collections.abc import Sequence\n\n@weather.handle()\nasync def _(\n    weas: Sequence[Weather] = SQLDepends(\n        select(Weather).where(Weather.weather == Depends(extract_arg_plain_text))\n    ),\n):\n    await weather.send(f\"今天的天气是{weas[0].weather}的城市有{'，'.join(wea.location for wea in weas)}\")\n```\n\n支持的类型标注请参见 [依赖注入](dependency)。\n\n我们也可以像 [类作为依赖](../../../advanced/dependency#类作为依赖) 那样，在类属性中声明子依赖：\n\n```python title=weather/__init__.py {5-6,10} showLineNumbers\nfrom collections.abc import Sequence\n\nclass Weather(Model):\n    location: Mapped[str] = mapped_column(primary_key=True)\n    weather: Mapped[str] = Depends(extract_arg_plain_text)\n    # weather: Annotated[Mapped[str], Depends(extract_arg_plain_text)]  # Annotated 支持\n\n\n@weather.handle()\nasync def _(weas: Sequence[Weather]):\n    await weather.send(\n        f\"今天的天气是{weas[0].weather}的城市有{'，'.join(wea.location for wea in weas)}\"\n    )\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/best-practice/database/developer/_category_.json",
    "content": "{\n  \"label\": \"开发者指南\",\n  \"position\": 3\n}\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/best-practice/database/developer/dependency.md",
    "content": "---\nsidebar_position: 3\ndescription: 依赖注入\n---\n\n# 依赖注入\n\n`nonebot-plugin-orm` 提供了强大且灵活的依赖注入，可以方便地帮助你获取数据库会话和查询数据。\n\n## 数据库会话\n\n### AsyncSession\n\n新数据库会话，常用于有独立的数据库操作逻辑的插件。\n\n```python {13,26}\nfrom nonebot import on_message\nfrom nonebot.params import Depends\nfrom nonebot_plugin_orm import AsyncSession, Model, async_scoped_session\nfrom sqlalchemy.orm import Mapped, mapped_column\n\nmessage = on_message()\n\n\nclass Message(Model):\n    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)\n\n\nasync def get_message(session: AsyncSession) -> Message:\n    # 等价于 session = get_session()\n    async with session:\n        msg = Message()\n\n        session.add(msg)\n        await session.commit()\n        await session.refresh(msg)\n\n        return msg\n\n\n@message.handle()\nasync def _(session: async_scoped_session, msg: Message = Depends(get_message)):\n    await session.rollback()  # 无法回退 get_message() 中的更改\n    await message.send(str(msg.id))  # msg 被存储，msg.id 递增\n```\n\n### async_scoped_session\n\n数据库作用域会话，常用于事件响应器和有与响应逻辑相关的数据库操作逻辑的插件。\n\n```python {13，26}\nfrom nonebot import on_message\nfrom nonebot.params import Depends\nfrom nonebot_plugin_orm import Model, async_scoped_session\nfrom sqlalchemy.orm import Mapped, mapped_column\n\nmessage = on_message()\n\n\nclass Message(Model):\n    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)\n\n\nasync def get_message(session: async_scoped_session) -> Message:\n    # 等价于 session = get_scoped_session()\n    msg = Message()\n\n    session.add(msg)\n    await session.flush()\n    await session.refresh(msg)\n\n    return msg\n\n\n@message.handle()\nasync def _(session: async_scoped_session, msg: Message = Depends(get_message)):\n    await session.rollback()  # 可以回退 get_message() 中的更改\n    await message.send(str(msg.id))  # msg 没有被存储，msg.id 不变\n```\n\n## 查询数据\n\n### Model\n\n支持类作为依赖。\n\n```python\nfrom typing import Annotated\n\nfrom nonebot.params import Depends\nfrom nonebot_plugin_orm import Model\nfrom sqlalchemy.orm import Mapped, mapped_column\n\n\ndef get_id() -> int: ...\n\n\nclass Message(Model):\n    id: Annotated[Mapped[int], Depends(get_id)] = mapped_column(\n        primary_key=True, autoincrement=True\n    )\n\n\nasync def _(msg: Message):\n    # 等价于 msg = (\n    #     await (await session.stream(select(Message).where(Message.id == get_id())))\n    #     .scalars()\n    #     .one_or_none()\n    # )\n    ...\n```\n\n### SQLDepends\n\n参数为一个 SQL 语句，决定依赖注入的内容，SQL 语句中可以使用子依赖。\n\n```python {11-13}\nfrom nonebot.params import Depends\nfrom nonebot_plugin_orm import Model, SQLDepends\nfrom sqlalchemy import select\n\n\ndef get_id() -> int: ...\n\n\nasync def _(\n    model: Model = SQLDepends(select(Model).where(Model.id == Depends(get_id))),\n): ...\n```\n\n参数可以是任意 SQL 语句，但不建议使用 `select` 以外的语句，因为语句可能没有返回值（`returning` 除外），而且代码不清晰。\n\n### 类型标注\n\n类型标注决定依赖注入的数据结构，主要影响以下几个层面：\n\n- 迭代器（`session.execute()`）或异步迭代器（`session.stream()`）\n- 标量（`session.execute().scalars()`）或元组（`session.execute()`）\n- 一个（`session.execute().one_or_none()`，注意 `None` 时可能触发 [重载](../../../appendices/overload#重载)）或全部（`session.execute()` / `session.execute().all()`）\n- 连续（`session().execute()`）或分块（`session.execute().partitions()`）\n\n具体如下（可以使用父类型作为类型标注）：\n\n- ```python\n  async def _(rows_partitions: AsyncIterator[Sequence[Tuple[Model, ...]]]):\n      # 等价于 rows_partitions = await (await session.stream(sql).partitions())\n\n      async for partition in rows_partitions:\n          for row in partition:\n              print(row[0], row[1], ...)\n  ```\n\n- ```python\n  async def _(model_partitions: AsyncIterator[Sequence[Model]]):\n      # 等价于 model_partitions = await (await session.stream(sql).scalars().partitions())\n\n      async for partition in model_partitions:\n          for model in partition:\n              print(model)\n  ```\n\n- ```python\n  async def _(row_partitions: Iterator[Sequence[Tuple[Model, ...]]]):\n      # 等价于 row_partitions = await session.execute(sql).partitions()\n\n      for partition in rows_partitions:\n          for row in partition:\n              print(row[0], row[1], ...)\n  ```\n\n- ```python\n  async def _(model_partitions: Iterator[Sequence[Model]]):\n      # 等价于 model_partitions = await (await session.execute(sql).scalars().partitions())\n\n      for partition in model_partitions:\n          for model in partition:\n              print(model)\n  ```\n\n- ```python\n  async def _(rows: sa_async.AsyncResult[Tuple[Model, ...]]):\n      # 等价于 rows = await session.stream(sql)\n\n      async for row in rows:\n          print(row[0], row[1], ...)\n  ```\n\n- ```python\n  async def _(models: sa_async.AsyncScalarResult[Model]):\n      # 等价于 models = await session.stream(sql).scalars()\n\n      async for model in models:\n          print(model)\n  ```\n\n- ```python\n  async def _(rows: sa.Result[Tuple[Model, ...]]):\n      # 等价于 rows = await session.execute(sql)\n\n      for row in rows:\n          print(row[0], row[1], ...)\n  ```\n\n- ```python\n  async def _(models: sa.ScalarResult[Model]):\n      # 等价于 models = await session.execute(sql).scalars()\n\n      for model in models:\n          print(model)\n  ```\n\n- ```python\n  async def _(rows: Sequence[Tuple[Model, ...]]):\n      # 等价于 rows = await (await session.stream(sql).all())\n\n      for row in rows:\n            print(row[0], row[1], ...)\n  ```\n\n- ```python\n  async def _(models: Sequence[Model]):\n      # 等价于 models = await (await session.stream(sql).scalars().all())\n\n      for model in models:\n          print(model)\n  ```\n\n- ```python\n  async def _(row: Tuple[Model, ...]):\n      # 等价于 row = await (await session.stream(sql).one_or_none())\n\n      print(row[0], row[1], ...)\n  ```\n\n- ```python\n  async def _(model: Model):\n      # 等价于 model = await (await session.stream(sql).scalars().one_or_none())\n\n      print(model)\n  ```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/best-practice/database/developer/test.md",
    "content": "---\nsidebar_position: 2\ndescription: 测试\n---\n\n# 测试\n\n百思不如一试，测试是发现问题的最佳方式。\n\n不同的用户会有不同的配置，为了提高项目的兼容性，我们需要在不同数据库后端上测试。\n手动进行大量的、重复的测试不可靠，也不现实，因此我们推荐使用 [GitHub Actions](https://github.com/features/actions) 进行自动化测试：\n\n```yaml title=.github/workflows/test.yml {12-42,52-53} showLineNumbers\nname: Test\n\non:\n  push:\n    branches:\n      - main\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n\n    strategy:\n      matrix:\n        db:\n          - sqlite+aiosqlite:///db.sqlite3\n          - postgresql+psycopg://postgres:postgres@localhost:5432/postgres\n          - mysql+aiomysql://mysql:mysql@localhost:3306/mymysql\n\n      fail-fast: false\n\n    env:\n      SQLALCHEMY_DATABASE_URL: ${{ matrix.db }}\n\n    services:\n      postgresql:\n        image: ${{ startsWith(matrix.db, 'postgresql') && 'postgres' || '' }}\n        env:\n          POSTGRES_USER: postgres\n          POSTGRES_PASSWORD: postgres\n          POSTGRES_DB: postgres\n        ports:\n          - 5432:5432\n\n      mysql:\n        image: ${{ startsWith(matrix.db, 'mysql') && 'mysql' || '' }}\n        env:\n          MYSQL_ROOT_PASSWORD: mysql\n          MYSQL_USER: mysql\n          MYSQL_PASSWORD: mysql\n          MYSQL_DATABASE: mymysql\n        ports:\n          - 3306:3306\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - uses: actions/setup-python@v5\n\n      - name: Install dependencies\n        run: pip install -r requirements.txt\n\n      - name: Run migrations\n        run: pipx run nb-cli orm upgrade\n\n      - name: Run tests\n        run: pytest\n```\n\n如果项目还需要考虑跨平台和跨 Python 版本兼容，测试矩阵中还需要增加这两个维度。\n但是，我们没必要在所有平台和 Python 版本上运行所有数据库的测试，因为很显然，PostgreSQL 和 MySQL 这类独立的数据库后端不会受平台和 Python 影响，而且 Github Actions 的非 Linux 平台不支持运行独立服务：\n\n|             | Python 3.9 | Python 3.10 | Python 3.11 | Python 3.12                 |\n| ----------- | ---------- | ----------- | ----------- | --------------------------- |\n| **Linux**   | SQLite     | SQLite      | SQLite      | SQLite / PostgreSQL / MySQL |\n| **Windows** | SQLite     | SQLite      | SQLite      | SQLite                      |\n| **macOS**   | SQLite     | SQLite      | SQLite      | SQLite                      |\n\n```yaml title=.github/workflows/test.yml {12-24} showLineNumbers\nname: Test\n\non:\n  push:\n    branches:\n      - main\n\njobs:\n  test:\n    runs-on: ${{ matrix.os }}\n\n    strategy:\n      matrix:\n        os: [ubuntu-latest, windows-latest, macos-latest]\n        python-version: [\"3.9\", \"3.10\", \"3.11\", \"3.12\"]\n        db: [\"sqlite+aiosqlite:///db.sqlite3\"]\n\n        include:\n          - os: ubuntu-latest\n            python-version: \"3.12\"\n            db: postgresql+psycopg://postgres:postgres@localhost:5432/postgres\n          - os: ubuntu-latest\n            python-version: \"3.12\"\n            db: mysql+aiomysql://mysql:mysql@localhost:3306/mymysql\n\n      fail-fast: false\n\n    env:\n      SQLALCHEMY_DATABASE_URL: ${{ matrix.db }}\n\n    services:\n      postgresql:\n        image: ${{ startsWith(matrix.db, 'postgresql') && 'postgres' || '' }}\n        env:\n          POSTGRES_USER: postgres\n          POSTGRES_PASSWORD: postgres\n          POSTGRES_DB: postgres\n        ports:\n          - 5432:5432\n\n      mysql:\n        image: ${{ startsWith(matrix.db, 'mysql') && 'mysql' || '' }}\n        env:\n          MYSQL_ROOT_PASSWORD: mysql\n          MYSQL_USER: mysql\n          MYSQL_PASSWORD: mysql\n          MYSQL_DATABASE: mymysql\n        ports:\n          - 3306:3306\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - uses: actions/setup-python@v5\n        with:\n          python-version: ${{ matrix.python-version }}\n\n      - name: Install dependencies\n        run: pip install -r requirements.txt\n\n      - name: Run migrations\n        run: pipx run nb-cli orm upgrade\n\n      - name: Run tests\n        run: pytest\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/best-practice/database/user.md",
    "content": "---\nsidebar_position: 2\ndescription: 用户指南\n---\n\n# 用户指南\n\n`nonebot-plugin-orm` 功能强大且复杂，使用上有一定难度。\n不过，对于用户而言，只需要掌握部分功能即可。\n\n:::caution 注意\n请注意区分插件的项目名（如：`nonebot-plugin-wordcloud`）和模块名（如：`nonebot_plugin_wordcloud`）。`nonebot-plugin-orm` 中统一使用插件模块名。参见 [插件命名规范](../../developer/plugin-publishing#插件命名规范)。\n:::\n\n## 示例\n\n### 创建新机器人\n\n我们想要创建一个机器人，并安装 `nonebot-plugin-wordcloud` 插件，只需要执行以下命令：\n\n```shell\nnb init  # 初始化项目文件夹\n\npip install nonebot-plugin-orm[sqlite]  # 安装 nonebot-plugin-orm，并附带 SQLite 支持\n\nnb plugin install nonebot-plugin-wordcloud  # 安装插件\n\n# nb orm heads  # 查看有什么插件使用到了数据库（可选）\n\nnb orm upgrade  # 升级数据库\n\n# nb orm check  # 检查一下数据库模式是否与模型定义一致（可选）\n\nnb run  # 启动机器人\n```\n\n### 卸载插件\n\n我们已经安装了 `nonebot-plugin-wordcloud` 插件，但是现在想要卸载它，并且**删除它的数据**，只需要执行以下命令：\n\n```shell\nnb plugin uninstall nonebot-plugin-wordcloud  # 卸载插件\n\n# nb orm heads  # 查看有什么插件使用到了数据库。（可选）\n\nnb orm downgrade nonebot_plugin_wordcloud@base  # 降级数据库，删除数据\n\n# nb orm check  # 检查一下数据库模式是否与模型定义一致（可选）\n```\n\n## CLI\n\n接下来，让我们了解下示例中出现的 CLI 命令的含义：\n\n### heads\n\n显示所有的分支头。一般一个分支对应一个插件。\n\n```shell\nnb orm heads\n```\n\n输出格式为 `<迁移 ID> (<插件模块名>) (<头部类型>)`：\n\n```\n46327b837dd8 (nonebot_plugin_chatrecorder) (head)\n9492159f98f7 (nonebot_plugin_user) (head)\n71a72119935f (nonebot_plugin_session_orm) (effective head)\nade8cdca5470 (nonebot_plugin_wordcloud) (head)\n```\n\n### upgrade\n\n升级数据库。每次安装新的插件或更新插件版本后，都需要执行此命令。\n\n```shell\nnb orm upgrade <插件模块名>@<迁移 ID>\n```\n\n其中，`<插件模块名>@<迁移 ID>` 是可选参数。如果不指定，则会将所有分支升级到最新版本，这也是最常见的用法：\n\n```shell\nnb orm upgrade\n```\n\n### downgrade\n\n降级数据库。当需要回滚插件版本或删除插件时，可以执行此命令。\n\n```shell\nnb orm downgrade <插件模块名>@<迁移 ID>\n```\n\n其中，`<迁移 ID>` 也可以是 `base`，即回滚到初始状态。常用于卸载插件后删除其数据：\n\n```shell\nnb orm downgrade <插件模块名>@base\n```\n\n### check\n\n检查数据库模式是否与模型定义一致。机器人启动前会自动运行此命令（`ALEMBIC_STARTUP_CHECK=true` 时），并在检查失败时阻止启动。\n\n```shell\nnb orm check\n```\n\n## 配置\n\n### sqlalchemy_database_url\n\n默认数据库连接 URL。参见 [数据库驱动和后端](.#数据库驱动和后端) 和 [引擎配置 — SQLAlchemy 2.0 文档](https://docs.sqlalchemy.org/en/20/core/engines.html#database-urls)。\n\n```shell\nSQLALCHEMY_DATABASE_URL=dialect+driver://username:password@host:port/database\n```\n\n### sqlalchemy_bind\n\nbind 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) 实例的字典。\n例如，我们想要让 `nonebot-plugin-wordcloud` 插件使用一个 SQLite 数据库，并开启 [Echo 选项](https://docs.sqlalchemy.org/en/20/core/engines.html#sqlalchemy.create_engine.params.echo) 便于 debug，而其他插件使用默认的 PostgreSQL 数据库，可以这样配置：\n\n```shell\nSQLALCHEMY_BINDS='{\n    \"\": \"postgresql+psycopg://scott:tiger@localhost/mydatabase\",\n    \"nonebot_plugin_wordcloud\": {\n        \"url\": \"sqlite+aiosqlite://\",\n        \"echo\": true\n    }\n}'\n```\n\n### sqlalchemy_engine_options\n\n[`create_async_engine()`](https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html#sqlalchemy.ext.asyncio.create_async_engine) 默认参数字典。\n\n```shell\nSQLALCHEMY_ENGINE_OPTIONS='{\n    \"pool_size\": 5,\n    \"max_overflow\": 10,\n    \"pool_timeout\": 30,\n    \"pool_recycle\": 3600,\n    \"echo\": true\n}'\n```\n\n### sqlalchemy_echo\n\n开启 [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。\n\n```shell\nSQLALCHEMY_ECHO=true\n```\n\n:::caution 注意\n以上配置之间有覆盖关系，遵循特殊优先于一般的原则，具体为 [`sqlalchemy_database_url`](#sqlalchemy_database_url) > [`sqlalchemy_bind`](#sqlalchemy_bind) > [`sqlalchemy_echo`](#sqlalchemy_echo) > [`sqlalchemy_engine_options`](#sqlalchemy_engine_options)。\n但覆盖顺序并非显而易见，出于清晰考虑，请只配置必要的选项。\n:::\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/best-practice/deployment.mdx",
    "content": "---\nsidebar_position: 3\ndescription: 部署你的机器人\n---\n\n# 部署\n\nimport Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\n\n在编写完成各类插件后，我们需要长期运行机器人来使得用户能够正常使用。通常，我们会使用云服务器来部署机器人。\n\n我们在开发插件时，机器人运行的环境称为开发环境；而在部署后，机器人运行的环境称为生产环境。与开发环境不同的是，在生产环境中，开发者通常不能随意地修改/添加/删除代码，开启或停止服务。\n\n## 部署前准备\n\n### 项目依赖管理\n\n由于部署后的机器人运行在生产环境中，因此，为确保机器人能够正常运行，我们需要保证机器人的运行环境与开发环境一致。我们可以通过以下几种方式来进行依赖管理：\n\n<Tabs groupId=\"tool\">\n  <TabItem value=\"poetry\" label=\"Poetry\" default>\n\n[Poetry](https://python-poetry.org/) 是一个 Python 项目的依赖管理工具。它可以通过声明项目所依赖的库，为你管理（安装/更新）它们。Poetry 提供了一个 `poetry.lock` 文件，以确保可重复安装，并可以构建用于分发的项目。\n\nPoetry 会在安装依赖时自动生成 `poetry.lock` 文件，在**项目目录**下执行以下命令：\n\n```bash\n# 初始化 poetry 配置\npoetry init\n# 添加项目依赖，这里以 nonebot2[fastapi] 为例\npoetry add nonebot2[fastapi]\n```\n\n  </TabItem>\n  <TabItem value=\"pdm\" label=\"PDM\">\n\n[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` 文件，以确保可重复安装，并可以构建用于分发的项目。\n\nPDM 会在安装依赖时自动生成 `pdm.lock` 文件，在**项目目录**下执行以下命令：\n\n```bash\n# 初始化 pdm 配置\npdm init\n# 添加项目依赖，这里以 nonebot2[fastapi] 为例\npdm add nonebot2[fastapi]\n```\n\n  </TabItem>\n  <TabItem value=\"pip\" label=\"pip\">\n\n[pip](https://pip.pypa.io/) 是 Python 包管理工具。他并不是一个依赖管理工具，为了尽可能保证环境的一致性，我们可以使用 `requirements.txt` 文件来声明依赖。\n\n```bash\npip freeze > requirements.txt\n```\n\n  </TabItem>\n</Tabs>\n\n### 安装 Docker\n\n[Docker](https://www.docker.com/) 是一个应用容器引擎，可以让开发者打包应用以及依赖包到一个可移植的镜像中，然后发布到服务器上。\n\n我们可以参考 [Docker 官方文档](https://docs.docker.com/get-docker/) 来安装 Docker 。\n\n在 Linux 上，我们可以使用以下一键脚本来安装 Docker 以及 Docker Compose Plugin：\n\n```bash\ncurl -fsSL https://get.docker.com | sh -s -- --mirror Aliyun\n```\n\n在 Windows/macOS 上，我们可以使用 [Docker Desktop](https://docs.docker.com/desktop/) 来安装 Docker 以及 Docker Compose Plugin。\n\n### 安装脚手架 Docker 插件\n\n我们可以使用 [nb-cli-plugin-docker](https://github.com/nonebot/cli-plugin-docker) 来快速部署机器人。\n\n插件可以帮助我们生成配置文件并构建 Docker 镜像，以及启动/停止/重启机器人。使用以下命令安装脚手架 Docker 插件：\n\n```bash\nnb self install nb-cli-plugin-docker\n```\n\n## Docker 部署\n\n### 快速部署\n\n使用脚手架命令即可一键生成配置并部署：\n\n```bash\nnb docker up\n```\n\n当看到 `Running` 字样时，说明机器人已经启动成功。我们可以通过以下命令来查看机器人的运行日志：\n\n<Tabs groupId=\"deploy-tool\">\n  <TabItem value=\"nb-cli\" label=\"NB CLI\" default>\n\n```bash\nnb docker logs\n```\n\n  </TabItem>\n  <TabItem value=\"docker-compose\" label=\"Docker Compose\">\n\n```bash\ndocker compose logs\n```\n\n  </TabItem>\n</Tabs>\n\n如果需要停止机器人，我们可以使用以下命令：\n\n<Tabs groupId=\"deploy-tool\">\n  <TabItem value=\"nb-cli\" label=\"NB CLI\" default>\n\n```bash\nnb docker down\n```\n\n  </TabItem>\n  <TabItem value=\"docker-compose\" label=\"Docker Compose\">\n\n```bash\ndocker compose down\n```\n\n  </TabItem>\n</Tabs>\n\n### 自定义部署\n\n在部分情况下，我们需要事先生成 Docker 配置文件，再到生产环境进行部署；或者自动生成的配置文件并不能满足复杂场景，需要根据实际需求手动修改配置文件。我们可以使用以下命令来生成基础配置文件：\n\n```bash\nnb docker generate\n```\n\nnb-cli 将会在项目目录下生成 `docker-compose.yml` 和 `Dockerfile` 等配置文件。在 nb-cli 完成配置文件的生成后，我们可以根据部署环境的实际情况使用 nb-cli 或者 Docker Compose 来启动机器人。\n\n我们可以参考 [Dockerfile 文件规范](https://docs.docker.com/engine/reference/builder/)和 [Compose 文件规范](https://docs.docker.com/compose/compose-file/)修改这两个文件。\n\n修改完成后我们可以直接启动或者手动构建镜像：\n\n<Tabs groupId=\"deploy-tool\">\n  <TabItem value=\"nb-cli\" label=\"NB CLI\" default>\n\n```bash\n# 启动机器人\nnb docker up\n# 手动构建镜像\nnb docker build\n```\n\n  </TabItem>\n  <TabItem value=\"docker-compose\" label=\"Docker Compose\">\n\n```bash\n# 启动机器人\ndocker compose up -d\n# 手动构建镜像\ndocker compose build\n```\n\n  </TabItem>\n</Tabs>\n\n### 持续集成\n\n我们可以使用 GitHub Actions 来实现持续集成（CI），我们只需要在 GitHub 上发布 Release 即可自动构建镜像并推送至镜像仓库。\n\n首先，我们需要在 [Docker Hub](https://hub.docker.com/) （或者其他平台，如：[GitHub Packages](https://github.com/features/packages)、[阿里云容器镜像服务](https://www.alibabacloud.com/zh/product/container-registry)等）上创建镜像仓库，用于存放镜像。\n\n前往项目仓库的 `Settings` > `Secrets` > `actions` 栏目 `New Repository Secret` 添加构建所需的密钥：\n\n- `DOCKERHUB_USERNAME`: 你的 Docker Hub 用户名\n- `DOCKERHUB_TOKEN`: 你的 Docker Hub PAT（[创建方法](https://docs.docker.com/docker-hub/access-tokens/)）\n\n将以下文件添加至**项目目录**下的 `.github/workflows/` 目录下，并将文件中高亮行中的仓库名称替换为你的仓库名称：\n\n```yaml title=.github/workflows/build.yml\nname: Docker Hub Release\n\non:\n  push:\n    tags:\n      - \"v*\"\n\njobs:\n  docker:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v3\n\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v2\n\n      - name: Setup Docker\n        uses: docker/setup-buildx-action@v2\n\n      - name: Login to DockerHub\n        uses: docker/login-action@v2\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n      - name: Generate Tags\n        uses: docker/metadata-action@v4\n        id: metadata\n        with:\n          images: |\n            # highlight-next-line\n            {organization}/{repository}\n          tags: |\n            type=semver,pattern={{version}}\n            type=semver,pattern={{major}}.{{minor}}\n            type=sha\n            type=raw,value=latest\n\n      - name: Build and Publish\n        uses: docker/build-push-action@v4\n        with:\n          context: .\n          push: true\n          tags: ${{ steps.metadata.outputs.tags }}\n          labels: ${{ steps.metadata.outputs.labels }}\n          cache-from: type=gha\n          cache-to: type=gha,mode=max\n```\n\n### 持续部署\n\n在完成发布并构建镜像后，我们可以自动将镜像部署到服务器上。\n\n前往项目仓库的 `Settings` > `Secrets` > `actions` 栏目 `New Repository Secret` 添加部署所需的密钥：\n\n- `DEPLOY_HOST`: 部署服务器的 SSH 地址\n- `DEPLOY_USER`: 部署服务器用户名\n- `DEPLOY_KEY`: 部署服务器私钥（[创建方法](https://github.com/appleboy/ssh-action#setting-up-a-ssh-key)）\n- `DEPLOY_PATH`: 部署服务器上的项目路径\n\n将以下文件添加至**项目目录**下的 `.github/workflows/` 目录下，在构建成功后触发部署：\n\n```yaml title=.github/workflows/deploy.yml\nname: Deploy\n\non:\n  workflow_run:\n    workflows:\n      - Docker Hub Release\n    types:\n      - completed\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    if: ${{ github.event.workflow_run.conclusion == 'success' }}\n    steps:\n      - name: Start Deployment\n        uses: bobheadxi/deployments@v1\n        id: deployment\n        with:\n          step: start\n          token: ${{ secrets.GITHUB_TOKEN }}\n          env: bot\n\n      - name: Run Remote SSH Command\n        uses: appleboy/ssh-action@master\n        env:\n          DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}\n        with:\n          host: ${{ secrets.DEPLOY_HOST }}\n          username: ${{ secrets.DEPLOY_USER }}\n          key: ${{ secrets.DEPLOY_KEY }}\n          envs: DEPLOY_PATH\n          script: |\n            cd $DEPLOY_PATH\n            docker compose up -d --pull always\n\n      - name: update deployment status\n        uses: bobheadxi/deployments@v0.6.2\n        if: always()\n        with:\n          step: finish\n          token: ${{ secrets.GITHUB_TOKEN }}\n          status: ${{ job.status }}\n          env: ${{ steps.deployment.outputs.env }}\n          deployment_id: ${{ steps.deployment.outputs.deployment_id }}\n```\n\n将上一部分的 `docker-compose.yml` 文件以及 `.env.prod` 配置文件添加至 `DEPLOY_PATH` 目录下，并修改 `docker-compose.yml` 文件中的镜像配置，替换为 Docker Hub 的仓库名称：\n\n```diff\n- build: .\n+ image: {organization}/{repository}:latest\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/best-practice/error-tracking.md",
    "content": "---\nsidebar_position: 2\ndescription: 使用 sentry 进行错误跟踪\n---\n\n# 错误跟踪\n\n在应用实际运行过程中，可能会出现各种各样的错误。可能是由于代码逻辑错误，也可能是由于用户输入错误，甚至是由于第三方服务的错误。这些错误都会导致应用的运行出现问题，这时候就需要对错误进行跟踪，以便及时发现问题并进行修复。NoneBot 提供了 `nonebot-plugin-sentry` 插件，支持 [sentry](https://sentry.io/) 平台，可以方便地进行错误跟踪。\n\n## 安装插件\n\n在使用前请先安装 `nonebot-plugin-sentry` 插件至项目环境中，可参考[获取商店插件](../tutorial/store.mdx#安装插件)来了解并选择安装插件的方式。如：\n\n在**项目目录**下执行以下命令：\n\n```bash\nnb plugin install nonebot-plugin-sentry\n```\n\n## 使用插件\n\n在安装完成之后，仅需要对插件进行简单的配置即可使用。\n\n### 获取 sentry DSN\n\n前往 [sentry](https://sentry.io/) 平台，注册并创建一个新的项目，然后在项目设置中找到 `Client Keys (DSN)`，复制其中的 `DSN` 值。\n\n### 配置插件\n\n:::caution 注意\n错误跟踪通常在生产环境中使用，因此开发环境中 `sentry_dsn` 留空即会停用插件。\n:::\n\n在项目 dotenv 配置文件中添加以下配置即可使用：\n\n```dotenv\nSENTRY_DSN=<your_sentry_dsn>\n```\n\n## 配置项\n\n配置项具体含义参考 [Sentry Docs](https://docs.sentry.io/platforms/python/configuration/options/)。\n\n- `sentry_dsn: str`\n- `sentry_debug: bool = False`\n- `sentry_release: str | None = None`\n- `sentry_release: str | None = None`\n- `sentry_environment: str | None = nonebot env`\n- `sentry_server_name: str | None = None`\n- `sentry_sample_rate: float = 1.`\n- `sentry_max_breadcrumbs: int = 100`\n- `sentry_attach_stacktrace: bool = False`\n- `sentry_send_default_pii: bool = False`\n- `sentry_in_app_include: List[str] = Field(default_factory=list)`\n- `sentry_in_app_exclude: List[str] = Field(default_factory=list)`\n- `sentry_request_bodies: str = \"medium\"`\n- `sentry_with_locals: bool = True`\n- `sentry_ca_certs: str | None = None`\n- `sentry_before_send: Callable[[Any, Any], Any | None] | None = None`\n- `sentry_before_breadcrumb: Callable[[Any, Any], Any | None] | None = None`\n- `sentry_transport: Any | None = None`\n- `sentry_http_proxy: str | None = None`\n- `sentry_https_proxy: str | None = None`\n- `sentry_shutdown_timeout: int = 2`\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/best-practice/htmlkit-render.md",
    "content": "---\nsidebar_position: 8\ndescription: 轻量化 HTML 绘图\n---\n\n# 轻量化 HTML 绘图\n\n图片是机器人交互中不可或缺的一部分，对于信息展示的直观性、美观性有很大的作用。\n基于 PIL 直接绘制图片具有良好的性能和存储开销，但是难以调试、维护过程式的绘图代码。\n使用浏览器渲染类插件可以方便地绘制网页，且能够直接通过 JS 对网页效果进行编程，但是它占用的存储和内存空间相对可观。\n\nNoneBot 提供的 `nonebot-plugin-htmlkit` 提供了另一种基于 HTML 和 CSS 语法的轻量化绘图选择：它基于 `litehtml` 解析库，无须安装额外的依赖即可使用，没有进程间通信带来的额外开销，且在支持 `webp` `avif` 等丰富图片格式的前提下，安装用的 wheel 文件大小仅有约 10 MB。\n\n作为粗略的性能参考，在一台 Ryzen 7 9700X 的 Windows 电脑上，渲染 [PEP 7](https://peps.python.org/pep-0007/) 的 HTML 页面（分辨率为 800x5788，大小约 1.4MB，从本地文件系统读取 CSS）大约需要 100ms，每个渲染任务内存最高占用约为 40MB.\n\n## 安装插件\n\n在使用前请先安装 `nonebot-plugin-htmlkit` 插件至项目环境中，可参考[获取商店插件](../tutorial/store.mdx#安装插件)来了解并选择安装插件的方式。如：\n\n在**项目目录**下执行以下命令：\n\n```bash\nnb plugin install nonebot-plugin-htmlkit\n```\n\n`nonebot-plugin-htmlkit` 插件目前兼容以下系统架构：\n\n- Windows x64\n- macOS arm64（M-系列芯片）\n- Linux x64 （非 Alpine 等 musl 系发行版）\n- Linux arm64 （非 Alpine 等 musl 系发行版）\n\n:::caution 访问网络内容\n\n如果需要访问网络资源（如 http(s) 网页内容），NoneBot 需要客户端型驱动器（Forward）。内置的驱动器有 `~httpx` 与 `~aiohttp`。\n\n详见[选择驱动器](../advanced/driver.md)。\n\n:::\n\n## 使用插件\n\n### 加载插件\n\n在使用本插件前同样需要使用 `require` 方法进行**加载**并**导入**需要使用的方法，可参考 [跨插件访问](../advanced/requiring.md) 一节进行了解，如：\n\n```python\nfrom nonebot import require\n\nrequire(\"nonebot_plugin_htmlkit\")\n\nfrom nonebot_plugin_htmlkit import html_to_pic, md_to_pic, template_to_pic, text_to_pic\n```\n\n插件会自动使用[配置中的参数](#配置-fontconfig)初始化 `fontconfig` 以提供字体查找功能。\n\n### 渲染 API\n\n`nonebot-plugin-htmlkit` 主要提供以下**异步**渲染函数：\n\n#### html_to_pic\n\n```python\nasync def html_to_pic(\n    html: str,\n    *,\n    base_url: str = \"\",\n    dpi: float = 144.0,\n    max_width: float = 800.0,\n    device_height: float = 600.0,\n    default_font_size: float = 12.0,\n    font_name: str = \"sans-serif\",\n    allow_refit: bool = True,\n    image_format: Literal[\"png\", \"jpeg\"] = \"png\",\n    jpeg_quality: int = 100,\n    lang: str = \"zh\",\n    culture: str = \"CN\",\n    img_fetch_fn: ImgFetchFn = combined_img_fetcher,\n    css_fetch_fn: CSSFetchFn = combined_css_fetcher,\n    urljoin_fn: Callable[[str, str], str] = urllib3.parse.urljoin,\n) -> bytes:\n    ...\n```\n\n最核心的渲染函数。\n\n`base_url` 和 `urljoin_fn` 控制着传入 `image_fetch_fn` 和 `css_fetch_fn` 回调的 url 内容。\n\n`allow_refit` 如果为真，渲染时会自动缩小产出图片的宽度到最适合的宽度，否则必定产出 `max_width` 宽度的图片。\n\n`max_width` 与 `device_height` 会在 `@media` 判断中被使用。\n\n`img_fetch_fn` 预期为一个异步可调用对象（函数），接收图片 url 并返回对应 url 的 jpeg 或 png 二进制数据（`bytes`），可在拒绝加载时返回 `None`.\n\n`css_fetch_fn` 预期为一个异步可调用对象（函数），接收目标 CSS url 并返回对应 url 的 CSS 文本（`str`），可在拒绝加载时返回 `None`.\n\n以下为辅助的封装函数，关键字参数若未特殊说明均与 `html_to_pic` 含义相同。\n\n#### text_to_pic\n\n```python\nasync def text_to_pic(\n    text: str,\n    css_path: str = \"\",\n    *,\n    max_width: int = 500,\n    allow_refit: bool = True,\n    image_format: Literal[\"png\", \"jpeg\"] = \"png\",\n    jpeg_quality: int = 100,\n) -> bytes:\n    ...\n```\n\n可用于渲染多行文本。\n\n`text` 会被放置于 `<div id=\"main\" class=\"main-box\"> <div class=\"text\">` 中，可据此编写 CSS 来改变文本表现。\n\n#### md_to_pic\n\n```python\nasync def md_to_pic(\n    md: str = \"\",\n    md_path: str = \"\",\n    css_path: str = \"\",\n    *,\n    max_width: int = 500,\n    img_fetch_fn: ImgFetchFn = combined_img_fetcher,\n    allow_refit: bool = True,\n    image_format: Literal[\"png\", \"jpeg\"] = \"png\",\n    jpeg_quality: int = 100,\n) -> bytes:\n    ...\n```\n\n可用于渲染 Markdown 文本。默认为 GitHub Markdown Light 风格，支持基于 `pygments` 的代码高亮。\n\n`md` 和 `md_path` 二选一，前者设置时应为 Markdown 的文本，后者设置时应为指向 Markdown 文本文件的路径。\n\n#### template_to_pic\n\n```python\nasync def template_to_pic(\n    template_path: str | PathLike[str] | Sequence[str | PathLike[str]],\n    template_name: str,\n    templates: Mapping[Any, Any],\n    filters: None | Mapping[str, Any] = None,\n    *,\n    max_width: int = 500,\n    device_height: int = 600,\n    base_url: str | None = None,\n    img_fetch_fn: ImgFetchFn = combined_img_fetcher,\n    css_fetch_fn: CSSFetchFn = combined_css_fetcher,\n    allow_refit: bool = True,\n    image_format: Literal[\"png\", \"jpeg\"] = \"png\",\n    jpeg_quality: int = 100,\n) -> bytes:\n    ...\n```\n\n渲染 jinja2 模板。\n\n`template_path` 为 jinja2 环境的路径，`template_name` 是环境中要加载模板的名字，`templates` 为传入模板的参数，`filters` 为过滤器名 -> 自定义过滤器的映射。\n\n### 控制外部资源获取\n\n通过传入 `img_fetch_fn` 与 `css_fetch_fn`，我们可以在实际访问资源前进行审查，修改资源的来源，或是对 IO 操作进行缓存。\n\n`img_fetch_fn` 预期为一个异步可调用对象（函数），接收图片 url 并返回对应 url 的 jpeg 或 png 二进制数据（`bytes`），可在拒绝加载时返回 `None`.\n\n`css_fetch_fn` 预期为一个异步可调用对象（函数），接收目标 CSS url 并返回对应 url 的 CSS 文本（`str`），可在拒绝加载时返回 `None`.\n\n如果你想要禁用外部资源加载/只从文件系统加载/只从网络加载，可以使用 `none_fetcher` `filesystem_***_fetcher` `network_***_fetcher`。\n\n默认的 fetcher 行为（对于 `file://` 从文件系统加载，其余从网络加载）位于 `combined_***_fetcher`，可以通过对其封装实现缓存等操作。\n\n## 配置项\n\n### 配置 fontconfig\n\n`htmlkit` 使用 `fontconfig` 查找字体，请参阅 [`fontconfig 用户手册`](https://fontconfig.pages.freedesktop.org/fontconfig/fontconfig-user) 了解环境变量的具体含义、如何通过编写配置文件修改字体配置等。\n\n#### fontconfig_file\n\n- **类型**: `str | None`\n- **默认值**: `None`\n\n覆盖默认的配置文件路径。\n\n#### fontconfig_path\n\n- **类型**: `str | None`\n- **默认值**: `None`\n\n覆盖默认的配置目录。\n\n#### fontconfig_sysroot\n\n- **类型**: `str | None`\n- **默认值**: `None`\n\n覆盖默认的 sysroot。\n\n#### fc_debug\n\n- **类型**: `str | None`\n- **默认值**: `None`\n\n设置 Fontconfig 的 debug 级别。\n\n#### fc_dbg_match_filter\n\n- **类型**: `str | None`\n- **默认值**: `None`\n\n当 `FC_DEBUG` 设置为 `MATCH2` 时，过滤 debug 输出。\n\n#### fc_lang\n\n- **类型**: `str | None`\n- **默认值**: `None`\n\n设置默认语言，否则从 `LOCALE` 环境变量获取。\n\n#### fontconfig_use_mmap\n\n- **类型**: `str | None`\n- **默认值**: `None`\n\n是否使用 `mmap(2)` 读取字体缓存。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/best-practice/multi-adapter.mdx",
    "content": "---\nsidebar_position: 4\ndescription: 插件跨平台支持\n---\n\n# 插件跨平台支持\n\nimport Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\n\n## 使用 NoneBot 本身\n\n由于不同平台的事件与接口之间，存在着极大的差异性，NoneBot 通过[重载](../appendices/overload.md)的方式，使得插件可以在不同平台上正确响应。但为了减少跨平台的兼容性问题，我们应该尽可能的使用基类方法实现原生跨平台，而不是使用特定平台的方法。当基类方法无法满足需求时，我们可以使用依赖注入的方式，将特定平台的事件或机器人注入到事件处理函数中，实现针对特定平台的处理。\n\n:::tip 提示\n如果需要在多平台上**使用**跨平台插件，首先应该根据[注册适配器](../advanced/adapter.md#注册适配器)一节，为机器人注册各平台对应的适配器。\n:::\n\n### 基于基类的跨平台\n\n在[事件通用信息](../advanced/adapter.md#获取事件通用信息)中，我们了解了事件基类能够提供的通用信息。同时，[事件响应器操作](../appendices/session-control.mdx#更多事件响应器操作)也为我们提供了基本的用户交互方式。使用这些方法，可以让我们的插件运行在任何平台上。例如，一个简单的命令处理插件：\n\n```python {5,11}\nfrom nonebot import on_command\nfrom nonebot.adapters import Event\n\nasync def is_blacklisted(event: Event) -> bool:\n    return event.get_user_id() not in BLACKLIST\n\nweather = on_command(\"天气\", rule=is_blacklisted, priority=10, block=True)\n\n@weather.handle()\nasync def handle_function():\n    await weather.finish(\"今天的天气是...\")\n```\n\n由于此插件仅使用了事件通用信息和事件响应器操作的纯文本交互方式，这些方法不使用特定平台的信息或接口，因此是原生跨平台的，并不需要额外处理。但在一些较为复杂的需求下，例如发送图片消息时，并非所有平台都具有统一的接口，因此基类便无能为力，我们需要引入特定平台的适配器了。\n\n### 基于重载的跨平台\n\n重载是 NoneBot 跨平台操作的核心，在[事件类型与重载](../appendices/overload.md#重载)一节中，我们初步了解了如何通过类型注解来实现针对不同平台事件的处理方式。在[依赖注入](../advanced/dependency.mdx)一节中，我们又对依赖注入的使用方法进行了详细的介绍。结合这两节内容，我们可以实现更复杂的跨平台操作。\n\n#### 处理近似事件\n\n对于一系列**差异不大**的事件，我们往往具有相同的处理逻辑。这时，我们不希望将相同的逻辑编写两遍，而应该复用代码，以实现在同一个事件处理函数中处理多个近似事件。我们可以使用[事件重载](../advanced/dependency.mdx#event)的特性来实现这一功能。例如：\n\n<Tabs groupId=\"python\">\n  <TabItem value=\"3.10\" label=\"Python 3.10+\" default>\n\n```python\nfrom nonebot import on_command\nfrom nonebot.adapters import Message\nfrom nonebot.params import CommandArg\nfrom nonebot.adapters.onebot.v11 import MessageEvent as OnebotV11MessageEvent\nfrom nonebot.adapters.onebot.v12 import MessageEvent as OnebotV12MessageEvent\n\necho = on_command(\"echo\", priority=10, block=True)\n\n@echo.handle()\nasync def handle_function(event: OnebotV11MessageEvent | OnebotV12MessageEvent, args: Message = CommandArg()):\n    await echo.finish(args)\n```\n\n  </TabItem>\n  <TabItem value=\"3.9\" label=\"Python 3.9\">\n\n```python\nfrom typing import Union\n\nfrom nonebot import on_command\nfrom nonebot.adapters import Message\nfrom nonebot.params import CommandArg\nfrom nonebot.adapters.onebot.v11 import MessageEvent as OnebotV11MessageEvent\nfrom nonebot.adapters.onebot.v12 import MessageEvent as OnebotV12MessageEvent\n\necho = on_command(\"echo\", priority=10, block=True)\n\n@echo.handle()\nasync def handle_function(event: Union[OnebotV11MessageEvent, OnebotV12MessageEvent], args: Message = CommandArg()):\n    await echo.finish(args)\n```\n\n  </TabItem>\n</Tabs>\n\n#### 在依赖注入中使用重载\n\nNoneBot 依赖注入系统提供了自定义子依赖的方法，子依赖的类型同样会影响到事件处理函数的重载行为。例如：\n\n```python\nfrom datetime import datetime\n\nfrom nonebot import on_command\nfrom nonebot.adapters.console import MessageEvent\n\necho = on_command(\"echo\", priority=10, block=True)\n\ndef get_event_time(event: MessageEvent):\n    return event.time\n\n# 处理控制台消息事件\n@echo.handle()\nasync def handle_function(time: datetime = Depends(get_event_time)):\n    await echo.finish(time.strftime(\"%Y-%m-%d %H:%M:%S\"))\n```\n\n示例中 ，我们为 `handle_function` 事件处理函数注入了自定义的 `get_event_time` 子依赖，而此子依赖注入参数为 Console 适配器的 `MessageEvent`。因此 `handle_function` 仅会响应 Console 适配器的 `MessageEvent` ，而不能响应其他事件。\n\n#### 处理多平台事件\n\n不同平台的事件之间，往往存在着极大的差异性。为了满足我们插件的跨平台运行，通常我们需要抽离业务逻辑，以保证代码的复用性。一个合理的做法是，在事件响应器的处理流程中，首先先针对不同平台的事件分别进行处理，提取出核心业务逻辑所需要的信息；然后再将这些信息传递给业务逻辑处理函数；最后将业务逻辑的输出以各平台合适的方式返回给用户。也就是说，与平台绑定的处理部分应该与平台无关部分尽量分离。例如：\n\n```python\nimport inspect\n\nfrom nonebot import on_command\nfrom nonebot.typing import T_State\nfrom nonebot.matcher import Matcher\nfrom nonebot.adapters import Message\nfrom nonebot.params import CommandArg, ArgPlainText\nfrom nonebot.adapters.console import Bot as ConsoleBot\nfrom nonebot.adapters.onebot.v11 import Bot as OnebotBot\nfrom nonebot.adapters.console import MessageSegment as ConsoleMessageSegment\n\nweather = on_command(\"天气\", priority=10, block=True)\n\n@weather.handle()\nasync def handle_function(matcher: Matcher, args: Message = CommandArg()):\n    if args.extract_plain_text():\n        matcher.set_arg(\"location\", args)\n\n\nasync def get_weather(state: T_State, location: str = ArgPlainText()):\n    if location not in [\"北京\", \"上海\", \"广州\", \"深圳\"]:\n        await weather.reject(f\"你想查询的城市 {location} 暂不支持，请重新输入！\")\n\n    state[\"weather\"] = \"⛅ 多云 20℃~24℃\"\n\n\n# 处理控制台询问\n@weather.got(\n    \"location\",\n    prompt=ConsoleMessageSegment.emoji(\"question\") + \"请输入地名\",\n    parameterless=[Depends(get_weather)],\n)\nasync def handle_console(bot: ConsoleBot):\n    pass\n\n# 处理 OneBot 询问\n@weather.got(\n    \"location\",\n    prompt=\"请输入地名\",\n    parameterless=[Depends(get_weather)],\n)\nasync def handle_onebot(bot: OnebotBot):\n    pass\n\n# 通过依赖注入或事件处理函数来进行业务逻辑处理\n\n# 处理控制台回复\n@weather.handle()\nasync def handle_console_reply(bot: ConsoleBot, state: T_State, location: str = ArgPlainText()):\n    await weather.send(\n        ConsoleMessageSegment.markdown(\n            inspect.cleandoc(\n                f\"\"\"\n                # {location}\n\n                - 今天\n\n                   {state['weather']}\n                \"\"\"\n            )\n        )\n    )\n\n# 处理 OneBot 回复\n@weather.handle()\nasync def handle_onebot_reply(bot: OnebotBot, state: T_State, location: str = ArgPlainText()):\n    await weather.send(f\"今天{location}的天气是{state['weather']}\")\n```\n\n## 使用插件\n\n得益于众多开发者为 NoneBot 社区做出的贡献，我们可以通过一系列插件来完成跨平台插件的开发。\n\n这些插件可以分为三类：\n\n### 事件处理\n\n- [all4one](https://github.com/nonepkg/nonebot-plugin-all4one): 将不同平台的事件转为符合 OneBot V12 协议的插件\n  - 支持的适配器: OneBot V11/V12, Discord, QQ, Telegram\n\n### 消息处理\n\n- [alconna](https://github.com/nonebot/plugin-alconna): 对几乎所有适配器中消息的收发、撤回、编辑、表态的统一插件\n  - 支持的适配器: OneBot V11/V12, Telegram, Feishu, Github, QQ, Ding, Console, Kaiheila, Mirai, NtChat, Minecraft, Discord, Satori, Red, Dodo, Kritor, Tailchat, Mail, WXMP, Heybox, Gewechat\n- [send-anything-anywhere](https://github.com/felinae98/nonebot-plugin-send-anything-anywhere): 帮助处理不同适配器消息的适配和发送的插件\n  - 支持的适配器: OneBot V11/V12, Kaiheila, Telegram, Feishu, Red, DoDo, Satori, QQ, Discord\n\n### 会话信息提取\n\n- [uninfo](https://github.com/RF-Tar-Railt/nonebot-plugin-uninfo): 多平台的会话信息(用户、群组、频道)获取插件\n  - 支持的适配器: OneBot V11/V12, Telegram, Feishu, QQ, Console, Kaiheila, Mirai, Minecraft, Discord, Satori, Dodo, Kritor, Mail, WXMP, Gewechat\n- [session](https://github.com/noneplugin/nonebot-plugin-session): 会话信息提取与会话 id 定义插件\n  - 支持的适配器: OneBot V11/V12, Console, Kaiheila, Telegram, Feishu, Red, DoDo, Satori, QQ, Discord\n- [userinfo](https://github.com/noneplugin/nonebot-plugin-userinfo: 用户信息获取插件\n  - 支持的适配器: OneBot V11/V12, Console, Kaiheila, Telegram, Feishu, Red, DoDo, Satori, QQ, Discord\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/best-practice/scheduler.md",
    "content": "---\nsidebar_position: 0\ndescription: 定时执行任务\n---\n\n# 定时任务\n\n[APScheduler](https://apscheduler.readthedocs.io/en/3.x/) (Advanced Python Scheduler) 是一个 Python 第三方库，其强大的定时任务功能被广泛应用于各个场景。在 NoneBot 中，定时任务作为一个额外功能，依赖于基于 APScheduler 开发的 [`nonebot-plugin-apscheduler`](https://github.com/nonebot/plugin-apscheduler) 插件进行支持。\n\n## 安装插件\n\n在使用前请先安装 `nonebot-plugin-apscheduler` 插件至项目环境中，可参考[获取商店插件](../tutorial/store.mdx#安装插件)来了解并选择安装插件的方式。如：\n\n在**项目目录**下执行以下命令：\n\n```bash\nnb plugin install nonebot-plugin-apscheduler\n```\n\n## 使用插件\n\n`nonebot-plugin-apscheduler` 本质上是对 [APScheduler](https://apscheduler.readthedocs.io/en/3.x/) 进行了封装以适用于 NoneBot 开发，因此其使用方式与 APScheduler 本身并无显著区别。在此我们会简要介绍其调用方法，更多的使用方面的功能请参考[APScheduler 官方文档](https://apscheduler.readthedocs.io/en/3.x/userguide.html)。\n\n### 导入调度器\n\n由于 `nonebot_plugin_apscheduler` 作为插件，因此需要在使用前对其进行**加载**并**导入**其中的 `scheduler` 调度器来创建定时任务。使用 `require` 方法可轻松完成这一过程，可参考 [跨插件访问](../advanced/requiring.md) 一节进行了解。\n\n```python\nfrom nonebot import require\n\nrequire(\"nonebot_plugin_apscheduler\")\n\nfrom nonebot_plugin_apscheduler import scheduler\n```\n\n### 添加定时任务\n\n在 [APScheduler 官方文档](https://apscheduler.readthedocs.io/en/3.x/userguide.html#adding-jobs) 中提供了以下两种直接添加任务的方式：\n\n```python\nfrom nonebot import require\n\nrequire(\"nonebot_plugin_apscheduler\")\n\nfrom nonebot_plugin_apscheduler import scheduler\n\n# 基于装饰器的方式\n@scheduler.scheduled_job(\"cron\", hour=\"*/2\", id=\"job_0\", args=[1], kwargs={arg2: 2})\nasync def run_every_2_hour(arg1: int, arg2: int):\n    pass\n\n# 基于 add_job 方法的方式\ndef run_every_day(arg1: int, arg2: int):\n    pass\n\nscheduler.add_job(\n    run_every_day, \"interval\", days=1, id=\"job_1\", args=[1], kwargs={arg2: 2}\n)\n```\n\n:::caution 注意\n由于 APScheduler 的定时任务并不是**由事件响应器所触发的事件**，因此其任务函数无法同[事件处理函数](../tutorial/handler.mdx#事件处理函数)一样通过[依赖注入](../tutorial/event-data.mdx#认识依赖注入)获取上下文信息，也无法通过事件响应器对象的方法进行任何操作，因此我们需要使用[调用平台 API](../appendices/api-calling.mdx#调用平台-api)的方式来获取信息或收发消息。\n\n相对于事件处理依赖而言，编写定时任务更像是编写普通的函数，需要我们自行获取信息以及发送信息，请**不要**将事件处理依赖的特殊语法用于定时任务！\n:::\n\n关于 APScheduler 的更多使用方法，可以参考 [APScheduler 官方文档](https://apscheduler.readthedocs.io/en/3.x/index.html) 进行了解。\n\n### 配置项\n\n#### apscheduler_autostart\n\n- **类型**: `bool`\n- **默认值**: `True`\n\n是否自动启动 `scheduler` ，若不启动需要自行调用 `scheduler.start()`。\n\n#### apscheduler_log_level\n\n- **类型**: `int`\n- **默认值**: `30`\n\napscheduler 输出的日志等级\n\n- `WARNING` = `30` (默认)\n- `INFO` = `20`\n- `DEBUG` = `10` (只有在开启 nonebot 的 debug 模式才会显示 debug 日志)\n\n#### apscheduler_config\n\n- **类型**: `dict`\n- **默认值**: `{ \"apscheduler.timezone\": \"Asia/Shanghai\" }`\n\n`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)\n\n配置需要包含 `apscheduler.` 作为前缀，例如 `apscheduler.timezone`。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/best-practice/testing/README.mdx",
    "content": "---\nsidebar_position: 1\ndescription: 使用 NoneBug 进行单元测试\n\nslug: /best-practice/testing/\n---\n\n# 配置与测试事件响应器\n\nimport Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\n\n> 在计算机编程中，单元测试（Unit Testing）又称为模块测试，是针对程序模块（软件设计的最小单位）来进行正确性检验的测试工作。\n\n为了保证代码的正确运行，我们不仅需要对错误进行跟踪，还需要对代码进行正确性检验，也就是测试。NoneBot 提供了一个测试工具——NoneBug，它是一个 [pytest](https://docs.pytest.org/en/stable/) 插件，可以帮助我们便捷地进行单元测试。\n\n:::tip 提示\n建议在阅读本文档前先阅读 [pytest 官方文档](https://docs.pytest.org/en/stable/)来了解 pytest 的相关术语和基本用法。\n:::\n\n## 安装 NoneBug\n\n在**项目目录**下激活虚拟环境后运行以下命令安装 NoneBug：\n\n<Tabs groupId=\"tool\">\n  <TabItem value=\"poetry\" label=\"Poetry\" default>\n\n```bash\npoetry add nonebug -G test\n```\n\n  </TabItem>\n  <TabItem value=\"pdm\" label=\"PDM\">\n\n```bash\npdm add nonebug -dG test\n```\n\n  </TabItem>\n  <TabItem value=\"pip\" label=\"pip\">\n\n```bash\npip install nonebug\n```\n\n  </TabItem>\n</Tabs>\n\n要运行 NoneBug 测试，还需要额外安装 pytest 异步插件 `pytest-asyncio` 或 `anyio` 以支持异步测试。文档中，我们以 `pytest-asyncio` 为例：\n\n<Tabs groupId=\"tool\">\n  <TabItem value=\"poetry\" label=\"Poetry\" default>\n\n```bash\npoetry add pytest-asyncio -G test\n```\n\n  </TabItem>\n  <TabItem value=\"pdm\" label=\"PDM\">\n\n```bash\npdm add pytest-asyncio -dG test\n```\n\n  </TabItem>\n  <TabItem value=\"pip\" label=\"pip\">\n\n```bash\npip install pytest-asyncio\n```\n\n  </TabItem>\n</Tabs>\n\n## 配置测试\n\n在开始测试之前，我们需要对测试进行一些配置，以正确启动我们的机器人。\n\n首先我们需要配置 pytest-asyncio，在 `pyproject.toml` 的 pytest 配置部分添加：\n\n```toml\n[tool.pytest.ini_options]\nasyncio_mode = \"auto\"\nasyncio_default_fixture_loop_scope = \"session\"\n```\n\n然后，我们在 `tests` 目录下新建 `conftest.py` 文件，添加以下内容：\n\n```python title=tests/conftest.py\nimport pytest\nimport nonebot\nfrom pytest_asyncio import is_async_test\n# 导入适配器\nfrom nonebot.adapters.console import Adapter as ConsoleAdapter\n\ndef pytest_collection_modifyitems(items: list[pytest.Item]):\n    pytest_asyncio_tests = (item for item in items if is_async_test(item))\n    session_scope_marker = pytest.mark.asyncio(loop_scope=\"session\")\n    for async_test in pytest_asyncio_tests:\n        async_test.add_marker(session_scope_marker, append=False)\n\n@pytest.fixture(scope=\"session\", autouse=True)\nasync def after_nonebot_init(after_nonebot_init: None):\n    # 加载适配器\n    driver = nonebot.get_driver()\n    driver.register_adapter(ConsoleAdapter)\n\n    # 加载插件\n    nonebot.load_from_toml(\"pyproject.toml\")\n```\n\n这样，我们就可以在测试中使用机器人的插件了。通常，我们不需要自行初始化 NoneBot，NoneBug 已经为我们运行了 `nonebot.init()`。如果需要自定义 NoneBot 初始化的参数，我们可以在 `conftest.py` 中添加 `pytest_configure` 钩子函数。例如，我们可以修改 NoneBot 配置环境为 `test` 并从环境变量中输入配置：\n\n```python {4,6,8-10} title=tests/conftest.py\nimport os\n\nimport pytest\nfrom nonebug import NONEBOT_INIT_KWARGS\n\nos.environ[\"ENVIRONMENT\"] = \"test\"\n\ndef pytest_configure(config: pytest.Config):\n    config.stash[NONEBOT_INIT_KWARGS] = {\"secret\": os.getenv(\"INPUT_SECRET\")}\n```\n\nNoneBug 默认也会为我们管理 lifespan 的 startup 与 shutdown。如果不希望 NoneBug 管理 lifespan，你可以在 `pytest_configure` 里添加以下配置：\n\n```python\nimport pytest\nfrom nonebug import NONEBOT_START_LIFESPAN\n\ndef pytest_configure(config: pytest.Config):\n    config.stash[NONEBOT_START_LIFESPAN] = False\n```\n\n## 编写插件测试\n\n在配置完成插件加载后，我们就可以在测试中使用插件了。NoneBug 通过 pytest fixture `app` 提供各种测试方法，我们可以在测试中使用它来测试插件。现在，我们创建一个测试脚本来测试[深入指南](../../appendices/session-control.mdx)中编写的天气插件。首先，我们先要导入我们需要的模块：\n\n<details>\n  <summary>插件示例</summary>\n\n```python title=weather/__init__.py\nfrom nonebot import on_command\nfrom nonebot.rule import to_me\nfrom nonebot.matcher import Matcher\nfrom nonebot.adapters import Message\nfrom nonebot.params import CommandArg, ArgPlainText\n\nweather = on_command(\"天气\", rule=to_me(), aliases={\"weather\", \"天气预报\"})\n\n@weather.handle()\nasync def handle_function(matcher: Matcher, args: Message = CommandArg()):\n    if args.extract_plain_text():\n        matcher.set_arg(\"location\", args)\n\n@weather.got(\"location\", prompt=\"请输入地名\")\nasync def got_location(location: str = ArgPlainText()):\n    if location not in [\"北京\", \"上海\", \"广州\", \"深圳\"]:\n        await weather.reject(f\"你想查询的城市 {location} 暂不支持，请重新输入！\")\n    await weather.finish(f\"今天{location}的天气是...\")\n```\n\n</details>\n\n```python {4,5,9,11-16} title=tests/test_weather.py\nfrom datetime import datetime\n\nimport pytest\nfrom nonebug import App\nfrom nonebot.adapters.console import User, Message, MessageEvent\n\n@pytest.mark.asyncio\nasync def test_weather(app: App):\n    from awesome_bot.plugins.weather import weather\n\n    event = MessageEvent(\n        time=datetime.now(),\n        self_id=\"test\",\n        message=Message(\"/天气 北京\"),\n        user=User(id=\"user\"),\n    )\n```\n\n在上面的代码中，我们引入了 NoneBug 的测试 `App` 对象，以及必要的适配器消息与事件定义等。在测试函数 `test_weather` 中，我们导入了要进行测试的事件响应器 `weather`。请注意，由于需要等待 NoneBot 初始化并加载插件完毕，插件内容必须在**测试函数内部**进行导入。然后，我们创建了一个 `MessageEvent` 事件对象，它模拟了一个用户发送了 `/天气 北京` 的消息。接下来，我们使用 `app.test_matcher` 方法来测试 `weather` 事件响应器：\n\n```python {11-15} title=tests/test_weather.py\n@pytest.mark.asyncio\nasync def test_weather(app: App):\n    from awesome_bot.plugins.weather import weather\n\n    event = MessageEvent(\n        time=datetime.now(),\n        self_id=\"test\",\n        message=Message(\"/天气 北京\"),\n        user=User(id=\"user\"),\n    )\n    async with app.test_matcher(weather) as ctx:\n        bot = ctx.create_bot()\n        ctx.receive_event(bot, event)\n        ctx.should_call_send(event, \"今天北京的天气是...\", result=None)\n        ctx.should_finished(weather)\n```\n\n这里我们使用 `async with` 语句并通过参数指定要测试的事件响应器 `weather` 来进入测试上下文。在测试上下文中，我们可以使用 `ctx.create_bot` 方法创建一个虚拟的机器人实例，并使用 `ctx.receive_event` 方法来模拟机器人接收到消息事件。然后，我们就可以定义预期行为来测试机器人是否正确运行。在上面的代码中，我们使用 `ctx.should_call_send` 方法来断言机器人应该发送 `今天北京的天气是...` 这条消息，并且将发送函数的调用结果作为第三个参数返回给事件处理函数。如果断言失败，测试将会不通过。我们也可以使用 `ctx.should_finished` 方法来断言机器人应该结束会话。\n\n为了测试更复杂的情况，我们可以为添加更多的测试用例。例如，我们可以测试用户输入了一个不支持的地名时机器人的反应：\n\n```python {17-21,23-26} title=tests/test_weather.py\ndef make_event(message: str = \"\") -> MessageEvent:\n    return MessageEvent(\n        time=datetime.now(),\n        self_id=\"test\",\n        message=Message(message),\n        user=User(id=\"user\"),\n    )\n\n@pytest.mark.asyncio\nasync def test_weather(app: App):\n    from awesome_bot.plugins.weather import weather\n\n    async with app.test_matcher(weather) as ctx:\n        ...  # 省略前面的测试用例\n\n    async with app.test_matcher(weather) as ctx:\n        bot = ctx.create_bot()\n        event = make_event(\"/天气 南京\")\n        ctx.receive_event(bot, event)\n        ctx.should_call_send(event, \"你想查询的城市 南京 暂不支持，请重新输入！\", result=None)\n        ctx.should_rejected(weather)\n\n        event = make_event(\"北京\")\n        ctx.receive_event(bot, event)\n        ctx.should_call_send(event, \"今天北京的天气是...\", result=None)\n        ctx.should_finished(weather)\n```\n\n在上面的代码中，我们使用 `ctx.should_rejected` 来断言机器人应该请求用户重新输入。然后，我们再次使用 `ctx.receive_event` 方法来模拟用户回复了 `北京`，并使用 `ctx.should_finished` 来断言机器人应该结束会话。\n\n更多的 NoneBug 用法将在后续章节中介绍。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/best-practice/testing/_category_.json",
    "content": "{\n  \"label\": \"单元测试\",\n  \"position\": 5\n}\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/best-practice/testing/behavior.mdx",
    "content": "---\nsidebar_position: 2\ndescription: 测试事件响应、平台接口调用和会话控制\n---\n\n# 测试事件响应与会话操作\n\nimport Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\n\n在 NoneBot 接收到事件时，事件响应器根据优先级依次通过权限、响应规则来判断当前事件是否应该触发。事件响应流程中，机器人可能会通过 `send` 发送消息或者调用平台接口来执行预期的操作。因此，我们需要对这两种操作进行单元测试。\n\n在上一节中，我们对单个事件响应器进行了简单测试。但是在实际场景中，机器人可能定义了多个事件响应器，由于优先级和响应规则的存在，预期的事件响应器可能并不会被触发。NoneBug 支持同时测试多个事件响应器，以此来测试机器人的整体行为。\n\n## 测试事件响应\n\nNoneBug 提供了六种定义 `Rule` 和 `Permission` 预期行为的方法：\n\n- `should_pass_rule`\n- `should_not_pass_rule`\n- `should_ignore_rule`\n- `should_pass_permission`\n- `should_not_pass_permission`\n- `should_ignore_permission`\n\n:::tip 提示\n事件响应器类型的检查属于 `Permission` 的一部分，因此可以通过 `should_pass_permission` 和 `should_not_pass_permission` 方法来断言事件响应器类型的检查。\n:::\n\n下面我们根据插件示例来测试事件响应行为，我们首先定义两个事件响应器作为测试的对象：\n\n```python title=example.py\nfrom nonebot import on_command\n\ndef never_pass():\n    return False\n\nfoo = on_command(\"foo\")\nbar = on_command(\"bar\", permission=never_pass)\n```\n\n在这两个事件响应器中，`foo` 当收到 `/foo` 消息时会执行，而 `bar` 则不会执行。我们使用 NoneBug 来测试它们：\n\n<Tabs groupId=\"testScope\">\n  <TabItem value=\"separate\" label=\"独立测试\" default>\n\n```python {21,22,28,29} title=tests/test_example.py\nfrom datetime import datetime\n\nimport pytest\nfrom nonebug import App\nfrom nonebot.adapters.console import User, Message, MessageEvent\n\ndef make_event(message: str = \"\") -> MessageEvent:\n    return MessageEvent(\n        time=datetime.now(),\n        self_id=\"test\",\n        message=Message(message),\n        user=User(id=\"user\"),\n    )\n\n@pytest.mark.asyncio\nasync def test_example(app: App):\n    from awesome_bot.plugins.example import foo, bar\n\n    async with app.test_matcher(foo) as ctx:\n        bot = ctx.create_bot()\n        event = make_event(\"/foo\")\n        ctx.receive_event(bot, event)\n        ctx.should_pass_rule()\n        ctx.should_pass_permission()\n\n    async with app.test_matcher(bar) as ctx:\n        bot = ctx.create_bot()\n        event = make_event(\"/foo\")\n        ctx.receive_event(bot, event)\n        ctx.should_not_pass_rule()\n        ctx.should_not_pass_permission()\n```\n\n在上面的代码中，我们分别对 `foo` 和 `bar` 事件响应器进行响应测试。我们使用 `ctx.should_pass_rule` 和 `ctx.should_pass_permission` 断言 `foo` 事件响应器应该被触发，使用 `ctx.should_not_pass_rule` 和 `ctx.should_not_pass_permission` 断言 `bar` 事件响应器应该被忽略。\n\n  </TabItem>\n  <TabItem value=\"global\" label=\"集成测试\">\n\n```python title=tests/test_example.py\nfrom datetime import datetime\n\nimport pytest\nfrom nonebug import App\nfrom nonebot.adapters.console import User, Message, MessageEvent\n\ndef make_event(message: str = \"\") -> MessageEvent:\n    return MessageEvent(\n        time=datetime.now(),\n        self_id=\"test\",\n        message=Message(message),\n        user=User(id=\"user\"),\n    )\n\n@pytest.mark.asyncio\nasync def test_example(app: App):\n    from awesome_bot.plugins.example import foo, bar\n\n    async with app.test_matcher() as ctx:\n        bot = ctx.create_bot()\n        event = make_event(\"/foo\")\n        ctx.receive_event(bot, event)\n        ctx.should_pass_rule(foo)\n        ctx.should_pass_permission(foo)\n        ctx.should_not_pass_rule(bar)\n        ctx.should_not_pass_permission(bar)\n```\n\n在上面的代码中，我们对 `foo` 和 `bar` 事件响应器一起进行响应测试。我们使用 `ctx.should_pass_rule` 和 `ctx.should_pass_permission` 断言 `foo` 事件响应器应该被触发，使用 `ctx.should_not_pass_rule` 和 `ctx.should_not_pass_permission` 断言 `bar` 事件响应器应该被忽略。通过参数，我们可以指定断言的事件响应器。\n\n  </TabItem>\n</Tabs>\n\n当然，如果需要忽略某个事件响应器的响应规则和权限检查，强行进入响应流程，我们可以使用 `should_ignore_rule` 和 `should_ignore_permission` 方法：\n\n```python {21,22} title=tests/test_example.py\nfrom datetime import datetime\n\nimport pytest\nfrom nonebug import App\nfrom nonebot.adapters.console import User, Message, MessageEvent\n\ndef make_event(message: str = \"\") -> MessageEvent:\n    return MessageEvent(\n        time=datetime.now(),\n        self_id=\"test\",\n        message=Message(message),\n        user=User(id=\"user\"),\n    )\n\n@pytest.mark.asyncio\nasync def test_example(app: App):\n    from awesome_bot.plugins.example import foo, bar\n\n    async with app.test_matcher(bar) as ctx:\n        bot = ctx.create_bot()\n        event = make_event(\"/foo\")\n        ctx.receive_event(bot, event)\n        ctx.should_ignore_rule(bar)\n        ctx.should_ignore_permission(bar)\n```\n\n在忽略了响应规则和权限检查之后，就会进入 `bar` 事件响应器的响应流程。\n\n## 测试平台接口使用\n\n上一节的示例插件测试中，我们已经尝试了测试插件对事件的消息回复。通常情况下，事件处理流程中对平台接口的使用会通过事件响应器操作或者调用平台 API 两种途径进行。针对这两种途径，NoneBug 分别提供了 `ctx.should_call_send` 和 `ctx.should_call_api` 方法来测试平台接口的使用情况。\n\n1. `should_call_send`\n\n   定义事件响应器预期发送的消息，即通过[事件响应器操作 send](../../appendices/session-control.mdx#send)进行的操作。`should_call_send` 有四个参数：\n   - `event`：回复的目标事件。\n   - `message`：预期的消息对象，可以是 `str`、`Message` 或 `MessageSegment`。\n   - `result`：send 的返回值，将会返回给插件。\n   - `bot`（可选）：发送消息的 bot 对象。\n   - `**kwargs`：send 方法的额外参数。\n\n2. `should_call_api`\n   定义事件响应器预期调用的平台 API 接口，即通过[调用平台 API](../../appendices/api-calling.mdx#调用平台-api)进行的操作。`should_call_api` 有四个参数：\n   - `api`：API 名称。\n   - `data`：预期的请求数据。\n   - `result`：call_api 的返回值，将会返回给插件。\n   - `adapter`（可选）：调用 API 的平台适配器对象。\n   - `**kwargs`：call_api 方法的额外参数。\n\n下面是一个使用 `should_call_send` 和 `should_call_api` 方法的示例：\n\n我们先定义一个测试插件，在响应流程中向用户发送一条消息并调用 `Console` 适配器的 `bell` API。\n\n```python {8,9} title=example.py\nfrom nonebot import on_command\nfrom nonebot.adapters.console import Bot\n\nfoo = on_command(\"foo\")\n\n@foo.handle()\nasync def _(bot: Bot):\n    await foo.send(\"message\")\n    await bot.bell()\n```\n\n然后我们对该插件进行测试：\n\n```python title=tests/test_example.py\nfrom datetime import datetime\n\nimport pytest\nimport nonebot\nfrom nonebug import App\nfrom nonebot.adapters.console import Bot, User, Adapter, Message, MessageEvent\n\ndef make_event(message: str = \"\") -> MessageEvent:\n    return MessageEvent(\n        time=datetime.now(),\n        self_id=\"test\",\n        message=Message(message),\n        user=User(id=\"user\"),\n    )\n\n@pytest.mark.asyncio\nasync def test_example(app: App):\n    from awesome_bot.plugins.example import foo\n\n    async with app.test_matcher(foo) as ctx:\n        # highlight-start\n        adapter = nonebot.get_adapter(Adapter)\n        bot = ctx.create_bot(base=Bot, adapter=adapter)\n        # highlight-end\n        event = make_event(\"/foo\")\n        ctx.receive_event(bot, event)\n        # highlight-start\n        ctx.should_call_send(event, \"message\", result=None, bot=bot)\n        ctx.should_call_api(\"bell\", {}, result=None, adapter=adapter)\n        # highlight-end\n```\n\n请注意，对于在依赖注入中使用了非基类对象的情况，我们需要在 `create_bot` 方法中指定 `base` 和 `adapter` 参数，确保不会因为重载功能而出现非预期情况。\n\n## 测试会话控制\n\n在[会话控制](../../appendices/session-control.mdx)一节中，我们介绍了如何使用事件响应器操作来实现对用户的交互式会话。在上一节的示例插件测试中，我们其实已经使用了 `ctx.should_finished` 来断言会话结束。NoneBug 针对各种流程控制操作分别提供了相应的方法来定义预期的会话处理行为。它们分别是：\n\n- `should_finished`：断言会话结束，对应 `matcher.finish` 操作。\n- `should_rejected`：断言会话等待用户输入并重新执行当前事件处理函数，对应 `matcher.reject` 系列操作。\n- `should_paused`: 断言会话等待用户输入并执行下一个事件处理函数，对应 `matcher.pause` 操作。\n\n我们仅需在测试用例中的正确位置调用这些方法，就可以断言会话的预期行为。例如：\n\n```python title=example.py\nfrom nonebot import on_command\nfrom nonebot.typing import T_State\n\nfoo = on_command(\"foo\")\n\n@foo.got(\"key\", prompt=\"请输入密码\")\nasync def _(state: T_State, key: str = ArgPlainText()):\n    if key != \"some password\":\n        try_count = state.get(\"try_count\", 1)\n        if try_count >= 3:\n            await foo.finish(\"密码错误次数过多\")\n        else:\n            state[\"try_count\"] = try_count + 1\n            await foo.reject(\"密码错误，请重新输入\")\n    await foo.finish(\"密码正确\")\n```\n\n```python title=tests/test_example.py\nfrom datetime import datetime\n\nimport pytest\nfrom nonebug import App\nfrom nonebot.adapters.console import User, Message, MessageEvent\n\ndef make_event(message: str = \"\") -> MessageEvent:\n    return MessageEvent(\n        time=datetime.now(),\n        self_id=\"test\",\n        message=Message(message),\n        user=User(id=\"user\"),\n    )\n\n@pytest.mark.asyncio\nasync def test_example(app: App):\n    from awesome_bot.plugins.example import foo\n\n    async with app.test_matcher(foo) as ctx:\n        bot = ctx.create_bot()\n        event = make_event(\"/foo\")\n        ctx.receive_event(bot, event)\n        ctx.should_call_send(event, \"请输入密码\", result=None)\n        ctx.should_rejected(foo)\n        event = make_event(\"wrong password\")\n        ctx.receive_event(bot, event)\n        ctx.should_call_send(event, \"密码错误，请重新输入\", result=None)\n        ctx.should_rejected(foo)\n        event = make_event(\"wrong password\")\n        ctx.receive_event(bot, event)\n        ctx.should_call_send(event, \"密码错误，请重新输入\", result=None)\n        ctx.should_rejected(foo)\n        event = make_event(\"wrong password\")\n        ctx.receive_event(bot, event)\n        ctx.should_call_send(event, \"密码错误次数过多\", result=None)\n        ctx.should_finished(foo)\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/best-practice/testing/mock-network.md",
    "content": "---\nsidebar_position: 3\ndescription: 模拟网络通信以进行测试\n---\n\n# 模拟网络通信\n\nNoneBot 驱动器提供了多种方法来帮助适配器进行网络通信，主要包括客户端和服务端两种类型。模拟网络通信可以帮助我们更加接近实际机器人应用场景，进行更加真实的集成测试。同时，通过这种途径，我们还可以完成对适配器的测试。\n\nNoneBot 中的网络通信主要包括以下几种：\n\n- HTTP 服务端（WebHook）\n- WebSocket 服务端\n- HTTP 客户端\n- WebSocket 客户端\n\n下面我们将分别介绍如何使用 NoneBug 来模拟这几种通信方式。\n\n## 测试 HTTP 服务端\n\n当 NoneBot 作为 ASGI 服务端应用时，我们可以定义一系列的路由来处理 HTTP 请求，适配器同样也可以通过定义路由来响应机器人相关的网络通信。下面假设我们使用了一个适配器 `fake` ，它定义了一个路由 `/fake/http` ，用于接收平台 WebHook 并处理。实际应用测试时，应将该路由地址替换为**真实适配器注册的路由地址**。\n\n我们首先需要获取测试用模拟客户端：\n\n```python {5,6} title=tests/test_http_server.py\nfrom nonebug import App\n\n@pytest.mark.asyncio\nasync def test_http_server(app: App):\n    async with app.test_server() as ctx:\n        client = ctx.get_client()\n```\n\n默认情况下，`app.test_server()` 会通过 `nonebot.get_asgi` 获取测试对象，我们也可以通过参数指定 ASGI 应用：\n\n```python\nasync with app.test_server(asgi=asgi_app) as ctx:\n    ...\n```\n\n获取到模拟客户端后，即可像 `requests`、`httpx` 等库类似的方法进行使用：\n\n```python {3,11-14,16} title=tests/test_http_server.py\nimport nonebot\nfrom nonebug import App\nfrom nonebot.adapters.fake import Adapter\n\n@pytest.mark.asyncio\nasync def test_http_server(app: App):\n    adapter = nonebot.get_adapter(Adapter)\n\n    async with app.test_server() as ctx:\n        client = ctx.get_client()\n        response = await client.post(\"/fake/http\", json={\"bot_id\": \"fake\"})\n        assert response.status_code == 200\n        assert response.json() == {\"status\": \"success\"}\n        assert \"fake\" in nonebot.get_bots()\n\n    adapter.bot_disconnect(nonebot.get_bot(\"fake\"))\n```\n\n在上面的测试中，我们向 `/fake/http` 发送了一个模拟 POST 请求，适配器将会对该请求进行处理，我们可以通过检查请求返回是否正确、Bot 对象是否创建等途径来验证机器人是否正确运行。在完成测试后，我们通常需要对 Bot 对象进行清理，以避免对其他测试产生影响。\n\n## 测试 WebSocket 服务端\n\n当 NoneBot 作为 ASGI 服务端应用时，我们还可以定义一系列的路由来处理 WebSocket 通信。下面假设我们使用了一个适配器 `fake` ，它定义了一个路由 `/fake/ws` ，用于处理平台 WebSocket 连接信息。实际应用测试时，应将该路由地址替换为**真实适配器注册的路由地址**。\n\n我们同样需要通过 `app.test_server()` 获取测试用模拟客户端，这里就不再赘述。在获取到模拟客户端后，我们可以通过 `client.websocket_connect` 方法来模拟 WebSocket 连接：\n\n```python {3,11-15} title=tests/test_ws_server.py\nimport nonebot\nfrom nonebug import App\nfrom nonebot.adapters.fake import Adapter\n\n@pytest.mark.asyncio\nasync def test_ws_server(app: App):\n    adapter = nonebot.get_adapter(Adapter)\n\n    async with app.test_server() as ctx:\n        client = ctx.get_client()\n        async with client.websocket_connect(\"/fake/ws\") as ws:\n            await ws.send_json({\"bot_id\": \"fake\"})\n            response = await ws.receive_json()\n            assert response == {\"status\": \"success\"}\n            assert \"fake\" in nonebot.get_bots()\n```\n\n在上面的测试中，我们向 `/fake/ws` 进行了 WebSocket 模拟通信，通过发送消息与机器人进行交互，然后检查机器人发送的信息是否正确。\n\n## 测试 HTTP 客户端\n\n~~暂不支持~~\n\n## 测试 WebSocket 客户端\n\n~~暂不支持~~\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/community/contact.md",
    "content": "---\nsidebar-position: 0\ndescription: 遇到问题如何获取帮助\n---\n\n# 参与讨论\n\n如果在安装或者开发 NoneBot 过程中遇到了任何问题，或者有新奇的点子，欢迎参与我们的社区讨论：\n\n1. 点击下方链接前往 GitHub，前往 Issues 页面，在 `New Issue` Template 中选择 `Question`\n\n   NoneBot：[![NoneBot project link](https://img.shields.io/github/stars/nonebot/nonebot2?style=social)](https://github.com/nonebot/nonebot2)\n\n2. 通过 QQ 群（点击下方链接直达）\n\n   [![QQ Chat Group](https://img.shields.io/badge/QQ%E7%BE%A4-768887710-orange?style=social)](https://jq.qq.com/?_wv=1027&k=5OFifDh)\n\n3. 通过 QQ 频道\n\n   [![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)\n\n4. 通过 Discord 服务器（点击下方链接直达）\n\n   [![Discord Server](https://discordapp.com/api/guilds/847819937858584596/widget.png?style=shield)](https://discord.gg/VKtE6Gdc4h)\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/community/contributing.md",
    "content": "---\nsidebar-position: 1\ndescription: 如何为 NoneBot 贡献代码\n---\n\n# 贡献指南\n\n## Code of Conduct\n\n请参阅 [Code of Conduct](https://github.com/nonebot/nonebot2/blob/master/CODE_OF_CONDUCT.md)。\n\n## 参与开发\n\n请参阅 [Contributing](https://github.com/nonebot/nonebot2/blob/master/CONTRIBUTING.md)。\n\n## 鸣谢\n\n感谢以下开发者对 NoneBot2 作出的贡献：\n\n<a href=\"https://github.com/nonebot/nonebot2/graphs/contributors\">\n  <img src=\"https://contrib.rocks/image?repo=nonebot/nonebot2&max=1000\" />\n</a>\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/developer/adapter-writing.md",
    "content": "---\nsidebar_position: 1\ndescription: 编写适配器对接新的平台\n---\n\n# 编写适配器\n\n在编写适配器之前，我们需要先了解[适配器的功能与组成](../advanced/adapter#适配器功能与组成)，适配器通常由 `Adapter`、`Bot`、`Event` 和 `Message` 四个部分组成，在编写适配器时，我们需要继承 NoneBot 中的基类，并根据实际平台来编写每个部分功能。\n\n## 组织结构\n\nNoneBot 适配器项目通常以 `nonebot-adapter-{adapter-name}` 作为项目名，并以**命名空间包**的形式编写，即在 `nonebot/adapters/{adapter-name}` 目录中编写实际代码，例如：\n\n```tree\n📦 nonebot-adapter-{adapter-name}\n├── 📂 nonebot\n│   ├── 📂 adapters\n│   │   ├── 📂 {adapter-name}\n│   │   │   ├── 📜 __init__.py\n│   │   │   ├── 📜 adapter.py\n│   │   │   ├── 📜 bot.py\n│   │   │   ├── 📜 config.py\n│   │   │   ├── 📜 event.py\n│   │   │   └── 📜 message.py\n├── 📜 pyproject.toml\n└── 📜 README.md\n```\n\n:::tip 提示\n\n上述的项目结构仅作推荐，不做强制要求，保证实际可用性即可。\n\n:::\n\n### 使用 NB-CLI 创建项目\n\n我们可以使用脚手架快速创建项目：\n\n```shell\nnb adapter create\n```\n\n按照指引，输入适配器名称以及存储位置，即可创建一个带有基本结构的适配器项目。\n\n## 组成部分\n\n:::tip 提示\n\n本章节的代码中提到的 `Adapter`、`Bot`、`Event` 和 `Message` 等，均为下文中适配器所编写的类，而非 NoneBot 中的基类。\n\n:::\n\n### Log\n\n适配器在处理时通常需要打印日志，但直接使用 NoneBot 的默认 `logger` 不方便区分适配器输出和其它日志。因此我们可以使用 NoneBot 提供的 `logger_wrapper` 方法，自定义一个 `log` 函数用于快捷打印适配器日志：\n\n```python {3} title=log.py\nfrom nonebot.utils import logger_wrapper\n\nlog = logger_wrapper(\"your_adapter_name\")\n```\n\n这个 `log` 函数会在默认 `logger` 中添加适配器名称前缀，它接收三个参数：日志等级、日志内容以及可选的异常，具体用法如下：\n\n```python\nfrom .log import log\n\nlog(\"DEBUG\", \"A DEBUG log.\")\nlog(\"INFO\", \"A INFO log.\")\n\ntry:\n    ...\nexcept Exception as e:\n    log(\"ERROR\", \"something error.\", e)\n```\n\n### Config\n\n通常适配器需要一些配置项，例如平台连接密钥等。适配器的配置方法与[插件配置](../appendices/config#%E6%8F%92%E4%BB%B6%E9%85%8D%E7%BD%AE)类似，例如：\n\n```python title=config.py\nfrom pydantic import BaseModel\n\nclass Config(BaseModel):\n    xxx_id: str\n    xxx_token: str\n```\n\n配置项的读取将在下方 [Adapter](#adapter) 中介绍。\n\n### Adapter\n\nAdapter 负责转换事件、调用接口，以及正确创建 Bot 对象并注册到 NoneBot 中。在编写平台相关内容之前，我们需要继承基类，并实现适配器的基本信息：\n\n```python {9,11,14,18} title=adapter.py\nfrom typing import Any\nfrom typing_extensions import override\n\nfrom nonebot.drivers import Driver\nfrom nonebot import get_plugin_config\nfrom nonebot.adapters import Adapter as BaseAdapter\n\nfrom .config import Config\n\nclass Adapter(BaseAdapter):\n    @override\n    def __init__(self, driver: Driver, **kwargs: Any):\n        super().__init__(driver, **kwargs)\n        # 读取适配器所需的配置项\n        self.adapter_config: Config = get_plugin_config(Config)\n\n    @classmethod\n    @override\n    def get_name(cls) -> str:\n        \"\"\"适配器名称\"\"\"\n        return \"your_adapter_name\"\n```\n\n#### 与平台交互\n\nNoneBot 提供了多种 [Driver](../advanced/driver) 来帮助适配器进行网络通信，主要分为客户端和服务端两种类型。我们需要**根据平台文档和特性**选择合适的通信方式，并编写相关方法用于初始化适配器，与平台建立连接和进行交互：\n\n##### 客户端通信方式\n\n```python {12,23,24} title=adapter.py\nimport asyncio\nfrom typing_extensions import override\n\nfrom nonebot import get_plugin_config\nfrom nonebot.exception import WebSocketClosed\nfrom nonebot.drivers import Request, WebSocketClientMixin\n\nclass Adapter(BaseAdapter):\n    @override\n    def __init__(self, driver: Driver, **kwargs: Any):\n        super().__init__(driver, **kwargs)\n        self.adapter_config: Config = get_plugin_config(Config)\n        self.task: Optional[asyncio.Task] = None  # 存储 ws 任务\n        self.setup()\n\n    def setup(self) -> None:\n        if not isinstance(self.driver, WebSocketClientMixin):\n            # 判断用户配置的Driver类型是否符合适配器要求，不符合时应抛出异常\n            raise RuntimeError(\n                f\"Current driver {self.config.driver} doesn't support websocket client connections!\"\n                f\"{self.get_name()} Adapter need a WebSocket Client Driver to work.\"\n            )\n        # 在 NoneBot 启动和关闭时进行相关操作\n        self.driver.on_startup(self.startup)\n        self.driver.on_shutdown(self.shutdown)\n\n    async def startup(self) -> None:\n        \"\"\"定义启动时的操作，例如和平台建立连接\"\"\"\n        self.task = asyncio.create_task(self._forward_ws())  # 建立 ws 连接\n\n    async def _forward_ws(self):\n        request = Request(\n            method=\"GET\",\n            url=\"your_platform_websocket_url\",\n            headers={\"token\": \"...\"},  # 鉴权请求头\n        )\n        while True:\n            try:\n                async with self.websocket(request) as ws:\n                    try:\n                        # 处理 websocket\n                        ...\n                    except WebSocketClosed as e:\n                        log(\n                            \"ERROR\",\n                            \"<r><bg #f8bbd0>WebSocket Closed</bg #f8bbd0></r>\",\n                            e,\n                        )\n                    except Exception as e:\n                        log(\n                            \"ERROR\",\n                            \"<r><bg #f8bbd0>Error while process data from \"\n                            \"websocket platform_websocket_url. \"\n                            \"Trying to reconnect...</bg #f8bbd0></r>\",\n                            e,\n                        )\n                    finally:\n                        # 这里要断开 Bot 连接\n            except Exception as e:\n                # 尝试重连\n                log(\n                    \"ERROR\",\n                    \"<r><bg #f8bbd0>Error while setup websocket to \"\n                    \"platform_websocket_url. Trying to reconnect...</bg #f8bbd0></r>\",\n                    e,\n                )\n                await asyncio.sleep(3)  # 重连间隔\n\n    async def shutdown(self) -> None:\n        \"\"\"定义关闭时的操作，例如停止任务、断开连接\"\"\"\n\n        # 断开 ws 连接\n        if self.task is not None and not self.task.done():\n            self.task.cancel()\n```\n\n##### 服务端通信方式\n\n```python {30,38} title=adapter.py\nfrom nonebot import get_plugin_config\nfrom nonebot.drivers import (\n    Request,\n    ASGIMixin,\n    WebSocket,\n    HTTPServerSetup,\n    WebSocketServerSetup\n)\n\nclass Adapter(BaseAdapter):\n    @override\n    def __init__(self, driver: Driver, **kwargs: Any):\n        super().__init__(driver, **kwargs)\n        self.adapter_config: Config = get_plugin_config(Config)\n        self.setup()\n\n    def setup(self) -> None:\n        if not isinstance(self.driver, ASGIMixin):\n            raise RuntimeError(\n                f\"Current driver {self.config.driver} doesn't support asgi server!\"\n                f\"{self.get_name()} Adapter need a asgi server driver to work.\"\n            )\n        # 建立服务端路由\n        # HTTP Webhook 路由\n        http_setup = HTTPServerSetup(\n            URL(\"your_webhook_url\"),  # 路由地址\n            \"POST\",  # 接收的方法\n            \"WEBHOOK name\",  # 路由名称\n            self._handle_http,  # 处理函数\n        )\n        self.setup_http_server(http_setup)\n\n        # 反向 Websocket 路由\n        ws_setup = WebSocketServerSetup(\n            URL(\"your_websocket_url\"),  # 路由地址\n            \"WebSocket name\",  # 路由名称\n            self._handle_ws,  # 处理函数\n        )\n        self.setup_websocket_server(ws_setup)\n\n\n    async def _handle_http(self, request: Request) -> Response:\n        \"\"\"HTTP 路由处理函数，只有一个类型为 Request 的参数，且返回值类型为 Response\"\"\"\n        ...\n        return Response(\n            status_code=200,  # 状态码\n            headers={\"something\": \"something\"},  # 响应头\n            content=\"xxx\",  # 响应内容\n        )\n\n    async def _handle_ws(self, websocket: WebSocket) -> Any:\n        \"\"\"WebSocket 路由处理函数，只有一个类型为 WebSocket 的参数\"\"\"\n        ...\n```\n\n更多通信交互方式可以参考以下适配器：\n\n- [OneBot](https://github.com/nonebot/adapter-onebot/blob/master/nonebot/adapters/onebot/v11/adapter.py) - `WebSocket 客户端`、`WebSocket 服务端`、`HTTP WEBHOOK`、`HTTP POST`\n- [QQ](https://github.com/nonebot/adapter-qq/blob/master/nonebot/adapters/qq/adapter.py) - `WebSocket 服务端`、`HTTP WEBHOOK`\n- [Telegram](https://github.com/nonebot/adapter-telegram/blob/beta/nonebot/adapters/telegram/adapter.py) - `HTTP WEBHOOK`\n\n#### 建立 Bot 连接\n\n在与平台建立连接后，我们需要将 [Bot](#bot) 实例化，并调用适配器提供的的 `bot_connect` 方法告知 NoneBot 建立了 Bot 连接。在与平台断开连接或出现某些异常进行重连时，我们需要调用 `bot_disconnect` 方法告知 NoneBot 断开了 Bot 连接。\n\n```python {7,8,11} title=adapter.py\nfrom .bot import Bot\n\nclass Adapter(BaseAdapter):\n\n    def _handle_connect(self):\n        bot_id = ...  # 通过配置或者平台 API 等方式，获取到 Bot 的 ID\n        bot = Bot(self, self_id=bot_id)  # 实例化 Bot\n        self.bot_connect(bot)  # 建立 Bot 连接\n\n    def _handle_disconnect(self):\n        self.bot_disconnect(bot)  # 断开 Bot 连接\n```\n\n#### 转换 Event 事件\n\n在接收到来自平台的事件数据后，我们需要将其转为适配器的 [Event](#event)，并调用 Bot 的 `handle_event` 方法来让 Bot 对事件进行处理：\n\n```python title=adapter.py\nimport asyncio\nfrom typing import Any, Dict\n\nfrom nonebot.compat import type_validate_python\n\nfrom .bot import Bot\nfrom .event import Event\nfrom .log import log\n\nclass Adapter(BaseAdapter):\n\n    @classmethod\n    def payload_to_event(cls, payload: Dict[str, Any]) -> Event:\n        \"\"\"根据平台事件的特性，转换平台 payload 为具体 Event\n\n        Event 模型继承自 pydantic.BaseModel，具体请参考 pydantic 文档\n        \"\"\"\n\n        # 做一层异常处理，以应对平台事件数据的变更\n        try:\n            return type_validate_python(your_event_class, payload)\n        except Exception as e:\n            # 无法正常解析为具体 Event 时，给出日志提示\n            log(\n                \"WARNING\",\n                f\"Parse event error: {str(payload)}\",\n            )\n            # 也可以尝试转为基础 Event 进行处理\n            return type_validate_python(Event, payload)\n\n\n    async def _forward(self, bot: Bot):\n\n        payload: Dict[str, Any]  # 接收到的事件数据\n        event = self.payload_to_event(payload)\n        # 让 bot 对事件进行处理\n        asyncio.create_task(bot.handle_event(event))\n```\n\n#### 调用平台 API\n\n我们需要实现 `Adapter` 的 `_call_api` 方法，使开发者能够调用平台提供的 API。如果通过 WebSocket 通信可以通过 `send` 方法来发送数据，如果采用 HTTP 请求，则需要通过 NoneBot 提供的 `Request` 对象，调用 `driver` 的 `request` 方法来发送请求。\n\n```python {11} title=adapter.py\nfrom typing import Any\nfrom typing_extensions import override\n\nfrom nonebot.drivers import Request, WebSocket\n\nfrom .bot import Bot\n\nclass Adapter(BaseAdapter):\n\n    @override\n    async def _call_api(self, bot: Bot, api: str, **data: Any) -> Any:\n        log(\"DEBUG\", f\"Calling API <y>{api}</y>\")  # 给予日志提示\n        platform_data = your_handle_data_method(data)  # 自行将数据转为平台所需要的格式\n\n        # 采用 HTTP 请求的方式，需要构造一个 Request 对象\n        request = Request(\n            method=\"GET\",  # 请求方法\n            url=api,  # 接口地址\n            headers=...,  # 请求头，通常需要包含鉴权信息\n            params=platform_data,  # 自行处理数据的传输形式\n            # json=platform_data,\n            # data=platform_data,\n        )\n        # 发送请求，返回结果\n        return await self.driver.request(request)\n\n\n        # 采用 WebSocket 通信的方式，可以直接调用 send 方法发送数据\n        # 通过某种方式获取到 bot 对应的 websocket 对象\n        ws: WebSocket = your_get_websocket_method(bot.self_id)\n\n        await ws.send_text(platform_data)  # 发送 str 类型的数据\n        await ws.send_bytes(platform_data)  # 发送 bytes 类型的数据\n        await ws.send(platform_data)  # 是以上两种方式的合体\n\n        # 接收并返回结果，同样的，也有 str 和 bytes 的区别\n        return await ws.receive_text()\n        return await ws.receive_bytes()\n        return await ws.receive()\n```\n\n`调用平台 API` 实现方式具体可以参考以下适配器：\n\nWebsocket:\n\n- [OneBot V11](https://github.com/nonebot/adapter-onebot/blob/54270edbbdb2a71332d744f90b1a3d7f4bf6463a/nonebot/adapters/onebot/v11/adapter.py#L167-L177)\n- [OneBot V12](https://github.com/nonebot/adapter-onebot/blob/54270edbbdb2a71332d744f90b1a3d7f4bf6463a/nonebot/adapters/onebot/v12/adapter.py#L204-L218)\n\nHTTP:\n\n- [OneBot V11](https://github.com/nonebot/adapter-onebot/blob/54270edbbdb2a71332d744f90b1a3d7f4bf6463a/nonebot/adapters/onebot/v11/adapter.py#L179-L215)\n- [OneBot V12](https://github.com/nonebot/adapter-onebot/blob/54270edbbdb2a71332d744f90b1a3d7f4bf6463a/nonebot/adapters/onebot/v12/adapter.py#L220-L266)\n- [QQ](https://github.com/nonebot/adapter-qq/blob/dc5d437e101f0e3db542de3300758a035ed7036e/nonebot/adapters/qq/adapter.py#L599-L605)\n- [Telegram](https://github.com/nonebot/adapter-telegram/blob/4a8633627e619245516767f5503dec2f58fe2193/nonebot/adapters/telegram/adapter.py#L148-L253)\n- [飞书](https://github.com/nonebot/adapter-feishu/blob/f8ab05e6d57a5e9013b944b0d019ca777725dfb0/nonebot/adapters/feishu/adapter.py#L201-L218)\n\n### Bot\n\nBot 是机器人开发者能够直接获取并使用的核心对象，负责存储平台机器人相关信息，并提供回复事件、调用 API 的上层方法。我们需要继承基类 `Bot`，并实现相关方法：\n\n```python {20,25,34} title=bot.py\nfrom typing import TYPE_CHECKING, Any, Union\nfrom typing_extensions import override\n\nfrom nonebot.message import handle_event\nfrom nonebot.adapters import Bot as BaseBot\n\nfrom .event import Event\nfrom .message import Message, MessageSegment\n\nif TYPE_CHECKING:\n    from .adapter import Adapter\n\n\nclass Bot(BaseBot):\n    \"\"\"\n    your_adapter_name 协议 Bot 适配。\n    \"\"\"\n\n    @override\n    def __init__(self, adapter: Adapter, self_id: str, **kwargs: Any):\n        super().__init__(adapter, self_id)\n        self.adapter: Adapter = adapter\n        # 一些有关 Bot 的信息也可以在此定义和存储\n\n    async def handle_event(self, event: Event):\n        # 根据需要，对事件进行某些预处理，例如：\n        # 检查事件是否和机器人有关操作，去除事件消息首尾的 @bot\n        # 检查事件是否有回复消息，调用平台 API 获取原始消息的消息内容\n        ...\n        # 调用 handle_event 让 NoneBot 对事件进行处理\n        await handle_event(self, event)\n\n    @override\n    async def send(\n        self,\n        event: Event,\n        message: Union[str, Message, MessageSegment],\n        **kwargs: Any,\n    ) -> Any:\n        # 根据平台实现 Bot 回复事件的方法\n\n        # 将消息处理为平台所需的格式后，调用发送消息接口进行发送，例如：\n        data = message_to_platform_data(message)\n        await self.send_message(\n            data=data,\n            ...\n        )\n```\n\n### Event\n\nEvent 是 NoneBot 中的事件主体对象，所有平台消息在进入处理流程前需要转换为 NoneBot 事件。我们需要继承基类 `Event`，并实现相关方法：\n\n```python {5,8,13,18,23,28,33} title=event.py\nfrom typing_extensions import override\n\nfrom nonebot.compat import model_dump\nfrom nonebot.adapters import Event as BaseEvent\n\nclass Event(BaseEvent):\n\n    @override\n    def get_event_name(self) -> str:\n        # 返回事件的名称，用于日志打印\n        return \"event name\"\n\n    @override\n    def get_event_description(self) -> str:\n        # 返回事件的描述，用于日志打印，请注意转义 loguru tag\n        return escape_tag(repr(model_dump(self)))\n\n    @override\n    def get_message(self):\n        # 获取事件消息的方法，根据事件具体实现，如果事件非消息类型事件，则抛出异常\n        raise ValueError(\"Event has no message!\")\n\n    @override\n    def get_user_id(self) -> str:\n        # 获取用户 ID 的方法，根据事件具体实现，如果事件没有用户 ID，则抛出异常\n        raise ValueError(\"Event has no context!\")\n\n    @override\n    def get_session_id(self) -> str:\n        # 获取事件会话 ID 的方法，根据事件具体实现，如果事件没有相关 ID，则抛出异常\n        raise ValueError(\"Event has no context!\")\n\n    @override\n    def is_tome(self) -> bool:\n        # 判断事件是否和机器人有关\n        return False\n```\n\n然后根据平台消息的类型，编写各种不同的事件，并且注意要根据事件类型实现 `get_type` 方法，具体请参考[事件类型](../advanced/adapter#事件类型)。消息类型事件还应重写 `get_message` 和 `get_user_id` 等方法，例如：\n\n```python {7,16,20,25,34,42} title=event.py\nfrom .message import Message\n\nclass HeartbeatEvent(Event):\n    \"\"\"心跳时间，通常为元事件\"\"\"\n\n    @override\n    def get_type(self) -> str:\n        return \"meta_event\"\n\nclass MessageEvent(Event):\n    \"\"\"消息事件\"\"\"\n    message_id: str\n    user_id: str\n\n    @override\n    def get_type(self) -> str:\n        return \"message\"\n\n    @override\n    def get_message(self) -> Message:\n        # 返回事件消息对应的 NoneBot Message 对象\n        return self.message\n\n    @override\n    def get_user_id(self) -> str:\n        return self.user_id\n\nclass JoinRoomEvent(Event):\n    \"\"\"加入房间事件，通常为通知事件\"\"\"\n    user_id: str\n    room_id: str\n\n    @override\n    def get_type(self) -> str:\n        return \"notice\"\n\nclass ApplyAddFriendEvent(Event):\n    \"\"\"申请添加好友事件，通常为请求事件\"\"\"\n    user_id: str\n\n    @override\n    def get_type(self) -> str:\n        return \"request\"\n```\n\n### Message\n\nMessage 负责正确序列化消息，以便机器人插件处理。我们需要继承 `MessageSegment` 和 `Message` 两个类，并实现相关方法：\n\n```python {9,12,17,22,27,30,36} title=message.py\nfrom typing import Type, Iterable\nfrom typing_extensions import override\n\nfrom nonebot.utils import escape_tag\n\nfrom nonebot.adapters import Message as BaseMessage\nfrom nonebot.adapters import MessageSegment as BaseMessageSegment\n\nclass MessageSegment(BaseMessageSegment[\"Message\"]):\n    @classmethod\n    @override\n    def get_message_class(cls) -> Type[\"Message\"]:\n        # 返回适配器的 Message 类型本身\n        return Message\n\n    @override\n    def __str__(self) -> str:\n        # 返回该消息段的纯文本表现形式，通常在日志中展示\n        return \"text of MessageSegment\"\n\n    @override\n    def is_text(self) -> bool:\n        # 判断该消息段是否为纯文本\n        return self.type == \"text\"\n\n\nclass Message(BaseMessage[MessageSegment]):\n    @classmethod\n    @override\n    def get_segment_class(cls) -> Type[MessageSegment]:\n        # 返回适配器的 MessageSegment 类型本身\n        return MessageSegment\n\n    @staticmethod\n    @override\n    def _construct(msg: str) -> Iterable[MessageSegment]:\n        # 实现从字符串中构造消息数组，如无字符串嵌入格式可直接返回文本类型 MessageSegment\n        ...\n```\n\n然后根据平台具体的消息类型，来实现各种 `MessageSegment` 消息段，具体可以参考以下适配器：\n\n- [OneBot V11](https://github.com/nonebot/adapter-onebot/blob/54270edbbdb2a71332d744f90b1a3d7f4bf6463a/nonebot/adapters/onebot/v11/message.py#L25-L259)\n- [QQ](https://github.com/nonebot/adapter-qq/blob/dc5d437e101f0e3db542de3300758a035ed7036e/nonebot/adapters/qq/message.py#L30-L520)\n- [Telegram](https://github.com/nonebot/adapter-telegram/blob/4a8633627e619245516767f5503dec2f58fe2193/nonebot/adapters/telegram/message.py#L13-L414)\n\n## 适配器测试\n\n关于适配器测试相关内容在这里不再展开，开发者可以根据需要进行合适的测试。这里为开发者提供几个常见问题的解决方法：\n\n1. 在测试中无法导入 editable 模式安装的适配器代码。在 pytest 的 `conftest.py` 内添加如下代码：\n\n   ```python title=tests/conftest.py\n   from pathlib import Path\n   import nonebot.adapters\n   nonebot.adapters.__path__.append(  # type: ignore\n       str((Path(__file__).parent.parent / \"nonebot\" / \"adapters\").resolve())\n   )\n   ```\n\n2. 需要计算适配器测试覆盖率，请在 `pyproject.toml` 中添加 pytest 配置：\n\n   ```toml title=pyproject.toml\n   [tool.pytest.ini_options]\n   addopts = \"--cov nonebot/adapters/{adapter-name} --cov-report term-missing\"\n   ```\n\n## 后续工作\n\n在完成适配器代码的编写后，如果想要将适配器发布到 NoneBot 商店，我们需要将适配器发布到 PyPI 中，然后前往[商店](/store/adapters)页面，切换到适配器页签，点击**发布适配器**按钮，填写适配器相关信息并提交。\n\n另外建议编写适配器文档或者一些插件开发示例，以便其他开发者使用我们的适配器。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/developer/plugin-publishing.mdx",
    "content": "---\nsidebar_position: 0\ndescription: 在商店发布自己的插件\n---\n\n# 发布插件\n\nimport Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\n\nNoneBot 为开发者提供了分享插件的官方商店。本指南囊括**从创建项目到发布到 PyPI，最终提交商店审核**的全过程。\n\n:::warning 警告\n如果你的插件只是满足自用需求，则完全可以选择**不发布插件**。发布插件**不是**使用插件的必要条件。\n\nNoneBot 社区对于插件有一定质量要求，对于不符合要求的插件，社区成员将会要求修改，直至符合要求后才能通过审核；如果长期未更新修改，社区将会关闭当前请求，之后如需发布请重新提交发布插件请求。相应的要求会在本章节以下部分介绍。\n:::\n\n:::tip 提示\n本章节仅包含插件发布流程指导，插件开发请查阅前述章节。\n:::\n\n## 准备工作\n\n### 插件命名规范\n\nNoneBot 插件使用下述命名规范：\n\n- 对于**项目名**，建议统一以 `nonebot-plugin-` 开头，之后为拟定的插件名字，词间用横杠 `-` 分隔；\n  - **项目名**用于代码仓库名称、PyPI 包的发布名称等；\n  - 本文使用 `nonebot-plugin-{your-plugin-name}` 为例。\n- 对于**模块名**，建议与**项目名**一致，但词间用下划线 `_` 分隔，即统一以 `nonebot_plugin_` 开头，之后为拟定的插件名字；\n  - **模块名**用于程序导入使用，应为插件文件（夹）的名称；\n  - 本文使用 `nonebot_plugin_{your_plugin_name}` 为例。\n\n### 项目结构\n\n:::tip 提示\n本段所述的项目结构仅作推荐，不做强制要求。\n:::\n\n插件程序本身结构可参考[插件结构](../tutorial/create-plugin.md#插件结构)一节，唯一区别在于，插件包可以直接处于项目顶层。\n\n插件项目的一种组织结构如下：\n\n```tree\n📦 nonebot-plugin-{your-plugin-name}\n├── 📂 nonebot_plugin_{your_plugin_name}\n│   ├── 📜 __init__.py\n│   └── 📜 config.py\n├── 📜 pyproject.toml\n└── 📜 README.md\n```\n\n功能开发可以在 `__init__.py` 中进行或在包内部创建其他模块并在 `__init__.py` 中导入。\n\n### 从项目模板开始\n\n为降低新手门槛，我们提供三条清晰、完整、可复制的发布路径。\n\n:::tip 提示\n你只需选择一条与你习惯一致的路径，**完整跟随即可成功发布**。无需在不同工具间切换或猜测配置。\n:::\n\nNoneBot 生态目前有如下插件项目模板：\n\n- [RF-Tar-Railt/nonebot-plugin-template](https://github.com/RF-Tar-Railt/nonebot-plugin-template)\n\n  此路径使用 **PDM** 项目管理器，符合 PEP 621 标准，自动化程度高。\n\n- [fllesser/nonebot-plugin-template](https://github.com/fllesser/nonebot-plugin-template)\n\n  此路径使用 **uv** 项目管理器和 **PoeThePoet** 任务运行器，构建速度快，适合追求效率的开发者。\n\n- [A-kirami/nonebot-plugin-template](https://github.com/A-kirami/nonebot-plugin-template)\n\n  此路径使用 **Poetry** 项目管理器，适合熟悉传统 Python 生态的开发者。\n\n#### 1. 创建项目\n\n1. 访问上述三个模板之一。\n2. 点击 **“Use this template”** → **“Create a new repository”**。\n3. 仓库名称填写：`nonebot-plugin-{your-plugin-name}`（此部分以 `nonebot-plugin-weather` 为例）。\n4. 点击 **“Create repository from template”**。\n\n#### 2. 配置发布权限\n\n1. 进入新仓库 → **Settings** → **Actions** → **General**。\n2. 在 **Workflow permissions** 下，勾选 **“Read and write permissions”** → 点击 **Save**。\n\n#### 3. 全局替换项目信息\n\n在仓库中点击 **“Add file”** → **“Create new file”**，创建一个空文件 `LICENSE`，选择开源协议并提交（此操作会触发工作流）。\n\n然后在本地克隆仓库，使用编辑器对以下内容进行**全局替换**：\n\n:::tip 提示\n此部分以“天气插件”为例，实际的替换内容应该根据你所创建的插件名称等相应调整。\n:::\n\n| 原内容                         | 替换为                             |\n| ------------------------------ | ---------------------------------- |\n| `nonebot-plugin-template`      | `nonebot-plugin-weather`           |\n| `nonebot_plugin_template`      | `nonebot_plugin_weather`           |\n| `<your_plugin_humanized_name>` | `天气查询`                         |\n| `<your_plugin_description>`    | `查询指定城市的实时天气与未来预报` |\n| `<your_github>`                | `你的GitHub用户名`                 |\n| `<your_email>`                 | `你的邮箱`                         |\n\n#### 4. 安装依赖与开发\n\n<Tabs groupId=\"publish-path\" defaultValue=\"pdm\" values={[\n  {label: 'PDM + RF-Tar-Railt 模板', value: 'pdm'},\n  {label: 'uv + fllesser 模板', value: 'uv'},\n  {label: 'Poetry + A-kirami 模板', value: 'poetry'},\n]}>\n  <TabItem value=\"pdm\" label=\"PDM + RF-Tar-Railt 模板\">\n\n```bash\n# 安装 PDM（若未安装）\ncurl -sSL https://pdm-project.org/install-pdm.py | python3 -\n\n# 安装项目依赖（自动创建虚拟环境）\npdm sync\n\n# 添加新依赖（如 httpx）\npdm add httpx\n```\n\n</TabItem>\n\n  <TabItem value=\"uv\" label=\"uv + fllesser 模板\">\n\n```bash\n# 安装 uv（Windows）\npowershell -c \"irm https://astral.sh/uv/install.ps1 | iex\"\n\n# 安装 uv（macOS/Linux）\ncurl -LsSf https://astral.sh/uv/install.sh | sh\n\n# 安装所有依赖（含 dev）\nuv sync --all-groups -p 3.12\n\n# 添加新依赖\nuv add httpx\n```\n\n</TabItem>\n\n  <TabItem value=\"poetry\" label=\"Poetry + A-kirami 模板\">\n\n```bash\n# 安装 Poetry（推荐方式）\ncurl -sSL https://install.python-poetry.org | python3 -\n\n# 安装项目依赖\npoetry install\n\n# 添加新依赖\npoetry add httpx\n```\n\n</TabItem>\n</Tabs>\n\n#### 5. 更新版本并发布\n\n<Tabs\n  groupId=\"publish-path-bump\"\n  defaultValue=\"bump-my-version\"\n  values={[\n    { label: \"使用 bump-my-version\", value: \"bump-my-version\" },\n    { label: \"使用项目管理器\", value: \"bump-manager\" },\n    { label: \"手动更新版本\", value: \"bump-manual\" },\n  ]}\n>\n  <TabItem value=\"bump-my-version\" label=\"使用 bump-my-version\">\n\n[bump-my-version](https://github.com/callowayproject/bump-my-version) 是一个功能强大、可配置的 Python 项目版本更新工具，支持自动提交到 Git 等 VCS。\n\n<Tabs groupId=\"publish-path\" defaultValue=\"pdm\" values={[\n  {label: 'PDM + RF-Tar-Railt 模板', value: 'pdm'},\n  {label: 'uv + fllesser 模板', value: 'uv'},\n  {label: 'Poetry + A-kirami 模板', value: 'poetry'},\n]}>\n  <TabItem value=\"pdm\" label=\"PDM + RF-Tar-Railt 模板\">\n\n```bash\n# 安装 bump-my-version\npdm add --dev bump-my-version\n\n# 更新 patch 版本\npdm run bump patch\n\n# 推送 tag 触发发布\ngit push origin --tags\n```\n\n</TabItem>\n\n  <TabItem value=\"uv\" label=\"uv + fllesser 模板\">\n\n```bash\n# 更新 patch 版本\nuv run poe bump patch\n\n# 推送 tag 触发发布\ngit push origin --tags\n```\n\n</TabItem>\n\n  <TabItem value=\"poetry\" label=\"Poetry + A-kirami 模板\">\n\n```bash\n# 安装 bump-my-version\npoetry add --dev bump-my-version\n\n# 更新 patch 版本\npoetry run bump patch\n\n# 推送 tag 触发发布\ngit push origin --tags\n```\n\n</TabItem>\n</Tabs>\n\n</TabItem>\n  <TabItem value=\"bump-manager\" label=\"使用项目管理器\">\n\n<Tabs groupId=\"publish-path\" defaultValue=\"pdm\" values={[\n  {label: 'PDM + RF-Tar-Railt 模板', value: 'pdm'},\n  {label: 'uv + fllesser 模板', value: 'uv'},\n  {label: 'Poetry + A-kirami 模板', value: 'poetry'},\n]}>\n  <TabItem value=\"pdm\" label=\"PDM + RF-Tar-Railt 模板\">\n\n需要安装 PDM 插件 [pdm-bump](https://github.com/carstencodes/pdm-bump)。\n\n```bash\n# 安装 pdm-bump\npdm self add pdm-bump\n\n# 更新 patch 版本\npdm bump patch\n\n# 推送 tag 触发发布\ngit push origin --tags\n```\n\n</TabItem>\n\n  <TabItem value=\"uv\" label=\"uv + fllesser 模板\">\n\n```bash\n# 更新 patch 版本\nuv version --bump patch\n\n# 创建相应提交与标签\ngit add pyproject.toml\ngit commit -m \"chore: release v0.1.1\"  # 替换为实际的版本号\ngit tag v0.1.1  # 替换为实际的版本号\n\n# 推送 tag 触发发布\ngit push origin --tags\n```\n\n</TabItem>\n\n  <TabItem value=\"poetry\" label=\"Poetry + A-kirami 模板\">\n\n```bash\n# 更新版本（自动提交并打标签）\npoetry version patch\n\n# 推送 tag 触发发布\ngit push origin --tags\n```\n\n</TabItem>\n</Tabs>\n\n</TabItem>\n  <TabItem value=\"bump-manual\" label=\"手动更新版本\">\n\n手动更新 `pyproject.toml` 中的 `version` 字段，然后推送 tag 触发发布工作流\n\n```bash\ngit add pyproject.toml\ngit commit -m \"chore: release v0.1.1\"  # 替换为实际的版本号\ngit tag v0.1.1  # 替换为实际的版本号\ngit push origin --tags\n```\n\n</TabItem>\n</Tabs>\n\n推送 `v*` 标签后，模板提供的 GitHub Actions 工作流将自动构建并发布到 PyPI。\n\n#### 6. 发布到 [PyPI](https://pypi.org)\n\n<Tabs groupId=\"publish-method\" defaultValue=\"template\" values={[\n  {label: '使用模板的自动发布工作流', value: 'template'},\n  {label: '手动发布', value: 'manual'},\n]}>\n  <TabItem value=\"template\" label=\"使用模板的自动发布工作流\">\n不同模板使用的发布方式可能不同，具体配置流程参考对应模板的详细使用指南。\n</TabItem>\n\n  <TabItem value=\"manual\" label=\"手动发布\">\n根据选用的构建系统，在项目的 `pyproject.toml` 中填入必要信息后进行构建与发布。\n\n:::tip 提示\n不同构建工具的使用可能存在差别。本文仅以 [`pdm`](https://pdm-project.org/zh/latest/),\n[`poetry`](https://python-poetry.org/docs/), [`setuptools`](https://setuptools.pypa.io/en/latest/)\n构建系统**本地构建与发布**为示例讲解，其余构建/管理工具等和自动化构建的用法请读者自行探索。\n:::\n\n<Tabs groupId=\"publishMethod\">\n  <TabItem value=\"poetry\" label=\"Poetry\" default>\n\n```bash\npoetry publish --build  # 构建并发布\n\n# 等效于以下两个命令\npoetry build            # 只构建\npoetry publish          # 只发布先前的构建\n```\n\n  </TabItem>\n\n  <TabItem value=\"pdm\" label=\"PDM\" default>\n\n```bash\npdm publish             # 构建并发布\n\n# 等效于以下两个命令\npdm build               # 只构建\npdm publish --no-build  # 只发布先前的构建\n```\n\n  </TabItem>\n\n  <TabItem value=\"setuptools\" label=\"Setuptools (PEP 517)\" default>\n\n```bash\npip install build twine             # 安装通用构建与发布工具\n\npython -m build --sdist --wheel .   # 只构建\ntwine upload dist/*                 # 只发布先前的构建\n```\n\n  </TabItem>\n</Tabs>\n\n</TabItem>\n</Tabs>\n\n:::tip 提示\n发布前建议自行测试构建包是否可用，避免遗漏代码文件或资源文件等问题。\n:::\n\n## 基本要求\n\n无论你选择哪条路径，以下内容**必须**完成，否则无法通过商店自动检查：\n\n### 能够正确加载\n\n插件包必须能够被 NoneBot 正确加载，在商店审核中会通过 **NoneFlow** 自动化加载测试进行。\n\n#### 依赖其他插件\n\n如果插件依赖其他插件提供的功能，则**必须**在代码中使用 `require()` 来引入该插件，然后才能 `import` 该插件提供的功能。具体细节参阅[跨插件访问](../advanced/requiring.md)。\n\n使用示例如下：\n\n```python title=nonebot_plugin_weather/__init__.py\nfrom nonebot import require\n\nrequire(\"nonebot_plugin_apscheduler\")\n\nfrom nonebot_plugin_apscheduler import scheduler\n```\n\n#### 不能零配置加载的插件\n\n如果插件需要必要配置项才能正常导入，则**必须**在商店提交表单中填写必要的配置项内容。\n\n但一种更好的做法是，**将插件设计为零配置即可加载**（允许缺少必要配置项时插件仍能正常导入，但不执行需要相应配置项的功能），尤其是对于一些必要配置含有敏感信息（如密钥、Token、API Key 等）的插件。这样可以避免在商店提交表单时填写敏感信息的风险。\n\n### 插件元数据\n\n插件包**必须**填写元数据才能通过 **NoneFlow** 自动化检查。\n\n下面是一个示例：\n\n```python title=nonebot_plugin_weather/__init__.py\nfrom nonebot.plugin import PluginMetadata\n\nfrom .config import Config\n\n__plugin_meta__ = PluginMetadata(\n    # 基本信息（必填）\n    name=\"天气查询\",  # 插件名称\n    description=\"查询指定城市的实时天气与未来预报\",  # 插件介绍\n    usage=\"发送【天气 城市名】获取天气信息\",  # 插件用法\n\n    # 发布额外信息\n    type=\"application\",  # 插件分类\n    # 发布必填，当前有效类型有：`library`（为其他插件编写提供功能），`application`（向机器人用户提供功能）。\n\n    homepage=\"https://github.com/你的用户名/nonebot-plugin-weather\",\n    # 发布必填。\n\n    config=Config,\n    # 插件配置项类，如果有配置类则必须填写。\n\n    supported_adapters={\"~onebot.v11\"},\n    # 支持的适配器集合，其中 `~` 在此处代表前缀 `nonebot.adapters.`，其余适配器亦按此格式填写。\n    # 若插件只使用了 NoneBot 基本抽象，应显式填写 None，否则应该列出插件支持的适配器。\n)\n```\n\n:::caution 注意\n`__plugin_meta__` 变量**必须**处于插件最外层（如 `__init__.py` 中），否则无法正常识别。\n\n一般做法是在 `__init__.py` 中定义 `__plugin_meta__`。\n:::\n\n#### 继承其他插件支持的适配器\n\n如果你的插件依赖于其他插件提供的支持功能，而其他插件可能支持更少的适配器，这时就应该使用\n[inherit_supported_adapters()](../api/plugin/load#inherit-supported-adapters) 函数来继承其他插件支持的适配器。\n\n示例用法如下：\n\n```python title=nonebot_plugin_weather/__init__.py\nfrom nonebot import require\nfrom nonebot.plugin import PluginMetadata, inherit_supported_adapters\n\nfrom .config import Config\n\nrequire(\"nonebot_plugin_alconna\")  # 必须先 require 才能被 inherit_supported_adapters 处理\n\n__plugin_meta__ = PluginMetadata(\n    name=\"天气查询\",\n    description=\"查询指定城市的实时天气与未来预报\",\n    usage=\"发送【天气 城市名】获取天气信息\",\n    type=\"application\",\n    homepage=\"https://github.com/你的用户名/nonebot-plugin-weather\",\n    config=Config,\n\n    supported_adapters=inherit_supported_adapters(\"nonebot_plugin_alconna\"),\n    # 继承 nonebot_plugin_alconna 插件的适配器支持列表\n)\n```\n\n### 准备项目主页\n\n通常可以使用 GitHub 项目页面作为项目主页，在 `README.md` 文件中编写插件介绍等内容。\n\n内容大致包括：\n\n- 插件功能介绍；\n- 安装方法\n  - **必须**有 NB-CLI 方式安装\n  - 可选依赖可以给出其他安装方式\n  - **不得**使用旧式的 `bot.py` 配置\n- 插件配置项（如 `Config` 类字段，若无可跳过）\n- 插件设置的触发规则（若无可跳过）\n- 插件的其它用法（按需编写）\n- 效果图、权限说明（按需编写）\n\n## 质量要求\n\n以下内容**强烈建议**完成，否则社区成员将会要求修改：\n\n### 依赖管理原则\n\n- **必须**包含 `nonebot2`。\n- **必须**将插件直接使用的适配器加入依赖列表，如：使用 OneBot 适配器的插件应添加 `nonebot-adapter-onebot` 依赖；\n- **禁止**使用 `==` 锁定单一版本，使用 `>=` 或 `~=`。\n- **禁止**添加 `nonebot`（V1）作为依赖。\n- 所有在代码中 `import` 的第三方库，必须在 `pyproject.toml` 的 `dependencies` 中列出。\n\n### 避免误用同步操作\n\nNoneBot 是一个异步框架，插件中**禁止**使用任何可能阻塞事件循环的同步操作，例如：\n\n- 同步 HTTP 请求（如 `requests` 库）；\n\n  **推荐**操作（以 `httpx` 为例）：\n\n  ```python\n  import httpx\n\n  async with httpx.AsyncClient() as client:\n      response = await client.get(\"https://api.example.com/data\")  # 异步操作，不阻塞机器人\n  ```\n\n  **禁止**操作：\n\n  ```python\n  import requests\n\n  requests.get(\"https://api.example.com/data\")  # 同步操作，会阻塞机器人\n  ```\n\n- 其他可能长时间运行阻塞事件循环的操作。\n\n### 本地文件存储\n\n如果插件需要在本地存储数据、配置或缓存文件，**必须**使用 [`nonebot-plugin-localstore`](https://github.com/nonebot/plugin-localstore) 管理，具体细节参阅[本地存储](../best-practice/data-storing.md)章节。\n\n参考示例：\n\n```python title=nonebot_plugin_weather/__init__.py\nfrom pathlib import Path\nfrom nonebot import require\nrequire(\"nonebot_plugin_localstore\")\n\nimport nonebot_plugin_localstore as store\n\n# 获取插件缓存文件（夹）路径\nweather_cache_dir: Path = store.get_plugin_cache_dir()\nweather_cache_file: Path = store.get_plugin_cache_file(\"cache.json\")\n\n# 获取插件配置文件（夹）路径\nweather_config_dir: Path = store.get_plugin_config_dir()\nweather_config_file: Path = store.get_plugin_config_file(\"config.toml\")\n\n# 获取插件数据文件（夹）路径\nweather_data_dir: Path = store.get_plugin_data_dir()\nweather_data_file: Path = store.get_plugin_data_file(\"resource-index.json\")\n```\n\n## 商店审核\n\n### 提交申请\n\n完成在 PyPI 的插件发布流程后，前往[商店](/store/plugins)页面，切换到插件页签，点击 **发布插件** 按钮。\n\n在弹出的插件信息提交表单内，填入您所要发布的相应插件信息。请注意，如果插件需要必要配置项才能正常导入，请在“插件配置项”中填写必要的内容（请勿填写密钥等敏感信息）。\n\n完成填写后，点击 **发布** 按钮，这将自动跳转 NoneBot 仓库内的“发布插件”页面。确认信息无误后点击页面下方的 `Submit new issue` 按钮进行最终提交即可。\n\n### 等待插件审核\n\n插件发布 Issue 创建后，将会经过 **NoneFlow Bot** 的自动前置检查，以确保插件信息正确无误、插件能被正确加载。\n\n:::tip 提示\n若插件检查未通过或信息有误，**不必**关闭当前 Issue。只需更新插件并上传到 PyPI/修改信息后勾选插件测试勾选框即可重新触发插件检查。\n:::\n\n之后，NoneBot 的维护者和一些志愿者会初步检查插件代码，帮助减少该插件的问题。\n\n完成这些步骤后，您的插件将会被自动合并到[商店](/store/plugins)，而您也将成为 [**NoneBot 贡献者**](https://github.com/nonebot/nonebot2/graphs/contributors)的一员。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/editor-support.md",
    "content": "---\nsidebar_position: 2\ndescription: 配置编辑器以获得最佳体验\n---\n\n# 编辑器支持\n\n框架基于 [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）工具进行类型检查，确保代码可以被编辑器正确解析。\n\n## CLI 脚手架提供的编辑器工具支持\n\n在使用 NB-CLI [创建项目](./quick-start.mdx#创建项目)时，如果选择了用于插件开发的 `simple` 模板，其会根据选择的开发工具，**自动配置项目根目录下的 `.vscode/extensions.json` 文件**，以推荐最匹配的 VS Code 插件，同时自动将相应的预设配置项写入 `pyproject.toml` 作为“开箱即用”配置，从而提升开发体验。\n\n```bash\n[?] 选择一个要使用的模板: simple (插件开发者)\n...\n[?] 要使用哪些开发工具?\n```\n\n### 支持的开发工具\n\n1. Pyright (Pylance)\n\n   [VS Code 插件](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance) | [项目](https://github.com/microsoft/pyright) | [文档](https://microsoft.github.io/pyright/)\n\n   由微软开发的 Python 静态类型检查器和语言服务器，提供智能感知、跳转定义、查找引用、实时错误检查等强大功能。\n\n   作为 VS Code 官方推荐的 Python 语言服务器，与 Pylance 扩展配合使用，能提供最流畅、最准确的代码补全和类型推断体验，是绝大多数开发者的首选。\n\n2. Ruff\n\n   [VS Code 插件](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff) | [项目](https://github.com/astral-sh/ruff) | [文档](https://docs.astral.sh/ruff/)\n\n   一个用 Rust 编写的超快 Python 代码格式化和 lint 工具，完全兼容 `black`、`isort`、`flake8` 等主流工具的规则。\n\n   速度极快（比 `black` 和 `flake8` 快 100 倍以上），配置简单，能自动格式化代码并检测潜在错误、代码风格问题（尤其是误用同步网络请求库），是提升代码质量和开发效率的必备利器。\n\n3. MyPy\n\n   [VS Code 插件](https://marketplace.visualstudio.com/items?itemName=matangover.mypy) | [项目](https://github.com/python/mypy) | [文档](https://mypy.readthedocs.io/en/stable/index.html)\n\n   一个官方实现的 Python 静态类型检查器，通过分析代码中的类型注解来发现类型错误。\n\n4. BasedPyright\n\n   [VS Code 插件](https://marketplace.visualstudio.com/items?itemName=detachhead.basedpyright) | [项目](https://github.com/DetachHead/basedpyright) | [文档](https://docs.basedpyright.com/)\n\n   一个基于 Pyright 的、由社区维护的替代性 Python 语言服务器，旨在提供更优的类型检查支持与接近 Pylance 的更好的使用体验。\n\n   相较于 Pylance，BasedPyright 允许配合 VS Code 之外的其他编辑器使用，同时也复刻了部分 Pylance 限定的功能。\n\n   如果您是高级用户，希望尝试 Pylance 的替代方案，或遇到 Pylance 在特定环境下的兼容性问题，可以考虑使用 BasedPyright。\n\n:::caution 提示\n为避免 `Pylance` 和 `BasedPyright` 相互冲突导致配置混乱甚至异常，脚手架默认不允许在创建项目时同时配置这两者。\n\n如果确实需要同时使用，请在创建项目时选择 Pylance/Pyright 并根据[相关文档](https://docs.basedpyright.com/latest/installation/ides/#vscode-vscodium)进行手动配置。\n:::\n\n### 配置效果\n\n选择上述工具后，NB-CLI 会在您的项目根目录下生成一个 `.vscode/extensions.json` 文件并在 `pyproject.toml` 文件中写入相应的配置项。当您在 VS Code 中打开此项目时，IDE\n会自动弹出提示，建议您安装这些推荐的扩展，一键即可完成开发环境的初始化，让您可以立即开始编写代码，无需手动搜索和安装插件。\n\n## 编辑器推荐配置\n\n### Visual Studio Code\n\n在 Visual Studio Code 中，可以使用 Pylance Language Server 并启用 `Type Checking` 配置以达到最佳开发体验。\n\n1. 在 VSCode 插件视图搜索并安装 `Python (ms-python.python)` 和 `Pylance (ms-python.vscode-pylance)` 插件。\n2. 修改 VSCode 配置\n   在 VSCode 设置视图搜索配置项 `Python: Language Server` 并将其值设置为 `Pylance`，搜索配置项 `Python > Analysis: Type Checking Mode` 并将其值设置为 `basic`。\n\n   或者向项目 `.vscode` 文件夹中配置文件添加以下内容：\n\n   ```json title=settings.json\n   {\n     \"python.languageServer\": \"Pylance\",\n     \"python.analysis.typeCheckingMode\": \"basic\"\n   }\n   ```\n\n### 其他\n\n欢迎提交 Pull Request 添加其他编辑器配置推荐。点击左下角 `Edit this page` 前往编辑。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/ospp/2021.md",
    "content": "---\nsidebar_position: 0\ndescription: 开源软件供应链点亮计划 - 暑期 2021\nmdx:\n  format: md\n---\n\n# 暑期 2021\n\n**开源软件供应链点亮计划 - 暑期 2021** 是**中国科学院软件研究所**与 **openEuler 社区**共同举办的一项面向高校学生的暑期活动，旨在鼓励在校学生积极参与开源软件的开发维护，促进优秀开源软件社区的蓬勃发展。关于具体的活动规划、报名方式，请查看该活动的 [官网](https://summer.iscas.ac.cn/) 和 [帮助文档](https://summer.iscas.ac.cn/help/)。\n\nNoneBot 社区有幸作为开源社区参与了本次活动，下面列出了目前我们已经发布的项目，欢迎感兴趣的同学在上面给出的活动官网报名，或通过 [contact@nonebot.dev](mailto:contact@nonebot.dev) 联系我们。\n\n## NoneBot v1\n\n### 更新 NoneBot v1 文档中的“指南”部分\n\n由于 NoneBot v1 和 aiocqhttp 最初基于的 QQ 机器人平台不再提供服务，CQHTTP 接口也转型且改名为 OneBot 标准，目前 NoneBot v1 文档的“指南”部分和 aiocqhttp 文档有部分过时内容需要更新。我们希望将其中与旧的机器人平台相关的内容改为基于 go-cqhttp 或通用的 OneBot 表述，同时对 NoneBot v1 的 awesome-bot 示例做一次全面检查，修改其中可能已经不可用的部分。\n\n**难度**：低\n\n**导师**：[@cleoold](https://github.com/cleoold)\n\n**产出要求**\n\n- 修改“指南”文档和 aiocqhttp 文档中与旧的 QQ 机器人平台相关的部分\n- 检查 awesome-bot 示例是否有已经过时/不可用的地方，并更新/修复\n- 修改“图灵机器人”案例，使用其它 AI 聊天 API 提供商（需先做简单调研）\n\n**技术要求**\n\n- 熟悉 Python 编程语言及 asyncio 机制\n- 了解 Git 基本用法\n- 了解聊天机器人基本开发过程\n- 了解 VuePress 更佳\n\n### NoneBot v1 API 文档自动生成\n\n目前 NoneBot v1 的文档中“API”部分是手动编写的，在更新代码接口的同时需要手动更新文档，可能造成文档与代码不匹配，形成额外的维护成本。我们希望将 API 文档改为直接编写在 Python docstring 中，通过工具自动生成 API 文档。\n\n**难度**：中\n\n**导师**：[@cleoold](https://github.com/cleoold)\n\n**产出要求**\n\n- 调研市面上常见的 Python API 文档生成工具\n- 在代码中补充 API 文档\n- 编写或应用开源工具自动生成 API 文档\n- 配置 GitHub Actions 或其它 CI 自动化构建和部署 API 文档\n\n**技术要求**\n\n- 熟悉 Python 编程语言及 asyncio 和 Type Hints\n- 了解 Git 基本用法\n- 了解 Sphinx 等文档生成工具更佳\n- 了解 GitHub Actions 等 CI 工具更佳\n\n## NoneBot v2\n\n### NoneBot v2 自动化测试框架“NoneBug”\n\n在聊天机器人的开发过程中，一套自动化的测试机制是非常重要的，特别是对于 NoneBot 2 这类为大型机器人开发而设计的项目来说，需要手动测试每一个边际条件是非常痛苦的。我们希望能够开发一款基于 NoneBot 2 插件机制的自动化测试框架，为 NoneBot 2 用户提供一套易用便捷、高度灵活的自动化测试框架。\n\n**难度**：高\n\n**导师**：[@yanyongyu](https://github.com/yanyongyu)\n\n**产出要求**\n\n- 调研现有的 Python 和其它语言集成测试框架\n- 设计 NoneBug 的用户 API 和实现方式\n- 实现 NoneBug 自动化测试框架\n- 编写详细的使用文档\n\n**技术要求**\n\n- 熟悉 Python 编程语言及 asyncio 和 Type Hints\n- 了解 Git 基本用法\n- 了解 NoneBot v2 的基本原理和使用方式\n- 了解主流的 Python 自动化测试框架\n\n### NoneBot v2 Telegram 适配器\n\n目前 NoneBot v2 已支持 OneBot、Mirai HTTP API、钉钉协议，社区反馈有更多的平台需求，希望能在 NoneBot v2 获得更多的跨平台支持，提高机器人的便携性。同时，我们也希望随着新平台加入，提升现有 NoneBot v2 核心代码的平台通用性。Telegram 是一款较为广泛使用的安全即时聊天软件，同时其官方提供了丰富的聊天机器人 API，因此我们希望为 NoneBot v2 编写一个 Telegram 适配器来支持 Telegram 机器人的开发。\n\n**难度**：中\n\n**导师**：[@yanyongyu](https://github.com/yanyongyu)\n\n**产出要求**\n\n- 调研 Telegram Bot API 以及 WebHook 等官方接口\n- 编写 Telegram 适配器并能够使用\n- 代码遵守项目 Contributing 规范\n\n**技术要求**\n\n- 熟悉 Python 编程语言及 asyncio 和 Type Hints\n- 了解 Git 基本用法\n- 了解 Web 开发相关知识\n- 了解 Sphinx 等文档生成工具更佳\n\n### NoneBot v2 飞书适配器\n\n目前 NoneBot v2 已支持 OneBot、Mirai HTTP API、钉钉协议，社区反馈有更多的平台需求，希望能在 NoneBot v2 获得更多的跨平台支持，提高机器人的便携性。同时，我们也希望随着新平台加入，提升现有 NoneBot v2 核心代码的平台通用性。飞书是目前企业用户广泛使用的即时聊天和协作软件，其官方提供了丰富的聊天机器人 API，因此我们希望为 NoneBot v2 编写一个飞书适配器来支持飞书机器人的开发。\n\n**难度**：中\n\n**导师**：[@yanyongyu](https://github.com/yanyongyu)\n\n**产出要求**\n\n- 调研飞书机器人 API 以及 WebHook 等官方接口\n- 编写飞书适配器并能够使用\n- 代码遵守项目 Contributing 规范\n\n**技术要求**\n\n- 熟悉 Python 编程语言及 asyncio 和 Type Hints\n- 了解 Git 基本用法\n- 了解 Web 开发相关知识\n- 了解 Sphinx 等文档生成工具更佳\n\n## OneBot\n\n### 设计 OneBot v12 接口标准\n\n目前的 OneBot 标准的 v11 版本仍然与 QQ 平台有较多耦合，我们希望在 v12 去掉与 QQ 耦合的历史包袱，形成一个通用的、可扩展的、易于使用的同时易于实现的聊天机器人接口标准。\n\n**难度**：中\n\n**导师**：[@richardchien](https://github.com/richardchien)\n\n**产出要求**\n\n- 调研各聊天机器人平台的官方/非官方接口特点\n- 通用化 OneBot 核心 API，分离 QQ 特定的 API，去掉无用 API\n- 优化现有的通信、消息表示机制\n- 补充 QQ 特定的缺失 API\n- 文档需符合风格指南\n\n**技术要求**\n\n- 熟悉至少两个聊天平台的聊天机器人开发\n- 了解 Git 基本用法\n- 了解使用不同语言编写聊天机器人时的常用实践\n- 对文档的优雅性与美观性有追求更佳\n\n### 实现 Rust 版 libonebot\n\n目前最常用的 OneBot 实现包括 go-cqhttp、onebot-kotlin、node-onebot 等，这些实现都各自重复实现了 Web 通信、消息解析、配置读写等功能，当社区中的开发者想针对一个新的聊天平台实现 OneBot 时，他们往往同样需要再次实现类似逻辑。我们希望使用 Rust 编写一个 libonebot 模块，该模块实现所有 OneBot 实现所共享的功能，从而方便其他开发者们使用 Rust 快速编写具体的 OneBot 实现。同时，我们希望借此项目在聊天机器人社区中推广 Rust 编程语言。\n\n> 注：这里的逻辑是 libonebot + 针对某聊天平台的对接代码 = 某聊天平台的 OneBot 实现，libonebot 要做的是让 OneBot 实现的开发者只需编写针对特定聊天平台的对接代码，而无需关心 OneBot 标准定义的通信方式、消息格式等。\n\n**难度**：高\n\n**导师**：[@richardchien](https://github.com/richardchien)\n\n**产出要求**\n\n- 实现所有 OneBot 实现所共享的功能，包括 Web 通信、消息解析、配置读写等\n- 充分考虑同时兼容 OneBot v11 和 v12 接口\n- 能够根据用户（OneBot 实现的开发者）所实现的接口自动实现类似 get_available_apis 等接口\n- 编写详细的使用文档\n- 如果可能，与 v12 设计项目联动，实现第一手 v12 支持\n\n**技术要求**\n\n- 熟悉聊天机器人开发\n- 熟悉 Rust Web 开发\n\n### 实现自选语言版 libonebot\n\n目前最常用的 OneBot 实现包括 go-cqhttp、onebot-kotlin、node-onebot 等，这些实现都各自重复实现了 Web 通信、消息解析、配置读写等功能，当社区中的开发者想针对一个新的聊天平台实现 OneBot 时，他们往往同样需要再次实现类似逻辑。我们希望使用 Python、Go、Kotlin、Node、PHP、C#.NET 等主流语言（任选一个）编写 libonebot 模块，该模块实现所有 OneBot 实现所共享的功能，从而方便其他开发者们使用对应语言快速编写具体的 OneBot 实现。\n\n> 注：这里的逻辑是 libonebot + 针对某聊天平台的对接代码 = 某聊天平台的 OneBot 实现，libonebot 要做的是让 OneBot 实现的开发者只需编写针对特定聊天平台的对接代码，而无需关心 OneBot 标准定义的通信方式、消息格式等。\n\n**难度**：中\n\n**导师**：[@richardchien](https://github.com/richardchien)\n\n**产出要求**\n\n- 实现所有 OneBot 实现所共享的功能，包括 Web 通信、消息解析、配置读写等\n- 充分考虑同时兼容 OneBot v11 和 v12 接口\n- 编写详细的使用文档\n- 如果可能，实现更多附加特性，如根据用户（OneBot 实现的开发者）所实现的接口自动实现类似 get_available_apis 等接口、实现第一手 v12 支持等\n\n**技术要求**\n\n- 熟悉聊天机器人开发\n- 熟悉所选语言的 Web 开发\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/ospp/2022.md",
    "content": "---\nsidebar_position: 1\ndescription: 开源之夏 - 暑期 2022\nmdx:\n  format: md\n---\n\n# 暑期 2022\n\n**开源之夏 - 暑期 2022** 是由**开源软件供应链点亮计划**发起、由**中国科学院软件研究所**与 **openEuler 社区**主办的一项面向高校学生的暑期活动，类似 Google Summer of Code（GSoC），旨在鼓励在校学生积极参与开源软件的开发维护，促进优秀开源软件社区的蓬勃发展。关于具体的活动规划、报名方式，请查看该活动的 [官网](https://summer-ospp.ac.cn/) 和 [帮助文档](https://summer-ospp.ac.cn/help/)。\n\nNoneBot 社区有幸作为开源社区 [参与](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) 联系我们。\n\n## NoneBot2 命令行 CLI 交互体验升级\n\nNoneBot2 为用户提供了命令行脚手架 ──`nb-cli`，辅助用户更好地上手项目以及进行开发。nb-cli 主要包括：创建项目、运行项目、安装与卸载插件、部署项目等功能。随着 NoneBot2 Beta 版本的发布，脚手架功能存在一定的定位不明确、功能体验不佳。本项目旨在重新设计 nb-cli 功能框架，完善功能，优化用户体验。\n\n**难度**：进阶\n\n**导师**：[@yanyongyu](https://github.com/yanyongyu)\n\n**产出要求**\n\n- 设计 nb-cli 功能框架\n  - 明确各功能模块\n  - 设计用户交互模式\n- 完成 nb-cli 主要功能代码\n  - 项目管理\n  - 插件管理\n  - 其它\n- 同步更新使用文档\n\n**技术要求**\n\n- 熟悉 Python 命令行交互代码编写\n- 熟悉 NoneBot2 框架功能\n- 熟悉 NoneBot2 项目组织方式\n\n**成果仓库**\n\n- <https://github.com/nonebot/nb-cli>\n- <https://github.com/nonebot/nonebot2>\n\n## NoneBot2 命令行即时交互通信设计与实现\n\nNoneBot2 在早期提供了基于网页的 nonebot-plugin-test 插件，无需平台适配接入即可对机器人进行测试，方便了开发者直观的感受机器人文本交互功能。我们希望提供一款基于命令行的适配器/驱动器，用于无平台适配接入、可以运行机器人的场景进行功能体验或测试。\n\n**难度**：进阶\n\n**导师**：[@mnixry](https://github.com/mnixry)\n\n**产出要求**\n\n- 设计命令行与 NoneBot2 通信模式\n  - 直接调用/HTTP/WebSocket\n- 设计命令行交互界面\n- 实现相应适配器/驱动器\n- 同步更新使用说明文档\n\n**技术要求**\n\n- 熟悉 Python 命令行交互代码编写\n- 熟悉 NoneBot2 框架功能\n- 熟悉 NoneBot2 项目组织方式\n\n**成果仓库**\n\n- <https://github.com/nonebot/adapter-console>\n\n## NoneBot2 用户上手与深入教程设计\n\nNoneBot2 为用户提供了详细的文档介绍，辅助用户更好的上手项目以及进行开发。文档分为基础与进阶两个部分。基础部分帮助新用户快速上手开发，主要包括：安装 NoneBot2、使用脚手架、创建配置项目、使用适配器、加载插件、定义消息事件、处理消息事件、调用平台 API 等。进阶部分向已经熟悉开发流程的用户介绍更多高级技巧，主要包括：NoneBot2 工作原理、定时任务、权限控制、钩子函数、跨插件访问、单元测试、发布插件等。目前文档对于用户而言过于费解，导致用户难以理解 NoneBot2 开发。本项目旨在优化文档内容，使其更加通俗易懂，不让文档成为用户上手的阻碍，同时完善进阶内容，让有更复杂需求的用户，同样能从文档中受益。\n\n相关 issue：\n\n- <https://github.com/nonebot/nonebot2/issues/793>\n- <https://github.com/nonebot/nonebot2/issues/295>\n\n**难度**：进阶\n\n**导师**：[@SK-415](https://github.com/SK-415)\n\n**产出要求**\n\n- 文档通俗易懂\n  - 附有适当的图片指引（如 asciinema）\n- 内容完整，由浅入深\n- 适当的界面美化，合理分配布局\n\n**技术要求**\n\n- 熟悉文档结构组织与语言表达\n- 熟悉 NoneBot2 框架功能\n- 熟悉 NoneBot2 项目组织方式\n\n**成果仓库**\n\n- <https://github.com/nonebot/nonebot2>\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/ospp/2023.md",
    "content": "---\nsidebar_position: 2\ndescription: 开源之夏 - 暑期 2023\nmdx:\n  format: md\n---\n\n# 暑期 2023\n\n**开源之夏 - 暑期 2023** 是由**开源软件供应链点亮计划**发起、由**中国科学院软件研究所**与 **openEuler 社区**主办的一项面向高校学生的暑期活动，类似 Google Summer of Code（GSoC），旨在鼓励在校学生积极参与开源软件的开发维护，促进优秀开源软件社区的蓬勃发展。关于具体的活动规划、报名方式，请查看该活动的 [官网](https://summer-ospp.ac.cn/) 和 [帮助文档](https://summer-ospp.ac.cn/help/)。\n\nNoneBot 社区有幸作为开源社区 [参与](https://summer-ospp.ac.cn/org/orgdetail/e1fb5b8d-125a-4138-b756-25bd32c0a31a?lang=zh) 了本次活动，下面列出了目前我们已经发布的项目，欢迎感兴趣的同学通过 [contact@nonebot.dev](mailto:contact@nonebot.dev) 联系我们。\n\n## NoneBot 项目管理图形化面板\n\nNoneBot 目前提供了开箱即用的命令行脚手架来帮助初次使用的用户更快的上手编写应用。但是，对于未有一定开发经验的用户，命令行的使用仍具有一定的困难。此外，其他项目如 koishi、vue 等，均可通过图形化界面的形式为用户提供更便捷的项目开发。因此，我们希望借助现有命令行脚手架的可扩展特性，提供一个项目管理面板服务，以网页的形式帮助用户开发 NoneBot 应用。\n\n**难度**：进阶\n\n**导师**：[@mnixry](https://github.com/mnixry)\n\n**产出要求**\n\n- 设计并实现项目管理面板相关功能\n  - 创建与管理项目\n  - 配置与运行项目\n  - NoneBot 插件管理\n- 实现相应 nb-cli 插件提供面板服务\n- 代码符合 NoneBot Contributing 规范\n\n**技术要求**\n\n- 熟悉 nb-cli 相关功能\n- 熟悉 NoneBot 框架功能\n- 熟悉前后端相关实现方式\n\n**成果仓库**\n\n- <https://github.com/nonebot/cli-plugin-webui>\n\n## NoneBot Discord 适配器\n\nNoneBot 作为一个跨平台聊天机器人框架，目前已有 OneBot、飞书、Telegram、QQ 频道等诸多平台的适配支持。作为众多用户期待的平台适配之一，我们希望借此机会接入 Discord 聊天机器人。\n\n**难度**：进阶\n\n**导师**：[@iyume](https://github.com/iyume)\n\n**产出要求**\n\n- 调研 Discord Bot 相关功能与接口\n- 设计与编写 NoneBot Discord 适配器\n- 代码符合 NoneBot Contributing 规范\n\n**技术要求**\n\n- 熟悉 NoneBot 框架功能\n- 熟悉 NoneBot 各模块职责与适配器编写\n\n**成果仓库**\n\n- <https://github.com/nonebot/adapter-discord>\n\n## NoneBot 数据库支持插件\n\nNoneBot 的插件系统为用户实现应用提供了极高的便捷性，但因此也增加了插件统一管理的难度。目前，我们发现许多用户发布的插件中存在文件存储结构化数据、数据存放散乱等现象，同时插件间也可能产生冲突。因此，我们希望提供一个统一的数据存储与管理方式，便于用户读写应用数据。\n\n**难度**：进阶\n\n**导师**：[@yanyongyu](https://github.com/yanyongyu)\n\n**产出要求**\n\n- 设计并实现 ORM 插件\n  - 提供关系模型定义功能\n  - 提供模型迁移与管理功能\n  - 能较好的支持 Python 类型检查与推导\n- 编写相应的用户使用文档\n- 代码符合 NoneBot Contributing 规范\n\n**技术要求**\n\n- 熟悉 NoneBot 框架功能与插件编写\n- 熟悉 SQLAlchemy 等 ORM 框架\n  - 熟悉 SQLAlchemy ORM\n  - 熟悉 alembic 等迁移工具\n- 熟悉 nb-cli 插件编写\n\n**成果仓库**\n\n- <https://github.com/nonebot/plugin-orm>\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/ospp/2024.md",
    "content": "---\nsidebar_position: 3\ndescription: 开源之夏 - 暑期 2024\nmdx:\n  format: md\n---\n\n# 暑期 2024\n\n**开源之夏 - 暑期 2024** 是**中国科学院软件研究所**发起的**开源软件供应链点亮计划**系列暑期活动，旨在鼓励高校学生积极参与开源软件的开发维护，促进优秀开源软件社区的蓬勃发展。活动联合各大开源社区，针对重要开源软件的开发与维护提供项目开发任务，并向全球高校学生开放报名。关于具体的活动规划、报名方式，请查看该活动的 [官网](https://summer-ospp.ac.cn/) 和 [帮助文档](https://summer-ospp.ac.cn/help/)。\n\nNoneBot 社区有幸作为开源社区 [参与](https://summer-ospp.ac.cn/org/orgdetail/e1fb5b8d-125a-4138-b756-25bd32c0a31a?lang=zh) 了本次活动，下面列出了目前我们已经发布的项目，欢迎感兴趣的同学通过 [contact@nonebot.dev](mailto:contact@nonebot.dev) 联系我们。\n\n## NonePress 官网组件库更新与优化\n\nNoneBot 官网目前采用基于 TailwindCSS 自研的 NonePress 组件库及 Docusaurus 框架进行构建。由于相关依赖版本迭代迅速，目前官网组件库已产生了较大的版本落后。本项目希望在跟进框架新版本的基础上，对文档整体视觉体验进行重新设计，提升页面的无障碍访问性，基于 React Hydrate 特性实现完整的静态网站生成（SSG）以提升搜索引擎优化（SEO）水平。在解决以上问题的基础上，可对网页的开发以及生产构建性能做相应的优化提升，例如在生产构建使用自有的 webpack loader、替换现有的热重载逻辑以减少开发环境启动耗时等。\n\n**难度**：进阶\n\n**导师**：[@yanyongyu](https://github.com/yanyongyu)\n\n**产出要求**\n\n- 基于 Docusaurus v3 重构 NonePress 组件库及相关插件\n  - 升级相关依赖并重新打造 Docusaurus theme（布局与组件）\n  - 根据需求实现/修改 Docusaurus 插件使得官网内容构建正常\n  - 能够提升页面渲染性能与 MDX 相关能力\n- 升级官网采用新版组件库\n  - Algolia 索引与 SEO 正常\n  - 桌面端与移动端显示正常\n  - 优化官网开发与生产构建体验\n- （可选）优化官网部分页面\n  - 优化官网过长的 changelog\n  - 优化官网插件商店的展示细节\n\n**技术要求**\n\n- 熟练掌握 TS、PostCSS、TSX、MDX等相关技术\n- 掌握 React、Docusaurus、tailwind css 等框架\n- 熟悉静态网站生成 SSG、SEO 优化与 Algolia 索引原理等\n\n**成果仓库**\n\n- <https://github.com/nonebot/docusaurus-theme-nonepress>\n\n## NoneFlow 社区自动化工作流管理优化\n\nNoneFlow 在 NoneBot 社区中承担着重要的角色，它由 NoneBot 框架基于 GitHub APP 编写而成，能够自动化的完成许多复杂流程的处理，如：用户请求提交插件到商店时进行自动化检测，并在人工审核通过后自动存储至 registry；定时自动更新 registry 内插件信息，跟进插件新版本情况等。但是，在长期的使用中发现了一些问题和不足的地方，例如：项目本身结构复杂耦合，添加新自动化流程与维护现有流程困难；目前采用了 GitHub 用户名作为插件作者名，但已有不少插件作者改名；插件存储至 registry 并定时更新，缺少统计相关信息以帮助商店更好的展示当前插件状态；插件作者想要修改插件信息时无法便捷的找到操作方式等。本项目希望针对以上问题与不足的地方进行修复与优化，提升用户体验。\n\n**难度**：进阶\n\n**导师**：[@uy/sun](https://github.com/he0119)\n\n**产出要求**\n\n- 重构现有工作流处理结构\n  - 整合现有 Issue、Pull Request、Git 相关操作\n  - 提供用户修改信息的处理方式\n  - 正确处理 PR 的 Open、Close、Draft 状态\n- 修复流程中存在的问题\n  - 插件作者名正确展示\n  - registry 定时更新中需要插件测试环境隔离\n- 在 registry 定时更新的同时提供统计数据\n\n**技术要求**\n\n- 掌握 GitHub APP 开发\n  - 熟悉 GitHub REST API、GraphQL 等\n  - 熟悉 GitHub APP 权限限制\n- 熟悉 NoneBot 框架与 Python 相关技术\n- 熟悉 Git、GitHub Action、GitHub 工作流\n\n**成果仓库**\n\n- <https://github.com/nonebot/noneflow>\n\n## NoneBlockly 低代码框架开发\n\n经过深入分析社区反馈，我们发现部分新手因不熟悉编程概念或框架本身而遇到问题。为了解决初学者在使用面向开发者的聊天机器人框架 NoneBot 时遇到的挑战，我们计划引入 Blockly 提供低代码编程支持。通过减少常见的编码错误和降低入门门槛，使框架对初学者更加友好，从而提升用户体验并有助于 NoneBot 生态的成长。本项目将基于 Blockly 实现 NoneBot 插件的低代码编写，使得用户能够快速搭建聊天机器人。\n\n**难度**：进阶\n\n**导师**：[@mnixry](https://github.com/mnixry)\n\n**产出要求**\n\n- 实现 NoneBlockly 低代码开发框架\n  - 能够基于 Alconna 编写跨平台插件\n  - 确保插件对 Python 和 NoneBot 版本的兼容性\n  - 支持对多种类型 NoneBot 事件的响应\n  - 支持对 NoneBot 消息对象的便捷操作\n  - 集成 localstore 文件存储、apscheduler 定时任务、网络请求等常用功能\n- 对接 NB-CLI 脚手架，通过脚手架扩展使用低代码框架\n\n**技术要求**\n\n- 掌握 Python 与 NoneBot 框架的使用\n  - 熟悉 NoneBot 插件的开发，包括事件响应与消息处理等\n  - 熟悉 NoneBot 生态组件（Alconna、localstore、apscheduler等）的使用\n  - 了解 NB-CLI 脚手架的扩展开发\n- 熟悉 Blockly 低代码框架的使用和开发\n\n**成果仓库**\n\n- <https://github.com/nonebot/noneblockly>\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/ospp/2025.md",
    "content": "---\nsidebar_position: 4\ndescription: 开源之夏 - 暑期 2025\nmdx:\n  format: md\n---\n\n# 暑期 2025\n\n**开源之夏 - 暑期 2025** 是**中国科学院软件研究所**发起的**开源软件供应链点亮计划**系列暑期活动，旨在鼓励高校学生积极参与开源软件的开发维护，培养和发掘更多优秀的开发者，促进优秀开源软件社区的蓬勃发展，助力开源软件供应链建设。活动联合各大开源社区，针对重要开源软件的开发与维护提供项目开发任务，并向全球高校学生开放报名。关于具体的活动规划、报名方式，请查看该活动的 [官网](https://summer-ospp.ac.cn/) 和 [帮助文档](https://summer-ospp.ac.cn/help/)。\n\nNoneBot 社区有幸作为开源社区 [参与](https://summer-ospp.ac.cn/org/orgdetail/e1fb5b8d-125a-4138-b756-25bd32c0a31a?lang=zh) 了本次活动，下面列出了目前我们已经发布的项目，欢迎感兴趣的同学通过 [contact@nonebot.dev](mailto:contact@nonebot.dev) 联系我们。\n\n## NoneBot HTML 图片渲染插件\n\n文字与图片一直是聊天机器人的两大主流交互方式，而图片的渲染一直是用户开发应用的一大痛点。常见的方式包括 PIL 图片编辑、浏览器渲染 HTML 截图等。PIL 图片编辑依赖人工构建图片布局，容易出现自适应问题，且提升图片特效、美观程度需要极大的开发成本。浏览器渲染方案通过 HTML 与 CSS 能够轻松完成美观自适应能力强的布局，但其部署门槛较高，难以支撑较大规模调用量。而其他轻量化渲染引擎通常不具有完整 HTML/CSS 现代化标准实现，且未提供 Python Binding 直接使用。\n\n本项目希望调研并实现一种高效、便捷的图片渲染方案。该方案需要在保障跨平台一致性、最大程度保证 HTML 与 CSS 现代化标准的前提下，低成本（资源消耗与吞吐量）将 HTML 渲染为对应图片。\n\n**难度**：进阶\n\n**导师**：[@MelodyKnit](https://github.com/MelodyKnit)\n\n**产出要求**\n\n- 调研 HTML/CSS 渲染引擎\n  - 调研 litehtml 等渲染引擎 标准支持能力与兼容性\n- 基于渲染引擎实现 HTML 图片渲染插件\n  - 将渲染引擎通过 binding 等方式集成为 Python 模块\n  - 基于集成模块实现 HTML 图片渲染能力\n  - 编写插件使用文档\n\n**技术要求**\n\n- 掌握 Python 及其异步编程\n- 熟悉 NoneBot 框架及其插件编写\n- 了解浏览器与 HTML 渲染原理\n\n**成果仓库**\n\n- <https://github.com/nonebot/plugin-htmlkit>\n\n## NB-CLI 命令行工具交互优化\n\nNB-CLI 作为 NoneBot 生态的核心入门与管理工具，主要负责新手引导项目创建、项目运行以及插件管理几大功能。目前该脚手架工具仍存在几点缺陷：\n\n- 作为插件管理工具，由于存储数据的局限性，无法很好地展示用户项目当前安装插件状态，并进行卸载等操作；\n- 当前插件管理高度依赖云端 registry 提供插件信息，在离线情况下完全无法使用；\n- 由于插件信息繁多，工具未能向用户展示充分的信息，交互复杂 体验较差。\n\n以上问题对用户使用 NB-CLI 管理项目插件造成了极大的阻碍。\n本项目希望重点针对插件管理部分，重构工具插件管理模块，完善框架缺陷，并通过缓存等方式确保可用性。其次，调研同类工具方案与 TUI 等相关技术，优化信息展示能力、用户交互方式，提升工具整体交互体验。\n\n**相关链接**\n\n- https://github.com/nonebot/nb-cli/issues/138\n- https://github.com/nonebot/nb-cli/issues/140\n\n**难度**：基础\n\n**导师**：[@yanyongyu](https://github.com/yanyongyu)\n\n**产出要求**\n\n- 重构 NB-CLI 插件管理模块\n  - 优化项目插件信息存储方式，支持列出、卸载插件等操作\n  - 通过缓存 registry 数据等方式确保离线场景的可用性\n- 提升 NB-CLI 交互体验\n  - 调研同类工具方案与 TUI 等相关技术\n  - 优化 registry 多字段信息展示能力\n  - 基于 TUI 等技术优化用户交互方式，提升整体交互体验\n\n**技术要求**\n\n- 熟练掌握 Python 及其异步编程\n- 熟悉 NoneBot 框架与 NB-CLI 使用方法\n- 了解 TUI 等终端交互技术\n\n**成果仓库**\n\n- <https://github.com/nonebot/nb-cli>\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/quick-start.mdx",
    "content": "---\nsidebar_position: 1\ndescription: 尝试使用 NoneBot\n\noptions:\n  menu:\n    - category: tutorial\n      weight: 10\n---\n\nimport Asciinema from \"@site/src/components/Asciinema\";\nimport Messenger from \"@site/src/components/Messenger\";\n\n# 快速上手\n\n:::caution 前提条件\n\n- 请确保你的 Python 版本 >= 3.9\n- **我们强烈建议使用虚拟环境进行开发**，如果没有使用虚拟环境，请确保已经卸载可能存在的 NoneBot v1！！！\n  ```bash\n  pip uninstall nonebot\n  ```\n\n:::\n\n在本章节中，我们将介绍如何使用脚手架来创建一个 NoneBot 简易项目。项目将基于 nb-cli 脚手架运行，并允许我们从商店安装插件。\n\n<Asciinema\n  url=\"https://asciinema.org/a/569440.cast\"\n  options={{ theme: \"monokai\", poster: \"npt:21.5\" }}\n/>\n\n## 安装脚手架\n\n确保你已经安装了 Python 3.9 及以上版本，然后在命令行中执行以下命令：\n\n1. 安装 [pipx](https://pypa.github.io/pipx/)\n\n   ```bash\n   python -m pip install --user pipx\n   python -m pipx ensurepath\n   ```\n\n   如果在此步骤的输出中出现了“open a new terminal”或者“re-login”字样，那么请关闭当前终端并重新打开一个新的终端。\n\n2. 安装脚手架\n\n   ```bash\n   pipx install nb-cli\n   ```\n\n安装完成后，你可以在命令行使用 `nb` 命令来使用脚手架。如果出现无法找到命令的情况（例如出现“Command not found”字样），请参考 [pipx 文档](https://pypa.github.io/pipx/) 检查你的环境变量。\n\n## 创建项目\n\n使用脚手架来创建一个项目：\n\n```bash\nnb create\n```\n\n这一指令将会执行创建项目的流程，你将会看到一些询问：\n\n1. 项目模板\n\n   ```bash\n   [?] 选择一个要使用的模板: bootstrap (初学者或用户)\n   ```\n\n   这里我们选择 `bootstrap` 模板，它是一个简单的项目模板，能够安装商店插件。如果你需要**自行编写插件**，这里请选择 `simple` 模板。\n\n2. 项目名称\n\n   ```bash\n   [?] 项目名称: awesome-bot\n   ```\n\n   这里我们以 `awesome-bot` 为例，作为项目名称。你可以根据自己的需要来命名。\n\n3. 其他选项\n   请注意，多选项使用**空格**选中或取消，**回车**确认。\n\n   ```bash\n   [?] 要使用哪些适配器? Console (基于终端的交互式适配器)\n   [?] 要使用哪些驱动器? FastAPI (FastAPI 驱动器)\n   [?] 要使用什么本地存储策略? 用户全局 (默认，适用于单用户下单实例)\n   [?] 立即安装依赖? (Y/n) Yes\n   [?] 创建虚拟环境? (Y/n) Yes\n   ```\n\n   这里我们选择了创建虚拟环境，nb-cli 在之后的操作中将会自动使用这个虚拟环境。如果你不需要自动创建虚拟环境或者已经创建了其他虚拟环境，nb-cli 将会安装依赖至当前激活的 Python 虚拟环境。\n\n4. 选择内置插件\n\n   ```bash\n   [?] 要使用哪些内置插件? echo\n   ```\n\n   这里我们选择 `echo` 插件作为示例。这是一个简单的复读回显插件，可以用于测试你的机器人是否正常运行。\n\n## 运行项目\n\n在项目创建完成后，你可以在**项目目录**中使用以下命令来运行项目：\n\n```bash\nnb run\n```\n\n你现在应该已经运行起来了你的第一个 NoneBot 项目了！请注意，生成的项目中使用了 `FastAPI` 驱动器和 `Console` 适配器，你之后可以自行修改配置或安装其他适配器。\n\n## 尝试使用\n\n在项目运行起来后，`Console` 适配器会在你的终端启动交互模式，你可以直接在输入框中输入 `/echo hello world` 来测试你的机器人是否正常运行。\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/echo hello world\" },\n    { position: \"left\", msg: \"hello world\" },\n  ]}\n/>\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/tutorial/application.mdx",
    "content": "---\nsidebar_position: 0\ndescription: 创建一个 NoneBot 项目\n\noptions:\n  menu:\n    - category: tutorial\n      weight: 20\n---\n\n# 手动创建项目\n\nimport Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\n\n在[快速上手](../quick-start.mdx)中，我们已经介绍了如何安装和使用 `nb-cli` 创建一个项目。在本章节中，我们将简要介绍如何在不使用 `nb-cli` 的方式创建一个机器人项目的**最小实例**并启动。如果你想要了解 NoneBot 的启动流程，也可以阅读本章节。\n\n:::caution 警告\n我们十分不推荐直接创建机器人项目，请优先考虑使用 nb-cli 进行项目创建。\n:::\n\n一个机器人项目的**最小实例**中**至少**需要包含以下内容:\n\n- 入口文件：初始化并运行机器人的 Python 文件\n- 配置文件：存储机器人启动所需的配置\n- 插件：为机器人提供具体的功能\n\n下面我们创建一个项目文件夹，来存放项目所需文件，以下步骤均在该文件夹中进行。\n\n## 安装依赖\n\n在创建项目前，我们首先需要将项目所需依赖安装至环境中。\n\n1. （可选）创建虚拟环境，以 venv 为例\n\n   <Tabs groupId=\"platform\">\n   <TabItem value=\"windows\" label=\"Windows\" default>\n\n   ```bash\n   # 创建虚拟环境\n   python -m venv .venv --prompt nonebot2\n   # 激活虚拟环境\n   .venv\\Scripts\\activate\n   ```\n\n   </TabItem>\n   <TabItem value=\"linux/macos\" label=\"Linux/macOS\">\n\n   ```bash\n   # 创建虚拟环境\n   python -m venv .venv --prompt nonebot2\n   # 激活虚拟环境\n   source .venv/bin/activate\n   ```\n\n   </TabItem>\n   </Tabs>\n\n2. 安装 nonebot2 以及驱动器，以 Fastapi 驱动器为例\n\n   <Tabs groupId=\"platform\">\n   <TabItem value=\"windows\" label=\"Windows\" default>\n\n   ```bash\n   pip install \"nonebot2[fastapi]\"\n   ```\n\n   </TabItem>\n   <TabItem value=\"linux/macos\" label=\"Linux/macOS\">\n\n   ```bash\n   pip install \"nonebot2[fastapi]\"\n   ```\n\n   </TabItem>\n   </Tabs>\n\n   驱动器包名可以在 [驱动器商店](/store/drivers) 中找到，请替换上文方括号中的内容。\n\n3. 安装适配器，以 Console 适配器为例\n\n   ```bash\n   pip install nonebot-adapter-console\n   ```\n\n   适配器包名可以在 [适配器商店](/store/adapters) 中找到。\n\n## 创建配置文件\n\n配置文件用于存放 NoneBot 运行所需要的配置项，使用 [`pydantic`](https://docs.pydantic.dev/) 以及 [`python-dotenv`](https://saurabh-kumar.com/python-dotenv/) 来读取配置。配置项需符合 dotenv 格式，复杂类型数据需使用 JSON 格式填写。具体可选配置方式以及配置项详情参考[配置](../appendices/config.mdx)。\n\n在**项目文件夹**中创建一个名为 `.env` 的文件，并写入以下内容:\n\n```bash title=.env\nHOST=0.0.0.0  # 配置 NoneBot 监听的 IP / 主机名\nPORT=8080  # 配置 NoneBot 监听的端口\nCOMMAND_START=[\"/\"]  # 配置命令起始字符\nCOMMAND_SEP=[\".\"]  # 配置命令分割字符\n```\n\n## 创建入口文件\n\n入口文件（ Entrypoint ）顾名思义，是用来初始化并运行机器人的 Python 文件。入口文件需要完成框架的初始化、注册适配器、加载插件等工作。\n\n:::tip 提示\n如果你使用 `nb-cli` 创建项目，入口文件不会被创建，该文件功能会被 `nb run` 命令代替。\n:::\n\n在**项目文件夹**中创建一个 `bot.py` 文件，并写入以下内容:\n\n```python title=bot.py\nimport nonebot\nfrom nonebot.adapters.console import Adapter as ConsoleAdapter  # 避免重复命名\n\n# 初始化 NoneBot\nnonebot.init()\n\n# 注册适配器\ndriver = nonebot.get_driver()\ndriver.register_adapter(ConsoleAdapter)\n\n# 在这里加载插件\nnonebot.load_builtin_plugins(\"echo\")  # 内置插件\n# nonebot.load_plugin(\"thirdparty_plugin\")  # 第三方插件\n# nonebot.load_plugins(\"awesome_bot/plugins\")  # 本地插件\n\nif __name__ == \"__main__\":\n    nonebot.run()\n```\n\n我们暂时不需要了解其中内容的含义，这些将会在稍后的章节中逐一介绍。在创建完成以上文件并确认已安装所需适配器和插件后，即可运行机器人。\n\n## 运行机器人\n\n在**项目文件夹**中，使用配置好环境的 Python 解释器运行入口文件:\n\n<Tabs groupId=\"platform\">\n  <TabItem value=\"windows\" label=\"Windows\" default>\n\n```bash\n# 激活虚拟环境（未使用虚拟环境时跳过此行）\n.venv\\Scripts\\activate\n# 运行机器人\npython bot.py\n```\n\n  </TabItem>\n  <TabItem value=\"linux/macos\" label=\"Linux/macOS\">\n\n```bash\n# 激活虚拟环境（未使用虚拟环境时跳过此行）\nsource .venv/bin/activate\n# 运行机器人\npython bot.py\n```\n\n  </TabItem>\n</Tabs>\n\n如果你后续使用了 `nb-cli` ，你仍可以使用 `nb run` 命令来运行机器人，`nb-cli` 会自动检测入口文件 `bot.py` 是否存在并运行。同时，你也可以使用 `nb run --reload` 来自动检测代码的更改并自动重新运行入口文件。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/tutorial/create-plugin.md",
    "content": "---\nsidebar_position: 3\ndescription: 创建并加载自定义插件\n\noptions:\n  menu:\n    - category: tutorial\n      weight: 50\n---\n\n# 插件编写准备\n\n在正式编写插件之前，我们需要先了解一下插件的概念。\n\n## 插件结构\n\n在 NoneBot 中，插件即是 Python 的一个[模块（module）](https://docs.python.org/zh-cn/3/glossary.html#term-module)。NoneBot 会在导入时对这些模块做一些特殊的处理使得他们成为一个插件。插件间应尽量减少耦合，可以进行有限制的相互调用，NoneBot 能够正确解析插件间的依赖关系。\n\n### 单文件插件\n\n一个普通的 `.py` 文件即可以作为一个插件，例如创建一个 `foo.py` 文件：\n\n```tree title=Project\n📂 plugins\n└── 📜 foo.py\n```\n\n这个时候模块 `foo` 已经可以被称为一个插件了，尽管它还什么都没做。\n\n### 包插件\n\n一个包含 `__init__.py` 的文件夹即是一个常规 Python [包 `package`](https://docs.python.org/zh-cn/3/glossary.html#term-regular-package)，例如创建一个 `foo` 文件夹：\n\n```tree title=Project\n📂 plugins\n└── 📂 foo\n    └── 📜 __init__.py\n```\n\n这个时候包 `foo` 同样是一个合法的插件，插件内容可以在 `__init__.py` 文件中编写。\n\n## 创建插件\n\n:::caution 注意\n如果在之前的[快速上手](../quick-start.mdx)章节中已经使用 `bootstrap` 模板创建了项目，那么你需要做出如下修改：\n\n1. 在项目目录中创建一个两层文件夹 `awesome_bot/plugins`\n\n   ```tree title=Project\n   📦 awesome-bot\n   ├── 📂 .venv\n   ├── 📂 awesome_bot\n   │   └── 📂 plugins\n   ├── 📜 .env.prod\n   ├── 📜 pyproject.toml\n   └── 📜 README.md\n   ```\n\n2. 修改 `pyproject.toml` 文件中的 `nonebot` 配置项，在 `plugin_dirs` 中添加 `awesome_bot/plugins`\n\n   ```toml title=pyproject.toml\n   [tool.nonebot]\n   plugin_dirs = [\"awesome_bot/plugins\"]\n   ```\n\n:::\n\n:::caution 注意\n如果在之前的[创建项目](./application.mdx)章节中手动创建了相关文件，那么你需要做出如下修改：\n\n1. 在项目目录中创建一个两层文件夹 `awesome_bot/plugins`\n\n   ```tree title=Project\n   📦 awesome-bot\n   ├── 📂 awesome_bot\n   │   └── 📂 plugins\n   └── 📜 bot.py\n   ```\n\n2. 修改 `bot.py` 文件中的加载插件部分，取消注释或者添加如下代码\n\n   ```python title=bot.py\n   # 在这里加载插件\n   nonebot.load_builtin_plugins(\"echo\")  # 内置插件\n   nonebot.load_plugins(\"awesome_bot/plugins\")  # 本地插件\n   ```\n\n:::\n\n创建插件可以通过 `nb-cli` 命令从完整模板创建，也可以手动新建空白文件。通过以下命令创建一个名为 `weather` 的插件：\n\n```bash\n$ nb plugin create\n[?] 插件名称: weather\n[?] 使用嵌套插件? (y/N) N\n[?] 请输入插件存储位置: awesome_bot/plugins\n```\n\n`nb-cli` 会在 `awesome_bot/plugins` 目录下创建一个名为 `weather` 的文件夹，其中包含的文件将在稍后章节中用到。\n\n```tree title=Project\n📦 awesome-bot\n├── 📂 .venv\n├── 📂 awesome_bot\n│   └── 📂 plugins\n|       └── 📂 weather\n|           ├── 📜 __init__.py\n|           └── 📜 config.py\n├── 📜 .env.prod\n├── 📜 pyproject.toml\n└── 📜 README.md\n```\n\n## 加载插件\n\n:::danger 警告\n请勿在插件被加载前 `import` 插件模块，这会导致 NoneBot 无法将其转换为插件而出现意料之外的情况。\n:::\n\n加载插件是在机器人入口文件中完成的，需要在框架初始化之后，运行之前进行。\n\n请注意，加载的插件模块名称（插件文件名或文件夹名）**不能相同**，且每一个插件**只能被加载一次**，重复加载将会导致异常。\n\n如果你使用 `nb-cli` 管理插件，那么你可以跳过这一节，`nb-cli` 将会自动处理加载。\n\n如果你**使用自定义的入口文件** `bot.py`，那么你需要在 `bot.py` 中加载插件。\n\n```python {5} title=bot.py\nimport nonebot\n\nnonebot.init()\n\n# 加载插件\n\nnonebot.run()\n```\n\n加载插件的方式有多种，但在底层的加载逻辑是一致的。以下是为加载插件提供的几种方式：\n\n### `load_plugin`\n\n通过点分割模块名称或使用 [`pathlib`](https://docs.python.org/zh-cn/3/library/pathlib.html) 的 `Path` 对象来加载插件。通常用于加载第三方插件或者项目插件。例如：\n\n```python\nfrom pathlib import Path\n\nnonebot.load_plugin(\"path.to.your.plugin\")  # 加载第三方插件\nnonebot.load_plugin(Path(\"./path/to/your/plugin.py\"))  # 加载项目插件\n```\n\n:::caution 注意\n请注意，本地插件的路径应该为相对机器人 **入口文件（通常为 bot.py）** 可导入的，例如在项目 `plugins` 目录下。\n:::\n\n### `load_plugins`\n\n加载传入插件目录中的所有插件，通常用于加载一系列本地编写的项目插件。例如：\n\n```python\nnonebot.load_plugins(\"src/plugins\", \"path/to/your/plugins\")\n```\n\n:::caution 注意\n请注意，插件目录应该为相对机器人 **入口文件（通常为 bot.py）** 可导入的，例如在项目 `plugins` 目录下。\n:::\n\n### `load_all_plugins`\n\n这种加载方式是以上两种方式的混合，加载所有传入的插件模块名称，以及所有给定目录下的插件。例如：\n\n```python\nnonebot.load_all_plugins([\"path.to.your.plugin\"], [\"path/to/your/plugins\"])\n```\n\n### `load_from_json`\n\n通过 JSON 文件加载插件，是 [`load_all_plugins`](#load_all_plugins) 的 JSON 变种。通过读取 JSON 文件中的 `plugins` 字段和 `plugin_dirs` 字段进行加载。例如：\n\n```json title=plugin_config.json\n{\n  \"plugins\": [\"path.to.your.plugin\"],\n  \"plugin_dirs\": [\"path/to/your/plugins\"]\n}\n```\n\n```python\nnonebot.load_from_json(\"plugin_config.json\", encoding=\"utf-8\")\n```\n\n:::tip 提示\n如果 JSON 配置文件中的字段无法满足你的需求，可以使用 [`load_all_plugins`](#load_all_plugins) 方法自行读取配置来加载插件。\n:::\n\n### `load_from_toml`\n\n通过 TOML 文件加载插件，是 [`load_all_plugins`](#load_all_plugins) 的 TOML 变种。通过读取 TOML 文件中的 `[tool.nonebot]` Table 中的 `plugin_dirs` Array 与\n`[tool.nonebot.plugins]` Table 中的多个 Array 进行加载。例如：\n\n```toml title=plugin_config.toml\n[tool.nonebot]\nplugin_dirs = [\"path/to/your/plugins\"]\n\n[tool.nonebot.plugins]\n\"@local\" = [\"path.to.your.plugin\"]  # 本地插件等非插件商店来源的插件\n\"nonebot-plugin-someplugin\" = [\"nonebot_plugin_someplugin\"]  # 插件商店来源的插件\n```\n\n```python\nnonebot.load_from_toml(\"plugin_config.toml\", encoding=\"utf-8\")\n```\n\n:::tip 提示\n如果 TOML 配置文件中的字段无法满足你的需求，可以使用 [`load_all_plugins`](#load_all_plugins) 方法自行读取配置来加载插件。\n:::\n\n### `load_builtin_plugin`\n\n加载一个内置插件，传入的插件名必须为 NoneBot 内置插件。该方法是 [`load_plugin`](#load_plugin) 的封装。例如：\n\n```python\nnonebot.load_builtin_plugin(\"echo\")\n```\n\n### `load_builtin_plugins`\n\n加载传入插件列表中的所有内置插件。例如：\n\n```python\nnonebot.load_builtin_plugins(\"echo\", \"single_session\")\n```\n\n### 其他加载方式\n\n有关其他插件加载的方式，可参考[跨插件访问](../advanced/requiring.md)和[嵌套插件](../advanced/plugin-nesting.md)。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/tutorial/event-data.mdx",
    "content": "---\nsidebar_position: 6\ndescription: 通过依赖注入获取所需事件信息\n\noptions:\n  menu:\n    - category: tutorial\n      weight: 80\n---\n\n# 获取事件信息\n\nimport Messenger from \"@site/src/components/Messenger\";\n\n在 NoneBot 事件处理流程中，获取事件信息并做出对应的操作是非常常见的场景。本章节中我们将介绍如何通过**依赖注入**获取事件信息。\n\n## 认识依赖注入\n\n在事件处理流程中，事件响应器具有自己独立的上下文，例如：当前响应的事件、收到事件的机器人或者其他处理流程中新增的信息等。这些数据可以根据我们的需求，通过依赖注入的方式，在执行事件处理流程中注入到事件处理函数中。\n\n相对于传统的信息获取方法，通过依赖注入获取信息的最大特色在于**按需获取**。如果该事件处理函数不需要任何额外信息即可运行，那么可以不进行依赖注入。如果事件处理函数需要额外的数据，可以通过依赖注入的方式灵活的标注出需要的依赖，在函数运行时便会被按需注入。\n\n## 使用依赖注入\n\n使用依赖注入获取上下文信息的方法十分简单，我们仅需要在函数的参数中声明所需的依赖，并正确的将函数添加为事件处理依赖即可。在 NoneBot 中，我们可以直接使用 `nonebot.params` 模块中定义的参数类型来声明依赖。\n\n例如，我们可以继续改进上一章节中的 `weather` 插件，使其可以获取到 `天气` 命令的地名参数，并根据地名返回天气信息。\n\n```python {9,11} title=weather/__init__.py\nfrom nonebot import on_command\nfrom nonebot.rule import to_me\nfrom nonebot.adapters import Message\nfrom nonebot.params import CommandArg\n\nweather = on_command(\"天气\", rule=to_me(), aliases={\"weather\", \"查天气\"}, priority=10, block=True)\n\n@weather.handle()\nasync def handle_function(args: Message = CommandArg()):\n    # 提取参数纯文本作为地名，并判断是否有效\n    if location := args.extract_plain_text():\n        await weather.finish(f\"今天{location}的天气是...\")\n    else:\n        await weather.finish(\"请输入地名\")\n```\n\n如上方示例所示，我们使用了 `args` 作为注入参数名，注入的内容为 `CommandArg()`，也就是**消息命令后跟随的内容**。在这个示例中，我们获得的参数会被检查是否有效，对无效参数则会结束事件。\n\n:::tip 提示\n命令与参数之间可以不需要空格，`CommandArg()` 获取的信息为命令后跟随的内容并去除了头部空白符。例如：`/天气 上海` 消息的参数为 `上海`。\n:::\n\n:::tip 提示\n`:=` 是 Python 3.8 引入的新语法 [Assignment Expressions](https://docs.python.org/zh-cn/3/reference/expressions.html#assignment-expressions)，也称为海象表达式，可以在表达式中直接赋值。\n:::\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/天气\" },\n    { position: \"left\", msg: \"请输入地名\" },\n    { position: \"right\", msg: \"/天气 上海\" },\n    { position: \"left\", msg: \"今天上海的天气是...\" },\n  ]}\n/>\n\nNoneBot 提供了多种依赖注入类型，可以获取不同的信息，具体内容可参考[依赖注入](../advanced/dependency.mdx)。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/tutorial/fundamentals.md",
    "content": "---\nsidebar_position: 1\ndescription: NoneBot 机器人构成及基本使用\n\noptions:\n  menu:\n    - category: tutorial\n      weight: 30\n---\n\n# 机器人的构成\n\n了解机器人的基本构成有助于你更好地使用 NoneBot，本章节将介绍 NoneBot 中的基本组成部分，稍后的文档中将会使用到这些概念。\n\n使用 NoneBot 框架搭建的机器人具有以下几个基本组成部分：\n\n1. NoneBot 机器人框架主体：负责连接各个组成部分，提供基本的机器人功能\n2. 驱动器 `Driver`：客户端/服务端的功能实现，负责接收和发送消息（通常为 HTTP 通信）\n3. 适配器 `Adapter`：驱动器的上层，负责将**平台消息**与 NoneBot 事件/操作系统的消息格式相互转换\n4. 插件 `Plugin`：机器人的功能实现，通常为负责处理事件并进行一系列的操作\n\n除 NoneBot 机器人框架主体外，其他部分均可按需选择、互相搭配，但由于平台的兼容性问题，部分插件可能仅在某些特定平台上可用（这由插件编写者决定）。\n\n在接下来的章节中，我们将重点介绍机器人功能实现，即插件 `Plugin` 部分。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/tutorial/handler.mdx",
    "content": "---\nsidebar_position: 5\ndescription: 处理接收到的特定事件\n\noptions:\n  menu:\n    - category: tutorial\n      weight: 70\n---\n\n# 事件处理\n\nimport Messenger from \"@site/src/components/Messenger\";\n\n在我们收到事件，并被某个事件响应器正确响应后，便正式开启了对于这个事件的**处理流程**。\n\n## 认识事件处理流程\n\n就像我们在解决问题时需要遵循流程一样，处理一个事件也需要一套流程。在事件响应器对一个事件进行响应之后，会依次执行一系列的**事件处理依赖**（通常是函数）。简单来说，事件处理流程并不是一个函数、一个对象或一个方法，而是一整套由开发者设计的流程。\n\n在这个流程中，我们**目前**只需要了解两个概念：函数形式的“事件处理依赖”（下称“事件处理函数”）和“事件响应器操作”。\n\n## 事件处理函数\n\n在事件响应器中，事件处理流程可以由一个或多个“事件处理函数”组成，这些事件处理函数将会按照顺序依次对事件进行处理，直到全部执行完成或被中断。我们可以采用事件响应器的“事件处理函数装饰器”来添加这些“事件处理函数”。\n\n顾名思义，“事件处理函数装饰器”是一个[装饰器（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)中所展示的包装用法相同。例如：\n\n```python {6-8} title=weather/__init__.py\nfrom nonebot import on_command\nfrom nonebot.rule import to_me\n\nweather = on_command(\"天气\", rule=to_me(), aliases={\"weather\", \"查天气\"}, priority=10, block=True)\n\n@weather.handle()\nasync def handle_function():\n    pass  # do something here\n```\n\n如上方示例所示，我们使用 `weather` 响应器的 `handle` 装饰器装饰了一个函数 `handle_function`。`handle_function` 函数会被添加到 `weather` 的事件处理流程中。在 `weather` 响应器被触发之后，将会依次调用 `weather` 响应器的事件处理函数，即 `handle_function` 来对事件进行处理。\n\n## 事件响应器操作\n\n在事件处理流程中，我们可以使用事件响应器操作来进行一些交互或改变事件处理流程，例如向机器人用户发送消息或提前结束事件处理流程等。\n\n事件响应器操作与事件处理函数装饰器类似，通常作为事件响应器 `Matcher` 的[类方法](https://docs.python.org/zh-cn/3/library/functions.html#classmethod)存在，因此事件响应器操作的调用方法也是 `Matcher.func()` 的形式。不过不同的是，事件响应器操作并不是装饰器，因此并不需要@进行标注。\n\n```python {8,9} title=weather/__init__.py\nfrom nonebot import on_command\nfrom nonebot.rule import to_me\n\nweather = on_command(\"天气\", rule=to_me(), aliases={\"weather\", \"查天气\"}, priority=10, block=True)\n\n@weather.handle()\nasync def handle_function():\n    # await weather.send(\"天气是...\")\n    await weather.finish(\"天气是...\")\n```\n\n如上方示例所示，我们使用 `weather` 响应器的 `finish` 操作方法向机器人用户回复了 `天气是...` 并结束了事件处理流程。效果如下：\n\n<Messenger\n  msgs={[\n    { position: \"right\", msg: \"/天气\" },\n    { position: \"left\", msg: \"天气是...\" },\n  ]}\n/>\n\n值得注意的是，在执行 `finish` 方法时，NoneBot 会在向机器人用户发送消息内容后抛出 `FinishedException` 异常来结束事件响应流程。也就是说，在 `finish` 被执行后，后续的程序是不会被执行的。如果你需要回复机器人用户消息但不想事件处理流程结束，可以使用注释的部分中展示的 `send` 方法。\n\n:::danger 警告\n由于 `finish` 是通过抛出 `FinishedException` 异常来结束事件的，因此异常可能会被未加限制的 `try-except` 捕获，影响事件处理流程正确处理，导致无法正常结束此事件。请务必在异常捕获中指定错误类型或排除所有 [MatcherException](../api/exception.md#MatcherException) 类型的异常（如下所示），或将 `finish` 移出捕获范围进行使用。\n\n```python\nfrom nonebot.exception import MatcherException\n\ntry:\n    await weather.finish(\"天气是...\")\nexcept MatcherException:\n    raise\nexcept Exception as e:\n    pass # do something here\n```\n\n:::\n\n目前 NoneBot 提供了多种事件响应器操作，其中包括用于机器人用户交互与流程控制两大类，进阶使用方法可以查看[会话控制](../appendices/session-control.mdx)。\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/tutorial/matcher.md",
    "content": "---\nsidebar_position: 4\ndescription: 响应接收到的特定事件\n\noptions:\n  menu:\n    - category: tutorial\n      weight: 60\n---\n\n# 事件响应器\n\n事件响应器（Matcher）是对接收到的事件进行响应的基本单元，所有的事件响应器都继承自 `Matcher` 基类。\n\n在 NoneBot 中，事件响应器可以通过一系列特定的规则**筛选**出**具有某种特征的事件**，并按照**特定的流程**交由**预定义的事件处理依赖**进行处理。例如，在[快速上手](../quick-start.mdx)中，我们使用了内置插件 `echo` ，它定义的事件响应器能响应机器人用户发送的“/echo hello world”消息，提取“hello world”信息并作为回复消息发送。\n\n## 事件响应器辅助函数\n\nNoneBot 中所有事件响应器均继承自 `Matcher` 基类，但直接使用 `Matcher.new()` 方法创建事件响应器过于繁琐且不能记录插件信息。因此，NoneBot 中提供了一系列“事件响应器辅助函数”（下称“辅助函数”）来辅助我们用**最简的方式**创建**带有不同规则预设**的事件响应器，提高代码可读性和书写效率。通常情况下，我们只需要使用辅助函数即可完成事件响应器的创建。\n\n在 NoneBot 中，辅助函数以 `on()` 或 `on_<type/rule>()` 形式出现（例如 `on_command()`），调用后根据不同的参数返回一个 `Type[Matcher]` 类型的新事件响应器。\n\n目前 NoneBot 提供了多种功能各异的辅助函数、具有共同命令名称前缀的命令组以及具有共同参数的响应器组，均可以从 `nonebot` 模块直接导入使用，具体内容参考[事件响应器进阶](../advanced/matcher.md)。\n\n## 创建事件响应器\n\n在上一节[创建插件](./create-plugin.md#创建插件)中，我们创建了一个 `weather` 插件，现在我们来实现他的功能。\n\n我们直接使用 `on_command()` 辅助函数来创建一个事件响应器：\n\n```python {3} title=weather/__init__.py\nfrom nonebot import on_command\n\nweather = on_command(\"天气\")\n```\n\n这样，我们就获得一个名为 `weather` 的事件响应器了，这个事件响应器会对 `/天气` 开头的消息进行响应。\n\n:::tip 提示\n如果一条消息中包含“@机器人”或以“机器人的昵称”开始，例如 `@bot /天气` 时，协议适配器会将 `event.is_tome()` 判断为 `True` ，同时也会自动去除 `@bot`，即事件响应器收到的信息内容为 `/天气`，方便进行命令匹配。\n:::\n\n### 为事件响应器添加参数\n\n在辅助函数中，我们可以添加一些参数来对事件响应器进行更加精细的调整，例如事件响应器的优先级、匹配规则等。例如：\n\n```python {4} title=weather/__init__.py\nfrom nonebot import on_command\nfrom nonebot.rule import to_me\n\nweather = on_command(\"天气\", rule=to_me(), aliases={\"weather\", \"查天气\"}, priority=10, block=True)\n```\n\n这样，我们就获得了一个可以响应 `天气`、`weather`、`查天气` 三个命令的响应规则，需要私聊或 `@bot` 时才会响应，优先级为 10（越小越优先），阻断事件向后续优先级传播的事件响应器了。这些内容的意义和使用方法将会在后续的章节中一一介绍。\n\n:::tip 提示\n需要注意的是，不同的辅助函数有不同的可选参数，在使用之前可以参考[事件响应器进阶 - 基本辅助函数](../advanced/matcher.md#基本辅助函数)或 [API 文档](../api/plugin/on.md#on)。\n:::\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/tutorial/message.md",
    "content": "---\nsidebar_position: 7\ndescription: 处理消息序列与消息段\n\noptions:\n  menu:\n    - category: tutorial\n      weight: 90\n---\n\n# 处理消息\n\n在不同平台中，一条消息可能会有承载有各种不同的表现形式，它可能是一段纯文本、一张图片、一段语音、一篇富文本文章，也有可能是多种类型的组合等等。\n\n在 NoneBot 中，为确保消息的正常处理与跨平台兼容性，采用了扁平化的消息序列形式，即 `Message` 对象。消息序列是 NoneBot 中的消息载体，无论是接收还是发送的消息，都采用消息序列的形式进行处理。\n\n## 认识消息类型\n\n### 消息序列 `Message`\n\n在 NoneBot 中，消息序列 `Message` 的主要作用是用于表达“一串消息”。由于消息序列继承自 `List[MessageSegment]`，所以 `Message` 的本质是由若干消息段所组成的序列。因此，消息序列的使用方法与 `List` 有很多相似之处，例如切片、索引、拼接等。\n\n在上一节的[使用依赖注入](./event-data.mdx#使用依赖注入)中，我们已经通过依赖注入 `CommandArg()` 获取了命令的参数，它的类型即是消息序列。我们使用了消息序列的 `extract_plain_text()` 方法来获取消息序列中的纯文本内容。\n\n### 消息段 `MessageSegment`\n\n顾名思义，消息段 `MessageSegment` 是一段消息。由于消息序列的本质是由若干消息段所组成的序列，消息段可以被认为是构成消息序列的最小单位。简单来说，消息序列类似于一个自然段，而消息段则是组成自然段的一句话。同时，作为特殊消息载体的存在，绝大多数的平台都有着**独特的消息类型**，这些独特的内容均需要由对应的**协议适配器**所提供，以适应不同平台中的消息模式。**这也意味着，你需要导入对应的协议适配器中的消息序列和消息段后才能使用其特殊的工厂方法。**\n\n:::caution 注意\n消息段的类型是由协议适配器提供的，因此你需要参考协议适配器的文档并导入对应的消息段后才能使用其特殊的消息类型。\n\n在上一节的[使用依赖注入](./event-data.mdx#使用依赖注入)中，我们导入的为 `nonebot.adapters.Message` 抽象基类，因此我们无法使用平台特有的消息类型。仅能使用 `str` 作为纯文本消息回复。\n:::\n\n## 使用消息序列\n\n:::caution 注意\n在以下的示例中，为了更好的理解多种类型的消息组成方式，我们将使用 `Console` 协议适配器来演示消息序列的使用方法。在实际使用中，你需要确保你使用的**消息序列类型**与你所要发送的**平台类型**一致。\n:::\n\n通常情况下，适配器在接收到消息时，会将消息转换为消息序列，可以通过依赖注入 [`EventMessage`](../advanced/dependency.mdx#eventmessage)，或者使用 `event.get_message()` 获取。\n\n由于消息序列是 `List[MessageSegment]` 的子类，所以你总是可以用和操作 `List` 类似的方式来处理消息序列。例如：\n\n```python\n>>> from nonebot.adapters.console import Message, MessageSegment\n>>> message = Message([\n    MessageSegment(type=\"text\", data={\"text\":\"hello\"}),\n    MessageSegment(type=\"markdown\", data={\"markup\":\"**world**\"}),\n])\n>>> for segment in message:\n...     print(segment.type, segment.data)\n...\ntext {'text': 'hello'}\nmarkdown {'markup': '**world**'}\n>>> len(message)\n2\n```\n\n### 构造消息序列\n\n在使用事件响应器操作发送消息时，既可以使用 `str` 作为消息，也可以使用 `Message`、`MessageSegment` 或者 `MessageTemplate`。那么，我们就需要先构造一个消息序列。消息序列可以通过多种方式构造：\n\n#### 直接构造\n\n`Message` 类可以直接实例化，支持 `str`、`MessageSegment`、`Iterable[MessageSegment]` 或适配器自定义类型的参数。\n\n```python\nfrom nonebot.adapters.console import Message, MessageSegment\n\n# str\nMessage(\"Hello, world!\")\n# MessageSegment\nMessage(MessageSegment.text(\"Hello, world!\"))\n# List[MessageSegment]\nMessage([MessageSegment.text(\"Hello, world!\")])\n```\n\n#### 运算构造\n\n`Message` 对象可以通过 `str`、`MessageSegment` 相加构造，详情请参考[拼接消息](#拼接消息)。\n\n#### 从字典数组构造\n\n`Message` 对象支持 Pydantic 自定义类型构造，可以使用 Pydantic 的 `TypeAdapter` 方法进行构造。\n\n```python\nfrom pydantic import TypeAdapter\nfrom nonebot.adapters.console import Message, MessageSegment\n\n# 由字典构造消息段\nTypeAdapter(MessageSegment).validate_python(\n    {\"type\": \"text\", \"data\": {\"text\": \"text\"}}\n) == MessageSegment.text(\"text\")\n\n# 由字典数组构造消息序列\nTypeAdapter(Message).validate_python(\n    [MessageSegment.text(\"text\"), {\"type\": \"text\", \"data\": {\"text\": \"text\"}}],\n) == Message([MessageSegment.text(\"text\"), MessageSegment.text(\"text\")])\n```\n\n### 获取消息纯文本\n\n由于消息中存在各种类型的消息段，因此 `str(message)` 通常**不能得到消息的纯文本**，而是一个消息序列的字符串表示。\n\nNoneBot 为消息段定义了一个方法 `is_text()` ，可以用于判断消息段是否为纯文本；也可以使用 `message.extract_plain_text()` 方法获取消息纯文本。\n\n```python\nfrom nonebot.adapters.console import Message, MessageSegment\n\n# 判断消息段是否为纯文本\nMessageSegment.text(\"text\").is_text() == True\n\n# 提取消息纯文本字符串\nMessage(\n    [MessageSegment.text(\"text\"), MessageSegment.markdown(\"**markup**\")]\n).extract_plain_text() == \"text\"\n```\n\n### 遍历\n\n消息序列继承自 `List[MessageSegment]` ，因此可以使用 `for` 循环遍历消息段。\n\n```python\nfor segment in message:\n    ...\n```\n\n### 比较\n\n消息和消息段都可以使用 `==` 或 `!=` 运算符比较是否相同。\n\n```python\nMessageSegment.text(\"text\") != MessageSegment.text(\"foo\")\n\nsome_message == Message([MessageSegment.text(\"text\")])\n```\n\n### 检查消息段\n\n我们可以通过 `in` 运算符或消息序列的 `has` 方法来：\n\n```python\n# 是否存在消息段\nMessageSegment.text(\"text\") in message\n# 是否存在指定类型的消息段\n\"text\" in message\n```\n\n我们还可以使用消息序列的 `only` 方法来检查消息中是否仅包含指定的消息段。\n\n```python\n# 是否都为指定消息段\nmessage.only(MessageSegment.text(\"test\"))\n# 是否仅包含指定类型的消息段\nmessage.only(\"text\")\n```\n\n### 过滤、索引与切片\n\n消息序列对列表的索引与切片进行了增强，在原有列表 `int` 索引与 `slice` 切片的基础上，支持 `type` 过滤索引与切片。\n\n```python\nfrom nonebot.adapters.console import Message, MessageSegment\n\nmessage = Message(\n    [\n        MessageSegment.text(\"test\"),\n        MessageSegment.markdown(\"test2\"),\n        MessageSegment.markdown(\"test3\"),\n        MessageSegment.text(\"test4\"),\n    ]\n)\n# 索引\nmessage[0] == MessageSegment.text(\"test\")\n# 切片\nmessage[0:2] == Message(\n    [MessageSegment.text(\"test\"), MessageSegment.markdown(\"test2\")]\n)\n# 类型过滤\nmessage[\"markdown\"] == Message(\n    [MessageSegment.markdown(\"test2\"), MessageSegment.markdown(\"test3\")]\n)\n# 类型索引\nmessage[\"markdown\", 0] == MessageSegment.markdown(\"test2\")\n# 类型切片\nmessage[\"markdown\", 0:2] == Message(\n    [MessageSegment.markdown(\"test2\"), MessageSegment.markdown(\"test3\")]\n)\n```\n\n我们也可以通过消息序列的 `include`、`exclude` 方法进行类型过滤。\n\n```python\nmessage.include(\"text\", \"markdown\")\nmessage.exclude(\"text\")\n```\n\n同样的，消息序列对列表的 `index`、`count` 方法也进行了增强，可以用于索引指定类型的消息段。\n\n```python\n# 指定类型首个消息段索引\nmessage.index(\"markdown\") == 1\n# 指定类型消息段数量\nmessage.count(\"markdown\") == 2\n```\n\n此外，消息序列添加了一个 `get` 方法，可以用于获取指定类型指定个数的消息段。\n\n```python\n# 获取指定类型指定个数的消息段\nmessage.get(\"markdown\", 1) == Message([MessageSegment.markdown(\"test2\")])\n```\n\n### 拼接消息\n\n`str`、`Message`、`MessageSegment` 对象之间可以直接相加，相加均会返回一个新的 `Message` 对象。\n\n```python\n# 消息序列与消息段相加\nMessage([MessageSegment.text(\"text\")]) + MessageSegment.text(\"text\")\n# 消息序列与字符串相加\nMessage([MessageSegment.text(\"text\")]) + \"text\"\n# 消息序列与消息序列相加\nMessage([MessageSegment.text(\"text\")]) + Message([MessageSegment.text(\"text\")])\n# 字符串与消息序列相加\n\"text\" + Message([MessageSegment.text(\"text\")])\n# 消息段与消息段相加\nMessageSegment.text(\"text\") + MessageSegment.text(\"text\")\n# 消息段与字符串相加\nMessageSegment.text(\"text\") + \"text\"\n# 消息段与消息序列相加\nMessageSegment.text(\"text\") + Message([MessageSegment.text(\"text\")])\n# 字符串与消息段相加\n\"text\" + MessageSegment.text(\"text\")\n```\n\n如果需要在当前消息序列后直接拼接新的消息段，可以使用 `Message.append`、`Message.extend` 方法，或者使用自加。\n\n```python\nmsg = Message([MessageSegment.text(\"text\")])\n# 自加\nmsg += \"text\"\nmsg += MessageSegment.text(\"text\")\nmsg += Message([MessageSegment.text(\"text\")])\n# 附加\nmsg.append(\"text\")\nmsg.append(MessageSegment.text(\"text\"))\n# 扩展\nmsg.extend([MessageSegment.text(\"text\")])\n```\n\n我们也可以通过消息段或消息序列的 `join` 方法来拼接一串消息：\n\n```python\nseg = MessageSegment.text(\"text\")\nmsg = seg.join(\n    [\n        MessageSegment.text(\"first\"),\n        Message(\n            [\n                MessageSegment.text(\"second\"),\n                MessageSegment.text(\"third\"),\n            ]\n        )\n    ]\n)\nmsg == Message(\n    [\n        MessageSegment.text(\"first\"),\n        MessageSegment.text(\"text\"),\n        MessageSegment.text(\"second\"),\n        MessageSegment.text(\"third\"),\n    ]\n)\n```\n\n### 使用消息模板\n\n为了提供安全可靠的跨平台模板字符，我们提供了一个消息模板功能来构建消息序列\n\n它在以下常见场景中尤其有用：\n\n- 多行富文本编排（包含图片，文字以及表情等）\n- 客制化（由 Bot 最终用户提供消息模板时）\n\n在事实上，它的用法和 `str.format` 极为相近，所以你在使用的时候，总是可以参考[Python 文档](https://docs.python.org/zh-cn/3/library/stdtypes.html#str.format)来达到你想要的效果，这里给出几个简单的例子。\n\n默认情况下，消息模板采用 `str` 纯文本形式的格式化：\n\n```python title=基础格式化用法\n>>> from nonebot.adapters import MessageTemplate\n>>> MessageTemplate(\"{} {}\").format(\"hello\", \"world\")\n'hello world'\n```\n\n如果 `Message.template` 构建消息模板，那么消息模板将采用消息序列形式的格式化，此时的消息将会是平台特定的：\n\n:::caution 注意\n使用 `Message.template` 构建消息模板时，应注意消息序列为平台适配器提供的类型，不能使用 `nonebot.adapters.Message` 基类作为模板构建。使用基类构建模板与使用 `str` 构建模板的效果是一样的，因此请使用上述的 `MessageTemplate` 类直接构建模板。：\n:::\n\n```python title=平台格式化用法\n>>> from nonebot.adapters.console import Message, MessageSegment\n>>> Message.template(\"{} {}\").format(\"hello\", \"world\")\nMessage(\n    MessageSegment.text(\"hello\"),\n    MessageSegment.text(\" \"),\n    MessageSegment.text(\"world\")\n)\n```\n\n消息模板支持使用消息段进行格式化：\n\n```python title=对消息段进行安全的拼接\n>>> from nonebot.adapters.console import Message, MessageSegment\n>>> Message.template(\"{}{}\").format(MessageSegment.markdown(\"**markup**\"), \"world\")\nMessage(\n    MessageSegment(type='markdown', data={'markup': '**markup**'}),\n    MessageSegment(type='text', data={'text': 'world'})\n)\n```\n\n消息模板同样支持使用消息序列作为模板：\n\n```python title=以消息对象作为模板\n>>> from nonebot.adapters.console import Message, MessageSegment\n>>> Message.template(\n...     MessageSegment.text(\"{user_id}\") + MessageSegment.emoji(\"tada\") +\n...     MessageSegment.text(\"{message}\")\n... ).format_map({\"user_id\": 123456, \"message\": \"hello world\"})\nMessage(\n    MessageSegment(type='text', data={'text': '123456'}),\n    MessageSegment(type='emoji', data={'emoji': 'tada'}),\n    MessageSegment(type='text', data={'text': 'hello world'})\n)\n```\n\n:::caution 注意\n只有消息序列中的文本类型消息段才能被格式化，其他类型的消息段将会原样添加。\n:::\n\n消息模板支持使用拓展控制符来控制消息段类型：\n\n```python title=使用消息段的拓展控制符\n>>> from nonebot.adapters.console import Message, MessageSegment\n>>> Message.template(\"{name:emoji}\").format(name='tada')\nMessage(MessageSegment(type='emoji', data={'name': 'tada'}))\n```\n"
  },
  {
    "path": "website/versioned_docs/version-2.4.4/tutorial/store.mdx",
    "content": "---\nsidebar_position: 2\ndescription: 从商店安装适配器和插件\n\noptions:\n  menu:\n    - category: tutorial\n      weight: 40\n---\n\n# 获取商店内容\n\nimport Tabs from \"@theme/Tabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Asciinema from \"@site/src/components/Asciinema\";\n\n:::tip 提示\n\n如果你暂时没有获取商店内容的需求，可以跳过本章节。\n\n:::\n\nNoneBot 提供了一个[商店](/store/plugins)，商店内容均由社区开发者贡献。你可以在商店中查找你需要的适配器和插件等，进行安装或者参考其文档等。\n\n商店中每个内容的卡片都包含了其名称和简介等信息，点击**卡片右上角**链接图标即可跳转到其主页。\n\n与此同时，NB-CLI 也提供了一个 TUI 版本的商店界面，可通过 `nb adapter store`、`nb plugin store`、`nb driver store` 命令或 CLI\n交互式界面进入。其提供了接近网页商店的体验，同时允许快捷安装到当前项目。\n\n## 安装插件\n\n<Asciinema\n  url=\"https://asciinema.org/a/569650.cast\"\n  options={{ theme: \"monokai\", poster: \"npt:16.8\" }}\n/>\n\n在商店插件页面中，点击你需要安装的插件下方的 `点击复制安装命令` 按钮，即可复制 `nb-cli` 命令。\n\n请在你的**项目目录**下执行该命令。`nb-cli` 会自动安装插件并将其添加到加载列表中。\n\n<Tabs groupId=\"cli-install\">\n  <TabItem value=\"cli\" label=\"使用命令\" default>\n\n```bash\nnb plugin install <插件名称>\n```\n\n  </TabItem>\n  <TabItem value=\"interactive\" label=\"交互式安装\">\n\n```bash\n$ nb plugin install\n[?] 想要安装的插件名称: <插件名称>\n```\n\n  </TabItem>\n  <TabItem value=\"pip\" label=\"使用 pip\">\n\n```bash\npip install <插件包名>\n```\n\n插件包名可以在商店插件卡片中找到，或者使用 `nb-cli` 搜索插件显示的详情中找到。安装完成后，需要参考[加载插件章节](./create-plugin.md#加载插件)自行加载。\n\n  </TabItem>\n</Tabs>\n\n如果想要查看插件列表，可以使用以下命令\n\n```bash\n# 列出商店所有插件\nnb plugin list\n# 搜索商店插件\nnb plugin search [可选关键词]\n```\n\n升级和卸载插件可以使用以下命令\n\n<Tabs groupId=\"cli-install\">\n  <TabItem value=\"cli\" label=\"使用命令\" default>\n\n```bash\nnb plugin update <插件名称>\nnb plugin uninstall <插件名称>\n```\n\n  </TabItem>\n  <TabItem value=\"interactive\" label=\"交互式安装\">\n\n```bash\n$ nb plugin update\n[?] 想要安装的插件名称: <插件名称>\n$ nb plugin uninstall\n[?] 想要卸载的插件名称: <插件名称>\n```\n\n  </TabItem>\n  <TabItem value=\"pip\" label=\"使用 pip\">\n\n```bash\npip install --upgrade <插件包名>\npip uninstall <插件包名>\n```\n\n插件包名可以在商店插件卡片中找到，或者使用 `nb-cli` 搜索插件显示的详情中找到。卸载完成后，需要自行移除插件加载。\n\n  </TabItem>\n</Tabs>\n\n## 安装适配器\n\n<Asciinema\n  url=\"https://asciinema.org/a/569664.cast\"\n  options={{ theme: \"monokai\", poster: \"npt:12.0\" }}\n/>\n\n安装适配器与安装插件类似，只是将命令换为 `nb adapter`，这里就不再赘述。\n\n请在你的**项目目录**下执行该命令。`nb-cli` 会自动安装适配器并将其添加到注册列表中。\n\n<Tabs groupId=\"cli-install\">\n  <TabItem value=\"cli\" label=\"使用命令\" default>\n\n```bash\nnb adapter install <适配器名称>\n```\n\n  </TabItem>\n  <TabItem value=\"interactive\" label=\"交互式安装\">\n\n```bash\n$ nb adapter install\n[?] 想要安装的适配器名称: <适配器名称>\n```\n\n  </TabItem>\n  <TabItem value=\"pip\" label=\"使用 pip\">\n\n```bash\npip install <适配器包名>\n```\n\n适配器包名可以在商店适配器卡片中找到，或者使用 `nb-cli` 搜索适配器显示的详情中找到。安装完成后，需要参考[注册适配器章节](../advanced/adapter.md#注册适配器)自行注册。\n\n  </TabItem>\n</Tabs>\n\n如果想要查看适配器列表，可以使用以下命令\n\n```bash\n# 列出商店所有适配器\nnb adapter list\n# 搜索商店适配器\nnb adapter search [可选关键词]\n```\n\n升级和卸载适配器可以使用以下命令\n\n<Tabs groupId=\"cli-install\">\n  <TabItem value=\"cli\" label=\"使用命令\" default>\n\n```bash\nnb adapter update <适配器名称>\nnb adapter uninstall <适配器名称>\n```\n\n  </TabItem>\n  <TabItem value=\"interactive\" label=\"交互式安装\">\n\n```bash\n$ nb adapter update\n[?] 想要安装的适配器名称: <适配器名称>\n$ nb adapter uninstall\n[?] 想要卸载的适配器名称: <适配器名称>\n```\n\n  </TabItem>\n  <TabItem value=\"pip\" label=\"使用 pip\">\n\n```bash\npip install --upgrade <适配器包名>\npip uninstall <适配器包名>\n```\n\n适配器包名可以在商店适配器卡片中找到，或者使用 `nb-cli` 搜索适配器显示的详情中找到。卸载完成后，需要自行移除适配器加载。\n\n  </TabItem>\n</Tabs>\n\n## 安装驱动器\n\n<Asciinema\n  url=\"https://asciinema.org/a/569665.cast\"\n  options={{ theme: \"monokai\", poster: \"npt:14.0\" }}\n/>\n\n安装驱动器与安装插件同样类似，只是将命令换为 `nb driver`，这里就不再赘述。\n\n如果你使用了虚拟环境，请在你的**项目目录**下执行该命令，`nb-cli` 会自动安装驱动器到虚拟环境中。\n\n请注意 `nb-cli` 并不会在安装驱动器后修改项目所使用的驱动器，请自行参考[配置方法](../appendices/config.mdx)章节以及 [`DRIVER` 配置项](../appendices/config.mdx#driver)修改驱动器。\n\n<Tabs groupId=\"cli-install\">\n  <TabItem value=\"cli\" label=\"使用命令\" default>\n\n```bash\nnb driver install <驱动器名称>\n```\n\n  </TabItem>\n  <TabItem value=\"interactive\" label=\"交互式安装\">\n\n```bash\n$ nb driver install\n[?] 想要安装的驱动器名称: <驱动器名称>\n```\n\n  </TabItem>\n  <TabItem value=\"pip\" label=\"使用 pip\">\n\n```bash\npip install <驱动器包名>\n```\n\n驱动器包名可以在商店驱动器卡片中找到，或者使用 `nb-cli` 搜索驱动器显示的详情中找到。\n\n  </TabItem>\n</Tabs>\n\n如果想要查看驱动器列表，可以使用以下命令\n\n```bash\n# 列出商店所有驱动器\nnb driver list\n# 搜索商店驱动器\nnb driver search [可选关键词]\n```\n\n升级和卸载驱动器可以使用以下命令\n\n<Tabs groupId=\"cli-install\">\n  <TabItem value=\"cli\" label=\"使用命令\" default>\n\n```bash\nnb driver update <驱动器名称>\nnb driver uninstall <驱动器名称>\n```\n\n  </TabItem>\n  <TabItem value=\"interactive\" label=\"交互式安装\">\n\n```bash\n$ nb driver update\n[?] 想要安装的驱动器名称: <驱动器名称>\n$ nb driver uninstall\n[?] 想要卸载的驱动器名称: <驱动器名称>\n```\n\n  </TabItem>\n  <TabItem value=\"pip\" label=\"使用 pip\">\n\n```bash\npip install --upgrade <驱动器包名>\npip uninstall <驱动器包名>\n```\n\n驱动器包名可以在商店驱动器卡片中找到，或者使用 `nb-cli` 搜索驱动器显示的详情中找到。卸载完成后，需要自行移除适配器加载。\n\n  </TabItem>\n</Tabs>\n"
  },
  {
    "path": "website/versioned_sidebars/version-2.4.2-sidebars.json",
    "content": "{\n  \"tutorial\": [\n    {\n      \"type\": \"category\",\n      \"label\": \"开始\",\n      \"collapsible\": false,\n      \"items\": [\"index\", \"quick-start\", \"editor-support\"]\n    },\n    {\n      \"type\": \"category\",\n      \"label\": \"指南\",\n      \"items\": [\n        {\n          \"type\": \"autogenerated\",\n          \"dirName\": \"tutorial\"\n        }\n      ]\n    },\n    {\n      \"type\": \"category\",\n      \"label\": \"深入\",\n      \"items\": [\n        {\n          \"type\": \"autogenerated\",\n          \"dirName\": \"appendices\"\n        }\n      ]\n    },\n    {\n      \"type\": \"category\",\n      \"label\": \"进阶\",\n      \"items\": [\n        {\n          \"type\": \"autogenerated\",\n          \"dirName\": \"advanced\"\n        }\n      ]\n    },\n    {\n      \"type\": \"category\",\n      \"label\": \"最佳实践\",\n      \"items\": [\n        {\n          \"type\": \"autogenerated\",\n          \"dirName\": \"best-practice\"\n        }\n      ]\n    },\n    {\n      \"type\": \"category\",\n      \"label\": \"开发者\",\n      \"items\": [\n        {\n          \"type\": \"autogenerated\",\n          \"dirName\": \"developer\"\n        }\n      ]\n    }\n  ],\n  \"api\": [\n    {\n      \"type\": \"autogenerated\",\n      \"dirName\": \"api\"\n    }\n  ],\n  \"ecosystem\": [\n    {\n      \"type\": \"category\",\n      \"label\": \"关于我们\",\n      \"collapsible\": false,\n      \"items\": [\n        {\n          \"type\": \"autogenerated\",\n          \"dirName\": \"community\"\n        }\n      ]\n    },\n    {\n      \"type\": \"category\",\n      \"label\": \"开源之夏\",\n      \"collapsible\": true,\n      \"items\": [\n        {\n          \"type\": \"autogenerated\",\n          \"dirName\": \"ospp\"\n        }\n      ]\n    },\n    {\n      \"type\": \"category\",\n      \"label\": \"社区资源\",\n      \"collapsible\": false,\n      \"items\": [\n        {\n          \"type\": \"link\",\n          \"label\": \"插件商店\",\n          \"href\": \"/store/plugins\"\n        },\n        {\n          \"type\": \"link\",\n          \"label\": \"适配器商店\",\n          \"href\": \"/store/adapters\"\n        },\n        {\n          \"type\": \"link\",\n          \"label\": \"驱动器商店\",\n          \"href\": \"/store/drivers\"\n        },\n        {\n          \"type\": \"link\",\n          \"label\": \"机器人商店\",\n          \"href\": \"/store/bots\"\n        },\n        {\n          \"type\": \"link\",\n          \"label\": \"Awesome NoneBot\",\n          \"href\": \"https://awesome.nonebot.dev\"\n        },\n        {\n          \"type\": \"link\",\n          \"label\": \"论坛\",\n          \"href\": \"https://discussions.nonebot.dev\"\n        }\n      ]\n    }\n  ],\n  \"changelog\": [\n    {\n      \"type\": \"category\",\n      \"label\": \"更新日志\",\n      \"collapsible\": false,\n      \"items\": [\n        {\n          \"type\": \"link\",\n          \"label\": \"v2.4.2\",\n          \"href\": \"/changelog/\"\n        },\n        {\n          \"type\": \"link\",\n          \"label\": \"v2.1.2\",\n          \"href\": \"/changelog/1\"\n        },\n        {\n          \"type\": \"link\",\n          \"label\": \"v2.0.0-beta.4\",\n          \"href\": \"/changelog/2\"\n        },\n        {\n          \"type\": \"link\",\n          \"label\": \"v2.0.0a9\",\n          \"href\": \"/changelog/3\"\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "website/versioned_sidebars/version-2.4.3-sidebars.json",
    "content": "{\n  \"tutorial\": [\n    {\n      \"type\": \"category\",\n      \"label\": \"开始\",\n      \"collapsible\": false,\n      \"items\": [\"index\", \"quick-start\", \"editor-support\"]\n    },\n    {\n      \"type\": \"category\",\n      \"label\": \"指南\",\n      \"items\": [\n        {\n          \"type\": \"autogenerated\",\n          \"dirName\": \"tutorial\"\n        }\n      ]\n    },\n    {\n      \"type\": \"category\",\n      \"label\": \"深入\",\n      \"items\": [\n        {\n          \"type\": \"autogenerated\",\n          \"dirName\": \"appendices\"\n        }\n      ]\n    },\n    {\n      \"type\": \"category\",\n      \"label\": \"进阶\",\n      \"items\": [\n        {\n          \"type\": \"autogenerated\",\n          \"dirName\": \"advanced\"\n        }\n      ]\n    },\n    {\n      \"type\": \"category\",\n      \"label\": \"最佳实践\",\n      \"items\": [\n        {\n          \"type\": \"autogenerated\",\n          \"dirName\": \"best-practice\"\n        }\n      ]\n    },\n    {\n      \"type\": \"category\",\n      \"label\": \"开发者\",\n      \"items\": [\n        {\n          \"type\": \"autogenerated\",\n          \"dirName\": \"developer\"\n        }\n      ]\n    }\n  ],\n  \"api\": [\n    {\n      \"type\": \"autogenerated\",\n      \"dirName\": \"api\"\n    }\n  ],\n  \"ecosystem\": [\n    {\n      \"type\": \"category\",\n      \"label\": \"关于我们\",\n      \"collapsible\": false,\n      \"items\": [\n        {\n          \"type\": \"autogenerated\",\n          \"dirName\": \"community\"\n        }\n      ]\n    },\n    {\n      \"type\": \"category\",\n      \"label\": \"开源之夏\",\n      \"collapsible\": true,\n      \"items\": [\n        {\n          \"type\": \"autogenerated\",\n          \"dirName\": \"ospp\"\n        }\n      ]\n    },\n    {\n      \"type\": \"category\",\n      \"label\": \"社区资源\",\n      \"collapsible\": false,\n      \"items\": [\n        {\n          \"type\": \"link\",\n          \"label\": \"插件商店\",\n          \"href\": \"/store/plugins\"\n        },\n        {\n          \"type\": \"link\",\n          \"label\": \"适配器商店\",\n          \"href\": \"/store/adapters\"\n        },\n        {\n          \"type\": \"link\",\n          \"label\": \"驱动器商店\",\n          \"href\": \"/store/drivers\"\n        },\n        {\n          \"type\": \"link\",\n          \"label\": \"机器人商店\",\n          \"href\": \"/store/bots\"\n        },\n        {\n          \"type\": \"link\",\n          \"label\": \"Awesome NoneBot\",\n          \"href\": \"https://awesome.nonebot.dev\"\n        },\n        {\n          \"type\": \"link\",\n          \"label\": \"论坛\",\n          \"href\": \"https://discussions.nonebot.dev\"\n        }\n      ]\n    }\n  ],\n  \"changelog\": [\n    {\n      \"type\": \"category\",\n      \"label\": \"更新日志\",\n      \"collapsible\": false,\n      \"items\": [\n        {\n          \"type\": \"link\",\n          \"label\": \"v2.4.3\",\n          \"href\": \"/changelog/\"\n        },\n        {\n          \"type\": \"link\",\n          \"label\": \"v2.1.3\",\n          \"href\": \"/changelog/1\"\n        },\n        {\n          \"type\": \"link\",\n          \"label\": \"v2.0.0-beta.5\",\n          \"href\": \"/changelog/2\"\n        },\n        {\n          \"type\": \"link\",\n          \"label\": \"v2.0.0a10\",\n          \"href\": \"/changelog/3\"\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "website/versioned_sidebars/version-2.4.4-sidebars.json",
    "content": "{\n  \"tutorial\": [\n    {\n      \"type\": \"category\",\n      \"label\": \"开始\",\n      \"collapsible\": false,\n      \"items\": [\"index\", \"quick-start\", \"editor-support\"]\n    },\n    {\n      \"type\": \"category\",\n      \"label\": \"指南\",\n      \"items\": [\n        {\n          \"type\": \"autogenerated\",\n          \"dirName\": \"tutorial\"\n        }\n      ]\n    },\n    {\n      \"type\": \"category\",\n      \"label\": \"深入\",\n      \"items\": [\n        {\n          \"type\": \"autogenerated\",\n          \"dirName\": \"appendices\"\n        }\n      ]\n    },\n    {\n      \"type\": \"category\",\n      \"label\": \"进阶\",\n      \"items\": [\n        {\n          \"type\": \"autogenerated\",\n          \"dirName\": \"advanced\"\n        }\n      ]\n    },\n    {\n      \"type\": \"category\",\n      \"label\": \"最佳实践\",\n      \"items\": [\n        {\n          \"type\": \"autogenerated\",\n          \"dirName\": \"best-practice\"\n        }\n      ]\n    },\n    {\n      \"type\": \"category\",\n      \"label\": \"开发者\",\n      \"items\": [\n        {\n          \"type\": \"autogenerated\",\n          \"dirName\": \"developer\"\n        }\n      ]\n    }\n  ],\n  \"api\": [\n    {\n      \"type\": \"autogenerated\",\n      \"dirName\": \"api\"\n    }\n  ],\n  \"ecosystem\": [\n    {\n      \"type\": \"category\",\n      \"label\": \"关于我们\",\n      \"collapsible\": false,\n      \"items\": [\n        {\n          \"type\": \"autogenerated\",\n          \"dirName\": \"community\"\n        }\n      ]\n    },\n    {\n      \"type\": \"category\",\n      \"label\": \"开源之夏\",\n      \"collapsible\": true,\n      \"items\": [\n        {\n          \"type\": \"autogenerated\",\n          \"dirName\": \"ospp\"\n        }\n      ]\n    },\n    {\n      \"type\": \"category\",\n      \"label\": \"社区资源\",\n      \"collapsible\": false,\n      \"items\": [\n        {\n          \"type\": \"link\",\n          \"label\": \"插件商店\",\n          \"href\": \"/store/plugins\"\n        },\n        {\n          \"type\": \"link\",\n          \"label\": \"适配器商店\",\n          \"href\": \"/store/adapters\"\n        },\n        {\n          \"type\": \"link\",\n          \"label\": \"驱动器商店\",\n          \"href\": \"/store/drivers\"\n        },\n        {\n          \"type\": \"link\",\n          \"label\": \"机器人商店\",\n          \"href\": \"/store/bots\"\n        },\n        {\n          \"type\": \"link\",\n          \"label\": \"Awesome NoneBot\",\n          \"href\": \"https://awesome.nonebot.dev\"\n        },\n        {\n          \"type\": \"link\",\n          \"label\": \"论坛\",\n          \"href\": \"https://discussions.nonebot.dev\"\n        }\n      ]\n    }\n  ],\n  \"changelog\": [\n    {\n      \"type\": \"category\",\n      \"label\": \"更新日志\",\n      \"collapsible\": false,\n      \"items\": [\n        {\n          \"type\": \"link\",\n          \"label\": \"v2.4.4\",\n          \"href\": \"/changelog/\"\n        },\n        {\n          \"type\": \"link\",\n          \"label\": \"v2.2.0\",\n          \"href\": \"/changelog/1\"\n        },\n        {\n          \"type\": \"link\",\n          \"label\": \"v2.0.0-rc.1\",\n          \"href\": \"/changelog/2\"\n        },\n        {\n          \"type\": \"link\",\n          \"label\": \"v2.0.0a11\",\n          \"href\": \"/changelog/3\"\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "website/versions.json",
    "content": "[\"2.4.4\", \"2.4.3\", \"2.4.2\"]\n"
  }
]