<!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>识别 & 解释用户的输入</li>
<li>对输入进行推理 & 执行相关任务</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>,提供事件驱动模型,其中传入的活动类型 & 子类型是 <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>格式化 & 发送响应活动</li>
<li><em>公开机器人连接器(Bot Connector)</em> REST API 提供的其他方法</li>
<li>捕获在轮次中不会被捕获到的错误 & 异常</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>,自动在基础存储层中读取 & 写入机器人的状态。状态以 <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>,用于实际读取 & 写入某个状态属性,提供了 <code>get</code>、<code>set</code> 和 <code>delete</code> 方法用于从轮次内部访问状态属性。</p>
<p>访问器创建需要用到属性名称。之后便可以使用访问器来获取和处理机器人状态的该属性。</p>
<p>访问器允许 SDK 从基础存储获取状态 & 更新机器人的状态缓存(机器人维护的本地缓存,用于存储状态对象和允许在不访问基础存储的情况下执行读取 & 写入操作)。</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> 活动时,如果已经在消息中添加 & 删除一个或多个回应,则调用处理程序。</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>定义一组对话框,这些对话框可以相互引用 & 协同工作。</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>