~cytrogen/blog-public

blog-public/posts/41a5.html -rw-r--r-- 45.0 KiB
88eebf3dCytrogen Deploy 2026-02-19 08:34:27 4 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
<!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>