~cytrogen/blog-public

blog-public/posts/60db.html -rw-r--r-- 46.6 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
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
<!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>Bot Framework SDK 学习日志 · Cytrogen 的个人博客</title><meta name="description" content="仅作个人用途,微软 Azure 的机器人框架 SDK Python 分支的学习日志。"><link rel="icon" href="../favicon.png"><link rel="canonical" href="https://cytrogen.icu/posts/60db.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/60db.html">永久链接</a><div class="p-summary visually-hidden"><p>仅作个人用途,微软 Azure 的机器人框架 SDK Python 分支的学习日志。</p></div><div class="visually-hidden"><a class="p-category" href="../categories/%E7%BF%BB%E8%AF%91/">翻译</a><a class="p-category" href="../tags/Python/">Python</a><a class="p-category" href="../tags/Azure/">Azure</a><a class="p-category" href="../tags/Bot/">Bot</a></div><h1 class="post-title p-name">Bot Framework SDK 学习日志</h1><div class="post-info"><time class="post-date dt-published" datetime="2023-03-07T18:28:26.000Z">3/7/2023</time><time class="dt-updated visually-hidden" datetime="2026-02-09T17:16:54.681Z"></time></div><div class="post-content e-content"><html><head></head><body><p>仅作个人用途,微软 Azure 的机器人框架 SDK Python 分支的学习日志。</p>
<span id="more"></span>
<h1 id="目录"><a class="markdownIt-Anchor" href="#目录"></a> 目录</h1>
<ul>
<li><a href="#%E7%9B%AE%E5%BD%95">目录</a></li>
<li><a href="#%E6%9C%BA%E5%99%A8%E4%BA%BA%E5%88%9D%E8%AF%86">机器人初识</a>
<ul>
<li><a href="#%E6%9C%BA%E5%99%A8%E4%BA%BA%E4%BA%A4%E4%BA%92">机器人交互</a></li>
<li><a href="#%E6%9C%BA%E5%99%A8%E4%BA%BA%E5%BA%94%E7%94%A8%E7%A8%8B%E5%BA%8F%E7%BB%93%E6%9E%84">机器人应用程序结构</a></li>
<li><a href="#%E6%9C%BA%E5%99%A8%E4%BA%BA%E9%80%BB%E8%BE%91">机器人逻辑</a></li>
<li><a href="#%E6%9C%BA%E5%99%A8%E4%BA%BA%E9%80%82%E9%85%8D%E5%99%A8">机器人适配器</a></li>
<li><a href="#%E8%BD%AE%E6%AC%A1%E4%B8%8A%E4%B8%8B%E6%96%87">轮次上下文</a></li>
<li><a href="#%E4%B8%AD%E9%97%B4%E4%BB%B6">中间件</a></li>
<li><a href="#%E6%B4%BB%E5%8A%A8%E5%A4%84%E7%90%86%E5%A0%86%E6%A0%88">活动处理堆栈</a></li>
<li><a href="#%E6%9C%BA%E5%99%A8%E4%BA%BA%E6%A8%A1%E6%9D%BF">机器人模板</a></li>
</ul>
</li>
<li><a href="#%E6%9C%BA%E5%99%A8%E4%BA%BA%E5%8A%A0%E6%B7%B1%E8%AE%A4%E8%AF%86">机器人加深认识</a>
<ul>
<li><a href="#%E7%AE%A1%E7%90%86%E7%8A%B6%E6%80%81">管理状态</a></li>
<li><a href="#%E6%B4%BB%E5%8A%A8%E5%A4%84%E7%90%86%E7%A8%8B%E5%BA%8F">活动处理程序</a></li>
<li><a href="#%E5%AF%B9%E8%AF%9D%E5%BA%93">对话库</a></li>
<li><a href="#%E4%B8%89%E5%A4%A7%E5%AF%B9%E8%AF%9D%E6%A1%86">三大对话框</a></li>
<li><a href="#%E4%BD%BF%E7%94%A8%E5%AF%B9%E8%AF%9D%E6%A1%86">使用对话框</a></li>
<li><a href="#%E9%85%8D%E7%BD%AE%E6%9C%BA%E5%99%A8%E4%BA%BA">配置机器人</a></li>
</ul>
</li>
</ul>
<center> -----------------------------</center>
<h1 id="机器人初识"><a class="markdownIt-Anchor" href="#机器人初识"></a> 机器人初识</h1>
<h2 id="机器人交互"><a class="markdownIt-Anchor" href="#机器人交互"></a> 机器人交互</h2>
<p>机器人交互涉及到活动的交换,而这些活动在轮次中进行处理。</p>
<ol>
<li>
<p><em>活动(activities)</em>:活动是用户或者 <em>通道(channel)</em> 与机器人之间的交互。</p>
</li>
<li>
<p><em>轮次(turns)</em>:一轮次的对话包含了用户传给机器人的活动,也包含了机器人发给用户的即时响应(也是活动)。</p>
<p>类似于回合制战斗,速度快的我方先行采取一个行动后,速度慢的对面再采取一个行动,双方行动过后该轮次(或称回合)结束。</p>
</li>
</ol>
<h2 id="机器人应用程序结构"><a class="markdownIt-Anchor" href="#机器人应用程序结构"></a> 机器人应用程序结构</h2>
<ol>
<li>* <strong> 机器人</strong>(bot)* 类,用于处理机器人应用的聊天推理
<ul>
<li>识别 &amp; 解释用户的输入</li>
<li>对输入进行推理 &amp; 执行相关任务</li>
<li>生成响应(如:机器人正在干什么)</li>
</ul>
</li>
<li>* <strong> 适配器</strong>(adapter)* 类,用于处理与通道的连接
<ul>
<li>提供用于处理来自用户通道的请求的方法</li>
<li>提供用于对用户通道生成请求的方法</li>
<li>包含一个中间件通道,包括机器人轮次处理程序外部的轮次处理</li>
<li>调用机器人的轮次处理程序</li>
<li>捕获不在轮次处理程序中处理的错误</li>
</ul>
</li>
</ol>
<p>机器人每个轮次还需要检索和存储 <em>状态(state)</em>。状态通过 <em>存储(storage)</em><em>机器人状态(bot state)</em><em>属性访问器(property accessor)</em> 类进行处理。</p>
<h2 id="机器人逻辑"><a class="markdownIt-Anchor" href="#机器人逻辑"></a> 机器人逻辑</h2>
<ol>
<li><em><strong>活动处理程序</strong>(activity handler)</em>,提供事件驱动模型,其中传入的活动类型 &amp; 子类型是 <em>事件(event)</em></li>
<li><em><strong>对话库</strong>(dialog library)</em>,提供基于状态的模型用于管理与用户进行的长时间聊天。</li>
</ol>
<h2 id="机器人适配器"><a class="markdownIt-Anchor" href="#机器人适配器"></a> 机器人适配器</h2>
<p>适配器提供用于启动轮次的 <em>过程活动(process activity)</em> 方法。</p>
<ul>
<li>将请求正文和请求头用于参数</li>
<li>检查身份验证头是否有效</li>
<li>为轮次创建一个 <em>上下文(context)</em> 对象
<ul>
<li>上下文对象包含有关活动的信息</li>
</ul>
</li>
<li>通过中间件管道发送上下文对象</li>
<li>将上下文对象发送到机器人对象的 <em>轮次处理程序(turn handler)</em></li>
</ul>
<blockquote>
<p>适配器还可以:</p>
<ul>
<li>格式化 &amp; 发送响应活动</li>
<li><em>公开机器人连接器(Bot Connector)</em> REST API 提供的其他方法</li>
<li>捕获在轮次中不会被捕获到的错误 &amp; 异常</li>
</ul>
</blockquote>
<h2 id="轮次上下文"><a class="markdownIt-Anchor" href="#轮次上下文"></a> 轮次上下文</h2>
<p><em>轮次上下文(turn context)</em> 对象提供有关活动的信息。</p>
<ul>
<li>例如发送方和接收方、通道、处理该活动所需的其他数据</li>
</ul>
<p>轮次上下文不仅将 <em>入站活动(inbound activity)</em> 传递到所有的中间件组件和应用程序逻辑,还提供了所需要的机制让中间件组件和机器人逻辑发送 <em>出站活动(outbound activity)</em></p>
<h2 id="中间件"><a class="markdownIt-Anchor" href="#中间件"></a> 中间件</h2>
<p>SDK 的中间件由一组线性组件构成,其中每一个组件都会按照顺序执行并有一个操作活动的机会。</p>
<p>中间件管道的最后一个阶段:回调机器人类中的轮次处理程序(已经被适配器的过程活动方法注册)。中间件执行被适配器调用的 <code>on turn</code> 方法。</p>
<p>轮次处理程序采用轮次上下文作为参数。在轮次处理程序函数内运行的应用程序逻辑会处理入站活动的内容,并生成活动作为响应,在轮次上下文中调用 <code>send activity</code> 方法来发送出站活动。调用 <code>send activity</code> 方法会导致中间件组件在出站活动上被调用。</p>
<p>中间件组件于轮次处理程序函数之前和之后执行。这些执行在本质上是套娃。</p>
<h2 id="活动处理堆栈"><a class="markdownIt-Anchor" href="#活动处理堆栈"></a> 活动处理堆栈</h2>
<ol>
<li>通道终结点向 Azure 机器人服务发送 HTTP POST 信息</li>
<li>Azure 机器人服务处理活动,发送给适配器和轮次上下文</li>
<li>适配器和轮次上下文调用 <code>on turn</code> 方法,发送给机器人</li>
<li>机器人调用 <code>send activity</code> 方法,一个个返回给通道终结点</li>
<li>通道终结点发送回状态码 200,机器人同理</li>
</ol>
<h2 id="机器人模板"><a class="markdownIt-Anchor" href="#机器人模板"></a> 机器人模板</h2>
<ul>
<li>资源预配</li>
<li>一个特定于语言的 HTTP 终结点实现,可以将传入的活动路由到一个适配器</li>
<li>一个适配器对象</li>
<li>一个机器人对象</li>
</ul>
<center>-----------------------------</center>
<h1 id="机器人加深认识"><a class="markdownIt-Anchor" href="#机器人加深认识"></a> 机器人加深认识</h1>
<h2 id="管理状态"><a class="markdownIt-Anchor" href="#管理状态"></a> 管理状态</h2>
<p>之前说过,机器人本质上是没有状态的。状态并不是必需的,部分机器人可以不需要状态(也就是用户不提供信息)就正常运行;部分机器人则必须提供了状态才能提供有用的聊天信息,例如以前收到的有关用户的数据。</p>
<p>状态就像是记忆,提供给了机器人后便能让机器人记住有关用户或者本次聊天的信息。</p>
<ul>
<li>
<p><em><strong>存储层</strong>(storage layer)</em>,在后端实际存储状态信息的一层。采用物理存储,如:内存、Azure 服务器、第三方服务器。</p>
<blockquote>
<ul>
<li>内存存储:临时存储,机器人一重开就清除</li>
<li>Azure Blob 存储:连接到 Azure Blob 存储对象数据库</li>
<li>Azure Cosmos DB 分区存储:连接到分区的 Cosmos DB NoSQL 数据库</li>
</ul>
</blockquote>
</li>
<li>
<p><em><strong>状态管理</strong>(state management)</em>,自动在基础存储层中读取 &amp; 写入机器人的状态。状态以 <em>状态属性(state properties)</em> 的键值对形式存储。</p>
<p>状态属性被集结到有范围的「桶」(帮助组织这些属性的集合)内,SDK 的三个桶分别是:<em>用户状态(user state)</em><em>聊天状态(conversation state)</em><em>私人聊天状态(private conversation state)</em>。这些桶又是 <code>bot state</code> 类的子类。</p>
<ul>
<li>
<p>用户状态适合用于跟踪有关用户的信息,如:用户的姓名</p>
</li>
<li>
<p>聊天状态适合用于跟踪聊天的上下文,如:机器人是否向用户提出了问题,这个问题又是啥</p>
</li>
<li>
<p>私人聊天状态适合用于支持群组聊天的通道,如:课堂抢答机器人(聚合每位学生的成绩,最终用私聊方式将信息发送给相应的学生)</p>
</li>
</ul>
</li>
<li>
<p><em><strong>状态属性访问器</strong>(state property accessors)</em>,用于实际读取 &amp; 写入某个状态属性,提供了 <code>get</code><code>set</code><code>delete</code> 方法用于从轮次内部访问状态属性。</p>
<p>访问器创建需要用到属性名称。之后便可以使用访问器来获取和处理机器人状态的该属性。</p>
<p>访问器允许 SDK 从基础存储获取状态 &amp; 更新机器人的状态缓存(机器人维护的本地缓存,用于存储状态对象和允许在不访问基础存储的情况下执行读取 &amp; 写入操作)。</p>
<ul>
<li>
<p><code>get</code> 方法,从状态缓存请求属性。如果在缓存中就返回属性,否则从状态管理对象获取该属性</p>
</li>
<li>
<p><code>set</code> 方法,使用新属性值更新状态缓存</p>
</li>
<li>
<p><code>delete</code> 方法,从缓存和基础存储中删除属性</p>
</li>
<li>
<p><code>save changes</code> 方法(状态管理对象的),检查状态缓存中属性所有的更改,并将属性写入存储</p>
<ul>
<li>要注意,<code>set</code> 方法记录更新的状态后,该状态属性尚未保存到持久性存储,只是保存到机器人的状态缓存内而已。</li>
</ul>
</li>
</ul>
</li>
</ul>
<p><em>对话框(dialog)</em> 库使用在机器人的 <em>会话状态(conversation state)</em> 上定义的对话框状态属性访问器来保留对话在会话中的位置。对话框状态属性还允许每个对话框在轮次之中存储临时信息。</p>
<p><em>对话框管理器(dialog manager)</em> 使用用户和会话状态管理对象提供内存范围(这些内存范围可以用于自适应对话框)。</p>
<h2 id="活动处理程序"><a class="markdownIt-Anchor" href="#活动处理程序"></a> 活动处理程序</h2>
<p>生成机器人时,用于处理和响应消息的机器人逻辑将进入 <code>on_message_activity</code> 处理程序。同样,用于处理正在添加到聊天中的成员的逻辑将进入 <code>on_members_added</code> 处理程序。每当一个成员加入到聊天,这个处理程序就会被调用。</p>
<p>机器人逻辑处理来自单个或多个通道的传入活动,并在响应中发生传出活动。</p>
<p>在 Python 里,要使用 <code>ActivityHandler</code> 派生机器人类,前者为不同类型的活动定义各种各样的处理程序,例如上文的 <code>on_message_activity</code> 处理程序。</p>
<details>
<table>
<thead>
<tr>
<th>事件</th>
<th style="text-align:left">Handler</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>已收到任一活动类型</td>
<td style="text-align:left"><code>on_turn</code></td>
<td>根据收到的活动类型,调用其他处理程序。</td>
</tr>
<tr>
<td>已收到消息活动</td>
<td style="text-align:left"><code>on_message_activity</code></td>
<td>处理 <code>message</code> 活动。</td>
</tr>
<tr>
<td>已收到聊天更新活动</td>
<td style="text-align:left"><code>on_conversation_update_activity</code></td>
<td>收到 <code>conversationUpdate</code> 活动时,如果除了机器人以外的成员加入或者退出聊天,则调用某个处理程序。</td>
</tr>
<tr>
<td>非机器人成员加入了聊天</td>
<td style="text-align:left"><code>on_members_added_activity</code></td>
<td>处理加入聊天的成员。</td>
</tr>
<tr>
<td>非机器人成员退出了聊天</td>
<td style="text-align:left"><code>on_members_removed_activity</code></td>
<td>处理退出聊天的成员。</td>
</tr>
<tr>
<td>已收到事件活动</td>
<td style="text-align:left"><code>on_event_activity</code></td>
<td>收到 <code>event</code> 活动时,调用特定于事件类型的处理程序。</td>
</tr>
<tr>
<td>已收到令牌响应事件活动</td>
<td style="text-align:left"><code>on_token_response_event</code></td>
<td>处理令牌响应时间。</td>
</tr>
<tr>
<td>已收到非令牌响应事件活动</td>
<td style="text-align:left"><code>on_event_activity</code></td>
<td>处理其他类型的事件。</td>
</tr>
<tr>
<td>已收到消息回应活动</td>
<td style="text-align:left"><code>on_message_reaction_activity</code></td>
<td>收到 <code>messageReaction</code> 活动时,如果已经在消息中添加 &amp; 删除一个或多个回应,则调用处理程序。</td>
</tr>
<tr>
<td>消息回应已添加到消息</td>
<td style="text-align:left"><code>on_reaction_added</code></td>
<td>处理添加到消息的回应。</td>
</tr>
<tr>
<td>从消息中删除了消息回应</td>
<td style="text-align:left"><code>on_reaction_removed</code></td>
<td>处理从消息中删除的回应。</td>
</tr>
<tr>
<td>已收到安装更新活动</td>
<td style="text-align:left"><code>on_installation_update</code></td>
<td>对于 <code>installationUpdate</code> 活动,根据机器人是「已安装」还是「已卸载」来调用处理程序。</td>
</tr>
<tr>
<td>安装了机器人</td>
<td style="text-align:left"><code>on_installation_update_add</code></td>
<td>添加逻辑来确定何时在组织单位中安装了机器人。</td>
</tr>
<tr>
<td>卸载了机器人</td>
<td style="text-align:left"><code>on_installation_update_remove</code></td>
<td>添加逻辑来确定何时在组织单位中卸载了机器人。</td>
</tr>
<tr>
<td>已收到其他活动类型</td>
<td style="text-align:left"><code>on_unrecognized_activity_type</code></td>
<td>处理未经处理的任何活动类型。</td>
</tr>
</tbody>
</table>
</details>
<p>每个处理程序都有一个 <code>turn_context</code>,用于提供有关对应于入站 HTTP 请求的传入活动的信息。</p>
<blockquote>
<p>示例:</p>
<ul>
<li>处理 <code>on_members_added</code> 来发送欢迎信息,并处理 <code>on_message</code> 来当复读机</li>
</ul>
<figure class="highlight python"><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><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">EchoBot</span>(<span class="title class_ inherited__">ActivityHandler</span>):</span><br><span class="line">    <span class="keyword">async</span> <span class="keyword">def</span> <span class="title function_">on_members_added_activity</span>(<span class="params"></span></span><br><span class="line"><span class="params">    	self,</span></span><br><span class="line"><span class="params">        members_added: [ChannelAccount],</span></span><br><span class="line"><span class="params">        turn_context: TurnContext</span></span><br><span class="line"><span class="params">    </span>):</span><br><span class="line">        <span class="string">"""</span></span><br><span class="line"><span class="string">        每当一个成员加入聊天,就发送`Hello and Welcome`。</span></span><br><span class="line"><span class="string">        """</span></span><br><span class="line">        <span class="keyword">for</span> member <span class="keyword">in</span> members_added:</span><br><span class="line">            <span class="keyword">if</span> member.<span class="built_in">id</span> != turn_context.activity.recipient.<span class="built_in">id</span>:</span><br><span class="line">                <span class="keyword">await</span> turn_context.send_activity(<span class="string">'Hello and Welcome!'</span>)</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">async</span> <span class="keyword">def</span> <span class="title function_">on_message_activity</span>(<span class="params"></span></span><br><span class="line"><span class="params">    	self,</span></span><br><span class="line"><span class="params">        turn_context: TurnContext</span></span><br><span class="line"><span class="params">    </span>):</span><br><span class="line">        <span class="string">"""</span></span><br><span class="line"><span class="string">        每收到一个消息,就发送`Echo: {消息}`</span></span><br><span class="line"><span class="string">        """</span></span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">await</span> turn_context.send_activity(</span><br><span class="line">        	MessageFactory.text(<span class="string">f'Echo: <span class="subst">{turn_context.activity.text}</span>'</span>)</span><br><span class="line">        )</span><br></pre></td></tr></tbody></table></figure>
</blockquote>
<h2 id="对话库"><a class="markdownIt-Anchor" href="#对话库"></a> 对话库</h2>
<p>对话框提供管理与用户长期对话的方法。</p>
<ul>
<li>每一个对话框都代表了一个会话任务(运行完成后可以返回收集到的信息)</li>
<li>每一个对话框都代表了一个基本的控制流单元:可以开始、继续和停止;暂停和恢复;或被取消</li>
<li>对话框类似于编程语言中的方法或者函数。启动对话框时可以传入参数,且该对话框之后可以在结束时生成一个返回值</li>
</ul>
<p>对话框可以实现 <em>多轮会话(multi-turn conversation)</em>,所以对话框依赖于跨多个轮次的 <em>持久性状态(persisted state)</em>。如果对话框中没有状态,机器人就会不知道它在会话中所处的位置,也不知道它已经收集好的信息。</p>
<p>因此,想要在会话中保留对话框的位置,就要在每个轮次中检索对话框状态并保存到内存。这一操作由(机器人的会话状态定义的)对话框状态属性访问器处理。</p>
<details>
<table>
<thead>
<tr>
<th></th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>对话框集(Dialog set)</td>
<td>定义一组对话框,这些对话框可以相互引用 &amp; 协同工作。</td>
</tr>
<tr>
<td>对话框上下文(Dialog context)</td>
<td>包含有关所有正在活动中的对话框的信息。</td>
</tr>
<tr>
<td>对话框实例(Dialog instance)</td>
<td>包含有关单个正在活动中的对话框的信息。</td>
</tr>
<tr>
<td>对话框轮次结果(Dialog turn result)</td>
<td>包含活动的或最近的活动对话框中的状态信息。如果活动对话框已经结束,则包含其返回值。</td>
</tr>
</tbody>
</table>
</details>
<br>
<p>为了简化管理机器人聊天,对话框库提供了一些对话框类型:</p>
<table>
<thead>
<tr>
<th>类型</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>对话框(Dialog)</td>
<td>所有对话框的基类。</td>
</tr>
<tr>
<td>容器对话框(Container dialog)</td>
<td>所有容器对话框的基类。</td>
</tr>
<tr>
<td>组件对话框(Component dialog)</td>
<td>一种通用类型的容器对话框。它封装了一组对话框作为一个整体重复使用集。<br>组件对话框启动后,将以其集合中的指定对话框开头。内部进程完成后,组件对话框便结束。</td>
</tr>
<tr>
<td>瀑布对话框(Waterfall dialog)</td>
<td>定义一系列步骤,使机器人能够引导用户完成线性流程。</td>
</tr>
<tr>
<td>提示对话框(Prompt dialogs)</td>
<td>要求用户输入并返回结果。</td>
</tr>
</tbody>
</table>
<h2 id="三大对话框"><a class="markdownIt-Anchor" href="#三大对话框"></a> 三大对话框</h2>
<ul>
<li>
<p><strong>组件对话框</strong> 是一种容器对话框,允许集合中的对话框调用集合中的其他对话框,如:瀑布式对话框调用提示对话框。</p>
<p>组件对话框还提供了一种创建独立对话框以及处理特定场景的策略,将一个大的对话框集分解成更易于管理的片段。每个片段又都有着自己的对话框集,并避免与包含它的对话框集发生任何名称冲突。</p>
</li>
<li>
<p><strong>提示对话框</strong> 是一个旨在向用户询问特定类型信息的对话框,如:一个日期。</p>
</li>
<li>
<p><strong>瀑布式对话框</strong> 是对话的具体实现,通常用于收集用户的信息,或者引导用户完成一系列的任务。对话的每一步都被实现为一个需要 <em>瀑布式步骤上下文(waterfall step context)</em> 作为参数的异步函数。</p>
<p>每一步,机器人提示用户输入,或者可以开启一个子对话框,等待回应,然后将结果传递给下一步。第一个函数的结果被作为参数传给下一个函数,以此类推。</p>
<blockquote>
<ol>
<li>对话框上下文开始瀑布</li>
<li>瀑布 #1:第一次提示</li>
<li>瀑布 #2:处理来自第一个提示的结果,并开始第二次提示</li>
<li>瀑布 #3:处理来自第二个提示的结果,结束对话(堆栈入口消失)</li>
</ol>
</blockquote>
<p>瀑布式对话框的上下文被存储在瀑布式步骤上下文中。这个步骤上下文与对话上下文类似,提供对当前轮次上下文和状态的访问。使用瀑布式步骤上下文对象来与瀑布式步骤中的对话框集进行交互。</p>
<p>对话框的返回值可以在瀑布式步骤中处理,也可以从机器人的轮次处理程序中处理(一般只需要在机器人的轮次论及中检查对话框轮次结果的状态)。</p>
</li>
</ul>
<br>
<p>瀑布步骤上下文包含以下属性:</p>
<ul>
<li><code>Options</code>:包含对话框的输入信息</li>
<li><code>Values</code>:包含可以添加到上下文中的信息,并被带入后续步骤中</li>
<li><code>Result</code>:包含前一个步骤的结果</li>
</ul>
<blockquote>
<p>Python 的 <code>next</code> 方法可以在同一轮次内继续进行瀑布式对话框的下一步,也就是在需要时跳过某个步骤。</p>
</blockquote>
<p><em>提示(Prompt)</em> 提供了一种简单的方法来询问用户的信息并评估他们的反应。</p>
<ul>
<li>
<p>提示本质上就是一个两步的对话框。首先提示会要求输入,然后返回有效值,或者从头开始重新提示。</p>
</li>
<li>
<p>调用提示时,可以在 <em>提示选项(prompt options)</em> 中指定要提示的文本、如果验证失败了的重新提示,以及回答提示的选择。</p>
<ul>
<li>一般而言,提示和重新提示属性都属于活动。</li>
</ul>
</li>
<li>
<p>当提示被创建时,也可以选择为提示添加自定义验证(如:小于 18 岁就不行的年龄验证)。提示先行检查它是否接收到了一个有效的数字,然后运行自定义验证。假如验证失败了,就重新提示。</p>
</li>
<li>
<p>当一个提示完成时,就会明确地返回所要求的结果值。</p>
</li>
</ul>
<table>
<thead>
<tr>
<th>提示</th>
<th>说明</th>
<th>返回值</th>
</tr>
</thead>
<tbody>
<tr>
<td>附件提示(Attachment prompt)</td>
<td>要求一个或多个附件,例如文档或者图片。</td>
<td>一个 <em>附件(attachment)</em> 对象的集合</td>
</tr>
<tr>
<td>选项提示(Choice prompt)</td>
<td>从一系列的选项中要求选择一个。</td>
<td>一个找到的选项对象</td>
</tr>
<tr>
<td>确认提示(Confirm prompt)</td>
<td>请求提供 <em>Yes</em><em>No</em></td>
<td>一个布尔值</td>
</tr>
<tr>
<td>日期时间提示(Date-time prompt)</td>
<td>请求提供一个日期时间</td>
<td>一个日期时间解析对象的集合</td>
</tr>
<tr>
<td>数字提示(Number prompt)</td>
<td>请求提供一个数字</td>
<td>一个数字值</td>
</tr>
<tr>
<td>文本提示(Text prompt)</td>
<td>请求提供一个常规的文字输入</td>
<td>一个字符串</td>
</tr>
</tbody>
</table>
<br>
<p>步骤上下文的 <code>prompt</code> 方法的第二个参数要求提供一个 <code>prompt options</code> 对象,该对象包含了这些属性:</p>
<table>
<thead>
<tr>
<th>属性</th>
<th>描述</th>
</tr>
</thead>
<tbody>
<tr>
<td>初始提示(Prompt / Initial prompt)</td>
<td>发送给用户的初始活动,用来征求用户的输入</td>
</tr>
<tr>
<td>重试提示(Retry prompt)</td>
<td>如果用户的第一个输入没有得到验证,就发送该活动</td>
</tr>
<tr>
<td>选项(Choices)</td>
<td>一个供用户选择的选项列表,和选项提示配合使用</td>
</tr>
<tr>
<td>验证(Validations)</td>
<td>用于自定义验证器的额外参数</td>
</tr>
<tr>
<td>样式(Style)</td>
<td>定义选项提示或确认提示的选项将如何呈现给用户</td>
</tr>
</tbody>
</table>
<p>始终应当指定好初始提示和重试提示。假设用户的输入无效,重试提示就会发送给用户;假设重试提示没有被指定,则会发送初始提示。</p>
<p>但是假设发回给用户的活动来自于验证器,就不会发送重试提示。</p>
<br>
<p>一个验证器函数带有一个 <em>提示验证器上下文(prompt validator context)</em> 参数,并返回一个布尔值,代表了输入是否通过了验证。</p>
<p>提示验证器上下文包含了这些属性:</p>
<table>
<thead>
<tr>
<th>属性</th>
<th>描述</th>
</tr>
</thead>
<tbody>
<tr>
<td>上下文(Context)</td>
<td>机器人当前的轮次上下文</td>
</tr>
<tr>
<td>识别(Recognized)</td>
<td>一个带有被识别器处理过的用户输入信息的 <em>提示识别器结果(prompt recognizer result)</em></td>
</tr>
<tr>
<td>选项(Options)</td>
<td>包含了在调用中提供的提示选项,以启动提示</td>
</tr>
</tbody>
</table>
<p>而提示识别器结果有这些属性:</p>
<table>
<thead>
<tr>
<th>属性</th>
<th>描述</th>
</tr>
</thead>
<tbody>
<tr>
<td>成功(Succeeded)</td>
<td>表示识别器是否能够解析输入的内容</td>
</tr>
<tr>
<td>值(Value)</td>
<td>识别器的返回值。如果必要,验证码可以修改此值</td>
</tr>
</tbody>
</table>
<h2 id="使用对话框"><a class="markdownIt-Anchor" href="#使用对话框"></a> 使用对话框</h2>
<p>位于堆栈最顶层的被视为活动中的对话框,对话框上下文会将所有的输入引向这个活动中的对话框。</p>
<ol>
<li>对话框开始</li>
<li>对话框被推入堆栈,成为活动中的对话框</li>
<li>对话框结束(被 <code>replace dialog</code> 方法移除)或者另一个对话框被推入堆栈并成为活动中的对话框</li>
</ol>
<center>-----------------------------</center>
<div class="danger"> 按照微软官方文档,我的 Python 版本为 3.8.3。
</div>
<h2 id="配置机器人"><a class="markdownIt-Anchor" href="#配置机器人"></a> 配置机器人</h2>
<p>在 Python 中,机器人的配置文件为 <code>config.py</code></p>
<p>配置格式:<code>XXX = os.environ.get(标识属性, 标识值)</code></p>
<figure class="highlight python"><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> os</span><br><span class="line"><span class="keyword">class</span> <span class="title class_">DefaultConfig</span>:</span><br><span class="line">    PORT = <span class="number">3978</span></span><br><span class="line">    APP_ID = os.environ.get(<span class="string">'MicrosoftAppId'</span>, <span class="string">''</span>)</span><br><span class="line">    APP_PASSWORD = os.environ.get(<span class="string">'MicrosoftAppPassword'</span>, <span class="string">''</span>)</span><br></pre></td></tr></tbody></table></figure>
<table>
<thead>
<tr>
<th>标识属性</th>
<th>标识值</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>MicrosoftAppType</code></td>
<td><code>MultiTenant</code>(多租户)</td>
</tr>
<tr>
<td><code>MicrosoftAppId</code></td>
<td>机器人的应用 ID</td>
</tr>
<tr>
<td><code>MicrosoftAppPassword</code></td>
<td>机器人的应用密码</td>
</tr>
<tr>
<td><code>MicrosoftAppTenantId</code></td>
<td>多租户可无视</td>
</tr>
</tbody>
</table>
</body></html></div></article></div></main><footer><div class="paginator"><a class="prev" href="40b3.html">上一篇</a><a class="next" href="891a.html">下一篇</a></div><!-- Webmention 显示区域--><div class="webmention-section webmention-empty" data-page-url="posts/60db.html" data-full-url="https://cytrogen.icu/posts/60db.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>