<!DOCTYPE html><html lang="zh" data-theme="dark"><head><meta charset="utf-8"><meta name="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Nonebot beta1 教程 · Cytrogen 的个人博客</title><meta name="description" content="本文是一篇面向新手的 NoneBot2 (beta1 版本) QQ 机器人开发入门教程。教程从零开始,详细讲解了从 Python 环境、nb-cli 脚手架的安装,到 go-cqhttp 协议端的配置,再到创建并运行一个基础机器人的完整流程。教程的核心部分深入介绍了 NoneBot2 的插件系统,包括如何创建、安装和使用插件,并以一个“hi-喵”的实例清晰地解释了事件响应器(Matcher)和处理器(Handler)的工作原理。本教程为初学者提供了一条清晰的学习路径,帮助你快速搭建并开始定制自己的 QQ 机器人。"><link rel="icon" href="../favicon.png"><link rel="canonical" href="https://cytrogen.icu/posts/41a5.html"><link rel="webmention" href="https://webmention.io/cytrogen.icu/webmention"><link rel="me" href="https://m.otter.homes/@Cytrogen"><link rel="me" href="https://github.com/cytrogen"><meta name="fediverse:creator" content="@Cytrogen@m.otter.homes"><link rel="preload" href="../fonts/opensans-regular-latin.woff2" as="font" type="font/woff2" crossorigin="anonymous"><style>@font-face {
font-family: 'Open Sans';
src: url('../fonts/opensans-regular-latin.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
size-adjust: 107%;
ascent-override: 97%;
descent-override: 25%;
line-gap-override: 0%;
}
</style><script>(function() {
try {
// 优先级:用户选择 > 系统偏好 > 默认浅色
const saved = localStorage.getItem('theme');
const theme = saved ||
(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', theme);
document.documentElement.style.colorScheme = theme;
} catch (error) {
// 失败时使用默认主题,不阻塞渲染
document.documentElement.setAttribute('data-theme', 'light');
}
})();
</script><link rel="stylesheet" href="../css/ares.css"><script data-netlify-skip-bundle="true">(function() {
document.addEventListener('DOMContentLoaded', function() {
const theme = document.documentElement.getAttribute('data-theme');
const pageWrapper = document.getElementById('page-wrapper');
if (pageWrapper && theme) {
pageWrapper.setAttribute('data-theme', theme);
}
});
})();
</script><!-- hexo injector head_end start -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/hexo-math@4.0.0/dist/style.css">
<!-- hexo injector head_end end --><meta name="generator" content="Hexo 8.1.1"><link rel="alternate" href="atom.xml" title="Cytrogen 的个人博客" type="application/atom+xml">
</head><body><div id="page-wrapper"><a class="skip-link" href="#main-content">跳到主要内容</a><div class="wrap"><header><a class="logo-link" href="../index.html"><img src="../favicon.png" alt="logo"></a><div class="h-card visually-hidden"><img class="u-photo" src="https://cytrogen.icu/favicon.png" alt="Cytrogen"><a class="p-name u-url u-uid" href="https://cytrogen.icu">Cytrogen</a><p class="p-note">Cytrogen 的个人博客,Cytrogen's Blog</p><a class="u-url" rel="me noopener" target="_blank" href="https://m.otter.homes/@Cytrogen">Mastodon</a><a class="u-url" rel="me noopener" target="_blank" href="https://github.com/cytrogen">GitHub</a></div><nav class="site-nav"><div class="nav-main"><div class="nav-primary"><ul class="nav-list hidden-mobile"><li class="nav-item"><a class="nav-link" href="../index.html">首页</a></li></ul><div class="nav-tools"><div class="language-menu"><button class="language-toggle" type="button"><svg class="icon icon-globe" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" focusable="false"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm7.5-6.923c-.67.204-1.335.82-1.887 1.855A7.97 7.97 0 0 0 5.145 4H7.5V1.077zM4.09 4a9.267 9.267 0 0 1 .64-1.539 6.7 6.7 0 0 1 .597-.933A7.025 7.025 0 0 0 2.255 4H4.09zm-.582 3.5c.03-.877.138-1.718.312-2.5H1.674a6.958 6.958 0 0 0-.656 2.5h2.49zM4.847 5a12.5 12.5 0 0 0-.338 2.5H7.5V5H4.847zM8.5 5v2.5h2.99a12.495 12.495 0 0 0-.337-2.5H8.5zM4.51 8.5a12.5 12.5 0 0 0 .337 2.5H7.5V8.5H4.51zm3.99 0V11h2.653c.187-.765.306-1.608.338-2.5H8.5zM5.145 12c.138.386.295.744.468 1.068.552 1.035 1.218 1.65 1.887 1.855V12H5.145zm.182 2.472a6.696 6.696 0 0 1-.597-.933A9.268 9.268 0 0 1 4.09 12H2.255a7.024 7.024 0 0 0 3.072 2.472zM3.82 11a13.652 13.652 0 0 1-.312-2.5h-2.49c.062.89.291 1.733.656 2.5H3.82zm6.853 3.472A7.024 7.024 0 0 0 13.745 12H11.91a9.27 9.27 0 0 1-.64 1.539 6.688 6.688 0 0 1-.597.933zM8.5 12v2.923c.67-.204 1.335-.82 1.887-1.855A7.97 7.97 0 0 0 10.855 12H8.5zm3.68-1h2.146c.365-.767.594-1.61.656-2.5h-2.49a13.65 13.65 0 0 1-.312 2.5zm2.802-3.5a6.959 6.959 0 0 0-.656-2.5H12.18c.174.782.282 1.623.312 2.5h2.49zM11.27 2.461c.247.464.462.98.64 1.539h1.835a7.024 7.024 0 0 0-3.072-2.472c.218.284.418.598.597.933zM10.855 4a7.966 7.966 0 0 0-.468-1.068C9.835 1.897 9.17 1.282 8.5 1.077V4h2.355z"></path></svg><span>中文</span></button><div class="language-dropdown"></div></div></div><div class="nav-controls"><div class="more-menu hidden-mobile"><button class="more-toggle" type="button"><span>更多</span><svg class="icon icon-chevron-down" width="12" height="12" viewBox="0 0 12 12" fill="currentColor" aria-hidden="true" focusable="false"><path d="M6 8.825c-.2 0-.4-.1-.5-.2l-3.3-3.3c-.3-.3-.3-.8 0-1.1s.8-.3 1.1 0l2.7 2.7 2.7-2.7c.3-.3.8-.3 1.1 0s.3.8 0 1.1l-3.3 3.3c-.1.1-.3.2-.5.2z"></path></svg></button><div class="more-dropdown"><ul class="dropdown-list"><li class="dropdown-item"><a class="nav-link" href="../archives/index.html">归档</a></li><li class="dropdown-item"><a class="nav-link" href="../categories/index.html">分类</a></li><li class="dropdown-item"><a class="nav-link" href="../tags/index.html">标签</a></li><li class="dropdown-item"><a class="nav-link" href="../about/index.html">关于</a></li><li class="dropdown-item"><a class="nav-link" href="../sitemap/index.html">领地地图</a></li></ul></div></div><div class="theme-switcher"><button class="theme-toggle" type="button" role="switch" aria-pressed="false" aria-label="切换主题"><div class="theme-icon moon-icon"><svg class="icon icon-moon" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" focusable="false"><path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z"></path></svg></div><div class="theme-icon sun-icon"><svg class="icon icon-sun" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" focusable="false"><path d="M8 11a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 1a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z"></path></svg></div></button></div><details class="mobile-menu-details hidden-desktop"><summary class="hamburger-menu" aria-label="nav.menu"><svg class="icon icon-bars" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" focusable="false"><path d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"></path></svg><span class="menu-text">nav.menu</span></summary><div class="mobile-menu-dropdown"><ul class="mobile-nav-list"><li class="mobile-nav-item"><a class="mobile-nav-link" href="../index.html">首页</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../archives/index.html">归档</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../categories/index.html">分类</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../tags/index.html">标签</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../about/index.html">关于</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../sitemap/index.html">领地地图</a></li></ul></div></details></div></div></div></nav></header><main class="container" id="main-content" tabindex="-1"><div class="post"><article class="post-block h-entry"><div class="post-meta p-author h-card visually-hidden"><img class="author-avatar u-photo" src="../favicon.png" alt="Cytrogen"><span class="p-name">Cytrogen</span><a class="u-url" href="https://cytrogen.icu">https://cytrogen.icu</a></div><a class="post-permalink u-url u-uid visually-hidden" href="https://cytrogen.icu/posts/41a5.html">永久链接</a><div class="p-summary visually-hidden"><p>Nonebot2 在更新到 beta1 版本后许多地方和 alpha16 版本有许多差异,包括但不限于适配器、依赖的更改。</p></div><div class="visually-hidden"><a class="p-category" href="../categories/%E7%BC%96%E7%A8%8B%E7%AC%94%E8%AE%B0/">编程笔记</a><a class="p-category" href="../tags/Python/">Python</a><a class="p-category" href="../tags/Nonebot/">Nonebot</a></div><h1 class="post-title p-name">Nonebot beta1 教程</h1><div class="post-info"><time class="post-date dt-published" datetime="2022-02-02T01:46:02.000Z">2/1/2022</time><time class="dt-updated visually-hidden" datetime="2026-02-09T17:16:54.733Z"></time></div><div class="post-content e-content"><html><head></head><body><p>Nonebot2 在更新到 beta1 版本后许多地方和 alpha16 版本有许多差异,包括但不限于适配器、依赖的更改。</p>
<span id="more"></span>
<p><img src="/posts/41a5/Nonebot.png" alt="Nonebot2"></p>
<center>
Nonebot2 是 Python 聊天机器人框架,目前仅支持 Python 3.7.3 以上的版本
</center>
<div class="danger">
<p>开始本教程前,需要知道该教程:</p>
<ul>
<li>Python 版本 ≥ 3.7</li>
<li>基于 Windows 10 版本(Win11 也可以但我只能说快逃)</li>
<li>使用了 pip 用于安装</li>
<li>使用了脚手架 nb-cli</li>
<li>使用了 onebot.v11 适配器</li>
</ul>
</div>
<h1 id="创建项目之前"><a class="markdownIt-Anchor" href="#创建项目之前"></a> 创建项目之前</h1>
<p>在创建机器人项目前需要安装:</p>
<ul>
<li><a href="#%E5%88%9B%E5%BB%BA%E9%A1%B9%E7%9B%AE%E4%B9%8B%E5%89%8D">创建项目之前</a>
<ul>
<li><a href="#%E4%B8%8B%E8%BD%BDpython">下载 Python</a></li>
<li><a href="#%E5%AE%89%E8%A3%85pip">安装 pip</a></li>
<li><a href="#%E5%AE%89%E8%A3%85nb-cli">安装 nb-cli</a></li>
<li><a href="#%E4%B8%8B%E8%BD%BDgo-cqhttp">下载 go-cqhttp</a></li>
</ul>
</li>
<li><a href="#%E5%88%9B%E5%BB%BA%E9%A1%B9%E7%9B%AE">创建项目</a>
<ul>
<li><a href="#%E6%96%B0%E5%BB%BA%E9%A1%B9%E7%9B%AE">新建项目</a></li>
<li><a href="#%E9%85%8D%E7%BD%AE%E9%A1%B9%E7%9B%AE">配置项目</a></li>
<li><a href="#%E7%94%A8%E4%B8%8B%E6%9C%BA%E5%99%A8%E4%BA%BA">用下机器人</a></li>
</ul>
</li>
<li><a href="#%E6%B7%B1%E5%85%A5%E6%8F%92%E4%BB%B6">深入插件</a>
<ul>
<li><a href="#%E4%BB%80%E4%B9%88%E6%98%AF%E6%8F%92%E4%BB%B6">什么是插件</a></li>
<li><a href="#%E5%88%9B%E5%BB%BA%E6%8F%92%E4%BB%B6">创建插件</a></li>
<li><a href="#%E7%94%A8%E5%88%AB%E4%BA%BA%E7%9A%84%E6%8F%92%E4%BB%B6">用别人的插件</a></li>
<li><a href="#%E4%BA%8B%E4%BB%B6%E5%93%8D%E5%BA%94%E5%99%A8">事件响应器</a></li>
<li><a href="#%E5%A4%84%E7%90%86%E6%B5%81%E7%A8%8B">处理流程</a></li>
<li><a href="#%E6%8F%92%E4%BB%B6%E5%AE%9E%E4%BE%8B">插件实例</a></li>
</ul>
</li>
</ul>
<p>而这些都会讲到,已经安装过了的建议 <a href="#%E5%88%9B%E5%BB%BA%E9%A1%B9%E7%9B%AE">空降</a>。</p>
<center>————————</center>
<h2 id="下载python"><a class="markdownIt-Anchor" href="#下载python"></a> 下载 Python</h2>
<ol>
<li>
<p>进入 <a target="_blank" rel="noopener" href="https://www.python.org/downloads/windows/">Python 官网</a>。</p>
</li>
<li>
<p>选择 3.7.3 版本以上的进行下载。</p>
<p><img src="/posts/41a5/python.png" alt="python 3.9.9"></p>
</li>
<li>
<p>我的 Python 版本是 3.9.9,选择上图中的 <strong>Windows installer (64-bit)</strong> 进行下载。</p>
</li>
<li>
<p>下载完成后点进 <strong>python-3.9.9-amd64.exe</strong>,勾选 <strong>Add Python 3.9 to PATH</strong> 后点击 <strong>Install Now</strong>。<br>
当然,你也可以自定义路径,但一定要勾选 PATH。</p>
<p><img src="/posts/41a5/python_version.png" alt="python --version"></p>
</li>
<li>
<p>等待下载……</p>
<p>下载完毕后开个 CMD 输入 <code>python --version</code>,出现 <code>Python 3.9.9</code> 就代表能用了。</p>
</li>
</ol>
<center>————————</center>
<h2 id="安装pip"><a class="markdownIt-Anchor" href="#安装pip"></a> 安装 pip</h2>
<p>pip 是 Python 包管理工具,用来下载 Python 的包。</p>
<ol>
<li>
<p>进入 <a target="_blank" rel="noopener" href="https://pypi.org/project/pip/">PyPI</a>,选择最新版本的 pip。</p>
<p><img src="/posts/41a5/pip.png" alt="pip"></p>
</li>
<li>
<p>选择 Download Files,下载 pip-22.0.2.tar.gz 进行解压。</p>
<p><img src="/posts/41a5/pip_com.png" alt="pip解压"></p>
</li>
<li>
<p>解压后进入文件夹内,右键空白处打开 CMD,输入 <code>python setup.py install</code></p>
</li>
<li>
<p>等待安装……<br>
安装完成后开个 CMD 输入 <code>pip --version</code>,出现 <code>pip 22.0.2 from 路径……</code> 时就代表能用了。</p>
</li>
</ol>
<center>————————</center>
<h2 id="安装nb-cli"><a class="markdownIt-Anchor" href="#安装nb-cli"></a> 安装 nb-cli</h2>
<p>Nonebot2 提供了相当便利的工具 nb-cli,安装它也会直接安装 nonebot 2.0.0-beta1。</p>
<ol>
<li>
<p>开个 CMD 输入 <code>pip install nb-cli</code></p>
</li>
<li>
<p>出现 <code>Successfully installed nb-cli……</code> 就代表 pip 安装 nb-cli 成功了。</p>
</li>
</ol>
<center>————————</center>
<h2 id="下载go-cqhttp"><a class="markdownIt-Anchor" href="#下载go-cqhttp"></a> 下载 go-cqhttp</h2>
<p>做机器人只有 Nonebot2 是不足够的,Nonebot2 仅能做到处理机器人上报的事件。<br>
机器人的实际工作流程为:</p>
<ol>
<li><strong>协议端</strong> 上报数据给 <strong>后端驱动</strong>;</li>
<li><strong>后端驱动</strong> 将原始数据转交给 <strong>协议适配</strong> 处理;</li>
<li><strong>协议适配</strong>,或称为 bot,将数据转化为 event 转交给 Nonebot2;</li>
<li>Nonebot2 处理 event。</li>
</ol>
<p>很显然,我们只有处理 event 的 Nonebot2。</p>
<p>获取数据的工作便交到了 go-cqhttp 身上:它相当于你的另一个 QQ,接收到信息后便转交到 Nonebot2 手上等着处理完和发送。</p>
<ol>
<li>
<p>进入 <a target="_blank" rel="noopener" href="https://github.com/Mrs4s/go-cqhttp/releases">go-cqhttp 仓库</a>,下载最新版本的 releases。</p>
</li>
<li>
<p>选择 <strong>go-cqhttp_windows_amd64.exe</strong> 进行下载和备用。</p>
</li>
</ol>
<center>————————</center>
<h1 id="创建项目"><a class="markdownIt-Anchor" href="#创建项目"></a> 创建项目</h1>
<p>现在该下的都下载完了,是时候创建机器人项目了:</p>
<ol>
<li><a href="#%E6%96%B0%E5%BB%BA%E9%A1%B9%E7%9B%AE">新建项目</a></li>
<li><a href="#%E9%85%8D%E7%BD%AE%E9%A1%B9%E7%9B%AE">配置项目</a></li>
<li><a href="#%E7%94%A8%E4%B8%8B%E6%9C%BA%E5%99%A8%E4%BA%BA">用下机器人</a></li>
</ol>
<center> ————————</center>
<h2 id="新建项目"><a class="markdownIt-Anchor" href="#新建项目"></a> 新建项目</h2>
<ol>
<li>
<p>新建一个文件夹,右键打开 CMD,输入 <code>nb create</code>。</p>
</li>
<li>
<p><code>Project Name:</code>,输入你想给你机器人取的名字。</p>
</li>
<li>
<p><code>Where to store the plugin?</code>,这意味着机器人插件的存放路径,建议选择 <code>In a "src" folder</code>。</p>
</li>
<li>
<p><code>Which builtin plugin(s)……巴拉巴拉</code>,这就是在问你你的机器人要不要 Nonebot2 内嵌的插件,建议只选择 <code>echo</code> 插件。</p>
<p><img src="/posts/41a5/echo.png" alt="echo"></p>
<p>需要注意的是,选择内嵌插件时要按下空格再回车继续。</p>
</li>
<li>
<p><code>Which adapter(s)……巴拉巴拉</code>,选择你想要的协议适配,这里要选择 <code>OneBot V11</code>,别忘了空格再回车。</p>
</li>
<li>
<p>期间会进行适配器等的安装,结束后你的文件夹内便会出现机器人名字的文件夹,差不多长这样:</p>
<figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">机器人名字</span><br><span class="line">├── src</span><br><span class="line">│ └── plugins</span><br><span class="line">├── .env</span><br><span class="line">├── .env.dev</span><br><span class="line">├── .env.prod</span><br><span class="line">├── .gitignore</span><br><span class="line">├── bot.py</span><br><span class="line">├── docker-compose.yml</span><br><span class="line">├── Dockerfile</span><br><span class="line">├── pyproject.toml</span><br><span class="line">└── README.md</span><br></pre></td></tr></tbody></table></figure>
</li>
</ol>
<ul>
<li><code>src/plugins</code> 为 Nonebot2 插件存放处,插件则是 Nonebot2 的核心,可以实现模块化、功能扩展等</li>
<li><code>.env.*</code> 等文件是项目的配置文件,内存有变量 <code>ENVIRONMENT</code>,是 Nonebot2 启动时就会寻找的系统环境变量</li>
<li><code>.gitignore</code>,如果不上传到 git 仓库的话可以无视</li>
<li><code>bot.py</code> 是是启动 Nonebot2 时默认的入口文件</li>
<li><code>pyproject.toml</code> 是项目插件配置文件</li>
<li><code>Dockerfile</code> 和 <code>docker-compose.yml</code> 皆为 Docker 镜像配置文件</li>
</ul>
<center>————————</center>
<h2 id="配置项目"><a class="markdownIt-Anchor" href="#配置项目"></a> 配置项目</h2>
<p><code>.env</code> 文件是基础的环境配置文件,不管是什么环境都会被加载,不过还是会被 <code>.env.{环境}</code> 文件所覆盖。</p>
<p>而环境有:</p>
<ul>
<li>开发环境 development 或 dev</li>
<li>生产环境 production 或 prod<br>
等等等等,就不一一列举了。</li>
</ul>
<ol>
<li>
<p>打开 <code>.env</code> 文件写入:<code>ENVIRONMENT=dev</code>。<br>
开发环境将会报告错误日志,这也会将环境变量的加载导向 <code>.env.dev</code> 文件。</p>
</li>
<li>
<p>打开 <code>.env.dev</code> 文件,写入以下配置:</p>
<figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">HOST=127.0.0.1</span><br><span class="line">PORT=xxxx</span><br><span class="line">SUPERUSERS=["xxxxx"]</span><br><span class="line">NICKNAME=["xxx"]</span><br><span class="line">COMMAND_START=["/",""]</span><br><span class="line">COMMAND_SEP=["x"]</span><br></pre></td></tr></tbody></table></figure>
<ul>
<li>其中,<code>HOST</code> 为 Nonebot2 监听的 IP /主机名</li>
<li><code>PORT</code> 为监听的端口,这里可以随意选择一个四位数</li>
<li><code>SUPERUSERS</code> 为超级用户,后面会说作用。值是一个 QQ 号,建议使用大号</li>
<li><code>NICKNAME</code> 为机器人的昵称</li>
<li><code>COMMAND_START</code> 为命令起始字符,后面会说作用,这里先用 "/"</li>
<li><code>COMMAND_SEP</code> 为命令分割字符</li>
</ul>
</li>
<li>
<p>第 2 步完成后对 Nonebot2 的基础配置就完成了,但我们还未配置 go-cqhttp。</p>
<p>再新建一个文件夹,里边存放我们之前下载的 <strong>go-cqhttp_windows_amd64.exe</strong>,同时我建议将该 exe 文件重命名为 <strong>go-cqhttp.exe</strong>。</p>
</li>
<li>
<p>新建一个 <code>config.yml</code> 文件,作为 go-cqhttp 的配置文件。</p>
<ul>
<li>配置信息前往 <a target="_blank" rel="noopener" href="https://docs.go-cqhttp.org/guide/config.html#%E9%85%8D%E7%BD%AE%E4%BF%A1%E6%81%AF">go-cqhttp 文档</a> 内进行复制粘贴</li>
<li><code>servers</code> 下面选择 <code>ws-reverse</code>,如下图</li>
</ul>
<p><img src="/posts/41a5/wsreverse.png" alt="ws-reverse"></p>
</li>
<li>
<p>修改配置信息:</p>
<ul>
<li><code>account</code> 下的 <code>uin</code> 需要填写机器人所用的 QQ 账号</li>
<li><code>password</code> 需要填写 QQ 账号的密码</li>
<li><code>servers</code> 下的 <code>port</code> 需要填写配置 Nonebot2 时填写的四位数 PORT</li>
</ul>
</li>
<li>
<p>启动 go-cqhttp.exe。百分百会弹出生成 .bat 文件的窗口,直接关闭后会发现文件夹内多出了一个 <strong>go-cqhttp.bat</strong>,以后启动 go-cqhttp 都需要用到这个文件。</p>
<p>启动后 go-cqhttp 会进行登录,出现 <code>[INFO]: アトリは、高性能ですから!</code> 时便是登陆成功,要开始连接到 Nonebot2 了。</p>
<p>因为我们 Nonebot2 还未开启,此时的 go-cqhttp 只会一直弹出黄色的连接失败。</p>
</li>
</ol>
<center>————————</center>
<h2 id="用下机器人"><a class="markdownIt-Anchor" href="#用下机器人"></a> 用下机器人</h2>
<p>一切准备成功,机器人已经可以使用了!</p>
<p>使用机器人时,Nonebot2 和 go-cqhttp 都需要 <strong>一直开启</strong>。对于不想长期开着电脑的小伙伴,建议使用云服务器。</p>
<ol>
<li>
<p>打开存放 <strong><a target="_blank" rel="noopener" href="http://bot.py">bot.py</a></strong> 的文件夹,右键开启 CMD,输入 <code>nb run</code>。</p>
</li>
<li>
<p>打开存放 <strong>go-cqhttp.bat</strong> 的文件夹,左键打开。</p>
</li>
<li>
<p>等待第 1 步的 CMD 出现了</p>
<figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">[INFO] nonebot | OneBot V11 | Bot QQ账号 connected</span><br><span class="line">[INFO] websockets | connection open</span><br></pre></td></tr></tbody></table></figure>
<p>便意味着机器人可以使用了。</p>
</li>
<li>
<p>快马加鞭通过大号给机器人账号发送 <code>/echo 喵</code></p>
<p><img src="/posts/41a5/echomeow.png" alt="echo"></p>
<p>这边是因为我的机器人命令起始字符为 "hoka",你也可以修改成自己喜欢的</p>
</li>
<li>
<p>机器人一旦复读了,便真正的代表着,你成功的搭建了一个 QQ 机器人。</p>
<p><s>好了,现在新手教程结束了,来试试这些插件吧.jpg</s></p>
</li>
</ol>
<center>————————</center>
<h1 id="深入插件"><a class="markdownIt-Anchor" href="#深入插件"></a> 深入插件</h1>
<p>咳咳,恭喜你获得了一个刚出生的 QQ 机器人!但它九年义务教育还未完成……<br>
对于一个功能完善的机器人而言,<strong>完善的插件系统</strong> 是不可缺少的。</p>
<p>我们接下来的才是 Nonebot2 的重头戏:<strong>插件讲解</strong>。</p>
<ul>
<li><a href="#%E5%88%9B%E5%BB%BA%E9%A1%B9%E7%9B%AE%E4%B9%8B%E5%89%8D">创建项目之前</a>
<ul>
<li><a href="#%E4%B8%8B%E8%BD%BDpython">下载 Python</a></li>
<li><a href="#%E5%AE%89%E8%A3%85pip">安装 pip</a></li>
<li><a href="#%E5%AE%89%E8%A3%85nb-cli">安装 nb-cli</a></li>
<li><a href="#%E4%B8%8B%E8%BD%BDgo-cqhttp">下载 go-cqhttp</a></li>
</ul>
</li>
<li><a href="#%E5%88%9B%E5%BB%BA%E9%A1%B9%E7%9B%AE">创建项目</a>
<ul>
<li><a href="#%E6%96%B0%E5%BB%BA%E9%A1%B9%E7%9B%AE">新建项目</a></li>
<li><a href="#%E9%85%8D%E7%BD%AE%E9%A1%B9%E7%9B%AE">配置项目</a></li>
<li><a href="#%E7%94%A8%E4%B8%8B%E6%9C%BA%E5%99%A8%E4%BA%BA">用下机器人</a></li>
</ul>
</li>
<li><a href="#%E6%B7%B1%E5%85%A5%E6%8F%92%E4%BB%B6">深入插件</a>
<ul>
<li><a href="#%E4%BB%80%E4%B9%88%E6%98%AF%E6%8F%92%E4%BB%B6">什么是插件</a></li>
<li><a href="#%E5%88%9B%E5%BB%BA%E6%8F%92%E4%BB%B6">创建插件</a></li>
<li><a href="#%E7%94%A8%E5%88%AB%E4%BA%BA%E7%9A%84%E6%8F%92%E4%BB%B6">用别人的插件</a></li>
<li><a href="#%E4%BA%8B%E4%BB%B6%E5%93%8D%E5%BA%94%E5%99%A8">事件响应器</a></li>
<li><a href="#%E5%A4%84%E7%90%86%E6%B5%81%E7%A8%8B">处理流程</a></li>
<li><a href="#%E6%8F%92%E4%BB%B6%E5%AE%9E%E4%BE%8B">插件实例</a></li>
</ul>
</li>
</ul>
<center> ————————</center>
<h2 id="什么是插件"><a class="markdownIt-Anchor" href="#什么是插件"></a> 什么是插件</h2>
<p>仔细读的都知道,插件是 Nonebot2 的核心。插件既可以是一个模块,也可以是一个包。</p>
<p>插件的存放路径是 <code>src/plugins</code>,在这个路径下,任何 <strong>.py 文件</strong> 或 <strong>有__init__.py 文件的文件夹</strong> 都算为插件。</p>
<div class="danger">
<p>插件名称不可以重复。</p>
</div>
<p>多说不如少做,瞅一眼 <a href="#%E6%8F%92%E4%BB%B6%E5%AE%9E%E4%BE%8B">插件实例</a>,说不定能够更理解插件的用处。</p>
<center>————————</center>
<h2 id="创建插件"><a class="markdownIt-Anchor" href="#创建插件"></a> 创建插件</h2>
<ol>
<li>
<p>在机器人文件夹里右键开启 CMD,输入 <code>nb plugin create</code>。</p>
</li>
<li>
<p><code>Project Name:</code>,输入插件的名称</p>
</li>
<li>
<p><code>Where to store the plugin?</code>,自处是询问插件存放的路径,选择 <code>src/plugins</code> 即可。</p>
</li>
<li>
<p><code>Do you want to load……巴拉巴拉</code>。如果你想在你创建的这个插件里再套娃一个插件,就选 <code>y</code>;反之就 <code>n</code>。正常都是不用套娃。</p>
</li>
<li>
<p>没了,你的插件创建完成噜。</p>
<p>进入 <code>src/plugins</code> 里边,就能看到插件名称的文件夹,里边分别躺着 <strong><strong>init</strong>.py</strong> 和 <strong><a target="_blank" rel="noopener" href="http://config.py">config.py</a></strong> 文件。</p>
<p>前者用于编写插件内容,相当于插件的入口文件;后者则是插件的配置文件。</p>
<p><s>其实直接在文件夹内新建也可以,就提一嘴。</s></p>
</li>
</ol>
<center>————————</center>
<h2 id="用别人的插件"><a class="markdownIt-Anchor" href="#用别人的插件"></a> 用别人的插件</h2>
<p><em>要是自己代码写不明白,岂不是永远都没法整一个机器人了?</em></p>
<p><strong>没有的事</strong>。Nonebot2 具有一个专属的 <a target="_blank" rel="noopener" href="https://v2.nonebot.dev/store">商店</a>,其包含了开源的</p>
<ul>
<li>驱动器</li>
<li>适配器</li>
<li>插件</li>
<li>机器人</li>
</ul>
<p>使用他人的插件很简易,方法也多,就是别忘了用了后给他们一个 <strong>STAR</strong> ~</p>
<center>……</center>
<p><strong>pip 安装法:</strong></p>
<p><img src="/posts/41a5/docs.png" alt="docs"></p>
<ol>
<li>
<p>举例:NoneBot 离线文档【nonebot_plugin_docs】</p>
<p>这个插件在加载后会输出一个可以 <strong>离线打开</strong> 的 Nonebot2 网址。</p>
</li>
<li>
<p>开个 CMD,输入 <code>pip install nonebot_plugin_docs</code></p>
</li>
<li>
<p>等待安装完成……</p>
<p><img src="/posts/41a5/bot.png" alt="bot.py"></p>
</li>
<li>
<p>来到机器人文件夹下的 <strong><a target="_blank" rel="noopener" href="http://bot.py">bot.py</a></strong>,找到上图这个位置。</p>
<p><code>nonebot.load_builtin_plugins("echo")</code> 意思便是 Nonebot2 加载了内嵌的 echo 插件,也就是我们之前试用机器人时用的 echo 功能。</p>
<p><code>nonebot.load_from_toml("pyproject.toml")</code> 加载了配置内插件存放路径下所有的插件,也就是 <code>src/plugins</code>。</p>
<p>实际上,插件的加载也有前后之分。按照上图的顺序来说,机器人启动时都会事先加载 echo 插件,其次才是 <code>src/plugins</code> 下的插件。</p>
</li>
<li>
<p>按照你想要的顺序,在 <strong><a target="_blank" rel="noopener" href="http://bot.py">bot.py</a></strong> 里加入 <code>nonebot.load_plugin("nonebot_plugin_docs")</code></p>
<p>注意,一定要是 <code>_</code>,而不是 <code>-</code>。</p>
</li>
<li>
<p>启动机器人,加载插件时应当会出现一句 <code>[INFO] nonebot_plugin_docs | Nonebot docs will be running at: http://localhost:端口/website/</code></p>
<p>有了便是加载成功,没有也没关系,必定会有一大串红红绿绿的错误日志等待着你。回头看看哪里疏漏,修复了就成。</p>
</li>
</ol>
<center>……</center>
<p><strong>脚手架安装法:</strong></p>
<p><img src="/posts/41a5/apscheduler.png" alt="apscheduler"></p>
<ol>
<li>
<p>举例:定时任务【nonebot_plugin_apscheduler】</p>
<p>该插件为定时任务插件,使用方式见 <a target="_blank" rel="noopener" href="https://github.com/nonebot/plugin-apscheduler">其仓库</a>。</p>
</li>
<li>
<p>进入有 <strong>pyproject.toml</strong> 的机器人文件夹内,右键开启 CMD,输入 <code>nb plugin install nonebot_plugin_apscheduler</code></p>
<p><img src="/posts/41a5/pyproject.png" alt="pyproject"></p>
</li>
<li>
<p>等待安装完成……</p>
<p>完成后便能在 <strong>pyproject.toml</strong> 里面发现多了一句 <code>plugins = ["nonebot_plugin_apscheduler"]</code></p>
<p>要是没有也没关系,还是那句话,一大串错误日志等待着你。看看哪里出了问题,修复了就成。</p>
</li>
</ol>
<center>……</center>
<p><strong>GitHub 下载法:</strong></p>
<p><img src="/posts/41a5/withdraw.png" alt="withdraw"></p>
<ol>
<li>
<p>这个法子的好处是可以自行修改源代码,不过手要勤奋点了。</p>
<p>GitHub 账号如何注册这里就不写了。<s>喂明明连 Python 怎么安装都写了</s></p>
<p>举例:Nonebot2 消息撤回插件【nonebot_plugin_withdraw】</p>
<p><img src="/posts/41a5/download.png" alt="download"></p>
</li>
<li>
<p>进入 <a target="_blank" rel="noopener" href="https://github.com/MeetWq/nonebot-plugin-withdraw">其仓库</a>,找到上图这个地方,选择 Download ZIP。</p>
<p><img src="/posts/41a5/zip.png" alt="zip"></p>
</li>
<li>
<p>单单解压上图选中的这个文件夹在你的 <code>src/plugins</code> 下。</p>
<p>解压时如果出现同名文件夹套文件夹,将被套的文件夹移上一级即可。</p>
</li>
<li>
<p>启动机器人,出现 <code>[SUCCESS] nonebot | Succeeded to import "nonebot_plugin_withdraw"</code> 这句便代表成功力。</p>
<p>没有的话,错误日志.jpg</p>
</li>
<li>
<p><s>如果文件不是很多的话,可以直接复制粘贴文件内容。</s></p>
</li>
</ol>
<center>————————</center>
<h2 id="事件响应器"><a class="markdownIt-Anchor" href="#事件响应器"></a> 事件响应器</h2>
<p>事件响应器,或 Matcher,是响应接收到的事件的单元。</p>
<p>多说不如少做,我们来写一个 <strong>接收到 "hi" 这个词时就作出响应</strong> 的事件响应器吧。</p>
<ol>
<li>
<p>定义辅助函数即可在插件内创建事件响应器。</p>
<p>不过事先我们得从 nonebot 主模块里导出辅助函数。</p>
</li>
<li>
<p>按照上边的教程,随便建立一个插件,进入其 <strong><strong>init</strong>.py</strong> 文件,写入 <code>from nonebot import on_command</code></p>
<p><code>on_command</code> 就是这里我们要用的辅助函数,它会创建命令消息事件响应器。</p>
<p>因为我们要在接收到 "hi" 时就有响应,"hi" 就是这里的命令消息。</p>
</li>
<li>
<p>写入 <code>from nonebot.permission import SUPERUSER</code></p>
<p>该步骤实际为可选,SUPERUSER 我们在上边配置 .env 文件时有提到过,即超级用户。</p>
<p>在 Nonebot2 中,超级用户就像是 QQ 群聊中的管理员,享受着高贵的特权。</p>
<p>被设置了超级用户权限的事件响应器 <strong>只会</strong> 对超级用户账号作出响应。</p>
<p><img src="/posts/41a5/hi.png" alt="hi"></p>
</li>
<li>
<p>写入 <code>matcher = on_command("hi", permission=SUPERUSER, priority=10)</code></p>
<p><code>matcher</code> 在这里是事件响应器的名称,<strong>可以随便取</strong>,就是在同个文件里不能重名。</p>
<p><code>"hi"</code> 指定了 "hi" 为命令消息,只有在接收到命令消息时机器人才会响应。</p>
<p><code>permission=SUPERUSER</code> 设置了超级用户权限,只有在超级用户发送 "hi" 时机器人才会响应。</p>
<p><code>priority=10</code> 定义了该事件响应器的优先级。优先级数字越小越先响应,以 1 开始。<strong>可以不填</strong>,这边提一嘴。</p>
<p>和优先级一样可选的变量还有:匹配规则、阻断等等。</p>
<p>辅助函数大全可见 <a target="_blank" rel="noopener" href="https://v2.nonebot.dev/docs/tutorial/plugin/create-matcher#%E5%88%9B%E5%BB%BA%E4%BA%8B%E4%BB%B6%E5%93%8D%E5%BA%94%E5%99%A8">官方网址</a>。</p>
</li>
</ol>
<p>不过现在只是建立了一个 <strong>可以响应</strong> 的事件响应器,处理流程还没写呢。</p>
<center>————————</center>
<h2 id="处理流程"><a class="markdownIt-Anchor" href="#处理流程"></a> 处理流程</h2>
<p>事件的处理流程由处理依赖组成,即 <strong>Dependent</strong>。</p>
<p>而装饰器能够装饰一个函数,让其自动转换为 Dependent 对象并加入事件响应器的处理流程中。</p>
<p>装饰器分为:</p>
<ul>
<li>handle;最基本的装饰器</li>
<li>receive;在 handle 的基础上会中断当前事件处理流程,并等待一个新的事件</li>
<li>got;和 receive 相似但用于接收消息,贴近于对话形式会话</li>
</ul>
<p>由于我们的例子是 <strong>接收到 "hi" 这个词时就作出响应</strong>,这里用 handle 装饰器即可。</p>
<ol>
<li>
<p>在事件响应器下面写入 <code>@matcher.handle()</code></p>
<p><code>matcher</code> 依旧是事件响应器的名称。</p>
<p>这里的 handle 为装饰器。</p>
</li>
<li>
<p>写入 <code>async def func():</code></p>
<p>我们定义了一个名为 func 的函数,而在 handle 装饰器下,func 函数已经变成了 Dependent 对象。</p>
<p><img src="/posts/41a5/await.png" alt="await"></p>
</li>
<li>
<p>在 QQ 里面,我们要如何才能知道发了 "hi" 后,机器人到底有没有作出响应?</p>
<p>我的建议是让机器人也发送什么,比如 "喵"。</p>
<p>在 func 函数内写入 <code>await matcher.send("喵")</code>,即这个名为 matcher 的事件响应器响应后发送了 "喵"。</p>
</li>
</ol>
<p>到这里,处理流程也写好了,不过还是缺些什么才能作为一个可运作的插件。</p>
<center>————————</center>
<h2 id="插件实例"><a class="markdownIt-Anchor" href="#插件实例"></a> 插件实例</h2>
<p>虽然处理流程写出模子来了,但问题也来了:这么写是 <strong>没法获取上下文信息</strong> 的。</p>
<p>获取上下文信息的重点就在 func 函数里,该函数需要从 Onebot.11 适配器里导出的 <code>Bot</code> 参数。</p>
<ol>
<li>
<p>分别添加 <code>from nonebot.params import State</code></p>
<p><code>from nonebot.typing import T_State</code></p>
<p><code>from nonebot.adapters.onebot.v11 import Bot, Event</code></p>
</li>
<li>
<p>在 func 函数括号内添加 <code>bot: Bot, event: Event, state: T_State = State()</code></p>
<p>对于更多的参数,见 <a target="_blank" rel="noopener" href="https://v2.nonebot.dev/docs/tutorial/plugin/create-handler#%E8%8E%B7%E5%8F%96%E4%B8%8A%E4%B8%8B%E6%96%87%E4%BF%A1%E6%81%AF">官方网址</a>。</p>
</li>
<li>
<p><code>Bot</code> 基类用于处理上报消息、提供 API 调用接口。</p>
<p><code>Event</code> 基类提供了关键信息的方法,如获取事件消息内容。</p>
<p><code>T_State</code> 表明了该事件处理状态为 State 类型。</p>
<p><code>State</code> 则是 Nonebot2 beta1 版本新添的依赖,<code>T_State</code> 必须要陪着一个 <code>State</code>。</p>
<p>对于我们的例子来说,除了 <code>Bot</code> 实际都是可选项,只是提一嘴。</p>
</li>
</ol>
<p><img src="/posts/41a5/awaitbot.png" alt="test_plugin"></p>
<p>上图便是一个小型插件的完整样貌。</p>
<p>启动机器人,快马加鞭使用大号向机器人账号发送 "hi",便会收获一个你自己写出来的 <s>猫咪女仆</s> 聊天机器人。</p>
</body></html></div></article></div></main><footer><div class="paginator"><a class="prev" href="9b61.html">上一篇</a></div><!-- Webmention 显示区域--><div class="webmention-section webmention-empty" data-page-url="posts/41a5.html" data-full-url="https://cytrogen.icu/posts/41a5.html" data-mode="static">
<h3 class="webmention-title">Webmentions (<span class="webmention-count">0</span>)</h3>
<div class="webmention-list"></div>
<span>暂无 Webmentions</span>
</div><div class="copyright"><p class="footer-links"><a href="../friends/index.html">友链</a><span class="footer-separator"> ·</span><a href="../links/index.html">邻邦</a><span class="footer-separator"> ·</span><a href="../contact/index.html">联络</a><span class="footer-separator"> ·</span><a href="../colophon/index.html">营造记</a><span class="footer-separator"> ·</span><a href="../atom.xml">RSS订阅</a></p><p>© 2025 - 2026 <a href="https://cytrogen.icu">Cytrogen</a>, powered by <a href="https://hexo.io/" target="_blank">Hexo</a> and <a href="https://github.com/cytrogen/hexo-theme-ares" target="_blank">hexo-theme-ares</a>.</p><p><a href="https://blogscn.fun" target="_blank" rel="noopener">BLOGS·CN</a></p></div></footer></div></div><a class="back-to-top" href="#top" aria-label="返回顶部"><svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M3.293 9.707a1 1 0 010-1.414L9.586 2a2 2 0 012.828 0l6.293 6.293a1 1 0 01-1.414 1.414L11 3.414V17a1 1 0 11-2 0V3.414L2.707 9.707a1 1 0 01-1.414 0z"></path></svg></a><script>document.addEventListener('DOMContentLoaded', function() {
const codeBlocks = document.querySelectorAll('figure.highlight');
codeBlocks.forEach(block => {
let caption = block.querySelector('figcaption');
if (!caption) {
caption = document.createElement('figcaption');
block.insertBefore(caption, block.firstChild);
}
const info = document.createElement('div');
info.className = 'info';
const filename = caption.querySelector('span');
if (filename) {
filename.className = 'filename';
info.appendChild(filename);
}
const lang = block.className.split(' ')[1];
if (lang) {
const langSpan = document.createElement('span');
langSpan.className = 'lang-name';
langSpan.textContent = lang;
info.appendChild(langSpan);
}
const sourceLink = caption.querySelector('a');
if (sourceLink) {
sourceLink.className = 'source-link';
info.appendChild(sourceLink);
}
const actions = document.createElement('div');
actions.className = 'actions';
const codeHeight = block.scrollHeight;
const threshold = 300;
if (codeHeight > threshold) {
block.classList.add('folded');
const toggleBtn = document.createElement('button');
toggleBtn.textContent = '展开';
toggleBtn.addEventListener('click', () => {
block.classList.toggle('folded');
toggleBtn.textContent = block.classList.contains('folded') ? '展开' : '折叠';
});
actions.appendChild(toggleBtn);
}
const copyBtn = document.createElement('button');
copyBtn.textContent = '复制';
copyBtn.addEventListener('click', async () => {
const codeLines = block.querySelectorAll('.code .line');
const code = Array.from(codeLines)
.map(line => line.textContent)
.join('\n')
.replace(/\n\n/g, '\n');
try {
await navigator.clipboard.writeText(code);
copyBtn.textContent = '已复制';
copyBtn.classList.add('copied');
setTimeout(() => {
copyBtn.textContent = '复制';
copyBtn.classList.remove('copied');
}, 3000);
} catch (err) {
console.error('复制失败:', err);
copyBtn.textContent = '复制失败';
setTimeout(() => {
copyBtn.textContent = '复制';
}, 3000);
}
});
actions.appendChild(copyBtn);
caption.innerHTML = '';
caption.appendChild(info);
caption.appendChild(actions);
const markedLines = block.getAttribute('data-marked-lines');
if (markedLines) {
const lines = markedLines.split(',');
lines.forEach(range => {
if (range.includes('-')) {
const [start, end] = range.split('-').map(Number);
for (let i = start; i <= end; i++) {
const line = block.querySelector(`.line-${i}`);
if (line) line.classList.add('marked');
}
} else {
const line = block.querySelector(`.line-${range}`);
if (line) line.classList.add('marked');
}
});
}
});
});</script><script async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" id="MathJax-script"></script><script>(function() {
document.addEventListener('DOMContentLoaded', function() {
const themeToggle = document.querySelector('.theme-toggle');
if (!themeToggle) return;
const getCurrentTheme = () => {
return document.documentElement.getAttribute('data-theme') || 'light';
};
const updateUI = (theme) => {
const isDark = theme === 'dark';
themeToggle.setAttribute('aria-pressed', isDark.toString());
};
const setTheme = (theme) => {
document.documentElement.setAttribute('data-theme', theme);
document.documentElement.style.colorScheme = theme;
const pageWrapper = document.getElementById('page-wrapper');
if (pageWrapper) {
pageWrapper.setAttribute('data-theme', theme);
}
// Find and remove the temporary anti-flicker style tag if it exists.
// This ensures the main stylesheet takes full control after the initial load.
const antiFlickerStyle = document.getElementById('anti-flicker-style');
if (antiFlickerStyle) {
antiFlickerStyle.remove();
}
localStorage.setItem('theme', theme);
updateUI(theme);
};
const toggleTheme = () => {
const current = getCurrentTheme();
const newTheme = current === 'light' ? 'dark' : 'light';
setTheme(newTheme);
};
updateUI(getCurrentTheme());
themeToggle.addEventListener('click', toggleTheme);
if (window.matchMedia) {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
mediaQuery.addEventListener('change', function(e) {
if (!localStorage.getItem('theme')) {
const theme = e.matches ? 'dark' : 'light';
setTheme(theme);
}
});
}
});
})();
</script><script src="../js/details-toggle.js" defer></script><script>(function() {
document.addEventListener('DOMContentLoaded', function() {
const backToTopBtn = document.querySelector('.back-to-top');
if (!backToTopBtn) return;
const toggleButtonVisibility = () => {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const shouldShow = scrollTop > 200;
if (shouldShow) {
backToTopBtn.classList.add('is-visible');
} else {
backToTopBtn.classList.remove('is-visible');
}
};
let ticking = false;
const handleScroll = () => {
if (!ticking) {
requestAnimationFrame(() => {
toggleButtonVisibility();
ticking = false;
});
ticking = true;
}
};
const scrollToTop = (event) => {
event.preventDefault();
window.scrollTo({
top: 0,
behavior: 'smooth'
});
};
window.addEventListener('scroll', handleScroll);
backToTopBtn.addEventListener('click', scrollToTop);
toggleButtonVisibility();
});
})();</script></body></html>